From b42ca4ea6c7e1c161ea9e627adca69bc18e5a2fb Mon Sep 17 00:00:00 2001 From: Peter Byfield Date: Thu, 30 Jan 2025 09:53:03 +0100 Subject: [PATCH] Update FiniteDatetimeRange.days to require passing a TZ It's possible to create mixed TZ FiniteDatetimeRange's. It probably shouldn't be, but it currently is and this is unlikely to change too soon. See https://github.com/octoenergy/xocto/issues/192 This makes the FiniteDatetimeRange.days poorly defined. For example, should the below range be 31 or 30 days? RANGE = ranges.FiniteDatetimeRange( # This is also 2024-03-01T00:00:00 in TZ_UTC datetime.datetime(2024, 3, 1, tzinfo=TZ_LONDON), # This is 2024-04-01T00:00:00 in TZ_LONDON datetime.datetime(2024, 3, 31, hour=23, tzinfo=TZ_UTC), ) This PR updates FiniteDatetimeRange.days to require passing a TZ, to remove this ambiguity. --- tests/test_ranges.py | 21 +++++++++++++++++++++ xocto/ranges.py | 6 +++--- 2 files changed, 24 insertions(+), 3 deletions(-) diff --git a/tests/test_ranges.py b/tests/test_ranges.py index 4550d4d..ddf1145 100644 --- a/tests/test_ranges.py +++ b/tests/test_ranges.py @@ -13,6 +13,10 @@ from xocto import localtime, ranges +TZ_UTC = zoneinfo.ZoneInfo("UTC") +TZ_LONDON = zoneinfo.ZoneInfo("Europe/London") + + @composite def valid_integer_range(draw): boundaries = draw(sampled_from(ranges.RangeBoundaries)) @@ -1278,6 +1282,23 @@ def test_errors_if_end_not_midnight(self): assert "End of range is not midnight-aligned" in str(exc_info.value) + class TestDays: + # This range crosses the DST boundary. + # At the start TZ_LONDON is equal to TZ_UTC. + # At the end TZ_LONDON is +1 to TZ_UTC. + RANGE = ranges.FiniteDatetimeRange( + # This is also 2024-03-01T00:00:00 in TZ_UTC + datetime.datetime(2024, 3, 1, tzinfo=TZ_LONDON), + # This is 2024-04-01T00:00:00 in TZ_LONDON + datetime.datetime(2024, 3, 31, hour=23, tzinfo=TZ_UTC), + ) + + def test_days_in_london(self): + assert self.RANGE.days(tz=TZ_LONDON) == 31 + + def test_days_in_utc(self): + assert self.RANGE.days(tz=TZ_UTC) == 30 + class TestAsFiniteDatetimePeriods: def test_converts(self): diff --git a/xocto/ranges.py b/xocto/ranges.py index 34581e9..2bd2791 100644 --- a/xocto/ranges.py +++ b/xocto/ranges.py @@ -901,12 +901,12 @@ def __and__( ) -> Optional["FiniteDatetimeRange"]: return self.intersection(other) - @property - def days(self) -> int: + def days(self, tz: datetime.tzinfo) -> int: """ Return the number of days between the start and end of the range. """ - return (self.end - self.start).days + range_ = self.localize(tz) + return (range_.end - range_.start).days @property def seconds(self) -> int: