diff --git a/tests/test_ranges.py b/tests/test_ranges.py index b0d0416..bc04572 100644 --- a/tests/test_ranges.py +++ b/tests/test_ranges.py @@ -1152,6 +1152,54 @@ def test_yields_correct_ranges(self, row): assert result == row["expected"] +class TestDateRangeForMidnightRange: + def test_returns_date_range(self): + dt_range = ranges.FiniteDatetimeRange( + datetime.datetime(2020, 1, 1), + datetime.datetime(2020, 1, 10), + ) + + assert ranges.date_range_for_midnight_range(dt_range) == ranges.FiniteDateRange( + datetime.date(2020, 1, 1), + datetime.date(2020, 1, 9), + ) + + def test_errors_if_different_timezones(self): + dt_range = ranges.FiniteDatetimeRange( + datetime.datetime(2020, 1, 1, tzinfo=zoneinfo.ZoneInfo("Asia/Dubai")), + datetime.datetime( + 2020, 1, 10, tzinfo=zoneinfo.ZoneInfo("Australia/Sydney") + ), + ) + + with pytest.raises(ValueError) as exc_info: + ranges.date_range_for_midnight_range(dt_range) + + assert "Start and end in different timezones" in str(exc_info.value) + + def test_errors_if_start_not_midnight(self): + dt_range = ranges.FiniteDatetimeRange( + datetime.datetime(2020, 1, 1, hour=1), + datetime.datetime(2020, 1, 10), + ) + + with pytest.raises(ValueError) as exc_info: + ranges.date_range_for_midnight_range(dt_range) + + assert "Start of range is not midnight-aligned" in str(exc_info.value) + + def test_errors_if_end_not_midnight(self): + dt_range = ranges.FiniteDatetimeRange( + datetime.datetime(2020, 1, 1), + datetime.datetime(2020, 1, 10, hour=1), + ) + + with pytest.raises(ValueError) as exc_info: + ranges.date_range_for_midnight_range(dt_range) + + assert "End of range is not midnight-aligned" in str(exc_info.value) + + def _rangeset_from_string(rangeset_str: str) -> ranges.RangeSet[int]: """ Convenience method to make test declarations clearer. diff --git a/xocto/ranges.py b/xocto/ranges.py index ea91a27..f43a02b 100644 --- a/xocto/ranges.py +++ b/xocto/ranges.py @@ -1085,3 +1085,34 @@ def iterate_over_months( yield FiniteDatetimeRange(start_at, this_end) start_at = next_start + + +def date_range_for_midnight_range( + range: FiniteDatetimeRange, +) -> FiniteDateRange: + """ + Returns the date range of a midnight-aligned datetime range. + + This can be useful where a range is available at datetime granularity, + but is used in functions that operate at date granularity. + + Raises: + ValueError: + If the range boundaries are in different timezeones. + If the range boundaries are not midnight-aligned. + """ + # First check range timezone is uniform. + if range.start.tzinfo != range.end.tzinfo: + raise ValueError("Start and end in different timezones") + + # Check datetimes are both midnight-aligned. + if range.start.time() != datetime.time(0, 0): + raise ValueError("Start of range is not midnight-aligned") + + if range.end.time() != datetime.time(0, 0): + raise ValueError("End of range is not midnight-aligned") + + return FiniteDateRange( + range.start.date(), + localtime.day_before(range.end.date()), + )