I've a case where i need to figure a formula to calculate total time between date range sets (from->to) that can overlap each other.
This is going to be used in a ticketing system where i need to calculate the total open time of all tickets (as part of an SLA agreement) without double calculating time that has been already counted.
For example, in the records below:
TicketID
Open Date
Close Date
Ticket 1
'2023-01-02 09:00:00'
'2023-01-02 14:00:00'
Ticket 2
'2023-01-02 11:00:00'
'2023-01-02 15:00:00'
Ticket 3
'2023-01-14 10:00:00'
'2023-01-14 11:00:00'
the total time i would need to have is:
from '2023-01-02 09:00:00' to '2023-01-02 15:00:00'
and from '2023-01-14 10:00:00' to '2023-01-02 11:00:00',
thus a total of 7 hours.
An ideas on where to start?
I've search for similar questions, such as this one PHP Determine when multiple(n) datetime ranges overlap each other but it is somewhat different than the one i need to have.
The following solution first compresses all overlapping intervals into one using the reduceOverlap function. This means that intervals are then available which do not overlap. These are then added using the diffTotal function. As a result, a DateInterval object is available that can be formatted as you wish with the Format method.
<?php
function reduceOverlap(array $arr){
$flag = true;
while($flag){
$flag = false;
foreach($arr as $key1 => $row1){
foreach($arr as $key2 => $row2){
if($key1 === $key2) continue;
if(($row1['open'] >= $row2['open'] && $row1['open'] <= $row2['close']) OR
($row1['close'] >= $row2['open'] && $row1['close'] <= $row2['close'])){
$arr[$key1]['open'] = min($row1['open'],$row2['open']);
$arr[$key1]['close'] = max($row1['close'],$row2['close']);
unset($arr[$key2]);
$flag = true;
break 2;
}
}
}
}
return $arr;
}
function diffTotal(array $arr){
$date = date_create('1970-01-01 00:00');
foreach($arr as $row){
$diff = date_create($row['open'])->diff(date_create($row['close']));
$date->add($diff);
}
return date_create('1970-01-01 00:00')->diff($date);
}
$times = [
['open' => '2023-01-02 09:00:00', 'close' => '2023-01-02 14:00:00'],
['open' => '2023-01-02 11:00:00', 'close' => '2023-01-02 15:00:00'],
['open' => '2023-01-14 10:00:00', 'close' => '2023-01-14 11:00:00'],
];
$arr = reduceOverlap($times);
$diffTotal = diffTotal($arr);
var_dump($diffTotal);
Output:
object(DateInterval)#2 (10) {
["y"]=>
int(0)
["m"]=>
int(0)
["d"]=>
int(0)
["h"]=>
int(7)
["i"]=>
int(0)
["s"]=>
int(0)
["f"]=>
float(0)
["invert"]=>
int(0)
["days"]=>
int(0)
["from_string"]=>
bool(false)
}
try self: https://3v4l.org/MPNPC
Related
I have a variable that stores current time with $current_time and another variable with an array of time stored in $time_slot I am looking for a way around to unset from array variable $time_slot all array of time that is less than the $current_time I tried below code but it didn't work:
$current_time = '4:00';
$time_slot = ['1:00','3:00','15:00','19:00'];
// removing the deleted value from array
if (($key = array_search($current_time, $time_slot)) !== false) {
if($current_time>$time_slot){
unset($time_slot[$key]);
}
}
print_r($time_slot);
You can convert your timeslots to a DateTime instance which will make it easier to compare your values.
$date = new \DateTime('15:00');
var_dump($date);
object(DateTime)#1 (3) {
["date"]=>
string(26) "2022-10-20 15:00:00.000000"
["timezone_type"]=>
int(3)
["timezone"]=>
string(3) "UTC"
}
In the snippet below, I unset the index of the timeslot if it's "expired"
<?php
$current_time = '4:00';
$time_slot = ['1:00','3:00','15:00','19:00'];
$current_date = new \DateTime($current_time);
foreach($time_slot as $index => $value) {
if ($current_date > new \DateTime($value)) unset($time_slot[$index]);
}
var_dump($time_slot);
demo
demo
By making natural sorting comparisons, you can call array_filter() to weed out the times which are smaller than the current time. strnatcmp() returns a 3-way result (-1, 0, and 1) -- if -1 then the the first value is smaller than the second value.
You don't need to create new DateTime objects on every iteration.
Code: (Demo)
var_export(
array_filter(
$time_slot,
fn($v) => strnatcmp($current_time, $v) === -1
)
);
I'm trying to figure out the number of months between two dates, the dates hypothetically lets say are between 2018-08-27 and 2018-10-10. What i want is a function based on those dates to return a difference of 3 months, 08,09,10. I have the following function, but it only seems to output 1 month;
public function getGraphMonthsCount(){
$now = '2018-08-27';
$then = '2018-10-10';
$newNow = new DateTime($now);
$newThen = new DateTime($then);
$result = $newNow->diff($newThen)->m;
return $result;
}
this return a value of 1.
this is what the diff function outputs without the ->m param
object(DateInterval)#157 (15) {
["y"]=>
int(0)
["m"]=>
int(1)
["d"]=>
int(13)
["h"]=>
int(0)
["i"]=>
int(0)
["s"]=>
int(0)
["weekday"]=>
int(0)
["weekday_behavior"]=>
int(0)
["first_last_day_of"]=>
int(0)
["invert"]=>
int(0)
["days"]=>
int(44)
["special_type"]=>
int(0)
["special_amount"]=>
int(0)
["have_weekday_relative"]=>
int(0)
["have_special_relative"]=>
int(0)
}
I don't know why it's providing only 13 'd' and 1 'm', but if you look further into the obj you can see it does have the correct amount of 'days'
Is there a better way of doing this?
What i want is a function based on those dates to return a difference of 3 months
You can try something like this:
$newNow = new DateTime($now);
$newNow = $newNow->modify('first day of this month');
$newThen = new DateTime($then);
$newThen = $newThen->modify('first day of next month');
$result = $newNow->diff($newThen)->m;
Test results:
$now = '2018-08-27';
$then = '2018-10-10';
// 3
$now = '2018-08-10';
$then = '2018-08-27';
// 1
I'm trying to store chosen weekdays in one field in DB. Bitwise seem to be perfect for that, but PHP dosen't give the expected result, and i don't know why.
mon tue wnd thu fri sat sun
1 1 1 1 1 1 1
1 2 4 8 16 32 64
so to select Tue and Fri: 18 (0100100)
and for example, to check if sunday is selected: decbin(18) & decbin(64) (should return "empty"),
but the reuslts are unexpected.
That's because you need to left-pad those binary strings with leading zeroes to the same length. decbin() creates a string that discards any leading zeroes. You're anding strings like:
10010 // 18
1000000 // 64
Either use str_pad() too add leading zeroes to a fixed length, or save a function call and use sprintf to do both the base conversion and the padding in a single step
sprintf('%08b', 18) & sprintf('%08b', 64)
You don't have to work with binary strings at all to work with bit flags.
Declare your days as integers 1,2,4,8 etc.
To make a value that is Monday and Friday just binary OR the values.
To check that a number contains a day, binary AND them and check that the result is equal to the day your checking.
Although technically these are all binary operations, you don't actually have to ever see or use the binary strings.
Some examples with the dump results below... please ignore my use of extract, it was just quicker this way
<?php
// Day names
$dayNames = array(
'monday',
'tuesday',
'wednesday',
'thursday',
'friday',
'saturday',
'sunday',
);
// Bit values
$dayValues = array();
foreach($dayNames as $key => $value) {
$dayValues[$value] = 1 << $key;
}
var_dump($dayValues);
extract($dayValues);
// Monday and Tuesday
$monANDtue = $monday | $tuesday;
var_dump($monANDtue);
var_dump(decbin($monANDtue));
// Monday and Sunday
$monANDsun = $monday | $sunday;
var_dump($monANDsun);
var_dump(decbin($monANDsun));
// Is on Monday?
$isOnMonday = ($monANDsun & $monday) == $monday;
var_dump($isOnMonday);
// Is on Tuesday?
$isOnTuesday = ($monANDsun & $tuesday) == $tuesday;
var_dump($isOnTuesday);
// Is on Tuesday?
$isOnSunday = ($monANDsun & $sunday) == $sunday;
var_dump($isOnSunday);
?>
and the output
/vhost/virtual/sandbox/public/index.php:27
array(7) {
[monday] = int(1) 1
[tuesday] = int(1) 2
[wednesday] = int(1) 4
[thursday] = int(1) 8
[friday] = int(2) 16
[saturday] = int(2) 32
[sunday] = int(2) 64
}
/vhost/virtual/sandbox/public/index.php:33
int(1) 3
/vhost/virtual/sandbox/public/index.php:34
string(2) "11"
/vhost/virtual/sandbox/public/index.php:40
int(2) 65
/vhost/virtual/sandbox/public/index.php:41
string(7) "1000001"
/vhost/virtual/sandbox/public/index.php:47
bool(true)
/vhost/virtual/sandbox/public/index.php:51
bool(false)
/vhost/virtual/sandbox/public/index.php:55
bool(true)
I have timestamps in the following format: 2013-10-19 18:47:30
I am using this to convert to relative (x minutes, hours, days ago) time but it returns nothing for the most recent users, I assume because my system time is GMT -3 hours so it is interpreting it as a time in the future. If that is the case, how can I take the users GMT offset into account in the result?
$time = strtotime('2013-04-28 17:25:43');
echo 'event happened '.humanTiming($time).' ago';
function humanTiming ($time)
{
$time = time() - $time; // to get the time since that moment
$tokens = array (
31536000 => 'year',
2592000 => 'month',
604800 => 'week',
86400 => 'day',
3600 => 'hour',
60 => 'minute',
1 => 'second'
);
foreach ($tokens as $unit => $text) {
if ($time < $unit) continue;
$numberOfUnits = floor($time / $unit);
return $numberOfUnits.' '.$text.(($numberOfUnits>1)?'s':'');
}
}
$eventTimeString = '2013-04-28 17:25:43';
$timezone = new DateTimeZone("Etc/GMT+3"); // Same as GMT-3, here you should use the users's timezone
$eventTime = new DateTime($eventTimeString , $timezone);
$diff = $eventTime->diff(new DateTime()); // Compare with
The $diff variable now has number of seconds, months, days etc. since $eventTime
object(DateInterval)#4 (15) {
["y"]=>
int(0)
["m"]=>
int(7)
["d"]=>
int(18)
["h"]=>
int(17)
["i"]=>
int(35)
["s"]=>
int(38)
["weekday"]=>
int(0)
["weekday_behavior"]=>
int(0)
["first_last_day_of"]=>
int(0)
["invert"]=>
int(0)
["days"]=>
int(232)
["special_type"]=>
int(0)
["special_amount"]=>
int(0)
["have_weekday_relative"]=>
int(0)
["have_special_relative"]=>
int(0)
}
Here is a rewrite of your method that will be more reliable/accurate:
function humanTiming($time){
$timezone=new DateTimeZone("Australia/Melbourne"); // declare whatever your user's timezone is
$time=new DateTime($time,$timezone); // input your datetime
$now=new DateTime("now",$timezone); // get current datetime
if($time>$now){
return "event hasn't happened yet";
}elseif($time==$now){
return "event is happening";
}
$diff=(array)$time->diff($now); // get time between now and $time, cast as array
$labels=array("y"=>"year","m"=>"month","d"=>"day","h"=>"hour","i"=>"minute","s"=>"second");
$readable=""; // declare as empty string
// filter the $diff array to only the desired elements and loop
foreach(array_intersect_key($diff,$labels) as $k=>$v){
if($v>0){ // only add non-zero values to $readable
$readable.=($readable!=""?", ":"")."$v {$labels[$k]}".($v>1?"s":"");
// use comma-space as glue | show value | show unit | pluralize when necessary
}
}
return "event happened $readable ago";
}
echo humanTiming('2013-04-28 17:25:43');
// event happened 3 years, 10 months, 23 days, 8 hours, 33 minutes, 59 seconds ago
This question already has answers here:
How to check if a date is in a given range?
(10 answers)
Finding whether time is in a defined range [duplicate]
(5 answers)
Closed 8 years ago.
I've been working at this code off and on for the past few days and can't figure it out.
What I need to do is return from a function either a 0 or 1 depending on if the current time is within the times set by a user. If the time and date is within a 4 value array set by the user, then return 1, if not, return 0. The user can set multiple arrays for multiple periods of times.
I've been trying to work with this code for a while:
functions.php:
function determineWoE($woe) {
$curDayWeek = date('N');
$curTime = date('H:i');
$amountWoE = count($woe['WoEDayTimes']); // Determine how many WoE times we have.
if ( $amountWoE == 0 ) {
return 0; // There are no WoE's set! WoE can't be on!
}
for ( $i=0; $i < $amountWoE; $i++ ) {
if ( $woe['WoEDayTimes'][$i][0] == $curDayWeek && $woe['WoEDayTimes'][$i][2] == $curDayWeek ) { // Check the day of the week.
if ( $woe['WoEDayTimes'][$i][1] >= $curTime && $woe['WoEDayTimes'][$i][3] <= $curTime ) { // Check current time of day.
// WoE is active
return 1;
}
else {
// WoE is not active
return 0;
}
}
else {
// WoE is not active
return 0;
}
}
}
And...where the user sets as many periods of time for this feature that they want:
$woe = array( // Configuration options for WoE and times.
// -- WoE days and times --
// First parameter: Starding day 1=Monday / 2=Tuesday / 3=Wednesday / 4=Thursday / 5=Friday / 6=Saturday / 7=Sunday
// Second parameter: Starting hour in 24-hr format.
// Third paramter: Ending day (possible value is same or different as starting day).
// Fourth (final) parameter: Ending hour in 24-hr format.
'WoEDayTimes' => array(
array(6, '18:00', 6, '19:00'), // Example: Starts Saturday 6:00 PM and ends Saturday 7:00 PM
array(3, '14:00', 3, '15:00') // Example: Starts Wednesday 2:00 PM and ends Wednesday 3:00 PM
),
);
But, no matter what I do...the function determineWoE always returns 0.
Am I needing a foreach in the function instead of a for? How do I get determineWoE to return 1 if the time is within the user settable times?
Tried changing the for to a foreach:
foreach ( $woe['WoEDayTimes'] as $i ) {
And now I get error:
Warning: Illegal offset type in /var/www/jemstuff.com/htdocs/ero/functions.php on line 76
...which I have no idea why I would be getting that error. Line 76 is:
if ( $woe['WoEDayTimes'][$i][0] == $curDayWeek && $woe['WoEDayTimes'][$i][2] == $curDayWeek ) { // Check the day of the week.
In functions.php
var_dump($woe)
array(2) { ["WhoOnline"]=> string(2) "no" ["WoEDayTimes"]=> array(2) { [0]=> array(4) { [0]=> int(6) [1]=> string(5) "18:00" [2]=> int(6) [3]=> string(5) "19:00" } [1]=> array(4) { [0]=> int(3) [1]=> string(5) "14:00" [2]=> int(3) [3]=> string(5) "15:00" } } }
Thanks for any help you can provide to me. :)
A couple minor points:
A foreach loop and a for loop would both work fine, but you might find the foreach more covenient, since you wouldn't have to count() the days/times to check for.
You should return boolean true or false instead of 1 or 0.
I'm not sure why you're getting that error, but the bigger problem I see is how you compare the times. You cast the string times to numeric types, and that won't convert entirely like you think it will. For example...
"14:00" < "14:59"
...Would be false, because it casts both strings to 14. Thus, the first string actually equals the second.
You might be better off converting the strings to Unix Timestamps (which are the seconds since 1/1/1970), and comparing those.
Here's a rough idea of how I would do it:
// Function to help get a timestamp, when only given a day and a time
// $today is the current integer day
// $str should be 'last <day>', 'next <day>', or 'today'
// $time should be a time in the form of hh:mm
function specialStrtotime($today, $day, $time) {
// An array to turn integer days into textual days
static $days = array(
1 => 'Monday',
2 => 'Tuesday',
3 => 'Wednesday',
4 => 'Thursday',
5 => 'Friday',
6 => 'Saturday',
7 => 'Sunday'
);
// Determine if the day (this week) is in the past, future, or today
if ($day < $today) {
$str = 'last ' . $days[$day];
} else if ($day > $today) {
$str = 'next ' . $days[$day];
} else {
$str = 'today';
}
// Get the day, at 00:00
$r = strtotime($str);
// Add the amount of seconds the time represents
$time = explode(':', $time);
$r += ($time[0] * 3600) + ($time[1] * 60);
// Return the timestamp
return $;
}
// Your function, modified
function determineWoE($timeNow, $woe) {
$dayNow = (int) date('N', $timeNow);
foreach ($woe as $a) {
// Determine current day
// Determine the first timestamp
$timeFirst = specialStrtotime($dayNow, $a[0], $a[1]);
// Determine the second timestamp
$timeSecond = specialStrtotime($dayNow, $a[2], $a[3]);
// See if current time is within the two timestamps
if ($timeNow > $timeFirst && $timeNow < $timeSecond) {
return true;
}
}
return false;
}
// Example of usage
$timeNow = time();
if (determineWoE($timeNow, $woe['WoEDayTimes'])) {
echo 'Yes!';
} else {
echo 'No!';
}
Good luck!