PHP Carbon date difference with only working days - php

I'm trying to get difference between two dates in human format but only with working days. Here is my actual code:
$start = '2018-09-13 09:30:00';
$end = '2018-10-16 16:30:00';
$from = Carbon::parse($start);
$to = Carbon::parse($end);
$weekDay = $from->diffInWeekdays($to);
$human = $to->diffForHumans($from, true, false, 6);
var_dump($weekDay); //24
var_dump($human); // 1 month 3 days 7 hours
diffForHumans is perfect for my needs but I can't find any way of filtering like diffInDaysFiltered
What I would like to achieve is to get this result: 24 days 7 hours because we only have 24 working days
I tried a preg_replace first to replace 3 days by $weekDay result but if we have a month before it's incorrect and I have: 1 month 24 days 7 hours
Is there any solution for my problem ?

Here is the answer thanks to imbrish
I need to use the cascade factor to define:
How many hours is a day
How many days is a week
How many days is a month
Then with the diffFiltered I can only select weekday and working hours. You can also add a filter on public holiday thanks to macro
CarbonInterval::setCascadeFactors(array(
'days' => array(10, 'hours'),
'weeks' => array(5, 'days'),
'months' => array(20, 'days'),
));
$resolution = CarbonInterval::hour();
$start = Carbon::parse('2017-07-13 11:00');
$end = Carbon::parse('2017-07-17 18:00');
$hours = $start->diffFiltered($resolution, function ($date) {
return $date->isWeekday() && $date->hour >= 8 && $date->hour < 18;
}, $end);
$interval = CarbonInterval::hours($hours)->cascade();
echo $interval->forHumans(); // 2 days 7 hours
Here is the macro part:
Carbon::macro('isHoliday', function ($self = null) {
// compatibility chunk
if (!isset($self) && isset($this)) {
$self = $this;
}
// Put your array of holidays here
return in_array($self->format('d/m'), [
'25/12', // Christmas
'01/01', // New Year
]);
});
Then simply add && !$date->isHoliday() inside the diffFiltered

The above-mentioned answers are good correct. But there are some other ways also with carbon.
$startDate = Carbon::create(2017, 4, 4);
$endDate = Carbon::create(2017, 4, 18);
$no_of_days = $startDate->diffInDaysFiltered(function(Carbon $date) {
return !$date->isSunday();
}, $endDate);
Some useful functions of Carbon
$dt->isMonday();
$dt->isTuesday();
$dt->isWednesday();
$dt->isThursday();
$dt->isFriday();
$dt->isSaturday();
$dt->isSunday();
$dt->isDayOfWeek(Carbon::SATURDAY); // is a saturday
For more information go to https://carbon.nesbot.com/docs/

Ok, The solution works, but i had some problem with more complex time calculation, so i figured out another solution with while cycle
Carbon::macro('isHoliday', function ($self = null) {
// compatibility chunk
if (!isset($self) && isset($this)) {
$self = $this;
}
// Put your array of holidays here
return in_array($self->format('d/m'), [
'23/12',
'24/12',
'25/12',
'26/12',
'27/12',
'28/12',
'29/12',
'30/12',
'31/12',
'01/01',
'02/01',
'03/01',
'04/01',
'05/01',
'06/01',
'07/01',
]);
});
$end = Carbon::now();
$hours = 160;
while($hours > 0) {
if ($end->hour < 9 || $end->hour >= 18 || $end->hour == 13 || $end->isWeekend() || $end->isHoliday()) {
$end->addHour();
} else {
$end->addHour();
$hours--;
}
}
Debugbar::addMessage($end);

Related

Counting working hours (different for weekdays and Saturdays)

I tried #ebelendez's code for Calculating working hours between two dates, however I'm confused on how to set the value of Saturdays by 3 hours (08:00-11:00). For example, the working hours per day during weekdays is 8 hours (excluding 1 hour break), let's say I want to get the total working hours from Thursday to Saturday, the expected result would be 19 hours.
Here is what I've done. Can someone help me with this?
$from = '2022-04-21 07:00:00';
$to = '2022-04-23 16:00:00';
echo abs(get_working_hours($from, $to));
function get_working_hours($from,$to){
//config
$ini_time = [7,0]; //hr, min
$end_time = [16,0]; //hr, min
//date objects
$ini = date_create($from);
$ini_wk = date_time_set(date_create($from),$ini_time[0],$ini_time[1]);
$end = date_create($to);
$end_wk = date_time_set(date_create($to),$end_time[0],$end_time[1]);
//days
$workdays_arr = get_workdays($ini,$end);
$workdays_count = count($workdays_arr);
$workday_seconds = (($end_time[0] * 60 + $end_time[1]) - ($ini_time[0] * 60 + $ini_time[1])) * 60 - 3600;
//get time difference
$ini_seconds = 0;
$end_seconds = 0;
if(in_array($ini->format('Y-m-d'),$workdays_arr)) $ini_seconds = $ini->format('U') - $ini_wk->format('U');
if(in_array($end->format('Y-m-d'),$workdays_arr)) $end_seconds = $end_wk->format('U') - $end->format('U');
$seconds_dif = $ini_seconds > 0 ? $ini_seconds : 0;
if($end_seconds > 0) $seconds_dif += $end_seconds;
//final calculations
$working_seconds = ($workdays_count * $workday_seconds) - $seconds_dif;
return $working_seconds / 3600; //return hrs
}
function get_workdays($ini,$end){
//config
$skipdays = [0]; //sunday:0
$skipdates = [];
//vars
$current = clone $ini;
$current_disp = $current->format('Y-m-d');
$end_disp = $end->format('Y-m-d');
$days_arr = [];
//days range
while($current_disp <= $end_disp){
if(!in_array($current->format('w'),$skipdays) && !in_array($current_disp,$skipdates)){
$days_arr[] = $current_disp;
}
$current->add(new DateInterval('P1D')); //adds one day
$current_disp = $current->format('Y-m-d');
}
return $days_arr;
}
Your code and linked answers seem unnecessarily complicated. All we really need is to:
Configure how many hours should be counted for for each day;
Create an iterable DatePeriod (with DateTime objects for each date in the period);
Iterate dates, look up how many hours should be counted for each day, sum it up.
class CountWorkingHours
{
// Define hours counted for each day:
public array $hours = [
'Mon' => 8,
'Tue' => 8,
'Wed' => 8,
'Thu' => 8,
'Fri' => 8,
'Sat' => 3,
'Sun' => 0
];
// Method for counting the hours:
public function get_hours_for_period(string $from, string $to): int
{
// Create DatePeriod with requested Start/End dates:
$period = new DatePeriod(
new DateTime($from),
new DateInterval('P1D'),
new DateTime($to)
);
$hours = [];
// Loop over DateTime objects in the DatePeriod:
foreach($period as $date) {
// Get name of day and add matching hours:
$day = $date->format('D');
$hours[] = $this->hours[$day];
}
// Return sum of hours:
return array_sum($hours);
}
}
Source # BitBucket
Usage (returns an integer with working hours in a given period):
$cwh = new CountWorkingHours();
$hours = $cwh->get_hours_for_period('2022-04-21 07:00:00', '2022-04-30 16:00:00');
// = 62
If you need to account for public holidays etc. exceptions to the standard weekly hour counts, you can add a check inside the period loop for "skip dates". For example, have a $skip_dates property with an array of non-work-days, then check for !in_array($date->format('Y-m-d'), $this->skip_dates) before incrementing the work hours for a given day.
P.S. This code assumes that you are calculating whole working days. If your start or end hours were defined in the middle of a working day, that wouldn't be accounted for. (If you wanted to factor that in, you'd have to configure daily work times and the code would have to account for that in evaluating start and end dates. Seemed an unnecessary exercise for current purposes.)

Generate random dates with random times between two dates for selected period and frequency

I have to create a scheduling component that will plan e-mails that need to be sent out. Users can select a start time, end time, and frequency. Code should produce a random moment for every frequency, between start and end time. Outside of office hours.
Paramaters:
User can select a period between 01/01/2020 (the start) and 01/01/2021 (the end). In this case user selects a timespan of one exactly year.
User can select a frequency. In this case user selects '2 months'.
Function:
Code produces a list of datetimes. The total time (one year) is divided by frequency (2 months). We expect a list of 6 datetimes.
Every datetime is a random moment in said frequency (2 months). Within office hours.
Result:
An example result for these paramaters might as follows, with the calculated frequency bounds for clarity:
[jan/feb] 21-02-2020 11.36
[mrt/apr] 04-03-2020 16.11
[mei/jun] 13-05-2020 09.49
[jul-aug] 14-07-2020 15.25
[sep-okt] 02-09-2020 14.09
[nov-dec] 25-12-2020 13.55
--
I've been thinking about how to implement this best, but I can't figure out an elegant solution.
How could one do this using PHP?
Any insights, references, or code spikes would be greatly appreciated. I'm really stuck on this one.
I think you're just asking for suggestions on how to generate a list of repeating (2 weekly) dates with a random time between say 9am and 5pm? Is that right?
If so - something like this (untested, pseudo code) might be a starting point:
$start = new Datetime('1st January 2021');
$end = new Datetime('1st July 2021');
$day_start = 9;
$day_end = 17;
$date = $start;
$dates = [$date]; // Start date into array
while($date < $end) {
$new_date = clone($date->modify("+ 2 weeks"));
$new_date->setTime(mt_rand($day_start, $day_end), mt_rand(0, 59));
$dates[] = $new_date;
}
var_dump($dates);
Steve's anwser seems good, but you should consider 2 additional things
holiday check, in the while after first $new_date line, like:
$holiday = array('2021-01-01', '2021-01-06', '2021-12-25');
if (!in_array($new_date,$holiday))
also a check if date is a office day or a weekend in a similar way as above with working days as an array.
It's kind of crappy code but I think it will work as you wish.
function getDiffInSeconds(\DateTime $start, \DateTime $end) : int
{
$startTimestamp = $start->getTimestamp();
$endTimestamp = $end->getTimestamp();
return $endTimestamp - $startTimestamp;
}
function getShiftData(\DateTime $start, \DateTime $end) : array
{
$shiftStartHour = \DateTime::createFromFormat('H:i:s', $start->format('H:i:s'));
$shiftEndHour = \DateTime::createFromFormat('H:i:s', $end->format('H:i:s'));
$shiftInSeconds = intval($shiftEndHour->getTimestamp() - $shiftStartHour->getTimestamp());
return [
$shiftStartHour,
$shiftEndHour,
$shiftInSeconds,
];
}
function dayIsWeekendOrHoliday(\DateTime $date, array $holidays = []) : bool
{
$weekendDayIndexes = [
0 => 'Sunday',
6 => 'Saturday',
];
$dayOfWeek = $date->format('w');
if (empty($holidays)) {
$dayIsWeekendOrHoliday = isset($weekendDayIndexes[$dayOfWeek]);
} else {
$dayMonthDate = $date->format('d/m');
$dayMonthYearDate = $date->format('d/m/Y');
$dayIsWeekendOrHoliday = (isset($weekendDayIndexes[$dayOfWeek]) || isset($holidays[$dayMonthDate]) || isset($holidays[$dayMonthYearDate]));
}
return $dayIsWeekendOrHoliday;
}
function getScheduleDates(\DateTime $start, \DateTime $end, int $frequencyInSeconds) : array
{
if ($frequencyInSeconds < (24 * 60 * 60)) {
throw new \InvalidArgumentException('Frequency must be bigger than one day');
}
$diffInSeconds = getDiffInSeconds($start, $end);
// If difference between $start and $end is bigger than two days
if ($diffInSeconds > (2 * 24 * 60 * 60)) {
// If difference is bigger than 2 days we add 1 day to start and subtract 1 day from end
$start->modify('+1 day');
$end->modify('-1 day');
// Getting new $diffInSeconds after $start and $end changes
$diffInSeconds = getDiffInSeconds($start, $end);
}
if ($frequencyInSeconds > $diffInSeconds) {
throw new \InvalidArgumentException('Frequency is bigger than difference between dates');
}
$holidays = [
'01/01' => 'New Year',
'18/04/2020' => 'Easter 1st official holiday because 19/04/2020',
'20/04/2020' => 'Easter',
'21/04/2020' => 'Easter 2nd day',
'27/04' => 'Konings',
'04/05' => '4mei',
'05/05' => '4mei',
'24/12' => 'Christmas 1st day',
'25/12' => 'Christmas 2nd day',
'26/12' => 'Christmas 3nd day',
'27/12' => 'Christmas 3rd day',
'31/12' => 'Old Year'
];
[$shiftStartHour, $shiftEndHour, $shiftInSeconds] = getShiftData($start, $end);
$amountOfNotifications = floor($diffInSeconds / $frequencyInSeconds);
$periodInSeconds = intval($diffInSeconds / $amountOfNotifications);
$maxDaysBetweenNotifications = intval($periodInSeconds / (24 * 60 * 60));
// If $maxDaysBetweenNotifications is equals to 1 then we have to change $periodInSeconds to amount of seconds for one day
if ($maxDaysBetweenNotifications === 1) {
$periodInSeconds = (24 * 60 * 60);
}
$dates = [];
for ($i = 0; $i < $amountOfNotifications; $i++) {
$periodStart = clone $start;
$periodStart->setTimestamp($start->getTimestamp() + ($i * $periodInSeconds));
$seconds = mt_rand(0, $shiftInSeconds);
// If $maxDaysBetweenNotifications is equals to 1 then we have to check only one day without loop through the dates
if ($maxDaysBetweenNotifications === 1) {
$interval = new \DateInterval('P' . $maxDaysBetweenNotifications . 'DT' . $seconds . 'S');
$date = clone $periodStart;
$date->add($interval);
$dayIsWeekendOrHoliday = dayIsWeekendOrHoliday($date, $holidays);
} else {
// When $maxDaysBetweenNotifications we have to loop through the dates to pick them
$loopsCount = 0;
$maxLoops = 3; // Max loops before breaking and skipping the period
do {
$day = mt_rand(0, $maxDaysBetweenNotifications);
$periodStart->modify($shiftStartHour);
$interval = new \DateInterval('P' . $day . 'DT' . $seconds . 'S');
$date = clone $periodStart;
$date->add($interval);
$dayIsWeekendOrHoliday = dayIsWeekendOrHoliday($date, $holidays);
// If the day is weekend or holiday then we have to increment $loopsCount by 1 for each loop
if ($dayIsWeekendOrHoliday === true) {
$loopsCount++;
// If $loopsCount is equals to $maxLoops then we have to break the loop
if ($loopsCount === $maxLoops) {
break;
}
}
} while ($dayIsWeekendOrHoliday);
}
// Adds the date to $dates only if the day is not a weekend day and holiday
if ($dayIsWeekendOrHoliday === false) {
$dates[] = $date;
}
}
return $dates;
}
$start = new \DateTime('2020-12-30 08:00:00', new \DateTimeZone('Europe/Sofia'));
$end = new \DateTime('2021-01-18 17:00:00', new \DateTimeZone('Europe/Sofia'));
$frequencyInSeconds = 86400; // 1 day
$dates = getScheduleDates($start, $end, $frequencyInSeconds);
var_dump($dates);
You have to pass $start, $end and $frequencyInSeconds as I showed in example and then you will get your random dates. Notice that I $start and $end must have hours in them because they are used as start and end hours for shifts. Because the rule is to return a date within a shift time only in working days. Also you have to provide frequency in seconds - you can calculate them outside the function or you can change it to calculate them inside. I did it this way because I don't know what are your predefined periods.
This function returns an array of \DateTime() instances so you can do whatever you want with them.
UPDATE 08/01/2020:
Holidays now are part of calculation and they will be excluded from returned dates if they are passed when you are calling the function. You can pass them in d/m and d/m/Y formats because of holidays like Easter and in case when the holiday is on weekend but people will get additional dayoff during the working week.
UPDATE 13/01/2020:
I've made updated code version to fix the issue with infinite loops when $frequencyInSeconds is shorter like 1 day. The new code used few functions getDiffInSeconds, getShiftData and dayIsWeekendOrHoliday as helper methods to reduce code duplication and cleaner and more readable code

Random Set of Dates for Laravel Project

This is a Laravel project that I'm attempting to get a random collection of dates between the two dates below. The random dates chosen need to be 6 years apart and be on either a Monday, Thursday or Sunday. I have the dates function below that works for another date range I need however with this situation there's the additional factor of 6 years so I need the additional modification for it and not sure what I need to do to account for it on this situation.
$start = Carbon::parse('First Monday of January 2000');
$nextMonth = Carbon::now()->addMonth();
collect([
'monday' => false,
'thursday' => false,
'sunday' => true
])->flatMap(function ($bool, $day) use ($start, $nextMonth) {
return dates($start, $nextMonth, $day, $bool);
})->sort(function ($a, $b) {
return strtotime($a) - strtotime($b);
})->values()->map(function ($date, $key) {
return factory(Event::class)->create([
'name' => 'Event ' . ($key + 1),
'date' => $date
]);
})->filter(function ($event) {
return $event->date->lt(Carbon::today());
function dates(Carbon $from, Carbon $to, $day, $last = false)
{
$step = $from->copy()->startOfMonth();
$modification = sprintf($last ? 'last %s of next month' : 'next %s', $day);
$dates = [];
while ($step->modify($modification)->lte($to)) {
if ($step->lt($from)) {
continue;
}
$dates[$step->timestamp] = $step->copy();
}
return $dates;
}
If the second date is exactly six years from the first random date:
$second_date = $first_date->diffInYears($first_date->copy()->addYears(6))
From Carbon docs: http://carbon.nesbot.com/docs/#api-difference
Update:
Here is a way of creating an array of dates, based on a start date, where each date is at least six years apart and either a Monday, Thursday or Sunday.
I've left the loop at 20 iterations so you can see the dates generated are different days over the years.
$start = Carbon::parse('First Monday of January 2000');
$dates = array();
for ($i = 1; $i < 20; $i ++)
{
$interval = $i * 6;
if ($start->copy()->addYears($interval)->dayOfWeek === Carbon::MONDAY OR $start->copy()->addYears($interval)->dayOfWeek === Carbon::THURSDAY OR $start->copy()->addYears($interval)->dayOfWeek === Carbon::SUNDAY)
{
$dates[] = $start->copy()->addYears($interval);
} else
{
$dates[] = $start->copy()->addYears($interval)->modify('next monday');
}
}

Stop retrieving articles if article is posted 12 months from now

I'm scraping a page for articles which all contains of a date in the following format:
2012-08-20T11:04:00+0200
What I want to do is to stop retrieve articles if the next article is posted 12 months from todays date. The way I can think of is the following:
while ($retrieveArticles == true) {
$stopDate = date('Y-m-d'); // <--- this gives me todays date while I need the date 12 months ago.
$date = $article->find('header div p span', 1);
$date = substr($date->title, 0, 10); // <--- becomes 2012-08-20
if ($date >= $stopDate) {
$retrieveArticles = false;
}
... not relevant code
}
What I need help with:
How can I subtract 12 months from todays date?
Am I thinking right by doing like this or is there better and more elegant ways of achieve what I want?
Thanks in advance!
it will be wrong if you compare Y-m-d format of date with together :
you need to convert that to time format with strtotime() function .
for 12 month that is ( 365*24*3600 sec) . so you can change your function like this :
while ($retrieveArticles == true) {
$stopDate = date('Y-m-d'); // <--- this gives me todays date while I need the date 12 months ago.
$date = $article->find('header div p span', 1);
$date = substr($date->title, 0, 10); // <--- becomes 2012-08-20
$stopDate = strtotime($stopDate);
$date = (int)strtotime($date) + (365*24*3600);
if ($stopDate >= $date) {
$retrieveArticles = false;
}
}
Yes, for sure :
$in_12_months = strtotime('+12 months');
while ($retrieveArticles == true) {
$article_date = strtotime($article->find('header div p span', 1));
if ($article_date >= $in_12_months) {
$retrieveArticles = false;
}
}
Here's how I did it:
<?php
$s = strtotime('2012-02-09T11:04:00+0200');
$timeDifference = time() - $s;
echo round($timeDifference / 60 / 60 / 24 / 30);
?>
Output:
11
Convert 2012-08-20T11:04:00+0200 to a timestamp: How to convert date to timestamp in PHP?
and then just do $seconds = time()-$theresult this will be the number of seconds since then. 12 months should be roughly equals to 31 Million Seconds
You could do something like this:
<?php
// Current date
$posted = strtotime("2012-08-20T11:04:00+0200");
// 12 months ago
$timestamp = strtotime("-12 months", $posted);
// days
$days = ($posted - $timestamp) / 60 / 60 / 24;
$get_items = true;
if($days >= 365){
$get_items = false;
}

How do I find date prior to another date in php

I need to find date x such that it is n working days prior to date y.
I could use something like date("Y-m-d",$def_date." -5 days");, but in that case it wont take into consideration the weekend or off-date. Let's assume my working days would be Monday to Saturday, any idea how I can accomplish this?
Try this
<?php
function businessdays($begin, $end) {
$rbegin = is_string($begin) ? strtotime(strval($begin)) : $begin;
$rend = is_string($end) ? strtotime(strval($end)) : $end;
if ($rbegin < 0 || $rend < 0)
return 0;
$begin = workday($rbegin, TRUE);
$end = workday($rend, FALSE);
if ($end < $begin) {
$end = $begin;
$begin = $end;
}
$difftime = $end - $begin;
$diffdays = floor($difftime / (24 * 60 * 60)) + 1;
if ($diffdays < 7) {
$abegin = getdate($rbegin);
$aend = getdate($rend);
if ($diffdays == 1 && ($astart['wday'] == 0 || $astart['wday'] == 6) && ($aend['wday'] == 0 || $aend['wday'] == 6))
return 0;
$abegin = getdate($begin);
$aend = getdate($end);
$weekends = ($aend['wday'] < $abegin['wday']) ? 1 : 0;
} else
$weekends = floor($diffdays / 7);
return $diffdays - ($weekends * 2);
}
function workday($date, $begindate = TRUE) {
$adate = getdate($date);
$day = 24 * 60 * 60;
if ($adate['wday'] == 0) // Sunday
$date += $begindate ? $day : -($day * 2);
return $date;
}
$def_date="";//define your date here
$preDay='5 days';//no of previous days
date_sub($date, date_interval_create_from_date_string($preDay));
echo businessdays($date, $def_date); //date prior to another date
?>
Modified from PHP.net
Thanks for the help guys, but to solve this particular problem I wrote a simple code:
$sh_padding = 5; //No of working days to count backwards
$temp_sh_padding = 1; //A temporary holder
$end_stamp = strtotime(date("Y-m-d", strtotime($date_format)) . " -1 day"); //The date(timestamp) from which to count backwards
$start_stamp = $end_stamp; //start from same as end day
while($temp_sh_padding<$sh_padding)
{
$sh_day = date('w',$start_stamp);
if($sh_day==0){ //Skip if sunday
}
else
{
$temp_sh_padding++;
}
$start_stamp = strtotime(date("Y-m-d",$start_stamp)." -1 day");
}
$sh_st_dte = date("Y-m-d",$start_stamp); //The required start day
A quick bit of googling got me to this page, which includes a function for calculating the number of working days between two dates.
It should be fairly trivial to adjust that concept to suit your needs.
Your problem, however, is that the concept of "working days" being monday to friday is not universal. If your software is only ever being used in-house, then it's okay to make some assumptions, but if it's intended for use by third parties, then you can't assume that they'll have the same working week as you.
In addition, public holidays will throw a big spanner in the works, by removing arbitrary dates from various working weeks throughout the year.
If you want to cater for these, then the only sensible way of doing it is to store the dates of the year in a calendar (ie a big array), and mark them individually as working or non-working days. And if you're going to do that, then you may as well use the same mechanism for weekends too.
The down-side, of course, is that this would need to be kept up-to-date. But for weekends, at least, that would be trivial (loop through the calendar in advance and mark weekend days where date('w')==0 or date('w')==6).

Categories