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 epochunix_to_iso(1)→ 1970-01-01T00:00:01Z one second after the epochunix_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
| seconds | int | seconds since 1970-01-01T00:00:00Z, negative before it; leap seconds are not counted, as in Unix time |
| returns | string | YYYY-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
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
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.
| Case | Arguments | Expected | |
|---|---|---|---|
| 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
| Case | Arguments | Expected | |
|---|---|---|---|
| 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
| Path | Bytes |
|---|---|
| README.md | 1,365 |
| impl/python.py | 969 |
| impl/rust.rs | 1,183 |
| impl/typescript.ts | 1,072 |
| vectors.json | 1,735 |