Calculating working hours time difference in PHP - php

I wanted to calculate the number of hours between the encoded date and completion date. As of now I can compute the time difference between the two datetimes excluding the Saturdays and Sundays. My problem is I need to exclude the lunch breaks (12:00 am to 1:00 pm) and holidays.
Here is my code:
<?php
function business_hours($start, $end){
$startDate = new DateTime($start);
$endDate = new DateTime($end);
$periodInterval = new DateInterval( "PT1H" );
$period = new DatePeriod( $startDate, $periodInterval, $endDate );
$count = 0;
$holidays = [
"Human Rights Day" => new DateTime(date('Y') . '-03-21'),
"Good Friday" => new DateTime(date('Y') . '-03-30'),
"Family Day" => new DateTime(date('Y') . '-04-02'),
"Freedom Day" => new DateTime(date('Y') . '-04-27'),
"Labour Day" => new DateTime(date('Y') . '-05-01'),
"Youth Day" => new DateTime(date('Y') . '-06-16'),
"National Women's Day" => new DateTime(date('Y') . '-08-09'),
"Heritage Day" => new DateTime(date('Y') . '-09-24'),
"Day of Reconciliation" => new DateTime(date('Y') . '-12-16'),
];
foreach($period as $date){
$startofday = clone $date;
$startofday->setTime(8,00);
$endofday = clone $date;
$endofday->setTime(17,00);
if($date > $startofday && $date <= $endofday && !in_array($date->format('l'), array('Sunday','Saturday'))){
$count++;
}
}
echo $count;
}
$start = '2020-02-14 08:00:00';
$end = '2020-02-17 08:00:00';
$go = business_hours($start,$end);
//output 9 hours
?>
As per my code, I set the working hours from 8:00 to 17:00 and then excluded the Saturdays and Sundays. As per my example the output will be 9 (including the lunch break). How can I exclude the lunch break and holidays?
Update
The completion time can be more than 1 day. For example, the start date/time is 2020-02-14 08:00:00 and the completion time is 2020-02-19 08:00:00 the output is 27 hours. The calculation should exclude the lunch breaks of each day that have 12:00am to 1:00pm and holidays in between.
Update
I added an array of holidays, how can I exclude these holidays?

Since you're using a static/defined lunch break, you can use the following logic:
If $startDate is a weekday, and before 12:00, that's the start date for lunch calculation; otherwise it's the next weekday.
If the $endDate is a weekday, and after 13:00, that's the end date for lunch calculation; otherwise, it's the previous weekday
Actual number of lunch breaks equals the difference between those recalculated dates, at one hour each.
The following is pseudocode:
if(!in_array($startDate->format('l'), array('Sunday','Saturday')) && ($startDate->format('g') < 12) {
$startForLunch = $startDate;
}
else {
$startForLunch = new DateTime($start, '+1 day');
while(in_array($startDate->format('l'), array('Sunday','Saturday'))){
$startForLunch->add('1 day');
}
}
if(!in_array($endDate->format('l'), array('Sunday','Saturday')) && ($endDate->format('g') > 13) {
$endForLunch = $end;
}
else {
$endForLunch = new DateTime($endDate, '-1 day');
while(in_array($endDate->format('l'), array('Sunday','Saturday'))){
$endForLunch->add('-1 day');
}
}
$lunchPeriods = $endForLunch - $startForLunch + 1;

how about try add some condition inside loop each day to skip some hours
($date->format('H') <= 12 || $date->format('H') > 13)
it will skip if hour above 12 to 13 (if you use 24 format hour)
if($date > $startofday && $date <= $endofday && !in_array($date->format('l'), array('Sunday','Saturday')) && ($date->format('H') <= 12 || $date->format('H') > 13)){
$count++;
}
UPDATE
okay if your "holydays" is cutom maybe be good to add some paramerter to your function to store list of your holydays
function business_hours($start, $end,$holyDays = null){
//another line
}
and check every loop of dates is in on holydays list or not
in_array($date->format('Y-m-d'), $holyDays)
you will get filter of break time and custom holyday
function business_hours($start, $end,$holyDays = null){
$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,00);
$endofday = clone $date;
$endofday->setTime(17,00);
//just some var to use in conditional check
$notHolyday = true;
//now check the array of holyday and check if in range dates is same with holydays we have
if($holyDays != null && is_array($holyDays) && in_array($date->format('Y-m-d'), $holyDays)){
$notHolyday = false;
}
if($date > $startofday && $date <= $endofday && !in_array($date->format('l'), array('Sunday','Saturday')) && ($date->format('H') <= 12 || $date->format('H') > 13) && $notHolyday){
$count++;
}
}
echo $count;
}
$start = '2020-02-14 08:00:00';
$end = '2020-02-19 08:00:00';
$holyDays = array('2020-02-17','2020-02-18');
$count = business_hours($start,$end,$holyDays);
echo $count;
i hope this is want you want

Related

Finding last 7 working days in PHP

I am trying to find out last 7 working days (excluding Saturday and Sunday) from the current day. I am able to get last 7 days of the week, but unable to get the 7 working day's.
//code to found last 7 days
$date = '04/30/2009'; // set current date
// parse about any English textual datetime description into a Unix timestamp
$ts = strtotime($date);
// calculate the number of days since Monday
$dow = date('w', $ts);
$offset = $dow - 1;
if ($offset < 0) {
$offset = 6;
}
// calculate timestamp for the Monday
$ts = $ts - $offset*86400;
// loop from Monday till Sunday
for ($i = 0; $i < 7; $i++, $ts += 86400){
print date("m/d/Y l", $ts) . "\n". "<br>";
}
But I wanted to find last 7 working days. Thanks in advance!
It's relatively easy to work backwards from a point int time to find the "working days" but finding "holidays" is far more complicated and would require considerably more code than this I suspect.
Using the DateTime class you can use the sub method to subtract whatever interval value you wish, in this case P1D is one day.
$strdate='04/30/2009';
$days=11;
$timezone=new DateTimeZone('Europe/London');
$interval=new DateInterval('P1D');
$start=new DateTime( date( DATE_COOKIE, strtotime( $strdate ) ), $timezone );
$end=new DateTime( date( DATE_COOKIE, strtotime( $start->format( DATE_COOKIE ) . '-' . $days .' days' ) ), $timezone );
$dates=array();
while( $start->sub( $interval ) > $end ){
/* is the day integer less than 6(sat)? */
if( $start->format('N') < 6 && count( $dates ) < 7 ) $dates[]=$start->format( DATE_COOKIE );
}
echo '<pre>',print_r( $dates,true ),'</pre>';
This outputs:
Array
(
[0] => Wednesday, 29-Apr-09 00:00:00 BST
[1] => Tuesday, 28-Apr-09 00:00:00 BST
[2] => Monday, 27-Apr-09 00:00:00 BST
[3] => Friday, 24-Apr-09 00:00:00 BST
[4] => Thursday, 23-Apr-09 00:00:00 BST
[5] => Wednesday, 22-Apr-09 00:00:00 BST
[6] => Tuesday, 21-Apr-09 00:00:00 BST
)
I did a little playing around with a public api for getting public holiday information and cobbled the following together...I don't know what sort of coverage this api has for different countries but might be worth investigation.
Different country and region codes can be found here
$api='http://kayaposoft.com/enrico/json/v1.0/';
$params=array(
'action' => 'getPublicHolidaysForDateRange',
'fromDate' => '',
'toDate' => '',
'country' => 'eng' #England
);
$strdate='2017/12/30';
$days=7;
$timezone=new DateTimeZone('Europe/London');
$interval=new DateInterval('P1D');
$start=new DateTime( date( 'Y-m-d', strtotime( $strdate ) ), $timezone );
$end=new DateTime( date( 'Y-m-d', strtotime( $start->format( 'Y-m-d' ) . '-' . $days .' days' ) ), $timezone );
$params['fromDate']=$end->format('d-m-Y');
$params['toDate']=$start->format('d-m-Y');
$query=http_build_query( $params );
$url=$api.'?'.$query;
$json=json_decode( file_get_contents( $url ) );
if( json_last_error() !== 0 ){
$json=false;
}
function isholiday( $obj=false, $y=0, $m=0, $d=0 ){
if( $obj && !empty( $obj ) ){
foreach( $obj as $item ){
$date=$item->date;
if( $date->day==$d && $date->month=$m && $date->year==$y ) return $item->localName;
}
}
return false;
}
$dates=array();
while( $start->sub( $interval ) > $end ){
if( $start->format('N') < 6 && count( $dates ) < $days ) {
$holiday=isholiday( $json, $start->format('Y'), $start->format('m'), $start->format('d') );
$date=$start->format( 'Y-m-d' );
$dates[]=$holiday ? $date .' - '.$holiday : $date;
}
}
echo '<pre>',print_r( $dates, true ),'</pre>';
This outputs the following:
Array
(
[0] => 2017-12-29
[1] => 2017-12-28
[2] => 2017-12-27
[3] => 2017-12-26 - Boxing Day
[4] => 2017-12-25 - Christmas Day
)
Finding holidays will be more complicated.
One solution can be like saving the holidays in advance and skip them if they comes in the condition.
One simple solution for your problem can be like this.
<?php
$holiday = array(
'2017-12-16' => 'Victory Day of Bangladesh',
'2017-12-25' => 'Christmas'
);
$i = 0;
$work_day = '2017-12-26';
while($i != 7)
{
$work_day = date('Y-m-d', strtotime('-1 day', strtotime($work_day)));
$day_name = date('l', strtotime($work_day));
if($day_name != 'Saturday' && $day_name != 'Sunday' && !isset($holiday[$work_day]))
{
echo $work_day."\n";
$i++;
}
}
?>
If you need 7 days without weekend days you need to check the numeric representation of the day of the week, ie "N" Format of date('N').
When skipping 2 weekend days you need increase the for loop to 9 days:
//code to found last 7 days
$date = '04/30/2009'; // set current date
// parse about any English textual datetime description into a Unix timestamp
$ts = strtotime($date);
// calculate the number of days since Monday
$dow = date('w', $ts);
$offset = $dow - 1;
if ($offset < 0) {
$offset = 6;
}
// calculate timestamp for the Monday
$ts = $ts - $offset*86400;
// loop from Monday till Sunday
for ($i = 0; $i < 9; $i++, $ts += 86400){
if(date("N", $ts) < 6){ // day code is less then weekday 6 & 7
print date("m/d/Y l", $ts) . "\n". "<br>";
}
}
https://eval.in/916514
Simple code with possibility to add holidays:
<?php
$date = new DateTime('03/02/2009');
$interval = new DateInterval('P1D');
function isWorkingDay($date)
{
$weekDay = $date->format('w');
if ($weekDay == 0) // sunday
return false;
if ($weekDay == 6) // saturday
return false;
if ($date->format('m-d') == '07-05') // your check, example 5th July
return false;
return true;
}
$workingDays = [];
while(count($workingDays) != 7) {
if(isWorkingDay($date)) {
$workingDays[] = clone $date;
}
$date->sub($interval);
}
foreach($workingDays as $workingDay) {
echo $workingDay->format('Y-m-d') . ', ';
}
Result - list of days without weekend days:
2009-03-02, 2009-02-27, 2009-02-26, 2009-02-25, 2009-02-24, 2009-02-23, 2009-02-20,

PHP Dates getting a start date and end date for each period

Okay I'm trying to generate the end of the billing period from one date; my question is if I have a variable $billing_period = 02/28/2016 and the billing periods are on every 14th and 28th of each month. from this one variable how can I generate the end date of the period when they have different days appart depending on the start date?
What's confusing to me is that if the date is the 28th it has either 15 or 16days apart from the 14th which is the start of the next billing period. And if the date is the 14th then it has 14 days apart from the 28th. Thanks for any help
EDIT
- The image here shows the date which is selected which is the 02/14/2016 how can I echo the next billing date which would be 02/28/2016 from just the start date
This is my code for the array and getting the start date.
<?
$date = array('16-01-14','16-01-28','16-02-14','16-02-28','16-03-14','16-03-28','16-04-14','16-04-28',
'16-05-14','16-05-28','16-06-14','16-06-28','16-07-14','16-07-28','16-08-14','16-08-28','16-09-14','16-09-28','16-10-14','16-10-28',
'16-11-14','16-11-28','16-12-14','16-12-28');
$currentdate = date('y-m-d');
foreach ($date as $i => $d) {
if ($currentdate >= $d && ($i == count($date)-1 || $currentdate < $date[$i+1])) {
$selected = "selected";
$selected_int = $i;
} else {
$selected = "";
}
list($year, $month, $day) = explode('-', $d);
echo "<option $selected>" . date("m/d/Y", strtotime($d)) . "</option>";
}
?>
Hope I understood well.
What you are trying to do is determine which is your billing period depending on the date you previously selected.
So, you select a date ($date).
Then you need to know the selected day
$timestamp = strtotime($date);
$day = date('D', $timestamp);
Now that you have your day you can make the comparations.
$datetime = new DateTime($date);
if ($day == 14 ){ // if you select 14th then your billing is on the 28th
$billing = $datetime->modify($date->format('Y-m-28'));
}else{ // if you didn't select 14th, then you select 28, and you add one month and set the day in 14th.
$next_month = $datetime->modify('+1 month');
$billing = $next_month->modify($date->format('Y-m-14'));
}
Build an array with the billing dates, get the current key and increment it to get the next value. Simple example with strings:
$dates = ['02/28/2016', '03/14/2016', '03/28/2016', ...];
$key = array_search( '03/14/2016', $dates );
$nextDate = $dates[$key + 1]; // 03/28/2016
Is this what you want to get?
Here the code to get you the next date, just in case you want to do it as a one off.
$date = new DateTime();
$next = $date->format('14/m/Y');
if ($date->format('d') >= 14 && $date->format('d') < 28) {
$next = $date->format('28/m/Y');
}
if ($date->format('d') >= 28) {
$date->modify('+1 month');
$next = $date->format('14/m/Y');
}
echo $next;
Here's the code turned into the function which you can call whenever you need it, it'll return a DateTime() object so you can output the date in whatever format you need at the time, or perform further comparisons/modifications should you need to down the line.
function nextBillingDate($date, $format = 'd/m/Y') {
$date = DateTime::createFromFormat($format, $date);
$next = $date->format('14/m/Y');
if ($date->format('d') >= 14 && $date->format('d') <= 28) {
$next = $date->format('28/m/Y');
}
if ($date->format('d') >= 28) {
$date->modify('+1 month');
$next = $date->format('14/m/Y');
}
return DateTime::createFromFormat('d/m/Y', $next);
}
Example use:
$date = nextBillingDate('28/02/2016');
echo $date->format('d/m/Y');
Outputs:
14/03/2016
Hope it helps.

find out how many weekend in a date range

for example I have two dates 2015-10-28 and 2015-12-31. from these I want to know how many saturday and sunday in that given date range. I can find the diff between that dates but I can't find how many weekends.
anyone ever made this?
here is my current code:
function createDateRange($maxDate, $cell, $lead, $offArray = array()){
$dates = [];
--$cell;
--$lead;
$edate = date('Y-m-d', strtotime($maxDate." -$lead day"));
$sdate = date('Y-m-d', strtotime($edate." -$cell day"));
$start = new DateTime($sdate);
$end = new DateTime($edate);
$end = $end->modify('+1 day');
$interval = DateInterval::createFromDateString('1 day');
$period = new DatePeriod($start, $interval, $end);
foreach($period as $d){
$dt = $d->format('Y-m-d');
if(!in_array($dt, $dates)){
$dates[] = $dt;
}
}
return $dates;
}
basically I want to add sat+sun count to the date range.
The trick is to use an O(1)-type algorithm to solve this.
Given your starting date, move to the first Saturday. Call that from
Given your ending date, move back to the previous Friday. Call that to
Unless you have an edge case (where to is less than from), compute (to - from) * 2 / 7 as the number of weekend days, and add that to any weekend days passed over in steps (1) and (2).
This is how I do it in production, although generalised for arbitrary weekend days.
Use this function:
function getDateForSpecificDayBetweenDates($startDate, $endDate, $weekdayNumber)
{
$startDate = strtotime($startDate);
$endDate = strtotime($endDate);
$dateArr = array();
do
{
if(date("w", $startDate) != $weekdayNumber)
{
$startDate += (24 * 3600); // add 1 day
}
} while(date("w", $startDate) != $weekdayNumber);
while($startDate <= $endDate)
{
$dateArr[] = date('Y-m-d', $startDate);
$startDate += (7 * 24 * 3600); // add 7 days
}
return($dateArr);
}
The function call to get dates for all Sunday's in year 2015:
$dateArr = getDateForSpecificDayBetweenDates('2015-01-01', '2015-12-31', 0);
print "<pre>";
print_r($dateArr);
//周日0 周一1 .....
$data = 4;//周四
$t1 ='2015-10-28';
$t2 = '2015-12-31';
$datetime1 = date_create($t1);
$datetime2 = date_create($t2);
$interval = date_diff($datetime1, $datetime2);
$day = $interval->format('%a');
$result = ($day)/7;
$start = getdate(strtotime($t1))['wday'];
$end = getdate(strtotime($t2))['wday'];
if($data>=$start && $data<=$end){
echo floor($result)+1;
}else{
echo floor($result);
}

PHP If an Hour Is Between Two Other Hours

I need to work out if the current hour is between two other times. For example to check if the time is between 07:00 and 10:00 I could use:
$currentTime = new DateTime('09:00');
$startTime = new DateTime('07:00');
$endTime = new DateTime('10:00');
if ($currentTime->format('H:i') >= $startTime->format('H:i') && $currentTime->format('H:i') <= $endTime->format('H:i')) {
// Do something
}
The problem I have is what happens if the time is 01:00 and I want to check if it's between 22:00 and 07:00. I'm not bothered that that it is another day, just if it falls between those two hours on a 24hr clock. Example 01:00 is between 22:00 and 07:00
22:00, 23:00, 00:00, 01:00, 02:00... 07:00
What I'm trying to achieve in the end is different prices can be set for a service between different times. So I currently loop through each hour working out what price bracket that hour falls into and the changing the price accordingly. If anyone has a more elegant solution for the problem I would be grateful to learn.
UPDATE:
Say I have a rule that says between 10pm and 7am I want to charge double. I loop through each hour from the start time to the end time and check if each hour falls between 22:00 (10pm) and 07:00 (7am) and if so it should be charged double. I want to avoid having to take in to account the date.
<?php
$currentTime = strtotime('1:00');
$startTime = strtotime('22:00');
$endTime = strtotime('7:00');
if (
(
$startTime < $endTime &&
$currentTime >= $startTime &&
$currentTime <= $endTime
) ||
(
$startTime > $endTime && (
$currentTime >= $startTime ||
$currentTime <= $endTime
)
)
) {
echo 'open';
} else {
echo 'clse';
}
No need to use DateTime::format() for your comparisons. DateTime objects are already comparable.
To handle working with time periods that span midnight you will need to change the date so you have an accurate reflection of the actual date.
$currentTime = (new DateTime('01:00'))->modify('+1 day');
$startTime = new DateTime('22:00');
$endTime = (new DateTime('07:00'))->modify('+1 day');
if ($currentTime >= $startTime && $currentTime <= $endTime) {
// Do something
}
I found another way starting from the first and more popular solution that has the problem that doesn't work for me.
I have to write something between 10:00 pm and 06:00 am and the comparison does not work because when i pass midnight the endTime was always > of the start time.
so if i don't mind about the day i solved in this way:
$currentTime = new DateTime();
$startTime = new DateTime('22:00');
$endTime = (new DateTime('06:00'))->modify('+1 day');
if ($currentTime->format("a") == "am") {
// echo "It's a new day <br />";
$currentTime = $currentTime->modify('+1 day');
}
if ($currentTime >= $startTime && $currentTime <= $endTime) {
echo "hello";
}
//get current DateTime
$date = new DateTime('now');
$currentHour = 0;
$currentMinutes = 0;
foreach ($date as $item=>$value ) {
if($item=="date"){
$p = explode(" ",$value)[1];
$hour = explode(":",$p);
$currentHour = $hour[0];
$currentMinutes = $hour[1];
}
}
$returnVO["success"] = ((int)$currentHour<12 || ((int)$currentHour>=22 && (int)$currentMinutes > 0) ) ? false : true;
$hour = (int)date('H');
echo ($hour > 7 && $hour < 22) ? 'Open' : 'Close';
$hour = (int)date('H');
echo ($hour <= 7 || $hour >= 22) ? 'Close':'Open';

PHP: DatePeriod with last day of month

I'd like to work with PHP DateInterval to iterate through months:
$from = new DateTime();
$from->setDate(2014, 1, 31);
$period = new DatePeriod($from, new DateInterval('P1M'), 12);
I'd expect it to returns 31 January, 28 February (as the DateInterval is 1 month), but it actually returns 31 January, 3 March, 3 of April... hence skipping February.
Is there any way to do this simply?
Thanks!
EDIT : as a refernece, here is a solution that seems to cover most use cases:
$date = new DateTime('2014-01-31');
$start = $date->format('n');
for ($i = 0; $i < 28; $i++) {
$current = clone $date;
$current->modify('+'.$i.' month');
if ($current->format('n') > ($start % 12) && $start !== 12) {
$current->modify('last day of last month');
}
$start++;
echo $current->format('Y-m-d').PHP_EOL;
}
You can use DateTime::modify():
$date = new DateTime('last day of january');
echo $date->format('Y-m-d').PHP_EOL;
for ($i = 1; $i < 12; $i++) {
$date->modify('last day of next month');
echo $date->format('Y-m-d').PHP_EOL;
}
EDIT: I think I didn't understand your question clearly. Here is a new version:
$date = new DateTime('2014-01-31');
for ($i = 0; $i < 12; $i++) {
$current = clone $date;
$current->modify('+'.$i.' month');
if ($current->format('n') > $i + 1) {
$current->modify('last day of last month');
}
echo $current->format('Y-m-d').PHP_EOL;
}
The issue is cause by the variance between the last day in each of the months within the range. ie. February ending on 28 instead of 31 and the addition of 1 month from the last day 2014-01-31 + 1 month = 2014-03-03 https://3v4l.org/Y42QJ
To resolve the issue with DatePeriod and simplify it a bit, adjust the initial date by resetting the specified date to the first day of the specified month, by using first day of this month.
Then during iteration, you can modify the date period date by using last day of this month to retrieve the bounds of the currently iterated month.
Example: https://3v4l.org/889mB
$from = new DateTime('2014-01-31');
$from->modify('first day of this month'); //2014-01-01
$period = new DatePeriod($from, new DateInterval('P1M'), 12);
foreach ($period as $date) {
echo $date->modify('last day of this month')->format('Y-m-d');
}
Result:
2014-01-31
2014-02-28
2014-03-31
2014-04-30
2014-05-31
2014-06-30
2014-07-31
2014-08-31
2014-09-30
2014-10-31
2014-11-30
2014-12-31
2015-01-31
Then to expand on this approach, in order to retrieve the desired day from the specified date, such as the 29th. You can extract the specified day and adjust the currently iterated month as needed when the day is out of bounds for that month.
Example: https://3v4l.org/SlEJc
$from = new DateTime('2014-01-29');
$day = $from->format('j');
$from->modify('first day of this month'); //2014-01-01
$period = new DatePeriod($from, new DateInterval('P1M'), 12);
foreach ($period as $date) {
$lastDay = clone $date;
$lastDay->modify('last day of this month');
$date->setDate($date->format('Y'), $date->format('n'), $day);
if ($date > $lastDay) {
$date = $lastDay;
}
echo $date->format('Y-m-d');
}
Result:
2014-01-29
2014-02-28 #notice the last day of february is used
2014-03-29
2014-04-29
2014-05-29
2014-06-29
2014-07-29
2014-08-29
2014-09-29
2014-10-29
2014-11-29
2014-12-29
2015-01-29
You may try like this:
$date = new DateTime();
$lastDayOfMonth = $date->modify(
sprintf('+%d days', $date->format('t') - $date->format('j'))
);
I would do it probably like this
$max = array (
31,28,31,30,31,30,31,31,30,31,30,31
); //days in month
$month = 1;
$year = 2014;
$day = 31;
$iterate = 12;
$dates = array();
for ($i = 0;$i < $iterate;$i++) {
$tmp_month = ($month + $i) % 12;
$tmp_year = $year + floor($month+$i)/12;
$tmp_day = min($day, $max[$tmp_month]);
$tmp = new DateTime();
$tmp->setDate($tmp_year, $tmp_month + 1, $tmp_day);
$dates[] = $tmp;
}
var_dump($dates);
This keeps to the same day each month if possible

Categories