# `Tempo.Holidays.DayStart`
[🔗](https://github.com/elixir-tempo/tempo_holidays/blob/v0.1.0/lib/tempo/holidays/day_start.ex#L1)

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 `t:Geo.Point.t/0` / `t: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.

# `location`

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

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

# `t`

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

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

# `project`

```elixir
@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: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: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`

```elixir
@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: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}}

---

*Consult [api-reference.md](api-reference.md) for complete listing*
