Functional Weave
Code in Rust

health.age-in-months Unreviewed

Age in completed months and days on a given date, for paediatric charts and doses; month ends handled like birthdays.

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

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

Unreviewed. This capability’s implementations agree in every language and pass its published test vectors, which were worked out from the official sources cited. But no qualified clinician has yet checked those vectors, or confirmed that the capability covers the cases it claims. Treat it as a draft. Do not use it for real people, money or decisions without your own expert review. Once a qualified reviewer signs off, this notice is replaced with their name, qualification and the date. Each new version needs fresh sign-off.

Not professional advice. This capability calculates health figures from published rules. It is a software component for developers, not medical advice. Rules change and every rate here has an effective date. Check that the dates cover your case. Verify results against the official sources listed in its README, and have a clinician review how you use it, before anyone relies on the output. Provided “as is” under its licence, without warranty.

Not a medical device. It is not intended to diagnose, treat or support clinical decisions about any individual. Anyone building it into clinical software is responsible for that software’s regulatory status, and must validate it under their own clinical governance.

What it does

A child's age as completed months plus days on a given date: born 10 March 2026, on 25 May 2026 the child is 2 months 15 days. Paediatric growth charts, immunisation schedules and many dosing tables are read in months, and the date to measure on is an argument, never the clock. `totalDays` is the age in days as well, for neonates.

**When a month is completed.** A month is completed on the same day of the month as the birth. When that day does not exist in the month (born 31 January; born 29 February in a common year), the month is completed on the first day of the following month, not on the last day of the short month. Born 31 January 2026, the child is 0 months 28 days on 28 February and 1 month 0 days on 1 March. This is the same rule `dates.age` applies to a 29 February birthday (the anniversary is 1 March), so a child born 29 February 2024 is 11 months 30 days on 28 February 2025 and 12 months, and one year old, on 1 March 2025. A naive "add a month and clamp" says the child born on 31 January is a month old on 28 February, three days early.

For example

  • age_in_months(2026-03-10, 2026-03-10) → months 0, days 0, total days 0 on the day of birth everything is zero
  • age_in_months(2026-03-10, 2026-03-11) → months 0, days 1, total days 1 one day old
  • age_in_months(2026-03-10, 2026-04-09) → months 0, days 30, total days 30 the day before the first monthly anniversary

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_in_months(birth_date: &str, on_date: &str) -> AgeInMonths
birth_datedatedate of birth, ISO
on_datedatethe date to measure on; never today implicitly
returnsAgeInMonths

The type it declares, generated into your project

/// A child's age as completed months plus days, and the days since birth.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct AgeInMonths {
    /// completed calendar months since birth
    pub months: i64,
    /// days since the last completed month, 0 or more
    pub days: i64,
    /// days since birth; 0 on the day of birth
    pub total_days: i64,
}

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

fune!(health.age-in-months@^1);  // then call age_in_months(…)
impl/rust.rs · 58 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.

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::{add_days, days_between, parse_iso_date};  ← from dates.add-days ^1.0.0 · built alongside by fune
use super::dates_add_months::add_months;  ← from dates.add-months ^1.0.0 · built alongside by fune

/// 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.
fn month_anniversary(birth_date: &str, birth_day: i64, months: i64) -> String {
    let shifted = add_months(birth_date, months);
    if parse_iso_date(&shifted).day < birth_day {
        add_days(&shifted, 1)
    } else {
        shifted
    }
}

/// Completed months and remaining days from `birth_date` to `on_date`.
///
/// # Panics
/// Panics on a malformed or impossible date, or `on_date` before `birth_date`.
pub fn age_in_months(birth_date: &str, on_date: &str) -> AgeInMonths {
    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 months = (on.year - birth.year) * 12 + (on.month - birth.month);
    let mut anniversary = month_anniversary(birth_date, birth.day, months);
    if anniversary.as_str() > on_date {
        months -= 1;
        anniversary = month_anniversary(birth_date, birth.day, months);
    }
    AgeInMonths {
        months,
        days: days_between(&anniversary, on_date),
        total_days: days_between(birth_date, on_date),
    }
}

pub fn age_in_months_to_value(age: &AgeInMonths) -> Value {
    Value::obj(vec![
        ("months", Value::Int(age.months)),
        ("days", Value::Int(age.days)),
        ("totalDays", Value::Int(age.total_days)),
    ])
}

fn date_arg(value: &Value) -> &str {
    match value {
        Value::Str(s) => s.as_str(),
        _ => panic!("{:?} is not an ISO date (YYYY-MM-DD)", value),
    }
}

pub fn fune_vector(args: &[Value]) -> Value {
    age_in_months_to_value(&age_in_months(date_arg(&args[0]), date_arg(&args[1])))
}

Install

fune build

With that line in your source, in a Rust project (language rust in fune.project), fune build resolves it and its 2 dependencies, 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 health.age-in-months
Download for Rust health.age-in-months-1.0.1-rust.fune · 10,436 bytes sha256 ebd0bdf9fd982ba8f34b988b709db396fc1ac4f9839c60bf86c38331bc9037e1

The manifest, vectors and README with only the Rust implementation. Install it without the registry with fune add ./health.age-in-months-1.0.1-rust.fune, or fetch it from a terminal with fune pull health.age-in-months@1.0.1:rust.

The whole function, every language, is one file too: health.age-in-months-1.0.1.fune, 13,308 bytes, sha256 6ba2f856fa499229596115645b913970f63d353863d146ba2c24df0dcfff919e. 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 health.age-in-months

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

// fune: after health.age-in-months

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 health.age-in-months
// fune: replace dates.add-months in health.age-in-months

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 health.age-in-months --steps.

// fune: step health.age-in-months 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
on the day of birth everything is zero 2026-03-10, 2026-03-10 → months 0, days 0, total days 0
one day old 2026-03-10, 2026-03-11 → months 0, days 1, total days 1
the day before the first monthly anniversary 2026-03-10, 2026-04-09 → months 0, days 30, total days 30
on the first monthly anniversary 2026-03-10, 2026-04-10 → months 1, days 0, total days 31
2 months 15 days 2026-03-10, 2026-05-25 → months 2, days 15, total days 76
born 31 January is not a month old on 28 February: a naive clamp says it is 2026-01-31, 2026-02-28 → months 0, days 28, total days 28
born 31 January completes the first month on 1 March 2026-01-31, 2026-03-01 → months 1, days 0, total days 29
born 31 January, 30 March is 1 month 29 days 2026-01-31, 2026-03-30 → months 1, days 29, total days 58
born 31 January, 31 March is 2 months: the 31st does not decay after February 2026-01-31, 2026-03-31 → months 2, days 0, total days 59
born 31 January 2024, 29 February 2024 is still short of the 31st 2024-01-31, 2024-02-29 → months 0, days 29, total days 29
Show the other 9 tests
CaseArgumentsExpected
born 31 January 2024, 1 March 2024 completes the month 2024-01-31, 2024-03-01 → months 1, days 0, total days 30
born 29 February 2024 is 11 months 30 days on 28 February 2025 2024-02-29, 2025-02-28 → months 11, days 30, total days 365
born 29 February 2024 is 12 months on 1 March 2025, as dates.age turns 1 2024-02-29, 2025-03-01 → months 12, days 0, total days 366
born 30 April, 30 May is one month 2026-04-30, 2026-05-30 → months 1, days 0, total days 30
across the year end 2025-11-20, 2026-01-05 → months 1, days 16, total days 46
a two-year-old counted in months 2024-06-15, 2026-09-23 → months 27, days 8, total days 830
a measuring date before the birth is an error 2026-03-10, 2026-03-09 → error: is before birthDate
an impossible birth date is an error 2026-02-30, 2026-04-01 → error: is not a real calendar date
a UK-style date is an error 10/03/2026, 2026-04-01 → error: is not an ISO date

More from the author

Months are always counted from the date of birth, not stepped a month at a time, so a 31st-of-the-month birthday does not decay to the 28th after February: born 31 January, 2 months are completed on 31 March.

A measuring date before the birth date is an error, not a negative age. Dates are ISO `YYYY-MM-DD`; an impossible date (2026-02-30) or a UK-style `15/05/2026` is an error.

Built on `dates.add-months` and `dates.add-days`; no clinical thresholds are applied here.

## Before you rely on this

**Not professional advice.** This capability calculates health figures from published rules. It is a software component for developers, not medical advice. Rules change and every rate here has an effective date. Check that the dates cover your case. Verify results against the official sources listed above, and have a clinician review how you use it, before anyone relies on the output. Provided "as is" under its licence, without warranty.

**Not a medical device.** It is not intended to diagnose, treat or support clinical decisions about any individual. Anyone building it into clinical software is responsible for that software's regulatory status, and must validate it under their own clinical governance.

**Unreviewed.** This capability's implementations agree in every language and pass its published test vectors, which were worked out from the official sources cited. But no qualified clinician has yet checked those vectors, or confirmed that the capability covers the cases it claims. Treat it as a draft. Do not use it for real people, money or decisions without your own expert review. Once a qualified reviewer signs off, this notice is replaced with their name, qualification and the date. Each new version needs fresh sign-off.

1.0.1 marks it unreviewed. The code and the tests are unchanged.

Files

PathBytes
README.md2,905
impl/python.py1,373
impl/rust.rs2,062
impl/typescript.ts1,389
vectors.json2,914