time.minutes-between
Minutes from one local date and time to another, correct across midnight; wall-clock time, no time zones.
1.0.0 · published 2026-10-03 by charlie · Anterra
Pinned by 15 tests, run in TypeScript, Python and Rust.
What it does
The minutes from a start date and time to an end date and time. A night shift from 22:00 on 22 September to 06:00 on 23 September is 480 minutes. Taking the dates as well as the times is the point: with times alone, 22:00 to 06:00 is either -960 minutes or a guess that the end is "probably tomorrow", and a shift longer than 24 hours cannot be expressed at all.
The answer is signed like dates.days-between: an end before the start is negative, so swapped arguments are visible rather than silently becoming a next-day shift.
For example
minutesBetween(2026-09-22, 09:00, 2026-09-22, 17:30)→ 510 a day shift on one dateminutesBetween(2026-09-22, 22:00, 2026-09-23, 06:00)→ 480 a night shift across midnight is 480, not -960minutesBetween(2026-09-22, 12:00, 2026-09-22, 12:00)→ 0 the same instant is zero
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 minutesBetween(startDate: string, startTime: string, endDate: string, endTime: string): number
| startDate | date | ISO date the period starts on |
| startTime | string | HH:MM, 24-hour, 00:00 to 23:59 |
| endDate | date | ISO date the period ends on |
| endTime | string | HH:MM, 24-hour, 00:00 to 23:59 |
| returns | int | end minus start in minutes; negative when the end is earlier |
Your code names it in one line, in the file that uses it
import { minutesBetween } from "#fune/time.minutes-between@^1";
Imports name this capability’s declared dependencies, which fune builds next to it in your project; each one links to its page.
import { epochDayFromIso } from "./dates_add_days.ts"; ← from dates.add-days ^1.0.0 · built alongside by fune
function minuteOfDay(time: string): number {
const ok =
typeof time === "string" &&
time.length === 5 &&
time[2] === ":" &&
[0, 1, 3, 4].every((i) => time[i] >= "0" && time[i] <= "9");
const hours = ok ? Number(time.slice(0, 2)) : -1;
const minutes = ok ? Number(time.slice(3, 5)) : -1;
if (!ok || hours > 23 || minutes > 59) {
throw new RangeError(`"${time}" is not a time of day (HH:MM, 00:00 to 23:59)`);
}
return hours * 60 + minutes;
}
/**
* Minutes from one local date and time to another: the difference between two
* wall-clock readings. It knows nothing of time zones or daylight saving, so
* across a clock change it differs from real elapsed time by the change.
*/
export function minutesBetween(startDate: string, startTime: string, endDate: string, endTime: string): number {
const days = epochDayFromIso(endDate) - epochDayFromIso(startDate);
return days * 1440 + minuteOfDay(endTime) - minuteOfDay(startTime);
}Install
fune build
With that line in your source, in a TypeScript project (language typescript in fune.project), fune build resolves it and its 1 dependency, 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 time.minutes-between
The manifest, vectors and README with only the TypeScript implementation. Install it without the registry with fune add ./time.minutes-between-1.0.0-typescript.fune, or fetch it from a terminal with fune pull time.minutes-between@1.0.0:typescript.
The whole function, every language, is one file too: time.minutes-between-1.0.0.fune, 8,862 bytes, sha256 f28512233b8908b71542c3178abccb2281cb736b93ba1f13ac1eede9c71e9386. 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 time.minutes-between
after — your function gets the result and the arguments, and returns the final result.
// fune: after time.minutes-between
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 time.minutes-between
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 time.minutes-between --steps.
// fune: step time.minutes-between 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 | |
|---|---|---|---|
| a day shift on one date | 2026-09-22, 09:00, 2026-09-22, 17:30 | → | 510 |
| a night shift across midnight is 480, not -960 | 2026-09-22, 22:00, 2026-09-23, 06:00 | → | 480 |
| the same instant is zero | 2026-09-22, 12:00, 2026-09-22, 12:00 | → | 0 |
| an end before the start is negative | 2026-09-22, 12:30, 2026-09-22, 12:00 | → | -30 |
| a whole day of minutes | 2026-09-22, 00:00, 2026-09-22, 23:59 | → | 1,439 |
| a shift longer than 24 hours | 2026-09-22, 08:00, 2026-09-23, 20:00 | → | 2,160 |
| across a year end | 2026-12-31, 23:45, 2027-01-01, 00:15 | → | 30 |
| across 29 February | 2024-02-28, 12:00, 2024-03-01, 12:00 | → | 2,880 |
| across the UK clocks going forward: wall-clock 180, though 120 real minutes passed | 2026-03-29, 00:30, 2026-03-29, 03:30 | → | 180 |
| across the UK clocks going back: wall-clock 180, though 240 real minutes passed | 2026-10-25, 00:30, 2026-10-25, 03:30 | → | 180 |
Show the other 5 tests
| Case | Arguments | Expected | |
|---|---|---|---|
| 24:00 is not a time of day; use 00:00 on the next date | 2026-09-22, 20:00, 2026-09-22, 24:00 | → | error: "24:00" is not a time of day |
| an unpadded hour is rejected | 2026-09-22, 9:00, 2026-09-22, 17:00 | → | error: "9:00" is not a time of day |
| minute 60 is rejected | 2026-09-22, 12:60, 2026-09-22, 17:00 | → | error: "12:60" is not a time of day |
| a dotted time is rejected | 2026-09-22, 12.30, 2026-09-22, 17:00 | → | error: "12.30" is not a time of day |
| an impossible date is rejected | 2026-02-30, 12:00, 2026-03-01, 12:00 | → | error: is not a real calendar date |
More from the author
**Local wall-clock time, no time zones.** Both times are read off the same clock on the wall, and the answer is the difference between those readings. It does not know about daylight saving: in the UK, 00:30 to 03:30 on 29 March 2026 is 180 minutes here though only 120 minutes passed, because the clocks went forward at 01:00; on 25 October 2026 the same readings are 180 here and 240 in reality. Payroll and time-and-attendance rules differ on which of those to pay, so this capability does not pick one silently: if real elapsed time matters, convert both instants to UTC with a time zone database first and subtract those.
Times are strictly HH:MM, 24-hour, 00:00 to 23:59. "24:00", "9:00" and "5pm" are errors rather than guesses; the end of a day is 00:00 on the next date.
Files
| Path | Bytes |
|---|---|
| README.md | 1,334 |
| impl/python.py | 989 |
| impl/rust.rs | 1,312 |
| impl/typescript.ts | 1,024 |
| vectors.json | 2,035 |