I have this function which checks the start, end and repeat date. So start and end dates serves as a date range and the repeatFollowup is the date which will be repeated every month within the time period. I am unable to debug the code as what is causing the delay or is it an infinite loop ?
Any help is appreciated. Thank you
public function calculateDaysOfMonth($startDate, $endDate, $repeatFollowup){
$begin = new \DateTime($startDate);
$end = new \DateTime($endDate);
$repeatDate = new \DateTime($begin->format('Y-m').'-'.date($repeatFollowup));
$days = array();
elseif ($repeatFollowup==30){
$newDate = Carbon::parse('first day of'.$repeatDate->format('Y-m-d'));
//var_dump($newDate->format('Y'));die();
while($repeatDate<=$end){
if($repeatDate->format('m')==2){
$days[] = $newDate->addDays(27);
$newDate->modify('first day of next month')
}
else{
$days[] = $newDate->addDays(29);
$newDate->modify('first day of next month')
}
}
var_dump($days);die();
return $days;
}
}
Yes you are in an infinite loop, because you are never adding to your repeatDate, so it will be always lower (if thats the case) than end, you should do something like:
while($repeatDate<=$end){
if($repeatDate->format('m')==2){
$days[] = $newDate->addDays(27);
$newDate->modify('first day of next month')
}
else{
$days[] = $newDate->addDays(29);
$newDate->modify('first day of next month')
}
$repeatDate->addDays(x);
}
Related
How can I count occurrences of 14th of a month between two dates
For example between 07.05.2018 and 04.07.2018
I have 2 occurrences of the 14th
Try this. Note that I've changed your date format, but you can just do a createFromFormat if you're really keen on your own format.
$startDate = new DateTime('2018-05-07');
$endDate = new DateTime('2018-07-04');
$dateInterval = new DateInterval('P1D');
$datePeriod = new DatePeriod($startDate, $dateInterval, $endDate);
$fourteenths = [];
foreach ($datePeriod as $dt) {
if ($dt->format('d') == '14') { // Note this is loosely checked!
$fourteenths[] = $dt->format('Y-m-d');
}
}
echo count($fourteenths) . PHP_EOL;
var_dump($fourteenths);
See it in action here: https://3v4l.org/vPZZ0
EDIT
This is probably not an optimal solution as you loop through every day in the date period and check whether it's the fourteenth. Probably easier is to modify the start date up to the next 14th and then check with an interval of P1M.
You don't need to loop at all.
Here's a solution that does not loop at all and uses the less memory and performance hungry date opposed to DateTime.
$start = "2018-05-07";
$end = "2018-07-04";
$times = 0;
// Check if first and last month in the range has a 14th.
if(date("d", strtotime($start)) <= 14) $times++;
if(date("d", strtotime($end)) >= 14) $times++;
// Create an array with the months between start and end
$months = range(strtotime($start . "+1 month"), strtotime($end . "-1 month"), 86400*30);
// Add the count of the months
$times += count($months);
echo $times; // 2
https://3v4l.org/RevLg
I'm solving following task>
I have two dates - $start and $end and target year as $year.
dates are php DateTime objects, year is string.
add:dates comes acutaly from MySql field from this format 2017-02-01 15:00:00 ...
add2: if end date is null, I use todays date ...
I need to figure out how many days are between these two dates for specific year.
Also I need to round it for whole days, even if one minute in day should be counted as whole day ...
I can solve it by many many following ifs.
Expected results for values I used in example are
2016 is 0 days
2017 is 31 days
2018 is 32 days
2019 is 0 days
But are there any elegant php functions which can help me with this ?
What I did it seems to be wrong way and giving bad results - seems it counts full days only ...
Please see my code here >
<?php
$diff = True;
$start = DateTime::createFromFormat('Y-m-d H:i:s','2017-12-01 23:05:00');
$end = DateTime::createFromFormat('Y-m-d H:i:s','2017-12-03 00:05:00');
$year = '2017';
// start date
if ($start->format('Y')<$year)
{
$newstart = new DateTime('first day of January '. $year);
}
if ($start->format('Y')==$year)
{
$newstart = $start;
}
if ($start->format('Y')>$year)
{
$result = 0;
$diff = False;
}
// end date
if ($end->format('Y')>$year)
{
$newend = new DateTime('last day of December '. $year);
}
if ($end->format('Y')==$year)
{
$newend = $end;
}
if ($end->format('Y')<$year)
{
$result = 0;
$diff = False;
}
// count if diff is applicable
if ($diff)
{
$result = $newend->diff($newstart)->format("%a");
}
echo $result;
?>
But are there any elegant php functions which can help me with this ?
Read about DateTime::diff(). It returns a DateInterval object that contains the number of days (in $days) and by inspecting the values of $h, $i and $s you can tell if you have to increment it to round the result. You can also use min() and max() to crop the time interval to the desired year.
function getDays(DateTimeInterface $start, DateTimeInterface $end, $year)
{
// Extend the start date and end date to include the entire day
$s = clone $start; // Don't modify $start and $end, use duplicates
$s->setTime(0, 0, 0);
$e = clone $end;
$e->setTime(0, 0, 0)->add(new DateInterval('P1D')); // start of the next day
// Crop the input interval to the desired year
$s = min($s, new DateTime("$year-01-01 00:00:00"));
$year ++;
$e = max(new DateTime("$year-01-01 00:00:00"), $end); // start of the next year
if ($e <= $s) {
// The input interval does not span across the desired year
return 0;
}
// Compute the difference and return the number of days
$diff = $e->diff($s);
return $diff->days;
}
$d1 = strtotime('2017-05-15');
$d2 = strtotime('2017-05-31');
$div = 24 * 3600;
echo abs(($d2 - $d1) / $div); // 16 days
Just make sure and ONLY have the date part and you shouldn't have to deal with rounding.
I have a function to return the difference between 2 dates, however I need to work out the difference in working hours, assuming Monday to Friday (9am to 5:30pm):
//DATE DIFF FUNCTION
// Set timezone
date_default_timezone_set("GMT");
// Time format is UNIX timestamp or
// PHP strtotime compatible strings
function dateDiff($time1, $time2, $precision = 6) {
// If not numeric then convert texts to unix timestamps
if (!is_int($time1)) {
$time1 = strtotime($time1);
}
if (!is_int($time2)) {
$time2 = strtotime($time2);
}
// If time1 is bigger than time2
// Then swap time1 and time2
if ($time1 > $time2) {
$ttime = $time1;
$time1 = $time2;
$time2 = $ttime;
}
// Set up intervals and diffs arrays
$intervals = array('year','month','day','hour','minute','second');
$diffs = array();
// Loop thru all intervals
foreach ($intervals as $interval) {
// Set default diff to 0
$diffs[$interval] = 0;
// Create temp time from time1 and interval
$ttime = strtotime("+1 " . $interval, $time1);
// Loop until temp time is smaller than time2
while ($time2 >= $ttime) {
$time1 = $ttime;
$diffs[$interval]++;
// Create new temp time from time1 and interval
$ttime = strtotime("+1 " . $interval, $time1);
}
}
$count = 0;
$times = array();
// Loop thru all diffs
foreach ($diffs as $interval => $value) {
// Break if we have needed precission
if ($count >= $precision) {
break;
}
// Add value and interval
// if value is bigger than 0
if ($value > 0) {
// Add s if value is not 1
if ($value != 1) {
$interval .= "s";
}
// Add value and interval to times array
$times[] = $value . " " . $interval;
$count++;
}
}
// Return string with times
return implode(", ", $times);
}
Date 1 = 2012-03-24 03:58:58
Date 2 = 2012-03-22 11:29:16
Is there a simple way of doing this, i.e - calculating the percentage of working hours in a week and dividing the difference using the above function - I have played around with this idea and got some very strange figures...
Or is there better way....?
This example uses PHP's built in DateTime classes to do the date math. How I approached this was to start by counting the number of full working days between the two dates and then multiply that by 8 (see notes). Then it gets the hours worked on the partial days and adds them to the total hours worked. Turning this into a function would be fairly straightforward to do.
Notes:
Does not take timestamps into account. But you already know how to do that.
Does not handle holidays. (That can be easily added by using an array of holidays and adding it to where you filter out Saturdays and Sundays).
Requires PHP 5.3.6+
Assumes an 8 hour workday. If employees do not take lunch change $hours = $days * 8; to $hours = $days * 8.5;
.
<?php
// Initial datetimes
$date1 = new DateTime('2012-03-22 11:29:16');
$date2 = new DateTime('2012-03-24 03:58:58');
// Set first datetime to midnight of next day
$start = clone $date1;
$start->modify('+1 day');
$start->modify('midnight');
// Set second datetime to midnight of that day
$end = clone $date2;
$end->modify('midnight');
// Count the number of full days between both dates
$days = 0;
// Loop through each day between two dates
$interval = new DateInterval('P1D');
$period = new DatePeriod($start, $interval, $end);
foreach ($period as $dt) {
// If it is a weekend don't count it
if (!in_array($dt->format('l'), array('Saturday', 'Sunday'))) {
$days++;
}
}
// Assume 8 hour workdays
$hours = $days * 8;
// Get the number of hours worked on the first day
$date1->modify('5:30 PM');
$diff = $date1->diff($start);
$hours += $diff->h;
// Get the number of hours worked the second day
$date1->modify('8 AM');
$diff = $date2->diff($end);
$hours += $diff->h;
echo $hours;
See it in action
Reference
DateTime Class
DatePeriod Class
DateInterval Class
Here's what I've come up with.
My solution checks the start and end times of the original dates, and adjusts them according to the actual start and end times of the work day (if the original start time is before work's opening time, it sets it to the latter).
After this is done to both start and end times, the times are compared to retrieve a DateInterval diff, calculating the total days, hours, etc. The date range is then checked for any weekend days, and if found, one total day is reduced from the diff.
Finally, the hours are calculated as commented. :)
Cheers to John for inspiring some of this solution, particularly the DatePeriod to check for weekends.
Gold star to anyone who breaks this; I'll be happy to update if anyone finds a loophole!
Gold star to myself, I broke it! Yeah, weekends are still buggy (try starting at 4pm on Saturday and ending at 1pm Monday). I will conquer you, work hours problem!
Ninja edit #2: I think I took care of the weekend bugs by reverting the start and end times to the most recent respective weekday if they fall on a weekend. Got good results after testing a handful of date ranges (starting and ending on the same weekend barfs, as expected). I'm not entirely convinced this is as optimized / simple as it could be, but at least it works better now.
// Settings
$workStartHour = 9;
$workStartMin = 0;
$workEndHour = 17;
$workEndMin = 30;
$workdayHours = 8.5;
$weekends = ['Saturday', 'Sunday'];
$hours = 0;
// Original start and end times, and their clones that we'll modify.
$originalStart = new DateTime('2012-03-22 11:29:16');
$start = clone $originalStart;
// Starting on a weekend? Skip to a weekday.
while (in_array($start->format('l'), $weekends))
{
$start->modify('midnight tomorrow');
}
$originalEnd = new DateTime('2012-03-24 03:58:58');
$end = clone $originalEnd;
// Ending on a weekend? Go back to a weekday.
while (in_array($end->format('l'), $weekends))
{
$end->modify('-1 day')->setTime(23, 59);
}
// Is the start date after the end date? Might happen if start and end
// are on the same weekend (whoops).
if ($start > $end) throw new Exception('Start date is AFTER end date!');
// Are the times outside of normal work hours? If so, adjust.
$startAdj = clone $start;
if ($start < $startAdj->setTime($workStartHour, $workStartMin))
{
// Start is earlier; adjust to real start time.
$start = $startAdj;
}
else if ($start > $startAdj->setTime($workEndHour, $workEndMin))
{
// Start is after close of that day, move to tomorrow.
$start = $startAdj->setTime($workStartHour, $workStartMin)->modify('+1 day');
}
$endAdj = clone $end;
if ($end > $endAdj->setTime($workEndHour, $workEndMin))
{
// End is after; adjust to real end time.
$end = $endAdj;
}
else if ($end < $endAdj->setTime($workStartHour, $workStartMin))
{
// End is before start of that day, move to day before.
$end = $endAdj->setTime($workEndHour, $workEndMin)->modify('-1 day');
}
// Calculate the difference between our modified days.
$diff = $start->diff($end);
// Go through each day using the original values, so we can check for weekends.
$period = new DatePeriod($start, new DateInterval('P1D'), $end);
foreach ($period as $day)
{
// If it's a weekend day, take it out of our total days in the diff.
if (in_array($day->format('l'), ['Saturday', 'Sunday'])) $diff->d--;
}
// Calculate! Days * Hours in a day + hours + minutes converted to hours.
$hours = ($diff->d * $workdayHours) + $diff->h + round($diff->i / 60, 2);
As the old saying goes "if you want something done right do it yourself". Not saying this is optimal but its atleast returning the correct amount of hours for me.
function biss_hours($start, $end){
$startDate = new DateTime($start);
$endDate = new DateTime($end);
$periodInterval = new DateInterval( "PT1H" );
$period = new DatePeriod( $startDate, $periodInterval, $endDate );
$count = 0;
foreach($period as $date){
$startofday = clone $date;
$startofday->setTime(8,30);
$endofday = clone $date;
$endofday->setTime(17,30);
if($date > $startofday && $date <= $endofday && !in_array($date->format('l'), array('Sunday','Saturday'))){
$count++;
}
}
//Get seconds of Start time
$start_d = date("Y-m-d H:00:00", strtotime($start));
$start_d_seconds = strtotime($start_d);
$start_t_seconds = strtotime($start);
$start_seconds = $start_t_seconds - $start_d_seconds;
//Get seconds of End time
$end_d = date("Y-m-d H:00:00", strtotime($end));
$end_d_seconds = strtotime($end_d);
$end_t_seconds = strtotime($end);
$end_seconds = $end_t_seconds - $end_d_seconds;
$diff = $end_seconds-$start_seconds;
if($diff!=0):
$count--;
endif;
$total_min_sec = date('i:s',$diff);
return $count .":".$total_min_sec;
}
$start = '2014-06-23 12:30:00';
$end = '2014-06-27 15:45:00';
$go = biss_hours($start,$end);
echo $go;
I'm looking to divide a span between two timestamps into weeks, in PHP.
Essentially, what I want to do is:
function divide_into_weeks($start_time, $end_time) {
// Iterate back from the end time,
// creating an array of timestamps broken into calendar weeks.
$done = FALSE;
$divider = $end_time;
for ($i = 0; !$done; $i++) {
$timeslots[$i]->end = $divider;
// Set the start time to the start of the current week.
$timeslots[$i]->start = strtotime("monday this week", $divider);
if ($timeslots[$i]->start <= $start_time) {$done = TRUE;}
// If we loop again, end the previous week one second before the start of this week.
$divider = $timeslots[$i]->start - 1;
}
}
However, the function hits an infinite loop.
Why? Because...
strtotime("monday this week", $monday_of_next_week -1) == $monday_of_last week;
... is true, strangely. This is unlike other strtotime operations I've tested. In other cases, you knock a second off, repeat, and it iterate back by one of whatever unit you've asked for. But not for Monday (or Sunday) of this week.
For example:
$today = strtotime("midnight");
$yesterday = strtotime("midnight", $today -1);
...produces sensible results.
But my attempts to use the same technique with "monday of this week" or "sunday of this week" have proven fruitless, so far.
So, can someone show how to iterate timestamps back by weeks?
I think this would help you:
function divide_into_weeks($start_time, $end_time, $tz) {
$tz = new DateTimezone($tz);
$start = new DateTime("#$start_time");
$end = new DateTime("#$end_time");
$start ->setTimezone($tz)->modify($start->format('o-\WW-1 00:00:00'));
$end ->setTimezone($tz)->modify($end ->format('o-\WW-7'));
$weeks = [];
do {
$weeks[] = [
'start' => clone $start,
'end' => new DateTime($start->format('o-\WW-7 23:59:59'), $tz),
];
$start->modify('+7 day');
} while ($start < $end);
return $weeks;
}
demo
This ended up being the workaround that did the trick:
(With some of the irrelevant bits trimmed.)
function divide_timespan_into_units($start_second, $end_second, $time_unit) {
$timeslots = array();
// The time_formatter guides strtotime in what parts of a timestamp to trim off to leave timestamps split on calendar divisions.
switch ($time_unit) {
case "hour":
$time_formatter = 'Y-m-d H:00:00';
break;
case "day":
$time_formatter = 'Y-m-d 00:00:00';
break;
case "week":
$time_formatter = "monday this week";
break;
}
$done = FALSE;
$divider = $end_second;
for ($i = 0; !$done; $i++) {
$timeslots[$i] = (object) array('end' => $divider);
if ($time_unit == "week") {
// Dividing timestamps up into calendar weeks requires special handling.
//
// Note on the strange constants "86399" & "86400" in the two lines following:
// This is a workaround for a fluke in how strtotime treats weeks:
//
// strtotime can't decide if Sunday or Monday is the start of the week.
// You have to force it by rolling back a full day plus one second from the start of Monday, to get into the previous week.
//
// Simply rolling back a second from Sunday by 1 second doesn't get you into the previous week,
// because if you ask for "this Sunday" on any day other than Sunday, it will return the coming Sunday, not the past Sunday.
// However, if you back up 1 second from the stroke of midnight Monday, and ask what week you're in,
// you'll be told that you're still in the same week.
//
// This nudge of an extra 86400 seconds (a full extra day) back and forth forces the week to click over.
//
// This will likely be settled in a later version of PHP, but as of 5.3.5 (when this is being coded) it's an issue.
$timeslots[$i]->start = strtotime("monday this week", $divider-86400);
$divider = $timeslots[$i]->start+86399;
} else {
$timeslots[$i]->start = strtotime(date($time_formatter, $divider));
}
if ($timeslots[$i]->start <= $start_second) {$done = TRUE;}
$divider = $timeslots[$i]->start - 1;
}
}
I want to calculate all Sunday's between given two dates. I tried following code. It works fine if days are less but if i enter more days. It keeps processing and Maximum execution time exceeds i changed the time but it even keeps processing even execution time is 200sec.
code is
<?php
$one="2013-01-01";
$two="2013-02-30";
$no=0;
for($i=$one;$i<=$two;$i++)
{
$day=date("N",strtotime($i));
if($day==7)
{
$no++;
}
}
echo $no;
?>
please help.
John Conde's answer is correct, but here is a more efficient and mathy solution:
$start = new DateTime('2013-01-06');
$end = new DateTime('2013-01-20');
$days = $start->diff($end, true)->days;
$sundays = intval($days / 7) + ($start->format('N') + $days % 7 >= 7);
echo $sundays;
Let me break it down for you.
$start = new DateTime('2013-01-06');
$end = new DateTime('2013-01-20');
First, create some DateTime objects, which are powerful built-in PHP objects meant for exactly this kind of problem.
$days = $start->diff($end, true)->days;
Next, use DateTime::diff to find the difference from $start to $end (passing true here as the second parameter ensures that this value is always positive), and get the number of days between them.
$sundays = intval($days / 7) + ($start->format('N') + $days % 7 >= 7);
Here comes the big one - but it's not so complicated, really. First, we know there is one Sunday for every week, so we have at least $days / 7 Sundays to begin with, rounded down to the nearest int with intval.
On top of that, there could be a Sunday in a span of time less than a week; for example, Friday to Monday of the next week contains 4 days; one of them is a Sunday. So, depending on when we start and end, there could be another. This is easy to account for:
$start->format('N') (see DateTime::format) gives us the ISO-8601 day of the week for the start date, which is a number from 1 to 7 (1 is Monday, 7 is Sunday).
$days % 7 gives us the number of leftover days that don't divide evenly into weeks.
If our starting day and the number of leftover days add up to 7 or more, then we reached a Sunday. Knowing that, we just have to add that expression, which will give us 1 if it's true or 0 if it's false, since we're adding it to an int value.
And there you have it! The advantage of this method is that it doesn't require iterating over every day between the given times and checking to see if it's a Sunday, which will save you a lot computation, and also it will make you look really clever. Hope that helps!
<?php
$no = 0;
$start = new DateTime('2013-01-01');
$end = new DateTime('2013-04-30');
$interval = DateInterval::createFromDateString('1 day');
$period = new DatePeriod($start, $interval, $end);
foreach ($period as $dt)
{
if ($dt->format('N') == 7)
{
$no++;
}
}
echo $no;
See it in action
Here is a solution if you want the Sundays in a specific date range.
function dateRange($begin, $end, $interval = null)
{
$begin = new DateTime($begin);
$end = new DateTime($end);
$end = $end->modify('+1 day');
$interval = new DateInterval($interval ? $interval : 'P1D');
return iterator_to_array(new DatePeriod($begin, $interval, $end));
}
/* define date range */
$dates = dateRange('2018-03-01', '2018-03-31');
/* define weekdays */
$weekends = array_filter($dates, function ($date) {
$day = $date->format("N");
return $day === '6' || $day === '7';
});
/* weekdays output */
foreach ($weekends as $date) {
echo $date->format("D Y-m-d") . "</br>";
}
/* define sundays */
$sundays = array_filter($dates, function ($date) {
return $date->format("N") === '7';
});
/* sundays output */
foreach ($sundays as $date) {
echo $date->format("D Y-m-d") . "</br>";
}
/* define mondays */
$mondays = array_filter($dates, function ($date) {
return $date->format("N") === '1';
});
/* mondays output */
foreach ($mondays as $date) {
echo $date->format("D Y-m-d") . "</br>";
}
Just change the number for any days you want in your output:
Monday = 1
Tuesday = 2
Wednesday = 3
Thursday = 4
Friday = 5
Saturday = 6
Sunday = 7