Tempo v0.5.0 - Apri 28th, 2026
Breaking — set operations now match textbook semantics
The named set operations now behave the way the symbols in A ∩ B, A ∖ B, and A △ B read in a textbook: each returns the trimmed instant-level result (covered time). Member-preserving filters — the "give me the whole events that survive" form — moved to explicitly named members_* companions. union/2 is unchanged (the only member-preserving default — coalesce explicitly with IntervalSet.coalesce/1 for the merged-span form).
Tempo.intersection/2now returns the trimmed overlap. Previous member-preserving form isTempo.members_overlapping/2. PreviousTempo.overlap_trim/2is removed —intersection/2does its job.Tempo.difference/2now returns the trimmed remainder (AwithB-shaped holes punched out — possibly splitting anAmember into multiple fragments). Previous member-preserving form isTempo.members_outside/2. PreviousTempo.split_difference/2is removed —difference/2does its job.Tempo.symmetric_difference/2now returns the trimmed non-shared edges of both operands. Previous member-preserving form isTempo.members_in_exactly_one/2.
Migration:
- "What's the overlap?" / "What time is in both?" →
intersection/2(no change in name; behaviour now trimmed). - "Which of these meetings hit the query window?" →
members_overlapping/2(wasintersection/2). - "Workday minus lunch as free-time blocks" / "Free time around busy" →
difference/2(no change in name; behaviour now trimmed). This fixes the previously brokenTempo.difference(workday, lunch)pattern, which used to drop the whole workday. - "Which workdays aren't holidays?" →
members_outside/2(wasdifference/2). The numeric result is the same when eachAmember is either fully covered or fully outside anyBmember (workdays/holidays case), butmembers_outsideis the right name for an event-list question. - Callers of
Tempo.overlap_trim/2→Tempo.intersection/2. - Callers of
Tempo.split_difference/2→Tempo.difference/2.
The motivation: when a user reads Tempo.intersection(japan_trip, enrolled) or Tempo.difference(workday, lunch) aloud, they're describing a covered-time question. The library should return that, not surprise them by collapsing whole members. The member-preserving forms remain available — and clearly named — for the event-list questions where they're the right shape.
Bug Fixes
Tempo.difference/2(formerlysplit_difference/2) no longer emits a zero-width residue interval when anAmember is fully consumed by aBmember and additionalBmembers remain. Surfaced when applying the new instant-leveldifferenceto multi-day workday/holiday set operations; previously masked because the trimmed form was rarely composed against multi-member B sets.
Changes
- Removed
Tempo.Sigilshim (was renamed toTempo.Sigils)
Tempo v0.4.1 - April 25th, 2026
Bug Fixes
- Update
ex_docdependency config to remove possible conflict with calendrical's configuration.
Tempo v0.4.0 - April 25th, 2026
Added
~oin match context. On the left-hand side ofmatch?/2,caseclauses,=, or a function head, the sigil now expands to a structural pattern — prefix-matching the value's:timekeyword list while leaving:calendar,:shift,:extended, and:qualificationunconstrained. Thanks to @am-kantox for the PR.
Tempo v0.3.0 - April 24th, 2026
Added
Tempo.Interval.metadata/1. Named accessor for the:metadatamap on an interval. Mirrorsfrom/1,to/1,endpoints/1, andresolution/1added in v0.2.0, so user-facing code never has to reach into struct fields to read iCalSUMMARY,LOCATION, event UIDs, or any other per-interval data attached by the caller.
Changed
Renamed
Tempo.compare/2andTempo.Interval.compare/2toTempo.relation/2andTempo.Interval.relation/2. The function returns one of 13 Allen interval-algebra relations (:precedes,:meets,:overlaps, …), not the:lt | :eq | :gtshape stdlib'scompare/2promises. The new name avoids the trap.Renamed
Tempo.SigiltoTempo.Sigils(plural), and movedcalendar_from/1out toTempo.Sigils.Options.import Tempo.Sigilsnow brings onlysigil_o/2andsigil_TEMPO/2into scope — no helper functions leak. The oldTempo.Sigilmodule remains as a deprecated compatibility shim and will be removed in a future major version.Tempo.VisualizerandTempo.Visualizer.Standalonenow compile only when both:plugand:banditare available. Previously Plug alone was enough to trigger compilation ofTempo.Visualizer, andStandalonereferencedBanditunguarded — so a downstream application that depended on Tempo without pulling in either library saw "undefined module" warnings during compilation. Both modules still expose stubinit/call/start/child_spec/stopfunctions that raise a single actionable error when called without the deps in place.
Bug Fixes
ISO week-date resolution now uses
Calendrical.ISOWeeksemantics throughout the validation path, regardless of the caller's declared calendar. There is room to be more selective than this (there can be multiple ways to construct a week-based calendar). However there isn't yet a clear way to influence that decision other than through a-u-caqualifier and that only allows ISO Week calendars.Tempo.to_date/1now handles ordinal dates ([year, day]— produced by theOdesignator, the extendedYYYY-DDDform, or by enumerating a year-only Tempo as days) and ISO week dates ([year, week, day_of_week]). Previously both shapes returned aTempo.ConversionErroreven though the components unambiguously identify a single calendar day. Examples:Tempo.to_date(~o"2020-166")now returns{:ok, ~D[2020-06-14]};Tempo.to_date(~o"2020-W24-3")returns{:ok, ~D[2020-06-10]}; and~o"2020Y{1..-1}D" |> Enum.to_list() |> hd() |> Tempo.to_date()returns{:ok, ~D[2020-01-01]}.
Tempo v0.2.0 - April 23rd, 2026
Adds
Tempo.new/1,Tempo.new!/1,Tempo.Interval.new/1,Tempo.Interval.new!/1,Tempo.Duration.new/1,Tempo.Duration.new!/1.Tempo.Interval.spans_leap_second?/1,leap_seconds_spanned/1, andTempo.Interval.duration(iv, leap_seconds: true). Interval-level leap-second detection and an opt-in duration that counts them. Lets scientific pipelines account for exact elapsed time without Tempo accepting23:59:60as a value.Tempo.LeapSeconds.removals/0. Extension point for future negative leap seconds (CGPM agreed in 2022 that they may become necessary from ~2035). Empty today; interval-level helpers already treat insertions and removals uniformly.Tempo.LeapSeconds. The 27 IERS-announced positive leap-second dates from 1972-06-30 through 2016-12-31, exposed asdates/0,on_date?/3, andlatest/0. Drives historical validation of:60seconds.Historical leap-second validation.
23:59:60is now accepted only on the 27 IERS-announced dates. The previous structural check (hour/minute/month-day/offset) remains; a new check rejects:60on any other June 30 or December 31. Error messages point callers atTempo.LeapSeconds.dates/0.Zone-gap parse rejection. A zoned wall time that falls inside a daylight-saving or zone-transition gap (e.g.
2024-03-10T02:30:00[America/New_York]) is now rejected at parse time viaTzdata.periods_for_time/3. DST fall-back ambiguity is accepted; coarser-than-minute values and unzoned values skip the check.Tempo.year/1,month/1,day/1,hour/1,minute/1,second/1. Commodity component accessors for%Tempo{}and%Tempo.Interval{}values. Returnnilwhen the component isn't specified; raiseArgumentErrorwhen called on an interval whose span covers multiple values of that unit.Tempo.Interval.from/1,to/1,endpoints/1,resolution/1. Named endpoint and span-resolution accessors so user-facing code never has to reach into struct fields.Tempo.IntervalSet.count/1,map/2,filter/2. Named helpers that treat the set as a sequence of member intervals — the complement to theEnumerableprotocol, which walks sub-points.Tempo.select/2. Polymorphic composition primitive: narrows a base span (%Tempo{},%Interval{}, or%IntervalSet{}) by a selector (integer lists, ranges,%Tempo{}/%Interval{}projection, or a function). Pure function — no ambient reads. Always returns{:ok, %IntervalSet{}}, composing with the other set ops.Tempo.workdays/1andTempo.weekend/1. Territory-aware day-of-week constructors that return%Tempo{}selector values — composable withTempo.select/2. Accept a territory atom (:US), territory string, locale string ("ar-SA"), or%Localize.LanguageTag{}; default chain isApplication.get_env(:ex_tempo, :default_territory)then ambient locale.workdays(t) ++ weekend(t)partitions the seven days of the week.Tempo.Territory.resolve/1. Normalises a territory, territory string, locale, or language-tag value to a canonical uppercase territory atom. The single resolution chain used byTempo.workdays/1andTempo.weekend/1.Tempo.explain/1. Returns a structured, prose explanation of any Tempo value.Tempo.Explainprovidesto_string/1,to_ansi/1, andto_iodata/1formatters so renderers (the visualizer, terminals, HTML surfaces) can style each tagged part independently.Inspect polish. Zoned Tempos round-trip via the sigil with the
[zone_id]IXDTF trailer.%Tempo.IntervalSet{}inspects as#Tempo.IntervalSet<…>with a preview and metadata summary.%Tempo.Interval{}with non-empty:metadatashows the event summary inline.iCalendar import.
Tempo.ICal.from_ical/2andfrom_ical_file/2parse RFC 5545.icsdata (via the optionalicaldependency) into%Tempo.IntervalSet{}with per-event metadata on each interval. Overlapping events are preserved.Full RFC 5545
RRULEexpansion. EveryBY*rule (BYMONTH,BYMONTHDAY,BYYEARDAY,BYWEEKNO,BYDAYwith and without ordinals,BYHOUR,BYMINUTE,BYSECOND),BYSETPOS,WKST, and theRDATE/EXDATEextras flow through one tagged AST intoTempo.to_interval/2andTempo.RRule.Selection. All 30 RFC 5545 §3.8.5.3 worked examples pass — Thanksgiving, Election Day, Friday-the-13th, first-Saturday-after-first-Sunday, last-weekday-of-month, and the rest. Calendar-aware throughout. Unbounded rules still require:bound.Tempo.RRule.parse/2+Tempo.to_rrule/1. Parse an RFC 5545 RRULE string to the shared AST; round-trip through the encoder preserves every supported field (includingWKSTand BYDAY-with-ordinal as pairs).Tempo.RRule.Expander.expand/3. Thin adapter from%Tempo.RRule.Rule{}or%ICal.Recurrence{}to%Tempo.Interval{}AST, delegating materialisation toTempo.to_interval/2. One interpreter path for every recurrence source.Tempo.to_interval/2. Accepts:bound(for unbounded recurrences). New stream pipelineiterate_recurrence/7is the single expansion loop — boundedn, unboundedUNTIL, and:bound-capped all share it.RDATEadditive andEXDATEsubtractive inTempo.ICal.from_ical/2.final = (expand(rrule) ∪ rdates) − exdates. RDATEs carry the event's span (DTEND − DTSTART); EXDATEs match on the occurrence's start moment viaTempo.Compare.compare_endpoints/2.Metadata on
%Tempo.Interval{}and%Tempo.IntervalSet{}. Free-form:metadatamaps travel through set operations — intersection and difference tag result fragments with the A-operand's metadata; set-level metadata follows the first operand.Set operations.
Tempo.union/2,intersection/2,complement/2,difference/2,symmetric_difference/2, and predicates (disjoint?,overlaps?,subset?,contains?,equal?) on any Tempo value. Results are always%Tempo.IntervalSet{}.Cross-calendar set operations. Operands in different calendars (e.g. Hebrew vs Gregorian) are converted via
Date.convert!/2; the result inherits the first operand's calendar.Midnight-crossing non-anchored intervals.
T23:30/T01:00anchored to day D materialises as[D T23:30, D+1 T01:00); on the pure time-of-day axis, such intervals are split before set-op sweep-line runs.Tempo.anchor/2. Axis composition primitive — combines a date-like value with a time-of-day into a datetime. Not a set operation; used to prepare cross-axis values for set algebra.Tempo.Compare. New shared module withcompare_time/2(start-moment keyword-list comparison, padding missing trailing units with their unit minimum) andto_utc_seconds/1(zone-aware projection viaTzdata, per-call, no cache).Tempo.Math.add/2andsubtract/2. Calendar-aware Tempo-plus-Duration arithmetic with end-of-month day clamping (Jan 31 + P1M = Feb 28,Feb 29 + P1Y = Feb 28). Weeks expand to days; negative components subtract.Non-contiguous mask expansion.
1985-XX-15now materialises to an IntervalSet of 12 day-intervals (the 15th of each month) instead of widening to year. Partial masks (1985-X5-15) narrow to valid candidates.Bounded recurrence and duration-bounded intervals.
R3/1985-01/P1Mexpands to N occurrences;1985-01/P3MandP1M/1985-06materialise to closed intervals viaTempo.Matharithmetic.Enum.to_list/1on a duration-bounded interval now respects the bound instead of running unbounded.%Tempo.IntervalSet{}— multi-interval values. Sorted, list of intervals.to_interval/1now returnsInterval | IntervalSetdepending on expansion; useto_interval_set/1when a uniform shape is wanted.Multi-interval materialisation. Range-in-slot (
{1..3}M), stepped ranges, cartesian ranges, and all-of sets expand to an IntervalSet. One-of sets ([a,b,c]) return an error — they're epistemic disjunctions, not free/busy lists.Unified conversion from Elixir date/time types.
Tempo.from_elixir/2acceptsDate.t,Time.t,NaiveDateTime.t, orDateTime.tand returns a%Tempo{}at an inferred or explicit resolution.Tempo.from_date_time/1. Previously missing forDateTime.t— the existingfrom_date/1,from_time/1,from_naive_date_time/1family now has its fourth member. UTC offset (including DST) populates:shift; the IANA zone name and numeric offset in minutes populate:extended.Tempo.extend_resolution/2* fills finer units with their start-of-unit minimum values up to a target resolution.Tempo.at_resolution/2* dispatches totrunc/2orextend_resolution/2based on whether the target is coarser or finer than the current resolution. Idempotent when the target matches. The single entry point for normalising a Tempo to a known resolution.Implicit-to-explicit interval conversion.
Tempo.to_interval/1andTempo.to_interval!/1materialise any implicit-span%Tempo{}into the equivalent%Tempo.Interval{}.Support the Internet Extended Date/Time Format (IXDTF) as defined in draft-ietf-sedate-datetime-extended-09. An optional suffix such as
[Europe/Paris][u-ca=hebrew]may follow an ISO 8601 datetime.Add an
:extendedfield to%Tempo{}holding%{calendar:, zone_id:, zone_offset:, tags:}parsed from the IXDTF suffix (ornilwhen no suffix is present).Tempo.Iso8601.Tokenizer.tokenize/1now returns{:ok, {tokens, extended_info}}whereextended_infois eithernilor the parsed IXDTF map.Astronomical seasons. ISO 8601-2 season codes 25–28 (Northern) and 29–32 (Southern) now expand to intervals bounded by the relevant March/September equinox and June/December solstice as computed by the
Astrolibrary. Codes 21–24 remain meteorological calendar approximations.Leap-second validation. ISO 8601 permits
second = 60as a positive leap second. Tempo now accepts it only when the minute is 59, the hour is 23, the calendar date (if present) is 30 June or 31 December, and any time-zone offset is zero. All other uses ofsecond = 60are rejected.ISO 8601-2 / EDTF qualification operators. Expression-level
?(uncertain),~(approximate) and%(both) are now parsed. The parsed qualification is carried on the new:qualificationfield of%Tempo{}; the bounded interval semantics of the value are unchanged.EDTF conformance corpus. 200+ valid and invalid strings from the
unt-libraries/edtf-validatecorpus (BSD-3-Clause) are now exercised as ExUnit tests. The known-failure list is tracked intest/tempo/iso8601/edtf_corpus_test.exs.EDTF Level 2 component-level qualification.
?,~and%qualifiers can now appear adjacent to individual date components (2022-?06-15,2022-06?-15,?2022-06-15,%-2011-06-13). The qualification is stored per-component on the new:qualificationsfield of%Tempo{}(a%{unit => qualifier}map). Expression-level qualifiers continue to populate the single:qualificationfield.Per-endpoint qualification in intervals. Each endpoint of an interval may now carry its own qualifier (
1984?/2004~,2019-12/2020%). The qualifier attaches to that endpoint's%Tempo{}struct rather than the interval as a whole.Open-ended intervals.
1985/..,../1985, and../..now parse, along with the equivalent trailing-/leading-slash forms1985/,/1985,/,/..,../. Open endpoints are represented as:undefinedon the%Tempo.Interval{}struct.Unspecified digits in negative years. Strings like
-1XXX-XX,-XXXX-12-XX, and-1X32-X1-X2now parse. The negative sign was previously discarded byform_number, causing a crash inparse_date/1; it is now carried on the mask as a:negativesentinel.EDTF long-year notation.
Y-prefix years with exponent notation (Y17E8,Y-17E7) or significant-digit annotations (Y171010000S3,Y-171010000S2) now parse. Combined with existing support for 4-digitY-prefix years (Y2022) and plain 5+ digit years (Y170000002), this completes Tempo's coverage of the geological-scale year syntax.100% EDTF corpus coverage. The
unt-libraries/edtf-validatecorpus — the only publicly-available conformance test suite we could find for ISO 8601-2 Part 2 — now passes in full. 183 strings exercised, 0 known failures.Web visualizer.
Tempo.Visualizeris aPlug.Routerthat shows a parsed ISO 8601 / ISO 8601-2 / IXDTF string as a large-font echo followed by a component-by-component breakdown.
Changed
tzadded as adev/testdependency and installed as the defaultCalendar.TimeZoneDatabaseinconfig/dev.exsandconfig/test.exs. Required forical2.0 to parseDTSTART;TZID=…properties — without a zone database installed, those events come through withdtstart: niland are silently dropped. Runtime consumers configure their own database (see the README).Internal builder
Tempo.Iso8601.ASTnow owns the token-to-struct conversion path formerly done by a@doc falseTempo.new/2. The old internalnew/2is removed. External callers should have been unaffected (the old function was never public); internal callers in the parser / range / set / interval paths have been rewired.Tempo.Clock.clock/0checksProcess.get({Tempo.Clock, :clock})before falling back to the application env. Lets theNowTest/ToRelativeStringTestsuites installTempo.Clock.Testprocess-locally so the swap doesn't leak into concurrent doctests. Fixes an intermittent CI failure in theutc_now/0/now/1/utc_today/0/today/1doctests when those suites ran interleaved.Leap-second handling is now ecosystem-aligned.
:second = 60is rejected at parse regardless of date (matchesCalendar.ISO,Time, andDateTimein Elixir/OTP). Leap-second information is preserved at the interval level viaspans_leap_second?/1,leap_seconds_spanned/1, andduration(iv, leap_seconds: true).Cross-calendar
Tempo.Interval.duration/1now raisesArgumentErrorwhen endpoints are in different calendars instead of silently computing a garbage value. Error message points at set operations (which handle cross-calendar inputs automatically).Numeric zone offsets now bounded to ±24h. Nonsensical values like
+25:00andZ28Hare rejected at validation; the ISO 8601 grammar still accepts them but the semantic check refuses anything outside a plausible UTC offset.IXDTF
[u-ca=NAME]suffix now swaps the Tempo struct's calendar. Parse routes the atom (e.g.:hebrew,:islamic-umalqura,:ethioaa) throughCalendrical.calendar_from_cldr_calendar_type/1to the correspondingCalendrical.*module. Explicitcalendarargument toTempo.from_iso8601/2still wins over IXDTF.mix.exsdocs structure follows the Localize layout —name:,source_url:,package(),links(),groups_for_modules,groups_for_extras,source_ref. Hex.pm landing page now anchors to the README rather than theTempomodule.Dialyzer build now enforces
:underspecs,:extra_return, and:missing_returnon top of the existing:error_handlingand:unknownflags. All spec mismatches inlib/have been resolved.Removed all CLDR-family dependencies.
ex_cldr_calendarshas been replaced by Calendrical for calendar functionality and byLocalize.Utils.Math/Localize.Utils.Digitsfor numeric helpers.Reduce parser compile time by ~85% (from ~190s to ~28s) and generated BEAM size by ~61% by converting high-fanout NimbleParsec combinators to
defparsecpfunction boundaries. No runtime performance regression.
Bug Fixes
Enumeration of zoned values now honours DST transitions. On the day a zone enters DST, the iterator skips the "missing" wall-clock hour (e.g.
Enum.take(~o"2026-10-04[Australia/Sydney]", 5)yields hours[0, 1, 3, 4, 5]— 02:00 never appears on a Sydney clock face that day). On the day a zone exits DST, the duplicated hour is emitted twice, distinguished by the:shiftfield: the first occurrence with the pre-transition offset, the second with the post-transition offset (per RFC 9557 IXDTF's explicit-offset fold disambiguator). The two emitted Tempos round-trip through the parser and project to distinct UTC instants 3600 seconds apart. Unzoned values and values outside DST transitions are unaffected.Fix parser interpretation of bare
~o"-1M". TheMdesignator was resolving to:minuteinside a time-zone shift ([minute: -1]) instead of:month(time: [month: -1]). Tightenedexplicit_time_shiftto requireZalone orZ-prefixed explicit components; the ambiguous sign-plus-single-unit form now parses as a signed calendar component per ISO 8601-2 §4.4.1.Fix
Tempo.selectwith negative components and week-of-month context.~o"-1M"on a year base now correctly resolves to December;~o"-1D"on a year base to Dec 31 (leap-aware);~o"-1W"on a year base to the last ISO week;~o"1W"on a month base to week-of-month. Week-of-year and week-of-month axes are now kept coherent through theproject_mergepipeline.Fix
Tempo.Inspectfor values with a:day_of_yearcomponent.~o"166O"(day-of-year 166) and its negative-count companion~o"-1O"now render through the ISO 8601-2Odesignator instead of raising a FunctionClauseError inside inspect.Removed
Tempo.Shift(no-op stub that silently dropped shifts) andTempo.Comparison(self-described as "badly wrong" template code with no callers). The one rounding branch that depended onTempo.Shift—round(time_of_day, :day)— now returns a clearTempo.RoundingErrorinstead of crashing.Tempo.Interval.spans_leap_second?/1boundary bug fixed. An interval like[23:59:59Z, next 00:00:00Z)now correctly reportstrue— the leap second 23:59:60Z is within this span under the half-open[from, to)convention. Previously an off-by-one in the containment test missed the boundary case.Tempo.Interval.empty?/1now returnstruefor inverted intervals (from > to), andduration/1returnsPT0Sfor any empty interval. Inverted intervals used to silently produce a negative duration.Explicit numeric offsets now disambiguate DST fall-back correctly.
01:30:00-04:00[America/New_York]and01:30:00-05:00[America/New_York]now resolve to different UTC instants as RFC 9557 §4.5 describes; previously the zone_id won unconditionally and the explicit offset was silently ignored.Tempo.from_iso8601!/1no longer silently overrides IXDTF[u-ca=NAME]withCalendrical.Gregorian. Previously the bang form always passed Gregorian explicitly, which (per the explicit-wins-over-IXDTF rule) nullified the calendar tag; now matches the behaviour ofTempo.from_iso8601/1.%Tempo.Interval{}inspect now preserves each endpoint's IXDTF extended trailer (zone, calendar, tags). Previously the sigil output dropped[zone]and[u-ca=cal]from interval endpoints even though the data was stored on the underlying Tempo values.Spec tightening across the public API to satisfy dialyzer's strict flags. Refined
@specs onTempo.Compare.to_utc_seconds/1,Operationspredicates (disjoint?/overlaps?/subset?/contains?/equal?),RRule.Expander.to_ast/2, andTempo.Interval.resolution/1.Recurrence cadence applies as
DTSTART + i × INTERVAL(scalar multiplication) rather thanisuccessive+ INTERVALsteps. The old iterative approach clamped Feb 29 → Feb 28 at step 1 and never recovered;YEARLYrules anchored on Feb 29 now correctly produce Feb 29 on every leap year.BY-rule EXPAND semantics per RFC 5545 §3.3.10 table.
BYMONTH/BYMONTHDAY/BYYEARDAY/BYWEEKNOexpand whenFREQis coarser than the rule's unit (previously they only filtered). Notes 1 and 2 are honoured —BYDAYdowngrades from EXPAND to LIMIT whenBYMONTHDAY/BYYEARDAYis co-present.DTSTART is always the first materialised occurrence. BY-rule EXPAND can legitimately produce candidates earlier than DTSTART (e.g.
BYMONTHDAY=1withDTSTART=Sep 30also yields Sep 1); those are now dropped by theiterate_recurrenceloop to match the RFC.matches_mask?/2checks digit equality position-by-position. The previous implementation always returnedtruefor concrete digit positions, which silently let non-contiguous year masks like1_6_accept any 4-digit candidate. The dialyzer silencer attached to this function has been removed.Fix compiler warnings around
%NaiveDateTime{}struct updates and unreachable clauses in the set enumerable protocol.Fix
Enum.take/2and related Enumerable operations on values with unspecified-digit year masks.Fix
Enum.take/2on year-month-day masks where the day is unspecified (e.g.1985-XX-XX,1985-12-XX).Tempo.Enumeration.add_implicit_enumeration/1now raises a clearArgumentErrorwhenTempo.Iso8601.Unit.implicit_enumerator/2returnsnil(e.g. trying to enumerate a fully-specified second-resolution datetime — no finer unit exists).Fix group enumeration (
2022Y5G2MU). The{:group, %Range{}}token shape produced by expandednGspanUNITUconstructs now has a matching clause inTempo.Enumeration.do_next/3that unwraps the range into the standard range-iteration path. Previously crashed withno function clause matching in Tempo.Enumeration.do_next/3.Fix selection enumeration (
2022YL1MN). The{:selection, _}clause indo_next/3is now ordered before the genericis_unitclause, which would otherwise match the selection's inner keyword list and destructively iterate it.explicitly_enumerable?/1no longer treats a bare selection as an enumerable shape on its own. The selection tuple is preserved verbatim on every yielded Tempo.Enumerate long-year significant-digit shapes (
1950S2,Y12345S3). Year values tagged{integer, [significant_digits: n]}now iterate over the block of candidate years sharing the leading n digits (1950S2→1900..1999,Y12345S3→12300..12399). Blocks larger than 10,000 candidates raise a clearArgumentErrorrather than hanging — callers who want to refer to a significant-digits year without iterating can still hold the parsed AST. Negative values enumerate in most-negative-first order.Extend
Tempo.Validation.resolve/2's{:year, year}, {:month, months}clause guard to accept%Range{}months. Previously onlyis_list(months) or is_integer(months)was accepted, which meant the implicit month enumerator (1..-1//-1) never conformed againstmonths_in_yearwhen the year was a range value. Enables correct1950S2-style significant-digits enumeration.Implement
Enumerable.Tempo.Interval. Closed intervals and open-upper intervals (1985/..) now iterate forward one resolution-unit at a time from the:fromendpoint; fully-open (../..) and open-lower (../1985) intervals raiseArgumentErrorwith a clear message (no anchor from which to iterate). Iteration honours the half-open[from, to)convention — the upper bound is exclusive, so adjacent intervals concatenate without overlap or gap.Enumeration of
from/durationintervals (1985-01/P3M) andR…/from/durationrecurrence intervals no longer crashes. The upper bound is currently treated as open — iteration proceeds forward from thefromendpoint andEnum.take/2/Stream.take/2are the idiomatic way to halt it. Computing a concrete upper bound fromfrom + durationis tracked separately; until that lands,Enum.to_list/1on such an interval is an infinite sequence (don't do it).duration/tointervals (P1M/1985-06) raise a clearArgumentErrorexplaining that Tempo-Duration subtraction is required to compute the lower bound.Enumeration of closed intervals with mismatched-resolution endpoints (
1985/1986-06,1985-06/1987) now compares endpoints as their concrete start-instants rather than bailing on unit-list length mismatch. Missing trailing units are filled with their unit minimum (:month/:day/:weekfrom 1, everything else from 0), so1985(start = 1985-01-01) correctly sorts before1986-06(start = 1986-06-01) and the interval yields both 1985 and 1986.Extend
Enumerable.Tempo.Intervalincrement rules to cover:week,:day_of_year, and:day_of_weekresolutions. Week-resolution intervals (2022-W05/2022-W08) now advance week-by-week, carrying into the next year atcalendar.weeks_in_year/1.
Tempo v0.1.0
This is the changelog for Tempo v0.1.0 released which was never released.
Enhancements
Add support for steps in set ranges. This is not ISO8601 compliant but is a natural expectation for Elixir. For example
~o"2023Y{1..-1//2}W"says "every second week in 2023".Add
Tempo.round/2to round a Tempo struct to a given resolution.Add
Tempo.to_date/1,Tempo.to_time/1andTempo.to_naive_date_time/1Add
Tempo.to_calendar/1that will convert aTempo.tstruct to the most appropriate native Elixir date, time or naive date time struct.
Bug Fixes
Fix implicit enumeration of standalone months like
~o"3M". The requires an updatedex_cldr_calendarslibrary that supports returning the number of days in the month without a year (returning an error if the result is ambiguous without a year).Many miscellaneous bug fixes.