Functional Weave
Code in Python

dates.uk-tax-year@1.0.0

impl/python.py

2,016 bytes · the Python implementation · view raw

Imports name this capability’s declared dependencies, which fune builds next to it in your project; each one links to its page.

from .dates_add_days import format_iso_date, parse_iso_date, CivilDate  ← from dates.add-days ^1.0.0 · built alongside by fune
from .dates_uk_tax_year_types import UkTaxYear


# The UK personal tax year begins on 6 April, a leftover of the 1752 calendar
# reform that nobody has been willing to move since.
TAX_YEAR_START_MONTH = 4
TAX_YEAR_START_DAY = 6


def uk_tax_year(iso: str) -> UkTaxYear:
    """The UK personal tax year containing a date: 6 April to 5 April inclusive.

    The boundary is where this gets implemented wrong. 5 April 2026 belongs to
    2025/26 and 6 April 2026 belongs to 2026/27, so a comparison written as
    ``month >= 4`` puts the whole of 1-5 April in the wrong year, and one
    written against 1 April - the company financial year end, a different thing
    entirely - moves the boundary by five days for everybody.
    """
    date = parse_iso_date(iso)

    # Compare (month, day) against (4, 6) as a pair. On or after 6 April the
    # date is in the tax year that started this calendar year; before it, the
    # date still belongs to the one that started last April.
    on_or_after_start = (date.month, date.day) >= (TAX_YEAR_START_MONTH, TAX_YEAR_START_DAY)
    start_year = date.year if on_or_after_start else date.year - 1

    if start_year < 1 or start_year + 1 > 9999:
        raise ValueError('"%s" is outside the supported range 0001-01-01 to 9999-12-31' % (iso,))

    return UkTaxYear(
        start=format_iso_date(CivilDate(start_year, TAX_YEAR_START_MONTH, TAX_YEAR_START_DAY)),
        end=format_iso_date(CivilDate(start_year + 1, TAX_YEAR_START_MONTH, TAX_YEAR_START_DAY - 1)),
        label=_tax_year_label(start_year),
    )


def uk_tax_year_label(iso: str) -> str:
    """Just the label, for report headings: "2026/27"."""
    return uk_tax_year(iso).label


def _tax_year_label(start_year: int) -> str:
    # The second half is two digits of the end year, zero-padded, so 1999/00 and
    # 2000/01 come out right rather than as 1999/0 or 1999/2000.
    return "%04d/%02d" % (start_year, (start_year + 1) % 100)