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
iso_to_unix(1970-01-01T00:00:00Z)→ 0 the epochiso_to_unix(2009-02-13T23:31:30Z)→ 1,234,567,890 the famous 1234567890iso_to_unix(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.
pub fn iso_to_unix(timestamp: &str) -> i64
| 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
fune!(time.iso-to-unix@^1); // then call iso_to_unix(…)
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 num(b: &[u8], from: usize, to: usize) -> i64 {
b[from..to].iter().fold(0, |n, d| n * 10 + i64::from(d - b'0'))
}
/// An RFC 3339 date-time to Unix seconds, parsed by position.
///
/// # Panics
/// Panics on anything that is not an RFC 3339 date-time with a zone, or on
/// a date that does not exist.
pub fn iso_to_unix(timestamp: &str) -> i64 {
let bad = || -> ! {
panic!(
"\"{}\" is not an RFC 3339 timestamp such as 2026-09-26T12:00:00Z",
timestamp
)
};
// Bytes: every accepted character is ASCII, so a multi-byte character
// fails a check wherever it sits, as it does in the other languages.
let t = timestamp.as_bytes();
if t.len() < 20 {
bad();
}
for i in [0, 1, 2, 3, 5, 6, 8, 9, 11, 12, 14, 15, 17, 18] {
if !t[i].is_ascii_digit() {
bad();
}
}
if t[4] != b'-' || t[7] != b'-' || (t[10] != b'T' && t[10] != b't') || t[13] != b':' || t[16] != b':' {
bad();
}
let mut i = 19;
if t[i] == b'.' {
i += 1;
let start = i;
while i < t.len() && t[i].is_ascii_digit() {
i += 1;
}
if i == start {
bad();
}
}
let offset: i64;
if i < t.len() && (t[i] == b'Z' || t[i] == b'z') && i + 1 == t.len() {
offset = 0;
} else if i < t.len()
&& (t[i] == b'+' || t[i] == b'-')
&& i + 6 == t.len()
&& t[i + 1].is_ascii_digit()
&& t[i + 2].is_ascii_digit()
&& t[i + 3] == b':'
&& t[i + 4].is_ascii_digit()
&& t[i + 5].is_ascii_digit()
{
let oh = num(t, i + 1, i + 3);
let om = num(t, i + 4, i + 6);
if oh > 23 || om > 59 {
bad();
}
offset = if t[i] == b'+' { 1 } else { -1 } * (oh * 3600 + om * 60);
} else {
bad();
}
let h = num(t, 11, 13);
let m = num(t, 14, 16);
let s = num(t, 17, 19);
if h > 23 || m > 59 || s > 59 {
bad();
}
epoch_day_from_iso(×tamp[0..10]) * 86400 + h * 3600 + m * 60 + s - offset
}
pub fn fune_vector(args: &[Value]) -> Value {
Value::Int(iso_to_unix(args[0].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.iso-to-unix
The manifest, vectors and README with only the Rust implementation. Install it without the registry with fune add ./time.iso-to-unix-1.0.0-rust.fune, or fetch it from a terminal with fune pull time.iso-to-unix@1.0.0:rust.
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 |