diff options
-rw-r--r-- | Doc/lib/libdatetime.tex | 68 | ||||
-rw-r--r-- | Doc/lib/tzinfo-examples.py | 2 | ||||
-rw-r--r-- | Lib/test/test_datetime.py | 47 | ||||
-rw-r--r-- | Modules/datetimemodule.c | 24 |
4 files changed, 68 insertions, 73 deletions
diff --git a/Doc/lib/libdatetime.tex b/Doc/lib/libdatetime.tex index 18edc32..2421fa7 100644 --- a/Doc/lib/libdatetime.tex +++ b/Doc/lib/libdatetime.tex @@ -30,7 +30,7 @@ the cost of ignoring some aspects of reality. For applications requiring more, \class{datetime} and \class{time} objects have an optional time zone information member, -\member{tzinfo}, that can contain an instance of a subclass of +\member{tzinfo}, that can contain an instance of a subclass of the abstract \class{tzinfo} class. These \class{tzinfo} objects capture information about the offset from UTC time, the time zone name, and whether Daylight Saving Time is in effect. Note that no @@ -1048,8 +1048,10 @@ implement all of them. If \method{utcoffset()} does not return \code{None}, \method{dst()} should not return \code{None} either. -\end{methoddesc} + The default implementation of \method{utcoffset()} raises + \exception{NotImplementedError}. +\end{methoddesc} \begin{methoddesc}{dst}{self, dt} Return the daylight saving time (DST) adjustment, in minutes east of @@ -1060,7 +1062,7 @@ implement all of them. Note that DST offset, if applicable, has already been added to the UTC offset returned by \method{utcoffset()}, so there's no need to consult \method{dst()} - unless you're interested in displaying DST info separately. For + unless you're interested in obtaining DST info separately. For example, \method{datetime.timetuple()} calls its \member{tzinfo} member's \method{dst()} method to determine how the \member{tm_isdst} flag should be set, and @@ -1080,6 +1082,10 @@ implement all of them. cannot detect violations; it's the programmer's responsibility to ensure it. + The default implementation of \method{dst()} raises + \exception{NotImplementedError}. +\end{methoddesc} + \begin{methoddesc}{tzname}{self, dt} Return the timezone name corresponding to the \class{datetime} object represented @@ -1092,8 +1098,9 @@ implement all of them. will wish to return different names depending on the specific value of \var{dt} passed, especially if the \class{tzinfo} class is accounting for daylight time. -\end{methoddesc} + The default implementation of \method{tzname()} raises + \exception{NotImplementedError}. \end{methoddesc} These methods are called by a \class{datetime} or \class{time} object, @@ -1106,21 +1113,23 @@ class \class{datetime}. When \code{None} is passed, it's up to the class designer to decide the best response. For example, returning \code{None} is appropriate if the class wishes to say that time objects don't participate in the -\class{tzinfo} protocol. In other applications, it may be more useful -for \code{utcoffset(None)} to return the standard UTC offset. +\class{tzinfo} protocol. It may be more useful for \code{utcoffset(None)} +to return the standard UTC offset, as there is no other convention for +discovering the standard offset. When a \class{datetime} object is passed in response to a \class{datetime} method, \code{dt.tzinfo} is the same object as \var{self}. \class{tzinfo} methods can rely on this, unless user code calls \class{tzinfo} methods directly. The intent is that the \class{tzinfo} methods interpret \var{dt} as being in local time, -and not need to worry about objects in other timezones. +and not need worry about objects in other timezones. Example \class{tzinfo} classes: \verbatiminput{tzinfo-examples.py} -Note that there are unavoidable subtleties twice per year in a tzinfo +Note that there are unavoidable subtleties twice per year in a +\class{tzinfo} subclass accounting for both standard and daylight time, at the DST transition points. For concreteness, consider US Eastern (UTC -0500), where EDT begins the minute after 1:59 (EST) on the first Sunday in @@ -1140,32 +1149,29 @@ When DST starts (the "start" line), the local wall clock leaps from 1:59 to 3:00. A wall time of the form 2:MM doesn't really make sense on that day, so \code{astimezone(Eastern)} won't deliver a result with \code{hour==2} on the -day DST begins. How an Eastern instance chooses to interpret 2:MM on -that day is its business. The example Eastern implementation above -chose to -consider it as a time in EDT, simply because it "looks like it's -after 2:00", and so synonymous with the EST 1:MM times on that day. -Your Eastern class may wish, for example, to raise an exception instead -when it sees a 2:MM time on the day EDT begins. +day DST begins. In order for \method{astimezone()} to make this +guarantee, the \class{tzinfo} \method{dst()} method must consider times +in the "missing hour" (2:MM for Eastern) to be in daylight time. When DST ends (the "end" line), there's a potentially worse problem: -there's an hour that can't be spelled unambiguously in local wall time, the -hour beginning at the moment DST ends. In this example, that's times of -the form 6:MM UTC on the day daylight time ends. The local wall clock +there's an hour that can't be spelled unambiguously in local wall time: +the last hour of daylight time. In Eastern, that's times of +the form 5:MM UTC on the day daylight time ends. The local wall clock leaps from 1:59 (daylight time) back to 1:00 (standard time) again. -1:MM is taken as daylight time (it's "before 2:00"), so maps to 5:MM UTC. -2:MM is taken as standard time (it's "after 2:00"), so maps to 7:MM UTC. -There is no local time that maps to 6:MM UTC on this day. - -Just as the wall clock does, \code{astimezone(Eastern)} maps both UTC -hours 5:MM -and 6:MM to Eastern hour 1:MM on this day. However, this result is -ambiguous (there's no way for Eastern to know which repetition of 1:MM -is intended). Applications that can't bear such ambiguity -should avoid using hybrid tzinfo classes; there are no -ambiguities when using UTC, or any other fixed-offset tzinfo subclass -(such as a class representing only EST (fixed offset -5 hours), or only -EDT (fixed offset -4 hours)). +Local times of the form 1:MM are ambiguous. \method{astimezone()} mimics +the local clock's behavior by mapping two adjacent UTC hours into the +same local hour then. In the Eastern example, UTC times of the form +5:MM and 6:MM both map to 1:MM when converted to Eastern. In order for +\method{astimezone()} to make this guarantee, the \class{tzinfo} +\method{dst()} method must consider times in the "repeated hour" to be in +standard time. This is easily arranged, as in the example, by expressing +DST switch times in the time zone's standard local time. + +Applications that can't bear such ambiguities should avoid using hybrid +\class{tzinfo} subclasses; there are no ambiguities when using UTC, or +any other fixed-offset \class{tzinfo} subclass (such as a class +representing only EST (fixed offset -5 hours), or only EDT (fixed offset +-4 hours)). \subsection{\method{strftime()} Behavior} diff --git a/Doc/lib/tzinfo-examples.py b/Doc/lib/tzinfo-examples.py index cea0309..b488dd0 100644 --- a/Doc/lib/tzinfo-examples.py +++ b/Doc/lib/tzinfo-examples.py @@ -91,7 +91,7 @@ def first_sunday_on_or_after(dt): DSTSTART = datetime(1, 4, 1, 2) # and ends at 2am (DST time; 1am standard time) on the last Sunday of Oct. # which is the first Sunday on or after Oct 25. -DSTEND = datetime(1, 10, 25, 2) +DSTEND = datetime(1, 10, 25, 1) class USTimeZone(tzinfo): diff --git a/Lib/test/test_datetime.py b/Lib/test/test_datetime.py index a46e3ca..8a8d315 100644 --- a/Lib/test/test_datetime.py +++ b/Lib/test/test_datetime.py @@ -2561,8 +2561,10 @@ DAY = timedelta(days=1) # In the US, DST starts at 2am (standard time) on the first Sunday in April. DSTSTART = datetime(1, 4, 1, 2) # and ends at 2am (DST time; 1am standard time) on the last Sunday of Oct, -# which is the first Sunday on or after Oct 25. -DSTEND = datetime(1, 10, 25, 2) +# which is the first Sunday on or after Oct 25. Because we view 1:MM as +# being standard time on that day, there is no spelling in local time of +# the last hour of DST (that's 1:MM DST, but 1:MM is taken as standard time). +DSTEND = datetime(1, 10, 25, 1) class USTimeZone(tzinfo): @@ -2616,9 +2618,9 @@ utc_real = FixedOffset(0, "UTC", 0) utc_fake = FixedOffset(-12*60, "UTCfake", 0) class TestTimezoneConversions(unittest.TestCase): - # The DST switch times for 2002, in local time. + # The DST switch times for 2002, in std time. dston = datetime(2002, 4, 7, 2) - dstoff = datetime(2002, 10, 27, 2) + dstoff = datetime(2002, 10, 27, 1) theclass = datetime @@ -2656,25 +2658,25 @@ class TestTimezoneConversions(unittest.TestCase): # We're not in the redundant hour. self.assertEqual(dt, there_and_back) - # Because we have a redundant spelling when DST begins, - # there is (unforunately) an hour when DST ends that can't - # be spelled at all in local time. When DST ends, the - # clock jumps from 1:59:59 back to 1:00:00 again. The - # hour beginning then has no spelling in local time: - # 1:MM:SS is taken to be daylight time, and 2:MM:SS as - # standard time. The hour 1:MM:SS standard time == - # 2:MM:SS daylight time can't be expressed in local time. - # Nevertheless, we want conversion back from UTC to mimic - # the local clock's "repeat an hour" behavior. + # Because we have a redundant spelling when DST begins, there is + # (unforunately) an hour when DST ends that can't be spelled at all in + # local time. When DST ends, the clock jumps from 1:59 back to 1:00 + # again. The hour 1:MM DST has no spelling then: 1:MM is taken to be + # standard time. 1:MM DST == 0:MM EST, but 0:MM is taken to be + # daylight time. The hour 1:MM daylight == 0:MM standard can't be + # expressed in local time. Nevertheless, we want conversion back + # from UTC to mimic the local clock's "repeat an hour" behavior. nexthour_utc = asutc + HOUR nexthour_tz = nexthour_utc.astimezone(tz) - if dt.date() == dstoff.date() and dt.hour == 1: - # We're in the hour before DST ends. The hour after + if dt.date() == dstoff.date() and dt.hour == 0: + # We're in the hour before the last DST hour. The last DST hour # is ineffable. We want the conversion back to repeat 1:MM. - expected_diff = ZERO + self.assertEqual(nexthour_tz, dt.replace(hour=1)) + nexthour_utc += HOUR + nexthour_tz = nexthour_utc.astimezone(tz) + self.assertEqual(nexthour_tz, dt.replace(hour=1)) else: - expected_diff = HOUR - self.assertEqual(nexthour_tz - dt, expected_diff) + self.assertEqual(nexthour_tz - dt, HOUR) # Check a time that's outside DST. def checkoutside(self, dt, tz, utc): @@ -2687,6 +2689,11 @@ class TestTimezoneConversions(unittest.TestCase): def convert_between_tz_and_utc(self, tz, utc): dston = self.dston.replace(tzinfo=tz) + # Because 1:MM on the day DST ends is taken as being standard time, + # there is no spelling in tz for the last hour of daylight time. + # For purposes of the test, the last hour of DST is 0:MM, which is + # taken as being daylight time (and 1:MM is taken as being standard + # time). dstoff = self.dstoff.replace(tzinfo=tz) for delta in (timedelta(weeks=13), DAY, @@ -2759,7 +2766,7 @@ class TestTimezoneConversions(unittest.TestCase): # wall 0:MM 1:MM 1:MM 2:MM against these for utc in utc_real, utc_fake: for tz in Eastern, Pacific: - first_std_hour = self.dstoff - timedelta(hours=3) # 23:MM + first_std_hour = self.dstoff - timedelta(hours=2) # 23:MM # Convert that to UTC. first_std_hour -= tz.utcoffset(None) # Adjust for possibly fake UTC. diff --git a/Modules/datetimemodule.c b/Modules/datetimemodule.c index 0bd49b2..d88fc9e 100644 --- a/Modules/datetimemodule.c +++ b/Modules/datetimemodule.c @@ -4046,7 +4046,7 @@ datetime_astimezone(PyDateTime_DateTime *self, PyObject *args, PyObject *kw) PyObject *result; PyObject *temp; - int selfoff, resoff, dst1, dst2; + int selfoff, resoff, dst1; int none; int delta; @@ -4128,26 +4128,8 @@ datetime_astimezone(PyDateTime_DateTime *self, PyObject *args, PyObject *kw) temp = new_datetime(y, m, d, hh, mm, ss, us, tzinfo); if (temp == NULL) goto Fail; - - dst2 = call_dst(tzinfo, temp, &none); - if (dst2 == -1 && PyErr_Occurred()) { - Py_DECREF(temp); - goto Fail; - } - if (none) { - Py_DECREF(temp); - goto Inconsistent; - } - - if (dst1 == dst2) { - /* The normal case: we want temp, not result. */ - Py_DECREF(result); - result = temp; - } - else { - /* The "unspellable hour" at the end of DST. */ - Py_DECREF(temp); - } + Py_DECREF(result); + result = temp; return result; Inconsistent: |