Functional Weave
Code in Python

dates.age

Age in whole years on a given date, with 29 February birthdays ageing on 1 March in other years.

1.0.0 · published 2026-10-03 by charlie · Anterra

Pinned by 18 tests, run in TypeScript, Python and Rust.

What it does

Completed years on a date: someone born on 15 May 1990 is 35 on 14 May 2026 and 36 on 15 May 2026. The date to measure on is an argument, never the clock, so "was this customer 18 when they signed?" and "how old is this employee today?" are the same call with a different date.

A person attains an age at the start of the anniversary of their birth. For someone born on 29 February, the anniversary in a year with no 29 February is taken to be 1 March, so they turn 18 on 1 March, not 28 February. That is the UK convention:

For example

  • age(1990-05-15, 2026-05-14) → 35 the day before a birthday
  • age(1990-05-15, 2026-05-15) → 36 on the birthday itself the new age applies
  • age(1990-05-15, 2026-05-16) → 36 the day after a birthday

The function

The same function in TypeScript, Python and Rust, pinned by the same tests. Pick your language; the choice follows you around the registry.

def age(birth_date: str, on_date: str) -> int
birth_datedatedate of birth, ISO
on_datedatethe date to measure on; never today implicitly
returnsintcompleted years on onDate

Your code names it in one line, in the file that uses it

from fune.dates.age import age  # dates.age@^1
impl/python.py · 22 lines · open · 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 is_leap_year, parse_iso_date  ← from dates.add-days ^1.0.0 · built alongside by fune


def age(birth_date: str, on_date: str) -> int:
    """Completed years on ``on_date``.

    A person attains an age at the start of their birthday; for a 29 February
    birthday in a year without one, the birthday is taken to be 1 March (Age of
    Legal Capacity (Scotland) Act 1991 s.6(2), and the UK reading of Family Law
    Reform Act 1969 s.9), not 28 February.
    """
    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))
    birthday_month, birthday_day = birth.month, birth.day
    if birth.month == 2 and birth.day == 29 and not is_leap_year(on.year):
        birthday_month, birthday_day = 3, 1
    years = on.year - birth.year
    if (on.month, on.day) < (birthday_month, birthday_day):
        years -= 1
    return years

Install

fune build

With that line in your source, in a Python project (language python in fune.project), fune build resolves it and its 1 dependency, pins them in fune.lock, downloads only the Python package of each, and builds the code above into your project’s .fune/build, one readable file per capability with a header linking back here. Or pin a range in fune.project and build in one step:

fune add dates.age
Download for Python dates.age-1.0.0-python.fune · 6,708 bytes sha256 8f42f6c75f05f0a587ecbd0e23e525c9b3b228286f931cad2d8909ac4982dc48

The manifest, vectors and README with only the Python implementation. Install it without the registry with fune add ./dates.age-1.0.0-python.fune, or fetch it from a terminal with fune pull dates.age@1.0.0:python.

The whole function, every language, is one file too: dates.age-1.0.0.fune, 8,959 bytes, sha256 55db52a3f8f0f2ddc2ef538a47e196d364d089b870e596f48b02fe9348c6e070. It installs into a project of any language.

Customise it in your app

The seams this capability offers. Put a marker directly above a function of your own and fune build wires it into the built code; the package on the registry is not changed, the built file’s header lists it under CUSTOMISED, and fune hooks lists every hook in the project. How hooks work.

before — your function gets the arguments and returns them, changed or not, or throws to refuse the call.

# fune: before dates.age

after — your function gets the result and the arguments, and returns the final result.

# fune: after dates.age

replace — inside this capability’s code only, calls to a dependency go to your function, with the same signature. Other capabilities that use it are unaffected; write in * to replace it everywhere.

# fune: replace dates.add-days in dates.age

step — your function runs at a numbered point inside the function’s body, receives the in-scope values it names as parameters, and may return replacements. List the points with fune show dates.age --steps.

# fune: step dates.age after <n|label>

Tests

A version published now needs at least 8 tests for every function, and one that expects the error for each function that throws; the registry refuses it otherwise. fune verify --all runs each case in TypeScript, Python and Rust, and a project runs them again with fune verify. This page lists the cases; it does not run them. The exact JSON is vectors.json.

CaseArgumentsExpected
the day before a birthday 1990-05-15, 2026-05-14 → 35
on the birthday itself the new age applies 1990-05-15, 2026-05-15 → 36
the day after a birthday 1990-05-15, 2026-05-16 → 36
on the day of birth the age is zero 2026-09-22, 2026-09-22 → 0
a day short of the first birthday is still zero 2025-09-22, 2026-09-21 → 0
born 31 December, measured on 1 January: one day later but a year number higher 2007-12-31, 2008-01-01 → 0
born 29 February 2008 is still 17 on 28 February 2026: a naive clamp says 18 2008-02-29, 2026-02-28 → 17
born 29 February 2008 turns 18 on 1 March 2026 2008-02-29, 2026-03-01 → 18
born 29 February, in a leap year the birthday is 29 February itself 2000-02-29, 2024-02-29 → 24
born 29 February, the day before a leap-year birthday 2000-02-29, 2024-02-28 → 23
Show the other 8 tests
CaseArgumentsExpected
born 29 February, 2100 is not a leap year so the birthday is 1 March 1996-02-29, 2100-02-28 → 103
born 29 February, 1 March 2100 1996-02-29, 2100-03-01 → 104
born 28 February is unaffected by leap years 2008-02-28, 2026-02-28 → 18
born 1 March, measured on 29 February of a leap year 2000-03-01, 2024-02-29 → 23
a pension record from before 1970 1901-07-04, 1968-07-04 → 67
a measuring date before the birth is an error, not a negative age 2026-09-22, 2026-09-21 → error: is before birthDate
an impossible birth date is an error 2026-02-29, 2027-01-01 → error: is not a real calendar date
a UK-style date is an error 15/05/1990, 2026-05-15 → error: is not an ISO date

More from the author

- Scotland states it outright. Age of Legal Capacity (Scotland) Act 1991, section 6: "(1) The time at which a person attains a particular age expressed in years shall be taken to be the beginning of the relevant anniversary of the date of his birth. (2) Where a person has been born on 29th February in a leap year, the relevant anniversary in any year other than a leap year shall be taken to be 1st March." https://www.legislation.gov.uk/ukpga/1991/50/section/6 - England, Wales and Northern Ireland: Family Law Reform Act 1969, section 9(1), "The time at which a person attains a particular age expressed in years shall be the commencement of the relevant anniversary of the date of his birth." https://www.legislation.gov.uk/ukpga/1969/46/section/9 . The Act does not name the anniversary of a 29 February birth; 1 March is the reading applied in practice and the one the Scottish Act writes down.

Some other jurisdictions and systems use 28 February instead. The difference is one day every three years in four, and it is exactly the day an age-gated sale or a pension start date is checked on, so this capability picks the UK rule and says so rather than leaving it to whatever a naive clamp does.

A measuring date before the birth date is an error rather than a negative or zero age: it is always a data or argument-order mistake.

Files

PathBytes
README.md1,902
impl/python.py927
impl/rust.rs1,192
impl/typescript.ts970
vectors.json2,190