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 birthdayage(1990-05-15, 2026-05-15)→ 36 on the birthday itself the new age appliesage(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.
pub fn age(birth_date: &str, on_date: &str) -> i64
| birth_date | date | date of birth, ISO |
| on_date | date | the date to measure on; never today implicitly |
| returns | int | completed years on onDate |
Your code names it in one line, in the file that uses it
fune!(dates.age@^1); // then call age(…)
Imports name this capability’s declared dependencies, which fune builds next to it in your project; each one links to its page.
use super::funejson::Value; ← the fune runtime: the JSON value the test vectors use; fune build keeps it only where a signature takes one
use super::dates_add_days::{is_leap_year, parse_iso_date}; ← from dates.add-days ^1.0.0 · built alongside by fune
/// 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.
///
/// # Panics
/// Panics on a malformed or impossible date, or `on_date` before `birth_date`.
pub fn age(birth_date: &str, on_date: &str) -> i64 {
let birth = parse_iso_date(birth_date);
let on = parse_iso_date(on_date);
if on_date < birth_date {
panic!("onDate {} is before birthDate {}", on_date, birth_date);
}
let (mut birthday_month, mut birthday_day) = (birth.month, birth.day);
if birth.month == 2 && birth.day == 29 && !is_leap_year(on.year) {
birthday_month = 3;
birthday_day = 1;
}
let mut years = on.year - birth.year;
if (on.month, on.day) < (birthday_month, birthday_day) {
years -= 1;
}
years
}
pub fn fune_vector(args: &[Value]) -> Value {
Value::Int(age(args[0].as_str(), args[1].as_str()))
}Install
fune build
With that line in your source, in a Rust project (language rust in fune.project), fune build resolves it and its 1 dependency, pins them in fune.lock, downloads only the Rust 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. A crate’s build.rs runs it before every compile. Or pin a range in fune.project and build in one step:
fune add dates.age
The manifest, vectors and README with only the Rust implementation. Install it without the registry with fune add ./dates.age-1.0.0-rust.fune, or fetch it from a terminal with fune pull dates.age@1.0.0:rust.
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.
| Case | Arguments | Expected | |
|---|---|---|---|
| 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
| Case | Arguments | Expected | |
|---|---|---|---|
| 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
| Path | Bytes |
|---|---|
| README.md | 1,902 |
| impl/python.py | 927 |
| impl/rust.rs | 1,192 |
| impl/typescript.ts | 970 |
| vectors.json | 2,190 |