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
minutes_between(2026-09-22, 09:00, 2026-09-22, 17:30)→ 510 a day shift on one dateminutes_between(2026-09-22, 22:00, 2026-09-23, 06:00)→ 480 a night shift across midnight is 480, not -960minutes_between(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.
pub fn minutes_between(start_date: &str, start_time: &str, end_date: &str, end_time: &str) -> i64
| start_date | date | ISO date the period starts on |
| start_time | string | HH:MM, 24-hour, 00:00 to 23:59 |
| end_date | date | ISO date the period ends on |
| end_time | 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
fune!(time.minutes-between@^1); // then call minutes_between(…)
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::epoch_day_from_iso; ← from dates.add-days ^1.0.0 · built alongside by fune
fn minute_of_day(time: &str) -> i64 {
let b = time.as_bytes();
let ok = b.len() == 5 && b[2] == b':' && [0, 1, 3, 4].iter().all(|&i| b[i].is_ascii_digit());
let digit = |i: usize| i64::from(b[i] - b'0');
let (hours, minutes) = if ok {
(digit(0) * 10 + digit(1), digit(3) * 10 + digit(4))
} else {
(-1, -1)
};
if !ok || hours > 23 || minutes > 59 {
panic!("\"{}\" is not a time of day (HH:MM, 00:00 to 23:59)", time);
}
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.
///
/// # Panics
/// Panics on a malformed date or time.
pub fn minutes_between(start_date: &str, start_time: &str, end_date: &str, end_time: &str) -> i64 {
let days = epoch_day_from_iso(end_date) - epoch_day_from_iso(start_date);
days * 1440 + minute_of_day(end_time) - minute_of_day(start_time)
}
pub fn fune_vector(args: &[Value]) -> Value {
Value::Int(minutes_between(
args[0].as_str(),
args[1].as_str(),
args[2].as_str(),
args[3].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 time.minutes-between
The manifest, vectors and README with only the Rust implementation. Install it without the registry with fune add ./time.minutes-between-1.0.0-rust.fune, or fetch it from a terminal with fune pull time.minutes-between@1.0.0:rust.
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 |