hr.length-of-service
Continuous service between two dates in completed years, months and days, counting both end dates.
1.0.0 · published 2026-10-03 by charlie · Anterra
Pinned by 11 tests, run in TypeScript, Python and Rust.
What it does
Continuous service from a start date to an end date, as completed years, months and days. Statutory rights hang off these numbers (one month for notice, two years for redundancy pay and unfair dismissal), so the boundary matters.
**Both dates count.** Someone who starts on 1 January 2024 and whose last counted day is 31 December 2025 has exactly 2 years, not 1 year 11 months and 30 days. Internally the end is moved to the next day and the calendar difference is taken from there, which is how the Employment Rights Act 1996 s.211 frames a period of continuous employment: it begins with the day the employee starts work and ends with the day in question.
For example
length_of_service(2024-01-01, 2025-12-31)→ years 2, months 0, days 0, total days 731 two whole years when both end dates countlength_of_service(2024-01-01, 2025-12-30)→ years 1, months 11, days 30, total days 730 one day short of two yearslength_of_service(2020-03-15, 2026-09-23)→ years 6, months 6, days 9, total days 2,384 years, months and days across two leap days
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 length_of_service(start_date: &str, end_date: &str) -> ServiceLength
| start_date | date | first day of employment |
| end_date | date | last day counted, inclusive: the date of notice, the relevant date or today's date as the caller decides |
| returns | ServiceLength |
The type it declares, generated into your project
/// Service as a person states it, plus the exact day count.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct ServiceLength {
/// completed years
pub years: i64,
/// completed months after the years, 0-11
pub months: i64,
/// days after the months
pub days: i64,
/// every day from startDate to endDate, both included
pub total_days: i64,
}
Your code names it in one line, in the file that uses it
fune!(hr.length-of-service@^1); // then call length_of_service(…)
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}; ← 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
/// Completed years, months and days of service, both end dates included.
///
/// The end is moved to the following day so that a service period ending on
/// the eve of an anniversary counts as a whole year; forgetting that is the
/// off-by-one that denies someone a statutory right by a day.
///
/// # Panics
/// Panics on a malformed date or an end before the start.
pub fn length_of_service(start_date: &str, end_date: &str) -> ServiceLength {
let total_days = days_between(start_date, end_date) + 1;
if total_days < 1 {
panic!("endDate {} is before startDate {}", end_date, start_date);
}
let end_exclusive = add_days(end_date, 1);
let num = |s: &str, a: usize, b: usize| s[a..b].parse::<i64>().unwrap();
let mut months = (num(&end_exclusive, 0, 4) - num(start_date, 0, 4)) * 12
+ (num(&end_exclusive, 5, 7) - num(start_date, 5, 7));
while months > 0 && add_months(start_date, months) > end_exclusive {
months -= 1;
}
let anniversary = add_months(start_date, months);
ServiceLength {
years: months / 12,
months: months % 12,
days: days_between(&anniversary, &end_exclusive),
total_days,
}
}
pub fn service_length_to_value(s: &ServiceLength) -> Value {
Value::obj(vec![
("years", Value::Int(s.years)),
("months", Value::Int(s.months)),
("days", Value::Int(s.days)),
("totalDays", Value::Int(s.total_days)),
])
}
pub fn fune_vector(args: &[Value]) -> Value {
service_length_to_value(&length_of_service(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 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 hr.length-of-service
The manifest, vectors and README with only the Rust implementation. Install it without the registry with fune add ./hr.length-of-service-1.0.0-rust.fune, or fetch it from a terminal with fune pull hr.length-of-service@1.0.0:rust.
The whole function, every language, is one file too: hr.length-of-service-1.0.0.fune, 9,867 bytes, sha256 ffcde6ff77b8bcb654ea1aeb5ac3a10f4e19609b983b285e8bd386b8e4d1dc82. 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 hr.length-of-service
after — your function gets the result and the arguments, and returns the final result.
// fune: after hr.length-of-service
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 hr.length-of-service
// fune: replace dates.add-months in hr.length-of-service
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 hr.length-of-service --steps.
// fune: step hr.length-of-service 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 | |
|---|---|---|---|
| two whole years when both end dates count | 2024-01-01, 2025-12-31 | → | years 2, months 0, days 0, total days 731 |
| one day short of two years | 2024-01-01, 2025-12-30 | → | years 1, months 11, days 30, total days 730 |
| years, months and days across two leap days | 2020-03-15, 2026-09-23 | → | years 6, months 6, days 9, total days 2,384 |
| the first day of employment is one day of service | 2026-09-23, 2026-09-23 | → | years 0, months 0, days 1, total days 1 |
| a start on the 31st completes two months on the 30th of a 31-day month | 2025-01-31, 2025-03-30 | → | years 0, months 2, days 0, total days 59 |
| a start on 31 January completes a month when February is covered | 2025-01-31, 2025-02-27 | → | years 0, months 1, days 0, total days 28 |
| eleven months and thirty days is not a year | 2025-06-15, 2026-06-13 | → | years 0, months 11, days 30, total days 364 |
| twelve years to the day | 2014-04-01, 2026-03-31 | → | years 12, months 0, days 0, total days 4,383 |
| under a month | 2026-09-01, 2026-09-23 | → | years 0, months 0, days 23, total days 23 |
| an end before the start is an error | 2026-09-23, 2026-09-22 | → | error: is before startDate |
Show the other 1 test
| Case | Arguments | Expected | |
|---|---|---|---|
| an impossible date is an error | 2026-02-30, 2026-09-22 | → | error: is not a real calendar date |
More from the author
**Month ends.** A month is completed when the same day-of-month comes round again; a start on the 29th, 30th or 31st uses the last day of a shorter month instead (dates.add-months clamps: 31 January plus one month is 28 February). So 31 January to 27 February inclusive is one whole month: every day of February up to its 28th day is covered. Callers with a contractual rule for these starts should apply it themselves.
This does not decide whether service was continuous (breaks, TUPE transfers, strikes and the ERA 1996 Part XIV rules); it measures the period it is given.
Source: Employment Rights Act 1996 s.211, https://www.legislation.gov.uk/ukpga/1996/18/section/211.
Files
| Path | Bytes |
|---|---|
| README.md | 1,362 |
| impl/python.py | 1,165 |
| impl/rust.rs | 1,714 |
| impl/typescript.ts | 1,216 |
| vectors.json | 1,755 |