Functional Weave
Code in TypeScript

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 count
  • lengthOfService(2024-01-01, 2025-12-30) → years 1, months 11, days 30, total days 730 one day short of two years
  • lengthOfService(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
startDatedatefirst day of employment
endDatedatelast day counted, inclusive: the date of notice, the relevant date or today's date as the caller decides
returnsServiceLength

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";
impl/typescript.ts · 29 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.

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
Download for TypeScript hr.length-of-service-1.0.0-typescript.fune · 6,877 bytes sha256 7606b2691ae2dc6a78e15ba43de78910407f012712b33ee10d861203a77479e9

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.

CaseArgumentsExpected
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
CaseArgumentsExpected
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

PathBytes
README.md1,362
impl/python.py1,165
impl/rust.rs1,714
impl/typescript.ts1,216
vectors.json1,755