Skip to content

Time and Time Scales

satkit::Instant is a point in time, stored as integer TAI microseconds since 1970-01-01 00:00:00 TAI, and satkit::Duration is an exact microsecond interval. Arithmetic on them is integer arithmetic that counts leap seconds, and saturates at the ends of the ±292,000-year range (checked_add / checked_sub report overflow instead). The Time Systems tutorial explains the scales and how satkit converts between them; this page shows the Rust API.

Time scales

TimeScale has UTC, TAI, TT, GPS, UT1 and TDB. Calendar constructors take UTC unless they say otherwise (from_datetime_with_scale), and the scale of a Julian date is always explicit: as_mjd_with_scale, as_jd_with_scale, from_mjd_with_scale, from_jd_with_scale, and the _utc shortcuts (as_mjd_utc, from_jd_utc, ...). UT1 uses the Earth orientation table (UT1 = UTC with a warning when it is not available).

Every function in the crate that takes a time is generic over the TimeLike trait, implemented by Instant and, with the chrono feature, by chrono::DateTime.

Parsing and formatting

  • Instant::from_rfc3339 for RFC 3339 / ISO 8601 (2024-06-15T12:00:00.25Z, with or without an offset).
  • Instant::from_string for free-form dates ("June 15 2024 13:30", "2024-06-15 13:30:00"); it tries RFC 3339 first. Locale-ordered numeric dates such as 06/15/2024 are ambiguous and not supported; use strptime for those.
  • Instant::strptime(s, fmt) with an explicit format (%Y %m %d %H %M %S %f %z %b %B).
  • Display and as_rfc3339() print 2024-06-15T12:00:00.000000Z; strftime formats with the same codes plus weekday names.

Parse failures are InstantError.

Example

//! Time scales, arithmetic, parsing and formatting with `satkit::Instant`.
//!
//! Runs offline: the leap-second table is compiled in. UT1 needs the Earth
//! orientation file `finals2000A.all` (downloaded on first use); without it
//! satkit warns once and uses UT1 = UTC.
//!
//! ```text
//! cargo run --example time_scales
//! ```

use satkit::{Duration, Instant, TimeScale};

fn main() -> Result<(), Box<dyn std::error::Error>> {
    // Calendar input is UTC; the seconds are rounded to the microsecond
    let t = Instant::from_datetime(2024, 6, 15, 12, 0, 0.0)?;
    println!("t = {t}");

    // The same instant expressed in each scale, as seconds ahead of UTC
    let mjd_utc = t.as_mjd_with_scale(TimeScale::UTC);
    for scale in [
        TimeScale::TAI,
        TimeScale::TT,
        TimeScale::GPS,
        TimeScale::UT1,
        TimeScale::TDB,
    ] {
        let ahead = (t.as_mjd_with_scale(scale) - mjd_utc) * 86_400.0;
        println!("  {scale:?} - UTC = {ahead:+.4} s");
    }

    // Calendar components interpreted in another scale
    let t_tt = Instant::from_datetime_with_scale(2024, 6, 15, 12, 0, 0.0, TimeScale::TT)?;
    let dt: Duration = t - t_tt; // exact, in integer microseconds
    println!("12:00 UTC - 12:00 TT = {} us", dt.as_microseconds());

    // Arithmetic is exact and counts leap seconds
    let t0 = Instant::from_datetime(2016, 12, 31, 23, 59, 59.0)?;
    let t1 = t0 + Duration::from_seconds(1.0);
    let t2 = t0 + Duration::from_seconds(2.0);
    println!("leap second: {t0} -> {t1} -> {t2}");
    let jan1 = Instant::from_date(2017, 1, 1)?;
    let dec31 = Instant::from_date(2016, 12, 31)?;
    println!("2016-12-31 lasted {} s", (jan1 - dec31).as_seconds());

    // Parsing: RFC 3339, free-form strings, and explicit formats
    let a = Instant::from_rfc3339("2024-06-15T12:00:00.25Z")?;
    let b = Instant::from_string("June 15 2024 13:30")?;
    let c = Instant::strptime("15/06/2024 14:45:10", "%d/%m/%Y %H:%M:%S")?;
    println!("parsed: {a}, {b}, {c}");

    // Formatting
    println!("strftime: {}", c.strftime("%A %d %B %Y, %H:%M")?);
    println!("day of year {}, {}", c.day_of_year(), c.day_of_week());

    // Other representations
    println!("JD (UTC)  = {:.6}", t.as_jd_with_scale(TimeScale::UTC));
    println!("MJD (TT)  = {:.6}", t.as_mjd_with_scale(TimeScale::TT));
    println!("Unix time = {}", t.as_unixtime());
    let g = Instant::from_gps_week_and_second(2318, 561_618.0);
    println!("GPS week 2318, 561618 s = {g}");

    Ok(())
}
$ cargo run --example time_scales
t = 2024-06-15T12:00:00.000000Z
  TAI - UTC = +37.0000 s
  TT - UTC = +69.1840 s
  GPS - UTC = +18.0000 s
  UT1 - UTC = -0.0164 s
  TDB - UTC = +69.1845 s
12:00 UTC - 12:00 TT = 69184000 us
leap second: 2016-12-31T23:59:59.000000Z -> 2016-12-31T23:59:60.000000Z -> 2017-01-01T00:00:00.000000Z
2016-12-31 lasted 86401 s
parsed: 2024-06-15T12:00:00.250000Z, 2024-06-15T13:30:00.000000Z, 2024-06-15T14:45:10.000000Z
strftime: Saturday 15 June 2024, 14:45
...
GPS week 2318, 561618 s = 2024-06-15T12:00:00.000000Z

The UT1 line depends on the Earth orientation table; everything else is fixed by the compiled-in leap-second table.

chrono interoperability

With the chrono feature, Instant and chrono::DateTime convert both ways with From / Into, exactly to the microsecond, and a chrono::DateTime can be passed directly to any function that takes a time. chrono has no leap seconds: an instant inside one (23:59:60.5) converts to 23:59:59.5, as Unix time does.

//! Converting between `satkit::Instant` and `chrono::DateTime`.
//!
//! Needs the `chrono` feature (off by default):
//!
//! ```text
//! cargo run --example chrono_interop --features chrono
//! ```

use chrono::{DateTime, TimeZone, Utc};
use satkit::{Duration, Instant, TimeScale};

fn main() -> Result<(), Box<dyn std::error::Error>> {
    // chrono -> satkit, exact to the microsecond (any time zone)
    let dt = Utc.with_ymd_and_hms(2024, 6, 15, 12, 0, 0).unwrap();
    let t = Instant::from(dt);
    println!("from chrono: {t}");

    // satkit -> chrono
    let back: DateTime<Utc> = (t + Duration::from_hours(1.5)).into();
    println!("to chrono:   {back}");

    // Every satkit function that takes a time is generic over `TimeLike`,
    // which `chrono::DateTime<Utc>` implements, so it can be passed as is
    let tt = satkit::TimeLike::as_mjd_with_scale(&dt, TimeScale::TT);
    let pos = satkit::lpephem::sun::pos_gcrf(&dt);
    println!("MJD (TT) = {tt:.6}, Sun distance = {:.4e} m", pos.norm());

    // chrono has no leap seconds: 23:59:60 maps to 23:59:59, as Unix time does
    let leap = Instant::from_datetime(2016, 12, 31, 23, 59, 60.5)?;
    let leap_chrono: DateTime<Utc> = leap.into();
    println!("{leap} -> {leap_chrono}");

    Ok(())
}
$ cargo run --example chrono_interop --features chrono
from chrono: 2024-06-15T12:00:00.000000Z
to chrono:   2024-06-15 13:30:00 UTC
MJD (TT) = 60476.500801, Sun distance = 1.5196e11 m
2016-12-31T23:59:60.500000Z -> 2016-12-31 23:59:59.500 UTC

See also