diff options
author | Alexander Belopolsky <alexander.belopolsky@gmail.com> | 2016-08-24 22:30:16 (GMT) |
---|---|---|
committer | Alexander Belopolsky <alexander.belopolsky@gmail.com> | 2016-08-24 22:30:16 (GMT) |
commit | 53868aaabb154c11bf666b31662bb4ae8bc6ec79 (patch) | |
tree | 6000aeb6049b12cc488304d909116a7377ff50be | |
parent | 208536132b6c952f4634c5193ef76f4a83218f7e (diff) | |
download | cpython-53868aaabb154c11bf666b31662bb4ae8bc6ec79.zip cpython-53868aaabb154c11bf666b31662bb4ae8bc6ec79.tar.gz cpython-53868aaabb154c11bf666b31662bb4ae8bc6ec79.tar.bz2 |
Closes #27595: Document PEP 495 (Local Time Disambiguation) features.
-rw-r--r-- | Doc/includes/tzinfo-examples.py | 130 | ||||
-rw-r--r-- | Doc/library/datetime.rst | 155 |
2 files changed, 181 insertions, 104 deletions
diff --git a/Doc/includes/tzinfo-examples.py b/Doc/includes/tzinfo-examples.py index 3a8cf47..ae5a509 100644 --- a/Doc/includes/tzinfo-examples.py +++ b/Doc/includes/tzinfo-examples.py @@ -1,46 +1,13 @@ -from datetime import tzinfo, timedelta, datetime +from datetime import tzinfo, timedelta, datetime, timezone ZERO = timedelta(0) HOUR = timedelta(hours=1) - -# A UTC class. - -class UTC(tzinfo): - """UTC""" - - def utcoffset(self, dt): - return ZERO - - def tzname(self, dt): - return "UTC" - - def dst(self, dt): - return ZERO - -utc = UTC() - -# A class building tzinfo objects for fixed-offset time zones. -# Note that FixedOffset(0, "UTC") is a different way to build a -# UTC tzinfo object. - -class FixedOffset(tzinfo): - """Fixed offset in minutes east from UTC.""" - - def __init__(self, offset, name): - self.__offset = timedelta(minutes=offset) - self.__name = name - - def utcoffset(self, dt): - return self.__offset - - def tzname(self, dt): - return self.__name - - def dst(self, dt): - return ZERO +SECOND = timedelta(seconds=1) # A class capturing the platform's idea of local time. - +# (May result in wrong values on historical times in +# timezones where UTC offset and/or the DST rules had +# changed in the past.) import time as _time STDOFFSET = timedelta(seconds = -_time.timezone) @@ -53,6 +20,16 @@ DSTDIFF = DSTOFFSET - STDOFFSET class LocalTimezone(tzinfo): + def fromutc(self, dt): + assert dt.tzinfo is self + stamp = (dt - datetime(1970, 1, 1, tzinfo=self)) // SECOND + args = _time.localtime(stamp)[:6] + dst_diff = DSTDIFF // SECOND + # Detect fold + fold = (args == _time.localtime(stamp - dst_diff)) + return datetime(*args, microsecond=dt.microsecond, + tzinfo=self, fold=fold) + def utcoffset(self, dt): if self._isdst(dt): return DSTOFFSET @@ -99,20 +76,37 @@ def first_sunday_on_or_after(dt): # In the US, since 2007, DST starts at 2am (standard time) on the second # Sunday in March, which is the first Sunday on or after Mar 8. DSTSTART_2007 = datetime(1, 3, 8, 2) -# and ends at 2am (DST time; 1am standard time) on the first Sunday of Nov. -DSTEND_2007 = datetime(1, 11, 1, 1) +# and ends at 2am (DST time) on the first Sunday of Nov. +DSTEND_2007 = datetime(1, 11, 1, 2) # From 1987 to 2006, DST used to start at 2am (standard time) on the first -# Sunday in April and to end at 2am (DST time; 1am standard time) on the last +# Sunday in April and to end at 2am (DST time) on the last # Sunday of October, which is the first Sunday on or after Oct 25. DSTSTART_1987_2006 = datetime(1, 4, 1, 2) -DSTEND_1987_2006 = datetime(1, 10, 25, 1) +DSTEND_1987_2006 = datetime(1, 10, 25, 2) # From 1967 to 1986, DST used to start at 2am (standard time) on the last -# Sunday in April (the one on or after April 24) and to end at 2am (DST time; -# 1am standard time) on the last Sunday of October, which is the first Sunday +# Sunday in April (the one on or after April 24) and to end at 2am (DST time) +# on the last Sunday of October, which is the first Sunday # on or after Oct 25. DSTSTART_1967_1986 = datetime(1, 4, 24, 2) DSTEND_1967_1986 = DSTEND_1987_2006 +def us_dst_range(year): + # Find start and end times for US DST. For years before 1967, return + # start = end for no DST. + if 2006 < year: + dststart, dstend = DSTSTART_2007, DSTEND_2007 + elif 1986 < year < 2007: + dststart, dstend = DSTSTART_1987_2006, DSTEND_1987_2006 + elif 1966 < year < 1987: + dststart, dstend = DSTSTART_1967_1986, DSTEND_1967_1986 + else: + return (datetime(year, 1, 1), ) * 2 + + start = first_sunday_on_or_after(dststart.replace(year=year)) + end = first_sunday_on_or_after(dstend.replace(year=year)) + return start, end + + class USTimeZone(tzinfo): def __init__(self, hours, reprname, stdname, dstname): @@ -141,27 +135,39 @@ class USTimeZone(tzinfo): # implementation) passes a datetime with dt.tzinfo is self. return ZERO assert dt.tzinfo is self - - # Find start and end times for US DST. For years before 1967, return - # ZERO for no DST. - if 2006 < dt.year: - dststart, dstend = DSTSTART_2007, DSTEND_2007 - elif 1986 < dt.year < 2007: - dststart, dstend = DSTSTART_1987_2006, DSTEND_1987_2006 - elif 1966 < dt.year < 1987: - dststart, dstend = DSTSTART_1967_1986, DSTEND_1967_1986 - else: - return ZERO - - start = first_sunday_on_or_after(dststart.replace(year=dt.year)) - end = first_sunday_on_or_after(dstend.replace(year=dt.year)) - + start, end = us_dst_range(dt.year) # Can't compare naive to aware objects, so strip the timezone from # dt first. - if start <= dt.replace(tzinfo=None) < end: + dt = dt.replace(tzinfo=None) + if start + HOUR <= dt < end - HOUR: + # DST is in effect. return HOUR - else: - return ZERO + if end - HOUR <= dt < end: + # Fold (an ambiguous hour): use dt.fold to disambiguate. + return ZERO if dt.fold else HOUR + if start <= dt < start + HOUR: + # Gap (a non-existent hour): reverse the fold rule. + return HOUR if dt.fold else ZERO + # DST is off. + return ZERO + + def fromutc(self, dt): + assert dt.tzinfo is self + start, end = us_dst_range(dt.year) + start = start.replace(tzinfo=self) + end = end.replace(tzinfo=self) + std_time = dt + self.stdoffset + dst_time = std_time + HOUR + if end <= dst_time < end + HOUR: + # Repeated hour + return std_time.replace(fold=1) + if std_time < start or dst_time >= end: + # Standard time + return std_time + if start <= std_time < end - HOUR: + # Daylight saving time + return dst_time + Eastern = USTimeZone(-5, "Eastern", "EST", "EDT") Central = USTimeZone(-6, "Central", "CST", "CDT") diff --git a/Doc/library/datetime.rst b/Doc/library/datetime.rst index a286fbe..272abee 100644 --- a/Doc/library/datetime.rst +++ b/Doc/library/datetime.rst @@ -522,7 +522,7 @@ objects are considered to be true. Instance methods: -.. method:: date.replace(year, month, day) +.. method:: date.replace(year=self.year, month=self.month, day=self.day) Return a date with the same value, except for those parameters given new values by whichever keyword arguments are specified. For example, if ``d == @@ -683,22 +683,26 @@ both directions; like a time object, :class:`.datetime` assumes there are exactl Constructor: -.. class:: datetime(year, month, day, hour=0, minute=0, second=0, microsecond=0, tzinfo=None) +.. class:: datetime(year, month, day, hour=0, minute=0, second=0, microsecond=0, tzinfo=None, *, fold=0) The year, month and day arguments are required. *tzinfo* may be ``None``, or an instance of a :class:`tzinfo` subclass. The remaining arguments may be integers, in the following ranges: - * ``MINYEAR <= year <= MAXYEAR`` - * ``1 <= month <= 12`` - * ``1 <= day <= number of days in the given month and year`` - * ``0 <= hour < 24`` - * ``0 <= minute < 60`` - * ``0 <= second < 60`` - * ``0 <= microsecond < 1000000`` + * ``MINYEAR <= year <= MAXYEAR``, + * ``1 <= month <= 12``, + * ``1 <= day <= number of days in the given month and year``, + * ``0 <= hour < 24``, + * ``0 <= minute < 60``, + * ``0 <= second < 60``, + * ``0 <= microsecond < 1000000``, + * ``fold in [0, 1]``. If an argument outside those ranges is given, :exc:`ValueError` is raised. + .. versionadded:: 3.6 + Added the ``fold`` argument. + Other constructors, all class methods: .. classmethod:: datetime.today() @@ -758,6 +762,8 @@ Other constructors, all class methods: instead of :exc:`ValueError` on :c:func:`localtime` or :c:func:`gmtime` failure. + .. versionchanged:: 3.6 + :meth:`fromtimestamp` may return instances with :attr:`.fold` set to 1. .. classmethod:: datetime.utcfromtimestamp(timestamp) @@ -794,7 +800,7 @@ Other constructors, all class methods: microsecond of the result are all 0, and :attr:`.tzinfo` is ``None``. -.. classmethod:: datetime.combine(date, time[, tzinfo]) +.. classmethod:: datetime.combine(date, time, tzinfo=self.tzinfo) Return a new :class:`.datetime` object whose date components are equal to the given :class:`date` object's, and whose time components @@ -886,6 +892,16 @@ Instance attributes (read-only): or ``None`` if none was passed. +.. attribute:: datetime.fold + + In ``[0, 1]``. Used to disambiguate wall times during a repeated interval. (A + repeated interval occurs when clocks are rolled back at the end of daylight saving + time or when the UTC offset for the current zone is decreased for political reasons.) + The value 0 (1) represents the earlier (later) of the two moments with the same wall + time representation. + + .. versionadded:: 3.6 + Supported operations: +---------------------------------------+--------------------------------+ @@ -974,23 +990,34 @@ Instance methods: .. method:: datetime.time() - Return :class:`.time` object with same hour, minute, second and microsecond. + Return :class:`.time` object with same hour, minute, second, microsecond and fold. :attr:`.tzinfo` is ``None``. See also method :meth:`timetz`. + .. versionchanged:: 3.6 + The fold value is copied to the returned :class:`.time` object. + .. method:: datetime.timetz() - Return :class:`.time` object with same hour, minute, second, microsecond, and + Return :class:`.time` object with same hour, minute, second, microsecond, fold, and tzinfo attributes. See also method :meth:`time`. + .. versionchanged:: 3.6 + The fold value is copied to the returned :class:`.time` object. + -.. method:: datetime.replace([year[, month[, day[, hour[, minute[, second[, microsecond[, tzinfo]]]]]]]]) +.. method:: datetime.replace(year=self.year, month=self.month, day=self.day, \ + hour=self.hour, minute=self.minute, second=self.second, microsecond=self.microsecond, \ + tzinfo=self.tzinfo, * fold=0) Return a datetime with the same attributes, except for those attributes given new values by whichever keyword arguments are specified. Note that ``tzinfo=None`` can be specified to create a naive datetime from an aware datetime with no conversion of date and time data. + .. versionadded:: 3.6 + Added the ``fold`` argument. + .. method:: datetime.astimezone(tz=None) @@ -999,23 +1026,20 @@ Instance methods: *self*, but in *tz*'s local time. If provided, *tz* must be an instance of a :class:`tzinfo` subclass, and its - :meth:`utcoffset` and :meth:`dst` methods must not return ``None``. *self* must - be aware (``self.tzinfo`` must not be ``None``, and ``self.utcoffset()`` must - not return ``None``). + :meth:`utcoffset` and :meth:`dst` methods must not return ``None``. If *self* + is naive (``self.tzinfo is None``), it is presumed to represent time in the + system timezone. If called without arguments (or with ``tz=None``) the system local - timezone is assumed. The ``.tzinfo`` attribute of the converted + timezone is assumed for the target timezone. The ``.tzinfo`` attribute of the converted datetime instance will be set to an instance of :class:`timezone` with the zone name and offset obtained from the OS. If ``self.tzinfo`` is *tz*, ``self.astimezone(tz)`` is equal to *self*: no adjustment of date or time data is performed. Else the result is local - time in time zone *tz*, representing the same UTC time as *self*: after - ``astz = dt.astimezone(tz)``, ``astz - astz.utcoffset()`` will usually have - the same date and time data as ``dt - dt.utcoffset()``. The discussion - of class :class:`tzinfo` explains the cases at Daylight Saving Time transition - boundaries where this cannot be achieved (an issue only if *tz* models both - standard and daylight time). + time in the timezone *tz*, representing the same UTC time as *self*: after + ``astz = dt.astimezone(tz)``, ``astz - astz.utcoffset()`` will have + the same date and time data as ``dt - dt.utcoffset()``. If you merely want to attach a time zone object *tz* to a datetime *dt* without adjustment of date and time data, use ``dt.replace(tzinfo=tz)``. If you @@ -1037,6 +1061,10 @@ Instance methods: .. versionchanged:: 3.3 *tz* now can be omitted. + .. versionchanged:: 3.6 + The :meth:`astimezone` method can now be called on naive instances that + are presumed to represent system local time. + .. method:: datetime.utcoffset() @@ -1113,6 +1141,10 @@ Instance methods: .. versionadded:: 3.3 + .. versionchanged:: 3.6 + The :meth:`timestamp` method uses the :attr:`.fold` attribute to + disambiguate the times during a repeated interval. + .. note:: There is no method to obtain the POSIX timestamp directly from a @@ -1342,16 +1374,17 @@ Using datetime with tzinfo: A time object represents a (local) time of day, independent of any particular day, and subject to adjustment via a :class:`tzinfo` object. -.. class:: time(hour=0, minute=0, second=0, microsecond=0, tzinfo=None) +.. class:: time(hour=0, minute=0, second=0, microsecond=0, tzinfo=None, *, fold=0) All arguments are optional. *tzinfo* may be ``None``, or an instance of a :class:`tzinfo` subclass. The remaining arguments may be integers, in the following ranges: - * ``0 <= hour < 24`` - * ``0 <= minute < 60`` - * ``0 <= second < 60`` - * ``0 <= microsecond < 1000000``. + * ``0 <= hour < 24``, + * ``0 <= minute < 60``, + * ``0 <= second < 60``, + * ``0 <= microsecond < 1000000``, + * ``fold in [0, 1]``. If an argument outside those ranges is given, :exc:`ValueError` is raised. All default to ``0`` except *tzinfo*, which defaults to :const:`None`. @@ -1404,6 +1437,17 @@ Instance attributes (read-only): ``None`` if none was passed. +.. attribute:: time.fold + + In ``[0, 1]``. Used to disambiguate wall times during a repeated interval. (A + repeated interval occurs when clocks are rolled back at the end of daylight saving + time or when the UTC offset for the current zone is decreased for political reasons.) + The value 0 (1) represents the earlier (later) of the two moments with the same wall + time representation. + + .. versionadded:: 3.6 + + Supported operations: * comparison of :class:`.time` to :class:`.time`, where *a* is considered less @@ -1439,13 +1483,17 @@ In boolean contexts, a :class:`.time` object is always considered to be true. Instance methods: -.. method:: time.replace([hour[, minute[, second[, microsecond[, tzinfo]]]]]) +.. method:: time.replace(hour=self.hour, minute=self.minute, second=self.second, \ + microsecond=self.microsecond, tzinfo=self.tzinfo, * fold=0) Return a :class:`.time` with the same value, except for those attributes given new values by whichever keyword arguments are specified. Note that ``tzinfo=None`` can be specified to create a naive :class:`.time` from an aware :class:`.time`, without conversion of the time data. + .. versionadded:: 3.6 + Added the ``fold`` argument. + .. method:: time.isoformat(timespec='auto') @@ -1754,9 +1802,19 @@ minute after 1:59 (EST) on the second Sunday in March, and ends the minute after 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 ``astimezone(Eastern)`` won't deliver a result with ``hour == 2`` on the day DST -begins. In order for :meth:`astimezone` to make this guarantee, the -:meth:`tzinfo.dst` method must consider times in the "missing hour" (2:MM for -Eastern) to be in daylight time. +begins. For example, at the Spring forward transition of 2016, we get + + >>> u0 = datetime(2016, 3, 13, 5, tzinfo=timezone.utc) + >>> for i in range(4): + ... u = u0 + i*HOUR + ... t = u.astimezone(Eastern) + ... print(u.time(), 'UTC =', t.time(), t.tzname()) + ... + 05:00:00 UTC = 00:00:00 EST + 06:00:00 UTC = 01:00:00 EST + 07:00:00 UTC = 03:00:00 EDT + 08:00:00 UTC = 04:00:00 EDT + 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 last hour of @@ -1765,28 +1823,41 @@ daylight time ends. The local wall clock leaps from 1:59 (daylight time) back to 1:00 (standard time) again. Local times of the form 1:MM are ambiguous. :meth:`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 -:meth:`astimezone` to make this guarantee, the :meth:`tzinfo.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. +form 5:MM and 6:MM both map to 1:MM when converted to Eastern, but earlier times +have the :attr:`~datetime.fold` attribute set to 0 and the later times have it set to 1. +For example, at the Fall back transition of 2016, we get + + >>> u0 = datetime(2016, 11, 6, 4, tzinfo=timezone.utc) + >>> for i in range(4): + ... u = u0 + i*HOUR + ... t = u.astimezone(Eastern) + ... print(u.time(), 'UTC =', t.time(), t.tzname(), t.fold) + ... + 04:00:00 UTC = 00:00:00 EDT 0 + 05:00:00 UTC = 01:00:00 EDT 0 + 06:00:00 UTC = 01:00:00 EST 1 + 07:00:00 UTC = 02:00:00 EST 0 + +Note that the :class:`datetime` instances that differ only by the value of the +:attr:`~datetime.fold` attribute are considered equal in comparisons. -Applications that can't bear such ambiguities should avoid using hybrid +Applications that can't bear wall-time ambiguities should explicitly check the +value of the :attr:`~datetime.fold` atribute or avoid using hybrid :class:`tzinfo` subclasses; there are no ambiguities when using :class:`timezone`, 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)). .. seealso:: - `pytz <https://pypi.python.org/pypi/pytz/>`_ + `datetuil.tz <https://dateutil.readthedocs.io/en/stable/tz.html>`_ The standard library has :class:`timezone` class for handling arbitrary fixed offsets from UTC and :attr:`timezone.utc` as UTC timezone instance. - *pytz* library brings the *IANA timezone database* (also known as the + *datetuil.tz* library brings the *IANA timezone database* (also known as the Olson database) to Python and its usage is recommended. `IANA timezone database <https://www.iana.org/time-zones>`_ - The Time Zone Database (often called tz or zoneinfo) contains code and + The Time Zone Database (often called tz, tzdata or zoneinfo) contains code and data that represent the history of local time for many representative locations around the globe. It is updated periodically to reflect changes made by political bodies to time zone boundaries, UTC offsets, and @@ -1806,7 +1877,7 @@ in different days of the year or where historical changes have been made to civil time. -.. class:: timezone(offset[, name]) +.. class:: timezone(offset, name=None) The *offset* argument must be specified as a :class:`timedelta` object representing the difference between the local time and UTC. It must |