diff --git a/tests/test_domain.py b/tests/test_domain.py index 97a8f8a..35f8e81 100644 --- a/tests/test_domain.py +++ b/tests/test_domain.py @@ -268,6 +268,30 @@ def test_split_into_days_keeps_shifted_boundaries_after_dst(monkeypatch): time.tzset() +@pytest.mark.skipif(not hasattr(time, "tzset"), reason="tzset is unavailable") +@pytest.mark.parametrize("shift", [0, 2 * HOUR_MS]) +def test_split_into_days_handles_dst_fall_back(monkeypatch, shift): + # 25 October 2026 lasts 25 hours in Berlin; 24 hours on never left it (#21). + previous = os.environ.get("TZ") + monkeypatch.setenv("TZ", "Europe/Berlin") + time.tzset() + try: + now = ms(2026, 10, 8, 12) + october = split_into_days(get_range(RangeLength.MONTH, now=now, start_of_day_shift=shift), shift) + year = split_into_days(get_range(RangeLength.YEAR, now=now, start_of_day_shift=shift), shift) + + assert len(october) == 31 + assert october[24] == day_range(date(2026, 10, 25), shift) + assert october[24].duration == 25 * HOUR_MS + assert len(year) == 365 + finally: + if previous is None: + monkeypatch.delenv("TZ", raising=False) + else: + monkeypatch.setenv("TZ", previous) + time.tzset() + + # --------------------------------------------------------------------------- # # Statistics # --------------------------------------------------------------------------- # diff --git a/timetracker/domain/ranges.py b/timetracker/domain/ranges.py index 58452e8..91d9842 100644 --- a/timetracker/domain/ranges.py +++ b/timetracker/domain/ranges.py @@ -6,7 +6,6 @@ from enum import Enum from ..models import Range -from .duration import DAY_MS class RangeLength(str, Enum): @@ -143,9 +142,11 @@ def split_into_days(time_range: Range, start_of_day_shift: int = 0) -> list[Rang return [] result: list[Range] = [] cursor = start_of_day(time_range.time_started, start_of_day_shift) + day = to_datetime(shift_timestamp(cursor, -start_of_day_shift)).date() while cursor < time_range.time_ended: - # The next boundary is simply the start of the day 24 hours on. - next_day = start_of_day(cursor + DAY_MS, start_of_day_shift) + # Step by calendar date: a fixed 24 hours never leaves a 25-hour DST day. + day += timedelta(days=1) + next_day = day_range(day, start_of_day_shift).time_started result.append(Range(max(cursor, time_range.time_started), min(next_day, time_range.time_ended))) cursor = next_day return result