====== Request for Comments: DateTime and Daylight Saving Time Transitions ====== * Version: 1.0 * Date: 2011-10-18 * Author: Daniel Convissor with feedback from Derick Rethans * Status: Accepted ([[rfc:datetime_and_daylight_saving_time:vote|voting results]]) * First Published at: https://wiki.php.net/rfc/datetime_and_daylight_saving_time ===== Introduction ===== PHP's DateTime class has unexpected outcomes when dealing with the transitions between Daylight Saving Time and Standard Time. Properly defining, documenting and unit testing DateTime's behaviors is important for PHP's future. This document seeks agreement on what the expected behaviors should be. Getting these issues straightened out before 5.4 goes into Release Candidate status seems wise. ===== Zone Types ===== PHP uses three different time zone data structures internally. Zone Type 3 objects contain the transition data required for proper calculations around the Daylight/Standard time changes. Zone Type 1 and 2 objects just know their offset from UTC, so addition and subtraction on them ignore crossing over Daylight/Standard thresholds. This RFC focuses on the changes needed to make Zone Type 3 work in a comprehensive manner. Zone Type 1 and 2 behaviors are covered to clarify their expected behavior. ==== Examples ==== Examples of how to create DateTime objects containing each zone type: === Zone Type 1: Offset === $d = new DateTime('2011-09-13 14:15:16 -0400'); === Zone Type 2: Abbreviation === $d = new DateTime('2011-09-13 14:15:16 EDT'); === Zone Type 3: ID === $d = new DateTime('2011-09-13 14:15:16'); // or $d = new DateTime('2011-09-13 14:15:16', new DateTimeZone('America/New_York')); ===== Time Zone for Sake of Discussion ===== The ''America/New_York'' time zone will be used for demonstration purposes. The Standard/Daylight transition in this zone happens at 2:00 am. The forward transition (in spring) jumps to 3:00 am, skipping over the hour of 2 am. The backward transition (in fall) jumps back to 1:00 am, repeating the hour of 1 am. Other time zone changes have different lengths of time and take effect at different days/times. ===== Construction During Transitions ===== ==== Forward Transitions ==== Times between ''2:00:00 am'' and ''2:59:59 am'' do not exist during the Forward Transitions transition. Attempts to create such times should be rounded forward, like PHP currently does when trying to create February 29th on a non-leap year. date_default_timezone_set('America/New_York'); $date = new DateTime('2010-03-14 02:30:00'); echo $date->format('Y-m-d H:i:s T') . "\n"; // Expected: 2010-03-14 03:30:00 EDT // Actual: 2010-03-14 02:30:00 EDT ==== Backward Transitions ==== PHP needs a means to create objects representing times during the hour repeated on the Daylight/Standard transition day (between ''1:00:00 am'' and ''1:59:59 am'' Standard Time). This RFC proposes adjusting the time string parser to handle strings containing ''DST'' and ''ST''. When ''DST'' or ''ST'' are not present, PHP will use the current behavior, which uses the time zone offset in place at the specified time and uses DST offset during the repeated hour during backward transitions. Force Daylight Saving Time: date_default_timezone_set('America/New_York'); $date = new DateTime('2010-11-07 01:30:00 DST'); echo $date->format('Y-m-d H:i:s T') . "\n"; // Expected: 2010-11-07 01:30:00 EDT Force Standard Time: date_default_timezone_set('America/New_York'); $date = new DateTime('2010-11-07 01:30:00 ST'); echo $date->format('Y-m-d H:i:s T') . "\n"; // Expected: 2010-11-07 01:30:00 EST Default to Daylight Time during repeated hour, PHP's current behavior: date_default_timezone_set('America/New_York'); $date = new DateTime('2010-11-07 01:30:00'); echo $date->format('Y-m-d H:i:s T') . "\n"; // Expected & Actual: 2010-11-07 01:30:00 EDT The ''ST'' or ''DST'' modifiers can only be used when specifying times during the backward transition period. Using the modifiers at other times will throw an exception in object-oriented style code while procedural style code will return false without triggering errors. ===== Math ===== The date format used in this section is for display purposes only, does not correspond to a PHP format, nor is intended to be parsed by PHP. Reminder: the time zone in the examples is ''America/New_York''. So "(ST)" represents Eastern Standard Time/EST/-0500 and "(DT)" represents Eastern Daylight Time/EDT/-0400. The behaviors indicated here are covered by unit tests in ''ext/date/tests'': * rfc-datetime_and_daylight_saving_time-type1.phpt * rfc-datetime_and_daylight_saving_time-type2.phpt * rfc-datetime_and_daylight_saving_time-type3.phpt Each output line in the test files is prefixed with an identifier that corresponds to the ''phpt'' columns in the tables below. Actual results for Zone Types 1 and 2 are shown as ''N/A'' for the moment. All Zone Type 1 tests pass. Zone Type 2 results are tainted by [[https://bugs.php.net/bug.php?id=55253|Bug 55253]]. ==== Forward Transitions ==== === Zone Type 3 === == diff() == ^ End ^ Start ^ Expected ^ phpt ^ Actual (if wrong) ^ | ''2010-03-14T03:00:00''//(DT)// | ''2010-03-14T01:59:59''//(ST)// | PT0H0M1S | fd1 | PT1H0M1S | | ''2010-03-14T04:30:00''//(DT)// | ''2010-03-13T04:30:00''//(ST)// | P1DT0H | fd2 | | | ''2010-03-14T03:30:00''//(DT)// | ''2010-03-13T04:30:00''//(ST)// | P0DT22H | fd3 | P0DT23H | | ''2010-03-14T01:30:00''//(ST)// | ''2010-03-13T04:30:00''//(ST)// | P0DT21H | fd4 | | | ''2010-03-14T01:30:00''//(ST)// | ''2010-03-13T01:30:00''//(ST)// | P1DT0H | fd5 | | | ''2010-03-14T03:30:00''//(DT)// | ''2010-03-13T03:30:00''//(ST)// | P1DT0H | fd6 | | | ''2010-03-14T03:30:00''//(DT)// | ''2010-03-13T02:30:00''//(ST)// | P1DT1H | fd7 | | == add() == ^ Start ^ Interval ^ Expected ^ phpt ^ Actual (if wrong) ^ | ''2010-03-14T01:59:59''//(ST)// | PT1S | ''2010-03-14T03:00:00''//(DT)// | fa1 | | | ''2010-03-13T04:30:00''//(ST)// | P1D | ''2010-03-14T04:30:00''//(DT)// | fa2 | | | ''2010-03-13T04:30:00''//(ST)// | PT22H | ''2010-03-14T03:30:00''//(DT)// | fa3 | | | ''2010-03-13T04:30:00''//(ST)// | PT21H | ''2010-03-14T01:30:00''//(ST)// | fa4 | | | ''2010-03-13T01:30:00''//(ST)// | P1D | ''2010-03-14T01:30:00''//(ST)// | fa5 | | | ''2010-03-13T02:30:00''//(ST)// | P1D | ''2010-03-14T02:30:00''//(DT)// | fa6 | | == sub() == ^ End ^ Interval ^ Expected ^ phpt ^ Actual (if wrong) ^ | ''2010-03-14T03:00:00''//(DT)// | PT1S | ''2010-03-14T01:59:59''//(ST)// | fs1 | ''2010-03-14T03:59:59''//(DT)// | | ''2010-03-14T04:30:00''//(DT)// | P1D | ''2010-03-13T04:30:00''//(ST)// | fs2 | | | ''2010-03-14T03:30:00''//(DT)// | PT22H | ''2010-03-13T04:30:00''//(ST)// | fs3 | ''2010-03-13T05:30:00''//(ST)// | | ''2010-03-14T01:30:00''//(ST)// | PT21H | ''2010-03-13T04:30:00''//(ST)// | fs4 | | | ''2010-03-14T01:30:00''//(ST)// | P1D | ''2010-03-13T01:30:00''//(ST)// | fs5 | | | ''2010-03-15T03:30:00''//(DT)// | P1D | ''2010-03-14T03:30:00''//(DT)// | fs6 | | | ''2010-03-15T02:30:00''//(DT)// | P1D | ''2010-03-14T03:30:00''//(DT)// | fs7 | | === Zone Types 1 & 2 === == diff() == ^ End ^ Start ^ Expected ^ phpt ^ Actual (if wrong) ^ | ''2010-03-14T03:00:00''//(DT)// | ''2010-03-14T01:59:59''//(ST)// | PT0H0M1S | fd1 | | | ''2010-03-14T04:30:00''//(DT)// | ''2010-03-13T04:30:00''//(ST)// | P0DT23H | fd2 | | | ''2010-03-14T03:30:00''//(DT)// | ''2010-03-13T04:30:00''//(ST)// | P0DT22H | fd3 | | | ''2010-03-14T01:30:00''//(ST)// | ''2010-03-13T04:30:00''//(ST)// | P0DT21H | fd4 | | | ''2010-03-14T01:30:00''//(ST)// | ''2010-03-13T01:30:00''//(ST)// | P1DT0H | fd5 | | | ''2010-03-14T03:30:00''//(DT)// | ''2010-03-13T03:30:00''//(ST)// | P0DT23H | fd6 | | | ''2010-03-14T03:30:00''//(DT)// | ''2010-03-13T02:30:00''//(ST)// | P1DT0H | fd7 | | == add() == ^ Start ^ Interval ^ Expected ^ phpt ^ Actual (if wrong) ^ | ''2010-03-14T01:59:59''//(ST)// | PT1S | ''2010-03-14T02:00:00''//(ST)// | fa1 | N/A | | ''2010-03-13T04:30:00''//(ST)// | P1D | ''2010-03-14T04:30:00''//(ST)// | fa2 | N/A | | ''2010-03-13T04:30:00''//(ST)// | PT22H | ''2010-03-14T02:30:00''//(ST)// | fa3 | N/A | | ''2010-03-13T04:30:00''//(ST)// | PT21H | ''2010-03-14T01:30:00''//(ST)// | fa4 | N/A | | ''2010-03-13T01:30:00''//(ST)// | P1D | ''2010-03-14T01:30:00''//(ST)// | fa5 | N/A | | ''2010-03-13T02:30:00''//(ST)// | P1D | ''2010-03-14T02:30:00''//(ST)// | fa6 | N/A | == sub() == ^ End ^ Interval ^ Expected ^ phpt ^ Actual (if wrong) ^ | ''2010-03-14T03:00:00''//(DT)// | PT1S | ''2010-03-14T02:59:59''//(DT)// | fs1 | N/A | | ''2010-03-14T04:30:00''//(DT)// | P1D | ''2010-03-13T04:30:00''//(DT)// | fs2 | N/A | | ''2010-03-14T03:30:00''//(DT)// | PT22H | ''2010-03-13T05:30:00''//(DT)// | fs3 | N/A | | ''2010-03-14T01:30:00''//(ST)// | PT21H | ''2010-03-13T04:30:00''//(ST)// | fs4 | N/A | | ''2010-03-14T01:30:00''//(ST)// | P1D | ''2010-03-13T01:30:00''//(ST)// | fs5 | N/A | | ''2010-03-15T03:30:00''//(DT)// | P1D | ''2010-03-14T03:30:00''//(DT)// | fs6 | N/A | | ''2010-03-15T02:30:00''//(DT)// | P1D | ''2010-03-14T02:30:00''//(DT)// | fs7 | N/A | ==== Backward Transitions ==== === Zone Type 3 === == diff() == ^ End ^ Start ^ Expected ^ phpt ^ Actual (if wrong) ^ | ''2010-11-07T01:00:00''//(ST)// | ''2010-11-07T01:59:59''//(DT)// | PT0H0M1S | bd1 | PT0H59M59S | | ''2010-11-07T04:30:00''//(ST)// | ''2010-11-06T04:30:00''//(DT)// | P1DT0H | bd2 | | | ''2010-11-07T03:30:00''//(ST)// | ''2010-11-06T04:30:00''//(DT)// | P0DT24H | bd3 | P0DT23H | | ''2010-11-07T02:30:00''//(ST)// | ''2010-11-06T04:30:00''//(DT)// | P0DT23H | bd4 | P0DT22H | | ''2010-11-07T01:30:00''//(ST)// | ''2010-11-06T04:30:00''//(DT)// | P0DT22H | bd5 | P0DT21H | | ''2010-11-07T01:30:00''//(DT)// | ''2010-11-06T04:30:00''//(DT)// | P0DT21H | bd6 | | | ''2010-11-07T01:30:00''//(DT)// | ''2010-11-06T01:30:00''//(DT)// | P1DT0H | bd7 | | | ''2010-11-07T01:30:00''//(ST)// | ''2010-11-06T01:30:00''//(DT)// | P1DT1H | bd8 | P1DT1H | == add() == ^ Start ^ Interval ^ Expected ^ phpt ^ Actual (if wrong) ^ | ''2010-11-07T01:59:59''//(DT)// | PT1S | ''2010-11-07T01:00:00''//(ST)// | ba1 | ''2010-11-07T01:00:00''//(ST)// | | ''2010-11-06T04:30:00''//(DT)// | P1D | ''2010-11-07T04:30:00''//(ST)// | ba2 | | | ''2010-11-06T04:30:00''//(DT)// | PT24H | ''2010-11-07T03:30:00''//(ST)// | ba3 | ''2010-11-07T04:30:00''//(ST)// | | ''2010-11-06T04:30:00''//(DT)// | PT23H | ''2010-11-07T02:30:00''//(ST)// | ba4 | ''2010-11-07T03:30:00''//(ST)// | | ''2010-11-06T04:30:00''//(DT)// | PT22H | ''2010-11-07T01:30:00''//(ST)// | ba5 | ''2010-11-07T02:30:00''//(ST)// | | ''2010-11-06T04:30:00''//(DT)// | PT21H | ''2010-11-07T01:30:00''//(DT)// | ba6 | | | ''2010-11-06T01:30:00''//(DT)// | P1D | ''2010-11-07T01:30:00''//(DT)// | ba7 | | | ''2010-11-06T01:30:00''//(DT)// | P1DT1H | ''2010-11-07T01:30:00''//(ST)// | ba8 | ''2010-11-07T02:30:00''//(ST)// | | ''2010-11-06T04:30:00''//(DT)// | PT25H | ''2010-11-07T04:30:00''//(ST)// | ba9 | ''2010-11-07T05:30:00''//(ST)// | | ''2010-11-06T03:30:00''//(DT)// | P1D | ''2010-11-07T03:30:00''//(ST)// | ba10| | | ''2010-11-06T02:30:00''//(DT)// | P1D | ''2010-11-07T02:30:00''//(ST)// | ba11| | == sub() == ^ End ^ Interval ^ Expected ^ phpt ^ Actual (if wrong) ^ | ''2010-11-07T01:00:00''//(ST)// | PT1S | ''2010-11-07T01:59:59''//(DT)// | bs1 | ''2010-11-07T00:59:59''//(DT)// | | ''2010-11-07T04:30:00''//(ST)// | P1D | ''2010-11-06T04:30:00''//(DT)// | bs2 | | | ''2010-11-07T03:30:00''//(ST)// | PT24H | ''2010-11-06T04:30:00''//(DT)// | bs3 | ''2010-11-06T03:30:00''//(DT)// | | ''2010-11-07T02:30:00''//(ST)// | PT23H | ''2010-11-06T04:30:00''//(DT)// | bs4 | ''2010-11-06T03:30:00''//(DT)// | | ''2010-11-07T01:30:00''//(ST)// | PT22H | ''2010-11-06T04:30:00''//(DT)// | bs5 | ''2010-11-06T03:30:00''//(DT)// | | ''2010-11-07T01:30:00''//(DT)// | PT21H | ''2010-11-06T04:30:00''//(DT)// | bs6 | | | ''2010-11-07T01:30:00''//(DT)// | P1D | ''2010-11-06T01:30:00''//(DT)// | bs7 | | | ''2010-11-07T01:30:00''//(ST)// | P1DT1H | ''2010-11-06T00:30:00''//(DT)// | bs8 | | | ''2010-11-07T03:30:00''//(ST)// | P1D | ''2010-11-06T03:30:00''//(DT)// | bs9 | | | ''2010-11-07T02:30:00''//(ST)// | P1D | ''2010-11-06T02:30:00''//(DT)// | bs10 | | === Zone Types 1 & 2 === == diff() == ^ End ^ Start ^ Expected ^ phpt ^ Actual (if wrong) ^ | ''2010-11-07T01:00:00''//(ST)// | ''2010-11-07T01:59:59''//(DT)// | PT0H0M1S | bd1 | | | ''2010-11-07T04:30:00''//(ST)// | ''2010-11-06T04:30:00''//(DT)// | P1DT1H | bd2 | | | ''2010-11-07T03:30:00''//(ST)// | ''2010-11-06T04:30:00''//(DT)// | P1DT0H | bd3 | | | ''2010-11-07T02:30:00''//(ST)// | ''2010-11-06T04:30:00''//(DT)// | P0DT23H | bd4 | | | ''2010-11-07T01:30:00''//(ST)// | ''2010-11-06T04:30:00''//(DT)// | P0DT22H | bd5 | | | ''2010-11-07T01:30:00''//(DT)// | ''2010-11-06T04:30:00''//(DT)// | P0DT21H | bd6 | | | ''2010-11-07T01:30:00''//(DT)// | ''2010-11-06T01:30:00''//(DT)// | P1DT0H | bd7 | | | ''2010-11-07T01:30:00''//(ST)// | ''2010-11-06T01:30:00''//(DT)// | P1DT1H | bd8 | | == add() == ^ Start ^ Interval ^ Expected ^ phpt ^ Actual (if wrong) ^ | ''2010-11-07T01:59:59''//(DT)// | PT1S | ''2010-11-07T02:00:00''//(DT)// | ba1 | N/A | | ''2010-11-06T04:30:00''//(DT)// | P1D | ''2010-11-07T04:30:00''//(DT)// | ba2 | N/A | | ''2010-11-06T04:30:00''//(DT)// | PT24H | ''2010-11-07T04:30:00''//(DT)// | ba3 | N/A | | ''2010-11-06T04:30:00''//(DT)// | PT23H | ''2010-11-07T03:30:00''//(DT)// | ba4 | N/A | | ''2010-11-06T04:30:00''//(DT)// | PT22H | ''2010-11-07T02:30:00''//(DT)// | ba5 | N/A | | ''2010-11-06T04:30:00''//(DT)// | PT21H | ''2010-11-07T01:30:00''//(DT)// | ba6 | N/A | | ''2010-11-06T01:30:00''//(DT)// | P1D | ''2010-11-07T01:30:00''//(DT)// | ba7 | N/A | | ''2010-11-06T01:30:00''//(DT)// | P1DT1H | ''2010-11-07T02:30:00''//(DT)// | ba8 | N/A | | ''2010-11-06T04:30:00''//(DT)// | PT25H | ''2010-11-07T05:30:00''//(DT)// | ba9 | N/A | | ''2010-11-06T03:30:00''//(DT)// | P1D | ''2010-11-07T03:30:00''//(DT)// | ba10| N/A | | ''2010-11-06T02:30:00''//(DT)// | P1D | ''2010-11-07T02:30:00''//(DT)// | ba11| N/A | == sub() == ^ End ^ Interval ^ Expected ^ phpt ^ Actual (if wrong) ^ | ''2010-11-07T01:00:00''//(ST)// | PT1S | ''2010-11-07T00:59:59''//(ST)// | bs1 | N/A | | ''2010-11-07T04:30:00''//(ST)// | P1D | ''2010-11-06T04:30:00''//(ST)// | bs2 | N/A | | ''2010-11-07T03:30:00''//(ST)// | PT24H | ''2010-11-06T03:30:00''//(ST)// | bs3 | N/A | | ''2010-11-07T02:30:00''//(ST)// | PT23H | ''2010-11-06T03:30:00''//(ST)// | bs4 | N/A | | ''2010-11-07T01:30:00''//(ST)// | PT22H | ''2010-11-06T03:30:00''//(ST)// | bs5 | N/A | | ''2010-11-07T01:30:00''//(DT)// | PT21H | ''2010-11-06T04:30:00''//(DT)// | bs6 | N/A | | ''2010-11-07T01:30:00''//(DT)// | P1D | ''2010-11-06T01:30:00''//(DT)// | bs7 | N/A | | ''2010-11-07T01:30:00''//(ST)// | P1DT1H | ''2010-11-06T00:30:00''//(ST)// | bs8 | N/A | | ''2010-11-07T03:30:00''//(ST)// | P1D | ''2010-11-06T03:30:00''//(ST)// | bs9 | N/A | | ''2010-11-07T02:30:00''//(ST)// | P1D | ''2010-11-06T02:30:00''//(ST)// | bs10 | N/A | ===== Other Issues? ===== If you know of other problems, please bring them to the attention of the authors. ===== Changelog =====