399 lines
14 KiB
Plaintext
399 lines
14 KiB
Plaintext
PEP: 495
|
|
Title: Local Time Disambiguation
|
|
Version: $Revision$
|
|
Last-Modified: $Date$
|
|
Author: Alexander Belopolsky <alexander.belopolsky@gmail.com>
|
|
Discussions-To: Datetime-SIG <datetime-sig@python.org>
|
|
Status: Draft
|
|
Type: Standards Track
|
|
Content-Type: text/x-rst
|
|
Created: 02-Aug-2015
|
|
|
|
|
|
|
|
Abstract
|
|
========
|
|
|
|
This PEP adds a boolean member to the instances of ``datetime.time``
|
|
and ``datetime.datetime`` classes that can be used to differentiate
|
|
between two moments in time for which local times are the same.
|
|
|
|
.. sidebar:: US public service advertisement
|
|
|
|
.. image:: pep-0495-daylightsavings.png
|
|
:align: right
|
|
:width: 15%
|
|
|
|
|
|
Rationale
|
|
=========
|
|
|
|
In the most world locations there have been and will be times when
|
|
local clocks are moved back. In those times, intervals are introduced
|
|
in which local clocks show the same time twice in the same day. In
|
|
these situations, the information displayed on a local clock (or
|
|
stored in a Python datetime instance) is insufficient to identify a
|
|
particular moment in time. The proposed solution is to add a boolean
|
|
flag to the ``datetime`` instances that will distinguish between the
|
|
two ambiguous times.
|
|
|
|
|
|
Terminology
|
|
===========
|
|
|
|
When clocks are moved back, we say that a *fold* is created in the
|
|
fabric of time. When the clock are moved forward, a *gap* is created.
|
|
A local time that falls in the fold is called *ambiguous*. A local
|
|
time that falls in the gap is called *missing*.
|
|
|
|
|
|
Proposal
|
|
========
|
|
|
|
The "first" flag
|
|
----------------
|
|
|
|
We propose adding a boolean member called ``first`` to the instances
|
|
of ``datetime.time`` and ``datetime.datetime`` classes. This member
|
|
should have the value True for all instances except those that
|
|
represent the second (chronologically) moment in time in an ambiguous
|
|
case. [#]_
|
|
|
|
.. [#] An instance that has ``first=False`` in a non-ambiguous case is
|
|
said to represent an invalid time (or is invalid for short), but
|
|
users are not prevented from creating invalid instances by passing
|
|
``first=False`` to a constructor or to a ``replace()`` method. This
|
|
is similar to the current situation with the instances that fall in
|
|
the spring-forward gap. Such instances don't represent any valid
|
|
time, but neither the constructors nor the ``replace()`` methods
|
|
check whether the instances that they produce are valid. Moreover,
|
|
this PEP specifies how various functions should behave when given an
|
|
invalid instance.
|
|
|
|
Affected APIs
|
|
-------------
|
|
|
|
Attributes
|
|
..........
|
|
|
|
Instances of ``datetime.time`` and ``datetime.datetime`` will get a
|
|
new boolean attribute called "first."
|
|
|
|
Constructors
|
|
............
|
|
|
|
The ``__new__`` methods of the ``datetime.time`` and
|
|
``datetime.datetime`` classes will get a new keyword-only argument
|
|
called ``first`` with the default value ``True``. The value of the
|
|
``first`` argument will be used to initialize the value of the
|
|
``first`` attribute in the returned instance.
|
|
|
|
Methods
|
|
.......
|
|
|
|
The ``replace()`` methods of the ``datetime.time`` and
|
|
``datetime.datetime`` classes will get a new keyword-only argument
|
|
called ``first``. It will
|
|
behave similarly to the other ``replace()`` arguments: if the ``first``
|
|
argument is specified and given a boolean value, the new instance
|
|
returned by ``replace()`` will have its ``first`` attribute set
|
|
to that value. In CPython, a non-boolean value of ``first`` will
|
|
raise a ``TypeError``, but other implementations may allow the value
|
|
``None`` to behave the same as when ``first`` is not given. If the
|
|
``first`` argument is not specified, the original value of the ``first``
|
|
attribute is copied to the result.
|
|
|
|
Affected Behaviors
|
|
------------------
|
|
|
|
Conversion from naive to aware
|
|
..............................
|
|
|
|
The ``astimezone()`` method will now work for naive ``self``. The
|
|
system local timezone will be assumed in this case and the ``first``
|
|
flag will be used to determine which local timezone is in effect
|
|
in the ambiguous case.
|
|
|
|
For example, on a system set to US/Eastern timezone::
|
|
|
|
>>> dt = datetime(2014, 11, 2, 1, 30)
|
|
>>> dt.astimezone().strftime('%D %T %Z%z')
|
|
'11/02/14 01:30:00 EDT-0400'
|
|
>>> dt.replace(first=False).astimezone().strftime('%D %T %Z%z')
|
|
'11/02/14 01:30:00 EST-0500'
|
|
|
|
Conversion to POSIX seconds from EPOCH
|
|
......................................
|
|
|
|
The ``timestamp()`` method of ``datetime.datetime`` will return different
|
|
values for ``datetime.datetime`` instances that differ only by the value
|
|
of their ``first`` attribute if and only if these instances represent an
|
|
ambiguous or a non-existent time.
|
|
|
|
When a ``datetime.datetime`` instance ``dt`` represents an ambiguous
|
|
(repeated) time, there are two values ``s0`` and ``s1`` such that::
|
|
|
|
datetime.fromtimestamp(s0) == datetime.fromtimestamp(s1) == dt
|
|
|
|
In this case, ``dt.timestamp()`` will return the smaller of ``s0``
|
|
and ``s1`` values if ``dt.first == True`` and the larger otherwise.
|
|
|
|
|
|
For example, on a system set to US/Eastern timezone::
|
|
|
|
>>> datetime(2014, 11, 2, 1, 30, first=True).timestamp()
|
|
1414906200.0
|
|
>>> datetime(2014, 11, 2, 1, 30, first=False).timestamp()
|
|
1414909800.0
|
|
|
|
|
|
When a ``datetime.datetime`` instance ``dt`` represents a missing
|
|
time, there is no value ``s`` for which::
|
|
|
|
datetime.fromtimestamp(s) == dt
|
|
|
|
but we can form two "nice to know" values of ``s`` that differ
|
|
by the size of the gap in seconds. One is the value of ``s``
|
|
that would correspond to ``dt`` in a timezone where the UTC offset
|
|
is always the same as the offset right before the gap and the
|
|
other is the similar value but in a timezone the UTC offset
|
|
is always the same as the offset right after the gap.
|
|
|
|
The value returned by ``dt.timestamp()`` given a missing
|
|
``dt`` will be the larger of the two "nice to know" values
|
|
if ``dt.first == True`` and the larger otherwise.
|
|
|
|
For example, on a system set to US/Eastern timezone::
|
|
|
|
>>> datetime(2015, 3, 8, 2, 30, first=True).timestamp()
|
|
1425799800.0
|
|
>>> datetime(2015, 3, 8, 2, 30, first=False).timestamp()
|
|
1425796200.0
|
|
|
|
|
|
Conversion from POSIX seconds from EPOCH
|
|
........................................
|
|
|
|
|
|
The ``fromtimestamp()`` static method of ``datetime.datetime`` will
|
|
set the ``first`` attribute appropriately in the returned object.
|
|
|
|
For example, on a system set to US/Eastern timezone::
|
|
|
|
>>> datetime.fromtimestamp(1414906200)
|
|
datetime.datetime(2014, 11, 2, 1, 30)
|
|
>>> datetime.fromtimestamp(1414906200 + 3600)
|
|
datetime.datetime(2014, 11, 2, 1, 30, first=False)
|
|
|
|
|
|
Combining and splitting date and time
|
|
.....................................
|
|
|
|
The ``datetime.datetime.combine()`` method will copy the value of the
|
|
``first`` attribute to the resulting ``datetime.datetime`` instance.
|
|
|
|
The ``datetime.datetime.time()`` method will copy the value of the
|
|
``first`` attribute to the resulting ``datetime.time`` instance.
|
|
|
|
|
|
Implementations of tzinfo in stdlib
|
|
...................................
|
|
|
|
No new implementations of ``datetime.tzinfo`` abstract class are
|
|
introduced in this PEP. The existing (fixed offset) timezones do
|
|
not introduce ambiguous local times and their ``utcoffset()``
|
|
implementation will return the same constant value as they do now
|
|
regardless of the value of ``first``.
|
|
|
|
The basic implementation of ``fromutc()`` in the abstract
|
|
``datetime.tzinfo`` class will not change. It is currently not
|
|
used anywhere in the stdlib because the only included ``tzinfo``
|
|
implementation (the ``datetime.timzeone`` class implementing fixed
|
|
offset timezones) override ``fromutc()``.
|
|
|
|
New guidelines will be published for implementing concrete timezones
|
|
with variable UTC offset.
|
|
|
|
|
|
Guidelines for new tzinfo implementations
|
|
-----------------------------------------
|
|
|
|
Implementors of concrete ``datetime.tzinfo`` subclasses who want to
|
|
support variable UTC offsets (due to DST and other causes) must follow
|
|
these guidelines.
|
|
|
|
New subclasses must override the base-class ``fromutc()`` method and
|
|
implement it so that in all cases where two UTC times ``u1`` and
|
|
``u2`` (``u1`` <``u2``) correspond to the same local time
|
|
``fromutc(u1)`` will return an instance with ``first=True`` and
|
|
``fromutc(u2)`` will return an instance with ``first=False``. In all
|
|
other cases the returned instance must have ``first=True``.
|
|
|
|
New implementations of ``utcoffset()`` and ``dst()`` methods should
|
|
ignore the value of ``first`` unless they are called on the ambiguous
|
|
or missing times.
|
|
|
|
On an ambiguous time introduced at the end of DST, the values returned
|
|
by ``utcoffset()`` and ``dst()`` methods should be as follows
|
|
|
|
+-----------------+----------------+------------------+
|
|
| | first=True | first=False |
|
|
+=================+================+==================+
|
|
| utcoff() | stdoff + hour | stdoff |
|
|
+-----------------+----------------+------------------+
|
|
| dst() | hour | zero |
|
|
+-----------------+----------------+------------------+
|
|
|
|
where ``stdoff`` is the standard (non-DST) offset,
|
|
``hour = timedelta(hours=1)`` and ``zero = timedelta(0)``.
|
|
|
|
On a missing time introduced at the start of DST, the values returned
|
|
by ``utcoffset()`` and ``dst()`` methods should be as follows
|
|
|
|
|
|
+-----------------+----------------+------------------+
|
|
| | first=True | first=False |
|
|
+=================+================+==================+
|
|
| utcoff() | stdoff | stdoff + hour |
|
|
+-----------------+----------------+------------------+
|
|
| dst() | zero | hour |
|
|
+-----------------+----------------+------------------+
|
|
|
|
|
|
On ambiguous/missing times introduced by the change in the standard time
|
|
offset, the ``dst()`` method should return the same value regardless of
|
|
the value of ``first`` and the ``utcoff()`` should return values
|
|
according to the following table:
|
|
|
|
+-----------------+----------------+-----------------------------+
|
|
| | first=True | first=False |
|
|
+=================+================+=============================+
|
|
| ambiguous | oldoff | newoff = oldoff - delta |
|
|
+-----------------+----------------+-----------------------------+
|
|
| missing | oldoff | newoff = oldoff + delta |
|
|
+-----------------+----------------+-----------------------------+
|
|
|
|
|
|
|
|
Pickle size
|
|
-----------
|
|
|
|
Pickle sizes for the ``datetime.datetime`` and ``datetime.time``
|
|
objects will not change. The ``first`` flag will be encoded in the
|
|
first bit of the 5th byte of the ``datetime.datetime`` pickle payload
|
|
or the 2nd byte of the datetime.time. In the `current implementation`_
|
|
these bytes are used to store minute value (0-59) and the first bit is
|
|
always 0. Note that ``first=True`` will be encoded as 0 in the first
|
|
bit and ``first=False`` as 1. (This change only affects pickle
|
|
format. In C implementation, the "first" member will get a full byte
|
|
to store the actual boolean value.)
|
|
|
|
We chose the minute byte to store the the "first" bit because this
|
|
choice preserves the natural ordering.
|
|
|
|
.. _current implementation: https://hg.python.org/cpython/file/d3b20bff9c5d/Include/datetime.h#l17
|
|
|
|
Temporal Arithmetics
|
|
--------------------
|
|
|
|
The value of "first" will be ignored in all operations except those
|
|
that involve conversion between timezones. [#]_ As a consequence,
|
|
`datetime.datetime`` or ``datetime.time`` instances that differ only
|
|
by the value of ``first`` will compare as equal. Applications that
|
|
need to differentiate between such instances should check the value of
|
|
``first`` or convert them to a timezone that does not have ambiguous
|
|
times.
|
|
|
|
The result of addition (subtraction) of a timedelta to (from) a
|
|
datetime will always have ``first`` set to ``True`` even if the
|
|
original datetime instance had ``first=False``.
|
|
|
|
.. [#] As of Python 3.5, ``tzinfo`` is ignored whenever timedelta is
|
|
added or subtracted from a ``datetime.datetime`` instance or when
|
|
one ``datetime.datetime`` instance is subtracted from another with
|
|
the same (even not-None) ``tzinfo``. This may change in the future,
|
|
but such changes are outside of the scope of this PEP.
|
|
|
|
|
|
Backward and Forward Compatibility
|
|
----------------------------------
|
|
|
|
This proposal will have little effect on the programs that do not read
|
|
the ``first`` flag explicitly or use tzinfo implementations that do.
|
|
The only visible change for such programs will be that conversions to
|
|
and from POSIX timestamps will now round-trip correctly (up to
|
|
floating point rounding). Programs that implemented work-arounds to
|
|
the old incorrect behavior will need to be modified.
|
|
|
|
Pickles produced by older programs will remain fully forward
|
|
compatible. Only datetime/time instances with ``first=False`` pickled
|
|
in the new versions will become unreadable by the older Python
|
|
versions. Pickles of instances with ``first=True`` (which is the
|
|
default) will remain unchanged.
|
|
|
|
Questions and Answers
|
|
=====================
|
|
|
|
1. Why not call the new flag "isdst"?
|
|
|
|
-------
|
|
|
|
* Alice: Bob - let's have a stargazing party at 01:30 AM tomorrow!
|
|
* Bob: Should I presume initially that summer time (for example, Daylight
|
|
Saving Time) is or is not (respectively) in effect for the specified time?
|
|
* Alice: Huh?
|
|
|
|
-------
|
|
|
|
* Bob: Alice - let's have a stargazing party at 01:30 AM tomorrow!
|
|
* Alice: You know, Bob, 01:30 AM will happen twice tomorrow. Which time do you have in mind?
|
|
* Bob: I did not think about it, but let's pick the first.
|
|
|
|
|
|
2. Why "first"?
|
|
|
|
* Rejections:
|
|
|
|
**second**
|
|
rejected because "second" is already there.
|
|
|
|
**later**
|
|
rejected because "later" is confusable with "latter".
|
|
|
|
**earlier**
|
|
rejected because "earlier" has the same issue as "first" (requires
|
|
default to be True) but is two characters longer.
|
|
|
|
* Remaining possibilities:
|
|
|
|
**repeated**
|
|
this is a strong candidate
|
|
|
|
**is_first**
|
|
arguably more grammatically correct than "first"
|
|
|
|
**ltdf**
|
|
(Local Time Disambiguation Flag) - short and no-one will
|
|
attempt to guess what it means without reading the docs.
|
|
|
|
Implementation
|
|
==============
|
|
|
|
* Github fork: https://github.com/abalkin/cpython
|
|
* Tracker issue: http://bugs.python.org/issue24773
|
|
|
|
|
|
Copyright
|
|
=========
|
|
|
|
This document has been placed in the public domain.
|
|
|
|
|
|
Picture Credit
|
|
==============
|
|
|
|
This image is a work of a U.S. military or Department of Defense
|
|
employee, taken or made as part of that person's official duties. As a
|
|
work of the U.S. federal government, the image is in the public
|
|
domain.
|