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 aGeo.Point.t/0/Geo.PointZ.t/0). A point resolves to a zone throughtz_world(an optional dependency) for the evening proxy;:sunsetcomputes 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.
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
Where a day-start boundary is taken: an IANA zone id or a {longitude, latitude} point.
@type t() :: :midnight | :evening | :sunset | {:evening | :sunset, location()}
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.
Arguments
intervalis a materialised occurrence, an in-calendar interval of days (an Islamic or Hebrew day, possibly spanning several days).calendaris the occurrence's Calendrical calendar module, used to choose the canonical reference when no location is given.day_startis at/0—:midnight(returnsintervalunchanged),:evening/:sunset, or a{boundary, location}pair.
Returns
{:ok, interval}— the projected Gregorian datetime interval, orintervalunchanged for:midnight.{:error, {:invalid_day_start, day_start}}for a value that is not at/0.{:error, reason}— a point was given for an evening projection but no zone could be resolved (tz_worldabsent 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 a day start.
Arguments
day_startis the value to check.
Returns
{:ok, day_start}for at/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}}