Functional Weave
Code in Rust

time.unix-to-iso

Unix time in seconds to an ISO 8601 UTC timestamp such as 2026-09-26T12:00:00Z, for years 0001 to 9999.

1.0.0 · published 2026-10-03 by charlie · Anterra

Pinned by 15 tests, run in TypeScript, Python and Rust.

What it does

`unixToIso(1234567890)` is `"2009-02-13T23:31:30Z"`. An API that reads the clock once (`time.time()`, `Date.now() / 1000`) and needs to show a moment to a person or another program, an account's `createdAt` or a token's `expiresAt`, formats it with this, so every language writes the same string.

The output is the one fixed shape `YYYY-MM-DDTHH:MM:SSZ`: UTC, a `Z` rather than `+00:00`, whole seconds and no fraction. It is valid ISO 8601 and RFC 3339, sorts correctly as text, and parses with `Date.parse`, Python's `datetime.fromisoformat` (3.11 and later) and `time.iso-to-unix`.

For example

  • unix_to_iso(0) → 1970-01-01T00:00:00Z the epoch
  • unix_to_iso(1) → 1970-01-01T00:00:01Z one second after the epoch
  • unix_to_iso(-1) → 1969-12-31T23:59:59Z one second before the epoch is the previous day, which truncating division gets wrong

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 unix_to_iso(seconds: i64) -> String
secondsintseconds since 1970-01-01T00:00:00Z, negative before it; leap seconds are not counted, as in Unix time
returnsstringYYYY-MM-DDTHH:MM:SSZ, always UTC, always with seconds and no fraction

Your code names it in one line, in the file that uses it

fune!(time.unix-to-iso@^1);  // then call unix_to_iso(…)
impl/rust.rs · 38 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::iso_from_epoch_day;  ← from dates.add-days ^1.0.0 · built alongside by fune

/// 0001-01-01T00:00:00Z and 9999-12-31T23:59:59Z in Unix seconds.
const MIN_SECONDS: i64 = -62135596800;
const MAX_SECONDS: i64 = 253402300799;

/// Unix seconds as YYYY-MM-DDTHH:MM:SSZ. The day is floored (`div_euclid`),
/// so a negative time falls on the day before the epoch rather than rounding
/// towards it.
///
/// # Panics
/// Panics outside years 0001 to 9999.
pub fn unix_to_iso(seconds: i64) -> String {
    if !(MIN_SECONDS..=MAX_SECONDS).contains(&seconds) {
        panic!(
            "seconds must be from {} to {} (years 0001 to 9999), received {}",
            MIN_SECONDS, MAX_SECONDS, seconds
        );
    }
    let day = seconds.div_euclid(86400);
    let rest = seconds.rem_euclid(86400);
    format!(
        "{}T{:02}:{:02}:{:02}Z",
        iso_from_epoch_day(day),
        rest / 3600,
        (rest % 3600) / 60,
        rest % 60
    )
}

pub fn fune_vector(args: &[Value]) -> Value {
    let seconds = match &args[0] {
        Value::Int(i) => *i,
        other => panic!("seconds must be a whole number, received {}", other),
    };
    Value::str(&unix_to_iso(seconds))
}

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.unix-to-iso
Download for Rust time.unix-to-iso-1.0.0-rust.fune · 5,936 bytes sha256 dfd9c381be2cd9a64f92d9b4458463d5509dc13368d5335510df6772c09a0e42

The manifest, vectors and README with only the Rust implementation. Install it without the registry with fune add ./time.unix-to-iso-1.0.0-rust.fune, or fetch it from a terminal with fune pull time.unix-to-iso@1.0.0:rust.

The whole function, every language, is one file too: time.unix-to-iso-1.0.0.fune, 8,080 bytes, sha256 5ece36497825e6cc03e39573f4f7148c832ce451c8951058ba30d1d63d3c1eed. 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.unix-to-iso

after — your function gets the result and the arguments, and returns the final result.

// fune: after time.unix-to-iso

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.unix-to-iso

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.unix-to-iso --steps.

// fune: step time.unix-to-iso 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 0 → 1970-01-01T00:00:00Z
one second after the epoch 1 → 1970-01-01T00:00:01Z
one second before the epoch is the previous day, which truncating division gets wrong -1 → 1969-12-31T23:59:59Z
the last second of the first day 86,399 → 1970-01-01T23:59:59Z
2000 was a leap year (divisible by 400) 951,782,400 → 2000-02-29T00:00:00Z
2100 will not be a leap year: 1 March follows 28 February 4,107,542,400 → 2100-03-01T00:00:00Z
the famous 1234567890 1,234,567,890 → 2009-02-13T23:31:30Z
the exp of the RFC 7519 example token 1,300,819,380 → 2011-03-22T18:43:00Z
the last second a signed 32-bit time_t can hold 2,147,483,647 → 2038-01-19T03:14:07Z
one second past 2038, which int64 handles 2,147,483,648 → 2038-01-19T03:14:08Z
Show the other 5 tests
CaseArgumentsExpected
the last second of year 9999 253,402,300,799 → 9999-12-31T23:59:59Z
the first second of year 0001 -62,135,596,800 → 0001-01-01T00:00:00Z
one second past year 9999 253,402,300,800 → error: seconds must be from -62135596800 to 253402300799 (years 0001 to 9999), received 253402300800
milliseconds passed as seconds land far past year 9999 1,790,000,000,000 → error: seconds must be from -62135596800 to 253402300799
a fractional second count 1.5 → error: seconds must be a whole number

More from the author

Seconds before 1970 are negative and are floored, not truncated: `-1` is `1969-12-31T23:59:59Z`, where dividing by 86,400 and rounding towards zero would give a time on 1970-01-01. The calendar arithmetic is `dates.add-days`'s, so leap years (2000 was one, 2100 will not be) come from one place.

The range is what a four-digit year can write, 0001-01-01T00:00:00Z (-62135596800) to 9999-12-31T23:59:59Z (253402300799); anything outside it, or a fractional second count, is an error. Milliseconds (JavaScript's `Date.now()`) must be divided by 1000 and floored first; passing them unchanged lands in the year 58,000 and is refused.

Unix time does not count leap seconds (POSIX.1-2017, Base Definitions, 4.16 "Seconds Since the Epoch"), so neither does this.

Files

PathBytes
README.md1,365
impl/python.py969
impl/rust.rs1,183
impl/typescript.ts1,072
vectors.json1,735