health.age-in-months
Age in completed months and days on a given date, for paediatric charts and doses; month ends handled like birthdays.
1.0.0 (not the latest) · published 2026-10-03 by charlie · Anterra
Pinned by 19 tests, run in TypeScript, Python and Rust.
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 zeroage_in_months(2026-03-10, 2026-03-11)→ months 0, days 1, total days 1 one day oldage_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.
def age_in_months(birth_date: str, on_date: str) -> AgeInMonths
| birth_date | date | date of birth, ISO |
| on_date | date | the date to measure on; never today implicitly |
| returns | AgeInMonths |
The type it declares, generated into your project
@dataclass(frozen=True)
class AgeInMonths:
"""A child's age as completed months plus days, and the days since birth."""
#: completed calendar months since birth
months: int
#: days since the last completed month, 0 or more
days: int
#: days since birth; 0 on the day of birth
total_days: int
Your code names it in one line, in the file that uses it
from fune.health.age_in_months import age_in_months # health.age-in-months@^1
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 add_days, days_between, parse_iso_date ← from dates.add-days ^1.0.0 · built alongside by fune
from .dates_add_months import add_months ← from dates.add-months ^1.0.0 · built alongside by fune
from .health_age_in_months_types import AgeInMonths
def _month_anniversary(birth_date: str, birth_day: int, months: int) -> str:
"""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."""
shifted = add_months(birth_date, months)
return add_days(shifted, 1) if parse_iso_date(shifted).day < birth_day else shifted
def age_in_months(birth_date: str, on_date: str) -> AgeInMonths:
"""Completed months and remaining days from ``birth_date`` to ``on_date``."""
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))
months = (on.year - birth.year) * 12 + (on.month - birth.month)
anniversary = _month_anniversary(birth_date, birth.day, months)
if anniversary > on_date:
months -= 1
anniversary = _month_anniversary(birth_date, birth.day, months)
return AgeInMonths(months=months, days=days_between(anniversary, on_date), total_days=days_between(birth_date, on_date))Install
fune build
With that line in your source, in a Python project (language python in fune.project), fune build resolves it and its 2 dependencies, 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 health.age-in-months
The manifest, vectors and README with only the Python implementation. Install it without the registry with fune add ./health.age-in-months-1.0.0-python.fune, or fetch it from a terminal with fune pull health.age-in-months@1.0.0:python.
The whole function, every language, is one file too: health.age-in-months-1.0.0.fune, 11,926 bytes, sha256 63bf286b1e35ae77f10d05f89522d6523c430157b518aa3e793912bf46e3c03f. 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.
| Case | Arguments | Expected | |
|---|---|---|---|
| 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
| Case | Arguments | Expected | |
|---|---|---|---|
| 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.
Files
| Path | Bytes |
|---|---|
| README.md | 1,563 |
| impl/python.py | 1,373 |
| impl/rust.rs | 2,062 |
| impl/typescript.ts | 1,389 |
| vectors.json | 2,914 |