Functional Weave
Code in Rust

health.gestational-age Unreviewed

Gestational age (weeks+days) and estimated due date from the last menstrual period by Naegele's rule, cycle-adjusted.

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

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

Unreviewed. Not for clinical use; regulatory review pending. 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 midwife or obstetrician 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 midwife or obstetrician 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

Status: needs review and sign-off by a qualified clinician before it is published. Not a medical device; for decision support only; always follow local clinical guidelines.

Gestational age on a date, as completed weeks plus days (12+3), and the estimated due date (EDD), from the first day of the last menstrual period (LMP). This is the LMP estimate only: NICE recommends dating a pregnancy by ultrasound between 11+2 and 14+1 weeks, and once a dating scan has been done its dates supersede these.

For example

  • gestational_age(2026-03-15, 2026-06-10, 28) → weeks 12, days 3, total days 87, due date 2026-12-20 NHS due date calculator: LMP 15 March 2026, 28-day cycle, due 20 December 2026; 12+3 on 10 June
  • gestational_age(2026-03-15, 2026-06-10, 35) → weeks 11, days 3, total days 80, due date 2026-12-27 NHS calculator: a 35-day cycle moves the due date a week later, to 27 December 2026
  • gestational_age(2026-03-15, 2026-06-10, 21) → weeks 13, days 3, total days 94, due date 2026-12-13 NHS calculator: a 21-day cycle moves it a week earlier, to 13 December 2026

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 gestational_age(lmp_date: &str, on_date: &str, cycle_length_days: i64) -> GestationalAge
lmp_datedatefirst day of the last menstrual period, ISO
on_datedatethe date to measure on; never today implicitly
cycle_length_daysintusual cycle length, 20 to 44; 28 for the unadjusted rule
returnsGestationalAge

The type it declares, generated into your project

/// Gestational age as weeks+days, and the estimated due date.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct GestationalAge {
    /// completed weeks
    pub weeks: i64,
    /// days beyond the completed weeks, 0 to 6
    pub days: i64,
    /// gestational age in days
    pub total_days: i64,
    /// estimated due date, 40+0 weeks
    pub due_date: String,
}

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

fune!(health.gestational-age@^1);  // then call gestational_age(…)
impl/rust.rs · 62 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

/// Naegele's rule: 280 days (40+0 weeks) from the LMP for a 28-day cycle.
const TERM_DAYS: i64 = 280;
/// 44+0 weeks: beyond this the dates are almost certainly wrong.
const MAX_DAYS: i64 = 308;

/// Gestational age and estimated due date from the last menstrual period. A
/// cycle longer or shorter than 28 days moves ovulation, so both are shifted by
/// the difference, as the NHS due date calculator does.
///
/// # Panics
/// Panics on a cycle length outside 20..=44, a malformed date, or a
/// gestational age below zero or above 44+0 weeks.
pub fn gestational_age(lmp_date: &str, on_date: &str, cycle_length_days: i64) -> GestationalAge {
    if !(20..=44).contains(&cycle_length_days) {
        panic!("cycleLengthDays must be a whole number from 20 to 44, received {}", cycle_length_days);
    }
    parse_iso_date(lmp_date);
    parse_iso_date(on_date);
    let shift = cycle_length_days - 28;
    let total_days = days_between(lmp_date, on_date) - shift;
    if total_days < 0 {
        panic!("onDate {} is before the adjusted start of pregnancy", on_date);
    }
    if total_days > MAX_DAYS {
        panic!("gestational age of {} days is more than 44+0 weeks: check the dates", total_days);
    }
    GestationalAge {
        weeks: total_days / 7,
        days: total_days % 7,
        total_days,
        due_date: add_days(lmp_date, TERM_DAYS + shift),
    }
}

pub fn gestational_age_to_value(age: &GestationalAge) -> Value {
    Value::obj(vec![
        ("weeks", Value::Int(age.weeks)),
        ("days", Value::Int(age.days)),
        ("totalDays", Value::Int(age.total_days)),
        ("dueDate", Value::str(&age.due_date)),
    ])
}

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 {
    // Refuse what the typed signature cannot hold, with the wording TypeScript
    // and Python use, rather than let the conversion below quietly change it.
    let cycle = match args[2] {
        Value::Int(i) => i,
        _ => panic!("cycleLengthDays must be a whole number from 20 to 44, received {:?}", args[2]),
    };
    gestational_age_to_value(&gestational_age(date_arg(&args[0]), date_arg(&args[1]), cycle))
}

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 health.gestational-age
Download for Rust health.gestational-age-1.0.1-rust.fune · 13,467 bytes sha256 0ed34df81f1328ccf17fe8f978dfa6199f416663b0b78795f754a84091b8f00d

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

The whole function, every language, is one file too: health.gestational-age-1.0.1.fune, 16,397 bytes, sha256 3a8338b49fe1e3953ced98e28cc50e831e4fb21b826a63e9a294e9106686c903. 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.gestational-age

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

// fune: after health.gestational-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 health.gestational-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 health.gestational-age --steps.

// fune: step health.gestational-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
NHS due date calculator: LMP 15 March 2026, 28-day cycle, due 20 December 2026; 12+3 on 10 June 2026-03-15, 2026-06-10, 28 → weeks 12, days 3, total days 87, due date 2026-12-20
NHS calculator: a 35-day cycle moves the due date a week later, to 27 December 2026 2026-03-15, 2026-06-10, 35 → weeks 11, days 3, total days 80, due date 2026-12-27
NHS calculator: a 21-day cycle moves it a week earlier, to 13 December 2026 2026-03-15, 2026-06-10, 21 → weeks 13, days 3, total days 94, due date 2026-12-13
NHS calculator: the shortest cycle it accepts, 20 days, due 12 December 2026 2026-03-15, 2026-06-10, 20 → weeks 13, days 4, total days 95, due date 2026-12-12
NHS calculator: a 22-day cycle, due 14 December 2026 2026-03-15, 2026-06-10, 22 → weeks 13, days 2, total days 93, due date 2026-12-14
NHS calculator: the longest cycle it accepts, 44 days, due 5 January 2027 2026-03-15, 2026-06-10, 44 → weeks 10, days 1, total days 71, due date 2027-01-05
NHS calculator: LMP 31 May 2026, due 7 March 2027 2026-05-31, 2026-09-23, 28 → weeks 16, days 3, total days 115, due date 2027-03-07
NHS calculator: LMP 1 January 2026, due 8 October 2026 2026-01-01, 2026-09-23, 28 → weeks 37, days 6, total days 265, due date 2026-10-08
NHS calculator: LMP 10 June 2026, due 17 March 2027, on the due date itself 40+0 2026-06-10, 2027-03-17, 28 → weeks 40, days 0, total days 280, due date 2027-03-17
NHS calculator: LMP 1 September 2026 with a 30-day cycle, due 10 June 2027 2026-09-01, 2026-12-01, 30 → weeks 12, days 5, total days 89, due date 2027-06-10
Show the other 10 tests
CaseArgumentsExpected
on the LMP itself with a 28-day cycle the gestational age is 0+0 2026-03-15, 2026-03-15, 28 → weeks 0, days 0, total days 0, due date 2026-12-20
a pregnancy spanning 29 February 2028 counts the leap day 2027-06-01, 2028-03-01, 28 → weeks 39, days 1, total days 274, due date 2028-03-07
44+0 weeks is still accepted 2026-01-01, 2026-11-05, 28 → weeks 44, days 0, total days 308, due date 2026-10-08
44+1 weeks is an error: check the dates 2026-01-01, 2026-11-06, 28 → error: more than 44+0 weeks
a 35-day cycle puts the adjusted start 7 days after the LMP, so 3 days after the LMP is an error 2026-03-15, 2026-03-18, 35 → error: is before the adjusted start of pregnancy
onDate before the LMP is an error 2026-03-15, 2026-03-14, 28 → error: is before the adjusted start of pregnancy
a 19-day cycle is outside the NHS calculator's 20 to 44 days 2026-03-15, 2026-06-10, 19 → error: cycleLengthDays must be a whole number from 20 to 44
a 45-day cycle is outside 20 to 44 days 2026-03-15, 2026-06-10, 45 → error: cycleLengthDays must be a whole number from 20 to 44
a fractional cycle length is an error 2026-03-15, 2026-06-10, 28.5 → error: cycleLengthDays must be a whole number from 20 to 44
an impossible LMP date is an error 2026-02-30, 2026-06-10, 28 → error: is not a real calendar date

More from the author

**The rule.** Naegele's rule puts the due date 280 days (40+0 weeks) after the LMP, which assumes a 28-day cycle with ovulation on day 14. A longer or shorter cycle moves ovulation, so both the due date and the gestational age are shifted by the difference from 28 days:

dueDate = LMP + 280 + (cycleLengthDays - 28) totalDays = (onDate - LMP) - (cycleLengthDays - 28) weeks = totalDays div 7, days = totalDays mod 7

With a 35-day cycle the due date is a week later and the pregnancy is a week less advanced on any date. Pass 28 for the unadjusted rule. This is exactly what the NHS due date calculator does, and its published answers are among the vectors (LMP 15 March 2026: due 20 December 2026 on a 28-day cycle, 27 December on 35 days, 12 December on 20 days, 5 January 2027 on 44 days).

**Limits, and why they are errors.**

- `cycleLengthDays` must be a whole number from 20 to 44, the range the NHS calculator accepts. - A measuring date before the adjusted start of pregnancy (the LMP plus the cycle shift) is an error, not a negative age: with a 35-day cycle, three days after the LMP is not yet 0+0. - A gestational age above 44+0 weeks (308 days) is an error asking for the dates to be checked. Pregnancy normally lasts 37 to 42 weeks from the LMP; 44 weeks is a guard rail against a wrong year or a transposed date, not a clinical threshold, and a clinician should confirm it.

Dates are ISO `YYYY-MM-DD`; the measuring date is always an argument, never today's date. Leap days are counted like any other day.

## Sources

- NHS, "Due date calculator" (nhs.uk): takes the first day of the last period and a cycle length that "must be between 20 and 44 days"; pregnancy "normally lasts from 37 weeks to 42 weeks from the first day of your last period". Its outputs for the dates above were checked on 23 September 2026. https://www.nhs.uk/pregnancy/finding-out/due-date-calculator/ - NICE guideline NG201, "Antenatal care" (2021), recommendation 1.2.14: offer an ultrasound scan between 11+2 and 14+1 weeks to determine gestational age. https://www.nice.org.uk/guidance/ng201/chapter/Recommendations

## 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 midwife or obstetrician 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.

**Not for clinical use; regulatory review pending.** Whether publishing this capability makes it a medical device is under regulatory review. Until that is settled it is a developer library only.

**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 midwife or obstetrician 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 (not for clinical use; regulatory review pending). The code and the tests are unchanged.

Files

PathBytes
README.md4,301
impl/python.py1,392
impl/rust.rs2,395
impl/typescript.ts1,422
vectors.json3,798