I am trying make the interval dynamic based on the date range.
Consider this dataset:
$data = [
"2016-06-01": 2,
"2016-06-01": 2,
"2016-06-02": 2,
"2016-06-02": 2,
"2016-06-03": 2,
"2016-06-03": 2,
"2016-06-04": 2,
"2016-06-04": 2,
...
];
I want to sum the values based on date range intervals.
So for eg:
$start_date = "2016-06-01";
$end_date = "2016-07-01";
// some function that returns interval dates based on range
$interval_dates = [
"2016-06-01",
"2016-06-05",
"2016-06-10,
...
];
Based on that I iterate through the $data array and get the sums for intervals.
The caveat is however that the interval dates should be based on the date range:
for eg:
if $end_date was next year, the interval dates would look like this:
$interval_dates = [
"2016-06-01",
"2016-08-01",
"2016-10-01"
];
So the interval is based on the difference between start and end date. The bigger the difference the bigger the intervals.
So far:
public function getIntervalDates($start_date, $end_date, $interval = 5)
{
$start_date = new \DateTime($start_date);
$end_date = new \DateTime($end_date);
// get the difference in days
$diff_object = $start_date->diff($end_date);
$diff_days = $diff_object->days;
// set the interval days based on difference in days
// and the interval
$interval_days = round(intval($diff_days) / $interval);
// set the first date to given start date
$interval_dates[] = $start_date->format("Y-m-d");
$current_date = $start_date;
for($i=0; $i < $interval; $i++) {
// set interval dates by incrementing
// by interval days
$current_date->add(new \DateInterval("P".$interval_days."D"));
// Dont go beyond end date
if($current_date < $end_date) {
array_push($interval_dates, $current_date->format("Y-m-d"));
}
}
// add end date
$interval_dates[] = $end_date->format("Y-m-d");
return $interval_dates;
}
This solution kinda works but is not reliable.
for eg: if choosing a big range eg: 2016-06-10 TO 2020-06-10, it only returns 2 dates.
I think there must be a mathematical formula that was made to solve precisely this kind of a problem.
Related
I have a job that runs every 28 days. and I want to assign it a cycle number based on a starting reference date.
e.g
1st cycle is 01/27/22. and that cycle number would be 2201.
subsequently I want to calculate the cycle number based on the current date. but for each year there could be either 13 or 14 cycles.
I've managed to figure out the number of cycles since the reference date to figure out the latest cycle date (see below)
const REF_ZERO_DATE = '01/27/2022';
const REF_ZERO_CYCLE_YEAR = "22";
const REF_ZERO_CYCLE_NUM = "01";
$today = new \DateTime("2023/12/29");
echo ("Today = ".$today->format("Y/m/d")."\n");
$ref_zero = new \DateTime(self::REF_ZERO_DATE);
echo ("ref_zero = ".$ref_zero->format("Y/m/d")."\n");
$number_of_days_since_ref_zero = $today->diff($ref_zero)->format("%a");
echo ("Number of days since ref zero = ".$number_of_days_since_ref_zero."\n");
$number_of_cycles_since_ref_zero = floor($number_of_days_since_ref_zero/28);
echo ("Number of cycles since ref zero = ".$number_of_cycles_since_ref_zero."\n");
$interval = 'P' . $number_of_cycles_since_ref_zero*28 . 'D';
echo ("Interval = ".$interval);
$date_of_lastest_cycle = date_add($ref_zero,new \DateInterval($interval));
echo ("last Cycle Date = ".$date_of_lastest_cycle->format("Y/m/d")."\n");
But my math for the cycle adjustment is missing coping with 12 or 13 cycle in a specific year.
It is not explicitly stated whether the cycle of the previous year continues into the next or not.
The scenario in which the cycles can overlap between years is more complicated, so this is assumed.
The interval count code was extracted to the following function:
function calculateIntervalCount($startDate, $endDate, $interval) {
$start = new \DateTime($startDate);
$end = new \DateTime($endDate);
$interval = new \DateInterval($interval);
$periodDays = intval($end->diff($start)->format('%a'));
$intervalDays = intval($interval->format('%d'));
return floor($periodDays / $intervalDays);
}
There are two cases when calculating the interval count of a particular year:
year of start and end are the same year
year of end is after year of start
In the first case the interval count is the same as the interval count of the whole period.
In the second case the interval count of a particular year can be calculated from the difference between the interval counts of the whole period and the period before the end year.
The following function returns the cycle number:
function calculateCycleNumber($startDate, $endDate, $interval) {
$totalCycles = calculateIntervalCount($startDate,$endDate,$interval);
$startYear = intval((new \DateTime($startDate))->format('Y'));
$endYear = intval((new \DateTime($endDate))->format('Y'));
if($startYear < $endYear) {
$endOfLastYearDate = (new \DateTime($endDate))->modify('last day of December last year')->format('Y-m-d');
$cyclesSinceEndOfLastYear = calculateIntervalCount($endOfLastYearDate, $endDate, $interval);
$yearCycle = $totalCycles - $cyclesSinceEndOfLastYear + 1;
} else {
$yearCycle = $totalCycles;
}
$yearCode = substr($endYear,-2);
$yearCycleCode = sprintf('%02d', $yearCycle);
return $yearCode . $yearCycleCode;
}
A cycle number of 2314 was obtained with the inputs provided.
echo calculateCycleNumber('01/27/2022','2023/12/29','P28D');
Note that 14 is possible in case of overlapping cycles.
You can use timestamp, where you add 28 days each time so you get the next date and so on.
Get the next timestamp
$next_date = strtotime('+28 day', $timestamp);
Convert to readable date
echo date('m/d/Y', $next_date);
I'm trying to write a function to calculate the next date that a piece of equipment needs to be checked. I'm using the code below (it's incomplete.)
function get_next_check(){
$today = date(get_option('date_format'));
$first_check = types_get_field_meta_value( 'first_check', $post_id );
// Interval is a number of weeks- ie. month = 4, year = 52
$interval = types_get_field_meta_value( 'interval', $post_id );
// Calculate the next date after today that the check needs to be performed
$next_check = ;
return $next_check;
}
add_shortcode( 'next_check', 'get_next_check' );
I'm guessing I need to create an array of all possible dates, and compare each to today's date, only returning the next one?
Assuming that $first_check is a string with the date in it, and you want the function to return the same, something like this might work:
$week = 60 * 60 * 24 * 7; // number of seconds in a week
$next_check = strtotime($first_check);
while ($next_check < time()) {
$next_check += $week * $interval; // advance the check time by the desired interval
}
return date(get_option('date_format'), $next_check);
You can eliminate your initialization of the $today variable with this.
You can achieve this on the proposed by Greg Schmidt , or alternatively by doing:
// Calculate the next date after today that the check needs to be performed
$next_check = date(get_option('date_format'), strtotime($today." ".$interval." weeks"));
I have comma separated days(1,3,5,6) and i want to count number of days between two days.
I have done this as below.
$days=$model->days; // comma saperated days
$days=explode(',',$days); // converting days into array
$count=0;
$start_date = $model->activity_start_date; // i.e. 2018-03-27
$end_date = date('Y-m-d');
while(strtotime($start_date) <= strtotime($end_date)){
if(in_array(date("N",strtotime($start_date)),$days)){ // check for day no
$count++;
}
$start_date = date ("Y-m-d", strtotime("+1 day", strtotime($start_date)));
}
This code is working fine. But the problem is if difference between two date will be year or more than a year than it loop 365 times or more.
Is there any way to reduce execution time to count days. Or it is possible to get counts of days using mysql query.
Real Scenario : I have one event which have start date and it occurs multiple in week(i.e. Monday and Wednesday ) and i want to find how many times event occur from start date to now. If start date is 2018-04-9 and today is 2018-05-9 than count will be 9
This requires no looping.
I find how many weeks between start and end and multiply full weeks (in your example 16,17,18) with count of days.
Then I find how many days there is in first week (15) that is higher than or equal to daynumber of startday.
Then the opposit for week 19.
$days="1,3"; // comma saperated days
$days=explode(',',$days); // converting days into array
$count=0;
$start_date = "2018-04-09";
$end_date = "2018-05-09";
// calculate number of weeks between start and end date
$yearW = floor(((strtotime($end_date) - strtotime($start_date)) / 86400)/7);
if($yearW >0) $count = ($yearW -1)*count($days); //full weeks multiplied with count of event days
$startday = date("N", strtotime($start_date));
$endday = date("N", strtotime($end_date));
//find numer of event days in first week (less than or equal to $startday)
$count += count(array_filter($days,function ($value) use($startday) {return ($value >= $startday);}));
//find numer of event days in last week (less than $endday)
$count += count(array_filter($days,function ($value) use($endday) {return ($value < $endday);}));
echo $count; // 9
https://3v4l.org/9kupt
Added $yearW to hold year weeks
Here, I have a code I have used previously in a project. It is used to find the distance between date from today.
public function diffFromToday($date){
$today = date('jS F, Y');
$today = str_replace(',','',$today);
$today=date_create($today);
$today = date_format($today,"Y-m-j");
$date = str_replace(',','',$date);
$date=date_create($date);
$date = date_format($date,"Y-m-j");
$today=date_create($today);
$date=date_create($date);
$diff=date_diff($today,$date);
$result = $diff->format("%R%a");
$result = str_replace('+','',$result);
return $result;
}
You can use this function according to your requirement. Date format used in this function is 'jS F, Y' DO not be confused and use your own format to convert.
$start_date = date_create('2018-03-27');
$end_date = date_create('2018-04-27');
$t = ceil(abs($endDate- $start_date) / 86400);
echo date_diff($start_date, $end_date)->format('%a');
Output with like this
396
Could you not use the date->diff function to get the values you can directly use like -5 or 5?
$diffInDays = (int)$dDiff->format("%r%a");
Here is something for format parameter descriptions if it could be helpful to you.
You may use tplaner/when library for this:
$days = str_replace(
['1', '2', '3', '4', '5', '6', '0', '7'],
['mo', 'tu', 'sa', 'th', 'fr', 'sa', 'su', 'su'],
'1,3'
);
$when = new When();
$when->rangeLimit = 1000;
$occurrences = $when->startDate(new DateTime('2018-04-9'))
->freq('daily')
->byday('mo,we')
->getOccurrencesBetween(new DateTime('2018-04-9'), new DateTime('2018-05-9'));
echo count($occurrences);
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());
How do I go about getting all the work days (mon-fri) in a given time period (let's say, today till the end of the next month) ?
If you're using PHP 5.2+ you can use the library I wrote in order to handle date recursion in PHP called When.
With the library, the code would be something like:
$r = new When();
$r->recur(<start date here>, 'weekly')
->until(<end date here>)
->wkst('SU')
->byday(array('MO', 'TU', 'WE', 'TH', 'FR'));
while($result = $r->next())
{
echo $result->format('c') . '<br />';
}
This sample does exactly what you need, in an quick and efficient way.
It doesn't do nested loops and uses the totally awesome DateTime object.
$oDateTime = new DateTime();
$oDayIncrease = new DateInterval("P1D");
$aWeekDays = array();
$sStart = $oDateTime->format("m-Y");
while($oDateTime->format("m-Y") == $sStart) {
$iDayInWeek = $oDateTime->format("w");
if ($iDayInWeek > 0 && $iDayInWeek < 6) {
$aWeekDays[] = clone $oDateTime;
}
$oDateTime->add($oDayIncrease);
}
Try it here: http://codepad.org/wuAyAqnF
To use it, simply pass a timestamp to get_weekdays. You'll get back an array of all the weekdays, as timestamps, for the rest of the current month. Optionally, you can pass a $to argument - you will get all weekdays between $from and $to.
function get_weekdays ($from, $to=false) {
if ($to == false)
$to = last_day_of_month($from);
$days = array();
for ($x = $from; $x < $to; $x+=86400 ) {
if (date('w', $x) > 0 && date('w', $x) < 6)
$days[] = $x;
}
return $days;
}
function last_day_of_month($ts=false) {
$m = date('m', $ts);
$y = date('y', $ts);
return mktime(23, 59, 59, ($m+1), 0, $y);
}
I suppose you could loop through the dates and check the day for each one, and increment a counter.
Can't think of anything else off the top of my head.
Pseudocode coming your way:
Calculate the number of days between now and the last day of the month
Get the current day of the week (i.e. Wednesday)
Based on the current day of the week, and the number of days left in the month, it's simple calculation to figure out how many weekend days are left in the month - it's going to be the number of days remaining in the month, minus the number of Sundays/Saturdays left in the month.
I would write a function, something like:
daysLeftInMonth(daysLeftInMonth, startingDayOfWeek, dayOfWeekToCalculate)
where:
daysLeftInMonth is last day of the month (30), minus the current date (15)
startingDayOfWeek is the day of the week you want to start on (for today it would be Wednesday)
dayOfWeekToCalculate is the day of the week you want to count, e.g. Saturday or Sunday. June 2011 currently has 2 Sunday, and 2 Saturdays left 'til the end of the month
So, your algorithm becomes something like:
getWeekdaysLeft(todaysDate)
...getWeekdaysLeft is something like:
sundaysLeft = daysLeftInMonth(lastDayOfMonth - todaysDate, "Wednesday", "Sunday");
saturdaysLeft = daysLeftInMonth(lastDayOfMonth - todaysDate, "Wednesday", "Saturday");
return ((lastDayOfMonth - todaysDate) - (sundaysLeft + saturdaysLeft));
This code does at least one part you ask for. Instead of "end of next month" it simply works with a given number of days.
$dfrom = time();
$fourweeks = 7 * 4;
for ($i = 0; $i < $fourweeks; $i ++) {
$stamp = $dfrom + ($i * 24 * 60 * 60);
$weekday = date("D", $stamp);
if (in_array($weekday, array("Mon", "Tue", "Wed", "Thu", "Fri"))) {
print date(DATE_RSS, $stamp) . "\n";
}
}
// Find today's day of the month (i.e. 15)
$today = intval(date('d'));
// Define the array that will hold the work days.
$work_days = array()
// Find this month's last day. (i.e. 30)
$last = intval(date('d', strtotime('last day of this month')));
// Loop through all of the days between today and the last day of the month (i.e. 15 through 30)
for ( $i = $today; $i <= $last; $i++ )
{
// Create a timestamp.
$timestamp = mktime(null, null, null, null, $i);
// If the day of the week is greater than Sunday (0) but less than Saturday (6), add the timestamp to an array.
if ( intval(date('w', $timestamp)) > 0 && intval(date('w', $timestamp)) < 6 )
$work_days[] = mktime($timestamp);
}
The $work_days array will contain timestamps which you could use this way:
echo date('Y-m-d', $work_days[0]);
The code above with work in PHP 4 as well as PHP 5. It does not rely on the functionality of the DateTime class which was not available until PHP 5.2 and does not require the use of "libraries" created by other people.