Typed astronomical time primitives for Rust.
tempoch provides:
Time<S>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.try_to::<UT1>()for the default monthly-ΔT UT1 route.to_with::<UT1>(&ctx)for context-backed UT1 routes.to::<JD>(),.to::<MJD>(),.to::<J2000s>()for coordinate views.try_to::<UnixSecs>()and.to::<GpsSecs>()for transport encodings
- UTC conversion through
chrono, leap-second aware over the official history, with an approximate pre-1961 continuation of the first official UTC segment for older civil labels. - Automatic
ΔT = TT - UT1handling forUT1conversions via an explicitTimeContext. For the currently compiled bundle fetched 2026-04-18, the default monthly-ΔT path stays within 10 ms of the bundled daily IERS-derived path over the observed overlap through 2026-04-16, and within 0.2 s over the compiled short-range prediction overlap through 2027-04-24. Opt intoTimeContext::with_builtin_eop()when you want the highest-fidelity bundled UT1 path; raw EOP values are available undertempoch::eopand bracketed by the publicEOP_START_MJD/EOP_OBSERVED_END_MJD/EOP_END_MJDconstants. - 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
constats::TDB_TT_MODEL_HIGH_ACCURACY_START_JD→constats::TDB_TT_MODEL_HIGH_ACCURACY_END_JDinterval (about 1600-01-01 to 2200-01-01 TT). - Julian Day, Modified Julian Day, and SI-second views via
JD,MJD, andJ2000sconversion targets on every built-in scale, including UTC's stored instant axis. - Unix/POSIX timestamps via
Time::<UTC>::from_unix_secondsand.try_to::<UnixSecs>(). - GPS transport values via
Time::<TAI>::from_gps_secondsand.to::<GpsSecs>(). - Compiled time-data tables generated from official UTC-TAI and Delta T sources.
- Optional
serdesupport forTime<S>as{"hi","lo"}andPeriod<S>/Interval<T>as{start, end}objects, plus explicittempoch::tagged::{TaggedTime, TaggedPeriod}wrappers when the payload must carry the scale name. - Automatic runtime freshness backed by a cached time-data bundle, while keeping the same public API.
- Public typed epoch/offset constants under
tempoch::constats, such asJ2000_JD_TT,TT_MINUS_TAI, andDELTA_T_PREDICTION_HORIZON_MJD. - A utility
Interval<T>type for half-open time ranges overTime<A>, with intersection, normalization, validation, and complement helpers.
Storage model: Time<S> stores a compensated (hi, lo) pair of seconds
since J2000 TT on the target axis. Tags such as JD, MJD, UnixSecs, and
GpsSecs are conversion targets, not storage types.
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.
[dependencies]
tempoch = "0.4"Enable serde if you want to serialize typed times and periods:
[dependencies]
tempoch = { version = "0.4", features = ["serde"] }The serde feature composes with the ordinary runtime refresh behavior:
[dependencies]
tempoch = { version = "0.4", features = ["serde"] }With the serde feature enabled:
Time<S>serializes as{"hi": ..., "lo": ...}.Period<S>serializes as{"start": ..., "end": ...}.- The scale remains type-level and is not embedded in the payload.
tagged::TaggedTime<S>andtagged::TaggedPeriod<S>serialize with an explicit"scale"field for interchange payloads.
use qtty::Second;
use tempoch::{
tagged::{TaggedPeriod, TaggedTime},
Period, Time, TT,
};
let tt = Time::<TT>::from_j2000_seconds(Second::new(42.5)).unwrap();
let period = Period::<TT>::new(42.5, 43.5);
assert_eq!(serde_json::to_string(&tt).unwrap(), r#"{"hi":42.5,"lo":0.0}"#);
assert_eq!(
serde_json::to_string(&period).unwrap(),
r#"{"start":{"hi":42.5,"lo":0.0},"end":{"hi":43.5,"lo":0.0}}"#
);
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}}"#
);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>().value());
println!("TT in MJD : {:.9}", tt_now.to::<MJD>().value());use qtty::Day;
use tempoch::{complement_within, intersect_periods, Period, Time, TT};
let day = Period::<TT>::new(
Time::<TT>::from_modified_julian_days(Day::new(61_000.0)).unwrap(),
Time::<TT>::from_modified_julian_days(Day::new(61_001.0)).unwrap(),
);
let a = vec![
Period::<TT>::new(
Time::<TT>::from_modified_julian_days(Day::new(61_000.1)).unwrap(),
Time::<TT>::from_modified_julian_days(Day::new(61_000.4)).unwrap(),
),
Period::<TT>::new(
Time::<TT>::from_modified_julian_days(Day::new(61_000.6)).unwrap(),
Time::<TT>::from_modified_julian_days(Day::new(61_000.9)).unwrap(),
),
];
let b = vec![
Period::<TT>::new(
Time::<TT>::from_modified_julian_days(Day::new(61_000.2)).unwrap(),
Time::<TT>::from_modified_julian_days(Day::new(61_000.3)).unwrap(),
),
Period::<TT>::new(
Time::<TT>::from_modified_julian_days(Day::new(61_000.7)).unwrap(),
Time::<TT>::from_modified_julian_days(Day::new(61_000.8)).unwrap(),
),
];
let overlap = intersect_periods(&a, &b);
let gaps = complement_within(day, &a);
assert_eq!(overlap.len(), 2);
assert_eq!(gaps.len(), 3);cargo run --example 01_quickstartcargo run --example 02_scalescargo run --example 03_formatscargo run --example 04_periodscargo run --example 05_serde --features serdecargo run -p tempoch --example 06_runtime_tablescargo run --example 07_conversions
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, Time::try_to::<UT1>(), Time::to_with, and the
normal UTC civil helpers consult a cached bundle in ~/.tempoch/data,
refreshing it once on first use when the cache is missing, invalid, or older
than 24 hours.
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_tablesuse tempoch::{JD, UnixSecs, Time, TimeContext, TT, UT1, UTC};
fn main() -> Result<(), Box<dyn std::error::Error>> {
let ctx = TimeContext::with_builtin_eop();
let tt = Time::<TT>::from_julian_days(2_460_000.25.into())?;
let ut1: Time<UT1> = tt.to_with::<UT1>(&ctx)?;
let unix = Time::<UTC>::from_unix_seconds(1_700_000_000.0.into())?;
let back = unix.try_to::<UnixSecs>()?;
println!("UT1 JD : {:.9}", ut1.to::<JD>().value());
println!("Unix roundtrip: {:.3}", back.value());
Ok(())
}The compile-time path still uses checked-in generated tables in tempoch-core.
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-featuresTo verify that the committed generated files are still in sync with upstream (this is also enforced in CI):
cargo run -p tempoch-time-data-updater -- --checkA scheduled GitHub Actions workflow runs the refresh automatically and pushes
the resulting commit directly to main when the generated tables or their
source hashes change.
cargo test --all-targets
cargo test --doc
cargo +nightly llvm-cov --workspace --all-features --doctests --summary-onlyCoverage is gated in CI at >= 90% line coverage.
AGPL-3.0-only