PHP check if time range is in a time range - php

I want to insert a time bounded to someone into the database.
For example: Peter 13:00 - 19:00
Peter told us he can't work from 18:00 - 22:00. (This is located in the database).
so the time 13:00 - 19:00 can't be inserted into the database because the end time (19:00) is within the timerange 18:00 - 22:00.
How to check this?
I've tried several things but I dont know how to do it right.
Got this right now
$van_besch = '18:00';
$tot_besch = '22:00';
$van = '13:00';
$tot = '19:00';
if (($van_besch >= $van) && ($tot_besch <= $tot)) {
$error ='<font color="red">Time '.$van_besch.' - '.$tot_besch.' Not available</font>';
}
This won't work.
Thanks for reading!

There are actually two cases you have to check:
$van is equal or between $van_besch and $tot_besch
$tot is equal or between $van_besch and $tot_besch
if $van and $tot are both between $van_besch and $tot_besch both cases are true.
Further more you need to handle shifts that straddle a day break e.g. 17:00 - 02:00. Another problem is you need to handle is 20:00 - 0:00, since 0:00 is smaller than 20:00.
Therefore we project the real time to our own time format.
That means 17:00 - 02:00 will become 17:00 - 26:00. Note that we need to do this for both $van_besch - $tot_besch and $van - $tot$.
In code that would look be something like:
if ($tot < $van){
$tot = 24:00 - $van + $tot;
}
if ($tot_besch < $van_besch){
$tot = 24:00 - $van + $tot;
}
if (($van >= $van_besch) && ($van <= $tot_besch) ) || (($tot >= $tot_besch) && ($tot <= $tot_besch)){
... Peter is not available ...
}

It is better you convert them and compare using strtotime. Something like this would work for you:
$van_besch = '18:00';
$tot_besch = '22:00';
$van = '13:00';
$tot = '19:00';
if (strtotime($van) < strtotime($van_besch) || strtotime($tot ) > strtotime($tot_besch)) {
//code goes here
}

You can handle this in an SQL query for sure. And if you're just storing time, it would be best to do it as an int 0000 - 2359.
So our table would look something like this.
CREATE TABLE off_time (
id int(12) AUTO_INCREMENT,
employee int(12) NOT NULL,
start_time smallInt(4) unsigned NOT NULL,
end_time smallInt(4) unsigned NOT NULL,
PRIMARY KEY (id)
);
We're storing a start and end time to create a block of time that they don't work. Obviously in your code it's important that you makes sure these blocks make sense and start_time <= end_time.
Now we can query.
// Here we use just < and > but you can make them <= and >= if you don't want time slots to possibly overlap start/end times.
SELECT ot.id, IF( 1300 < ot.end_time AND 1900 > ot.start_time, 1, 0) AS blocked
FROM off_time as ot
WHERE ot.employee = 1;
You will get a table back where each row will have the id of the blocked time slot, and a blocked column with 1 meaning it's blocked and 0 meaning it's not. So if any of the entries are 1, then you know your time is blocked out. Doing it this way is also handy because you can also relate a reasons table, and be able to respond with a reason. LEFT JOIN time_off_reasons as r on r.id = ot.id
If you want to be specific with dates. Just store timestamp for start and end times. The comparison will be the same, just with timestamps instead.
EDIT
Marc B. made a good point in the comments about time blocks that stretch over the midnight mark i.e. 2200 - 0200. With my method you would want to make sure you split those apart as separate entries.
So INSERT INTO off_time (start_time, end_time) VALUES(2200, 2400)(0000,0200)
This will still work as expected. But you'll just have two entries for 1 time off block, which might be annoying, but still easier than writing extra code to compensate for the logic.

Related

Calculating the availiability of things in a time range

the following situiation:
We have 3 things available at the same time.
The first is booked from 09:00 to 11:00.
The second is booked from 11:00 to 13:00
I want to calculate how much things are available from 10:00 to 12:00.
I've done it calculating how much things are booked in the range from 10:00 to 12:00.
It was 2. So the available things are 1.
But the first and the second could be the same. So the available things in the designated time are 2 and my calculation was wrong.
So I created the following algorithm to calculate the occupied things in a time range. the results came from a database query and contains the booking of things which are in my time range (result is an array and contains start,end and cnt which is the amount of booked things):
$blocks = array();
$times = array();
foreach($results as $result){
$block = array();
$block['start'] = DateTime::createFromFormat("Y-m-d H:i:s", $result['start'])->getTimestamp();
$block['end'] = DateTime::createFromFormat("Y-m-d H:i:s", $result['end'])->getTimestamp();
$block['things'] = $result['cnt'];
$blocks[] = $block;
$times[] = $block['start'];
$times[] = $block['end'];
}
$times = array_unique($times);
$times = array_values($times);
$peak = 0;
foreach($times as $time){
$timePeak = 0;
foreach($blocks as $block){
if($time >= $block['start'] && $time <= $block['end']){
$timePeak += $block['things'];
}
}
if($timePeak > $peak){
$peak = $timePeak;
}
}
return $peak;
}
whith this method, I am creating timestamps for every start and endtime of every booking in this range.
Then I have calculated the sum of all bookings of every timestamp. The maximum of the calculations (peak) was the maximum amount of bookings. So the available things are max - peak.
It works.
But is there a more elegant way to calculate it?
Sounds similar to this common coding interview problem: https://www.geeksforgeeks.org/find-the-point-where-maximum-intervals-overlap/
If your "max things" is hardcoded then yeah I think you find the maximum booking of things during the given time and subtract from the max to get minimum availability over the range. To do this, you need to consider all bookings that either start or end in your range. As the efficient solution on that link suggests, you sort the starts and ends and then go through them to see your running utilization.
To handle the boundary case you talked about where a booking ending lines up with a booking start, make sure your sorting of "starts" and "ends" always sorts an "end" before a "start" when the time is the same so that you don't get the appearance of an overlap where one doesn't really exist.
I'm assuming you are working on a table like this:
CREATE TABLE `bookings` (
`id` int(11) unsigned NOT NULL AUTO_INCREMENT,
`start` datetime NOT NULL,
`end` datetime NOT NULL,
PRIMARY KEY (`id`)
);
And, you have data like this:
INSERT INTO `bookings` (`id`, `start`, `end`)
VALUES
(1, '2019-05-05 09:00:00', '2019-05-05 11:00:00'),
(2, '2019-05-05 11:00:00', '2019-05-05 13:00:00'),
(3, '2019-05-05 07:00:00', '2019-05-05 10:00:00'),
(4, '2019-05-05 12:00:00', '2019-05-05 14:00:00'),
(5, '2019-05-05 14:00:00', '2019-05-05 17:00:00'),
(6, '2019-05-05 10:30:00', '2019-05-05 11:30:00');
This data has all the relevant cases you need to consider:
Bookings that end before your range
Bookings that start after your range
Bookings that intersect the start of your range
Bookings that intersect the end of your range
Bookings contained entirely within your range
Bookings that "hand-off" within your range
I looked at making a SQL query for this but stopped when it got more complicated than you would actually want to code in SQL. I think it's probably do-able but requires really ugly SQL. Definitely do this in code. I think the algorithm is to load all bookings relevant to your range with a query like:
SELECT * FROM bookings
WHERE start BETWEEN :start AND :end
OR end BETWEEN :start AND :end
That's all the bookings that will possibly matter. Then you sort the start and end events as distinct events as described earlier and loop over the list of events keeping a running counter going as well as a max value seen so far. The running count will first go up and at the end it will go back down to zero, but may have multiple peaks in the middle. So, you can't just stop the first time it goes down, you have to keep scanning until you get through all the events (or at least to your end time, but simpler and no real cost to just go all the way through the list). Once done, you've got the maximum number of concurrent bookings.
I haven't tested or compiled this, so take it as psuedo-code. I'd change your code to something like this:
//assumedly, prior to this point you have a
//$startTime and $endTime that you used to do the query mentioned above
//and the results of the query are in $results
$times = array();
foreach($results as $result){
$time = array();
$time['time'] = DateTime::createFromFormat("Y-m-d H:i:s", $result['start'])->getTimestamp();
$time['change'] = $result['cnt'];
$times[] = $time;
$time['time'] = DateTime::createFromFormat("Y-m-d H:i:s", $result['end'])->getTimestamp();
$time['change'] = -1 * $result['cnt'];
$times[] = $time;
}
usort($times, function($lh, $rh) {
if ($lh['time'] === $rh['time']) {
return $lh['change'] - $rh['change']
} else {
return $lh['time'] < $rh['time'] ? -1 : 1;
}
}
$maxPeak = 0;
$curVal = 0;
foreach($times as $time){
//breaking early here isn't so much about optimization as it is about
//dealing with the instantaneous overlap problem where something starts
//right at the end time. You don't want that to look like a utilization
//that counts against you.
if ($time['time'] === $endTime) {
break;
}
$curVal += $time['change'];
if ($curVal > $maxPeak) {
$maxPeak = $curVal;
}
}
//$maxPeak is the max number of things in use during the period

how to know the minimum period of week for two date, increasing by different frequence, to overlap

to start im sorry for my bad english and i hope that you can understand my problem and finding a solution for it....
my problem is that i have two period
the first period: dateStart1-dateEnd1
the secondperiod: dateStart2-dateEnd2
for the first couple the frequence = 2 :
dte=dateStar1;dateEnd1>dte;dte+2week
for the second, the frequence = 3 :
dte=dateStar2;dateEnd2>dte;dte+3week
Exemple:
first period 2016-04-04 -> 2016-05-09
frequence 2 weeks 2016-04-04 , 2016-04-18 , 2016-05-02
the second : 2016-04-11 -> 2016-05-09
frequence 3 weeks 2016-04-11, 2016-05-02
the two periods overlaps in 2016-05-02
my question in how to know the minimum number of weeks for the two period or dates to overlaps ?
thank you
There is probably a better algorithm, but the only thing I can think of is a check on all available weeks for a period.
$result = [];
$current = clone $dateStart1;
while ($current < $dateEnd1) {
// Calculate the difference betweenthe current date and dateStart2
$diff = $current->diff($dateStart2);
// Check if the difference is a multiple of the frequency, i.e. if the number of days is congruent to 7 times the frequency
if ($diff->days % ($frequency2 * 7) == 0) {
// If the anwser is yes, adds the current date to the list of overlapping dates.
$result[] = clone $current;
}
// Get the next date possible for period 1 by adding $frequency1 weeks to the current date, and then try again till $dateEnd1 is reached.
$current = $current->add(new \DateInterval('PT'.$frequency1.'W'););
}
return $result;
I haven't fully tested it, but this might at least help you to get on the rails.

Calculating different bands of overtime in php

This is my first time posting here so I'm sorry if I get something wrong. I'm trying to calculate how many hours overtime a worker has worked based on when they signed in. The problem is that we have different bands of overtime:
If the worker works between 5 and 7 then it's 25% extra per hour
If they worked between 7pm and 10pm then its 50% extran for each hour
If the worker works between 10 and 12 then it's 75% extra
If the worker works between 12am and 7am is 100% more
I need to count how many hours they worked at each of the overtime bands
$number_of_25_percent_hours=0;
$number_of_50_percent_hours=0;
$number_of_75_percent_hours=0;
$number_of_100_percent_hours=0;
$clockInTime=$arr['4'];
$clockOutTime=$arr['5'];
$startingPieces=explode(':',$clockInTime);
$startingHour=$startingPieces[0];
$finishingPieces=explode(':',$clockInTime);
$finishingHour=$finishingPieces[0];
//Regular hours are between 7am and and 5pm
//If the worker works between 5 and 7 then it's 25% extra per hour
if(($startingHour<=5)&&($finishingHour>=6)){$number_of_25_percent_hours++;}
if(($startingHour<=6)&&($finishingHour>=7)){$number_of_25_percent_hours++;}
The problem with using the lines above is that it does not work if for example they worked an hour from 6:30 to 7:30.
I'm interested in finding other ways to do this.
you need to store the data more exactly. From your script it looks as if you were only saving the starting hour - which propably is a full number (1,2,3,4 whatsoever)
You script however needs a exact time representation. There are surely many ways to do this but for the sake of a better Script (and as you will propably be able to use some of these more exact values later on) I'd recommend you to store it as a UNIX Timestamp, then get the hour of the Timestamp :
$startingHour = date('H' $timeStampStored)
and check if it's in any of your "bonus" segments. If the user started working at 6:30, the value will hold 6.
This code is completely off the top of my head, untested etc. It's intended as a suggestion of one method you might use to solve the problem, not as a robust example of working code. It uses integers instead of dates, relies on array data being entered in order etc, and probably wouldn't even run.
The basic idea is to set up the scales for each level of overtime multiplier, as well as the hours for non-overtime pay in an array, then loop through that array checking how many hours of each level of overtime have been worked between the inputted times, meanwhile keeping track of a total billable hours value.
$PayMultipliers = array();
$PayMultipliers[0] = array(17,19,1.25);
$PayMultipliers[1] = array(19,22,1.5);
$PayMultipliers[2] = array(22,24,1.75);
$PayMultipliers[3] = array(0,7,1.5);
$PayMultipliers[4] = array(7, 17, 1);
$Start = 3;
$End = 11;
$TotalHours = 0;
for($i = 0; $i <= count($PayMultipliers); $i++)
{
if($Start > $PayMultipliers[$i][0] && $Start < $PayMultipliers[$i][1])
{
$TotalHours += ($PayMultipliers[$i][1] - $Start) * $PayMultipliers[$i][2];
$Start = $PayMultipliers[$i][1];
}
}
echo $TotalHours;
If you want to calculate from 6:30 to 7:30 you'll have to caclulate in minutes, not hours. You can convert the hours and minutes to timestamps, check each time period, and then convert the seconds back to hours.
<?php
$number_of_overtime_hours = array();
$clockInTime = "18:30:00";
$clockOutTime = "19:30:00";
$startingPieces = explode(':',$clockInTime);
$finishingPieces = explode(':',$clockOutTime);
//Timestamps
$startTimestamp = mktime($startingPieces[0],$startingPieces[1],$startingPieces[2]);
$finishTimestamp = mktime($finishingPieces[0],$finishingPieces[1],$finishingPieces[2]);
//finish after 0h
if ($finishTimestamp < $startTimestamp){
$finishTimestamp += 3600 * 24;
}
//set starting and ending points
$overtimePeriods = array(
25 => array (17,19),
50 => array (19,22),
75 => array (22,24),
100 => array (24,31)
);
$overtimeWork = array();
foreach ($overtimePeriods as $key => $val){
//create Timestamps for overtime periods
$beginTimestamp = mktime($val[0],0,0);
$endTimestamp = mktime($val[1],0,0);
//calculate hours inside the given period
$overtimeWork[$key] = (min($finishTimestamp,$endTimestamp) - max($startTimestamp,$beginTimestamp)) / 3600;
//negative values mean zero work in this period
if ($overtimeWork[$key] < 0) $overtimeWork[$key] = 0;
}
var_dump($overtimeWork);

Check if appointment time fits users working time with SQL - Wrong proposal during night

I defined hours where an employee is working aka working hours, e.g.
$start = "09:00:00";
$end = "18:00:00";
A user can now select an appointment, and the query should validate it, e.g.
$appointment_start = "09:00:00";
$appointment_end = "10:00:00";
My SQL query should now check if the employee is working at that specific time. If that is the case, propose this appointment to the user. This is how I do it:
...
AND w.start <= '$appointment_start'
AND w.end >= '$appointment_end';
There seems to be a problem during night when the day changes, e.g. when start is 23:00:00 and end is 00:30:00. This should not be a valid appointment time, but my query proposes it:
start substr: 23:00:00 || end substr: 00:00:00
start substr: 23:30:00 || end substr: 00:30:00
How do I have to change the WHERE statement of my SQL query to fix this issue?
You could use a datetime rather than just a time to avoid this issue altogether.
Or you could do something like:
AND (( w.start < w.end
AND w.start <= '$appointment_start'
AND w.end >= '$appointment_end') OR
( w.start > w.end
AND w.start >= '$appointment_start'
AND w.end <= '$appointment_end' ) )
Basically, you invert your comparison operators when start happens after end.
If you are unable to use a datetime you could massage your values.
$appointment_end = ($appointment_end < $appointment_start) ? $appointment_end + [24hours] : $appointment_end;
$end = ($end < $start) ? $end + [24hours] : $end;
Basically if the end time is less than the start time, assume it's the next day and add 24 hours, then do the check as normal. (not sure of the syntax in php to add the [24hours])

Querying for a range of days and hours

I want to be able to calculate if something is within a range and return all results.
We have a table that has scheduled weekly activities. Each row of the table has an unique id, a day number, and an hour number.
id (auto int) | dayNumber (int from 0 to 6) | hourNumber (int from 0 to 23)
Based off of this table, we can determine when an event is supposed to occur on the schedule. For example, an entry of dayNumber=2 and hourNumber=3 would mean that an event is scheduled for Tuesday at 3 AM.
The part that I am finding difficult is that I need to run a query to get a range of scheduled events. So I will pass in a minimum dayNumber and hourNumber and a maximum dayNumber and hourNumber. My expected results will be every matching entry in that range.
The easier case is when the minimum is less than the maximum. For example (in PHP),
$minimumDayNumber = 1; $minimumHourNumber = 3;
$maximumDayNumber = 5; $maximumHourNumber = 6;
Where it gets more complicated is when I want to search the range from Saturday to Tuesday. For that, the variables would look something like this:
$minimumDayNumber = 6; $minimumHourNumber = 3;
$maximumDayNumber = 2; $maximumHourNumber = 6;
Using these inputs, I would need to query the database and find all entries within that range. There must be a good algorithm to either construct the query or a good query to do this fairly directly.
This seems easy on paper and I have done it with date ranges (actual date fields), but I just cannot get this to work correctly. Thank you in advance for your help and let me know if you need additional clarification.
Try this
where (($maximumDayNumber > $minimumDayNumber and (dayNumber+hourNumber/24) >= ($minimumDayNumber+$minimumHourNumber/24) and (dayNumber+hourNumber/24) <= ($maximumDayNumber+$maximumHourNumber/24))
or ($maximumDayNumber < $minimumDayNumber and ((dayNumber+hourNumber/24) <= ($minimumDayNumber+$minimumHourNumber/24) OR (dayNumber+hourNumber/24) >= ($maximumDayNumber+$maximumHourNumber/24))))
Will this work?
SELECT * FROM activities
WHERE IF( $minDayNum = $maxDayNum,
( dayNumber = $minDayNum AND hourNumber >= $minHourNum AND hourNumber <= $maxHourNum ),
(
IF( $minDayNum > $maxDayNum,
( dayNumber > $minDayNum AND dayNumber < $maxDayNum ),
( dayNumber > $minDayNum OR dayNumber < $maxDayNum )
)
OR ( dayNumber = $minDayNum AND hourNumber >= $minHourNum )
OR ( dayNumber = $maxDayNum AND hourNumber <= $maxHourNum )
)
)
Pardon the rubber duck:
The first IF runs a check to see if the day is the same
If so, a simple where clause will narrow down the hours of the day
If not, we go to another if statement to see if we are traversing weeks
The conditional pulls in all events between the range (Not equal to), conscious of week endings
Last two OR conditions pull in the specific start/end day/time events
I didn't mock anything up, let me know how it works out.

Categories