Functional Weave
Code in Python

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.

def unix_to_iso(seconds: int) -> str
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

from fune.time.unix_to_iso import unix_to_iso  # time.unix-to-iso@^1
impl/python.py · 23 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.

from .dates_add_days import 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.
MIN_SECONDS = -62135596800
MAX_SECONDS = 253402300799


def unix_to_iso(seconds: int) -> str:
    """Unix seconds as YYYY-MM-DDTHH:MM:SSZ.

    Written on dates.add-days rather than datetime so the calendar comes from
    the same code in every language; // floors, so a negative time falls on
    the day before the epoch.
    """
    if isinstance(seconds, bool) or not isinstance(seconds, int):
        raise TypeError("seconds must be a whole number, received %s" % (seconds,))
    if seconds < MIN_SECONDS or seconds > MAX_SECONDS:
        raise ValueError(
            "seconds must be from %d to %d (years 0001 to 9999), received %d" % (MIN_SECONDS, MAX_SECONDS, seconds)
        )
    day = seconds // 86400
    rest = seconds - day * 86400
    return "%sT%02d:%02d:%02dZ" % (iso_from_epoch_day(day), rest // 3600, (rest % 3600) // 60, rest % 60)

Install

fune build

With that line in your source, in a Python project (language python in fune.project), fune build resolves it and its 1 dependency, pins them in fune.lock, downloads only the Python 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.unix-to-iso
Download for Python time.unix-to-iso-1.0.0-python.fune · 5,717 bytes sha256 67a8c3d0aa68c7663b8a98de5daa135281b5b11ffa6ffc8625085d2590098bc4

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

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