How to calculate hour intervals from two datetimes in PHP? - php

I am creating an online booking system. when user clicks on a date in calendar, it returns two datetimes (start and end) for that date. I am trying to calculate all the hours from start to end which I was able to do, but I need to display the hours in intervals.
Lets say user has added available time tomorrow from 10.00-14.00 then I need to display the times like this:
10.00-11.00
11.00-12.00
12.00-13.00
13.00-14.00
for the specific day.
What I have so far.
public function getTimes()
{
$user_id = Input::get("id"); //get the user id
$selectedDay = Input::get('selectedDay'); // We get the data from AJAX for the day selected, then we get all available times for that day
$availableTimes = Nanny_availability::where('user_id', $user_id)->get();
// We will now create an array of all booking datetimes that belong to the selected day
// WE WILL NOT filter this in the query because we want to maintain compatibility with every database (ideally)
// For each available time...
foreach($availableTimes as $t => $value) {
$startTime = new DateTime($value->booking_datetime);
if ($startTime->format("Y-m-d") == $selectedDay) {
$endTime = new DateTime($value->booking_datetime);
date_add($endTime, DateInterval::createFromDateString('3600 seconds'));
// Try to grab any appointments between the start time and end time
$result = Nanny_bookings::timeBetween($startTime->format("Y-m-d H:i"), $endTime->format("Y-m-d H:i"));
// If no records are returned, the time is okay, if not, we must remove it from the array
if($result->first()) {
unset($availableTimes[$t]);
}
} else {
unset($availableTimes[$t]);
}
}
return response()->json($availableTimes);
}
How can I get the intervals?

Assuming the hour difference between start and end is 1 as per your question, you could use DateInterval and DatePeriod, to iterate over the times like:
$startDate = new DateTime( '2017-07-18 10:15:00' );
$endDate = new DateTime( '2017-07-18 14:15:00' );
$interval = new DateInterval('PT1H'); //interval of 1 hour
$daterange = new DatePeriod($startDate, $interval ,$endDate);
$times = [];
foreach($daterange as $date){
$times[] = $date->format("H:i") . " -- "
. $date->add(new DateInterval("PT1H"))->format("H:i");
}
echo "<pre>"; print_r($times);
//gives
Array
(
[0] => 10:15 -- 11:15
[1] => 11:15 -- 12:15
[2] => 12:15 -- 13:15
[3] => 13:15 -- 14:15
)
Update
You could use json_encode() in order to return the json times data, as:
$jsonTimes = json_encode($times);

Related

Return the average time difference between dates excluding "Non-Working hours"

I am making a ticketing system for my company. In my database I record the timestamp of when a ticket is first raised and a timestamp of when the ticket is marked as completed.
I have written a function which returns the average time (hrs) a ticket takes to complete:
public function calculateAvgResolveTime()
{
$timeQuery = $this->database->query('SELECT ticketCreated, ticketCompletedOn FROM employeeTickets');
$cumulativeTicketTime = $cumulativeTimes = 0;
while($time = $timeQuery->fetch_assoc()) {
$timeCreated = strtotime($time['ticketCreated']);
$timeCompleted = strtotime($time['ticketCompletedOn']);
if($timeCompleted > $timeCreated) {
$cumulativeTimes++;
$cumulativeTicketTime = $cumulativeTicketTime + ($timeCompleted - $timeCreated);
}
}
$time = ($cumulativeTicketTime / 60 / 60);
$time = sprintf('%02d:%02d', (int) $time, round(fmod($time, 1) * 60));
return $time;
}
Is there a way I could exclude certain hours? For example our office is open from 09:00-17:00 Monday to Friday.
At the moment if a ticket is raised at 16:30 on a Friday and is completed 09:15 on Monday the average time would be quite high when actually the ticket only took 45 minutes of working time.
Result of var_export():
array(
array ( 'ticketCreated' => '2020-02-03 15:59:30','ticketCompletedOn' => '2020-02-04 09:53:35'),
array ( 'ticketCreated' => '2020-02-04 14:00:00', 'ticketCompletedOn' => '2020-02-04 14:36:00')
)
You will have to loop over the dates between ticketCreated and ticketCompletedOn day by day.
There seems to be no mathy way(or at least not in readable format) to solve this as you have time constraints of excluding Saturdays and Sundays as well as the working period being from 09:00:00 to 17:00:00.
Snippet:
<?php
$data =array(
array ( 'ticketCreated' => '2020-02-03 15:59:30','ticketCompletedOn' => '2020-02-04 09:53:35'),
array ( 'ticketCreated' => '2020-02-04 14:00:00', 'ticketCompletedOn' => '2020-02-04 14:36:00')
);
$sum_time = 0;
foreach($data as $details){
$start_time = new DateTime($details['ticketCreated']);
$end_time = new DateTime($details['ticketCompletedOn']);
$end_of_day = new DateTime($start_time->format('Y-m-d') . ' 17:00:00'); // since a day ends at 17:00
do{
$diff = $end_time->diff($start_time);
$diff2 = $end_of_day->diff($start_time);
if($end_time->format('Y-m-d') === $start_time->format('Y-m-d')){ // meaning finished on the same day
$sum_time += ($diff->h * 60) + ($diff->i) + ($diff->s / 60);
}else if(!in_array($end_of_day->format('N'),[6,7])){ // skipping Saturdays and Sundays
$sum_time += ($diff2->h * 60) + ($diff2->i) + ($diff2->s / 60); // add the day's offset(480 minutes)
}
$end_of_day->add(new DateInterval('P1D'));
$start_time = new DateTime($end_of_day->format('Y-m-d') . ' 09:00:00'); // start time for next day which is 09:00
}while($start_time <= $end_time);
}
$avg = $sum_time / count($data);
echo "$avg minutes",PHP_EOL;
Demo: https://3v4l.org/gpFt4
Explanation:
We first create DateTime instances of the dates.
We now have a do while loop inside.
If the end time and start time fall on the same day, we just take differences in terms of hours, minutes and seconds.
If the end time and start time doesn't fall on the same day, then we subtract the times from start_time from end_of_day which will be 480 minutes for a proper start or remaining offset of that day till 17:00:00.
If we come across a day which is Saturday or Sunday, we just skip it.
In the end, we just print the average by dividing sum by total number of tickets.

How to loop over weeks and find the exact date of some days?

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;
}
}
}

Find NOW() within two odd date selections

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') ."'
";

Split time by day of the week with an interval defined by two Zend_Date

I have two Zend_Date that represent an interval :
$start = new Zend_Date($punch->getStart());
$end = new Zend_Date($punch->getEnd());
$nbHours = $start->sub($end , Zend_Date::HOUR);
$nbMinutes = $start->sub($end , Zend_Date::MINUTE);
$hoursTotal = $nbHours->get(Zend_Date::HOUR);
$minutesTotal = $nbMinutes->get(Zend_Date::MINUTE);
Is there an simple way to split the interval by day of the week with Zend_Date when the interval > 24 hours?
For example, if I have an interval from Monday 8am to Tuesday 4:30pm, I would like to have an array containing monday = 16h and tuesday = 16:30.
You don't need to use Zend_Date for this, in fact it is probably better not to. You should use the date/time classes in PHP instead.
If I understand your question correctly you want an array of days and the hours worked for those days.
I first created a mock class to reflect your code example, I have assumed it is returning timestamps:-
class Punch
{
public function getStart()
{
return time();
}
public function getEnd()
{
return strtotime('+36 hours 45 minutes');
}
}
Then we set up the DateTime objects-
$Punch = new Punch();
$start = new DateTime();
$start->setTimestamp($Punch->getStart());
$end = new DateTime();
$end->setTimestamp($Punch->getEnd());
Then we use a DateInterval object to generate our iterable DatePeriod:-
$interval = new DateInterval('PT1M');
$minutes = new DatePeriod($start, $interval, $end);
Then we simply iterate over it counting the minutes worked in each day:-
$format = 'l';
foreach($minutes as $minute){
if(!isset($result[$minute->format($format)])) $result[$minute->format($format)] = 0;
$result[$minute->format($format)]++;
}
See the manual page for acceptable formats.
We now have the number of minutes worked in each day, converting them to hours is trivial:-
foreach($result as $key => $r){
$result[$key] = $r/60;
}
var_dump($result);
Output (Obviously, you will get a different result running it at a different time) :-
array
'Monday' => float 17.483333333333
'Tuesday' => float 19.266666666667
So on Monday 17.48 hours were worked and 19.27 on Tuesday.
Alternatively:-
foreach($result as $key => $r){
$result[$key] = floor($r/60) . ':' . $r % 60;
}
Would give the following output if that is closer to what you want:-
array
'Monday' => string "17:29"
'Tuesday' => string "19:16"
That's the simplest way I can think of doing it.

How to display converted time zones in a 'generic week' (Sunday thru Saturday)?

We are building a scheduling application wherein one user may set his "general availability" for all weeks like the following:
Sunday | Monday | ... | Friday | Saturday
When we ask a person A in India to indicate his "availability", we ask him to select from a drop down of values something like this:
12:00am
12:30am
01:00am
...
11:30pm
We ask him to select BOTH the "From" time (starting) and the "Till" time (ending).
What we SAVE in the database is JUST these values (see the following example):
user_id avail_day from to
1 Sunday 12:00:00 12:15:00
2 Monday 12:00:00 12:15:00
So, in essence, it looks like the following (in his LOCAL time zone)
(A)
Sunday | Monday | ... | Friday | Saturday
-----------------------------------------
| | | | 8:30am to 10:30am
As a separate piece of information, we know that he has selected to work in the IST (Indian Standard Time), which is presently GMT + 5:30 hours, so we can assume that the values he chooses are FOR the time zone he's presently in.
Now, for a person B on the East Coast, which is presently GMT - 4 hours (EDT), this time would be actually
Friday, 23:00:00 to Saturday, 01:00:00
We need help in figuring out how to:
(a) convert the earlier "text value" of the person A in IST to the local value of the EST person (NOTE that we know JUST the day and hours of availability as TEXT values)
(b) AND, then, we need to figure out how to display it on a "Standard week" beginning on a Sunday and ending on a Saturday.
What we want displayed should be something like this:
(B)
Sunday | Monday | ... | Friday | Saturday
--------------------------------------------------------------
| | | 11:00pm to 12:00am | 12:00am to 1:00am
Any smart ways of converting (A) into (B)?
Artefacto's code made into a generic function (Revision 2)
// This function should be used relative to a "from_date"
// The $from_timebegin and $from_timeend MUST be for the same day, not rolling over to the next
function shift_timezones_onweek3($from_timezone, $from_date, $from_timebegin, $from_timeend, $to_timezone)
{
$tz1 = new DateTimezone($from_timezone);
$datetime1 = new DateTime("$from_date $from_timebegin", $tz1);
$datetime2 = new DateTime("$from_date $from_timeend", $tz1);
$interval = $datetime1->diff($datetime2);
$indiaAvail = array(
array($datetime1, $datetime2)
);
$tz2 = new DateTimezone($to_timezone);
//convert periods:
$times = array_map(
function (array $p) use ($tz2) {
$res = array();
foreach ($p as $d) {
$res[] = $d->setTimezone($tz2);
}
return $res;
},
$indiaAvail
);
$res = array();
foreach ($times as $t) {
$t1 = reset($t);
$t2 = next($t);
if ($t1->format("d") == $t2->format("d")) {
$res[$t1->format("l")][] = $t1->format("g:ia") . " to ".
$t2->format("g:ia");
}
else {
$res[$t1->format("l")][] = $t1->format("g:ia") . " to 11:59pm";
$res[$t2->format("l")][] = "12:00am to ". $t2->format("g:ia");
}
}
return $res;
}
Your question doesn't make sense considering weekdays in the vacuum. These weekdays must be actual days, because the time conversion rules change along the year (DST) and through the years (politicians sometimes change the timezones and/or the date in which DST starts/ends).
That said, let's say you have you have a week availability plan for the first week of August, here defined as the week Aug 1 to Aug 7 2010:
<?php
$tz1 = new DateTimezone("Asia/Calcutta");
$indiaAvail = array(
new DatePeriod(new DateTime("2010-08-01 10:00:00", $tz1),
new DateInterval("PT2H15M"), 1),
new DatePeriod(new DateTime("2010-08-07 03:00:00", $tz1),
new DateInterval("PT8H"), 1),
);
$tz2 = new DateTimezone("America/New_York");
//convert periods:
$times = array_map(
function (DatePeriod $p) use ($tz2) {
$res = array();
foreach ($p as $d) {
$res[] = $d->setTimezone($tz2);
}
return $res;
},
$indiaAvail
);
$res = array();
foreach ($times as $t) {
$t1 = reset($t);
$t2 = next($t);
if ($t1->format("d") == $t2->format("d")) {
$res[$t1->format("l")][] = $t1->format("g:ia") . " to ".
$t2->format("g:ia");
}
else {
$res[$t1->format("l")][] = $t1->format("g:ia") . " to 11:59pm";
$res[$t2->format("l")][] = "12:00am to ". $t2->format("g:ia");
}
}
print_r($res);
gives
Array
(
[Sunday] => Array
(
[0] => 12:30am to 2:45am
)
[Friday] => Array
(
[0] => 5:30pm to 11:59pm
)
[Saturday] => Array
(
[0] => 12:00am to 1:30am
)
)
This may put in the same basket weekdays that are actually different days, but there's obviously no way to avoid it without explicitly indicating the day (or adding something like "Saturday (week after)" and "Saturday (week before)". This appears to be what you want, though.
How about this:
Ask user to provide his time zone (maybe you can even detect it by getting the user's location based on IP address, but the user still might want to change it)
Convert the provided time to you webserver's time and save it.
When displaying the time convert to the viewing user's time zone.
The Pear::Date package may help you doing this:
http://www.go4expert.com/forums/showthread.php?t=3494
Hope this helps,
Manuel
Use PHP's build-in DateTime class to do timezone conversions. Save the data in UTC to the database. When displaying your standard week schedule, use local timezone. To handle ±23 hours of timezone differences, you'll have to query 9 days (±1 days) from the DB before conversion.
Edit: To convert times to current user's local time, you need to get the timezone of each event. Join the scheduled events to user information for the user who made the event. This way you'll have the timezone from which to convert to the user's timezone.
The following pseudoish PHP will show:
$usersTZ = new DateTimeZone('EDT');
$now = new DateTime("now", $usersTZ);
$today = $now->format("Y-m-d");
$sql = "SELECT A.date, A.start, A.end, B.tz FROM schedule A JOIN users B ON (schedule.user_id = users.user_id) WHERE A.date BETWEEN '$sunday_minus_1_day' AND '$saturday_plus_1_day' ORDER BY A.date, A.start";
foreach ($db->dothequery($sql) as $event) {
$eventTZ = new DateTimeZone($event['tz']);
$eventStartDate = new DateTime("$today {$event['start']}", $eventTZ);
$eventStartDate->setTimeZone($usersTZ);
$eventEndDate = /* do the same for the end date */
if ($eventStartDate->format("Y-m-d") != $eventEndDate->format("Y-m-d")) {
/* create 2 events */
} else {
/* save the event to list of events with the new start and end times */
}
}
/* sort events, their order may be different now */
Of course, it would all be a lot simpler if you could save the start and end times with TZ to the DB and let the DB do all the hard work for you.

Categories