66 lines
1.8 KiB
Python
66 lines
1.8 KiB
Python
"""Date calculation utilities for leave planning.
|
|
|
|
Pure functions with no I/O or state. Used by planning and analytics services.
|
|
"""
|
|
|
|
import datetime
|
|
from typing import Union
|
|
|
|
|
|
def add_months(date: datetime.date, n: int) -> datetime.date:
|
|
"""Add or subtract months from a date, handling month-end edge cases.
|
|
|
|
Examples:
|
|
add_months(datetime.date(2026, 1, 31), 1) -> 2026-02-28 (or 29 in leap year)
|
|
add_months(datetime.date(2026, 3, 31), -1) -> 2026-02-28
|
|
"""
|
|
month = date.month + n
|
|
year = date.year
|
|
|
|
while month > 12:
|
|
month -= 12
|
|
year += 1
|
|
|
|
while month < 1:
|
|
month += 12
|
|
year -= 1
|
|
|
|
# Handle month-end: if original day doesn't exist in target month,
|
|
# use last day of that month
|
|
day = min(date.day, _days_in_month(year, month))
|
|
|
|
return datetime.date(year, month, day)
|
|
|
|
|
|
def _days_in_month(year: int, month: int) -> int:
|
|
"""Return number of days in the given month."""
|
|
if month == 2:
|
|
return 29 if _is_leap_year(year) else 28
|
|
if month in (4, 6, 9, 11):
|
|
return 30
|
|
return 31
|
|
|
|
|
|
def _is_leap_year(year: int) -> bool:
|
|
"""Check if a year is a leap year."""
|
|
return year % 4 == 0 and (year % 100 != 0 or year % 400 == 0)
|
|
|
|
|
|
def age_in_months(birth_date: datetime.date, on: datetime.date) -> int:
|
|
"""Calculate exact age in months between two dates.
|
|
|
|
If on < birth_date, returns negative value (person not yet born).
|
|
"""
|
|
months = (on.year - birth_date.year) * 12 + (on.month - birth_date.month)
|
|
|
|
# Adjust if birthday hasn't occurred this month yet
|
|
if on.day < birth_date.day:
|
|
months -= 1
|
|
|
|
return months
|
|
|
|
|
|
def fourth_birthday(birth_date: datetime.date) -> datetime.date:
|
|
"""Return the date when a child turns 4 years old."""
|
|
return add_months(birth_date, 48)
|