Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add localtime function period_exceeds_one_year #41

Merged
merged 1 commit into from
Feb 28, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
58 changes: 58 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,60 @@ 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 28th.
localtime.datetime(2020, 2, 28),
localtime.datetime(2021, 2, 28, microsecond=1),
),
(
# Start on a leap year, Feb 29th.
localtime.datetime(2020, 2, 29),
localtime.datetime(2021, 3, 1, microsecond=1), # !important
),
(
# Start on a leap year, March 1st.
localtime.datetime(2020, 3, 1),
localtime.datetime(2021, 3, 1, microsecond=1),
),
(
# End on a leap year, Feb 28th.
localtime.datetime(2019, 2, 28),
localtime.datetime(2020, 2, 28, 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)

one_year_after_start_at = tz_unaware_start_at + relativedelta(years=1)

if tz_unaware_start_at.month == 2 and tz_unaware_start_at.day == 29:
# Leap year case, on 29th Feb.
# 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