Laravel: Skip time slots from Carbon\CarbonPeriod some time ranges - php

I'm developing a booking system for a various packages having different durations for each. The customer should get available time slots from a selected date. The date may contain different package bookings with different time intervals. So, What I'm trying to do is to extract available time slots on the date by skipping all booked time slots.
Expected output is:
08:00 AM
10:30 AM // 09:00 AM has a booking. and its duration is 1.5 hours
11:30 AM // The current package has 1 hour duration
... until the shop closes for eg 08:00 PM
I'm using CarbonPeriod to get list of time slots. But, I can't apply the filters to skip all booked time slots.
$hours = new CarbonPeriod(
$opening,
$this->duration() . ' minutes',
$closing->subMinutes($this->duration())
);
$hours->filter(function ($date) use ($booked) {
$toSkip = [];
foreach ($booked as $bookedItem) {
$bookingTime = Carbon::parse($bookedItem->time);
$completingTime = Carbon::parse($bookedItem->time)->addMinutes($bookedItem->package->duration());
if ($date->isBetween($bookingTime, $completingTime)) {
array_push($toSkip, $date);
}
}
return !in_array($date, $toSkip);
});
Any help would be highly appreciated!

If you don't want to reinvent the wheel, this sounds like a job for spatie/opening-hours or the version dedicated to Carbon: cmixin/business-time
Definition looks like:
BusinessTime::enable(Carbon::class, [
'monday' => ['09:00-12:00', '13:00-18:00'],
'tuesday' => ['09:00-12:00', '13:00-18:00'],
'wednesday' => ['09:00-12:00'],
'thursday' => ['09:00-12:00', '13:00-18:00'],
'friday' => ['09:00-12:00', '13:00-20:00'],
'saturday' => ['09:00-12:00', '13:00-16:00'],
]);
Then with CarbonPeriod, use the custom step:
$period = CarbonPeriod::create(
$opening,
static fn ($date) => $date->nextOpen(),
$closing,
);
foreach ($period as $slot) {
echo "$slot\n";
}
You also can easily convert AM/PM string to h0-23 format with:
Carbon::parse('11:20 PM')->format('H:i')

Related

PHP, repeating events check next one with unix timestamps

I have a problem with php that i don't really know how to solve. I have an array full of unix timestamps coming from a mysql query.
These timestamps are events that repeat every week ( For example, every Tuesday and Thursday ). They can repeat various days or just one.
Knowing the days that repeat, which day will be the next one.
For example:
In the Array I have :
1595289600 --> 2020/07/21 (Tuesday)
1595116800 --> 2020/07/19 (Sunday)
Today we are at 1595376000 (Wednesday) , so it should return 1595116800 + 604800 (Sunday).
In 5 days ( next monday) it should return 1595289600 + 604800 = 1595721600 (First tuesday + one week )
in one week (next Wednesday) , it should return the next Sunday (2020/08/02 ): 1596326400
And so on...
Thank you!
For every timestamp you have - calculate next timestamp (add a week) until it is after current timestamp. Then return lowest from those as that one will be the closest to now (but also in the future).
So lets say it is 2020-07-22 Wednesday.
Your 2020-07-21 Tuesday is in the past, so add a week: 2020-07-28 Tuesday - its in the future, so its our candidate.
Your 2020-07-19 Sunday is also in the past, so add a week: 2020-07-26 Sunday - its in the future so its out second candidate.
Now pick lower from 2 candidates: 2020-07-26 Sunday.
If the dates are more in the past then you will need more a week to them more times.
Something like this:
<?php
// those are your timestamps: $timestamps = [1595289600, 1595116800];
// $time is optional int for when you want to perform the calculation. defaults to current timestamp
function nextOccurence(array $timestamps, $time = null) {
$now = $time ?? time();
$nextTimestamps = [];
foreach ($timestamps as $timestamp) {
while ($timestamp < $now) {
$timestamp += 604800;
}
$nextTimestamps[] = $timestamp;
}
return min($nextTimestamps);
}

ical file recurring events

I have created a PHP script to read an ical file from my mailserver. This script is used to plan events in my PBX so when people call and I'm out of office it automatically redirects them to voicemail. This works perfectly for one time events, but I would like it to work for recurring events.
This is the result of the script:
[BEGIN] => VEVENT
[DTSTAMP] => 20170920T120736Z
[UID] => 80462778A326E04EBD831336D01F2A2F179EBCBCC8BD7A45954DE9CF
[DESCRIPTION] => \n
[PRIORITY] => 5
[SUMMARY] => Summary
[CLASS] => PUBLIC
[LOCATION] => Place
[X-ALARM-TRIGGER] => -PT30M
[DTSTART] => 20170912T083000
[DTEND] => 20170912T173000
[RRULE] => FREQ=WEEKLY;BYDAY=TU
[END] => VEVENT
I can recognize a recurring event by the "RRULE" parameter, but thinking it through the script would have to calculate the recurring date and that would take a lot of CPU I guess. Especially since my ical file already has over 1800 events and these would all have to be checked. Then there is also the question on how to check this, because recurring events can be daily, weekly, monthly, yearly, and that is without intervals, e.g. every other week.
Any idea on how to go about this?
Give something like this a try
<?php
$recur = 'FREQ=WEEKLY;BYDAY=TU';
$ex = explode(';', $recur);
$freq = str_replace('FREQ=','',$ex[0]);
$day = str_replace('BYDAY=','',$ex[1]);
$dates = [];
switch ($freq) {
case 'WEEKLY':
$date = new DateTime(); // set to correct day obvs
$dates[] = $date;
for ($x = 0; $x <= 52; $x ++) {
$date = clone $date;
$date->modify('+1 week');
$dates[] = $date;
}
break;
}
foreach($dates as $date) {
echo $date->format('Y-m-d')."\n";
}
You'll need to code the rest yourself, but this generates a bunch of dates one week apart from each other.
See it working here https://3v4l.org/Dgriv

How to calculate hour intervals from two datetimes in 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);

How to find earliest upcoming date given a list of weekdays?

Dates in PHP are a nightmare for me so please help me out fellow coders... I want to notify customers about the day their order will be delivered. It works like this:
I have 2 shipping zones, A & B. Orders for zone A are delivered each Monday, Wednesday & Friday, whereas zone B is on Tuesday, Thursday, Saturday. For each order, the delivery day is scheduled for the NEXT AVAILABLE day, depending on the zone. Please consider that if someone places an order on Monday the goods will be delivered on the NEXT available date, that would be Tuesday for zone B and Wednesday for zone A.
How can I calculate the NEXT AVAILABLE delivery date and notify the customer?
Thanks.
This will certainly not be the fastest or most clever answer, but it's going to be a pleasure to read the code.
Assuming we are shipping in zone A:
$dates = array(
new DateTime('next monday'), // magic!
new DateTime('next wednesday'),
new DateTime('next friday'),
);
// Seems to work because since PHP 5.2.2 DateTime objects
// can be compared with the < and > operators
$shippingDate = min($dates);
echo $shippingDate->format('Y-m-d');
You might want to take a look at the relative date formats available in PHP, this is the part where the "next monday" magic happens. For information on what you can do with $shippingDate, see the documentation on class DateTime.
Update
For completeness, here is a more old-school version which does not need PHP 5.3 and should also be faster (although speed is practically irrelevant here). I don't like it as much, because it's not easy to verify that it works correctly. In contrast to the version above, this one had a bug when I first wrote it. Simple is good.
$today = date('w');
// These values are from http://www.php.net/manual/en/function.date.php
$shippingDays = array(
1, // mon
3, // wed
5, // fri
);
// Each of these weekdays is how many days into the future?
foreach($shippingDays as &$day) {
$day = (7 + $day - $today) % 7;
}
// Get the earliest one, but not if it's today
// array_filter is used to remove a possible 0
$daysInFuture = min(array_filter($shippingDays));
$shippingDate = new DateTime('+'.$daysInFuture.' days');
echo $shippingDate->format('Y-m-d');
See it in action.
Try this:
// Info
$date = array(date("d"), date("m"), date("Y"));
$zone = "A";
// ------
$zones = array("A" => array(1 => "Monday",
3 => "Wednesday",
5 => "Friday")
,"B" => array(2 => "Tuesday",
4 => "Thursday",
6 => "Saturday"));
$found = false;
$days_plus = 1; // always next day
// Retrieve last day from the zone
end($zones[$zone]);
$last_day = key($zones[$zone]);
do {
$mk = mktime(0, 0, 0, $date[1], ($date[0] + $days_plus), $date[2]);
$week = date("w", $mk);
// if week not passed last day of zone
if ($week <= $last_day)
{
if (!isset($zones[$zone][$week]))
{
$days_plus++;
}
else
{
$found = true;
}
}
else
{
$days_plus++;
}
} while (!$found);
echo "Next date: " . date("d/m/Y - l", $mk);
$timestamp = strtotime('next Monday');
$date = date('Y-m-d', $timestamp);

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