ISO 8601 Conformance Guide

Copy Markdown View Source

Tempo implements a substantial subset of ISO 8601:2019 Part 1 and ISO 8601-2:2019 Part 2, plus the IETF IXDTF draft (draft-ietf-sedate-datetime-extended-09). This guide catalogues what is supported, what isn't, and where Tempo diverges from the strict standard.

The canonical authority is the PDF of the ISO standard (held locally in ~/Documents/Development/iso_standards/). When this document and the standard disagree, the standard wins — please file an issue.

1. Core principle — bounded intervals

Every Tempo value is a bounded interval on the time line, not an instant. 2026-01 is not the single instant "January 2026"; it is the interval [2026-01-01, 2026-02-01) — inclusive of the first boundary, exclusive of the last. This is called the implicit-span semantics: a partial date specification spans the next-finer unit that isn't given.

A single Tempo.from_iso8601/1 call therefore always returns a bounded value, never a "partial" or "unresolved" date. This lets the set operations (Tempo.union/2, intersection/2, difference/2, and interval-set coalescing) reason about every value uniformly.

2. ISO 8601 Part 1 — core representations

Supported

FeatureExamples
Year2022, -0001, +002022
Year-month2022-06, 202206
Year-month-day2022-06-15, 20220615
Ordinal date2022-166, 2022166
Week date2022-W24, 2022-W24-3, 2022W243
Month-day06-15 (the truncated --06-15 / --0615 forms are deprecated — see below)
Time of dayT10, T10:30, T10:30:00, T103000
Fractional secondsT10:30:00.5, T10:30:00,5
Time zone Z, +HH, +HH:MM, +HHMM10:30:00Z, 10:30:00+05:30
Combined datetime2022-06-15T10:30:00Z
Durations PnYnMnDTnHnMnSP1Y, PT30M, P3Y6M4DT12H30M5S
Negative duration-P100D
Fixed-endpoint interval2022-01/2022-06, 20220101/20220630
Duration-relative interval2022-01-01/P1Y, P1Y/2022-12-31
Recurring intervalR/2022-01/P1M, R5/2022-01/P1M
Expanded (large) years+002022, -0001
Leap second23:59:60Z on 30 June or 31 December UTC

Partial or divergent

  • Truncated representations (--06-15, 85-06-15 — 2-digit year) were deprecated in the 2019 edition and are not accepted. Modernise your input.

  • Fractional seconds are preserved as a :microsecond {value, precision} component (not truncated to whole seconds). The digit count is significant — T10:30:00.120 is millisecond resolution and T10:30:00.12 is centisecond resolution, two distinct interval widths. ISO 8601 permits an unbounded number of fractional digits; Tempo caps precision at 6 (microsecond), matching Elixir's Time/DateTime. Input with more than 6 fractional digits is truncated to microsecond. Fractional minutes and hours (T10:30,5, T10,5) still cascade to a coarser-unit remainder as before.

Not supported

  • Nothing known to be missing from Part 1 as of v0.2.0.

3. ISO 8601-2 Part 2 — extensions

Supported

FeatureExamples
Unspecified digits (X)156X, 1XXX, 2022-XX, 1985-XX-XX, -1XXX-XX, -XXXX-12-XX
EDTF Level 1 qualification (?, ~, %)2022?, 2022~, 2022%
ISO 8601-2 §8 component qualification (implicit form)2004-06~-11 (group), 2004-?06-11 (individual), ?2022-06-15 (leading individual). See "Component qualification" below.
Per-endpoint qualification in intervals1984?/2004~, 2019-12/2020%, 2004-06-11%/2004-06~
Leading prefix qualifier?2022-06-15, %2001, ?-2004-06
Open-ended intervals1985/.., ../1985, ../.., 1985/, /1985, /, /.., ../
Set of dates — all of{1960,1961,1962}, {1960..1970}
Set of dates — one of[1984,1986,1988], [1667..1672]
Range in set[1900..2000], {-1640-06..-1200-01}
Groups5G10DU (5th group of 10 days), 2018Y4G60DU6D (2018, day 6 of the 4th group of 60 days)
SelectionsL1MN, L2MI3N (1st month, 3rd instance of the 2nd month)
Meteorological seasons (codes 21–24)2022-21 (spring), 2022-22 (summer), 2022-23 (autumn), 2022-24 (winter)
Astronomical seasons (codes 25–32)2022-25 (N spring), 2022-26 (N summer), 2022-27 (N autumn), 2022-28 (N winter), 2022-29..32 (Southern hemisphere). Boundaries computed via the Astro library using March/September equinoxes and June/December solstices (accurate to ≈2 minutes for years 1000–3000 CE).
Quarters (codes 33–36)2022-33 (Q1), 2022-36 (Q4)
Quadrimesters (codes 37–39)2022-37, 2022-38, 2022-39
Semestrals / halves (codes 40–41)2022-40 (H1), 2022-41 (H2)
Negative calendar qualification-2004?, -2004-06?, -2001-34 (Q1 BCE)
Sets / ranges with negative members[-1667,1668], {-1640-06..-1200-01}
Margin of errornumeric literal with ± (via form_number)
Exponents on year2018E3 style — parsed by numbers.ex exponent()
Significant-digit annotations (short form)1950S2, -1859S5, Y3388E2S3
Year-zero (0000, -0000)Parses as year 0. Interpretation per astronomical convention (year 0 = 1 BCE) is the caller's responsibility.

Not supported

FeatureExampleReason
Cross-endpoint semantic validation of intervals2012-24/2012-21 (winter before spring)Parses at the syntax level; a semantic ordering check across the two endpoints is not currently enforced, so a small number of syntactically-valid but semantically-inverted intervals are accepted.

All other EDTF Level 2 features — including wide-range exponent years (Y17E8, Y-170000002) and long-year significant-digit annotations (Y171010000S3) — are supported.

Component qualification (ISO 8601-2 §8)

A ? (uncertain), ~ (approximate), or % (both) qualifier's position sets its scope, per §8.2. Tempo honours all three scopes for implicit-form dates, storing whole-value qualification on :qualification and per-component qualification on the :qualifications map (keyed by unit):

Position§8 scopeExampleResult
Rightmost end§8.2.1 complete2004-06-11%qualification: :uncertain_and_approximate
Right of a component§8.2.2 group — that component and every coarser one to its left2004-06~-11qualifications: %{year: :approximate, month: :approximate}
Left of a component§8.2.3 individual — that component only2004-?06-11qualifications: %{month: :uncertain}
Leading (left of the first component)§8.2.3 individual?2004-06-11qualifications: %{year: :uncertain}

Overlapping qualifiers on one component combine: 2004-?06~-11 (individual ? on the month, group ~ on the month and year) yields %{year: :approximate, month: :uncertain_and_approximate}.

The explicit (designator) form is also parsed: a qualifier between a value and its designator (2004~Y6?M11D, §8.3, including a qualified BC year 2004~YB) is always individual. inspect/1 and to_iso8601/1 render the :qualifications map back in this form, so component qualification round-trips — a parsed group such as 2004-06~-11 re-encodes as the equivalent explicit individual qualifiers 2004~Y6~M11D. Per §8.2.4, a value whose every present component shares one qualifier collapses to the compact complete form (2004%Y6%M11%D2004Y6M11D%); group collapse is not attempted, as the explicit output form has no group representation.

4. IXDTF — Internet Extended Date/Time Format

Tempo implements the IXDTF draft suffix syntax. After any ISO 8601 date-time, an optional suffix may carry:

SuffixExampleStored on %Tempo{}
IANA time zone[Europe/Paris]extended.zone_id
Numeric offset[+08:45], [-03:30], [+0530]extended.zone_offset (minutes from UTC)
Calendar (u-ca=)[u-ca=hebrew], [u-ca=gregory]extended.calendar (atom)
Generic tag[_foo=bar-baz]extended.tags (%{"key" => ["value", ...]})

Each bracket may be prefixed with ! to mark it critical. Unrecognised critical tags cause the parse to fail; unrecognised elective tags are retained verbatim in extended.tags.

Time zones are validated against Tzdata.zone_exists?/1. Calendars are validated against Localize.validate_calendar/1, which also handles the "gregory":gregorian alias per BCP 47.

5. Project-specific extensions (not in ISO 8601)

These syntaxes are Tempo conveniences, not part of any standard:

  • Step in range{1990..1999//2}Y or 2023Y{1..-1//2}W means "every second week in 2023".

  • Explicit suffixes2022Y11M20D instead of 2022-11-20. Used by the ~o sigil as the canonical output form.

  • Repeat rule/F combinator inside a parsed expression.

  • Selection instance countL…N with an I modifier for the nth instance (2I1K = "the 2nd Monday").

  • RRULE selection designators — a recurrence selection may carry two RFC 5545 (iCalendar RRULE / cron) filters that have no ISO 8601 representation. Tempo assigns them project-specific designators so a parsed rule round-trips through inspect/1/Tempo.to_iso8601/1 instead of being lost:

    • V — set position (RRULE BYSETPOS): keep the Nth occurrence(s) of the per-period candidate set, e.g. -1V = the last, {1,3}V = the 1st and 3rd.
    • Q — week start (RRULE WKST): the weekday a week begins on, e.g. 7Q = Sunday.

    So "the last weekday of each month" (FREQ=MONTHLY;BYDAY=MO,TU,WE,TH,FR;BYSETPOS=-1) inspects as ~o"R/../P1M/FL{1..5}K-1VN". These two designators are the only non-standard letters Tempo emits inside a selection; the canonical external form for such a rule is still the RRULE string via Tempo.to_rrule/1.

None of these break ISO 8601 compatibility — Tempo accepts the standard forms too.

6. Ambiguity resolution

A few ISO 8601 constructs are genuinely ambiguous; Tempo resolves them as follows.

ConstructStandard saysTempo does
Seasons 21-24Hemisphere unspecifiedTreated as Northern meteorological (21 = spring = March-May).
Z without offset"UTC is known, local offset unknown" (per RFC 5322 / IXDTF)Stored as shift: [hour: 0]. No distinction from +00:00.
-00:00ISO 8601:2000 forbade; ISO 8601:2019 permitsPermitted; equivalent to Z.
Leading qualifier on a date (?2022-06-15)§8.2.3: left of a component qualifies that componentIndividual qualification of the leftmost (coarsest) component — ?2022-06-15 stamps %{year: :uncertain} on :qualifications, not the whole value. See §3 "Component qualification".

7. Test coverage

Tempo's conformance is exercised by:

  • test/tempo/iso8601/dates_times_test.exs — Part 1 core representations.
  • test/tempo/iso8601/duration_test.exs, interval_test.exs, set_test.exs — interval, duration, and set constructs.
  • test/tempo/iso8601/extended_test.exs — IXDTF suffix parsing.
  • test/tempo/iso8601/qualification_test.exs — EDTF L1 and L2 qualification.
  • test/tempo/iso8601/open_interval_test.exs — open-ended intervals.
  • test/tempo/iso8601/unspecified_digit_test.exs — unspecified-digit masks.
  • test/tempo/iso8601/leap_second_test.exs — leap-second validation.
  • test/tempo/iso8601/round_trip_test.exs — one representative per token round-trips through inspect/1: parse a value, re-parse the canonical ~o"…" form inspect/1 shows, and get the identical value back — no component dropped or mangled.
  • test/tempo/iso8601/edtf_corpus_test.exs — the full unt-libraries/edtf-validate corpus (BSD-3-Clause), exercised at 100%. See test/support/edtf_corpus.ex for the raw strings and attribution.

The EDTF corpus is the only publicly-available conformance test set we know of for ISO 8601-2 Part 2; Tempo passes it in full.

8. Comparison to other implementations

No other known implementation in any language supports full ISO 8601 Parts 1 and 2. The RFC 3339 subset is covered by every mainstream date library (java.time, chrono, Python datetime, etc.). Part 2 — sets, groups, uncertainty, seasons, selections — is covered only by the EDTF family of libraries (python-edtf, edtf.js, edtf-validate, mbklein/nulib for Elixir), which themselves target EDTF's subset of the 2019 standard. Tempo's Part 2 coverage is roughly equivalent to EDTF Level 2 plus Tempo-specific extensions (groups, selections, step ranges).

See docs/iso8601-conformance-research.md for the detailed prior-art survey.

9. Reporting a conformance gap

If you have an ISO 8601 or EDTF string that should parse and doesn't, please open an issue including:

  • The input string.
  • The expected result (cite the ISO 8601 clause or EDTF level).
  • The actual output from Tempo.from_iso8601/1.

Pull requests with a failing test case are especially welcome.