Tempo.Holidays.DayStart (Tempo.Holidays v0.1.0)

Copy Markdown View Source

Projects a sunset-starting calendar day onto the Gregorian timeline.

A day in Tempo is just a day: an Islamic or Hebrew holiday materialises to an in-calendar day value — ~o"1447Y1M1D[u-ca=islamic-umalqura]" — that carries no day-start convention. The midnight-versus-sunset question only arises when that day is projected onto the Gregorian civil timeline, and then the honest answer is a datetime interval that begins when the day begins — sunset, or an 18:00 proxy, the evening before — and ends at the next such boundary. A single day becomes [sunset(D − 1), sunset(D)); a multi-day holiday keeps its half-open span, [sunset of the first evening, sunset that starts the day after the last).

project/3 performs that projection, and Tempo.Holidays.day_start/2 applies it to a set of holiday occurrences. It is deliberately self-contained — the day-start-aware projection of a calendar day belongs in Tempo, and this module is written to lift there with little change.

The two axes

The day_start argument selects a boundary and, optionally, where it is taken:

  • :midnight — no projection; the in-calendar day value is returned unchanged.

  • :evening / :sunset — the 18:00 proxy, or true sunset, at the calendar's canonical reference (Mecca for Islamic dates, Jerusalem for Hebrew).

  • {:evening, location} / {:sunset, location} — the same, at a location of your own: an IANA zone id ("Asia/Kuala_Lumpur") or a {longitude, latitude} point (also a Geo.Point.t/0 / Geo.PointZ.t/0). A point resolves to a zone through tz_world (an optional dependency) for the evening proxy; :sunset computes from the point directly and returns a UTC instant, so it needs no zone database.

Summary

Types

Where a day-start boundary is taken: an IANA zone id or a {longitude, latitude} point.

t()

How a calendar day begins when projected onto the Gregorian timeline.

Functions

Project an in-calendar day interval onto the Gregorian timeline per day_start.

Validate a day start.

Types

location()

@type location() :: String.t() | {number(), number()} | struct()

Where a day-start boundary is taken: an IANA zone id or a {longitude, latitude} point.

t()

@type t() :: :midnight | :evening | :sunset | {:evening | :sunset, location()}

How a calendar day begins when projected onto the Gregorian timeline.

Functions

project(interval, calendar, day_start)

@spec project(Tempo.t(), module(), t()) :: {:ok, Tempo.t()} | {:error, term()}

Project an in-calendar day interval onto the Gregorian timeline per day_start.

Arguments

  • interval is a materialised occurrence, an in-calendar interval of days (an Islamic or Hebrew day, possibly spanning several days).

  • calendar is the occurrence's Calendrical calendar module, used to choose the canonical reference when no location is given.

  • day_start is a t/0 — :midnight (returns interval unchanged), :evening/:sunset, or a {boundary, location} pair.

Returns

  • {:ok, interval} — the projected Gregorian datetime interval, or interval unchanged for :midnight.

  • {:error, {:invalid_day_start, day_start}} for a value that is not a t/0.

  • {:error, reason} — a point was given for an evening projection but no zone could be resolved (tz_world absent or the point is over open water), or the sunset/boundary could not be computed.

Examples

iex> import Tempo.Sigils
iex> {:ok, rule} = Tempo.DateRule.parse("1 Shawwal")
iex> {:ok, days} = Tempo.to_interval(rule, within: ~o"2025")
iex> [day] = Tempo.IntervalSet.members(days)
iex> # :midnight leaves the in-calendar day untouched
iex> {:ok, same} = Tempo.Holidays.DayStart.project(day, Calendrical.Islamic.UmmAlQura, :midnight)
iex> same == day
true
iex> # projected onto Gregorian, the day begins at sunset the evening before
iex> {:ok, projected} = Tempo.Holidays.DayStart.project(day, Calendrical.Islamic.UmmAlQura, :sunset)
iex> {:ok, start} = Tempo.to_elixir(Tempo.Interval.from(projected))
iex> DateTime.to_date(start)
~D[2025-03-29]

validate(boundary)

@spec validate(term()) :: {:ok, t()} | {:error, {:invalid_day_start, term()}}

Validate a day start.

Arguments

  • day_start is the value to check.

Returns

  • {:ok, day_start} for a t/0: a boundary, or a boundary and a location that is a non-empty zone id or a point with a longitude from -180 to 180 and a latitude from -90 to 90.

  • {:error, {:invalid_day_start, day_start}} otherwise.

Examples

iex> Tempo.Holidays.DayStart.validate({:sunset, {101.69, 3.14}})
{:ok, {:sunset, {101.69, 3.14}}}

iex> Tempo.Holidays.DayStart.validate(:dusk)
{:error, {:invalid_day_start, :dusk}}