Split a time range into pieces by other time ranges - php

I have a complicated task that I have been beating my head against the wall now for a few days. I've tried about 4 different approaches, however each seems to stall and it is becoming extremely frustrating.
I have a time range. For example, 14:30:00 until 18:30:00. Consider this time range somebody's work shift. During this time range, they state they cannot work from 15:30:00 until 16:30:00 and from 17:30:00 until 18:30:00. I need to modify the original shift's start and end times to remove the conflicting shifts.
The original shift array looks like this:
$original_shift[0]['start'] = '14:30:00';
$original_shift[0]['end'] = '18:30:00';
And the time ranges to be removed from the original shift look like this:
$subshift[0]['start'] = '15:30:00';
$subshift[0]['end'] = '16:30:00';
$subshift[1]['start'] = '17:30:00';
$subshift[1]['end'] = '18:30:00';
Here is a visualization:
So, I basically need my original shift to look like this when I'm done:
$original_shift[0]['start'] = '14:30:00';
$original_shift[0]['end'] = '15:30:00';
$original_shift[1]['start'] = '16:30:00';
$original_shift[1]['end'] = '17:30:00';
Some complications that I also need to consider are:
These time ranges may be any times (not constrained to the half hour as I have used in my example), however I will know with 100% certainty the the unavailable time ranges will always start and end on or in between the original shift's start and end times.
Unavailable times may butt up and/or take the entire original shift's time up.
I'm not looking for someone to "write my code" as much as I am looking for someone who has dealt with something like this in the past and may have some insight on how they accomplished it.

As you specifically asked for "some insight" rather than a full working answer, I'd personally go with arrays populated with "minutes".
$shift = array(
'start' => '15:30:00',
'end' => '18:30:00',
'original' => array(),
'unavailable' => array(),
'modified' => array()
);
You'd then do some jiggery pokery to convert 15:30:00 into 930 and 18:30:00 into 1110 (number of minutes) which will give you the difference between start and end times.
Use range() to quickly fill up the original array, load in your unavailable in a similar format and then use things like array_intersect() and array_diff() to work out which minutes from the original shift are unavailable.
From that, build up the modified array, and read directly from there to your output.

You need to do calculations of time-ranges. As the image shows this seems like a simple subtraction. It would be nice to just have objects that do these.
I had no code for this ready, so the following concept is a bit rough although probably not that bad.
A Range type that represents a time from-to. Those are as DateTime so that the benefits of these existing types can be used. I didn't use much of the benefits so far, however for the rest of the application this can make sense.
The Range type already contains some basic comparison methods I thought were useful to do parts of the calculations.
However as an object can not divide itself into two I also created a Ranges type which can represent one or more Ranges. This was necessary to have something that can be "divided".
I cheated a little here because I implemented the difference calculation as a member of Range, returning an array with one or multiple Range objects. The final calculation then is just having a shift and substract the unavailable ranges from it:
$shift = new Ranges(new DateTime('14:30:00'), new DateTime('18:30:00'));
$unavailables = new Ranges([
new Range(new DateTime('15:30:00'), new DateTime('16:30:00')),
new Range(new DateTime('17:30:00'), new DateTime('18:30:00')),
]);
$shift->subtract($unavailables);
The shift then spans:
14:30:00 - 15:30:00
16:30:00 - 17:30:00
Demo; Gist
I can not say if it is worth the abstraction, what is nice with DateTime objects is that you can compare them with >, < and =. The real benefit from these classes might shed into light when you need more calculations between Range and Ranges. Maybe the interface is not sweet yet, however the basic calculations behind are outlined in the code already.
One caveat: The difference between 14:00-15:00 and 14:00-15:00 in my code will result to 14:00-14:00. I keep the start time to not run empty, but you can run empty, too. The Ranges object should handle it nicely.

The code should speak for itself:
$original_shift[0]['start'] = '14:30:00';
$original_shift[0]['end'] = '18:30:00';
$breaks[0]['start'] = '14:30:00';
$breaks[0]['end'] = '15:30:00';
$breaks[1]['start'] = '16:30:00';
$breaks[1]['end'] = '17:30:00';
$modified_shift = array(
array('start' => $original_shift[0]['start'])
);
for($x = 0, $y = count($breaks), $z = 0; $x < $y; $x++){
$modified_shift[$z]['end'] = $breaks[$x]['start'];
if($modified_shift[$z]['end'] != $modified_shift[$z]['start']){
$z++;
}
$modified_shift[$z]['start'] = $breaks[$x]['end'];
}
$modified_shift[$z]['end'] = $original_shift[0]['end'];
if($modified_shift[$z]['end'] == $modified_shift[$z]['start']){
unset($modified_shift[$z]);
}

Related

Find dates between 2 dates with custom interval and without overflow

I have a customisable DateInterval object, along with a customisable start and end date. I want to find the dates between the start and end using the interval. I am using Carbon to try and help with this.
Here's the problem:
I the interval is 𝑥 months but the start date is > 28 I cannot control the overflow using CarbonPeriod.
Here is the code I am testing with:
$di = CarbonInterval::create('P1M');
$start = Carbon::parse('31 january 2020')->startOfDay();
$end = Carbon::parse('01 april 2020')->startOfDay();
$period = CarbonPeriod::create($start, $di, $end);
$items = [];
foreach ($period as $item) {
$items[] = $item;
}
I want the above to result in
2020-01-31
2020-02-29
2020-03-30
But I get
2020-01-31
2020-03-02
2020-04-02
Remember, the DateInterval is customisable (or I would just use Carbon::addMonthNoOverflow()).
Can anyone please help with how I achieve what I need to, above?
tl;dr: It doesn't work with Carbon.
Date arithmetics are hard to convey and harder to get right. So for example, you couldn't even express, that you want "the last day of a month" in an interval, when the start date is for example in february:
start: 2020-02-29
P1M -> 2020-03-29 (not end of month, obviously)
An interval alone can't express the semantics that you want the end of month.
And this problem will carry over even if you would find a way to get the overflow working such that you don't end up on the start of the next month. (I have tried some approaches that all failed)
All you can offer (to the user) is to apply extra functions to the array of dates to achieve your goal perhaps, like $item->endOfMonth(). But you would still have to pay attention that the start's day of month is <= 28.
Ironically, you can call ->settings(['monthOverflow'=>false, 'yearOverflow'=>false]) on all Carbon, CarbonInterval and CarbonPeriod, and it has no effect (except when you call addMonth() on it, which is rather disappointing, it won't be applied on $start->add($di)). This comes down to the fact that Carbon is ultimately just a wrapper around the standard DateTime objects, which don't support overflow either.
Long story short, there is no elegant and/or easy solution with Carbon (current version). ;o/
After much digging, and realising that #Jakumi is correct, I have just come up with a solution using Carbon after asking the same question here.
The reason why DatePeriod() does not work is that it is always reliant on the previous date in the loop. That is why you get stuck at 29/28 for february and then repeating throughout the rest of the loop.
Here is my solution:
$endDate = CarbonImmutable::parse('10 april 2020')->startOfDay();
$startDate = CarbonImmutable::parse('31 january 2020')->startOfDay();
$interval = CarbonInterval::create('P1M'); // only works for number of months, not composite intervals
$workingDate = $startDate->copy();
for ($i = 1; $workingDate <= $endDate; $i = $i + $interval->m) {
echo = $workingDate->format('Y-m-d') . "\n";
$workingDate = $startDate->addMonthsNoOverflow($i);
}
I am involved in a bit of a code-off with a contributor of the Carbon codebase. If he finds a better solution then I will update my answer. For now, this works.

Creating a conditional function based on timeframe

As the title suggests, I need to create a conditional function in PHP based on time frame. I am very basic when it comes to knowledge of PHP but can understand concepts.
I have some 3rd party code but I need it to function only between a certain time range and to be essentially disabled when out of range.
I am trying to achieve something as follows:
IF time >=0800<=1700 (
DO NOTHING
) ELSE (
RUN CODE
)
The backstory for the question is I have some 3rd party code to integrate googles dialogflow into a website but it currently runs 24/7, I have a livechat system running from 0800 until 1700 and want googles diaglogflow (chatbot) to run when the livechat isn't running.
I'm not really looking for a straight up answer, but more of the concept behind how I can achieve this so I can better learn for the future.
Thanks,
Martyn
IF time >=0800<=1700 (
DO NOTHING
) ELSE (
RUN CODE
)
First things first, you don't need if else, else alone is fine, in pseudocode:
IF time < 0800 OR TIME > 1700
RUN CODE
Next thing to know will be how to check current time, there's plenty of way to do it so lets think which way will be the best.
We only care about hours and minutes, so lets take them.
$hourNow = date('H'); // $hour now is a string but we'd rather have it as an integer for better comparison.
$hourNow = (int) $hourNow; // this will cast, for example "15" to 15
$minuteNow = (int) date('i'); // as you can see you can do casting and getting a minute value at once
Now when we have all the info we need, it should be easy to write:
if ($hourNow < 8 || ($hourNow === 17 && $minuteNow > 0) || $hourNow > 17) {
// Do whatever you need in this case.
}
The conditional is pretty simple: if hour is less than 8 - we know it's prior to 8 o'clock.
If it's > 17 it's obviously later.
And if it's 17, it's kind of an edge case, as it could be 17:00 - so we need to specify that the minute is greater than 0.
I hope that helps, feel free to ask if something's not clear.

Is this a PHP bug on date?

Doing this on PHP 5.6.14:
$a = strtotime('2016-10-01');
$b = $a+29*24*3600;
$c = $a+30*24*3600;
die(date("d/m/Y",$b).' - '.date("d/m/Y",$c));
will return 30/10/2016 - 30/10/2016
How strange since I expect $c to be 31/10/2016 instead of 30/10/2016. Is it a PHP bug?
Notes: I know there are several ways to do date operations, but I'm asking specifically on this. If I change the $a month to other numbers than 10 (e.g. 8,9,11,12) it gave me expected results.
The way to fix this issue is by adding midday like so:
$a = strtotime('2016-10-01 12:00');
$b = $a+29*24*3600;
$c = $a+30*24*3600;
die(date("d/m/Y",$b).' - '.date("d/m/Y",$c));
The reason why is because you did not include a time so PHP assumed "now" for the time part of strtotime. You then have day light savings in October in your country (well, the country of the server) and this pulled back the closing time value of $c.
Adding midday (12:00) gives you a buffer. It does not need to be 12:00 as 1:00 would have worked as well but personally I like to work with 12:00.

How to convert varied "h m s" Excel time formats?

I have an excel sheet that I am importing but I would like some of the data to be converted down to minutes. Though the formats vary from 5h 5m 6.64s to 6.64s how would i be able to convert this down to only minutes in PHP? (I'm sure DateTime::createFromFormat() wont work as its range is from 0 to 59.)
Also would be converting down to minutes be an easier format to manipulate and graph within a PHP application or converting it to some time object from a PHP class better?
Keep in mind the data has to be formatted, then imported to a MySQL server then read back to a PHP application to graph for statistical purpose. I'm also using cakePHP framework to build the app. Thanks for any feedback.
If all the times have a format like h m s (where any of those are optional), I don't think it would be hard at all to extract the numbers. This could be done with a simple regex like:
/(([0-9]{1,2})h)?(([0-9]{1,2})m)?(([0-9]{1,2}\.[0-9]{0,2})s)?/
And then you can simply put those numbers in PHP's DateTime object. And convert it to a format to store in the database.
If the different parts of the string are always separated by a space, you could simply use:
$timeParts = explode(' ', $timestring); //Separates your time string in parts
//To sum these parts into a total number of minutes:
//First make an array with all multiplication factors to go from part of string to a number of minutes
$factors = array(1/60, 1, 60, 60*24); //Last value is for days, which are probably not necessary.
//Iterate through the $timeParts array
$minutes = 0; //Create $minutes variable, so we can add minutes to it for each string part
while($part = array_pop($timeParts)) { //Process each part of the string, starting from the end (because seconds will always be there even if minutes aren't)
$part = floatval($part); //I guess your numbers will technically be strings, so we need to convert them to floats (because the seconds need to be floats). Also, this function should strip off any letters appended to your numbers.
$factor = array_shift($factors); //Take the first part of the $factors array (because in that array the seconds are first, then minutes and so on)
$minutes += ($part * $factor); //Multiply the string part by its matching factor and add the result to the $minutes variable.
}
I haven't tested this, so you'll need to debug it yourself.

"Hour:Minute" difference from two "Hour:Minute" strings

Simple, eh? -sigh-
A function. Two string arguments ( I guess it doesn't matter the type). Returning a string which is the time difference between the arguments. Think about it as a counter at your local CS internet-caffe.
function time_diff($start, $stop) {
...
return $diff;
}
echo "Your time playing CS: " . time_diff('19:37', '00:05') . ". Go home!";
I tried. Just too embarrassed to say how much time I've invested in this algorithm.
I can't change the format (tell me about it!).
If you happen to know a class, a file or a piece of code from the depths of the internets, I'd be happy to make use of it.
"Thanks" you very much.
You can't do this correctly unless you know:
The timezone.
The day associated with those times.
Once you know that, you can do:
date_default_timezone_set('timezone here');
$seconds_diff = strtotime("2010-08-29 $end") - strtotime("2010-08-29 $start");

Categories