I'm working on a system that controls the finances of the company. One of the features is to schedule fixed income/expenses that happens every 15 days.
I already have a function that schedules incomes/expenses on a weekly basis, and I tried to copy that to make it work every 15 days. This is the code:
$registry_list = array();
foreach ($list as $item) {
/**
* Day of month 01-31 of the income/expense.
* This is the day the user created the first registered item.
*/
$firstDay = $item->day;
// How many days there is on the month
$daysOnMonth = cal_days_in_month(CAL_GREGORIAN, $params['month'], $params['year']);
// We check if the current month/year is greater than the first registry
// expire_date is always day 01. The day itself is on the property 'day'
if ($item->expire_date < $params['year'].$params['month'].'-01') {
$newDate = new \DateTime($item->expire_date);
$newYear = $newDate->format('Y');
$newMonth = $newDate->format('m');
$newDate = $newDate->setDate($newYear, $newMonth, $firstDay);
$newDate = $newDate->format('l');
$firstDay = strtotime("first ".$newDate." ".$params['year']."-".$params['month']);
$firstDay = date('d', $firstDay);
}
// First registry
$newItem = clone($item);
$newItem->expire_date = $params['year'].'-'.$params['month'].'-'.$firstDay;
$newItem = new Registry($newItem); // Just to format some data
array_push($registry_list, $newItem);
while ($firstDay < $daysOnMonth) {
$firstDay = $firstDay + 14; // 14 because the day itself count as +1
if ($firstDay <= $daysOnMonth) {
$newItem=clone($item);
$newItem->expire_date = $params['year'].'-'.$params['month'].'-'.$firstDay;
$newItem = new Registry($newItem);
array_push($registry_list, $newItem);
}
}
}
return $registry_list;
This code runs just fine when the month/year is the same as when the registry was created. But as it goes to the next month, sometimes it's not correct. For example, if I start it on 02/08/2022 (d/m/Y) the schedules for that month are:
02/08/2022
16/08/2022
30/08/2022
Which is correct. However, when September starts it messes with the list. Since I'm starting on the first weekday (based on the first registry) it's not always correct. So September list is as follow:
06/09/2022
20/09/2022
Which is incorrect. This should be the correct dates:
13/09/2022
27/09/2022
Because the last one from August was 30/08/2022.
I just don't know how to identify when I need to skip the first week of the month based on the last month. I also don't want to always start counting from the date the registry was created, otherwise I would go a long way to find the correct date when we pass a year or 2.
Since it's business financial control, they tend to look further on the calendar.
Is there a way to fix this issue?
This is the kind of problem that DateInterval can solve pretty easily:
/*
This function returns all dates between two dates at the given interval
Interval syntax: https://www.php.net/manual/en/dateinterval.construct.php
Date format syntax: https://www.php.net/manual/en/datetime.format.php
*/
function get_schedule_dates($start_date, $end_date, $interval, $format = 'Y-m-d')
{
$date = new DateTime($start_date);
$end = new DateTime($end_date);
$schedule = [$date->format($format)];
while ($date->add(new DateInterval($interval)) <= $end)
$schedule[] = $date->format($format);
return $schedule;
}
var_dump(get_schedule_dates('2022-08-02', '2022-10-03', 'P14D', 'd/m/Y'));
Yields
array(5) {
[0]=>
string(10) "02/08/2022"
[1]=>
string(10) "16/08/2022"
[2]=>
string(10) "30/08/2022"
[3]=>
string(10) "13/09/2022"
[4]=>
string(10) "27/09/2022"
}
What you could do is get the expiry_date of the first item as the start date, then calculate a date in advance, say 2 years, based on that, and use that as the end date.
You could then iterate through the scheduled dates and clone and create new Registry objects, pushing to $registry_list
Related
i would like to calculate the number of free days (absences) in a specific week. I use an API that returns the following data:
{
"count": 1,
"data": [
{
"id": "11ec62ff1df2654d8bd6f1d234a6c496",
"type": "HOLIDAY",
"from": "2021-12-22",
"to": "2021-12-23",
"resourceId": "11ec46d6547a00728be3e1ed8ff29535",
"createdAt": "2021-12-22T08:14:00"
}
],
"success": true
}
These are vacation and sickness data. I have a weekly report where I need to calculate the number of absence days during that week. I need to find an easy way to calculate the number of absence days during the week.
I have tried by using https://www.php.net/manual/de/datetime.format.php and convert it into "z" format, but it doesn't look elegant and from performance perspective i think it's not best.
//The week range
$weekStart = new DateTime("2021-12-20");
$weekEnd = new DateTime("2021-12-24");
//The Planned absence
$absenceStart = new DateTime("2021-12-22");
$absenceEnd = new DateTime("2021-12-23");
//Specify the DateInterval for calculating the period
$interval = DateInterval::createFromDateString('1 day');
//Need to add the interval to the end date in order to consider the end as well
$weekEnd->add($interval);
$absenceEnd->add($interval);
//Getting the 2 periods week and absence
$weekPeriod = new DatePeriod($weekStart, $interval, $weekEnd);
$absencePeriod = new DatePeriod($absenceStart, $interval, $absenceEnd);
$weekArray = array();
$absenceArray = array();
//put the day number format('z') into an array of the week
foreach ($weekPeriod as $i => $dt) {
$weekArray[$i] = $dt->format('z');
}
//put the day number format('z') into an array of the absence
foreach ($absencePeriod as $i => $dt) {
$absenceArray[$i] = $dt->format('z');
}
//get the intersection between both arrays
$ergebnis = array_intersect($weekArray, $absenceArray);
//calculate the number of entries
echo "The employee has <b>".count($ergebnis)."</b> free days in the week from 2021-12-20 until 2021-12-24";
This is returning the right information.
The employee has 2 free days in the week from 2021-12-20 until 2021-12-24
Can anyone please suggest if there is a better way or if i can tweak at least to make it more elegant and performat?
Thanks alot
Instead of looping over the week array, absence array, then week array again (array_intersect), you could eliminate looping through the absence array and the second week array loop if that information is not needed. So you only have 1 loop instead of 3 should be more performant.
//The week range
$weekStart = new DateTime("2021-12-20 00:00:00");
$weekEnd = new DateTime("2021-12-24 23:59:59");
//The Planned absence
$absenceStart = new DateTime("2021-12-22 00:00:00");
$absenceEnd = new DateTime("2021-12-23 23:59:59");
//Specify the DateInterval for calculating the period
$interval = DateInterval::createFromDateString('1 day');
//Getting the available period
$weekPeriod = new DatePeriod($weekStart, $interval, $weekEnd);
$availableDayCount = 0;
//put the day number format('z') into an array of the week
foreach ($weekPeriod as $dt) {
// Filter out days the employee is absent.
if($dt < $absenceStart || $dt > $absenceEnd) {
$availableDayCount += 1;
}
}
//calculate the number of entries
echo "The employee has <b>" . $availableDayCount . "</b> free days in the week from " . $weekStart->format('Y-m-d') . " until " . $weekEnd->format('Y-m-d');
Additionally, it didn't appear to be providing the correct information (maybe I am wrong?). Your question states it came to 2 days but I count 3:
2021-12-20
2021-12-21
2021-12-24
(The 22nd and 23rd being absent)
If that is incorrect, you can change $weekEnd to 2021-12-23 (I added start/end of day times so the entire day is counted).
I'm working on a website where the user can create some events every X days (where X is the name of a day in the week). Then, he needs to enter the number of events he wants to create in the future.
For example, the user selects every Monday and Tuesday and decides to create 150 events.
Here is the code I have made until now :
// Init the date counter
$cpt_date_found = 0;
// Number of date to find
$rec_occ = 150;
// Init an ending date far in the future
$endDate = strtotime('+10 years', time());
// Loop over the weeks
for($i = strtotime('Monday', strtotime(date("d.m.Y"))); $i <= $endDate; $i = strtotime('+1 week', $i)) {
// -- Monday date found, create the event in the database
$cpt_date_found++;
// Break the loop if we have enough dates found
if($cpt_date_found == $rec_occ) {
break;
}
}
This code finds the date of every Monday in the future and breaks the loop once we have reached the number of occurrences the user specified.
I have entered an ending date far in the future to make sure I can break the loop before the end of the occurrences count specified by the user.
First I'm not sure about the "quality" of my code... I know that breaking the loop is not the best idea and am wondering if another solution would better fit my needs.
Then, instead of repeating the loop more times if the user specified several days (let's say, Monday, Tuesday and Friday), is there a way to loop one time for every provided days?
Thanks!
The following code will loop over a period of 5 years. For each week in those 5 years it will generate a DatePeriod containing each day of that week. It will compare each of those days to your preset array with days you are looking for. You can then generate your event after which the code will countdown for a certain amount of times. If the counter hits zero, you are done.
$searchDates = array('Mon', 'Tue', 'Fri');
$amountOfTimes = 27;
$startDate = new DateTime();
$endDate = new DateTime('next monday');
$endDate->modify('+5 years');
$interval = new DateInterval('P1W');
$dateRange = new DatePeriod($startDate, $interval ,$endDate);
// Loop through the weeks
foreach ($dateRange as $weekStart) {
$weekEnd = clone $weekStart;
$weekEnd->modify('+6 days');
$subInterval = new DateInterval('P1D');
// Generate a DatePeriod for the current week
$subRange = new DatePeriod($weekStart, $subInterval ,$weekEnd);
foreach ($subRange as $weekday) {
if (in_array($weekday, array('Mon', 'Fri', 'Sun'))) {
// Create event
// Countdown
$amountOfTimes--;
}
if ($amountOfTimes == 0) {
break;
}
}
}
i am trying to get dates of current month or any other month from a date range.
Let Suppose I have a Date Range as Below.
$startDate = "2014-12-10";
$endDate = "2015-2-3";
I want to count only days/dates of current month "February"
which would result in 3 if start date and end date is above one.
but how can i make it work in a programming manner??
-=-=-=-=-==-=
update:
I think i could not explain my question,
Lets Take the same date range..
if i want to programmatically take out days of December Month
it would be like
21 days, as start date is 2014-12-10;
Dates Are in Range Coming Programmatically from database..
-=-==-=-=-=-=-=
UPDATE 2:
An other simple example
Lets Suppose If Leaves Have Been Approved For an Employee from 28-1-2015 to 6-2-2015
so Here Employees Leaves Taken Start Date Would Be
$sartDate = '28-1-2015';
$endDate = '6-2-2015';
So Total Leaves Employee would be taking is
$totalleaves = $endDate - $startDate //It is not right way to take out the differece only For sake of Example shown it
which would give me total leaves 9 or 10 days
But If We See, These Leaves Are Divided in Two Different Months.
And i want to generate a Report and i want to see how many leaves employee has taken for specific month which is lets suppose last month January
it would be 4 days i suppose for below dates as below dates comes in date range and they belong to January.
28-1-2015
29-1-2015
30-1-2015
31-1-2015
so if i would like to have a result of array of every month leaves it would
be like
array(
'January' => array(
'TotalLeavesTaken' => 4
),
'February' => array(
'TotalLeavesTaken' => 6
)
);
I think thats the best i could explain..
Adjusted after update in question
Ok, now I have adjusted after your last update. Hope it is what you're looking for:
function getLeavesInPeriod($start, $end) {
$date = new DateTime($start);
$endDate = new DateTime($end);
$leaves = array();
while($date <= $endDate ) {
$year = $date->format('Y');
$month = $date->format('M');
if(!array_key_exists($year, $leaves))
$leaves[$year] = array();
if(!array_key_exists($month, $leaves[$year]))
$leaves[$year][$month] = 0;
$leaves[$year][$month]++;
$date->modify("+1 day");
}
return $leaves;
}
$leaves = getLeavesInPeriod("2015-1-5", "2015-2-3");
print $leaves[2015]["Jan"]; //27
Not sure if understood your question correctly,
date('d', strtotime($endDate));
Lets try this
$month = strtotime(date('Y-m-01', time()));
$daysCount = (int)(time() - $month) / (24 * 3600);
It will help you: i m using this to get days
$date_diff = $end_date - $start_date;
//difference of two dates
This will return you number of days.Is just you want this or something else.Please confirm if it will help you.
$days_left = floor($date_diff / (60*60*24));// No. of days
From October 1st to March 31 the fee is $1 (season 1). From April 1st to September 30 the fee is $2 (season 2).
How can I calculate the total fee of a given date range (user input) depending on how many days of this date range fall into season 1 and season 2?
The following gives me the number of days of the userĀ“s date range, but I have no idea how to test against season 1 or season 2:
$user_input_start_date = getdate( $a );
$user_input_end_date = getdate( $b );
$start_date_new = mktime( 12, 0, 0, $user_input_start_date['mon'], $user_input_start_date['mday'], $user_input_start_date['year'] );
$end_date_new = mktime( 12, 0, 0, $user_input_end_date['mon'], $user_input_end_date['mday'], $user_input_end_date['year'] );
return round( abs( $start_date_new - $end_date_new ) / 86400 );
Given that a date range starts and ends in 2012 or starts in 2012 and ends in 2013 alone gives me 10 different possibilities of in which season a date range can start and where it can end.
There must be a better solution than iterating if/else and comparing dates over and over again for the following conditions:
Date range is completely within season 1
Date range starts in season 1 and ends in season 2
Date range starts in season 1, spans across season 2 and ends in the second part of season 1
... and so forth with "Starts in season 2", etc
This not a duplicate of How many days until X-Y-Z date? as that only deals with counting the number of days. It does not address the issue of comparing one date range with another.
The key to this problem is to simplify it as much as possible. I think using an array as a lookup table for the cost of each day of the year is the way to go. The first thing to do then, is to generate the array. The array just represents each day of the year and doesn't represent any particular year. I chose to use 2012 to generate the lookup array as it is a leap year and so has every possible day in it.
function getSeasonArray()
{
/**
* I have chosen 2012 as it was a leap year. All we want to do is
* generate an array which has avery day of the year in it.
*/
$startDate = new DateTime('1st January 2012');
//DatePeriod always drops the last day.
$endDate = new DateTime('1st January 2013');
$season2Start = new DateTime('1st April 2012');
$season2End = new DateTime('1st October 2012');
$allDays = new DatePeriod($startDate, new DateInterval('P1D'), $endDate);
$season2Days = new DatePeriod($season2Start, new DateInterval('P1D'), $season2End);
$seasonArray = array();
foreach($allDays as $day){
$seasonArray[] = $day->format('d-M');
$seasonArray[$day->format('d-M')]['season'] = 1;
}
foreach($season2Days as $day){
$seasonArray[$day->format('d-M')]['season'] = 2;
}
return $seasonArray;
}
Once that is done you just need the period over which to calculate:-
$bookingStartDate = new DateTime();//Or wherever you get this from
$bookingEndDate = new DateTime();
$bookingEndDate->setTimestamp(strtotime('+ 7 month'));//Or wherever you get this from
$bookingPeriod = new DatePeriod($bookingStartDate, new DateInterval('P1D'), $bookingEndDate);
Then we can do the calculation:-
$seasons = getSeasonArray();
$totalCost = 0;
foreach($bookingPeriod as $day){
$totalCost += $seasons[$day->format('d-M')]['season'];
var_dump($day->format('d-M') . ' = $' . $seasons[$day->format('d-M')]['season']);
}
var_dump($totalCost);
I have chosen a long booking period, so that you can scan through the var_dump() output and verify the correct price for each day of the year.
This is a quick stab done between distractions at work and I'm sure that with a bit of thought you can mould it into a more elegant solution. I'd like to get rid of the double iteration for example, unfortunately, work pressures prevent me from spending further time on this.
See the PHP DateTime man page for further information on these useful classes.
At first I suggested using the DateTime class that PHP provides, naively assuming that it has some kind of thought-out API that one could use. It turns out that it does not. While it features very basic DateTime functionality, it is mostly unusable because, for most operations, it relies on the DateInterval class. In combination, those classes represent another masterpiece of bad API design.
An interval should be defined like so:
An interval in Joda-Time represents an interval of time from one millisecond instant to another instant. Both instants are fully specified instants in the datetime continuum, complete with time zone.
In PHP, however, an Interval is just a duration:
A date interval stores either a fixed amount of time (in years, months, days, hours etc) or a relative time string [such as "2 days"].
Unfortunately, PHP's DateInterval definition does not allow for intersection/overlap calculation (which the OP needs) because PHP's Intervals have no specific position in the datetime continuum. Therefore, I've implemented a (very rudimentary) class that adheres to JodaTime's definition of an interval. It is not extensively tested, but it should get the work done:
class ProperDateInterval {
private $start = null;
private $end = null;
public function __construct(DateTime $start, DateTime $end) {
$this->start = $start;
$this->end = $end;
}
/**
* Does this time interval overlap the specified time interval.
*/
public function overlaps(ProperDateInterval $other) {
$start = $this->getStart()->getTimestamp();
$end = $this->getEnd()->getTimestamp();
$oStart = $other->getStart()->getTimestamp();
$oEnd = $other->getEnd()->getTimestamp();
return $start < $oEnd && $oStart < $end;
}
/**
* Gets the overlap between this interval and another interval.
*/
public function overlap(ProperDateInterval $other) {
if(!$this->overlaps($other)) {
// I haven't decided what should happen here yet.
// Returning "null" doesn't seem like a good solution.
// Maybe ProperDateInterval::EMPTY?
throw new Exception("No intersection.");
}
$start = $this->getStart()->getTimestamp();
$end = $this->getEnd()->getTimestamp();
$oStart = $other->getStart()->getTimestamp();
$oEnd = $other->getEnd()->getTimestamp();
$overlapStart = NULL;
$overlapEnd = NULL;
if($start === $oStart || $start > $oStart) {
$overlapStart = $this->getStart();
} else {
$overlapStart = $other->getStart();
}
if($end === $oEnd || $end < $oEnd) {
$overlapEnd = $this->getEnd();
} else {
$overlapEnd = $other->getEnd();
}
return new ProperDateInterval($overlapStart, $overlapEnd);
}
/**
* #return long The duration of this interval in seconds.
*/
public function getDuration() {
return $this->getEnd()->getTimestamp() - $this->getStart()->getTimestamp();
}
public function getStart() {
return $this->start;
}
public function getEnd() {
return $this->end;
}
}
It may be used like so:
$seasonStart = DateTime::createFromFormat('j-M-Y', '01-Apr-2012');
$seasonEnd = DateTime::createFromFormat('j-M-Y', '30-Sep-2012');
$userStart = DateTime::createFromFormat('j-M-Y', '01-Jan-2012');
$userEnd = DateTime::createFromFormat('j-M-Y', '02-Apr-2012');
$i1 = new ProperDateInterval($seasonStart, $seasonEnd);
$i2 = new ProperDateInterval($userStart, $userEnd);
$overlap = $i1->overlap($i2);
var_dump($overlap->getDuration());
I have used numorous date functions in the past, but I need help on a specific function...
Is there a function where I can assign two unknown dates and the system knows where I am within these two dates? Let me explain:
Our monthly salaries run from the 23rd of each month to the 22nd of the next month. Because earnings work on an hourly basis, my boss wants to know anywhere within the month where the accumulative salaries are. For instance the salary period started on the 23rd of September 2012 and we are now on the 29th, I want my query to be able to know where we are in the current salary period.
As you know, months move on, thus my script must automatically know in which period and where whithin that period we are now.
I can do the queries surrounding this, I just need to know the date function (s) to use to assign this array...
Any help will be appreciated - Thanx
You can use PHP's DateTime classes to do this quite easily:-
$periodStart = new DateTime('23rd September');
$now = new DateTime();
$interval = $now->diff($periodStart);
echo "We are {$interval->d} days into the payment period";
Output:
We are 6 days into the payment period.
I prefer to extend the DateTime class for this kind of thing, so everything is in the same place:-
class MyDateTime extends DateTime
{
public function elapsedDays(DateTime $since = null)
{
if ($since === null) {
$since = new DateTime();
}
$interval = $since->diff($this);
return (int) $interval->d;
}
}
$periodStart = new MyDateTime('23rd September');
echo "We are {$periodStart->elapsedDays()} days into the payment period";
Gives the same output.
You can then create periods and intervals and iterate over it to aggregate the sum like:
$datePeriodStart = new DateTime('23rd September');
$datePeriodEnd = clone $datePeriodStart;
$datePeriodEnd->add(new DateInterval('P1M'));
$dateToday = new DateTime();
$interval1 = $dateToday->diff($datePeriodStart);
$interval2 = $dateToday->diff($datePeriodEnd);
echo "We are {$interval1->d} day(s) into the payment period, {$interval2->d} day(s) left.\n";
$period = new DatePeriod($datePeriodStart, new DateInterval('P1D'), $dateToday);
$days = new IteratorIterator($period);
$totalSalary = 0;
$totalDays = 0;
foreach($days as $day)
{
$salary = get_salary_for_day($day);
$totalSalary += $salary;
$totalDays++;
printf("#%d: %s %s\n", $totalDays, $day->format('Y-m-d'), number_format($salary));
}
printf("Total Salary for %d day(s): %s\n", $totalDays, number_format($totalSalary));
Example output:
We are 6 day(s) into the payment period, 23 day(s) left.
#1: 2012-09-23 12,500
#2: 2012-09-24 12,500
#3: 2012-09-25 12,500
#4: 2012-09-26 12,500
#5: 2012-09-27 12,500
#6: 2012-09-28 12,500
#7: 2012-09-29 12,500
Total Salary for 7 day(s): 87,500
You could simply use a TIMESTAMP value (seconds since epoch, also called a "unix timestamp") and just test to see if the current date in unix timestamp is in between the first and last unix timestamp dates.
Essentially, that way you are merely converting the date into a big integer (number of seconds since 1969/70), and arithmetic and testing functions become a LOT easier to handle.
Get FROM and TO date:
$to = new DateTime();
$from = new DateTime($to->format('Y-m-23'));
if ($to->format('j') < 23) {
$from->modify('-1 month');
}
Var_dump:
var_dump($from->format('Y-m-d')); # 2012-09-23
var_dump($to->format('Y-m-d')); # 2012-09-29
SQL
$sql = "
SELECT ...
FROM ...
WHERE some_time BETWEEN '" . $from->format('Y-m-d') . "' AND '" . $to->format('Y-m-d') ."'
";