from .dates_add_days import add_days, days_between, parse_iso_date from .dates_add_months import add_months from .health_age_in_months_types import AgeInMonths def _month_anniversary(birth_date: str, birth_day: int, months: int) -> str: """The day the given number of months is completed. add_months clamps 31 January + 1 month to 28 February; a clamped day has not yet reached the birth day, so the month completes the day after it (1 March), the same rule dates.age uses for a 29 February birthday.""" shifted = add_months(birth_date, months) return add_days(shifted, 1) if parse_iso_date(shifted).day < birth_day else shifted def age_in_months(birth_date: str, on_date: str) -> AgeInMonths: """Completed months and remaining days from ``birth_date`` to ``on_date``.""" birth = parse_iso_date(birth_date) on = parse_iso_date(on_date) if on_date < birth_date: raise ValueError("onDate %s is before birthDate %s" % (on_date, birth_date)) months = (on.year - birth.year) * 12 + (on.month - birth.month) anniversary = _month_anniversary(birth_date, birth.day, months) if anniversary > on_date: months -= 1 anniversary = _month_anniversary(birth_date, birth.day, months) return AgeInMonths(months=months, days=days_between(anniversary, on_date), total_days=days_between(birth_date, on_date))