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
lengthOfService(2024-01-01, 2025-12-31)→ years 2, months 0, days 0, total days 731 two whole years when both end dates countlengthOfService(2024-01-01, 2025-12-30)→ years 1, months 11, days 30, total days 730 one day short of two yearslengthOfService(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.
export function lengthOfService(startDate: string, endDate: string): ServiceLength
| startDate | date | first day of employment |
| endDate | 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. */
export interface ServiceLength {
/** completed years */
readonly years: number;
/** completed months after the years, 0-11 */
readonly months: number;
/** days after the months */
readonly days: number;
/** every day from startDate to endDate, both included */
readonly totalDays: number;
}
Your code names it in one line, in the file that uses it
import { lengthOfService } from "#fune/hr.length-of-service@^1";
Imports name this capability’s declared dependencies, which fune builds next to it in your project; each one links to its page.
import { addDays, daysBetween } from "./dates_add_days.ts"; ← from dates.add-days ^1.0.0 · built alongside by fune
import { addMonths } from "./dates_add_months.ts"; ← from dates.add-months ^1.0.0 · built alongside by fune
import { type ServiceLength } from "./hr_length_of_service_types.ts";
/**
* 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.
*/
export function lengthOfService(startDate: string, endDate: string): ServiceLength {
const totalDays = daysBetween(startDate, endDate) + 1;
if (totalDays < 1) {
throw new RangeError(`endDate ${endDate} is before startDate ${startDate}`);
}
const endExclusive = addDays(endDate, 1);
let months =
(Number(endExclusive.slice(0, 4)) - Number(startDate.slice(0, 4))) * 12 +
(Number(endExclusive.slice(5, 7)) - Number(startDate.slice(5, 7)));
while (months > 0 && addMonths(startDate, months) > endExclusive) months -= 1;
const anniversary = addMonths(startDate, months);
return {
years: Math.floor(months / 12),
months: months % 12,
days: daysBetween(anniversary, endExclusive),
totalDays,
};
}Install
fune build
With that line in your source, in a TypeScript project (language typescript in fune.project), fune build resolves it and its 2 dependencies, pins them in fune.lock, downloads only the TypeScript 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 hr.length-of-service
The manifest, vectors and README with only the TypeScript implementation. Install it without the registry with fune add ./hr.length-of-service-1.0.0-typescript.fune, or fetch it from a terminal with fune pull hr.length-of-service@1.0.0:typescript.
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 |