monitor.format-duration
A number of seconds as a short human duration for dashboards and alerts: "3h 12m", "2d 4h", "45s".
1.0.0 · published 2026-10-03 by charlie · Anterra
Pinned by 18 tests, run in TypeScript, Python and Rust.
What it does
A number of seconds as the short duration a dashboard or alert shows: `"3h 12m"`, `"2d 4h"`, `"45s"`, `"0s"`. Units are days, hours, minutes and seconds (`d h m s`); days never roll up into weeks or years, because months and years have no fixed length (400 days is `"400d"`).
## The rule
For example
formatDuration(0, 2)→ 0s zero is 0s, not an empty stringformatDuration(45, 2)→ 45s under a minute is seconds onlyformatDuration(11,520, 2)→ 3h 12m three hours twelve minutes
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 formatDuration(seconds: number, maxUnits: number): string
| seconds | int | 0 or more |
| maxUnits | int | how many consecutive units to consider from the largest non-zero one, 1 to 4 (d h m s) |
| returns | string |
Your code names it in one line, in the file that uses it
import { formatDuration } from "#fune/monitor.format-duration@^1";
const UNITS: readonly (readonly [string, number])[] = [["d", 86400], ["h", 3600], ["m", 60], ["s", 1]];
/**
* Seconds as "3h 12m". Starts at the largest non-zero unit and looks at
* `maxUnits` consecutive units from there, leaving out the zero ones, so
* 3605 s with two units is "1h", not "1h 0m" and not "1h 5s": the seconds
* are beyond the precision asked for. Truncates, never rounds up, so a
* duration is never shown longer than it was (86399 s is "23h", not "1d").
*/
export function formatDuration(seconds: number, maxUnits: number): string {
if (!Number.isSafeInteger(seconds)) throw new RangeError(`seconds must be a whole number, received ${seconds}`);
if (seconds < 0) throw new RangeError(`seconds must not be negative, received ${seconds}`);
if (!Number.isSafeInteger(maxUnits) || maxUnits < 1 || maxUnits > 4) {
throw new RangeError(`maxUnits must be 1 to 4, received ${maxUnits}`);
}
if (seconds === 0) return "0s";
let first = 0;
while (Math.floor(seconds / UNITS[first][1]) === 0) first += 1;
const parts: string[] = [];
let rest = seconds;
for (let i = 0; i < UNITS.length; i++) {
const [label, size] = UNITS[i];
const n = Math.floor(rest / size);
rest -= n * size;
if (i >= first && i < first + maxUnits && n > 0) parts.push(`${n}${label}`);
}
return parts.join(" ");
}Install
fune build
With that line in your source, in a TypeScript project (language typescript in fune.project), fune build resolves it and nothing else, 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 monitor.format-duration
The manifest, vectors and README with only the TypeScript implementation. Install it without the registry with fune add ./monitor.format-duration-1.0.0-typescript.fune, or fetch it from a terminal with fune pull monitor.format-duration@1.0.0:typescript.
The whole function, every language, is one file too: monitor.format-duration-1.0.0.fune, 9,390 bytes, sha256 44f871f699d6fea3ca40422a12abc585030b3392315c13e5dd4c90297fe0841f. 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 monitor.format-duration
after — your function gets the result and the arguments, and returns the final result.
// fune: after monitor.format-duration
replace — it requires no other capability, so there is no dependency to replace.
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 monitor.format-duration --steps.
// fune: step monitor.format-duration 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 | |
|---|---|---|---|
| zero is 0s, not an empty string | 0, 2 | → | 0s |
| under a minute is seconds only | 45, 2 | → | 45s |
| three hours twelve minutes | 11,520, 2 | → | 3h 12m |
| two days four hours | 187,200, 2 | → | 2d 4h |
| zero minutes are dropped and the seconds are beyond two units, so 1h | 3,605, 2 | → | 1h |
| with three units the seconds come back and the zero minutes stay out | 3,605, 3 | → | 1h 5s |
| every unit | 90,061, 4 | → | 1d 1h 1m 1s |
| one unit only | 90,061, 1 | → | 1d |
| truncates: one second short of a day is 23h, never 1d | 86,399, 1 | → | 23h |
| one second short of a day in full | 86,399, 4 | → | 23h 59m 59s |
Show the other 8 tests
| Case | Arguments | Expected | |
|---|---|---|---|
| exactly a minute | 60, 4 | → | 1m |
| a million seconds to three units | 1,000,000, 3 | → | 11d 13h 46m |
| days never roll up into years | 34,560,000, 2 | → | 400d |
| negative seconds are an error | -1, 2 | → | error: seconds must not be negative, received -1 |
| fractional seconds are an error | 1.5, 2 | → | error: seconds must be a whole number, received 1.5 |
| zero units is an error | 60, 0 | → | error: maxUnits must be 1 to 4, received 0 |
| five units is an error: there are only four | 60, 5 | → | error: maxUnits must be 1 to 4, received 5 |
| fractional units are an error | 60, 2.5 | → | error: maxUnits must be 1 to 4, received 2.5 |
More from the author
1. Find the largest unit that is not zero. 2. Look at `maxUnits` consecutive units from there, down to seconds at most. 3. Show the non-zero ones among them, separated by a space.
So zeros in between are dropped (`3605` with 3 units is `"1h 5s"`, not `"1h 0m 5s"`), and a unit beyond the window is never shown however large (`3605` with 2 units is `"1h"`: minutes are zero and seconds are beyond the precision asked for). The result is always truncated, never rounded up: a duration is never displayed longer than it was, so `86399` with one unit is `"23h"`, not `"1d"`.
Zero is `"0s"`, never an empty string.
`time.duration` also formats durations, but in whole minutes with no spaces (`"2h15m"`); this one is the second-resolution format monitoring tools use.
## Errors
- `seconds must be a whole number, received X` - `seconds must not be negative, received X` - `maxUnits must be 1 to 4, received X`
Files
| Path | Bytes |
|---|---|
| README.md | 1,226 |
| impl/python.py | 1,344 |
| impl/rust.rs | 1,855 |
| impl/typescript.ts | 1,342 |
| vectors.json | 1,751 |