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.
def iso_to_unix(timestamp: str) -> int
| 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
from fune.time.iso_to_unix import iso_to_unix # time.iso-to-unix@^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 epoch_day_from_iso ← from dates.add-days ^1.0.0 · built alongside by fune
def _is_digit(ch: str) -> bool:
# A character range, not str.isdigit(), which accepts other scripts' digits.
return "0" <= ch <= "9"
def iso_to_unix(timestamp: str) -> int:
"""An RFC 3339 date-time to Unix seconds, parsed by position.
datetime.fromisoformat accepts more shapes than this (and fewer before
3.11), so the rules are written out to match the other languages.
"""
if not isinstance(timestamp, str):
raise ValueError('"%s" is not an RFC 3339 timestamp such as 2026-09-26T12:00:00Z' % (timestamp,))
t = timestamp
def bad() -> ValueError:
return ValueError('"%s" is not an RFC 3339 timestamp such as 2026-09-26T12:00:00Z' % (t,))
if len(t) < 20:
raise bad()
for i in (0, 1, 2, 3, 5, 6, 8, 9, 11, 12, 14, 15, 17, 18):
if not _is_digit(t[i]):
raise bad()
if t[4] != "-" or t[7] != "-" or t[10] not in ("T", "t") or t[13] != ":" or t[16] != ":":
raise bad()
i = 19
if t[i] == ".":
i += 1
start = i
while i < len(t) and _is_digit(t[i]):
i += 1
if i == start:
raise bad()
if i < len(t) and t[i] in ("Z", "z") and i + 1 == len(t):
offset = 0
elif (
i < len(t)
and t[i] in ("+", "-")
and i + 6 == len(t)
and _is_digit(t[i + 1])
and _is_digit(t[i + 2])
and t[i + 3] == ":"
and _is_digit(t[i + 4])
and _is_digit(t[i + 5])
):
oh = int(t[i + 1 : i + 3])
om = int(t[i + 4 : i + 6])
if oh > 23 or om > 59:
raise bad()
offset = (1 if t[i] == "+" else -1) * (oh * 3600 + om * 60)
else:
raise bad()
h, m, s = int(t[11:13]), int(t[14:16]), int(t[17:19])
if h > 23 or m > 59 or s > 59:
raise bad()
return epoch_day_from_iso(t[0:10]) * 86400 + h * 3600 + m * 60 + s - offsetInstall
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.iso-to-unix
The manifest, vectors and README with only the Python implementation. Install it without the registry with fune add ./time.iso-to-unix-1.0.0-python.fune, or fetch it from a terminal with fune pull time.iso-to-unix@1.0.0:python.
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 |