Skip to content

Commit

Permalink
Add localtime function period_exceeds_one_year
Browse files Browse the repository at this point in the history
Useful for determining whether two datetimes are more than
one year apart. Takes into account leap year and DST issues.

Context: In germany an invoice may not legally cover more
than one year of consumption
  • Loading branch information
Peter554 committed Feb 27, 2023
1 parent dad4f9b commit 2f69da1
Show file tree
Hide file tree
Showing 2 changed files with 71 additions and 0 deletions.
43 changes: 43 additions & 0 deletions tests/test_localtime.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import pytest
import pytz
import time_machine
from dateutil import relativedelta
from django.conf import settings
from django.test import override_settings
from django.utils import timezone
Expand Down Expand Up @@ -1029,3 +1030,45 @@ def test_timestamp_british_summer_time_after_clocks_move_backward(self):
assert dt.day == 25
assert dt.hour == 1
assert dt.minute == 30


@pytest.mark.parametrize(
("period_start_at", "first_dt_exceeding_one_year"),
[
(
# Basic case.
localtime.datetime(2021, 1, 1),
localtime.datetime(2022, 1, 1, microsecond=1),
),
(
# A leap year.
localtime.datetime(2020, 1, 1),
localtime.datetime(2021, 1, 1, microsecond=1),
),
(
# Start on a leap year, Feb 29th.
localtime.datetime(2020, 2, 29),
localtime.datetime(2021, 3, 1, microsecond=1),
),
(
# End on a leap year, March 1st.
localtime.datetime(2019, 3, 1),
localtime.datetime(2020, 3, 1, microsecond=1),
),
(
# Clock moves backward twice.
localtime.datetime(2021, 10, 31),
localtime.datetime(2022, 10, 31, microsecond=1),
),
(
# Clock moves forward twice.
localtime.datetime(2021, 3, 28),
localtime.datetime(2022, 3, 28, microsecond=1),
),
],
)
def test_period_exceeds_one_year(period_start_at, first_dt_exceeding_one_year):
assert localtime.period_exceeds_one_year(period_start_at, first_dt_exceeding_one_year)
assert not localtime.period_exceeds_one_year(
period_start_at, first_dt_exceeding_one_year - relativedelta.relativedelta(microseconds=1)
)
28 changes: 28 additions & 0 deletions xocto/localtime.py
Original file line number Diff line number Diff line change
Expand Up @@ -692,3 +692,31 @@ def translate_english_month_to_spanish(month: int) -> str:
"December": "deciembre",
}
return month_name_lookup[month_name]


def period_exceeds_one_year(start_at: DateTime, end_at: DateTime) -> bool:
"""
Returns true if the passed period exceeds one year.
Edge cases such as leap years and daylight savings time are handled, where a simple approach
using only relativedelta would not be sufficient.
"""

# We take everything as localtime, and then remove the timezone information. This is to
# avoid false results when the period starts or ends on a leap day, or when an uneven
# number of DST changes happen in the year covered by the invoice.

start_at = as_localtime(start_at)
end_at = as_localtime(end_at)

tz_unaware_start_at = start_at.replace(tzinfo=None)
tz_unaware_end_at = end_at.replace(tzinfo=None)

if tz_unaware_start_at.month == 2 and tz_unaware_start_at.day == 29:
# Leap year case, on 29th Feb.
one_year_after_start_at = tz_unaware_start_at + relativedelta(years=1)
# One year after 29th Feb is 1st March (not 28th Feb).
one_year_after_start_at = one_year_after_start_at.replace(month=3, day=1)
return tz_unaware_end_at > one_year_after_start_at

return tz_unaware_end_at > tz_unaware_start_at + relativedelta(years=1)

0 comments on commit 2f69da1

Please sign in to comment.