Time-unit arithmetic primitives used by enumeration, interval
materialisation (Tempo.to_interval/1), and eventually
Tempo + Duration / Tempo − Duration operations.
The core function is add_unit/3: given a keyword-list time
representation (or a %Tempo{}), advance it by exactly one unit
at a specified resolution, carrying into coarser units as needed.
Carry is calendar-aware — months per year and days per month vary
by calendar, week counts too.
unit_minimum/1 answers "what is the start-of-unit value?" —
used when reasoning about mixed-resolution intervals and when
constructing the lower bound of an implicit span.
The module is kept deliberately minimal and pure: no Tempo struct
construction, no side effects, no exceptions beyond the
ArgumentError raised when a unit has no known carry rule.
Summary
Functions
Add a Tempo.Duration.t/0 to a Tempo.t/0.
Advance a %Tempo{} or a keyword-list time representation by
exactly one unit at the given resolution.
Subtract a Tempo.Duration.t/0 from a Tempo.t/0.
The mirror of add_unit/3: advance a %Tempo{} or keyword-list
time representation backward by exactly one unit at the given
resolution, borrowing from coarser units as needed.
Return the start-of-unit minimum value — used when a trailing unit is unspecified in a mixed-resolution comparison or when constructing the lower bound of an implicit span.
Functions
@spec add(Tempo.t(), Tempo.Duration.t()) :: Tempo.t()
Add a Tempo.Duration.t/0 to a Tempo.t/0.
The duration's components are applied largest-unit-first
(year → month → day → hour → minute → second), with week
components expanded to days (P2W = 14 days). After the
month-level arithmetic, the day field is clamped to the valid
range for the resulting month — so 2022-01-31 + P1M yields
2022-02-28, matching the semantics used by
java.time.LocalDate.plus/2.
Single add operations are atomic: Jan 31 + P1M = Feb 28, but
Jan 31 + P1M + P1M is not the same as Jan 31 + P2M — date
arithmetic is not associative. If you need the "absorb" chained
semantic, do the add in one call with a single P2M duration.
Negative duration components subtract. ~o"P-100D" added to
~o"2022Y1M10D" yields a date 100 days earlier.
The input Tempo must carry every unit referenced by the
duration. If the duration has a :hour component but the Tempo
is at year resolution, the Tempo is extended via
Tempo.extend_resolution/2 first.
Arguments
tempois anyTempo.t/0.durationis anyTempo.Duration.t/0.
Returns
- A new
Tempo.t/0with the duration applied.
Examples
iex> Tempo.Math.add(~o"2022Y1M1D", ~o"P1M")
~o"2022Y2M1D"
iex> Tempo.Math.add(~o"2022Y1M31D", ~o"P1M")
~o"2022Y2M28D"
iex> Tempo.Math.add(~o"2022Y12M31D", ~o"P1D")
~o"2023Y1M1D"
iex> Tempo.Math.add(~o"2022Y1M1D", ~o"P2W")
~o"2022Y1M15D"
Advance a %Tempo{} or a keyword-list time representation by
exactly one unit at the given resolution.
Uses Keyword.replace!/3 (preserves position) rather than
Keyword.put/3 (removes + prepends). Keyword-list order is an
invariant maintained elsewhere in Tempo: compare_time/2,
inspect, and to_iso8601 all depend on it.
Arguments
tempo_or_timeis either aTempo.t/0or the keyword list stored in its:timefield.unitis the unit at which to increment. Supported units::year,:month,:day,:hour,:minute,:second,:week,:day_of_year,:day_of_week.calendaris the calendar module used for calendar-sensitive carry (months per year, days per month, weeks per year).
Returns
- The input with the unit advanced by 1, carrying into coarser
units as needed. Shape matches the input — a
%Tempo{}in yields a%Tempo{}out; a keyword list yields a keyword list.
Raises
ArgumentErrorwhen no increment rule is defined for the requested unit.
Examples
iex> Tempo.Math.add_unit(~o"2022Y12M31D", :day, Calendrical.Gregorian)
~o"2023Y1M1D"
iex> Tempo.Math.add_unit(~o"2022Y6M", :month, Calendrical.Gregorian)
~o"2022Y7M"
@spec subtract(Tempo.t(), Tempo.Duration.t()) :: Tempo.t()
Subtract a Tempo.Duration.t/0 from a Tempo.t/0.
Equivalent to add/2 with every duration component negated.
Month arithmetic still clamps day-of-month at the end.
Arguments
tempois anyTempo.t/0.durationis anyTempo.Duration.t/0.
Returns
- A new
Tempo.t/0with the duration subtracted.
Examples
iex> Tempo.Math.subtract(~o"2022Y3M1D", ~o"P1M")
~o"2022Y2M1D"
iex> Tempo.Math.subtract(~o"2022Y3M31D", ~o"P1M")
~o"2022Y2M28D"
iex> Tempo.Math.subtract(~o"2022Y1M1D", ~o"P1D")
~o"2021Y12M31D"
The mirror of add_unit/3: advance a %Tempo{} or keyword-list
time representation backward by exactly one unit at the given
resolution, borrowing from coarser units as needed.
Used internally by subtract/2 and by any future
backward-walking iteration.
Arguments
tempo_or_timeis aTempo.t/0or its time keyword list.unitis the unit to decrement. Same vocabulary asadd_unit/3.calendaris the calendar module used for borrow lookups.
Returns
- The input with the unit decremented by 1.
Examples
iex> Tempo.Math.subtract_unit(~o"2023Y1M1D", :day, Calendrical.Gregorian)
~o"2022Y12M31D"
iex> Tempo.Math.subtract_unit(~o"2022Y1M", :month, Calendrical.Gregorian)
~o"2021Y12M"
Return the start-of-unit minimum value — used when a trailing unit is unspecified in a mixed-resolution comparison or when constructing the lower bound of an implicit span.
Arguments
unitis any time unit atom.
Returns
1for:month,:day,:week,:day_of_year, and:day_of_week— these count from 1.0for every other unit (including:hour,:minute,:second,:year, and any unrecognised atom).
Examples
iex> Tempo.Math.unit_minimum(:month)
1
iex> Tempo.Math.unit_minimum(:hour)
0