Releases: elixir-tempo/tempo
Release list
Tempo version 1.6.4
[v1.6.4] — 2026-09-03
Fixed
-
Tempo.select/2no longer projects a date that cannot exist. Selecting~o"2M29D"across~o"{2026..2029}Y"yields only 2028, where before it fabricated four February 29ths. -
Tempo.explain/1describes every representation. An audit over all 1777 ISO literals in the repository found 140 values it crashed on, described generically, or described wrongly; all of them now explain. -
A yearless date (
~o"4M3D", a birthday) is no longer reported as "an anchored Tempo value" with a[?, ?)span. It reads as "April 3, in any year" spanning--04-03. -
Tempo.to_interval/1no longer raises on a yearless week (1W), a clock-only value (T-1S), a bare offset (Z), or a grouped component (2018-{1,3,5}G2MU). Grouped components reportTempo.MaterialisationErrorrather than crashing insideKeyword. -
Tempo.resolution/1handles a value with no components, and grouped components resolve to their group size instead of raisingCaseClauseError.
Tempo version 1.6.3
[v1.6.3] — 2026-09-03
Fixed
-
An interval's
[u-ca=…]calendar now propagates backward from its end onto an endpoint carrying no tag of its own, as the zone already did.1448Y9M24D/25D[u-ca=islamic-civil]was parsing as a mixed Gregorian/Islamic pair, which its abbreviated end cannot mean. -
An interval whose endpoints share a calendar writes
[u-ca=…]once, at the end. A deliberately mixed pair still names the calendar on each endpoint.
Tempo version 1.6.2
[v1.6.2] — 2026-09-03
Fixed
-
An interval whose endpoints share a zone writes the IXDTF suffix once, at the end (
2026Y9M2DT18H/T20H[Australia/Melbourne]), where before it was repeated on both. A per-endpoint[u-ca=…]is still written on each, because it decides how that endpoint is read. -
2026-09-02T18:00/2026-09-02T20:00[Australia/Melbourne]— the suffix written once, as IXDTF defines it — no longer fails endpoint-order validation. The endpoint frame now propagates before the order check rather than after.
Tempo version 1.6.1
[v1.6.1] — 2026-09-03
Fixed
-
Un-anchored arithmetic reports a missing anchor by return value rather than
throw, so it can no longer escape to a caller as an uncaught{:tempo_math, :requires_anchor}.lib/now contains nothrow,catchor explicittry. -
Tempo.to_interval/1materialises an unspecified year (X*Y12M28D) instead of raising aFunctionClauseError. Where the answer depends on the missing year (X*Y2M28D) it returnsTempo.RequiresAnchorError. -
Tempo.to_interval/1resolves a yearless masked month (XX-15, the 15th of any month) instead of raising aKeyError. Calendars whose month count varies by year returnTempo.RequiresAnchorError. -
Enumerating a masked value that needs an anchor (
XX-15in a calendar whose month count varies by year) raisesTempo.RequiresAnchorErrorrather than a bareKeyError.Enumerablehas no error channel;Tempo.to_interval/1returns an error tuple for the same value. -
Tempo.explain/1describes a start-and-duration interval (2026-06-15T09:00/PT8H) rather than reporting "an unusual shape". -
Tempo.explain/1no longer calls a duration-and-end interval (P1D/2026-06-15) open-lower. The duration implies the lower bound, so the value is bounded. -
Tempo.explain/1explains an unanchored recurrence (R/../P1Y/FL11M4I4KN) instead of reporting "an unusual shape". Its selection, cadence and missing anchor are now named in prose.
Tempo version 1.6.0
[v1.6.0] — 2026-09-02
Added
-
Tempo.parse_date/2,parse_datetime/2,parse_time/2,parse_interval/2andparse_duration/1, with bang variants, parse a string that must be one ISO 8601 shape. A date, a datetime and a time are all%Tempo{}, sofrom_iso8601/1cannot report a time of day arriving in a date field; declaring the profile makes it an error. -
Profile parsing is 4–28× faster than
from_iso8601/1, being a narrower grammar. Correctness, not speed, is the reason to prefer it. -
parse_time/2reads"2026"as 20:26 wherefrom_iso8601/1reads the year 2026 — the declared profile resolves the ISO 8601 basic-format ambiguity.
Fixed
-
Count-from-the-end bounds on fixed-extent units now resolve at validation, so a set-valued clock component expands instead of yielding nothing.
T{-4..-1}His the last four hours of the day. -
Week-and-weekday and ordinal-day values with ranges (
{1..3}W{1..-1}K,{1..-1}O) expand to real calendar dates.{1..-1}Opreviously raised aKeyError. -
A set resolving to a single member collapses to that member (
{12..-1}Mis12M), fixing aFunctionClauseErrorindays_in_month/3. -
Ranges in any combination of component positions now expand:
{2000..2010}Y{1..-1}M{1..-1}Dyields its 4018 days instead of looping forever. Each component resolves against its already-concrete coarser units, so a month range follows the year's own month count and a day range the month's own length. -
A day set under a month of unknown length is bounded by that month's maximum across years rather than refused, so
{2020,2021}Y2M{1..-1}Dand yearless2M{1..-1}Dresolve. Days that exist in no year (2M30D) and day sets overflowing a concrete month (2026Y9M{28..31}D) are still rejected.
Tempo version 1.5.4
[v1.5.4] — 2026-09-01
Fixed
-
A negative component under a set-valued container (
~o"2026Y{1..12}M-1D", the last day of each month) resolves against each expanded member's own context — leap-aware, per ISO 8601-2 §4.4.1 — instead of leaking an unresolved-1into the materialised intervals. The literal, the recurrence (R12/2026-01-01/P1M/FL-1DN), andTempo.select/2now agree. -
RRULE
BYMONTHexpansion no longer lets DTSTART's day-of-month filter occurrences: whenBYMONTHDAY/BYDAYdetermine the day, results are identical from any DTSTART (a rule anchored on the 31st no longer returns leap-years-only or empty sets), and when nothing later sets the day, the anchor's original day clamps to each occurrence's month — per occurrence, leap-aware, in every calendar (Feb 29 anchors restore the 29th in leap years; Hebrew day-30 anchors track each year's month length; leap months drop in common years).
Tempo version 1.5.3
[v1.5.3] — 2026-08-30
Fixed
-
Tempo.to_elixir/1andto_date_time/1convert offset-grounded values: aZor+10:00value becomes the instant as anEtc/UTCDateTime(matchingDateTime.from_iso8601/1's normalisation), a zoned value keeps its zone instead of degrading to aNaiveDateTime, and a floating value's refusal now says why and points atTempo.in_zone/2. -
A negative UTC offset with minutes (
-03:30) projects as −(3 h 30 m), not −3 h + 30 m — comparisons and conversions for half-hour zones west of Greenwich were off by an hour. -
Tempo.equal?/2compares member extents by instant, so the same moment expressed at different offsets (09:00+05:30vs03:30Z) is equal, agreeing withrelation/2.
Added
Tempo.shift/2,3accepts an ISO 8601 duration string (Tempo.shift(t, "-PT30M")) — iCalendarDURATION/TRIGGER/REPEATvalues shift without a parse at every call site; a string that parses to a non-duration is refused with direction.
Tempo version 1.5.2
[v1.5.2] — 2026-08-26
Fixed
Tempo.equal?/2,3now ignores per-member metadata, honouring its contract: two extent-equal values differing only in metadata (a summary, PRODID) compare equal and agree withTempo.compare/3's:eq. The internal unit-strip was leaving metadata on the struct comparison.
Tempo version 1.5.1
[v1.5.1] — 2026-08-26
Fixed
- An interval whose end omits the higher order components it shares with its start —
2018-01-15/02-20, ISO 8601-1 §5.5.1's own example — now takes those components from the start, as the clause requires.
Changed
inspect/1andTempo.to_iso8601/1render an interval's end without the components it shares with its start, so2026-06-15/2026-06-16prints as~o"2026Y6M15D/16D". This is the form §5.5.1 defines and the one people write; it round-trips unchanged now that the parser expands it.
Tempo version 1.5.0
[v1.5.0] — 2026-08-26
Changed
- Calendrical 1.3 and Astro 2.5 in the lock: every calendar-touching example in the cookbook, guides, and livebooks re-validated by execution; Hebrew and Islamic calendar weeks now parse and materialise through Tempo (
Tempo.from_iso8601("5786-W03", Calendrical.Hebrew)), andbeginning_of_week/1on those calendars lands on their own week start.
Added
-
Tempo.Duration.subtract/2(add/2of the negation) and the missing bang variantsTempo.to_calendar!/2,to_interval_set!/1, andselect!/2, completing the!convention across the conversion and selection API. -
Tempo.week/1completes the component-accessor family for week-axis values (~o"2026Y32W", Hebrew/Islamic calendar weeks, retail weeks): the week number,niloff the week axis, and anArgumentErrorfor an interval spanning more than one week. -
Tempo.RRule.parse/2accepts:duration,:base_toand:metadata— the occurrence-span controlsTempo.RRule.Expander.to_ast/3already had — so a parsedRRULEcan carry a per-occurrence span rather than a granule at its resolution.parse("FREQ=MONTHLY;BYDAY=1WE", from: dtstart, duration: ~o"PT2H")emits two-hour occurrences, the RRULE echo of iCalendar'sDTSTART+DURATION. -
Tempo.IntervalSet.last/1— the latest member interval, ornilwhen empty, pairing withfirst/1. Unlikefirst/1's constant-time peek, it is O(n) and bounded-only: an unbounded set raisesTempo.UnboundedSetErrorrather than walking to find an end that isn't there. -
Tempo.to_calendar/2converts a day-resolution value from its calendar into another viaDate.convert/2—Tempo.to_calendar(~o"2026-06-15", Calendrical.Hebrew)is 30 Tevet 5786, and it round-trips.Tempo.to_calendar/1is now a deprecated alias forTempo.to_elixir/1, the outbound mirror offrom_elixir/2that also converts durations.Tempo.to_calendar/2also takes at:Tempo.Interval.t/0— converting both endpoints and carrying duration, recurrence, repeat rule, unit and metadata across untouched, with an unbounded end left unbounded — and at:Tempo.IntervalSet.t/0, converting each member. Taking the whole span saves the caller pulling it apart and rebuilding it, which silently drops whatever it was carrying.
Fixed
-
Shifting a week-axis value by weeks (
Tempo.shift(~o"2026Y32W", week: 1)) steps weeks natively with year-rollover carry instead of raisingKeyError; component accessors (Tempo.year/1) on single-week intervals no longer crash for the same reason. Month-axis values still take a week as seven days. -
A day duration on a week-axis value computes as a
day_of_weekstep (~o"2026Y32W" + P2Dis~o"2026Y32W3K", carrying across week and year boundaries), and a sub-day duration (PT1H) returns{:error, %Tempo.ResolutionError{}}instead of raisingKeyError— the week axis has no path to an hour slot. -
The tour livebook called
Tempo.precedes?/2(not a Tempo function — the predicate isbefore?/2) and compared a zoned time against a floating window; both cells now run. Theweekend?/2doc describes the actual mechanism: the date converts toCalendar.ISObefore the weekday is read, so Calendrical 1.3's culturally-native weekday numbering (Hebrew/Islamic weeks from Sunday, Persian from Saturday) cannot misclassify a weekend. -
Tempo.trunc/2with:weeknames the day that value's week begins on, at day resolution, instead of silently answering with the month.trunc(~o"2026-08-16", :week)returned~o"2026Y8M"— it walked past:day, found:monthstill coarser than:week, and gave back the month. It now answers~o"2026Y8M10D", taking the week boundary from the value's own calendar so a Sunday-start calendar gives a different day from an ISO one. Units on another axis that name nothing expressible —:day_of_week,:day_of_yearagainst a Gregorian value — return aTempo.ResolutionErrorrather than an unrelated coarser unit. Coarsening within an axis is unchanged. -
Component accessors (
Tempo.year/1,month/1,day/1,hour/1, …) on an interval exactly one granule wide now read the component instead of raising "ambiguous" when the half-open upper bound rolls into a coarser unit — soTempo.month/1of[2026Y12M, 2027Y1M)(December 2026, e.g. fromTempo.select(~o"2026", ~o"-1M")) is12. A genuinely multi-unit span still raises. -
Materialised recurrence occurrences no longer carry the internal
occurrence_duration/occurrence_base_tospan directives in their metadata: they are consumed to size each occurrence and then dropped, so occurrences from aDURATION-bearing rule inspect cleanly instead of showing a spurious metadata key. -
Tempo.to_elixir/1,to_date/1andto_naive_date_time/1now preserve a non-Gregorian value's calendar at the native boundary, mapping only Tempo's internalCalendrical.Gregorianto Elixir'sCalendar.ISO. Previously a Hebrew value converted to aDatemislabelledCalendar.ISOcarrying the Hebrew numbers — a corrupt value; Gregorian conversions are unchanged.