Diff between dates calculates incorrect - php

I have two dateTime fields and I am trying to get the difference between them. However my results are coming out less than they should. I have looked at examples and the code seems fine.
$entryTime = new \DateTime($journalEntry->entry_date_time->toTimeString());
$closeTime = new \DateTime($journalEntry->close_date_time->toTimeString());
$interval = $entryTime->diff($closeTime);
debug($journalEntry->entry_date_time);
debug($journalEntry->close_date_time);
echo $interval->format('%d days %h hours %m minutes');
I am getting 0 days 11 hours 0 minutes
/src/Controller/JournalEntriesController.php (line 145)
object(Cake\I18n\FrozenTime) {
'time' => '2020-09-05 07:49:04.000000+00:00',
'timezone' => 'UTC',
'fixedNowTime' => false
}
/src/Controller/JournalEntriesController.php (line 146)
object(Cake\I18n\FrozenTime) {
'time' => '2020-09-07 19:36:53.000000+00:00',
'timezone' => 'UTC',
'fixedNowTime' => false
}

If you want to diff date and time, then there's no reason to create new DateTime objects, especially not from a time-only string, which would result in diffing only the time.
Simply diff the instances that you already have, they extend from \DateTime and \DateTimeImmutable respectively:
$interval = $journalEntry->entry_date_time->diff($journalEntry->close_date_time);
echo $interval->format('%d days %h hours %i minutes');
and as mentioned in the comments, m is for months, the pattern character for minutes is i.
CakePHP's datetime objects also provide additional diffing functionality for obtaining specific units (diffInHours(), diffInMinutes(), etc) or human readable formats (diffForHumans()).
See also
Chronos Cookbook > Chronos > Generating Differences
Chronos API > \Cake\Chronos\Traits\DifferenceTrait
PHP Manual > Function Reference > Date and Time Related Extensions > Date/Time > DateInterval > DateInterval::format

Related

FrozenDate::parseDate() doesn't reflect the date passed as parameter

It doesn't seem to matter what format I specify to FrozenDate::parseDate(), I always get the same wrong date.
Code:
$d1 = '8/28/2021';
$d2 = '10/27/2021';
debug($d1);
debug($d2);
debug(FrozenDate::parseDate($d1, 'm/d/Y'));
debug(FrozenDate::parseDate($d2, 'm/d/Y'));
exit;
Output:
\src\Controller\AdministratorsController.php (line 559)
'8/28/2021'
\src\Controller\AdministratorsController.php (line 560)
'10/27/2021'
\src\Controller\AdministratorsController.php (line 561)
object(Cake\I18n\FrozenDate) {
'time' => '2020-12-20 00:00:00.000000+00:00',
'timezone' => 'UTC',
'fixedNowTime' => false
}
\src\Controller\AdministratorsController.php (line 562)
object(Cake\I18n\FrozenDate) {
'time' => '2020-12-20 00:00:00.000000+00:00',
'timezone' => 'UTC',
'fixedNowTime' => false
}
Check the API docs, the second argument accepts:
array|string|int|null $format Any format accepted by IntlDateFormatter.
The docblock also has some examples:
$time = Time::parseDate('10/13/2013');
$time = Time::parseDate('13 Oct, 2013', 'dd MMM, y');
$time = Time::parseDate('13 Oct, 2013', IntlDateFormatter::SHORT);
So that's not the syntax as known from for example PHP's date() function, it's the same syntax that is accepted by i18nFormat(), the ICU Date/Time Format Syntax, where the pattern you're looking for would be:
M/d/y
It is worth noting that parsing is rather lenient, and also locale dependent. For example while M would only ever format as the month without a leading zero, when parsing it will accept the month as number with and without leading zero, as well as short and long month names (in the current language), meaning all of these would actually work (when en is set as the language):
FrozenDate::parseDate('8/6/2021', 'M/d/y')
FrozenDate::parseDate('08/06/2021', 'M/d/y')
FrozenDate::parseDate('Aug/6/2021', 'M/d/y')
FrozenDate::parseDate('August/06/21', 'M/d/y')

Using Nesbot Carbon to return date to arrival object accounting for business opening hours

I'm trying to use Carbon/Carbon and Cmixin\BusinessTime to return expected arrival DateTime carbon object, based on current DateTime. But also accounting for business opening hours.
I have an integer value $days, which I am adding to the current DateTime.
<?php
/**
* #param int $days // example defaulted to 1 day
* #return object
*/
public static function expected_delivery_date($days = 1)
{
// the current date time
$time = Carbon::now(get_option('timezone_string'))->addBusinessDays($transit);
// return delivery date object
return $time;
}
In this same php class file Shop() I am setting business hours using Cmixin\BusinessTime...
<?php
use Carbon\Carbon;
use Cmixin\BusinessTime;
class Shop
{
public function __construct()
{
BusinessTime::enable(Carbon::class,[
'monday' => ['09:00-17:30'],
'tuesday' => ['09:00-17:30'],
'wednesday' => ['09:00-17:30'],
'thursday' => ['09:00-17:30'],
'friday' => ['09:00-17:30'],
'saturday' => [],
'sunday' => [],
'exceptions' => [],
'holidaysAreClosed' => true,
'holidays' => []
]);
Carbon::setHolidaysRegion('gb-national');
}
}
new Shop();
The problem is, even tho I have defined my business hours using Cmixin\BusinessTime in my Shop() php class.
When using Carbon chain function ->addBusinessDays($days) (see docs), today (time now) is Sunday, which means it should add 2 days because the business is not open today.
Currently expected_delivery_date() is returning tomorrows date (Monday). The expected result should be returning Tuesday date, accounting for BusinessTime opening hours.
Any ideas on how to add days to Carbon DateTime object and accounting for closed business hours, would be awesome many thanks.
addBusinessDays does not take open hours into account.
But as you have a fixed day duration, you can use ->addOpenMinutes($days * (8 * 60 + 30))
When wanting to add days, it will depends on your input and business rules as Carbon instances handle a date and a time. So is it the same in your business logic starting from Sunday 00:00 or Sunday 23:59, what time is expected for the returned date. Does any day with no open hours counts for 0 and all other day (no matter they have 1 minute open or the whole day) counts for 1?

Normalize DateInterval in PHP

The situation/issue
I have an issue with DateInterval in PHP.
Given the following code
$interval = new DateInterval("PT6000S");
echo $interval->h; // 0
echo $interval->i; // 0
echo $interval->s; // 6000
echo $interval->format("%h:%i"); // 0:0
I want this to represent 1 hour and 40 minutes, not 6000 seconds.
The question
Is there a built-in way to normalize the DateInterval? A specific way of writing the duration string? Or is this something that must be done normally?
I can modify the way it's created and formated if anyone has a "work-around" to suggest.
I have done my research, but strangely enough I have not found anything helpful with this issue.
The DateInterval class is a bit annoying like that. Strictly it's doing the right thing, since your interval spec string specified zero minutes and hours, but it's definitely not helpful that it doesn't roll over excess seconds into minutes and hours.
One option I've used before is to add the interval to a new DateTime object initialised to the Unix epoch, then format the resulting time instead. This would mean using the standard date format strings (so h rather than %h, etc)
echo (new DateTime('#0'))->add(new DateInterval("PT6000S"))->format('h:i');
// 01:40
See https://3v4l.org/OX6JF
The solution
Like every time I ask for help, I cannot stop trying and I generally find the answer right after. No exception this time.
I've found a user contributed note in the DateInterval documentation.
You have to create two DateTime (they can be any date and time), add your interval to the second and substract the first from the second. This will populate the DateInterval in a normalized way.
Here is the code the user wrote, wrapped in a handy-dandy function:
public function createDateInterval(string $duration): DateInterval
{
$d1 = new DateTime();
$d2 = new DateTime();
$d2->add(new DateInterval($duration));
return $d2->diff($d1);
}
(Note that this function can throw an exception if your $duration string is not correctly formatted.)
This solution uses DateTime with a fixed time. Why ?
DateTime also supports microseconds. There is therefore a certain probability that 2 calls to new Date() will not return the same time. Then we can get wrong results.
//360000 Sec = 100 hours = 4 Days + 4 hours
$di = new DateInterval("PT360000S");
$diNormalize = date_create('#0')->diff(date_create('#0')->add($di));
var_export($diNormalize);
/*
DateInterval::__set_state(array(
'y' => 0,
'm' => 0,
'd' => 4,
'h' => 4,
'i' => 0,
's' => 0,
'f' => 0.0,
'weekday' => 0,
'weekday_behavior' => 0,
'first_last_day_of' => 0,
'invert' => 0,
'days' => 4,
'special_type' => 0,
'special_amount' => 0,
'have_weekday_relative' => 0,
'have_special_relative' => 0,
))
*/
echo $diNormalize->format('%a days %h hours %i minutes');
//4 days 4 hours 0 minutes
DateInterval has a special format method for the output. This should be used here and no detour via date, gmdate or DateTime should be taken.

PHP Time Since Function Bug

i am writing a time since function to return the time since a given mysql datetime. When taking the $oldtime from current time() it is returning a negative int when i need a positive int. I have written similar functions before in other languages but i have become blind to this problem, so any help would be much appreciated.
function timeSince($time){
$today = date("Y");
$oldtime = strtotime($time);
$time = time() - $oldtime;
$tokens = array (
3600 => 'h',
60 => 'm',
1 => 's'
);
if($time >= 86400){
}
}
echo timeSince('2016-02-25 14:35:00');
it could be much more convenient if you use PHP's DateTime and DateInterval classes and their methods:
function timeSince($datetime) {
$now = strtotime("now");
$then = strtotime($datetime);
$dt_now = new DateTime("#" . $now);
$dt_then = new DateTime("#" . $then);
//DateTime's diff method returns a DateInterval object which got a format method:
return $dt_now->diff($dt_then)->format('%a days, %h hours, %i minutes and %s seconds');
}
some test cases:
//my local date & time is around "2016-02-25 19:49:00" when testing
echo '<pre>';
echo timeSince('2016-02-25 19:30:00');
//0 days, 0 hours, 19 minutes and 11 seconds
echo PHP_EOL;
echo timeSince('2013-11-02 15:43:12');
//845 days, 4 hours, 4 minutes and 3 seconds
echo PHP_EOL;
echo timeSince('2017-01-31 00:22:45');
//340 days, 4 hours, 35 minutes and 30 seconds
echo PHP_EOL;
echo timeSince('1950-05-14 07:10:05');
//24028 days, 12 hours, 37 minutes and 10 seconds
echo PHP_EOL;
code partially based on this answer: https://stackoverflow.com/a/19680778/3391783
strtotime uses timezone in your PHP settings. Depending on timezone set, it might convert to the time that is yet to happen. For example, on my ukrainian server, strtotime('2016-02-25 14:35:00') converts to 1456403700, on a server in another timezone (US/Pacific) it converts to 1456439700.
Quote from PHP docs:
The function expects to be given a string containing an English date format and will try to parse that format into a Unix timestamp (the number of seconds since January 1 1970 00:00:00 UTC), relative to the timestamp given in now, or the current time if now is not supplied.
Each parameter of this function uses the default time zone unless a time zone is specified in that parameter. Be careful not to use different time zones in each parameter unless that is intended. See date_default_timezone_get() on the various ways to define the default time zone.
You can add UTC/GMT offset to your datetime (1st param), for example strtotime('2016-02-25 14:35:00 +0800') or ('2016-02-25 14:35:00 GMT+08:00') will convert to 1456382100
In your example, $oldtime must be smaller value than current time().
So, if you want to count time between larger value, simply reverse your equation:
This line:
$time = time() - $oldtime;
Becomes:
$time = $oldtime - time();

PHP - Time based math

How exactly is this done? There's so many questions on stack-overflow about what I'm trying to do; However all of the solutions are to edit the MYSQL Query, and I need to do this from within PHP.
I read about the strtotime('-30 days') method on another question and tried it, but I can't get any results. Here's what I'm trying:
$current_date = date_create();
$current_date->format('U');
... mysql code ...
$transaction_date = date_create($affiliate['Date']);
$transaction_date->format('U');
if($transaction_date > ($current_date - strtotime('-30 days'))) {
} else if(($transaction_date < (($current_date) - (strtotime('-30 days'))))
&& ($transaction_date > (($current_date) - (strtotime('-60 days'))))) {
}
Effectively, I'm trying to sort all of the data in the database based on a date, and if the database entry was posted within the last 30 days, I want to perform a function, then I want to see if the database entry is older than 30 days, but not older than 60 days, and perform more actions.
This epoch math is really weird, you'd think that getting the epoch of the current time, the epoch of the data entry, and the epoch of 30 and 60 days ago would be good enough to do what I wanted, but for some reason it's not working, everything is returning as being less than 30 days old, even if I set the date in the database to last year.
No need to convert to unix timestamp, you can already compare DateTime objects:
$current_date = data_create();
$before_30_day_date = date_create('-30 day');
$before_60_day_date = date_create('-60 day');
$transaction_date = date_create($affiliate['Date']);
if ($transaction_date > $before_30_day_date) {
# transation date is between -30 day and future
} elseif ($transaction_date < $before_30_day_date && $transaction_date > $before_60_day_date) {
# transation date is between -60 day and -30 day
}
This creates (inefficiently, see my comment above) an object:
$current_date = date_create(date("Y-m-d H:i:s"));
From which you try to subtract an integer:
if($transaction_date > ($current_date - strtotime('-30 days'))) {
which is basically
if (object > (object - integer))
which makes no sense.
you're mixing the oldschool time() system, which deals purely with unix timestamps, and the newer DateTime object system, which deals with objects.
What you should have is
$current_date = date_create(); // "now"
$d30 = new DateInterval('P30D'); // 30 days interval
$transaction_date = date_create($affiliate['Date']);
if ($transaction_date > ($current_date->sub($d30)) { ... }
You might consider DatePeriod class, which in essence gives you the ability to deal with a seires of DateTime objects at specified intervals.
$current_date = new DateTime();
$negative_thirty_days = new DateInterval::createFromDateString('-30 day');
$date_periods = new DatePeriod($current_date, $negative_thrity_days, 3);
$thirty_days_ago = $date_periods[1];
$sixty_day_ago = $date_periods[2];
Here you would use $thirty_days_ago, $sixty_days_ago, etc. for your comparisons.
Just showing this as alternative to other options (which will work) as this is more scalable if you need to work with a larger number of interval periods.

Categories