A scale-tagged instant at the IONEX boundary.
sidereon-core holds an instant either as a split Julian date or as exact
integer nanoseconds, and the two are not interchangeable without loss. An
epoch here names which one it carries, and nothing converts between them or
rounds in either direction:
%Epoch{time_scale: "UTC", jd_whole: 2_459_024.0, jd_fraction: 0.5}%Epoch{time_scale: "UTC", j2000_nanos: 646_228_800_000_000_000}
Both examples are 2020-06-24 00:00:00, in the same scale.
The nanosecond origin is J2000, not Unix
j2000_nanos counts nanoseconds from the J2000 epoch, JD 2451545.0 in the
instant's own scale, which is the origin the core reads this representation
against: it divides the count by 1_000_000_000 and uses the result directly
as the J2000 second of the map. So j2000_nanos: 0 is JD 2451545.0, and a
count before it is negative. It is not a Unix timestamp; feeding Unix
nanoseconds here lands roughly thirty years late.
The standalone Sidereon.GNSS.Ionosphere.TecGrid is the other convention: its
epochs_ns axis and its unix_nanos query argument are Unix nanoseconds,
because that is what its core type carries. The two surfaces keep their own
origins and neither is converted into the other here.
Exactly one representation is populated. The IONEX parser builds its map epochs as split Julian dates, so a parsed product reads back as split Julian dates; a product built from nanosecond epochs reads back as the same integer nanosecond count it was given.
The IONEX epoch axis is whole seconds, so a product built from a count that is
not a whole number of seconds is refused with :epoch_not_representable
rather than rounded onto it.
What is refused
{:invalid_field, "fraction", "must be within one residual day"}- a split Julian date whose fraction is outside[-1, 1], with the core's own field and reason.{:value_out_of_range, :j2000_nanos, value}- a count past the signed 128-bit range the boundary carries it in.{:value_out_of_range, :jd_whole, value}or{:value_out_of_range, :jd_fraction, value}- a split Julian date part that is an integer larger in magnitude than the largest finite double, which has no double to be read onto.{:invalid_epoch_field, field, value}- fromfrom_civil/2, ayear,month,day,hourorminutethat is not an integer, or asecondthat is not a number. The shared calendar helper takes the first five as 32-bit integers, and anhourarrived at by division is a float, since Elixir's/always gives one.{:value_out_of_range, field, value}- fromfrom_civil/2, ayear,month,day,hourorminutethat is an integer outside the 32-bit range, or an integersecondlarger in magnitude than the largest finite double. ANaiveDateTimeis checked the same way: its year can be past the 32-bit range.:ambiguous_epoch_representation- both representations are populated, or neither is.:bad_epoch- the value is not anEpoch.
Time scales
time_scale is the core's own abbreviation. The boundary reads all eleven
scales the core names: "UTC", "TAI", "TT", "TCG", "TDB", "TCB",
"GPST", "GST", "BDT", "GLONASST" and "QZSST". Any other string is
reported as an unknown scale rather than replaced with a default. IONEX
products are UTC throughout, so a parsed product's epochs are "UTC"; the
scale is carried because the core instant carries it, and a product built from
samples is tagged with whatever the caller states.
There is no default epoch and no default scale: from_civil/2 takes the scale
it tags the result with.
Summary
Functions
An epoch from a civil NaiveDateTime or {{y, m, d}, {h, min, s}} tuple, as
the split Julian date of that calendar instant, tagged with time_scale.
An epoch held as exact integer nanoseconds since J2000 (JD 2451545.0) in
time_scale.
An epoch held as the split Julian date jd_whole + jd_fraction in time_scale.
Types
Functions
@spec from_civil(String.t(), NaiveDateTime.t() | tuple()) :: {:ok, t()} | {:error, term()}
An epoch from a civil NaiveDateTime or {{y, m, d}, {h, min, s}} tuple, as
the split Julian date of that calendar instant, tagged with time_scale.
The scale is the caller's statement about the calendar fields; this function
does not shift the instant between scales. A fractional second is carried
into the Julian date fraction as given.
Returns {:ok, epoch}, or {:error, reason} when a field has no value of
the kind the shared calendar helper takes, as listed under "What is refused"
in this module's documentation. Unlike the other constructors here, this one
converts when it is called, because the calendar arithmetic runs in the core,
so its refusals are returned here rather than when the epoch is converted.
Examples
{:ok, epoch} = Epoch.from_civil("UTC", {{2020, 6, 24}, {0, 0, 0}})
{:error, {:invalid_epoch_field, :hour, 2.0}} =
Epoch.from_civil("UTC", {{2020, 6, 24}, {7200 / 3600, 0, 0}})
An epoch held as exact integer nanoseconds since J2000 (JD 2451545.0) in
time_scale.
The count is carried to the core as the integer it is, with no scaling and no rounding. Zero is JD 2451545.0 itself and a count before it is negative.
Examples
# 2000-01-01 12:00:00 UTC, the J2000 epoch itself
Epoch.j2000_nanos("UTC", 0)
# one hour before it
Epoch.j2000_nanos("UTC", -3_600_000_000_000)
An epoch held as the split Julian date jd_whole + jd_fraction in time_scale.
The two parts are kept as they were given, integer or float, and are read onto the doubles the boundary takes when the epoch is converted, so an integer no double holds is a named refusal of the build rather than an exception out of this constructor.