Skip to content
SiderustPublic

About

A Rust crate for high-precision, type-safe time computations. Provides strongly-typed time units, durations, and arithmetic with compile-time guarantees, enabling robust temporal calculations for scientific and systems software.

Resources

Stars

2 stars

Watchers

1 watching

Forks

Repository files navigation

tempoch

Crates.io Docs Code Quality

Typed astronomical time primitives for Rust.

tempoch is built around a few deliberate modeling choices:

  • A Time<S, F> value is an instant on a scale-specific axis with a typed external format, not a bare f64.
  • Time instants are modeled with affine semantics: time_a - time_b yields a duration, while shifting an instant is time + seconds.
  • The canonical internal carrier is a split (hi, lo) pair of J2000-based seconds so epoch-sized values can retain sub-second precision through conversions and arithmetic.
  • JD, MJD, J2000s, Unix, and GPS are views or transport encodings, not alternate storage backends.
  • UTC keeps its civil meaning, but internally it is stored on a continuous instant axis and interpreted through the active UTC-TAI data tables.

tempoch provides:

  • Time<S, F> instants parameterized by a physical or civil scale (TT, TAI, UTC, UT1, TDB, TCG, TCB).
  • Unified target-based conversions:
    • .to::<TT>(), .to::<UTC>(), .to::<TDB>() for infallible scale routes
    • .to_with::<UT1>(&ctx) for context-backed UT1 routes, or .try_to::<UT1>() shorthand (snapshots active data at call time)
    • .to::<JD>() / .to::<MJD>() for infallible coordinates on continuous scales; UTC uses .to_with::<MJD>(&ctx) (or JD) followed by try_raw_with(&ctx)
    • .try_to::<Unix>() and .to::<GPS>() for transport encodings
  • Period<S, F = J2000s> aliases Interval<Time<S, F>> and supports the same to, try_to, and to_with target conversions as Time<S, F>. TimeSeries<S, F = J2000s> produces matching typed instants over a half-open range.
  • JulianDate<S>, ModifiedJulianDate<S>, UnixTime, and GpsTime implement Into<Time<_>> (same instant as .to_j2000s()), so default-format Period::<S>::new(...) / try_new(...) accept encoded endpoints directly. Period<S, MJD> takes MJD-tagged endpoints without retagging.
  • UTC conversion through chrono, leap-second aware over the official history (1961-01-01 onward). Requests for dates before the UTC standard was defined return ConversionError::UtcBeforeDefinition by default; opt in to the approximate segment back-extrapolation by building your context with TimeContext::new().allow_pre_definition_utc().
  • Automatic ΔT = TT - UT1 handling for UT1 conversions via an explicit TimeContext. For the currently compiled bundle fetched 2026-04-18, the default monthly-ΔT path stays within 15 ms of the bundled daily IERS-derived path over the observed overlap, and within 0.2 s over the compiled short-range prediction overlap. Opt into TimeContext::with_builtin_eop() when you want the highest-fidelity bundled UT1 path; raw EOP values are available under tempoch::eop and bracketed by the public EOP_START_MJD / EOP_OBSERVED_END_MJD / EOP_END_MJD constants.
  • TT↔TDB conversion via the built-in seven-term Fairhead–Bretagnon approximation from USNO Circular 179. The crate documents about 10 µs accuracy only inside the public interval bracketed by [tdb_tt_model_high_accuracy_start_jd] / [tdb_tt_model_high_accuracy_end_jd] in [tempoch::constats] (day literals also exposed as TDB_TT_MODEL_HIGH_ACCURACY_*_JD_DAY; roughly 1600-01-01 to 2200-01-01 TT).
  • Julian Day and Modified Julian Day coordinates via JD and MJD on each scale's time axis. For UTC these are civil fractional-day coordinates using 86,400 nominal seconds per day (POSIX style); for TAI, TT, and other continuous scales they remain coordinates on that scale's continuous axis. UTC conversion requires TimeContext because it uses UTC-TAI data, and a leap-second instant returns ConversionError::InvalidLeapSecond since it cannot be uniquely represented by a POSIX-style day count. The internal compensated J2000-second instant storage is unchanged.
  • Unix/POSIX timestamps via UnixTime::try_new(sec).map(Into::into) and .try_to::<Unix>().
  • GPS transport values via GpsTime::try_new(sec).map(Into::into) and .to::<GPS>().
  • Compiled time-data tables generated from official UTC-TAI and Delta T sources.
  • Optional serde support for Time<S, F> as the format's scalar quantity and Period<S, F> / Interval<T> as {start, end} objects, plus explicit tempoch::tagged::{TaggedTime, TaggedPeriod} wrappers when the payload must carry the scale name and compensated split seconds. UTC JD/MJD scalars use civil coordinates and report conversion failures as Serde errors. Each Serde operation captures a fresh default TimeContext; the time-data snapshot is not stored in the payload. For reproducible conversion with a fixed context, serialize time.try_raw_with(&ctx)? and decode the quantity with Time::try_new_with(raw, &ctx).
  • Automatic runtime freshness backed by a cached time-data bundle, while keeping the same public API.
  • Public typed epochs under tempoch::constats: [Time] helpers such as [j2000_jd_tt], [qtty::Day] literals such as [J2000_JD_TT_DAY], offsets like [TT_MINUS_TAI], and the ΔT horizon marker [DELTA_T_PREDICTION_HORIZON_MJD] (qtty::Day).
  • A utility Interval<T> type for half-open time ranges over Time<S, F>, with intersection, normalization, validation, and complement helpers.

Compiled Tables And Official References

The generated tables under tempoch-time-data/src/generated/ are tied to explicit authoritative sources:

Generated table Purpose Official reference Canonical upstream
UTC_TAI_SEGMENTS UTC-to-TAI history, including pre-1972 rate segments and post-1972 leap-second steps IERS Bulletin C / UTC-TAI.history https://hpiers.obspm.fr/eoppc/bul/bulc/UTC-TAI.history
MODERN_DELTA_T_POINTS Compiled modern ΔT = TT - UT1 series USNO observed monthly deltat.data plus USNO predicted deltat.preds https://maia.usno.navy.mil/ser7/deltat.data and https://maia.usno.navy.mil/ser7/deltat.preds
EOP_POINTS Daily Earth-orientation parameters used by the bundled UT1/EOP path IERS combined Bulletin A + C04 file finals2000A.all https://datacenter.iers.org/data/9/finals2000A.all

MODERN_DELTA_T_POINTS is a derived bundle in this crate: the observed USNO monthly series is concatenated with C0-adjusted USNO predictions, and the observed/predicted boundary is exposed through MODERN_DELTA_T_OBSERVED_END_MJD.

Storage model: Time<S> stores a compensated (hi, lo) pair of seconds since J2000 TT on the target axis. Tags such as JD, MJD, Unix, and GPS are conversion targets, not storage types.

Design Decisions

Time<S> is an affine point, not a scalar

tempoch treats an instant as a point on a time axis. That is why the API is deliberately shaped around:

  • time_a - time_b -> duration
  • time + duration -> time
  • time - duration -> time

and does not model "adding two instants" as a meaningful operation.

Internally, this is represented with affn::SplitPoint1, which keeps the affine semantics aligned with the rest of the geometry model instead of re-introducing scalar-like mistakes through ad hoc time arithmetic.

Why time uses split storage

Astronomical epochs are large in absolute value, but many important corrections are tiny:

  • leap-second boundaries
  • TT-TAI offsets
  • TT-TDB corrections
  • UT1 adjustments
  • sub-second transport roundtrips

Using one f64 for "seconds since epoch" would lose low-order precision once a small correction is combined with a value on the order of 1e9 seconds. tempoch therefore stores every Time<S> as a normalized compensated pair (hi, lo) whose sum is the represented instant. The high part carries the epoch-sized component; the low part preserves the small remainder.

The crate reuses affn::SplitQuantity / SplitPoint1 to preserve this pair. Scalar Serde encodings project it into the selected format's f64 coordinate; tagged::TaggedTime<S> preserves both components in its payload.

Why J2000 seconds are canonical

The crate exposes Julian Day, Modified Julian Day, Unix, and GPS helpers, but those are public coordinate or transport views. The storage choice remains canonical J2000-based seconds because it gives one precise internal axis for arithmetic and scale conversion, while still allowing JD/MJD-style APIs at the boundary.

Format vs Scale

Two orthogonal phantom-type axes drive all typed time values in tempoch:

  • Scale — the physical time axis: TT, TAI, UTC, UT1, TDB, TCG, TCB. A scale tells you which reference frame's clock is ticking.
  • Format — how an instant is numerically encoded: JD (Julian Day), MJD (Modified Julian Day), J2000s (SI seconds since J2000), Unix (POSIX seconds), GPS (days since GPS epoch). A format tells you how you read the number off the wire.

The two are always kept separate:

  • [Time<S>] defaults to tag [J2000s] and stores the compensated J2000-second pair on scale S; [reinterpret] selects another [TimeFormat] view without changing storage.
  • [EncodedTime<S, F>] (and its aliases JulianDate<S>, ModifiedJulianDate<S>, etc.) pair a scale with a format for I/O.
  • Crate epochs are expressed either as [Time<S, F>] via helpers in [tempoch::constats] (for example [j2000_jd_tt]) or as canonical plain [qtty::Day] / [qtty::Second] facts such as [J2000_JD_TT_DAY], [UNIX_EPOCH_JD_DAY], and [GPS_EPOCH_TAI_MINUS_UTC].
  • Generic unit facts such as Julian years and Julian centuries live in qtty (qtty::time::JULIAN_YEAR, qtty::time::JULIAN_CENTURY). Derived epoch coordinates such as Unix MJD or GPS epoch JD(TAI) are available through typed helpers instead of public scalar constants.

The type parameter order is always Scale first, Format second — matching the reading direction "JD on TT" → [EncodedTime<TT, JD>] / [JulianDate<TT>].

UTC is a civil scale with leap-second labeling, so it cannot be treated as a simple uniform scalar everywhere. tempoch keeps the internal instant storage continuous, then maps that stored instant to civil UTC through the active UTC-TAI table. That separation lets the crate support both:

  • precise instant arithmetic and transport encodings
  • leap-second-aware civil conversions

UTC JD/MJD values now follow that same civil mapping. Construct with Time::<UTC, MJD>::try_new_with(day, &ctx) and encode with time.to_with::<MJD>(&ctx)?.try_raw_with(&ctx)? (substitute JD as needed). The context controls time-data selection and the pre-1961 UTC policy. Previous code such as Time::<UTC, MJD>::new(value), time.to::<MJD>().raw(), and ModifiedJulianDate::<UTC>::new(value) must migrate to these fallible APIs. UTC JD/MJD .raw() and .new() are intentionally unavailable; other continuous scales retain their infallible operations.

Migration example for a numeric UTC MJD field:

// Before: interpreted the number on the continuous stored axis.
let utc = ModifiedJulianDate::<UTC>::new(mjd);
let mjd_back = utc.to::<MJD>().raw();

// After: decode and encode the civil UTC day count with one context snapshot.
let ctx = TimeContext::new();
let utc = Time::<UTC, MJD>::try_new_with(Day::new(mjd), &ctx)?;
let mjd_back = utc.to_with::<MJD>(&ctx)?.try_raw_with(&ctx)?;

The compiled modern ΔT series runs through MJD 63871 (2033-10-01). Beyond that date the built-in bundle stops and UT1 conversions fail with ConversionError::Ut1HorizonExceeded unless an active runtime bundle extends the horizon. Use the exported DELTA_T_PREDICTION_HORIZON_MJD typed qtty::Day constant to reference the compiled boundary programmatically.

Installation

[dependencies]
tempoch = "0.10"

Enable serde if you want to serialize typed times and periods:

[dependencies]
tempoch = { version = "0.10", features = ["serde"] }

The serde feature composes with the ordinary runtime refresh behavior:

[dependencies]
tempoch = { version = "0.10", features = ["serde", "runtime-data-fetch"] }

no_std

tempoch is written against core / alloc. Default builds enable std.

# no_std + alloc (including bare-metal targets)
tempoch = { version = "0.10", default-features = false, features = ["alloc"] }

Bundled IERS tables come from siderust-archive 0.1.5+, which is itself no_std + alloc when its std feature is off. Runtime download (runtime-data-fetch) still requires std.

Serde

With the serde feature enabled:

  • Time<S, F> serializes as the format's scalar quantity; Time<S> uses J2000 seconds.
  • Period<S, F> serializes as {"start": ..., "end": ...} using F for each endpoint.
  • The scale remains type-level and is not embedded in the payload.
  • tagged::TaggedTime<S, F> and tagged::TaggedPeriod<S, F> carry an explicit "scale" field and compensated split J2000 seconds for interchange. F is only a typed view; it does not alter the tagged wire encoding.
  • UTC JD/MJD Serde captures fresh default time data per operation and returns Serde errors for leap-second encoding, pre-definition UTC, and unusable coordinates. Serialize an explicitly converted quantity when a fixed TimeContext is required.
use qtty::Second;
use tempoch::{
    tagged::{TaggedPeriod, TaggedTime},
    J2000Seconds, Period, TT,
};

let tt = J2000Seconds::<TT>::try_new(Second::new(42.5)).unwrap();
let period = Period::<TT>::new(
    tt,
    J2000Seconds::<TT>::try_new(Second::new(43.5)).unwrap(),
);

assert_eq!(serde_json::to_string(&tt).unwrap(), "42.5");
assert_eq!(
    serde_json::to_string(&period).unwrap(),
    r#"{"start":42.5,"end":43.5}"#
);
assert_eq!(
    serde_json::to_string(&TaggedTime(tt)).unwrap(),
    r#"{"scale":"TT","hi":42.5,"lo":0.0}"#
);
assert_eq!(
    serde_json::to_string(&TaggedPeriod(period)).unwrap(),
    r#"{"scale":"TT","start":{"scale":"TT","hi":42.5,"lo":0.0},"end":{"scale":"TT","hi":43.5,"lo":0.0}}"#
);

Quick Start

use chrono::Utc;
use tempoch::{JD, MJD, Time, TT, UTC};

let utc_now = Time::<UTC>::from_chrono(Utc::now());
let tt_now: Time<TT> = utc_now.to::<TT>();

println!("UTC       : {}", utc_now.to_chrono().unwrap());
println!("TT in JD  : {:.9}", tt_now.to::<JD>());
println!("TT in MJD : {:.9}", tt_now.to::<MJD>());

Period Operations

Time<S, F> models an instant; Period<S, F> is the half-open Interval<Time<S, F>>; TimeSeries<S, F> steps over matching instants. All three default to F = J2000s. The format is a phantom tag, not another storage layout.

use qtty::Day;
use tempoch::{ModifiedJulianDate, Period, TT, JD, MJD};

let day = Period::<TT, MJD>::new(
  ModifiedJulianDate::<TT>::try_new(Day::new(61_000.0)).unwrap(),
  ModifiedJulianDate::<TT>::try_new(Day::new(61_001.0)).unwrap(),
);
let a = vec![
  Period::<TT, MJD>::new(
    ModifiedJulianDate::<TT>::try_new(Day::new(61_000.1)).unwrap(),
    ModifiedJulianDate::<TT>::try_new(Day::new(61_000.4)).unwrap(),
  ),
  Period::<TT, MJD>::new(
    ModifiedJulianDate::<TT>::try_new(Day::new(61_000.6)).unwrap(),
    ModifiedJulianDate::<TT>::try_new(Day::new(61_000.9)).unwrap(),
  ),
];
let b = vec![
  Period::<TT, MJD>::new(
    ModifiedJulianDate::<TT>::try_new(Day::new(61_000.2)).unwrap(),
    ModifiedJulianDate::<TT>::try_new(Day::new(61_000.3)).unwrap(),
  ),
  Period::<TT, MJD>::new(
    ModifiedJulianDate::<TT>::try_new(Day::new(61_000.7)).unwrap(),
    ModifiedJulianDate::<TT>::try_new(Day::new(61_000.8)).unwrap(),
  ),
];

let jd_day: Period<TT, JD> = day.to::<JD>();
assert_eq!(jd_day.to::<MJD>(), day);

let overlap = Period::intersect_many(&a, &b);
let gaps = day.complement(&a);

assert_eq!(overlap.len(), 2);
assert_eq!(gaps.len(), 3);

Examples

  • cargo run --example 01_quickstart
  • cargo run --example 02_scales
  • cargo run --example 03_formats
  • cargo run --example 04_periods
  • cargo run --example 05_serde --features serde
  • cargo run -p tempoch --example 06_runtime_tables
  • cargo run --example 07_conversions

Runtime Time Data

tempoch automatically prefers a cached runtime bundle for fresher UTC-TAI history, modern Delta T, and daily IERS EOP while keeping the public API unchanged. TimeContext and Time::to_with consult a cached bundle in ~/.tempoch/data, refreshing it once on first use when the cache is missing, invalid, or older than 24 hours. Data-dependent shorthand methods (e.g. try_to::<UT1>(), try_to::<Unix>(), try_to_chrono()) snapshot a fresh TimeContext internally. For reproducible pipelines, use the _with variants with an explicit context.

Set TEMPOCH_DATA_DIR to override the cache location.

For a runnable example that uses the ordinary API with runtime refresh, run:

cargo run -p tempoch --example 06_runtime_tables
use qtty::{Day, Second};
use tempoch::{JD, JulianDate, Time, TimeContext, UnixTime, Unix, TT, UT1, UTC};

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let ctx = TimeContext::with_builtin_eop();
    let tt: Time<TT> = JulianDate::<TT>::try_new(Day::new(2_460_000.25))?.into();
    let ut1: Time<UT1> = tt.to_with::<UT1>(&ctx)?;

    let unix = UnixTime::try_new(Second::new(1_700_000_000.0)).map(Into::into)?;
    let back = unix.try_to::<Unix>()?;

    println!("UT1 JD     : {:.9}", ut1.to::<JD>());
    println!("Unix roundtrip: {:.3}", back);
    Ok(())
}

Time Data Updates

The compile-time path still uses checked-in generated tables in tempoch-time-data. The dedicated Rust CLI tempoch-time-data-updater regenerates those committed files from the official UTC-TAI, Delta T, and IERS finals2000A.all sources. Its fetch/parse/build pipeline now reuses the same shared support crate that powers runtime refresh. The updater intentionally keeps only render/write orchestration; parser and bundle-building logic is centralized in tempoch-time-data to avoid runtime/compile-time drift. To refresh locally:

cargo run -p tempoch-time-data-updater
cargo test --all-features

To verify manually that the committed generated files are still in sync with upstream:

cargo run -p tempoch-time-data-updater -- --check

A scheduled GitHub Actions workflow runs the refresh automatically every Monday at 05:23 UTC and pushes the resulting commit directly to main when the generated tables or their source hashes change. GitHub cron schedules are defined in UTC.

Tests and Coverage

cargo test --all-targets
cargo test --doc
cargo +nightly llvm-cov --workspace --all-features --doctests --summary-only

Coverage is gated in CI at >= 90% line coverage.

License

AGPL-3.0-only

About

A Rust crate for high-precision, type-safe time computations. Provides strongly-typed time units, durations, and arithmetic with compile-time guarantees, enabling robust temporal calculations for scientific and systems software.

Resources

Stars

2 stars

Watchers

1 watching

Forks

Releases

Packages

Contributors

Languages