Sigils for constructing and pattern-matching %Tempo{} values at
compile time.
Provides ~o (and its verbose alias ~TEMPO) to turn an ISO 8601
/ ISO 8601-2 / IXDTF / EDTF string into a %Tempo{},
%Tempo.Interval{}, %Tempo.Duration{}, %Tempo.Range{}, or
%Tempo.Set{} struct — or into a pattern that matches one.
import Tempo.Sigils
~o"2026-06-15" #=> %Tempo{…}
~o"2026-06-15T10:30:00Z" #=> zoned datetime
~o"1984?/2004~" #=> qualified interval
~o"2026Y"w #=> ISO week calendar (w modifier)Why a module just for sigils
The module exposes only the sigil macros so import Tempo.Sigils
in application code adds exactly sigil_o/2 and sigil_TEMPO/2 to
the caller's scope — no helper functions leak into the caller's
namespace. Any expansion-time helpers live in a private sibling
module that isn't part of the public API.
Modifiers in value context
When ~o"…" appears as an expression that produces a value (the
default context), a single modifier letter selects the calendar
the string is parsed against:
No modifier — Gregorian calendar (the common case).
w— ISO Week calendar (Calendrical.ISOWeek). Use when the input is in a week-based form you want parsed under ISO week semantics explicitly.
Match context
When ~o"…" is used on the left-hand side of a match —
match?/2, case clauses, =, or function-head patterns —
the sigil expands to a structural pattern rather than a
literal value. The generated pattern is deliberately permissive:
it constrains only what the sigil string actually names.
1. Prefix matching on %Tempo{} and %Tempo.Duration{}
The sigil string is parsed as usual; the resulting :time
keyword list becomes a cons-pattern terminated by a wildcard,
so the sigil matches any value whose :time starts with the
listed {unit, value} pairs. Other struct fields
(:calendar, :shift, :extended, :qualification, …) are
left unconstrained — a Gregorian-looking sigil matches a
Hebrew-calendar value just as happily, because the intent of
the sigil is purely temporal.
import Tempo.Sigils
today = Tempo.new!(year: 2026, month: 4, day: 24)
match?(~o[2026Y], today) #=> true — year prefix
match?(~o[2026Y4M], today) #=> true — year+month prefix
match?(~o[2026Y4M24D], today) #=> true — full prefix
match?(~o[2026Y1M], today) #=> false — month disagrees
match?(~o[2025Y], today) #=> false — year disagrees
hebrew = Tempo.new!(year: 2026, month: 4, day: 24,
calendar: Calendrical.Hebrew)
match?(~o[2026Y], hebrew) #=> true — :calendar is free
duration = Tempo.Duration.new!(year: 1, month: 6)
match?(~o[P1Y6M], duration) #=> true
match?(~o[P2Y6M], duration) #=> false
match?(~o[1Y6M], duration) #=> false — %Tempo{} sigil
# vs %Duration{}2. Modifier-driven bindings
Modifier letters after the closing delimiter bind the matched
value's unit to a same-named variable in the caller's scope.
Bindings are only meaningful on %Tempo{} and
%Tempo.Duration{} values — the only shapes with a
:time keyword list to destructure.
Letter → unit mapping (deliberately avoids overloading M,
which ISO 8601 uses for both month and minute):
Y— yearO— month (mOnth)W— weekD— dayI— day of yearK— day of weekH— hourN— minute (miNute)S— second
The pattern builder lays out the canonical calendar-axis slice between the earliest and latest unit requested (either by the sigil literal or by a modifier), filling positions not named with wildcards. Axis choice — Gregorian, ISO week, or ordinal — is inferred from the union of sigil and modifier units.
import Tempo.Sigils
today = Tempo.new!(year: 2026, month: 4, day: 24)
case today do
~o[2026Y]D -> day #=> 24
end
case today do
~o[2026Y]OD -> {month, day} #=> {4, 24}
end
point = Tempo.new!(year: 2026, month: 6, day: 15,
hour: 14, minute: 30, second: 45)
case point do
~o[2026Y6M]DN -> {day, minute} #=> {15, 30}
end
case point do
~o[2026Y6M15DT14H30M]S -> second #=> 45
end
duration = Tempo.Duration.new!(year: 1, month: 6, day: 15)
case duration do
~o[P1Y]D -> day #=> 15
endA modifier that targets a unit the matched value doesn't carry
simply fails to match; the case falls through to the next
clause. Modifier order inside the sigil is irrelevant —
~o[2026Y]DN and ~o[2026Y]ND expand to the same pattern.
Expansion-time errors
- Unknown modifier letter →
ArgumentError. - Modifier targets a unit already fixed by the sigil (e.g.
~o[2026Y]Y) →ArgumentError. - Mixing calendar axes (e.g.
~o[2026Y4M]W— Gregorian month plus ISO week) →ArgumentError.
3. Matching containers
The sigil also expands to a structural pattern when the parsed
value is a container: %Tempo.Interval{}, %Tempo.Range{},
or %Tempo.Set{}. Each endpoint is itself prefix-matched
using the phase-1 rules, so an endpoint sigil like ~o"2022Y"
inside an interval matches any Tempo whose :time starts with
year 2022.
Container patterns only constrain fields the sigil string
actually names. For intervals, that means from and to are
always constrained, while duration, recurrence,
direction, and repeat_rule are only constrained when they
differ from their struct defaults — so
~o[1984Y/2004Y] doesn't accidentally require
metadata: %{}.
import Tempo.Sigils
{:ok, closed} = Tempo.from_iso8601("1984Y/2004Y")
{:ok, open_up} = Tempo.from_iso8601("1984/..")
{:ok, dur_iv} = Tempo.from_iso8601("P1D/2022-01-01")
match?(~o[1984Y/2004Y], closed) #=> true
match?(~o[1984Y/..], closed) #=> false — closed vs open
match?(~o[1984Y/..], open_up) #=> true
match?(~o[../..], open_up) #=> false
match?(~o[P1D/2022-01-01], dur_iv) #=> true
{:ok, one_of} = Tempo.from_iso8601("[1984,1986,1988]")
{:ok, all_of} = Tempo.from_iso8601("{1960,1961-12}")
match?(~o"[1984Y,1986Y,1988Y]", one_of) #=> true
match?(~o"{1960Y,1961Y12M}", all_of) #=> true
match?(~o"{1984Y,1986Y,1988Y}", one_of) #=> false — :one ≠ :allSets whose literal text begins with [ or { collide with
the ~o[…] and ~o{…} delimiters, so use the string-delim
form ~o"…" for them.
Set members are matched in order and by count — a sigil set
with three members never matches a value with two or four.
Ranges (%Tempo.Range{first: …, last: …}) are matched
structurally whether they appear at the top level or nested
inside a %Tempo.Set{}.
Modifiers on containers
Modifier bindings are not accepted on %Tempo.Interval{},
%Tempo.Range{}, %Tempo.Set{}, or %Tempo.IntervalSet{}
sigils — there is no single keyword-list :time to
destructure. Using one raises ArgumentError at expansion
time. Reach into an interval's endpoint manually:
case interval do
~o[1984Y/..] ->
%Tempo.Interval{from: ~o[1984Y]D = from} = interval
from.time[:day]
end4. Guards
Patterns from ~o"…" cannot appear in a when clause. The
sigil detects the :guard context and raises
ArgumentError with a clear message — use a preceding match
==comparison instead.
5. What match context does not do
No calendar modifier.
Win value context selects the ISO Week calendar; in match context it always means "bind:week". Sigils in match context always parse their string as Gregorian and leave the matched value's:calendarfield unconstrained.No stdlib types.
~o"…"produces a%Tempo{}-family pattern, so it cannot match%Date{},%Time{},%NaiveDateTime{}, or%DateTime{}. For those, use~D/~T/~N/~Udirectly, or convert the value withTempo.from_elixir/1before matching.No complex time elements. Groups, selections, ranges, margin-of-error tuples, and continuations aren't expressible as static Elixir patterns — attempting to use a sigil that contains them in match context raises
ArgumentError.
Summary
Functions
Parse an ISO 8601 / EDTF / IXDTF string at compile time, or expand it to a structural pattern in match context.
Verbose alias for sigil_o. Use when ~o might be confused with
another sigil in scope, or when you want the three-letter form
for readability in dense code.
Functions
Parse an ISO 8601 / EDTF / IXDTF string at compile time, or expand it to a structural pattern in match context.
The sigil's behaviour depends on where it appears in the source:
Value context (the default)
When ~o"…" is used as an expression that produces a value, the
parser fully resolves the string to its %Tempo{} /
%Tempo.Interval{} / %Tempo.Duration{} / %Tempo.Set{} form
and escapes it as a compile-time literal — no runtime parse cost
at the call site.
~o"2026-06-15" #=> %Tempo{…}
~o"2026-06-15T10:30:00Z" #=> zoned datetime
~o"1984?/2004~" #=> qualified intervalThe single modifier letter w selects the ISO Week calendar;
otherwise the string is parsed against the Gregorian calendar.
Match context
When ~o"…" appears on the left-hand side of a match —
match?/2, case clauses, =, or function-head patterns — the
sigil expands to a structural pattern that matches by :time
prefix.
today = Tempo.new!(year: 2026, month: 4, day: 24)
match?(~o[2026Y], today) #=> true (year prefix)
match?(~o[2026Y4M], today) #=> true (year+month prefix)
match?(~o[2025Y], today) #=> false (year disagrees)Modifier letters after the closing delimiter bind matched units
to same-named variables in scope. The letters are Y O W D I K H N S — see Tempo.Sigils for the full mapping.
case today do
~o[2026Y]OD -> {month, day} #=> {4, 24}
endMatch context also matches %Tempo.Duration{},
%Tempo.Interval{}, %Tempo.Range{}, and %Tempo.Set{}
values structurally.
Using the sigil inside a when guard is rejected at expansion
time — use a preceding match or an == comparison instead.
See the module documentation for the full specification of match context, including container patterns, modifier-binding rules, and expansion-time errors.
Verbose alias for sigil_o. Use when ~o might be confused with
another sigil in scope, or when you want the three-letter form
for readability in dense code.
Behaves identically to sigil_o/2 in both value and match
contexts — see that function's documentation for the full
specification.