Skip to content

Fix comparison across Daylight Savings Time boundary #596

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 11 commits into
base: master
Choose a base branch
from
38 changes: 22 additions & 16 deletions pendulum/datetime.py
Original file line number Diff line number Diff line change
Expand Up @@ -1310,22 +1310,28 @@ def __reduce__(self) -> tuple:
def __reduce_ex__(self, protocol: int) -> tuple:
return self.__class__, self._getstate(protocol)

def _cmp(self, other: datetime.datetime, **kwargs) -> int:
# Fix for pypy which compares using this method
# which would lead to infinite recursion if we didn't override
dt = datetime.datetime(
self.year,
self.month,
self.day,
self.hour,
self.minute,
self.second,
self.microsecond,
tzinfo=self.tz,
fold=self.fold,
)

return 0 if dt == other else 1 if dt > other else -1
def __le__(self, other):
if isinstance(other, DateTime):
return self._cmp(other) <= 0
return super().__le__(other)

def __lt__(self, other):
if isinstance(other, DateTime):
return self._cmp(other) < 0
return super().__lt__(other)

def __ge__(self, other):
# Will default to the negative of its reflection
return NotImplemented

def __gt__(self, other):
# Will default to the negative of its reflection
return NotImplemented

def _cmp(self, other, **kwargs):
sts = self.timestamp()
ots = other.timestamp()
return 0 if sts == ots else 1 if sts > ots else -1


DateTime.min: DateTime = DateTime(1, 1, 1, 0, 0, tzinfo=UTC)
Expand Down
95 changes: 94 additions & 1 deletion tests/datetime/test_comparison.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
from __future__ import annotations

from datetime import datetime
from datetime import datetime, timedelta, tzinfo
from dateutil import tz
from pytz import timezone

import pytz
import pytest


import pendulum

Expand Down Expand Up @@ -195,6 +199,95 @@ def test_less_than_with_timezone_false():
assert not d1 < d3


@pytest.mark.parametrize(
'truth_fun',
(
lambda earlier, later: earlier < later,
lambda earlier, later: earlier <= later,
lambda earlier, later: later > earlier,
lambda earlier, later: later >= earlier,
)
)
def test_comparison_crossing_dst_transitioning_off_pendulum_pendulum(truth_fun):
# We only need to test turning off DST, since that's when the time
# component goes backwards.
# We start with 2019-11-03T01:30:00-0700
earlier = pendulum.datetime(2019, 11, 3, 8, 30).in_tz("US/Pacific")
# Adding 55 minutes to it, we turn off DST, but the time component is
# slightly less than before, i.e. we get 2019-11-03T01:25:00-0800
later = earlier.add(minutes=55)
# Run through all inequality-comparison functions
assert truth_fun(earlier, later)


@pytest.mark.parametrize(
'truth_fun',
(
lambda earlier, later: earlier < later,
lambda earlier, later: earlier <= later,
lambda earlier, later: later > earlier,
lambda earlier, later: later >= earlier,
)
)
def test_comparison_crossing_dst_transitioning_off_pendulum_datetime(truth_fun):
# We only need to test turning off DST, since that's when the time
# component goes backwards.
# We start with 2019-11-03T01:30:00-0700
earlier_pendulum = pendulum.datetime(2019, 11, 3, 8, 30).in_tz("US/Pacific")
us_pacific = timezone("US/Pacific")
earlier_datetime = datetime(2019, 11, 3, 1, 30, tzinfo=us_pacific)
# Adding 55 minutes to it, we turn off DST, but the time component is
# slightly less than before, i.e. we get 2019-11-03T01:25:00-0800
later_datetime = earlier_datetime + timedelta(minutes=55)
# Run through all inequality-comparison functions
assert truth_fun(earlier_pendulum, later_datetime)


@pytest.mark.parametrize(
'truth_fun',
(
lambda earlier, later: earlier < later,
lambda earlier, later: earlier <= later,
lambda earlier, later: later > earlier,
lambda earlier, later: later >= earlier,
)
)
def test_comparison_crossing_dst_transitioning_off_datetime_pendulum(truth_fun):
# We only need to test turning off DST, since that's when the time
# component goes backwards.
# We start with 2019-11-03T01:30:00-0700
earlier_pendulum = pendulum.datetime(2019, 11, 3, 8, 30).in_tz("US/Pacific")
us_pacific = timezone("US/Pacific")
earlier_datetime = datetime(2019, 11, 3, 1, 30, tzinfo=us_pacific)
# Adding 55 minutes to it, we turn off DST, but the time component is
# slightly less than before, i.e. we get 2019-11-03T01:25:00-0800
later_pendulum = earlier_pendulum.add(minutes=55)
# Run through all inequality-comparison functions
assert truth_fun(earlier_datetime, later_pendulum)


@pytest.mark.parametrize(
'truth_fun',
(
lambda earlier, later: earlier < later,
lambda earlier, later: earlier <= later,
lambda earlier, later: later > earlier,
lambda earlier, later: later >= earlier,
)
)
def test_comparison_crossing_dst_transitioning_off_datetime_datetime(truth_fun):
# We only need to test turning off DST, since that's when the time
# component goes backwards.
# We start with 2019-11-03T01:30:00-0700
us_pacific = timezone("US/Pacific")
earlier = datetime(2019, 11, 3, 1, 30, tzinfo=us_pacific)
# Adding 55 minutes to it, we turn off DST, but the time component is
# slightly less than before, i.e. we get 2019-11-03T01:25:00-0800
later = earlier + timedelta(minutes=55)
# Run through all inequality-comparison functions
assert truth_fun(earlier, later)


def test_less_than_or_equal_true():
d1 = pendulum.datetime(2000, 1, 1)
d2 = pendulum.datetime(2000, 1, 2)
Expand Down