time.iso-to-unix
An RFC 3339 timestamp with a Z or offset, such as 2026-09-26T13:00:00+01:00, to Unix time in whole seconds.
1.0.0 · published 2026-10-03 by charlie · Anterra
Pinned by 20 tests, run in TypeScript, Python and Rust.
What it does
`isoToUnix("2009-02-13T23:31:30Z")` is `1234567890`, and `isoToUnix("2026-09-26T13:00:00+01:00")` is the same instant as `2026-09-26T12:00:00Z`. It reads the timestamps `time.unix-to-iso` writes, and the ones other systems send, so that times can be compared as integers.
The accepted form is RFC 3339's `date-time`: `YYYY-MM-DD`, `T`, `HH:MM:SS`, an optional fraction (`.5`, `.123456`), and a zone of `Z` or `+HH:MM` / `-HH:MM`. RFC 3339 lets `T` and `Z` be lowercase, so they may be. A time with no zone is refused, because the same wall-clock time is a different instant in every zone and guessing one is how an hour goes missing twice a year.
For example
isoToUnix(1970-01-01T00:00:00Z)→ 0 the epochisoToUnix(2009-02-13T23:31:30Z)→ 1,234,567,890 the famous 1234567890isoToUnix(1969-12-31T23:59:59Z)→ -1 one second before the epoch
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 isoToUnix(timestamp: string): number
| timestamp | string | YYYY-MM-DDTHH:MM:SS, an optional fraction, then Z or +HH:MM / -HH:MM |
| returns | int | seconds since 1970-01-01T00:00:00Z; a fraction of a second is dropped |
Your code names it in one line, in the file that uses it
import { isoToUnix } from "#fune/time.iso-to-unix@^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 isDigit(text: string, i: number): boolean {
const c = text.charCodeAt(i);
return c >= 48 && c <= 57;
}
function num(text: string, from: number, to: number): number {
let n = 0;
for (let i = from; i < to; i++) n = n * 10 + (text.charCodeAt(i) - 48);
return n;
}
/**
* An RFC 3339 date-time to Unix seconds. Parsed by position, not with a
* regular expression or Date.parse, which accepts a dozen other shapes and
* reads a zoneless time in the machine's own zone.
*/
export function isoToUnix(timestamp: string): number {
const bad = (): Error => new RangeError(`"${timestamp}" is not an RFC 3339 timestamp such as 2026-09-26T12:00:00Z`);
if (typeof timestamp !== "string") throw bad();
const t = timestamp;
if (t.length < 20) throw bad();
for (const i of [0, 1, 2, 3, 5, 6, 8, 9, 11, 12, 14, 15, 17, 18]) {
if (!isDigit(t, i)) throw bad();
}
if (t[4] !== "-" || t[7] !== "-" || (t[10] !== "T" && t[10] !== "t") || t[13] !== ":" || t[16] !== ":") throw bad();
let i = 19;
if (t[i] === ".") {
i++;
const start = i;
while (i < t.length && isDigit(t, i)) i++;
if (i === start) throw bad();
}
let offset = 0;
if ((t[i] === "Z" || t[i] === "z") && i + 1 === t.length) {
offset = 0;
} else if ((t[i] === "+" || t[i] === "-") && i + 6 === t.length && isDigit(t, i + 1) && isDigit(t, i + 2) && t[i + 3] === ":" && isDigit(t, i + 4) && isDigit(t, i + 5)) {
const oh = num(t, i + 1, i + 3);
const om = num(t, i + 4, i + 6);
if (oh > 23 || om > 59) throw bad();
offset = (t[i] === "+" ? 1 : -1) * (oh * 3600 + om * 60);
} else {
throw bad();
}
const h = num(t, 11, 13);
const m = num(t, 14, 16);
const s = num(t, 17, 19);
if (h > 23 || m > 59 || s > 59) throw bad();
return epochDayFromIso(t.slice(0, 10)) * 86400 + h * 3600 + m * 60 + s - offset;
}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.iso-to-unix
The manifest, vectors and README with only the TypeScript implementation. Install it without the registry with fune add ./time.iso-to-unix-1.0.0-typescript.fune, or fetch it from a terminal with fune pull time.iso-to-unix@1.0.0:typescript.
The whole function, every language, is one file too: time.iso-to-unix-1.0.0.fune, 11,961 bytes, sha256 2d3f5e9d5cc2f5bf95e84e0be406e3b7a459602bbc4581e126bdd823dc7f1cf4. 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.iso-to-unix
after — your function gets the result and the arguments, and returns the final result.
// fune: after time.iso-to-unix
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.iso-to-unix
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.iso-to-unix --steps.
// fune: step time.iso-to-unix 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 | |
|---|---|---|---|
| the epoch | 1970-01-01T00:00:00Z | → | 0 |
| the famous 1234567890 | 2009-02-13T23:31:30Z | → | 1,234,567,890 |
| one second before the epoch | 1969-12-31T23:59:59Z | → | -1 |
| the last second of a signed 32-bit time_t | 2038-01-19T03:14:07Z | → | 2,147,483,647 |
| British Summer Time: an hour ahead of UTC, so an hour is taken off | 2026-09-26T13:00:00+01:00 | → | 1,790,424,000 |
| a negative half-hour offset is added back | 2026-09-26T06:30:00-05:30 | → | 1,790,424,000 |
| a fraction is dropped, not rounded up to the next second | 2026-09-26T12:00:00.999Z | → | 1,790,424,000 |
| RFC 3339 allows a lowercase t and z | 2026-09-26t12:00:00z | → | 1,790,424,000 |
| -00:00 is UTC, on a leap day | 2000-02-29T12:00:00-00:00 | → | 951,825,600 |
| the first second of year 0001 | 0001-01-01T00:00:00Z | → | -62,135,596,800 |
Show the other 10 tests
| Case | Arguments | Expected | |
|---|---|---|---|
| a time with no zone is ambiguous | 2026-09-26T12:00:00 | → | error: is not an RFC 3339 timestamp such as 2026-09-26T12:00:00Z |
| a space instead of T | 2026-09-26 12:00:00Z | → | error: is not an RFC 3339 timestamp such as 2026-09-26T12:00:00Z |
| hour 24 | 2026-09-26T24:00:00Z | → | error: is not an RFC 3339 timestamp such as 2026-09-26T12:00:00Z |
| a leap second has no Unix time | 2026-09-26T23:59:60Z | → | error: is not an RFC 3339 timestamp such as 2026-09-26T12:00:00Z |
| a decimal point with no digits | 2026-09-26T12:00:00.Z | → | error: is not an RFC 3339 timestamp such as 2026-09-26T12:00:00Z |
| no seconds | 2026-09-26T12:00Z | → | error: is not an RFC 3339 timestamp such as 2026-09-26T12:00:00Z |
| an offset without its colon | 2026-09-26T12:00:00+0100 | → | error: is not an RFC 3339 timestamp such as 2026-09-26T12:00:00Z |
| a trailing newline | 2026-09-26T12:00:00Z | → | error: is not an RFC 3339 timestamp such as 2026-09-26T12:00:00Z |
| Arabic-Indic digits in the year | ٢٠٢٦-09-26T12:00:00Z | → | error: is not an RFC 3339 timestamp such as 2026-09-26T12:00:00Z |
| a well-formed timestamp on a day that does not exist | 2026-02-30T00:00:00Z | → | error: is not a real calendar date |
More from the author
A fraction is dropped, so `.999` still belongs to the second it started in (Unix time floors). Seconds run 00-59: RFC 3339 allows `:60` for a leap second, but Unix time has no number for it, so it is refused rather than silently moved. Hours 00-23 (`24:00:00` is refused), offsets up to 23:59.
The date itself is checked by `dates.add-days`, so `2026-02-30` is refused with its message (`is not a real calendar date`), and years run 0001 to 9999. No spaces, no week or ordinal dates, no missing seconds; ASCII digits only.
Source: RFC 3339, Date and Time on the Internet: Timestamps, section 5.6 (https://www.rfc-editor.org/rfc/rfc3339#section-5.6).
Files
| Path | Bytes |
|---|---|
| README.md | 1,321 |
| impl/python.py | 1,943 |
| impl/rust.rs | 2,266 |
| impl/typescript.ts | 1,903 |
| vectors.json | 2,573 |