Functional Weave
Code in Rust

dates.uk-tax-year@1.0.0

impl/typescript.ts

2,169 bytes · the TypeScript 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.

import { formatIsoDate, parseIsoDate } from "./dates_add_days.ts";  ← from dates.add-days ^1.0.0 · built alongside by fune
import { type UkTaxYear } from "./dates_uk_tax_year_types.ts";

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

/**
 * 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.
 */
export function ukTaxYear(iso: string): UkTaxYear {
  const date = parseIsoDate(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.
  const onOrAfterStart =
    date.month > TAX_YEAR_START_MONTH ||
    (date.month === TAX_YEAR_START_MONTH && date.day >= TAX_YEAR_START_DAY);
  const startYear = onOrAfterStart ? date.year : date.year - 1;

  if (startYear < 1 || startYear + 1 > 9999) {
    throw new RangeError(`"${iso}" is outside the supported range 0001-01-01 to 9999-12-31`);
  }

  return {
    start: formatIsoDate({ year: startYear, month: TAX_YEAR_START_MONTH, day: TAX_YEAR_START_DAY }),
    end: formatIsoDate({ year: startYear + 1, month: TAX_YEAR_START_MONTH, day: TAX_YEAR_START_DAY - 1 }),
    label: taxYearLabel(startYear),
  };
}

/** Just the label, for report headings: "2026/27". */
export function ukTaxYearLabel(iso: string): string {
  return ukTaxYear(iso).label;
}

function taxYearLabel(startYear: number): string {
  // 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.
  const endShort = String((startYear + 1) % 100).padStart(2, "0");
  return `${String(startYear).padStart(4, "0")}/${endShort}`;
}