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
Related
A time attendance system can have many shifts.
For instance:
Shift a) 08:00 - 16:00,
Shift b) 16:00 - 00:00,
Shift c) 00:00 - 08:00,
A user starts working at 07:55 what is the best way to match this user with the correct shift which is shift a?
Keep in mind that the time attendance system may have many shifts much closer together, for instance a
shift that starts at 8:00 and a shift that starts at 9:00.
Important info:
What i have done is a foreach loop that compares all starting times of the shifts (in our example 09:00, 16:00, 00:00) with the time that user started working. In our example 7:55.
The one that is closer to the users start working time is the correct shift.
This looks like its ok but in reality its not. The reason is that when time is round 00:00:00 and since times of shifts do not have a date, when the comparison is 23:59:59 and 00:00:01 i get 86400 secs instead of just 2 secs.
Additional you never know which date is greater than the other, because a user may come earlier for work or late.
So any ideas must take these into consideration.
Thanks for efforts
I've updated the answer based on the comment, but there is not enough information in the question to show you how to query your database and convert your shifts into the array I'm using in this example.
This codeblock is reference, not code to use. This is the array of start times you need to convert your database table to.
$shift_starts = [
// 1 represents the ID of the shift in your database.
1 => [
// Shift ID 1 starts at midnight, hour 0, minute 0
[0, 0],
],
// 2 represents the ID of the shift in your database.
2 => [
// Shift ID 2 starts at 8am, hour 8, minute 0
[8, 0],
],
// 3 represents the ID of the shift in your database.
3 => [
// Shift ID 3 starts at 4pm, hour 16, minute 0
[16, 0],
],
];
Create a function, something like this. Again, I don't know how you are querying your database, nor the schema. I just know how you are storing the start times:
// Psudeo Code!!! Study and write your own function that returns
// the array as defined above.
function get_shift_start_array() {
$shift_starts = [];
$result = mysqli_query($db, "SELECT * FROM shifts ORDER BY start_time");
while ($row = mysqli_fetch_row($result)) {
// If the start_time is formatted h:m:s, then make it so you can get hours
// and minutes into their own variables:
$arr = explode(':', $row['start_time']);
$hour = $arr[0];
$minute = $arr[1];
$shift_starts[$row->id] = [$hour, $minute];
}
return $shift_starts;
}
Now that we have a way to get your shift data into a format we can code around, this function will take a unix timestamp and return the database ID of the shift. (Notice this function calls the function above)
Read the comments and study the PHP functions you might not understand.
/**
* Get the shift ID for a specific time.
*
* #param int $punchin_time Unix timestamp Default is the current time.
* #return int The shift id (the array key from $shift_starts)
*/
function findShift($punchin_time = null): int
{
if ($punchin_time === null) {
$punchin_time = time();
}
// Call the psudo code function to get an array of shift start times keyed by shift id.
$shift_starts = get_shift_start_array();
// Set $day to the unix timestamp of midnight yesterday.
$day = strtotime(date('Y-m-d', $punchin_time - 86400));
// We'll be checking the difference between punchin time and the shift time.
// $last_diff will be used to compare the diff of the current shift to the last shift.
// Initialize this with an arbitrarily high value beyond the 3 days we're looking at.
$last_diff = 86400 * 5; //
$last_index = null;
// Loop over 3 days starting with yesterday to accommodate punchin times before midnight.
// Return the shift ID when we find the smallest difference between punchin time and shift start.
for ($i = 0; $i <= 3; $i++) {
// Get the month, day, and year numbers for the day we are iterating on.
// We will use these in our calls to mktime()
$m = date('n', $day);
$d = date('j', $day);
$y = date('y', $day);
// Loop over each shift.
foreach ($shift_starts as $index => $start) {
// Create a unix timestamp of the shift start time.
// This is the date and time the shift starts based on the day iteration.
$time = mktime($start[0], $start[1], 0, $m, $d, $y);
// Get the difference in seconds between this shift start time and the punchin time.
$diff = abs($punchin_time - $time);
// $diff should be getting smaller as we get closer to the actual shift.
if ($diff > $last_diff) {
// If $diff got bigger than the last one, we've past the shift.
// Return the index of the last shift.
return $last_index;
}
$last_index = $index;
$last_diff = $diff;
}
$day = strtotime('+1 day', $day);
}
// Return null if no shift found.
return null;
}
Now that the functions are defined, you just need to call the last one to convert specific time, to a shift.
$punchin_time = mktime(15, 55, 0, 4, 15, 2020);
$shift_id = findShift($punchin_time);
Alternatively, don't pass a time in and the current time will be used.
$shift_id = findShift($punchin_time);
mktime
strtotime()
DateTime::getTimestamp()
I have a cron job that gets results from the DB to check it the interval set by user falls on today's date. I am currently thinking of doing it as below :
Get the time column for the row. Ex:2017-05-25 00:00:00
Get the frequency set. Ex:Every 2 weeks.
Get the current date in above format. Ex:2017-05-31 00:00:00
Get the difference in days. Ex:6 days.
Convert the frequency set to days. Ex:2 weeks = 14 days;
Divide (difference in time(days)) by (frequency in days). Ex:6/14
This way I will only get the result to be true when 2 weeks have passed since the time set. I.e., 14/14, 28/14, 42/14,...
If the frequency is in months, I can start dividing by 30. But somehow this feels like a hacky way of doing it. So my question is if there is better way of doing this calculation to check the difference.
This is what I have done as explained by above example.
` $frequency = ; // Get the relevant fields from db
$today = date(Y-m-d H:i:s);
foreach ($frequency as $key => $value) {
$frequency_in_days;
$frequency_type = $value->type;
$frequency_repeat = $value->repeat;
if($frequency_type == 1){
$frequency_in_days = $frequency_repeat;
} elseif($frequency_type == 2) {
$frequency_in_days = $frequency_repeat * 7;
} elseif($frequency_type == 3) {
$frequency_in_days = $frequency_repeat * 30;
} elseif($frequency_type == 4) {
$frequency_in_days = $frequency_repeat * 365;
}
// Get number of days spent between start_date and today in days.
$interval = date_diff($value->start_date, $today)->format('%a');
$result = $interval % $frequency_in_days;
if ($result == 0) {
// Frequency falls today! Do the job.
}
}`
Note: The cron job runs this script. The script again needs to check if the today falls under the frequency set.
Also for argument's sake, is this the best logic to calculate the difference?
Thank you.
This will work
Table "schedule"
`last_run` timestamp,
`frequency_seconds` int
example query for tasks that should go every two weeks:
SELECT *
FROM schedule
WHERE TIMESTAMPDIFF(last_run, NOW()) >= frequency_seconds
after fetching rows update last_run to NOW()
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.
I am generating reference no REF-082013-001 (REF-mmyyyy-001) and incrementing the last part for every entry. REF-082013-001 for first record and REF-082013-002 for second and so on.
My Question is How can i reset the last number by php for every new month. Let say September 2013 I want it to be REF-09-2013-001 and auto increment it till the end of September and then in November reset it again.
Is there a ways to do this in php. Your help is much appreciated. Thank you
Update
Currently 'REF-'.date('m-Y').$maxnumberfromdb in single column called reference_no and Now thinking to store the mm-yyyy ref1 and last number in ref2 separately and start ref2 every first day of the month.
You can probably go with a single table, with an AUTO_INCREMENT field to handle the last part of the reference number, and a date/time field to track when it was last reset.
CREATE TABLE track_reference
ref_number INT(11) AUTO_INCREMENT,
last_reset DATETIME;
Then you write a function in PHP to get a new reference number, which (pseudo-code):
if (MONTH(time()) > MONTH(last_reset)) {
reset ref_number to 0 (use ALTER TABLE);
}
select (ref_number) into $variable;
return 'REF_<month><year>_$variable;
}
It's rough, but you get the idea. I'd also have YEAR appear before MONTH for making sorting by reference number easier later.
This is my code for reset counter and update last_reset, this is effective for me
<pre>
$date2 = new DateTime("now", new DateTimeZone('America/New_York') );
$month_end = strtotime('last day of this month', time());
$count = $data['sim_tarif_count'];
$date3=$date2->format('Y-m-d');
foreach ( $count as $r2 ){
$counts[] = $r2['count'];
$last_reset[] = $r2['last_reset'];}
$paddedNum = sprintf("%04d", $counts[0]);
$reg_id = 'SEQ'.$date2->format('Ymd').$paddedNum;
echo " ";
echo date('j', $month_end);
$counter = $counts[0]+1;
//for reset
if($date2->format('Y-m') > $last_reset[0])
{
$counter = 0;
$counting_update=$this->docs_model->update_time_counter($counter,$date3);
echo $counter = 0;
} else
{
$counting_update=$this->docs_model->update_time_count($counter);
}
</pre>
I had this problem some years ago and back then I implemented a "different logic" in order to deliver the project but the doubt remains in my mind and hopefully with your help I'll be able to understand it now.
Suppose I have some scheduled events on my database that may or may not spawn over several days:
id event start end
-----------------------------------------------
1 fishing trip 2009-12-15 2009-12-15
2 fishCON 2009-12-18 2009-12-20
3 fishXMAS 2009-12-24 2009-12-25
Now I wish to display the events in a calendar, lets take the month of December:
for ($day = 1; $day <= 31; $day++)
{
if (dayHasEvents('2009-12-' . $day) === true)
{
// display the day number w/ a link
}
else
{
// display the day number
}
}
What query should the dayHasEvents() function do to check if there are (or not) events for the day? I'm guessing SELECT .. WHERE .. BETWEEN makes the most sense here but I've no idea how to implement it. Am I in the right direction?
Thanks in advance!
#James:
Lets say we're on December 19th:
SELECT *
FROM events
WHERE start >= '2009-12-19 00:00:00'
AND end <= '2009-12-19 23:59:59'
Should return the event #2, but returns nothing. =\
You should scratch that approach and grab all events for the given month up front so you only need to perform a single query as opposed to N queries where N is the number of days in the month.
You could then store the returned results in a multidimensional array like so:
// assume event results are in an array of objects in $result
$events = array();
foreach ($result as $r) {
// add event month and day as they key index
$key = (int) date('j', strtotime($r->start));
// store entire returned result in array referenced by key
$events[$key][] = $r;
}
Now you'll have a multidimensional array of events for the given month with the key being the day. You can easily check if any events exist on a given day by doing:
$day = 21;
if (!empty($events[$day])) {
// events found, iterate over all events
foreach ($events[$day] as $event) {
// output event result as an example
var_dump($event);
}
}
You're definitely on the right track. Here is how I would go about doing it:
SELECT *
FROM events
WHERE start <= '2009-12-01 00:00:00'
AND end >= '2009-12-01 23:59:59'
And you obviously just replace those date values with the day you're checking on.
James has the right idea on the SQL statement. You definitely don't want to run multiple MySQL SELECTs from within a for loop. If daysHasEvents runs a SELECT that's 31 separate SQL queries. Ouch! What a performance killer.
Instead, load the days of the month that have events into an array (using one SQL query) and then iterate through the days. Something like this:
$sql= "SELECT start, end FROM events WHERE start >= '2009-12-01' AND end <= '2009-12-31'";
$r= mysql_query($sql);
$dates= array();
while ($row = mysql_fetch_assoc($r)) {
// process the entry into a lookup
$start= date('Y-m-d', strtotime($row['start']));
if (!isset($dates[$start])) $dates[$start]= array();
$dates[$start][]= $row;
$end= date('Y-m-d', strtotime($row['end']));
if ($end != $start) {
if (!isset($dates[$end])) $dates[$end]= array();
$dates[$end][]= $row;
}
}
// Then step through the days of the month and check for entries for each day:
for ($day = 1; $day <= 31; $day++)
{
$d= sprintf('2009-12-%02d', $day);
if (isset($dates[$d])) {
// display the day number w/ a link
} else {
// display the day number
}
}
For your purposes a better SQL statement would be one that grabs the start date and the number of events on each day. This statement will only work properly if the start column is date column with no time component:
$sql= "SELECT start, end, COUNT(*) events_count FROM events WHERE start >= '2009-12-01' AND end <= '2009-12-31' GROUP BY start, end";