Carbon
Carbon copied to clipboard
🐛 Improve DST handling
PHP will fix some DST issues, and behave differently when using add()
or modify()
which also leads to inconsistency as we use modify()
for our add/sub methods.
We also have set of addReal/subReal methods which are quite "good" for hours/minutes/seconds because we use timestamp for "Real" and modify()
without "Real" which will convert 25/23 hours on DST days to 24 hours to somehow "ignores" the DST. So "Real" here makes sense. But on days/month/year, they more relate to UTC as 2021-11-07 in New York "really" is a 25-hours day. So adding 24 hours when asking for addRealDay()
is no longer the correct word.
For those reasons, we should revise the "Real" behavior and prepare for PHP 8.1.
With the following code:
echo CarbonImmutable::parse('2021-11-07T00:00:00 America/New_York')->addHours(24)->format('j G') . "\n";
echo CarbonImmutable::parse('2021-11-07T00:00:00 America/New_York')->addRealHours(24)->format('j G') . "\n";
echo CarbonImmutable::parse('2021-11-07T00:00:00 America/New_York')->modify('24 hours')->format('j G') . "\n";
echo CarbonImmutable::parse('2021-11-07T00:00:00 America/New_York')->add(CarbonInterval::hours(24))->format('j G') . "\n";
echo "\n";
echo CarbonImmutable::parse('2021-11-07T00:00:00 America/New_York')->addDay()->format('j G') . "\n";
echo CarbonImmutable::parse('2021-11-07T00:00:00 America/New_York')->addRealDay()->format('j G') . "\n";
echo CarbonImmutable::parse('2021-11-07T00:00:00 America/New_York')->modify('1 day')->format('j G') . "\n";
echo CarbonImmutable::parse('2021-11-07T00:00:00 America/New_York')->add(CarbonInterval::day())->format('j G') . "\n";
echo "\n\n";
echo CarbonImmutable::parse('2021-03-14T00:00:00 America/New_York')->addHours(24)->format('j G') . "\n";
echo CarbonImmutable::parse('2021-03-14T00:00:00 America/New_York')->addRealHours(24)->format('j G') . "\n";
echo CarbonImmutable::parse('2021-03-14T00:00:00 America/New_York')->modify('24 hours')->format('j G') . "\n";
echo CarbonImmutable::parse('2021-03-14T00:00:00 America/New_York')->add(CarbonInterval::hours(24))->format('j G') . "\n";
echo "\n";
echo CarbonImmutable::parse('2021-03-14T00:00:00 America/New_York')->addDay()->format('j G') . "\n";
echo CarbonImmutable::parse('2021-03-14T00:00:00 America/New_York')->addRealDay()->format('j G') . "\n";
echo CarbonImmutable::parse('2021-03-14T00:00:00 America/New_York')->modify('1 day')->format('j G') . "\n";
echo CarbonImmutable::parse('2021-03-14T00:00:00 America/New_York')->add(CarbonInterval::day())->format('j G') . "\n";
Note due to DST in New York: There is 25 hours between 2021-11-07 midnight and 2021-11-08 midnight There is 23 hours between 2021-03-14 midnight and 2021-03-15 midnight
Carbon version: 2.48.1
Current result on PHP 8.0.0:
8 0
7 23
8 0
7 23
8 0
7 23
8 0
8 0
15 0
15 1
15 0
15 0
15 0
15 1
15 0
15 0
Current result on PHP 8.1.0-dev (WIP master branch):
8 0
7 23
8 0
7 23
8 0
7 23
8 0
8 0
15 0
15 1
15 0
15 1
15 0
15 1
15 0
15 0
Difference from 8.0 to 8.1 is:
CarbonImmutable::parse('2021-03-14T00:00:00 America/New_York')->add(CarbonInterval::hours(24))->format('j G')
8.0: 15 0 8.1: 15 1 This behavior aligns what happen on March DST change with what happen on November DST change.
For the record:
- Lines with G (hour) = 0 match the proper expectation for
adding 1 day
from New York point of view. Oradding 24 to the hour property of the date with overflow
(that's kind of real-time-agnostic point of view where you don't consider "24 hours" as a duration but as an increment of the hour unit) - Lines with G (hour) = 1 or 23 match the proper expectation for
adding 24 hours
from New York point of view (such as concretely waiting 24 hours = 24 * 3600 seconds).
PHP 8.1.0 with current 3@dev version of Carbon now provides:
7 23
7 23
8 0
7 23
8 0
7 23
8 0
8 0
15 1
15 1
15 0
15 1
15 0
15 1
15 0
15 0
Which is now consistent:
- modify() native method of PHP always modify digit components of the date with no consideration of DST that can exist in the current timezone, then readjust the timezone offset if the result is in an invalid timezone (such as CET in summer or EST in winter in Toronto), readjust here = shiftTimezone (change the timezone keeping all the digits unchanged)
- addReal* will always proceed in UTC and so consider 1 day is always 24 hours
- add* Hours/Days/etc. will be equivalent to adding an interval of the same value
- add($interval) will always take the timezone (and both +1 and -1 DST changes) into account, which mean an integer number of day added/subtracted always gives you a result with the time digits are the same than the initial date and lower units (hours, minutes, seconds, milliseconds, microseconds) will follow DST: a DST day is 23 or 25 hours long. If an interval has different units, they are added/subtracted from biggest to smallest units.
- now add/subtract operation supports decimal numbers
To be finally consistent, the addReal*
should be renamed addUTC*
(we could keep addReal*
for now in v3 but deprecated with the removal announced for v4 and recommendation to switch to addUTC*
instead or to simple add*
which are now (assuming using PHP 8.1) matching the "real".