diff --git a/src/borg/helpers/time.py b/src/borg/helpers/time.py index a05952ebe9..011d47e40a 100644 --- a/src/borg/helpers/time.py +++ b/src/borg/helpers/time.py @@ -142,7 +142,7 @@ def calculate_relative_offset(format_string, from_ts, earlier=False): match unit: case "y": - return from_ts.replace(year=from_ts.year + offset) + return offset_n_months(from_ts, offset * 12) case "m": return offset_n_months(from_ts, offset) case "w": @@ -173,8 +173,15 @@ def get_month_and_year_from_total(total_completed_months): following_month, year_of_following_month = get_month_and_year_from_total(total_months + 1) max_days_in_month = (datetime(year_of_following_month, following_month, 1) - timedelta(1)).day - return datetime(day=min(from_ts.day, max_days_in_month), month=target_month, year=target_year).replace( - tzinfo=from_ts.tzinfo + return datetime( + day=min(from_ts.day, max_days_in_month), + month=target_month, + year=target_year, + hour=from_ts.hour, + minute=from_ts.minute, + second=from_ts.second, + microsecond=from_ts.microsecond, + tzinfo=from_ts.tzinfo, ) diff --git a/src/borg/testsuite/helpers/time_test.py b/src/borg/testsuite/helpers/time_test.py index 7dcffc8278..c43b315309 100644 --- a/src/borg/testsuite/helpers/time_test.py +++ b/src/borg/testsuite/helpers/time_test.py @@ -1,7 +1,7 @@ import pytest from datetime import datetime, timezone -from ...helpers.time import safe_ns, safe_s, SUPPORT_32BIT_PLATFORMS +from ...helpers.time import safe_ns, safe_s, SUPPORT_32BIT_PLATFORMS, calculate_relative_offset def utcfromtimestamp(timestamp): @@ -36,3 +36,18 @@ def test_safe_timestamps(): utcfromtimestamp(beyond_y10k) assert utcfromtimestamp(safe_s(beyond_y10k)) > datetime(2262, 1, 1) assert utcfromtimestamp(safe_ns(beyond_y10k) / 1000000000) > datetime(2262, 1, 1) + + +def test_calculate_relative_offset_year_from_leap_day(): + # regression test for #9967: year offset from Feb 29 must not crash on non-leap target year. + leap_day = datetime(2024, 2, 29, tzinfo=timezone.utc) + assert calculate_relative_offset("1y", leap_day, earlier=False) == datetime(2025, 2, 28, tzinfo=timezone.utc) + assert calculate_relative_offset("1y", leap_day, earlier=True) == datetime(2023, 2, 28, tzinfo=timezone.utc) + # target year is also a leap year -> keep Feb 29. + assert calculate_relative_offset("4y", leap_day, earlier=False) == datetime(2028, 2, 29, tzinfo=timezone.utc) + + +def test_calculate_relative_offset_year_regular(): + ts = datetime(2024, 6, 15, 12, 30, 45, tzinfo=timezone.utc) + assert calculate_relative_offset("2y", ts, earlier=False) == datetime(2026, 6, 15, 12, 30, 45, tzinfo=timezone.utc) + assert calculate_relative_offset("2y", ts, earlier=True) == datetime(2022, 6, 15, 12, 30, 45, tzinfo=timezone.utc)