I have a problem that seems quite hard to solve so I hope someone will have a good solution for me:)
I have a PHP program with a list of reservation for many conference rooms of an hotel. I need to calculate how much time during the day at least 1 conference room is in use.
Here is an exemple:
Room 1: 10h 00 to 13h 00
Room 2: 11h 00 to 14h 00
Room 3: 15h 00 to 16h 00
With theses numbers, I need to calculate that the hotel is used during 5 hours (10h to 14h and 15h to 16h).
In the end, this tells me how many hours the hotel has to pay someone to check the rooms in case someone has a problem.
If you don't have a library that could help me, an algorithm could be a good start.
Algorithm outline:
Order you intervals by starting time.
Go through them and join neighbors that intersect. The intersection condition will be that the second interval's start time is before or equal to the first interval's end time. Because of the ordering only one loop will be needed.
At this point you have only disjoint intervals and you can sum their spans to get the total span.
And here is an implementation of that algorithm:
<?php
// assuming hh:mm format for all the dates //
$intervals = array(
array(
'start' => '15:30',
'end' => '16:00',
),
array(
'start' => '10:00',
'end' => '13:00',
),
array(
'start' => '15:00',
'end' => '16:09',
),
array(
'start' => '11:00',
'end' => '14:00',
),
);
// 1. sort the intervals by start date //
function mySortIntervals($a, $b){
return $a > $b;
}
usort($intervals, 'mySortIntervals');
// 2. merge adjoining intervals //
$active = 0;
$current = 1;
$length = count($intervals);
while($current < $length){
if($intervals[ $current ]['start'] <= $intervals[ $active ]['end']){
$intervals[ $active ]['end'] = max($intervals[ $active ]['end'], $intervals[ $current ]['end']);
unset($intervals[ $current ]);
}
else{
$active = $current;
}
$current++;
}
// 3. cout the total time //
$time = 0;
foreach($intervals as $interval){
$time += strtotime($interval['end']) - strtotime($interval['start']);
}
// output //
echo str_pad((int) ($time/60/60), 2, '0', STR_PAD_LEFT).':';
echo str_pad((int) (($time/60)%60), 2, '0', STR_PAD_LEFT);
?>
First of all, you need to define your interval or time span, for example every 15 minutes.
Why this? because you need to control the gaps caused by room that finish before time, or for some other reason.
The time span can NOT be more than 15 minutes.(Time is money).
Order a room (time span). It must check avaliable time span every time a request happen.
Once the cell (time span) is checked that time span will not be available anymore.
To order a room just select the hour (00 til 23) and the quarter (00; 15; 30; 45;)
It worked to me quite well for an ISP business. I think is not different for a hotel. Because the idea of time span is the same.
But of course you can look for some class if you don't want to write your code at all.
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 am trying to display a number every day in a loop. After the last element is reached it needs to get to the first element again. This needs to happen daily. I have overworked my brains out but did not managed to solve it. Function needs to return current number by day/hour/minute, like . This is what I tried till now.
<?php
function recursive_daily_deals($i = 1) {
$current_date = strtotime(date('d-m-Y h:i:s'));
$dbs_date_1 = strtotime('29-06-2017 8:20:16');
$current_hour = date('h');
var_dump($current_hour);
$products = [
451,
455,
453
];
if ($i < count($products)) {
return recursive_daily_deals($i+1);
}
}
?>
EXPECTED output
> First day - June 29 2017
> It will appear 451
> Second day - June 30 2017
> It will appear 455
> 3rd day - July 1st 2017
> It will appear 453
> 4th Day - July 2nd 2017
> It will appear 453
> And start over
First you need to know how many days have been since the starting day. To do that you just need to sub the starting timestamp from the actual timestamp :
$dbs_date_1 = strtotime('29-06-2017 8:20:16');
$actualTimestamp = time();
$elapsedSec = $dbs_date_1 - $actualTimestamp;
// we need to get days from seconds
$elapsedDays = $elapsedSec / (3600*24);
$elapsedDays = floor($elapsedDays);
So when you have how many days have been since the starting day. We use floor() instead of round() because if the script runs after the half of the day it will return the number of days +1.
With this number of days we can have the number of cycles already done by dividing the number of elapsed days by the number of items in our array :
$nbItems = count($products);
$cycleCount = $elapsedDays / $nbItems;
$finishedCycles = floor($cycleCount);
We store the number of finished cycles by flooring the number of cycles. Then we just have to sub the days it took to complete those cycles from the elapsed days and we will get the position of the index.
$completeDays = $finishedCycles * $nbItems;
$actualPosition = $elapsedDays - $completeDays;
return $products[$actualPosition];
While this is a simplified version of the code originally posted, I believe it contains the kind of logic that the OP seeks, as follows:
<?php
$products = [
451,
455,
453
];
$modfactor = count($products);
$days = null;
$weekdays = ["Monday","Tuesday","Wednesday","Thursday","Friday","Saturday","Sunday"];
for ($i=0, $max = 7; $i < $max; $i++) {
$days[$i] = $i % $modfactor;
}
foreach ($weekdays as $dex => $wday) {
echo "$wday, ";
echo $products[ $days[$dex] ], "\n";
}
See demo
Update: See demo here which makes use of array_map() and gets the current product ID, too.
While the loop is ever present, it is not infinite. If the number of products changes, then the modfactor changes, too. What stays constant are the days of the week. What makes the code work is taking advantage of a modulo operation.
I have some code that loops through each day within a 10 year range starting on a specific date. The date should only be added if it meets the selected criteria from a form.
The form has fields for months and weekdays to be selected. Within each weekday there are options for every, first, second, etc. I'm adding an 'every other' option to each weekday along with a start date field.
I am trying to determine the best way to check if the current date is within the 'every other' criteria based on the start date.
After looking at other questions, I couldn't find an ideal answer that took into account year that have 53 weeks. I was able to come up with some test code that seems to be giving me accurate results, but I'm wondering if there is a simpler way to perform this check.
Test Code:
// set start date
$date = new \DateTime('2019-12-15');
// get start date week of year and modulus
$start_week = date('W', $date->getTimestamp());
$start_mod = $start_week % 2;
// set end time (10 years from start date)
$end_time = strtotime('+10 years', $date->getTimestamp());
// init previous year and week modifier
$prev_year = false;
$week_modifier = 1;
// each day in range
while($date->getTimestamp() <= $end_time){
// get year
$y = $date->format('Y');
// previous year doesn't match current year
if($prev_year != $y){
// previous year set
if($prev_year){
// get number of weeks in year
$weeks_in_year = date('W', mktime(0, 0, 0, 12, 28, $prev_year));
// year has odd number of weeks
if($weeks_in_year % 2){
// increment week modifier
$week_modifier++;
}
}
// update previous year
$prev_year = $y;
}
// get week of year
$w = $date->format('W') + $week_modifier;
// check if meets every other criteria (based on start date)
$every_other = false;
if( ($w % 2) == $start_mod ){
$every_other = true;
}
// print date if it is part of every other Tuesday
if($date->format('w') == 2 && $every_other){
echo $date->format('Y-m-d');
echo '<br/>';
}
// increment day
$date->modify('+1 day');
}
Note 1: 2020 is the next year in which there are 53 weeks.
Note 2: I had a typo in this test code that was incrementing the week modifier instead of initializing it to 0. It would make more sense to me that this code would work if the modifier was initialized to be 0, but instead it only works when initialized to an odd number.
Since the "every other" is evaluated in a continuous cycle, you might just keep track of the days:
$odd = [ true, true, true, true, true, true, true ];
...
// Flip the appropriate day of the week.
$odd[date('w')] = !$odd[date('w')];
// Or start with all 1's, then $odd[date('w')] ^= 1;
if ($odd[date('w')]) {
// This is the "not-other" day
}
Modular arithmetic
This day is $w and we mark it:
$odd[$w] = !$odd[$w];
Now we advance by an unknown number of days $d. We need to properly flip all the days in this interval.
One way to do that is to cycle through all the days. But it's clear that this can't be necessary - we have seven days in a week, and they are either odd or even; even if we flipped them all, it would be seven updates. Cycling through one year would be 365 or 366 updates.
On the one hand, the 365-cycle solution has the advantage of simplicity. Is running 7 flips instead of 3652 really worth our while? If it is not, we're done. So let's suppose it is; but this evaluation should be re-done for every project.
So note that if we advanced by 1 day, we need do nothing. If we advance by 2 days, day[w+1] must be flipped. If we advance by 5 days, days from w+1 to w+4 need to be flipped. In general, days from w+1 to w+d-1 need to be flipped:
for ($i = 1; $i < $w+$d; $i++) {
$odd[$i % 7] = !$odd[$i % 7];
}
But now notice that if we advanced by 15 days, we would again need do nothing, as if we had advanced of only 1 day, since every day of the week would find itself being flipped twice:
d need to flip w+...
1 (none)
2 1
3 1, 2
4 1, 2, 3
5 1, 2, 3, 4
6 1, 2, 3, 4, 5
7 1, 2, 3, 4, 5, 6
8 1, 2, 3, 4, 5, 6, 0 (or 7)
9 2, 3, 4, 5, 6, 0
10 3, 4, 5, 6, 0
11 4, 5, 6, 0
12 5, 6, 0
13 6, 0
14 0
15 (none)
So here's a very reasonable compromise: if we need to advance by X days, treat it as if we had to advance by (X % 14) days. So now we will run at most 13 updates. Stopping now means that our code is the trivial version, enhanced by a strategically placed "% 14". We went from 3652 to 14 updates for a ten-year interval, and the best we could hope for was 7 updates. We got almost all of the bang, for very little buck.
If we want to settle for nothing but the best, we go on (but note that the additional arithmetic might end up being more expensive than the saving in updates from the worst value of 13 to, at best, zero. In other words, doing additional checks means we will save at best 13 updates; if those checks cost more than 13 updates, we're better off not checking and going through blindly).
So we start flipping at dIndex 1 if (1 < d%14 < 9), or (d%7) if (d%14 >=9 ). And we end at (d%14)-1 if (d%14)<8, 0 otherwise. If d%14 is 1, the start (using a simplified rule 1: d%14 < 9) is 1, the end is at 0, and since 0 is less than 1, the cycle would not even start. This means that the simplified rule should work:
// increase by d days
d1 = (d%14) < 9 ? 1 : (d%7);
d2 = (d%14) < 8 ? (d%14-1) : 7;
for (dd = d1; dd <= d2; dd++) {
odd[(w+dd)%7)] = !odd[(w+dd)%7)];
}
The above should flip correctly the "every other XXX" bit doing at most 7 writes, whatever the value of d. The approximate cost is about 6-7 updates, so if we're doing this in memory, it's not really that worth it, on average, if compared with the "% 14" shortcut. If we're flipping with separated SQL queries to a persistency layer, on the other hand...
(And you really want to check out I didn't make mistakes...)
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);
I have a small script that splits out an amount of revenue over a span of days. I can't seem to wrap my head around allowing for any number of days. Currently I can only get it to work if I hard-set an array of %s for each amount of days, which obviously isn't going to work for large sets of day combinations.
If anyone has an idea as to how to accomplish this, I'd be ever grateful.
My Code:
<?php
//define variables
$ammount = 11320.00;
$start_date = "12/30/2011";
$days = 5;
//setup array of percents
$percent_array = array(3=>array(0 => 45, 1 => 30, 2 => 25), 4=>array(0 => 40, 1 => 30, 2 => 20, 3 => 10),4=>array(0 => 35, 1 => 25, 2 => 20, 3 => 10, 4 => 10));
//handle formatting the date for use in loop
$format = 'D m/d/Y';
$sDate = strtotime($start_date);
if ($usersTS !== false) {
for ($i = 0; $i < $days; $i++) {
echo date($format, strtotime('+' . $i . ' day', $sDate));
echo " : ";
echo number_format($ammount * ($percent_array[$days][$i] / 100), 2, ".", ",");
echo "<br />";
}
} else {
echo "No valid date supplied";
};
?>
Output:
Fri 12/30/2011 : 3,962.00
Sat 12/31/2011 : 2,830.00
Sun 01/01/2012 : 2,264.00
Mon 01/02/2012 : 1,132.00
Tue 01/03/2012 : 1,132.00
So you want to distribute the amount over X number of days such that the earlier days are higher weighted?
Sounds like you just need to find the right function. You can try y=1/(x+1) for example. Then solve for x number of days, and use each day as a percent sum of the y values.
well, I'm not quite sure by what amount you want to split for each day, but I think you are over thinking this problem? Couldn't you just divide the amount by the days? Or am I missing something... If you could post more info about your objective that would be helpful :)