From 7a2fb2af7cf240b792a8f082c2cfdc97eac2d047 Mon Sep 17 00:00:00 2001 From: Mantas Kantautas Date: Sun, 25 Oct 2026 17:28:27 +0200 Subject: [PATCH 1/2] fix: preserve microseconds in Time differences --- src/pendulum/time.py | 4 ++-- tests/time/test_diff.py | 45 +++++++++++++++++++++++++++++++++++------ tests/time/test_sub.py | 22 ++++++++++++++++++++ 3 files changed, 63 insertions(+), 8 deletions(-) diff --git a/src/pendulum/time.py b/src/pendulum/time.py index 58ca7e819..05007a7c6 100644 --- a/src/pendulum/time.py +++ b/src/pendulum/time.py @@ -226,11 +226,11 @@ def diff(self, dt: time | None = None, abs: bool = True) -> Duration: us1 = ( self.hour * SECS_PER_HOUR + self.minute * SECS_PER_MIN + self.second - ) * USECS_PER_SEC + ) * USECS_PER_SEC + self.microsecond us2 = ( dt.hour * SECS_PER_HOUR + dt.minute * SECS_PER_MIN + dt.second - ) * USECS_PER_SEC + ) * USECS_PER_SEC + dt.microsecond klass = Duration if abs: diff --git a/tests/time/test_diff.py b/tests/time/test_diff.py index 629a0580e..abb1531a2 100644 --- a/tests/time/test_diff.py +++ b/tests/time/test_diff.py @@ -1,5 +1,7 @@ from __future__ import annotations +from datetime import time + import pendulum from pendulum import Time @@ -96,6 +98,37 @@ def test_diff_in_seconds_ensure_is_truncated(): assert dt.diff(dt.add(seconds=1.9)).in_seconds() == 1 +def test_diff_includes_microseconds(): + dt = Time(12, 0, 0, 500000) + diff = dt.diff(Time(12, 0, 1)) + + assert diff.total_seconds() == 0.5 + assert diff.microseconds == 500000 + assert diff.in_seconds() == 0 + + +def test_diff_includes_microseconds_negative_with_sign(): + dt = Time(12, 0, 1) + diff = dt.diff(Time(12, 0, 0, 250000), False) + + assert diff.total_seconds() == -0.75 + assert diff.microseconds == -750000 + + +def test_diff_includes_microseconds_within_the_same_second(): + dt = Time(12, 0, 0, 1) + assert dt.diff(Time(12, 0, 0, 999999)).total_seconds() == 0.999998 + + +def test_diff_includes_microseconds_of_native_time(): + dt = Time(12, 0, 0) + assert dt.diff(time(12, 0, 0, 750000)).total_seconds() == 0.75 + + +def test_diff_min_and_max(): + assert Time.min.diff(Time.max).total_seconds() == 86399.999999 + + def test_diff_for_humans_now_and_second(): with pendulum.travel_to(pendulum.today().at(12, 34, 56)): now = pendulum.now().time() @@ -167,42 +200,42 @@ def test_diff_for_humans_now_and_future_seconds(): def test_diff_for_humans_now_and_nearly_future_minute(): - with pendulum.travel_to(pendulum.today().at(12, 34, 56)): + with pendulum.travel_to(pendulum.today().at(12, 34, 56), freeze=True): now = pendulum.now().time() assert now.add(seconds=59).diff_for_humans() == "in 59 seconds" def test_diff_for_humans_now_and_future_minute(): - with pendulum.travel_to(pendulum.today().at(12, 34, 56)): + with pendulum.travel_to(pendulum.today().at(12, 34, 56), freeze=True): now = pendulum.now().time() assert now.add(minutes=1).diff_for_humans() == "in 1 minute" def test_diff_for_humans_now_and_future_minutes(): - with pendulum.travel_to(pendulum.today().at(12, 34, 56)): + with pendulum.travel_to(pendulum.today().at(12, 34, 56), freeze=True): now = pendulum.now().time() assert now.add(minutes=2).diff_for_humans() == "in 2 minutes" def test_diff_for_humans_now_and_nearly_future_hour(): - with pendulum.travel_to(pendulum.today().at(12, 34, 56)): + with pendulum.travel_to(pendulum.today().at(12, 34, 56), freeze=True): now = pendulum.now().time() assert now.add(minutes=59).diff_for_humans() == "in 59 minutes" def test_diff_for_humans_now_and_future_hour(): - with pendulum.travel_to(pendulum.today().at(12, 34, 56)): + with pendulum.travel_to(pendulum.today().at(12, 34, 56), freeze=True): now = pendulum.now().time() assert now.add(hours=1).diff_for_humans() == "in 1 hour" def test_diff_for_humans_now_and_future_hours(): - with pendulum.travel_to(pendulum.today().at(12, 34, 56)): + with pendulum.travel_to(pendulum.today().at(12, 34, 56), freeze=True): now = pendulum.now().time() assert now.add(hours=2).diff_for_humans() == "in 2 hours" diff --git a/tests/time/test_sub.py b/tests/time/test_sub.py index 6d05d31ee..5e39d400d 100644 --- a/tests/time/test_sub.py +++ b/tests/time/test_sub.py @@ -111,3 +111,25 @@ def test_subtract_time(): with pytest.raises(TypeError): t3 - t + + +def test_subtract_time_includes_microseconds(): + t = Time(12, 0, 1) + t1 = Time(12, 0, 0, 500000) + t2 = time(12, 0, 0, 500000) + + diff = t - t1 + assert isinstance(diff, pendulum.Duration) + assert diff.total_seconds() == 0.5 + + diff = t1 - t + assert diff.total_seconds() == -0.5 + + diff = t - t2 + assert diff.total_seconds() == 0.5 + + diff = t2 - t + assert diff.total_seconds() == -0.5 + + diff = Time(12, 0, 0, 999999) - t + assert diff.total_seconds() == -1e-06 From 109fcae9f4014cb737ad51c80204bbbd456761ae Mon Sep 17 00:00:00 2001 From: Mantas Kantautas Date: Sun, 25 Oct 2026 19:54:41 +0200 Subject: [PATCH 2/2] fix: compare precise distances in Time selection --- src/pendulum/time.py | 4 +-- tests/time/test_comparison.py | 50 +++++++++++++++++++++++++++++++++++ 2 files changed, 52 insertions(+), 2 deletions(-) diff --git a/src/pendulum/time.py b/src/pendulum/time.py index 05007a7c6..6c53e4cff 100644 --- a/src/pendulum/time.py +++ b/src/pendulum/time.py @@ -69,7 +69,7 @@ def closest(self, dt1: Time | time, dt2: Time | time) -> Self: dt1 = self.__class__(dt1.hour, dt1.minute, dt1.second, dt1.microsecond) dt2 = self.__class__(dt2.hour, dt2.minute, dt2.second, dt2.microsecond) - if self.diff(dt1).in_seconds() < self.diff(dt2).in_seconds(): + if self.diff(dt1).total_seconds() < self.diff(dt2).total_seconds(): return dt1 return dt2 @@ -81,7 +81,7 @@ def farthest(self, dt1: Time | time, dt2: Time | time) -> Self: dt1 = self.__class__(dt1.hour, dt1.minute, dt1.second, dt1.microsecond) dt2 = self.__class__(dt2.hour, dt2.minute, dt2.second, dt2.microsecond) - if self.diff(dt1).in_seconds() > self.diff(dt2).in_seconds(): + if self.diff(dt1).total_seconds() > self.diff(dt2).total_seconds(): return dt1 return dt2 diff --git a/tests/time/test_comparison.py b/tests/time/test_comparison.py index 23dd176ca..7a96c7003 100644 --- a/tests/time/test_comparison.py +++ b/tests/time/test_comparison.py @@ -178,6 +178,56 @@ def test_farthest_with_equals(): assert t2 == farthest +def test_closest_with_microseconds(): + instance = pendulum.time(12, 0, 0, 900000) + t1 = pendulum.time(12, 0, 0, 900000) + t2 = pendulum.time(12, 0, 1, 100000) + + assert instance.closest(t1, t2) == t1 + assert instance.closest(t2, t1) == t1 + + +def test_closest_with_microseconds_across_a_second(): + instance = pendulum.time(12, 0, 0, 900000) + t1 = pendulum.time(12, 0, 1) + t2 = pendulum.time(12, 0, 0) + + assert instance.closest(t1, t2) == t1 + assert instance.closest(t2, t1) == t1 + + +def test_closest_with_microseconds_of_native_times(): + instance = pendulum.time(12, 0, 0, 900000) + closest = instance.closest(time(12, 0, 0, 900000), time(12, 0, 1, 100000)) + + assert_time(closest, 12, 0, 0, 900000) + + +def test_farthest_with_microseconds(): + instance = pendulum.time(12, 0, 0, 900000) + t1 = pendulum.time(12, 0, 0, 900000) + t2 = pendulum.time(12, 0, 1, 100000) + + assert instance.farthest(t1, t2) == t2 + assert instance.farthest(t2, t1) == t2 + + +def test_farthest_with_microseconds_across_a_second(): + instance = pendulum.time(12, 0, 0, 900000) + t1 = pendulum.time(12, 0, 1) + t2 = pendulum.time(12, 0, 0) + + assert instance.farthest(t1, t2) == t2 + assert instance.farthest(t2, t1) == t2 + + +def test_farthest_with_microseconds_of_native_times(): + instance = pendulum.time(12, 0, 0, 900000) + farthest = instance.farthest(time(12, 0, 1, 100000), time(12, 0, 0, 900000)) + + assert_time(farthest, 12, 0, 1, 100000) + + def test_comparison_to_unsupported(): t1 = pendulum.now().time()