php.net |  support |  documentation |  report a bug |  advanced search |  search howto |  statistics |  random bug |  login
Bug #42645 getdate/mktime does not always add/subtract dst offset
Submitted: 2007-09-12 16:06 UTC Modified: 2015-04-17 01:43 UTC
Votes:4
Avg. Score:4.5 ± 0.5
Reproduced:3 of 3 (100.0%)
Same Version:2 (66.7%)
Same OS:2 (66.7%)
From: carstenklein at yahoo dot de Assigned:
Status: Not a bug Package: Date/time related
PHP Version: 5.2.4 OS: Linux Ubuntu
Private report: No CVE-ID:
 [2007-09-12 16:06 UTC] carstenklein at yahoo dot de
Description:
------------
getdate() a/o mktime() do not always add/subtract the currently valid dst offset. Actually, they do only add the dst offset under certain conditions, but not always and not to all possible normal time/summer time, summer time/normal time transitions.

see below code for an example on how to reprocude the erroneous behaviour. the lines being marked with "good/failure" represent the lines where the dst offset has been added. As you can see, it happens not for all possible dates since 1970-1-1, at least in the local configuration being set to CET/CEST.

BTW: opposed to what is expected, CEST to CET transitions do not seem to work at all, I was unable to setup a scenario where the dst offset would have been subtracted from the resulting timestamp. It seems as if the mktime() or timelib functions do not take dst into account when transitioning a date from summer time to normal time.

I would therefore like mktime and the timelib to no longer try to guess the actual current time and automatically add the required dst offsets to either date and time as it is prone to error as one can clearly see in the below example outputs of below program fragment. Additionally, the results often are non reproducible and may seem quite random. If you don't believe, go and try for yourself, change the number of days to be added to the start date from 20 to for example 10 or other reasonable values. Furthermore, try to play with the starting date, make it some date in February instead of for example March, but make sure that you transition the date beyond the last Sunday in March, 1:00am. As you can see, the results will vary greatly.

Of course one could always double check the results returned by mktime, however, this is a lot of special casing etc. Therefore I urge you to at least provide a timezone offset less version of mktime in order to at least get some reproducible results.

And, using the seventh optional parameter to mktime() makes no difference at all, besides that, it is considered deprecated.


similar bug reports, all having been closed so far:

http://bugs.php.net/bug.php?id=245
http://bugs.php.net/bug.php?id=741



Reproduce code:
---------------
<?php
function addDuration( $date, $durationArray ) {
        $t = getdate( $date ); $t[ "year" ] += $durationArray[ "years" ]; $t[ "mon" ] += $durationArray[ "months" ]; $t[ "mday" ] += $durationArray[ "days" ];
        $t[ "hours" ] += $durationArray[ "hours" ]; $t[ "minutes" ] += $durationArray[ "minutes" ]; $t[ "seconds" ] += $durationArray[ "seconds" ];
        return mktime( $t[ "hours" ], $t[ "minutes" ], $t[ "seconds" ], $t[ "mon" ], $t[ "mday" ], $t[ "year" ] );
}
function durationToString( $durationArray ) {
        return "P" . $durationArray[ "years" ] . "Y" . $durationArray[ "months" ] . "M" . $durationArray[ "days" ] . "D" . "T" . $durationArray[ "hours" ] . ":" . $durationArray[ "minutes" ] . ":" . $durationArray[ "seconds" ];
}
echo "Testing Normal Time to DST Transition:\n";
for( $y = 1902; $y < 2038; $y++ ) {
        for( $x = 14; $x < 32; $x++ ) {
                $d = strtotime( "$y-03-$x 02:30:30" ); echo ( date( "Y-m-d H:i:s", $d ) ) . "\t";
                $durationArray = array( "years" => 0, "months" => 0, "days" => 20, "hours" => 0, "minutes" => 0, "seconds" => 0 ); echo "+\t" . durationToString( $durationArray ) . "\t+\t";
                $t = addDuration( $d, $durationArray ); $td = getdate( $t ); echo date( "Y-m-d H:i:s", $t ) . ( $td[ "hours" ] == 3 ? "  <-  FAILURE" : "" ) . "\n";
} }
echo "Testing DST to Normal Time Transition:\n";
for( $y = 2037; $y > 1901; $y-- ) {
        for( $x = 24; $x < 32; $x++ ) {
                $d = strtotime( "$y-10-$x 00:00:10" ); echo ( date( "Y-m-d H:i:s", $d ) ) . "\t";
                $durationArray = array( "years" => 0, "months" => 0, "days" => 1, "hours" => 0, "minutes" => 0, "seconds" => 0 ); echo "+\t" . durationToString( $durationArray ) . "\t+\t";
                $t = addDuration( $d, $durationArray ); $td = getdate( $t ); echo date( "Y-m-d H:i:s", $t ) . ( $td[ "hours" ] == 23 ? "  <-  FAILURE" : "" ) . "\n";
} }
?>


Expected result:
----------------
1981-03-25 02:30:30 + P0Y0M20DT0:0:0 + 1981-04-14 03:30:30
1981-03-26 02:30:30 + P0Y0M20DT0:0:0 + 1981-04-15 03:30:30
1981-03-27 02:30:30 + P0Y0M20DT0:0:0 + 1981-04-16 03:30:30
1981-03-28 02:30:30 + P0Y0M20DT0:0:0 + 1981-04-17 03:30:30
1981-03-29 03:30:30 + P0Y0M20DT0:0:0 + 1981-04-18 03:30:30  <-  FAILURE
1981-03-30 02:30:30 + P0Y0M20DT0:0:0 + 1981-04-19 02:30:30
1981-03-31 02:30:30 + P0Y0M20DT0:0:0 + 1981-04-20 02:30:30
1982-03-14 02:30:30 + P0Y0M20DT0:0:0 + 1982-04-03 02:30:30
1982-03-15 02:30:30 + P0Y0M20DT0:0:0 + 1982-04-04 02:30:30
1982-03-16 02:30:30 + P0Y0M20DT0:0:0 + 1982-04-05 02:30:30
1982-03-17 02:30:30 + P0Y0M20DT0:0:0 + 1982-04-06 02:30:30

* what is marked failure is actually the last sunday in march, 1981


Actual result:
--------------
1981-03-25 02:30:30 + P0Y0M20DT0:0:0 + 1981-04-14 02:30:30
1981-03-26 02:30:30 + P0Y0M20DT0:0:0 + 1981-04-15 02:30:30
1981-03-27 02:30:30 + P0Y0M20DT0:0:0 + 1981-04-16 02:30:30
1981-03-28 02:30:30 + P0Y0M20DT0:0:0 + 1981-04-17 02:30:30
1981-03-29 03:30:30 + P0Y0M20DT0:0:0 + 1981-04-18 03:30:30  <-  FAILURE
1981-03-30 02:30:30 + P0Y0M20DT0:0:0 + 1981-04-19 02:30:30
1981-03-31 02:30:30 + P0Y0M20DT0:0:0 + 1981-04-20 02:30:30
1982-03-14 02:30:30 + P0Y0M20DT0:0:0 + 1982-04-03 02:30:30
1982-03-15 02:30:30 + P0Y0M20DT0:0:0 + 1982-04-04 02:30:30
1982-03-16 02:30:30 + P0Y0M20DT0:0:0 + 1982-04-05 02:30:30
1982-03-17 02:30:30 + P0Y0M20DT0:0:0 + 1982-04-06 02:30:30

*) what is marked as FAILURE is actually correct, however,
all the other dates before also transition from non-dst to dst,
in the local configuration, i.e. CET to CEST, so these
should also have a local time reading of 03:30:30 instead
of just 02:30:30.

Patches

Add a Patch

Pull Requests

Add a Pull Request

History

AllCommentsChangesGit/SVN commitsRelated reports
 [2007-09-12 18:04 UTC] carstenklein at yahoo dot de
Small err on my behalf, where it reads:

"result often are non reproducible and may seem quite random"

it must read:

"result are reproducible but may seem quite random"

and

"Therefore I urge you to at least provide a timezone offset less version of mktime in order to at least get some reproducible results."

it should read

"Therefore I urge you to at least provide a timezone offset less version of mktime and let the user do the rest."



A provable solution would be to take the seventh, now deprecated parameter to mktime and make it a flag indicating wether or not offsets will be applied to the resulting date/time.
 [2007-09-13 08:09 UTC] jani@php.net
Assigned to the ext/date maintainer. :)
 [2007-10-17 20:12 UTC] carstenklein at yahoo dot de
Additional information:

it seems as if the timelib or some component thereof behaves like a clock that will only adjust the dst when transitioning from the day that the transition is being scheduled for depending on the current timezone setting.

This is actually hard to figure out since the information required to find out whether there is a scheduled dst transition on a particular date is actually not available from within php, AFAIK.

A more sophisticated application therefore must replicate the timezone database along with all the code in order to find out whether or not a dst transition should take place on a particular date.

Here are a few pointers as to how this problem could actually be solved:

a) make available dst transitioning dates via the date/time api in order for the user to find out whether or not on a particular date a dst transition will take place

or

b) leave it to the user whether or not to take care of dst transitioning by altogether removing existing behaviour


the latter would actually break with existing applications, therefore, 
the initial proposal of reusing the obsoleted seventh parameter to mktime for indicating whether or not dst should be applied seems to be the less painful one. additionally, a) should also be taken into consideration in order for the user not having to reimplement a timezone database along with all the possible transition dates for applying or removing dst.

Hope this helps with your task.

Regards
Carsten
 [2008-11-02 13:03 UTC] jani@php.net
Please try using this CVS snapshot:

  http://snaps.php.net/php5.2-latest.tar.gz
 
For Windows:

  http://windows.php.net/snapshots/


 [2008-11-10 01:00 UTC] php-bugs at lists dot php dot net
No feedback was provided for this bug for over a week, so it is
being suspended automatically. If you are able to provide the
information that was originally requested, please do so and change
the status of the bug back to "Open".
 [2015-04-08 07:52 UTC] carstenklein at yahoo dot de
No feedback does not mean that the bug does not exist. Especially when one provided code to actually reproduce it.

BTW the issue still exists. Currently testing this with 
PHP 5.6.7-1 (cli) (built: Mar 24 2015 12:30:15).
 [2015-04-08 08:52 UTC] requinix@php.net
-Status: No Feedback +Status: Feedback
 [2015-04-08 08:52 UTC] requinix@php.net
Could be there is a bug, yes. Mostly the No Feedback status means that the reporter did not answer someone's question. A question such as "does this latest snapshot fix your problem?"


There's a ton of irrelevant output and crazy looking code that's a chore to sift through, so to be clear:

The bug you're reporting is that spring leap date-times like 1981-03-29 2:30 AM, plus an interval of some number of days, results in a time that's 3:30 AM, while other days around then remain as 2:30?

Does this repro it? http://3v4l.org/ddo01
 [2015-04-08 14:14 UTC] carstenklein at yahoo dot de
I do not know whether this would reproduce it, as there is an error preventing the script from running:

Timezone: 
Fatal error: Call to undefined function date_default_timezone_get() in /in/ddo01 on line 3

Process exited with code 255.
 [2015-04-08 14:17 UTC] carstenklein at yahoo dot de
Yes, it does reproduce the issue, the error occurs with older PHP versions I believe.

Timezone: Europe/Amsterdam

Expected: 1981-03-28 02:30:00 + 3 days = 1981-03-31 02:30:00
Actual:   1981-03-28 02:30:00 + 3 days = 1981-03-31 02:30:00

Expected: 1981-03-29 02:30:00 + 3 days = 1981-04-01 02:30:00
Actual:   1981-03-29 03:30:00 + 3 days = 1981-04-01 03:30:00    <--- ERR

Expected: 1981-03-30 02:30:00 + 3 days = 1981-04-02 02:30:00
Actual:   1981-03-30 02:30:00 + 3 days = 1981-04-02 02:30:00
 [2015-04-08 20:46 UTC] requinix@php.net
-Status: Feedback +Status: Not a bug -Assigned To: derick +Assigned To:
 [2015-04-08 20:46 UTC] requinix@php.net
Alright. Then after almost 8 years of this bug being around, I'm going to close this as NAB. Here's why:

The most important thing is that 1981-03-29 02:30:00 is not a valid time (in that timezone). It does not exist. See how the code tries to use 2:30 for the start time but the actual printout says 3:30? The most reasonable interpretation of 2:30 is "2 hours 30 minutes after midnight", but because of daylight savings and the jump from 1:59:59 to 3:00:00, that time turns out to be 3:30 AM.

If you add whatever day interval to that then you're still going to get 3:30 AM because (1) it didn't start at 2:30 and (2) all "+N days" does is add N to the day number, then potentially adjust for wrapping out-of-bounds numbers and such (eg, 3/29 +3 days = 3/32, which wraps to 4/1).

So you can't actually start with a datetime of 1981-03-29 02:MM:SS. You can, however, simulate that by using mktime() *and adding your day interval at the same time*. As in

mktime(2, 30, 0, 3, 29 + 3, 1981) // adding the day at this point
http://3v4l.org/8ob1c

By doing that you bypass the "invalid time" problem but still make use of the "adjust for wrapping" feature. It also more closely reflects the logical process of "take this date and add 3 days to it" that a human would follow.
 [2015-04-08 21:05 UTC] carstenklein at yahoo dot de
It seems as if you are missing the actual point here.

Actual:   1981-03-29 03:30:00 + 3 days = 1981-04-01 03:30:00    <--- ERR


See the actual, it already has the timezone offset added to it. Naturally, the calculated datetime will have it, too.

So the actual problem lies within 

$t = getdate( $date )

which will already apply the dst offset on a rather random basis.

As such I would consider this a standing bug rather than a NAB.
 [2015-04-08 21:10 UTC] carstenklein at yahoo dot de
Or rather not so much random as sunday is considered the first day of the week in some regions of the world.

Perhaps in getdate() there is some special cased dst adjustment logic that needs to be eliminated?
 [2015-04-08 21:14 UTC] carstenklein at yahoo dot de
*) what is marked as FAILURE is actually correct, however,
all the other dates before also transition from non-dst to dst,
in the local configuration, i.e. CET to CEST, so these
should also have a local time reading of 03:30:30 instead
of just 02:30:30.


considering the above, overall dst behaviour of getdate() seems to be off.

and besides of that, if you feel happy with your current solution, go on with it as I have moved pass PHP and am no longer using it.
 [2015-04-08 21:18 UTC] carstenklein at yahoo dot de
One more thing, though

1981-03-29 02:30:00

is a very valid date and time.

Not so much

1981-02-29 02:30:00

which I believe you had in mind.
 [2015-04-08 22:15 UTC] requinix@php.net
Reply to the invalid thing first:

>One more thing, though
>1981-03-29 02:30:00
>is a very valid date and time.
>Not so much
>1981-02-29 02:30:00
>which I believe you had in mind.
Not in the Europe/Amsterdam timezone, which is where the 3v4l code I posted is running. Their 1981 spring DST change was on 1981-03-29 and they went from 01:59:59 to 03:00:00. There was no 02:30:00 on that day. So, invalid.

And the rest:

>See the actual, it already has the timezone offset added to it.
"Already" because the code tries to use 2:30, which isn't valid (for that date), and the most reasonable interpretation of that would be after the timezone offset change where it becomes 3:30.

>$t = getdate( $date )
Not sure what you're trying to say with this...

getdate() seems to be behaving as it should.
http://3v4l.org/OCfb2
For the 28th and 30th there's nothing special, but for the 29th it does the same thing demonstrated in my code where it wraps 2:30 to 3:30. Because, again, invalid time (for that date).

>which will already apply the dst offset on a rather random basis.
DST was only applied in one place: when correcting the start time given for the 3/29 date.

>Or rather not so much random as sunday is considered the first day of the week in some regions of the world.
Sunday being the first day or not is irrelevant to any of this: it's purely a matter of DST changes.
 [2015-04-16 20:50 UTC] carstenklein at yahoo dot de
Here is another example running the original script for the Europe/Berlin timezone for a time span that certainly has DST in place. I wonder where these extra hours come from?

Timezone: Europe/Berlin

Testing Normal Time to DST Transition:
1980-03-17 02:30:30     +       P0Y0M20DT0:0:0  +       1980-04-06 03:30:30 <-  FAILURE
1981-03-29 03:30:30     +       P0Y0M20DT0:0:0  +       1981-04-18 03:30:30 <-  FAILURE
1982-03-28 03:30:30     +       P0Y0M20DT0:0:0  +       1982-04-17 03:30:30 <-  FAILURE
1983-03-27 03:30:30     +       P0Y0M20DT0:0:0  +       1983-04-16 03:30:30 <-  FAILURE
1984-03-25 03:30:30     +       P0Y0M20DT0:0:0  +       1984-04-14 03:30:30 <-  FAILURE
1985-03-31 03:30:30     +       P0Y0M20DT0:0:0  +       1985-04-20 03:30:30 <-  FAILURE
1986-03-30 03:30:30     +       P0Y0M20DT0:0:0  +       1986-04-19 03:30:30 <-  FAILURE
1987-03-29 03:30:30     +       P0Y0M20DT0:0:0  +       1987-04-18 03:30:30 <-  FAILURE
1988-03-27 03:30:30     +       P0Y0M20DT0:0:0  +       1988-04-16 03:30:30 <-  FAILURE
1989-03-26 03:30:30     +       P0Y0M20DT0:0:0  +       1989-04-15 03:30:30 <-  FAILURE
1990-03-25 03:30:30     +       P0Y0M20DT0:0:0  +       1990-04-14 03:30:30 <-  FAILURE
1991-03-31 03:30:30     +       P0Y0M20DT0:0:0  +       1991-04-20 03:30:30 <-  FAILURE
1992-03-29 03:30:30     +       P0Y0M20DT0:0:0  +       1992-04-18 03:30:30 <-  FAILURE
1993-03-28 03:30:30     +       P0Y0M20DT0:0:0  +       1993-04-17 03:30:30 <-  FAILURE
1994-03-27 03:30:30     +       P0Y0M20DT0:0:0  +       1994-04-16 03:30:30 <-  FAILURE
1995-03-26 03:30:30     +       P0Y0M20DT0:0:0  +       1995-04-15 03:30:30 <-  FAILURE
1996-03-31 03:30:30     +       P0Y0M20DT0:0:0  +       1996-04-20 03:30:30 <-  FAILURE
1997-03-30 03:30:30     +       P0Y0M20DT0:0:0  +       1997-04-19 03:30:30 <-  FAILURE
1998-03-29 03:30:30     +       P0Y0M20DT0:0:0  +       1998-04-18 03:30:30 <-  FAILURE
1999-03-28 03:30:30     +       P0Y0M20DT0:0:0  +       1999-04-17 03:30:30 <-  FAILURE
2000-03-26 03:30:30     +       P0Y0M20DT0:0:0  +       2000-04-15 03:30:30 <-  FAILURE
2001-03-25 03:30:30     +       P0Y0M20DT0:0:0  +       2001-04-14 03:30:30 <-  FAILURE
2002-03-31 03:30:30     +       P0Y0M20DT0:0:0  +       2002-04-20 03:30:30 <-  FAILURE
2003-03-30 03:30:30     +       P0Y0M20DT0:0:0  +       2003-04-19 03:30:30 <-  FAILURE
2004-03-28 03:30:30     +       P0Y0M20DT0:0:0  +       2004-04-17 03:30:30 <-  FAILURE
2005-03-27 03:30:30     +       P0Y0M20DT0:0:0  +       2005-04-16 03:30:30 <-  FAILURE
2006-03-26 03:30:30     +       P0Y0M20DT0:0:0  +       2006-04-15 03:30:30 <-  FAILURE
2007-03-25 03:30:30     +       P0Y0M20DT0:0:0  +       2007-04-14 03:30:30 <-  FAILURE
2008-03-30 03:30:30     +       P0Y0M20DT0:0:0  +       2008-04-19 03:30:30 <-  FAILURE
2009-03-29 03:30:30     +       P0Y0M20DT0:0:0  +       2009-04-18 03:30:30 <-  FAILURE
2010-03-28 03:30:30     +       P0Y0M20DT0:0:0  +       2010-04-17 03:30:30 <-  FAILURE
2011-03-27 03:30:30     +       P0Y0M20DT0:0:0  +       2011-04-16 03:30:30 <-  FAILURE
2012-03-25 03:30:30     +       P0Y0M20DT0:0:0  +       2012-04-14 03:30:30 <-  FAILURE
2013-03-31 03:30:30     +       P0Y0M20DT0:0:0  +       2013-04-20 03:30:30 <-  FAILURE
2014-03-30 03:30:30     +       P0Y0M20DT0:0:0  +       2014-04-19 03:30:30 <-  FAILURE
2015-03-29 03:30:30     +       P0Y0M20DT0:0:0  +       2015-04-18 03:30:30 <-  FAILURE
2016-03-27 03:30:30     +       P0Y0M20DT0:0:0  +       2016-04-16 03:30:30 <-  FAILURE
2017-03-26 03:30:30     +       P0Y0M20DT0:0:0  +       2017-04-15 03:30:30 <-  FAILURE
2018-03-25 03:30:30     +       P0Y0M20DT0:0:0  +       2018-04-14 03:30:30 <-  FAILURE
2019-03-31 03:30:30     +       P0Y0M20DT0:0:0  +       2019-04-20 03:30:30 <-  FAILURE
2020-03-29 03:30:30     +       P0Y0M20DT0:0:0  +       2020-04-18 03:30:30 <-  FAILURE
2021-03-28 03:30:30     +       P0Y0M20DT0:0:0  +       2021-04-17 03:30:30 <-  FAILURE
2022-03-27 03:30:30     +       P0Y0M20DT0:0:0  +       2022-04-16 03:30:30 <-  FAILURE
2023-03-26 03:30:30     +       P0Y0M20DT0:0:0  +       2023-04-15 03:30:30 <-  FAILURE
2024-03-31 03:30:30     +       P0Y0M20DT0:0:0  +       2024-04-20 03:30:30 <-  FAILURE
2025-03-30 03:30:30     +       P0Y0M20DT0:0:0  +       2025-04-19 03:30:30 <-  FAILURE
2026-03-29 03:30:30     +       P0Y0M20DT0:0:0  +       2026-04-18 03:30:30 <-  FAILURE
2027-03-28 03:30:30     +       P0Y0M20DT0:0:0  +       2027-04-17 03:30:30 <-  FAILURE
2028-03-26 03:30:30     +       P0Y0M20DT0:0:0  +       2028-04-15 03:30:30 <-  FAILURE
2029-03-25 03:30:30     +       P0Y0M20DT0:0:0  +       2029-04-14 03:30:30 <-  FAILURE
2030-03-31 03:30:30     +       P0Y0M20DT0:0:0  +       2030-04-20 03:30:30 <-  FAILURE
2031-03-30 03:30:30     +       P0Y0M20DT0:0:0  +       2031-04-19 03:30:30 <-  FAILURE
2032-03-28 03:30:30     +       P0Y0M20DT0:0:0  +       2032-04-17 03:30:30 <-  FAILURE
2033-03-27 03:30:30     +       P0Y0M20DT0:0:0  +       2033-04-16 03:30:30 <-  FAILURE
2034-03-26 03:30:30     +       P0Y0M20DT0:0:0  +       2034-04-15 03:30:30 <-  FAILURE
2035-03-25 03:30:30     +       P0Y0M20DT0:0:0  +       2035-04-14 03:30:30 <-  FAILURE
2036-03-30 03:30:30     +       P0Y0M20DT0:0:0  +       2036-04-19 03:30:30 <-  FAILURE
2037-03-29 03:30:30     +       P0Y0M20DT0:0:0  +       2037-04-18 03:30:30 <-  FAILURE
 [2015-04-17 01:43 UTC] requinix@php.net
Still daylight savings. Still time overflowing from 2:30 to 3:30.

The spring DST dates for the first few of those years are:
1980: 4/6
1981: 3/29
1982: 3/28
1983: 3/27
1984: 3/25
http://www.timeanddate.com/time/change/germany/berlin?year=1980

All but the first (that I sampled) are still the exact same thing I described earlier: starting time overflowed to 3:30, thus the ending time stayed at 3:30.

For the first one, the DST date was at the *end* of the period which is why the starting time was 2:30 but the ending time was 3:30.
 
PHP Copyright © 2001-2017 The PHP Group
All rights reserved.
Last updated: Sat Jun 24 12:01:43 2017 UTC