Skip to content
Open
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
16 changes: 16 additions & 0 deletions docs/docs/comparison.md
Original file line number Diff line number Diff line change
Expand Up @@ -75,3 +75,19 @@ True
# Compares to now by default
True
```

The `closest()` and `farthest()` methods also work with naive datetimes.
When the reference datetime is naive, naive candidates remain naive:

```python
>>> reference = pendulum.naive(2026, 10, 20, 12)
>>> before = pendulum.naive(2026, 10, 20, 11)
>>> after = pendulum.naive(2026, 10, 20, 14)
>>> reference.closest(before, after) == before
True
>>> reference.farthest(before, after) == after
True
```

As with datetime subtraction, a naive reference cannot be compared with an aware
candidate. For an aware reference, naive candidates continue to use UTC.
6 changes: 4 additions & 2 deletions src/pendulum/datetime.py
Original file line number Diff line number Diff line change
Expand Up @@ -499,15 +499,17 @@ def closest(self, *dts: datetime.datetime) -> Self: # type: ignore[override]
"""
Get the closest date to the instance.
"""
pdts = [self.instance(x) for x in dts]
tz = UTC if self.tzinfo is not None else None
pdts = [self.instance(x, tz=tz) for x in dts]

return min((abs(self - dt), dt) for dt in pdts)[1]

def farthest(self, *dts: datetime.datetime) -> Self: # type: ignore[override]
"""
Get the farthest date from the instance.
"""
pdts = [self.instance(x) for x in dts]
tz = UTC if self.tzinfo is not None else None
pdts = [self.instance(x, tz=tz) for x in dts]

return max((abs(self - dt), dt) for dt in pdts)[1]

Expand Down
102 changes: 102 additions & 0 deletions tests/datetime/test_comparison.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@

from datetime import datetime

import pytest

import pendulum

from tests.conftest import assert_datetime
Expand Down Expand Up @@ -374,6 +376,106 @@ def test_farthest_with_equals():
assert farthest == dts[-1]


@pytest.mark.parametrize("method", ["closest", "farthest"])
@pytest.mark.parametrize("native", [False, True])
@pytest.mark.parametrize("reverse", [False, True])
def test_closest_and_farthest_with_naive_datetimes(method, native, reverse):
instance = pendulum.naive(2026, 10, 20, 12, 34, 56, 123456)
near = instance.subtract(microseconds=1)
far = instance.add(microseconds=2)
candidates = [near, far]
if native:
candidates = [
datetime(
dt.year,
dt.month,
dt.day,
dt.hour,
dt.minute,
dt.second,
dt.microsecond,
)
for dt in candidates
]
if reverse:
candidates.reverse()

result = getattr(instance, method)(*candidates)

assert result == (near if method == "closest" else far)
assert type(result) is pendulum.DateTime
assert result.tzinfo is None


@pytest.mark.parametrize("method", ["closest", "farthest"])
def test_closest_and_farthest_with_naive_single_candidate(method):
instance = pendulum.naive(2026, 10, 20)

assert getattr(instance, method)(instance) == instance


@pytest.mark.parametrize("method", ["closest", "farthest"])
@pytest.mark.parametrize("reverse", [False, True])
@pytest.mark.parametrize("tz", [None, "UTC"])
def test_closest_and_farthest_with_ties(method, reverse, tz):
instance = pendulum.datetime(2026, 10, 20, 12, tz=tz)
before = instance.subtract(hours=1)
after = instance.add(hours=1)
candidates = [before, after]
if reverse:
candidates.reverse()

result = getattr(instance, method)(*candidates)

assert result == candidates[0]
assert result.tzinfo == instance.tzinfo


@pytest.mark.parametrize("method", ["closest", "farthest"])
def test_closest_and_farthest_with_naive_subclass(method):
class CustomDateTime(pendulum.DateTime):
pass

instance = CustomDateTime(2026, 10, 20, 12)
candidates = [datetime(2026, 10, 20, 11), datetime(2026, 10, 20, 14)]

result = getattr(instance, method)(*candidates)

assert type(result) is CustomDateTime
assert result.hour == (11 if method == "closest" else 14)
assert result.tzinfo is None


@pytest.mark.parametrize("method", ["closest", "farthest"])
@pytest.mark.parametrize("tz", [None, "UTC", "Europe/Paris"])
def test_closest_and_farthest_without_candidates(method, tz):
instance = pendulum.datetime(2026, 10, 20, tz=tz)

with pytest.raises(ValueError):
getattr(instance, method)()


@pytest.mark.parametrize("method", ["closest", "farthest"])
def test_closest_and_farthest_with_naive_reference_and_aware_candidate(method):
instance = pendulum.naive(2026, 10, 20)
candidate = pendulum.datetime(2026, 10, 21)

with pytest.raises(TypeError, match="offset-naive and offset-aware"):
getattr(instance, method)(candidate)


@pytest.mark.parametrize("method", ["closest", "farthest"])
def test_closest_and_farthest_aware_reference_keeps_naive_candidate_default(method):
instance = pendulum.datetime(2026, 10, 20, 12, tz="Europe/Paris")
naive = datetime(2026, 10, 20, 10)
aware = pendulum.datetime(2026, 10, 20, 13, tz="Europe/Paris")

result = getattr(instance, method)(naive, aware)

assert result == (pendulum.instance(naive) if method == "closest" else aware)
assert result.timezone_name == ("UTC" if method == "closest" else "Europe/Paris")


def test_is_same_day():
dt1 = pendulum.datetime(2015, 5, 28, 12, 0, 0)
dt2 = pendulum.datetime(2015, 5, 29, 12, 0, 0)
Expand Down
Loading