Functional Weave
Code in Rust

education.school-year-group

School year group from date of birth on a date, by UK nation: Reception to Year 13, P1 to S6, P1 to Year 14.

1.0.0 (not the latest) · published 2026-10-03 by charlie · Anterra

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

What it does

The school year group a child is in on a date, from their date of birth, in each UK nation: the normal age-for-year placement, before any deferral or decision to place a child out of their year group.

| nation | the cohort is born | starts | year groups | |---|---|---|---| | England, Wales | 1 September to 31 August | Reception, in the September after the 4th birthday | Reception, Year 1 to Year 13 | | Scotland | 1 March to the end of February | P1, in August, aged about 4½ to 5½ | P1 to P7, S1 to S6 | | Northern Ireland | 2 July to 1 July (4 on or before 1 July) | P1, in September | P1 to P7, Year 8 to Year 14 |

For example

  • school_year_group(2020-09-01, 2025-09-15, england) → school year start 2025-09-01, entry year 2,025, index 0, year group Reception, stage primary England: born 1 September 2020, the oldest in Reception in 2025/26
  • school_year_group(2021-08-31, 2025-09-15, england) → school year start 2025-09-01, entry year 2,025, index 0, year group Reception, stage primary England: born 31 August 2021, the youngest in the same Reception class
  • school_year_group(2020-08-31, 2025-09-15, england) → school year start 2025-09-01, entry year 2,024, index 1, year group Year 1, stage primary England: born a day earlier, 31 August 2020, is a year ahead in Year 1

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 school_year_group(birth_date: &str, on_date: &str, nation: &str) -> SchoolYearGroup
birth_datedatethe child's date of birth
on_datedatethe date to place the child on; the school year containing it is used
nationUkNationwhose cut-off and year names apply
returnsSchoolYearGroup

The types it declares, generated into your project

// UkNation is a string in Rust, one of: "england", "wales", "scotland", "northern-ireland".
// Parameters take it as &str and results hold it as String.

/// Where a child of that age sits in the school system on a date.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SchoolYearGroup {
    /// first day of the school year containing onDate, for this calculation
    pub school_year_start: String,
    /// calendar year the child's cohort starts Reception or P1
    pub entry_year: i64,
    /// years since that start: 0 is Reception or P1, negative before it
    pub index: i64,
    /// Reception, Year 1 ... Year 13; P1 ... S6; P1 ... Year 14; null before or after school
    pub year_group: Option<String>,
    /// primary or secondary; null when yearGroup is
    pub stage: Option<String>,
}

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

fune!(education.school-year-group@^1);  // then call school_year_group(…)
impl/rust.rs · 52 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::parse_iso_date;  ← from dates.add-days ^1.0.0 · built alongside by fune
use super::education_school_year_group_data::{COHORT_RULES, YEAR_GROUPS};  ← this capability’s own data, compiled from data/cohort-rules.json into the same file by fune build

/// The year group a child is in on a date, placed by their birth date.
///
/// The cohort comes from the birth date itself (born on or after the cohort
/// start joins a year later), never from an age on a cut-off date, which
/// misplaces 29 February births where the cohort starts on 1 March.
///
/// # Panics
/// Panics on a malformed date, onDate before birthDate, or an unknown nation.
pub fn school_year_group(birth_date: &str, on_date: &str, nation: &str) -> SchoolYearGroup {
    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 rule = match COHORT_RULES.iter().find(|r| r.nation == nation) {
        Some(r) => r,
        None => panic!("unknown nation \"{}\": use england, wales, scotland or northern-ireland", nation),
    };
    let entry_year = birth.year + rule.entry_age + if &birth_date[5..] >= rule.cohort_start { 1 } else { 0 };
    let start_year = if &on_date[5..] >= rule.school_year_start { on.year } else { on.year - 1 };
    let index = start_year - entry_year;
    let name = YEAR_GROUPS.iter().find(|g| g.nation == nation && g.index == index);
    SchoolYearGroup {
        school_year_start: format!("{:04}-{}", start_year, rule.school_year_start),
        entry_year,
        index,
        year_group: name.map(|g| g.label.to_string()),
        stage: name.map(|g| g.stage.to_string()),
    }
}

pub fn school_year_group_to_value(g: &SchoolYearGroup) -> Value {
    let opt = |v: &Option<String>| match v {
        Some(s) => Value::str(s),
        None => Value::Null,
    };
    Value::obj(vec![
        ("schoolYearStart", Value::str(&g.school_year_start)),
        ("entryYear", Value::Int(g.entry_year)),
        ("index", Value::Int(g.index)),
        ("yearGroup", opt(&g.year_group)),
        ("stage", opt(&g.stage)),
    ])
}

pub fn fune_vector(args: &[Value]) -> Value {
    school_year_group_to_value(&school_year_group(args[0].as_str(), args[1].as_str(), args[2].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 education.school-year-group
Download for Rust education.school-year-group-1.0.0-rust.fune · 21,473 bytes sha256 b403538d23e79640d1bceee1efbda7a55cc1fc8c6d53b1975479c56eb1ba24b0

The manifest, vectors and README with only the Rust implementation. Install it without the registry with fune add ./education.school-year-group-1.0.0-rust.fune, or fetch it from a terminal with fune pull education.school-year-group@1.0.0:rust.

The whole function, every language, is one file too: education.school-year-group-1.0.0.fune, 24,778 bytes, sha256 b4d15c86fa3862b18b1a8af454e95e82fcb9669d81b4996bc4a5a1331349bb8d. 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 education.school-year-group

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

// fune: after education.school-year-group

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 education.school-year-group

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 education.school-year-group --steps.

// fune: step education.school-year-group 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
England: born 1 September 2020, the oldest in Reception in 2025/26 2020-09-01, 2025-09-15, england → school year start 2025-09-01, entry year 2,025, index 0, year group Reception, stage primary
England: born 31 August 2021, the youngest in the same Reception class 2021-08-31, 2025-09-15, england → school year start 2025-09-01, entry year 2,025, index 0, year group Reception, stage primary
England: born a day earlier, 31 August 2020, is a year ahead in Year 1 2020-08-31, 2025-09-15, england → school year start 2025-09-01, entry year 2,024, index 1, year group Year 1, stage primary
England: 31 August is still the previous school year, before Reception 2020-09-01, 2025-08-31, england → school year start 2024-09-01, entry year 2,025, index -1, year group —, stage —
England: Year 7 is secondary 2014-01-15, 2025-11-01, england → school year start 2025-09-01, entry year 2,018, index 7, year group Year 7, stage secondary
England: Year 13, the last year 2008-01-15, 2026-03-01, england → school year start 2025-09-01, entry year 2,012, index 13, year group Year 13, stage secondary
England: after Year 13 there is no year group 2007-01-15, 2026-03-01, england → school year start 2025-09-01, entry year 2,011, index 14, year group —, stage —
Wales uses the same cut-off and names 2019-02-10, 2026-01-10, wales → school year start 2025-09-01, entry year 2,023, index 2, year group Year 2, stage primary
Scotland: born 1 March 2020 starts P1 in August 2025 2020-03-01, 2025-08-20, scotland → school year start 2025-08-01, entry year 2,025, index 0, year group P1, stage primary
Scotland: born 28 February 2021 is in the same P1 2021-02-28, 2025-08-20, scotland → school year start 2025-08-01, entry year 2,025, index 0, year group P1, stage primary
Show the other 12 tests
CaseArgumentsExpected
Scotland: born 29 February 2020 belongs to the year before, P2, though aged like the 1 March children 2020-02-29, 2025-08-20, scotland → school year start 2025-08-01, entry year 2,024, index 1, year group P2, stage primary
Scotland: July is still the previous session 2020-03-01, 2025-07-31, scotland → school year start 2024-08-01, entry year 2,025, index -1, year group —, stage —
Scotland: S1 is secondary 2013-06-01, 2025-10-01, scotland → school year start 2025-08-01, entry year 2,018, index 7, year group S1, stage secondary
Scotland: S6 is the last year 2008-05-01, 2026-05-01, scotland → school year start 2025-08-01, entry year 2,013, index 12, year group S6, stage secondary
Northern Ireland: 4 on 1 July 2025 starts P1 in September 2025 2021-07-01, 2025-09-10, northern-ireland → school year start 2025-09-01, entry year 2,025, index 0, year group P1, stage primary
Northern Ireland: 4 on 2 July 2025 waits a year 2021-07-02, 2025-09-10, northern-ireland → school year start 2025-09-01, entry year 2,026, index -1, year group —, stage —
Northern Ireland: secondary years are numbered on from primary 2011-12-01, 2025-10-01, northern-ireland → school year start 2025-09-01, entry year 2,016, index 9, year group Year 10, stage secondary
Northern Ireland: Year 14, the last year 2008-03-01, 2026-03-01, northern-ireland → school year start 2025-09-01, entry year 2,012, index 13, year group Year 14, stage secondary
born today is years before school 2025-09-15, 2025-09-15, england → school year start 2025-09-01, entry year 2,030, index -5, year group —, stage —
onDate before birthDate is an error 2020-09-01, 2019-09-01, england → error: onDate 2019-09-01 is before birthDate 2020-09-01
an unknown nation is an error 2020-09-01, 2025-09-15, ireland → error: unknown nation "ireland"
an impossible birth date is an error 2021-02-29, 2025-09-15, scotland → error: "2021-02-29" is not a real calendar date

More from the author

## How it works, and the leap-day trap

The cohort is decided from the birth date itself: a child born on or after the nation's cohort start date (1 September, 1 March, 2 July) joins the class that starts the year after four years on; one born before it, the year before. The school year containing `onDate` then gives how many years along they are.

Working from the child's *age* on a cut-off date instead goes wrong on 29 February in Scotland: a child born 29 February 2020 is in the cohort born 1 March 2019 to 29 February 2020, but has the same age as the 1 March 2020 children on every late-February date in later years (a birthday of 29 February counts as 1 March in common years). A vector pins it.

## Decisions

- **School year boundaries** are 1 September (England, Wales, Northern Ireland) and 1 August (Scotland, whose schools go back mid-August; each council sets the exact day). A date between that boundary and the first day of term is placed in the new year group. - `index` is always returned; `yearGroup` is null before Reception or P1 (nursery and pre-school are not named) and after the last year. - Deferred entry (Scotland's January and February births, summer-born children in England, Northern Ireland's 1 April to 1 July births) and children educated out of their year group are the caller's to apply: shift `index` by the years deferred. - The rules are long-standing conventions rather than dated rates, so the tables carry no `effective` columns and always ship whole.

## Sources

- GOV.UK, "School admissions: school starting age" ("Most children start reception full-time in September after their fourth birthday"): https://www.gov.uk/schools-admissions/school-starting-age - mygov.scot, "Choosing a school for your child", and Education Scotland Parentzone, "Starting school" (a year group is born between the start of March and the end of February; P1 starts in August): https://www.mygov.scot/register-your-child-for-a-school , https://education.gov.scot/parentzone/my-child/transitions/starting-school - nidirect, "Deferral of pre-school or primary school places", and Department of Education NI, "Compulsory education" (children aged 4 on or before 1 July start P1 that September): https://www.nidirect.gov.uk/articles/deferral-pre-school-or-primary-school-places , https://www.education-ni.gov.uk/articles/compulsory-education

Files

PathBytes
README.md3,057
data/cohort-rules.json725
data/year-groups.json4,302
impl/python.py1,551
impl/rust.rs2,212
impl/typescript.ts1,633
vectors.json5,751