Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 15 additions & 6 deletions Doc/library/datetime.rst
Original file line number Diff line number Diff line change
Expand Up @@ -1329,9 +1329,11 @@ Supported operations:

Naive and aware :class:`.datetime` objects are never equal.

If both comparands are aware, and have the same :attr:`!tzinfo` attribute,
the :attr:`!tzinfo` and :attr:`~.datetime.fold` attributes are ignored and
the base datetimes are compared.
If both comparands are aware, and have the same :attr:`!tzinfo` and
:attr:`~.datetime.fold` attributes, the base datetimes are compared.
If both comparands are aware, and have the same :attr:`!tzinfo` but
differing :attr:`~.datetime.fold` attributes, the objects are converted to
timestamps, and the timestamps are compared.
If both comparands are aware and have different :attr:`~.datetime.tzinfo`
attributes, the comparison acts as comparands were first converted to UTC
datetimes except that the implementation never overflows.
Expand All @@ -1345,9 +1347,11 @@ Supported operations:
Order comparison between naive and aware :class:`.datetime` objects
raises :exc:`TypeError`.

If both comparands are aware, and have the same :attr:`!tzinfo` attribute,
the :attr:`!tzinfo` and :attr:`~.datetime.fold` attributes are ignored and
the base datetimes are compared.
If both comparands are aware, and have the same :attr:`!tzinfo` and
:attr:`~.datetime.fold` attributes, the base datetimes are compared.
If both comparands are aware, and have the same :attr:`!tzinfo` but
differing :attr:`~.datetime.fold` attributes, the objects are converted to
timestamps, and the timestamps are compared.
If both comparands are aware and have different :attr:`~.datetime.tzinfo`
attributes, the comparison acts as comparands were first converted to UTC
datetimes except that the implementation never overflows.
Expand All @@ -1364,6 +1368,11 @@ Supported operations:
The default behavior can be changed by overriding the special comparison
methods in subclasses.

.. versionchanged:: 3.15
Comparison between :class:`.datetime` objects with matching :attr:`!tzinfo`
and differing :attr:`~.datetime.fold` attributes uses timestamps for
comparison, so that ordering is preserved even in the case of a repeated
interval.

Instance methods:

Expand Down
5 changes: 5 additions & 0 deletions Lib/_pydatetime.py
Original file line number Diff line number Diff line change
Expand Up @@ -2296,6 +2296,11 @@ def _cmp(self, other, allow_mixed=False):
myoff = otoff = None

if mytz is ottz:
# If the objects' fold properties differ, the `fold=1` timestamp may
# follow the `fold=0` timestamp even though fielf-by-field comparison
# would otherwise conclude that it occurs before. (#146236)
if self.fold != other.fold:
return _cmp(self.timestamp(), other.timestamp())
base_compare = True
else:
myoff = self.utcoffset()
Expand Down
15 changes: 15 additions & 0 deletions Lib/test/datetimetester.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
import textwrap
import unittest
import warnings
import zoneinfo

from array import array

Expand Down Expand Up @@ -5985,6 +5986,20 @@ def test_tricky(self):
self.assertEqual(astz.replace(tzinfo=None), expected)
asutcbase += HOUR

@unittest.skipUnless("America/Los_Angeles" in zoneinfo.available_timezones(),
"Can't find timezone database")
def test_ordering_dst(self):
for utc in utc_real, utc_fake:
for tz in zoneinfo.ZoneInfo("America/Los_Angeles"), zoneinfo.ZoneInfo("America/New_York"):
print(f"{tz!r} {self.dstoff!r} {utc is utc_fake} {id(datetime)}")
tm = tm0 = self.dstoff.replace(tzinfo=tz, hour=0)
print(f"{tm0!r}")
for h in range(4):
for m in 1, 30, 59:
tm1 = (tm.astimezone(utc) + timedelta(hours=h, minutes=m)).astimezone(tz)
print(f"{tm1!r}")
self.assertLess(tm0, tm1)
tm0 = tm1

def test_bogus_dst(self):
class ok(tzinfo):
Expand Down
15 changes: 15 additions & 0 deletions Lib/test/test_zoneinfo/test_zoneinfo.py
Original file line number Diff line number Diff line change
Expand Up @@ -389,6 +389,21 @@ def test_folds_from_utc(self):
dt_after = dt_after_utc.astimezone(zi)
self.assertEqual(dt_after.fold, 1, (dt_after, dt_utc))


def test_ordering_dst(self):
UTC = self.klass("UTC")
dstoff = datetime(2002, 10, 27, 1)
tz = self.klass("America/Los_Angeles")
print(f"{tz!r} {dstoff!r} {id(datetime)}")
tm = tm0 = dstoff.replace(tzinfo=tz, hour=0)
print(f"{tm0!r}")
for h in range(4):
for m in 1, 30, 59:
tm1 = (tm.astimezone(UTC) + timedelta(hours=h, minutes=m)).astimezone(tz)
print(f"{tm1!r}")
self.assertLess(tm0, tm1)
tm0 = tm1

def test_time_variable_offset(self):
# self.zones() only ever returns variable-offset zones
for key in self.zones():
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
Comparison of datetime values with ``fold=1`` now compares the objects'
timestamps so that correct ordering of timestamps is maintained at the end
of DST.
15 changes: 15 additions & 0 deletions Modules/_datetimemodule.c
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,9 @@ typedef struct {
#define CONST_EPOCH(st) st->epoch
#define CONST_UTC(st) ((PyObject *)&utc_timezone)

static PyObject *
datetime_timestamp(PyObject *op, PyObject *Py_UNUSED(dummy));

static datetime_state *
get_module_state(PyObject *module)
{
Expand Down Expand Up @@ -6565,6 +6568,18 @@ datetime_richcompare(PyObject *self, PyObject *other, int op)
}

if (GET_DT_TZINFO(self) == GET_DT_TZINFO(other)) {
// If the objects' fold properties differ, the `fold=1` timestamp may
// follow the `fold=0` timestamp even though fielf-by-field comparison
// would otherwise conclude that it occurs before. (#146236)
if (DATE_GET_FOLD(self) != DATE_GET_FOLD(other)) {
PyObject *ts_self = datetime_timestamp(self, NULL);
PyObject *ts_other = datetime_timestamp(other, NULL);
PyObject *result = PyObject_RichCompare(ts_self, ts_other, op);
Py_DECREF(ts_self);
Py_DECREF(ts_other);
return result;
}

diff = memcmp(((PyDateTime_DateTime *)self)->data,
((PyDateTime_DateTime *)other)->data,
_PyDateTime_DATETIME_DATASIZE);
Expand Down
Loading