Functional Weave
Code in Rust

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 epoch
  • iso_to_unix(2009-02-13T23:31:30Z) → 1,234,567,890 the famous 1234567890
  • iso_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
timestampstringYYYY-MM-DDTHH:MM:SS, an optional fraction, then Z or +HH:MM / -HH:MM
returnsintseconds 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(…)
impl/rust.rs · 77 lines · open · raw

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(&timestamp[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
Download for Rust time.iso-to-unix-1.0.0-rust.fune · 7,899 bytes sha256 de116205cacb2e941e92d9dedfa2041b8b56a086bc931a027e562eb10a26bb88

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.

CaseArgumentsExpected
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
CaseArgumentsExpected
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

PathBytes
README.md1,321
impl/python.py1,943
impl/rust.rs2,266
impl/typescript.ts1,903
vectors.json2,573