Tempo.Math (Tempo v0.5.0)

Copy Markdown View Source

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

Advance a %Tempo{} or a keyword-list time representation by exactly one unit at the given resolution.

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

add(tempo, duration)

@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

Returns

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"

add_unit(tempo, unit, calendar)

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_time is either a Tempo.t/0 or the keyword list stored in its :time field.

  • unit is the unit at which to increment. Supported units: :year, :month, :day, :hour, :minute, :second, :week, :day_of_year, :day_of_week.

  • calendar is 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

  • ArgumentError when 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"

subtract(tempo, duration)

@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

Returns

  • A new Tempo.t/0 with 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"

subtract_unit(tempo, unit, calendar)

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_time is a Tempo.t/0 or its time keyword list.
  • unit is the unit to decrement. Same vocabulary as add_unit/3.
  • calendar is 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"

unit_minimum(arg1)

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

  • unit is any time unit atom.

Returns

  • 1 for :month, :day, :week, :day_of_year, and :day_of_week — these count from 1.

  • 0 for 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