# `Sidereon.GNSS.Time`
[🔗](https://github.com/neilberkman/sidereon-ex/blob/main/lib/sidereon/gnss/time.ex#L1)

Epoch conversions shared by the GNSS correction wrappers.

These helpers turn an Elixir `NaiveDateTime` or a
`{{year, month, day}, {hour, minute, second}}` tuple into the two
representations the `sidereon-core` crate consumes:

  * a split Julian date `{jd_whole, fraction}` where `jd_whole` is the `*.5`
    midnight boundary of the civil day and `fraction` is the within-day part
    (the same convention the SP3 reader uses);
  * integer or continuous seconds since the J2000 epoch (JD 2451545.0), used
    by NIF calls that consume either exact product epoch axes or fractional
    receive times.

No leap-second shifting is applied: the epoch stays in the time scale the
caller supplied it in (typically GPS time for these models).

# `leap_second_table`

```elixir
@type leap_second_table() :: %{
  source: String.t(),
  first_mjd: integer(),
  last_mjd: integer(),
  entries: non_neg_integer()
}
```

# `time_scale`

```elixir
@type time_scale() :: atom() | String.t()
```

A time scale, named either by atom (`:gpst`, `:utc`, `:glonasst`, ...) or by
its uppercase abbreviation string (`"GPST"`, `"UTC"`, ...).

# `ut1_coverage`

```elixir
@type ut1_coverage() :: %{
  source: String.t(),
  first_mjd: integer(),
  last_mjd: integer(),
  first_jd_tt: float(),
  last_jd_tt: float(),
  entries: non_neg_integer()
}
```

# `day_of_year`

```elixir
@spec day_of_year(NaiveDateTime.t() | tuple()) :: float()
```

Fractional day-of-year of the epoch, as the `float` the Niell troposphere
seasonal term consumes.

January 1 00:00 is 1.0. The continuous day-of-year comes from
`sidereon_core::astro::time::civil::day_of_year`, matching the crate's
fractional `SolveInputs.day_of_year` convention, so the SPP troposphere and
`Sidereon.GNSS.Troposphere` agree for the same epoch.

# `epoch_to_j2000_seconds`

```elixir
@spec epoch_to_j2000_seconds(NaiveDateTime.t() | tuple()) ::
  {:ok, integer()} | {:error, term()}
```

Convert an epoch to integer seconds since the J2000 epoch (JD 2451545.0).

A whole-second epoch yields an exact integer (the core returns the exact
whole-second value, which is converted back to an integer here). Returns
`{:ok, seconds}` or `{:error, :non_integer_second_epoch}` if the epoch carries
a sub-second part. The continuous seconds come from
`sidereon_core::astro::time::civil::j2000_seconds`.

# `epoch_to_j2000_seconds_fractional`

```elixir
@spec epoch_to_j2000_seconds_fractional(NaiveDateTime.t() | tuple()) ::
  {:ok, float()} | {:error, term()}
```

Convert an epoch to continuous floating-point seconds since J2000.

Unlike `epoch_to_j2000_seconds/1`, this accepts sub-second `NaiveDateTime`
values and tuple epochs with a floating-point seconds field. Delegates to
`sidereon_core::astro::time::civil::j2000_seconds`.

# `epoch_to_split_jd`

```elixir
@spec epoch_to_split_jd(NaiveDateTime.t() | tuple()) :: {float(), float()}
```

Convert an epoch to the split Julian date `{jd_whole, fraction}`.

The calendar arithmetic lives in `sidereon-core`
(`sidereon_core::astro::time::civil::split_julian_date`); this module only
marshals the epoch into civil `(year, month, day, hour, minute, second)`
fields.

# `gps_utc_offset_s`

```elixir
@spec gps_utc_offset_s(integer(), integer(), integer()) :: float()
```

GPS minus UTC, in seconds, in effect at a UTC calendar date.

This is the IS-GPS-200 quantity broadcast in the navigation message (the leap
seconds a GPS receiver applies): 18 s from 2017. It is `leap_seconds/3` minus
the constant 19 s `TAI - GPST`, so it is the value to use whenever you mean
"GPS - UTC", not `leap_seconds/3` (which is `TAI - UTC`).

# `leap_second_table_info`

```elixir
@spec leap_second_table_info() :: leap_second_table()
```

Provenance and coverage of the embedded leap-second table.

# `leap_seconds`

```elixir
@spec leap_seconds(integer(), integer(), integer()) :: float()
```

TAI minus UTC, in seconds, in effect at a UTC calendar date.

Delegates to `sidereon_core::astro::time::scales::julian_day_number` and
`sidereon_core::astro::time::scales::find_leap_seconds`.

# `leap_seconds_batch`

```elixir
@spec leap_seconds_batch([{integer(), integer(), integer()}]) :: [float()]
```

TAI minus UTC for a list of UTC calendar dates.

Each date is `{year, month, day}` and each result delegates to the same core
functions as `leap_seconds/3`.

# `second_of_day`

```elixir
@spec second_of_day(NaiveDateTime.t() | tuple()) :: float()
```

Seconds-of-day in `[0, 86400)`, formed from the epoch's clock fields.

Used by the Klobuchar diurnal term, which takes the GPS second-of-day
directly. The arithmetic delegates to
`sidereon_core::astro::time::civil::second_of_day`.

# `tai_utc_offset_s`

```elixir
@spec tai_utc_offset_s(integer(), integer(), integer()) :: float()
```

TAI minus UTC, in seconds, in effect at a UTC calendar date.

The unambiguously named alias of `leap_seconds/3` (the IERS Bulletin C
quantity, 37 s from 2017); it returns the identical value. Use
`gps_utc_offset_s/3` for the GNSS "GPS - UTC" offset instead.

# `time_scales`

```elixir
@spec time_scales() :: [atom()]
```

The supported time-scale atoms.

# `timescale_offset`

```elixir
@spec timescale_offset(time_scale(), time_scale()) ::
  {:ok, float()} | {:error, term()}
```

Fixed inter-system time offset `to - from`, in seconds.

Returns the value that, added to a reading in the `from` scale, yields the
`to`-scale reading of the same instant. Defined only for the atomic scales
(TAI/TT/GPST/GST/QZSST/BDT) whose mutual offset is a constant.

Returns `{:error, {:epoch_required, scale}}` for the UTC-based scales (UTC and
GLONASST) whose offset carries the leap-second count (use
`timescale_offset_at/3` with an epoch), and `{:error, {:unsupported, "TDB"}}`
for TDB (its offset from TT is an epoch-dependent periodic term).

    iex> Sidereon.GNSS.Time.timescale_offset(:gpst, :tai)
    {:ok, 19.0}

    iex> Sidereon.GNSS.Time.timescale_offset(:gpst, :utc)
    {:error, {:epoch_required, "UTC"}}

# `timescale_offset_at`

```elixir
@spec timescale_offset_at(time_scale(), time_scale(), number()) ::
  {:ok, float()} | {:error, term()}
```

Leap-aware inter-system time offset `to - from`, in seconds, at `utc_jd`.

`utc_jd` is the UTC Julian date of the instant; it only affects the result
when `from` or `to` is UTC-based (UTC/GLONASST), resolving the leap-second
count. For purely atomic pairs it is ignored and the result matches
`timescale_offset/2`.

    iex> {:ok, off} = Sidereon.GNSS.Time.timescale_offset_at(:glonasst, :utc, 2_451_545.0)
    iex> Float.round(off, 1)
    -10800.0

# `ut1_coverage_info`

```elixir
@spec ut1_coverage_info() :: ut1_coverage()
```

Provenance and coverage of the embedded UT1/EOP table.

# `utc_instant_split`

```elixir
@spec utc_instant_split(NaiveDateTime.t() | tuple()) ::
  {:ok, {float(), float()}} | {:error, term()}
```

Validated UTC instant for an epoch, as the split Julian date `{jd_whole, fraction}`.

Delegates to `sidereon_core::astro::time::model::Instant::from_utc_civil`, the
entry the ionosphere/troposphere delay dispatchers build their `epoch` argument
from. Unlike `epoch_to_split_jd/1`, this runs the core's `JulianDateSplit`
guard, so an out-of-day clock field is rejected as `{:error, :invalid_instant}`
rather than producing an out-of-range fraction.

    iex> {:ok, {jd_whole, _fraction}} =
    ...>   Sidereon.GNSS.Time.utc_instant_split({{2020, 6, 25}, {12, 0, 0}})
    iex> jd_whole
    2_459_025.5

---

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