diff --git a/CHANGELOG.md b/CHANGELOG.md index 6b51577..4ad0bf7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,15 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project follows [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [0.6.4] - 2026-06-01 + +### Changed + +- Refreshed the vendored `qtty-cpp` integration to the latest patch release, + including the corrected angular trig helpers and the `Ratio` FFI unit. +- Kept the `tempoch` workspace aligned with the new patch-level `qtty` API + and dependency versions used by the vendored adapter crates. + ## [0.6.3] - 2026-05-30 ### Removed diff --git a/Cargo.lock b/Cargo.lock index 0de59eb..82e5eeb 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -638,9 +638,9 @@ dependencies = [ [[package]] name = "qtty" -version = "0.8.4" +version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ad9c85853b4435eb62be239f8cd0e8740ad67b65245c165d68004672c0eb1da" +checksum = "311988508597047d023edfb10cf4299b22a317be2b2c95745aaa4d489d9a9265" dependencies = [ "qtty-core", "qtty-derive", @@ -648,9 +648,9 @@ dependencies = [ [[package]] name = "qtty-core" -version = "0.8.4" +version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0977cb3d61376379cdb24781f2d1c53ad09fbadcfc925813703442b84249a97" +checksum = "dc07e03ed9d81d4b4494b661635dc87f0a1a38cdce3e51136c39af6887716b02" dependencies = [ "libm", "qtty-derive", @@ -660,9 +660,9 @@ dependencies = [ [[package]] name = "qtty-derive" -version = "0.8.4" +version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "593b4191ff7f54e773a9560a22b24851383ca56b35c6fbdb6fb30cdcf9be96c2" +checksum = "7ee2574d42b017be334315c8cf4fdb241d728b093c5c7aba9bb1e7c5a3a708e8" dependencies = [ "proc-macro2", "quote", @@ -1016,7 +1016,7 @@ dependencies = [ [[package]] name = "tempoch" -version = "0.6.3" +version = "0.6.4" dependencies = [ "chrono", "qtty", @@ -1026,7 +1026,7 @@ dependencies = [ [[package]] name = "tempoch-core" -version = "0.6.3" +version = "0.6.4" dependencies = [ "affn", "chrono", @@ -1039,7 +1039,7 @@ dependencies = [ [[package]] name = "tempoch-ffi" -version = "0.6.3" +version = "0.6.4" dependencies = [ "cbindgen", "chrono", diff --git a/README.md b/README.md index 8aafe0e..eeea649 100644 --- a/README.md +++ b/README.md @@ -183,21 +183,21 @@ the horizon. Use the exported `DELTA_T_PREDICTION_HORIZON_MJD` typed ```toml [dependencies] -tempoch = "0.6.2" +tempoch = "0.6.4" ``` Enable `serde` if you want to serialize typed times and periods: ```toml [dependencies] -tempoch = { version = "0.6.2", features = ["serde"] } +tempoch = { version = "0.6.4", features = ["serde"] } ``` The `serde` feature composes with the ordinary runtime refresh behavior: ```toml [dependencies] -tempoch = { version = "0.6.2", features = ["serde", "runtime-data-fetch"] } +tempoch = { version = "0.6.4", features = ["serde", "runtime-data-fetch"] } ``` ## Serde diff --git a/tempoch-core/Cargo.toml b/tempoch-core/Cargo.toml index 167d74d..4945839 100644 --- a/tempoch-core/Cargo.toml +++ b/tempoch-core/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "tempoch-core" -version = "0.6.3" +version = "0.6.4" edition = "2021" authors = ["VPRamon "] description = "Core astronomical time primitives for tempoch." @@ -19,7 +19,7 @@ path = "src/lib.rs" [dependencies] affn = { version = "0.7" } chrono = "0.4.43" -qtty = { version = "0.8.4", features = ["julian-time", "astro"] } +qtty = { version = "0.8.5", features = ["julian-time", "astro"] } serde = { version = "1.0", default-features = false, features = ["derive", "alloc"], optional = true } siderust-archive = { version = "0.1", features = ["time"] } diff --git a/tempoch-ffi/Cargo.toml b/tempoch-ffi/Cargo.toml index b0c80ca..05158e4 100644 --- a/tempoch-ffi/Cargo.toml +++ b/tempoch-ffi/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "tempoch-ffi" -version = "0.6.3" +version = "0.6.4" edition = "2021" authors = ["VPRamon "] description = "C FFI bindings for tempoch — astronomical time primitives." @@ -13,9 +13,9 @@ name = "tempoch_ffi" crate-type = ["cdylib", "staticlib", "rlib"] [dependencies] -tempoch = { path = "../tempoch", version = "0.6.3" } -tempoch-core = { path = "../tempoch-core", version = "0.6.3" } -qtty = "0.8.4" +tempoch = { path = "../tempoch", version = "0.6.4" } +tempoch-core = { path = "../tempoch-core", version = "0.6.4" } +qtty = "0.8.5" qtty-ffi = "0.8" chrono = "0.4.44" diff --git a/tempoch-ffi/include/tempoch_ffi.h b/tempoch-ffi/include/tempoch_ffi.h index 6d52c88..30440b2 100644 --- a/tempoch-ffi/include/tempoch_ffi.h +++ b/tempoch-ffi/include/tempoch_ffi.h @@ -111,6 +111,16 @@ enum tempoch_scale_tag_t TEMPOCH_SCALE_TAG_T_TCG = 5, // Barycentric Coordinate Time. TEMPOCH_SCALE_TAG_T_TCB = 6, + // Ephemeris Time (NAIF/SPICE compatibility marker; routes through TDB). + TEMPOCH_SCALE_TAG_T_ET = 7, + // GPS System Time (`TAI − 19 s`). + TEMPOCH_SCALE_TAG_T_GPST = 8, + // Galileo System Time (`TAI − 19 s`). + TEMPOCH_SCALE_TAG_T_GST = 9, + // BeiDou Navigation Satellite System Time (`TAI − 33 s`). + TEMPOCH_SCALE_TAG_T_BDT = 10, + // Quasi-Zenith Satellite System Time (aligned with GPST). + TEMPOCH_SCALE_TAG_T_QZSST = 11, }; #ifndef __cplusplus #if __STDC_VERSION__ >= 202311L @@ -157,6 +167,28 @@ typedef struct tempoch_eop_values_t { uint8_t ut1_observed; } tempoch_eop_values_t; +// Split J2000-second instant on a scale-specific axis. +typedef struct tempoch_time_t { + // High part of the compensated J2000-second pair. + double hi_seconds; + // Low residual part of the compensated J2000-second pair. + double lo_seconds; +} tempoch_time_t; + +// Decomposed GNSS week-number form since the constellation's defined epoch. +// +// Mirrors [`tempoch::GnssWeek`]. The week number is *full* (no rollover); +// `seconds_of_week` lies in `[0, 604_800)` and `subsecond_nanos` in +// `[0, 1_000_000_000)`. +typedef struct TempochGnssWeek { + // Full week number since the constellation's epoch (no rollover applied). + uint32_t week; + // Seconds since the start of `week`, in `[0, 604_800)`. + uint32_t seconds_of_week; + // Subsecond nanoseconds remainder, in `[0, 1_000_000_000)`. + uint32_t subsecond_nanos; +} TempochGnssWeek; + // A time period expressed in Modified Julian Date, suitable for C interop. typedef struct tempoch_period_mjd_t { // Start of the period (MJD). @@ -165,13 +197,23 @@ typedef struct tempoch_period_mjd_t { double end_mjd; } tempoch_period_mjd_t; -// Split J2000-second instant on a scale-specific axis. -typedef struct tempoch_time_t { - // High part of the compensated J2000-second pair. - double hi_seconds; - // Low residual part of the compensated J2000-second pair. - double lo_seconds; -} tempoch_time_t; +// Validity horizons of the active time-data bundle, in MJD (UTC days). +// +// EOP horizon fields are `NaN` when no EOP data is loaded. +typedef struct TempochDataHorizons { + // First MJD covered by the EOP series, or `NaN` when no EOP is loaded. + double eop_start_mjd; + // Last observed (non-predicted) EOP MJD, or `NaN` when no EOP is loaded. + double eop_observed_end_mjd; + // Last EOP MJD including predictions, or `NaN` when no EOP is loaded. + double eop_end_mjd; + // Last MJD with observed ΔT in the archive-provided modern table. + double modern_delta_t_observed_end_mjd; + // Last MJD covered by the ΔT prediction table. + double delta_t_prediction_horizon_mjd; + // Source of the active bundle as a [`TempochTimeDataSource`] discriminant. + int32_t source; +} TempochDataHorizons; // UTC date-time breakdown for C interop. typedef struct tempoch_utc_t { @@ -240,6 +282,29 @@ extern "C" { // Last MJD with modern observed ΔT data (post-1955 atomic-clock era). double tempoch_const_modern_delta_t_observed_end_mjd(void); +// Constant TT − TAI offset, in seconds (32.184). + double tempoch_const_tt_minus_tai_seconds(void); + +// Number of nanoseconds in one SI second (1e9). + double tempoch_const_nanos_per_second(void); + +// IAU time-scale epoch T0 as a Julian Day on the TT axis (1977-01-01 TAI). + double tempoch_const_iau_time_epoch_t0_jd(void); + +// First JD(TT) of the high-accuracy TDB−TT model validity window. + double tempoch_const_tdb_tt_model_high_accuracy_start_jd(void); + +// Last JD(TT) of the high-accuracy TDB−TT model validity window. + double tempoch_const_tdb_tt_model_high_accuracy_end_jd(void); + +// ΔT = TT − UT1 in seconds for a UT1 Julian Day, using the compiled USNO +// model. Returns NaN when the requested epoch is outside the model domain. + double tempoch_delta_t_seconds(double jd_ut1); + +// ΔT = TT − UT1 in seconds for a UT1 Julian Day, extrapolating with the +// long-term parabola beyond the tabulated range (always finite). + double tempoch_delta_t_seconds_extrapolated(double jd_ut1); + // Create a default context backed by the monthly ΔT model. // // # Safety @@ -280,6 +345,24 @@ tempoch_status_t tempoch_context_allow_pre_definition_utc(const struct tempoch_c // `out` must be a valid, writable pointer to `TempochEopValues`. tempoch_status_t tempoch_eop_at(double mjd_utc, struct tempoch_eop_values_t *out); +// Decompose a GNSS-scale split instant into its week-number form. +// +// # Safety +// `out` must be a valid, writable pointer to `TempochGnssWeek`. + +tempoch_status_t tempoch_time_to_gnss_week(struct tempoch_time_t value, + int32_t scale, + struct TempochGnssWeek *out); + +// Build a GNSS-scale split instant from a week-number decomposition. +// +// # Safety +// `out` must be a valid, writable pointer to `TempochTime`. + +tempoch_status_t tempoch_time_from_gnss_week(struct TempochGnssWeek value, + int32_t scale, + struct tempoch_time_t *out); + // Create a new MJD period. Returns `InvalidPeriod` when an endpoint is NaN or `start_mjd > end_mjd`. // // # Safety @@ -429,6 +512,12 @@ tempoch_status_t tempoch_period_list_normalize(const struct tempoch_period_mjd_t struct tempoch_period_mjd_t **out, uintptr_t *out_count); +// Capture the active time-data status into `out`. +// +// # Safety +// `out` must be a valid, writable pointer to `TempochDataHorizons`. + tempoch_status_t tempoch_time_data_status(struct TempochDataHorizons *out); + // Validate and normalize a split J2000-second pair. // // # Safety diff --git a/tempoch-ffi/src/constats.rs b/tempoch-ffi/src/constats.rs index 344cbfb..e96d419 100644 --- a/tempoch-ffi/src/constats.rs +++ b/tempoch-ffi/src/constats.rs @@ -9,8 +9,11 @@ //! canonical typed helpers. use tempoch::{ - DELTA_T_PREDICTION_HORIZON_MJD, GPS_EPOCH_JD_UTC_DAY, GPS_EPOCH_TAI_MINUS_UTC, J2000_JD_TT_DAY, - MODERN_DELTA_T_OBSERVED_END_MJD, UNIX_EPOCH_JD_DAY, UTC_DEFINED_FROM_MJD_DAY, + delta_t_seconds, delta_t_seconds_extrapolated, DELTA_T_PREDICTION_HORIZON_MJD, + GPS_EPOCH_JD_UTC_DAY, GPS_EPOCH_TAI_MINUS_UTC, IAU_TIME_EPOCH_T0_JD_DAY, J2000_JD_TT_DAY, + MODERN_DELTA_T_OBSERVED_END_MJD, NANOS_PER_SECOND, TDB_TT_MODEL_HIGH_ACCURACY_END_JD_DAY, + TDB_TT_MODEL_HIGH_ACCURACY_START_JD_DAY, TT_MINUS_TAI, UNIX_EPOCH_JD_DAY, + UTC_DEFINED_FROM_MJD_DAY, }; /// J2000.0 epoch as JD(TT) — 2 451 545.0. @@ -98,6 +101,52 @@ pub extern "C" fn tempoch_const_modern_delta_t_observed_end_mjd() -> f64 { MODERN_DELTA_T_OBSERVED_END_MJD.value() } +/// Constant TT − TAI offset, in seconds (32.184). +#[no_mangle] +pub extern "C" fn tempoch_const_tt_minus_tai_seconds() -> f64 { + TT_MINUS_TAI.value() +} + +/// Number of nanoseconds in one SI second (1e9). +#[no_mangle] +pub extern "C" fn tempoch_const_nanos_per_second() -> f64 { + NANOS_PER_SECOND as f64 +} + +/// IAU time-scale epoch T0 as a Julian Day on the TT axis (1977-01-01 TAI). +#[no_mangle] +pub extern "C" fn tempoch_const_iau_time_epoch_t0_jd() -> f64 { + IAU_TIME_EPOCH_T0_JD_DAY.value() +} + +/// First JD(TT) of the high-accuracy TDB−TT model validity window. +#[no_mangle] +pub extern "C" fn tempoch_const_tdb_tt_model_high_accuracy_start_jd() -> f64 { + TDB_TT_MODEL_HIGH_ACCURACY_START_JD_DAY.value() +} + +/// Last JD(TT) of the high-accuracy TDB−TT model validity window. +#[no_mangle] +pub extern "C" fn tempoch_const_tdb_tt_model_high_accuracy_end_jd() -> f64 { + TDB_TT_MODEL_HIGH_ACCURACY_END_JD_DAY.value() +} + +/// ΔT = TT − UT1 in seconds for a UT1 Julian Day, using the compiled USNO +/// model. Returns NaN when the requested epoch is outside the model domain. +#[no_mangle] +pub extern "C" fn tempoch_delta_t_seconds(jd_ut1: f64) -> f64 { + delta_t_seconds(qtty::Day::new(jd_ut1)) + .map(|s| s.value()) + .unwrap_or(f64::NAN) +} + +/// ΔT = TT − UT1 in seconds for a UT1 Julian Day, extrapolating with the +/// long-term parabola beyond the tabulated range (always finite). +#[no_mangle] +pub extern "C" fn tempoch_delta_t_seconds_extrapolated(jd_ut1: f64) -> f64 { + delta_t_seconds_extrapolated(qtty::Day::new(jd_ut1)).value() +} + #[cfg(test)] mod tests { use super::*; diff --git a/tempoch-ffi/src/gnss.rs b/tempoch-ffi/src/gnss.rs new file mode 100644 index 0000000..d102ea3 --- /dev/null +++ b/tempoch-ffi/src/gnss.rs @@ -0,0 +1,206 @@ +// SPDX-License-Identifier: AGPL-3.0-only +// Copyright (C) 2026 Vallés Puig, Ramon + +//! GNSS week-number decomposition exposed over the C ABI. +//! +//! Mirrors [`tempoch::GnssWeek`] and the `Time::::to_gnss_week` / +//! `Time::::from_gnss_week` conversions, which are defined only for the GNSS +//! coordinate scales (`GPST`, `GST`, `BDT`, `QZSST`). Any other scale tag is +//! rejected with [`TempochStatus::InvalidScaleId`]. + +use crate::catch_panic; +use crate::error::TempochStatus; +use crate::typed::{status_from_conversion, TempochScaleTag, TempochTime}; +use qtty::Second; +use tempoch::ConversionError; +use tempoch::{GnssWeek, Time, BDT, GPST, GST, QZSST}; + +/// Decomposed GNSS week-number form since the constellation's defined epoch. +/// +/// Mirrors [`tempoch::GnssWeek`]. The week number is *full* (no rollover); +/// `seconds_of_week` lies in `[0, 604_800)` and `subsecond_nanos` in +/// `[0, 1_000_000_000)`. +#[repr(C)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct TempochGnssWeek { + /// Full week number since the constellation's epoch (no rollover applied). + pub week: u32, + /// Seconds since the start of `week`, in `[0, 604_800)`. + pub seconds_of_week: u32, + /// Subsecond nanoseconds remainder, in `[0, 1_000_000_000)`. + pub subsecond_nanos: u32, +} + +macro_rules! with_gnss_scale { + ($tag:expr, $Scale:ident, $body:block) => { + match $tag { + TempochScaleTag::GPST => { + type $Scale = GPST; + $body + } + TempochScaleTag::GST => { + type $Scale = GST; + $body + } + TempochScaleTag::BDT => { + type $Scale = BDT; + $body + } + TempochScaleTag::QZSST => { + type $Scale = QZSST; + $body + } + _ => return TempochStatus::InvalidScaleId, + } + }; +} + +/// Decompose a GNSS-scale split instant into its week-number form. +/// +/// # Safety +/// `out` must be a valid, writable pointer to `TempochGnssWeek`. +#[no_mangle] +pub unsafe extern "C" fn tempoch_time_to_gnss_week( + value: TempochTime, + scale: i32, + out: *mut TempochGnssWeek, +) -> TempochStatus { + catch_panic!(TempochStatus::InternalPanic, { + if out.is_null() { + return TempochStatus::NullPointer; + } + let Some(scale) = TempochScaleTag::from_raw(scale) else { + return TempochStatus::InvalidScaleId; + }; + let result = with_gnss_scale!(scale, S, { + Time::::try_from_raw_j2000_seconds_split( + Second::new(value.hi_seconds), + Second::new(value.lo_seconds), + ) + .and_then(|t| t.to_gnss_week()) + }); + match result { + Ok(gw) => { + let encoded = TempochGnssWeek { + week: gw.week.value(), + seconds_of_week: gw.seconds_of_week.value(), + subsecond_nanos: gw.subsecond_nanos.value(), + }; + // SAFETY: `out` was checked for null and the safety contract + // requires it to point to writable `TempochGnssWeek` storage. + unsafe { *out = encoded }; + TempochStatus::Ok + } + Err(err) => status_from_conversion(err), + } + }) +} + +/// Build a GNSS-scale split instant from a week-number decomposition. +/// +/// # Safety +/// `out` must be a valid, writable pointer to `TempochTime`. +#[no_mangle] +pub unsafe extern "C" fn tempoch_time_from_gnss_week( + value: TempochGnssWeek, + scale: i32, + out: *mut TempochTime, +) -> TempochStatus { + catch_panic!(TempochStatus::InternalPanic, { + if out.is_null() { + return TempochStatus::NullPointer; + } + let Some(scale) = TempochScaleTag::from_raw(scale) else { + return TempochStatus::InvalidScaleId; + }; + let gw = match GnssWeek::new( + qtty::u32::Week::new(value.week), + qtty::u32::Second::new(value.seconds_of_week), + qtty::u32::Nanosecond::new(value.subsecond_nanos), + ) { + Ok(gw) => gw, + Err(err) => return status_from_conversion(err), + }; + let result: Result = with_gnss_scale!(scale, S, { + Time::::from_gnss_week(gw).map(|time| { + let (hi, lo) = time.raw_seconds_pair(); + TempochTime { + hi_seconds: hi.value(), + lo_seconds: lo.value(), + } + }) + }); + match result { + Ok(encoded) => { + // SAFETY: `out` was checked for null and the safety contract + // requires it to point to writable `TempochTime` storage. + unsafe { *out = encoded }; + TempochStatus::Ok + } + Err(err) => status_from_conversion(err), + } + }) +} + +#[cfg(test)] +mod tests { + use super::*; + + fn gpst_epoch_instant() -> TempochTime { + // GPST week-0/second-0 epoch in J2000 seconds. + TempochTime { + hi_seconds: -630_763_200.0, + lo_seconds: 0.0, + } + } + + #[test] + fn gpst_epoch_round_trips_to_week_zero() { + let mut gw = TempochGnssWeek { + week: u32::MAX, + seconds_of_week: u32::MAX, + subsecond_nanos: u32::MAX, + }; + let status = unsafe { + tempoch_time_to_gnss_week(gpst_epoch_instant(), TempochScaleTag::GPST as i32, &mut gw) + }; + assert_eq!(status, TempochStatus::Ok); + assert_eq!(gw.week, 0); + assert_eq!(gw.seconds_of_week, 0); + assert_eq!(gw.subsecond_nanos, 0); + + let mut back = TempochTime { + hi_seconds: 0.0, + lo_seconds: 0.0, + }; + let status = + unsafe { tempoch_time_from_gnss_week(gw, TempochScaleTag::GPST as i32, &mut back) }; + assert_eq!(status, TempochStatus::Ok); + assert!((back.hi_seconds + back.lo_seconds - (-630_763_200.0)).abs() < 1e-6); + } + + #[test] + fn non_gnss_scale_is_rejected() { + let mut gw = TempochGnssWeek { + week: 0, + seconds_of_week: 0, + subsecond_nanos: 0, + }; + let status = unsafe { + tempoch_time_to_gnss_week(gpst_epoch_instant(), TempochScaleTag::TT as i32, &mut gw) + }; + assert_eq!(status, TempochStatus::InvalidScaleId); + } + + #[test] + fn null_pointer_is_rejected() { + let status = unsafe { + tempoch_time_to_gnss_week( + gpst_epoch_instant(), + TempochScaleTag::GPST as i32, + std::ptr::null_mut(), + ) + }; + assert_eq!(status, TempochStatus::NullPointer); + } +} diff --git a/tempoch-ffi/src/lib.rs b/tempoch-ffi/src/lib.rs index 7345375..3d391a2 100644 --- a/tempoch-ffi/src/lib.rs +++ b/tempoch-ffi/src/lib.rs @@ -23,7 +23,9 @@ mod constats; mod context; mod eop; mod error; +mod gnss; mod period; +mod status; mod time; mod typed; @@ -145,6 +147,11 @@ mod tests { assert_eq!(TempochScaleTag::TDB as i32, 4); assert_eq!(TempochScaleTag::TCG as i32, 5); assert_eq!(TempochScaleTag::TCB as i32, 6); + assert_eq!(TempochScaleTag::ET as i32, 7); + assert_eq!(TempochScaleTag::GPST as i32, 8); + assert_eq!(TempochScaleTag::GST as i32, 9); + assert_eq!(TempochScaleTag::BDT as i32, 10); + assert_eq!(TempochScaleTag::QZSST as i32, 11); } #[test] diff --git a/tempoch-ffi/src/status.rs b/tempoch-ffi/src/status.rs new file mode 100644 index 0000000..77c3467 --- /dev/null +++ b/tempoch-ffi/src/status.rs @@ -0,0 +1,109 @@ +// SPDX-License-Identifier: AGPL-3.0-only +// Copyright (C) 2026 Vallés Puig, Ramon + +//! Active time-data status exposed over the C ABI. +//! +//! Mirrors [`tempoch::time_data_status`]: the validity horizons of the active +//! bundle plus the source it was loaded from. Optional EOP horizons are encoded +//! as `NaN` when no EOP data is loaded. + +use crate::error::TempochStatus; +use tempoch::{time_data_status, ActiveTimeDataSource}; + +/// Origin of the currently active time-data bundle. +/// +/// cbindgen:prefix-with-name +#[repr(i32)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum TempochTimeDataSource { + /// Compiled archive snapshot bundled at build time. + Bundled = 0, + /// Bundle loaded through the runtime fetch/cache path. + RuntimeCache = 1, + /// Test or caller-provided override is active. + Override = 2, +} + +impl From for TempochTimeDataSource { + #[inline] + fn from(value: ActiveTimeDataSource) -> Self { + match value { + ActiveTimeDataSource::Bundled => Self::Bundled, + ActiveTimeDataSource::RuntimeCache => Self::RuntimeCache, + ActiveTimeDataSource::Override => Self::Override, + } + } +} + +/// Validity horizons of the active time-data bundle, in MJD (UTC days). +/// +/// EOP horizon fields are `NaN` when no EOP data is loaded. +#[repr(C)] +#[derive(Debug, Clone, Copy, PartialEq)] +pub struct TempochDataHorizons { + /// First MJD covered by the EOP series, or `NaN` when no EOP is loaded. + pub eop_start_mjd: f64, + /// Last observed (non-predicted) EOP MJD, or `NaN` when no EOP is loaded. + pub eop_observed_end_mjd: f64, + /// Last EOP MJD including predictions, or `NaN` when no EOP is loaded. + pub eop_end_mjd: f64, + /// Last MJD with observed ΔT in the archive-provided modern table. + pub modern_delta_t_observed_end_mjd: f64, + /// Last MJD covered by the ΔT prediction table. + pub delta_t_prediction_horizon_mjd: f64, + /// Source of the active bundle as a [`TempochTimeDataSource`] discriminant. + pub source: i32, +} + +/// Capture the active time-data status into `out`. +/// +/// # Safety +/// `out` must be a valid, writable pointer to `TempochDataHorizons`. +#[no_mangle] +pub unsafe extern "C" fn tempoch_time_data_status(out: *mut TempochDataHorizons) -> TempochStatus { + if out.is_null() { + return TempochStatus::NullPointer; + } + let status = time_data_status(); + let h = status.horizons; + let horizons = TempochDataHorizons { + eop_start_mjd: h.eop_start_mjd.unwrap_or(f64::NAN), + eop_observed_end_mjd: h.eop_observed_end_mjd.unwrap_or(f64::NAN), + eop_end_mjd: h.eop_end_mjd.unwrap_or(f64::NAN), + modern_delta_t_observed_end_mjd: h.modern_delta_t_observed_end_mjd, + delta_t_prediction_horizon_mjd: h.delta_t_prediction_horizon_mjd, + source: TempochTimeDataSource::from(status.source) as i32, + }; + // SAFETY: `out` was checked for null and the safety contract requires it to + // point to writable `TempochDataHorizons` storage. + unsafe { *out = horizons }; + TempochStatus::Ok +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn status_reports_finite_delta_t_horizons() { + let mut out = TempochDataHorizons { + eop_start_mjd: 0.0, + eop_observed_end_mjd: 0.0, + eop_end_mjd: 0.0, + modern_delta_t_observed_end_mjd: 0.0, + delta_t_prediction_horizon_mjd: 0.0, + source: -1, + }; + let status = unsafe { tempoch_time_data_status(&mut out) }; + assert_eq!(status, TempochStatus::Ok); + assert!(out.modern_delta_t_observed_end_mjd.is_finite()); + assert!(out.delta_t_prediction_horizon_mjd.is_finite()); + assert!(out.source >= 0); + } + + #[test] + fn status_rejects_null_pointer() { + let status = unsafe { tempoch_time_data_status(std::ptr::null_mut()) }; + assert_eq!(status, TempochStatus::NullPointer); + } +} diff --git a/tempoch-ffi/src/typed.rs b/tempoch-ffi/src/typed.rs index e6d811f..f098068 100644 --- a/tempoch-ffi/src/typed.rs +++ b/tempoch-ffi/src/typed.rs @@ -9,8 +9,8 @@ use crate::time::TempochUtc; use crate::{catch_panic, QttyQuantity, UnitId}; use qtty::Second; use tempoch::{ - ConversionError, FormatForScale, GpsTime, J2000Seconds, Time, TimeContext, Unix, UnixTime, JD, - MJD, TAI, TCB, TCG, TDB, TT, UT1, UTC, + ConversionError, FormatForScale, GpsTime, J2000Seconds, Time, TimeContext, Unix, UnixTime, BDT, + ET, GPST, GST, JD, MJD, QZSST, TAI, TCB, TCG, TDB, TT, UT1, UTC, }; /// Scale tags used by the split-instant C ABI. @@ -33,6 +33,16 @@ pub enum TempochScaleTag { TCG = 5, /// Barycentric Coordinate Time. TCB = 6, + /// Ephemeris Time (NAIF/SPICE compatibility marker; routes through TDB). + ET = 7, + /// GPS System Time (`TAI − 19 s`). + GPST = 8, + /// Galileo System Time (`TAI − 19 s`). + GST = 9, + /// BeiDou Navigation Satellite System Time (`TAI − 33 s`). + BDT = 10, + /// Quasi-Zenith Satellite System Time (aligned with GPST). + QZSST = 11, } impl TempochScaleTag { @@ -47,6 +57,11 @@ impl TempochScaleTag { 4 => Some(Self::TDB), 5 => Some(Self::TCG), 6 => Some(Self::TCB), + 7 => Some(Self::ET), + 8 => Some(Self::GPST), + 9 => Some(Self::GST), + 10 => Some(Self::BDT), + 11 => Some(Self::QZSST), _ => None, } } @@ -131,7 +146,7 @@ unsafe fn context_or_default<'a>(ptr: *const TempochContext) -> ContextBorrow<'a } #[inline] -fn status_from_conversion(err: ConversionError) -> TempochStatus { +pub(crate) fn status_from_conversion(err: ConversionError) -> TempochStatus { match err { ConversionError::Ut1HorizonExceeded => TempochStatus::Ut1HorizonExceeded, _ => TempochStatus::ConversionFailed, @@ -177,6 +192,26 @@ macro_rules! with_scale { type $Scale = TCB; $body } + TempochScaleTag::ET => { + type $Scale = ET; + $body + } + TempochScaleTag::GPST => { + type $Scale = GPST; + $body + } + TempochScaleTag::GST => { + type $Scale = GST; + $body + } + TempochScaleTag::BDT => { + type $Scale = BDT; + $body + } + TempochScaleTag::QZSST => { + type $Scale = QZSST; + $body + } } }; } @@ -189,6 +224,41 @@ fn split_time(raw: TempochTime) -> Result, ) } +/// Exhaustively convert an infallible coordinate-scale instant to `target`. +/// +/// All targets are reachable infallibly except `UT1`, which is context-driven. +macro_rules! convert_infallible_to_target { + ($time:expr, $target:expr, $ctx:expr) => { + match $target { + TempochScaleTag::TT => Ok(TempochTime::from_time($time.to::())), + TempochScaleTag::TAI => Ok(TempochTime::from_time($time.to::())), + TempochScaleTag::UTC => Ok(TempochTime::from_time($time.to::())), + TempochScaleTag::UT1 => Ok(TempochTime::from_time($time.to_with::($ctx)?)), + TempochScaleTag::TDB => Ok(TempochTime::from_time($time.to::())), + TempochScaleTag::TCG => Ok(TempochTime::from_time($time.to::())), + TempochScaleTag::TCB => Ok(TempochTime::from_time($time.to::())), + TempochScaleTag::ET => Ok(TempochTime::from_time($time.to::())), + TempochScaleTag::GPST => Ok(TempochTime::from_time($time.to::())), + TempochScaleTag::GST => Ok(TempochTime::from_time($time.to::())), + TempochScaleTag::BDT => Ok(TempochTime::from_time($time.to::())), + TempochScaleTag::QZSST => Ok(TempochTime::from_time($time.to::())), + } + }; +} + +/// Encode an infallible coordinate-scale instant in the requested public format. +macro_rules! encode_infallible { + ($time:expr, $format:expr, $ctx:expr) => { + match $format { + TempochFormatTag::JD => Ok($time.to::().raw().value()), + TempochFormatTag::MJD => Ok($time.to::().raw().value()), + TempochFormatTag::J2000Seconds => Ok($time.to::().raw().value()), + TempochFormatTag::Unix => encode_unix_from_utc_time($time.to::(), $ctx), + TempochFormatTag::GPS => encode_gps_from_tai_time($time.to::()), + } + }; +} + fn decode_unix_to_target( raw_seconds: f64, target: TempochScaleTag, @@ -198,15 +268,7 @@ fn decode_unix_to_target( return Err(ConversionError::NonFinite); } let utc = UnixTime::try_new(Second::new(raw_seconds))?.to_j2000s(); - match target { - TempochScaleTag::TT => Ok(TempochTime::from_time(utc.to::())), - TempochScaleTag::TAI => Ok(TempochTime::from_time(utc.to::())), - TempochScaleTag::UTC => Ok(TempochTime::from_time(utc)), - TempochScaleTag::UT1 => Ok(TempochTime::from_time(utc.to_with::(ctx)?)), - TempochScaleTag::TDB => Ok(TempochTime::from_time(utc.to::())), - TempochScaleTag::TCG => Ok(TempochTime::from_time(utc.to::())), - TempochScaleTag::TCB => Ok(TempochTime::from_time(utc.to::())), - } + convert_infallible_to_target!(utc, target, ctx) } fn decode_gps_to_target( @@ -218,15 +280,7 @@ fn decode_gps_to_target( return Err(ConversionError::NonFinite); } let tai = GpsTime::new(raw_seconds).to_j2000s(); - match target { - TempochScaleTag::TT => Ok(TempochTime::from_time(tai.to::())), - TempochScaleTag::TAI => Ok(TempochTime::from_time(tai)), - TempochScaleTag::UTC => Ok(TempochTime::from_time(tai.to::())), - TempochScaleTag::UT1 => Ok(TempochTime::from_time(tai.to_with::(ctx)?)), - TempochScaleTag::TDB => Ok(TempochTime::from_time(tai.to::())), - TempochScaleTag::TCG => Ok(TempochTime::from_time(tai.to::())), - TempochScaleTag::TCB => Ok(TempochTime::from_time(tai.to::())), - } + convert_infallible_to_target!(tai, target, ctx) } fn encode_unix_from_utc_time(utc: Time, ctx: &TimeContext) -> Result { @@ -237,161 +291,41 @@ fn encode_gps_from_tai_time(tai: Time) -> Result { Ok(tai.to::().raw().value()) } -fn encode_time_tt( - raw: TempochTime, - format: TempochFormatTag, - ctx: &TimeContext, -) -> Result { - let time = split_time::(raw)?; - match format { - TempochFormatTag::JD => Ok(time.to::().raw().value()), - TempochFormatTag::MJD => Ok(time.to::().raw().value()), - TempochFormatTag::J2000Seconds => Ok(time.to::().raw().value()), - TempochFormatTag::Unix => encode_unix_from_utc_time(time.to::(), ctx), - TempochFormatTag::GPS => encode_gps_from_tai_time(time.to::()), - } -} - -fn encode_time_tai( - raw: TempochTime, - format: TempochFormatTag, - ctx: &TimeContext, -) -> Result { - let time = split_time::(raw)?; - match format { - TempochFormatTag::JD => Ok(time.to::().raw().value()), - TempochFormatTag::MJD => Ok(time.to::().raw().value()), - TempochFormatTag::J2000Seconds => Ok(time.to::().raw().value()), - TempochFormatTag::Unix => encode_unix_from_utc_time(time.to::(), ctx), - TempochFormatTag::GPS => encode_gps_from_tai_time(time), - } -} - -fn encode_time_utc( - raw: TempochTime, - format: TempochFormatTag, - ctx: &TimeContext, -) -> Result { - let time = split_time::(raw)?; - match format { - TempochFormatTag::JD => Ok(time.to::().raw().value()), - TempochFormatTag::MJD => Ok(time.to::().raw().value()), - TempochFormatTag::J2000Seconds => Ok(time.to::().raw().value()), - TempochFormatTag::Unix => encode_unix_from_utc_time(time, ctx), - TempochFormatTag::GPS => encode_gps_from_tai_time(time.to::()), - } -} - -fn encode_time_ut1( - raw: TempochTime, - format: TempochFormatTag, - ctx: &TimeContext, -) -> Result { - let time = split_time::(raw)?; - match format { - TempochFormatTag::JD => Ok(time.to::().raw().value()), - TempochFormatTag::MJD => Ok(time.to::().raw().value()), - TempochFormatTag::J2000Seconds => Ok(time.to::().raw().value()), - TempochFormatTag::Unix => encode_unix_from_utc_time(time.to_with::(ctx)?, ctx), - TempochFormatTag::GPS => encode_gps_from_tai_time(time.to_with::(ctx)?), - } -} - -fn encode_time_tdb( - raw: TempochTime, - format: TempochFormatTag, - ctx: &TimeContext, -) -> Result { - let time = split_time::(raw)?; - match format { - TempochFormatTag::JD => Ok(time.to::().raw().value()), - TempochFormatTag::MJD => Ok(time.to::().raw().value()), - TempochFormatTag::J2000Seconds => Ok(time.to::().raw().value()), - TempochFormatTag::Unix => encode_unix_from_utc_time(time.to::(), ctx), - TempochFormatTag::GPS => encode_gps_from_tai_time(time.to::()), - } -} - -fn encode_time_tcg( - raw: TempochTime, - format: TempochFormatTag, - ctx: &TimeContext, -) -> Result { - let time = split_time::(raw)?; - match format { - TempochFormatTag::JD => Ok(time.to::().raw().value()), - TempochFormatTag::MJD => Ok(time.to::().raw().value()), - TempochFormatTag::J2000Seconds => Ok(time.to::().raw().value()), - TempochFormatTag::Unix => encode_unix_from_utc_time(time.to::(), ctx), - TempochFormatTag::GPS => encode_gps_from_tai_time(time.to::()), - } -} - -fn encode_time_tcb( - raw: TempochTime, - format: TempochFormatTag, - ctx: &TimeContext, -) -> Result { - let time = split_time::(raw)?; - match format { - TempochFormatTag::JD => Ok(time.to::().raw().value()), - TempochFormatTag::MJD => Ok(time.to::().raw().value()), - TempochFormatTag::J2000Seconds => Ok(time.to::().raw().value()), - TempochFormatTag::Unix => encode_unix_from_utc_time(time.to::(), ctx), - TempochFormatTag::GPS => encode_gps_from_tai_time(time.to::()), - } -} - -fn scale_convert_tt( - raw: TempochTime, - target: TempochScaleTag, - ctx: &TimeContext, -) -> Result { - let time = split_time::(raw)?; - match target { - TempochScaleTag::TT => Ok(TempochTime::from_time(time)), - TempochScaleTag::TAI => Ok(TempochTime::from_time(time.to::())), - TempochScaleTag::UTC => Ok(TempochTime::from_time(time.to::())), - TempochScaleTag::UT1 => Ok(TempochTime::from_time(time.to_with::(ctx)?)), - TempochScaleTag::TDB => Ok(TempochTime::from_time(time.to::())), - TempochScaleTag::TCG => Ok(TempochTime::from_time(time.to::())), - TempochScaleTag::TCB => Ok(TempochTime::from_time(time.to::())), - } -} +/// Generate the per-source scale converter and format encoder for an infallible +/// coordinate scale. +macro_rules! define_scale_codec { + ($convert_fn:ident, $encode_fn:ident, $Scale:ty) => { + fn $convert_fn( + raw: TempochTime, + target: TempochScaleTag, + ctx: &TimeContext, + ) -> Result { + let time = split_time::<$Scale>(raw)?; + convert_infallible_to_target!(time, target, ctx) + } -fn scale_convert_tai( - raw: TempochTime, - target: TempochScaleTag, - ctx: &TimeContext, -) -> Result { - let time = split_time::(raw)?; - match target { - TempochScaleTag::TT => Ok(TempochTime::from_time(time.to::())), - TempochScaleTag::TAI => Ok(TempochTime::from_time(time)), - TempochScaleTag::UTC => Ok(TempochTime::from_time(time.to::())), - TempochScaleTag::UT1 => Ok(TempochTime::from_time(time.to_with::(ctx)?)), - TempochScaleTag::TDB => Ok(TempochTime::from_time(time.to::())), - TempochScaleTag::TCG => Ok(TempochTime::from_time(time.to::())), - TempochScaleTag::TCB => Ok(TempochTime::from_time(time.to::())), - } + fn $encode_fn( + raw: TempochTime, + format: TempochFormatTag, + ctx: &TimeContext, + ) -> Result { + let time = split_time::<$Scale>(raw)?; + encode_infallible!(time, format, ctx) + } + }; } -fn scale_convert_utc( - raw: TempochTime, - target: TempochScaleTag, - ctx: &TimeContext, -) -> Result { - let time = split_time::(raw)?; - match target { - TempochScaleTag::TT => Ok(TempochTime::from_time(time.to::())), - TempochScaleTag::TAI => Ok(TempochTime::from_time(time.to::())), - TempochScaleTag::UTC => Ok(TempochTime::from_time(time)), - TempochScaleTag::UT1 => Ok(TempochTime::from_time(time.to_with::(ctx)?)), - TempochScaleTag::TDB => Ok(TempochTime::from_time(time.to::())), - TempochScaleTag::TCG => Ok(TempochTime::from_time(time.to::())), - TempochScaleTag::TCB => Ok(TempochTime::from_time(time.to::())), - } -} +define_scale_codec!(scale_convert_tt, encode_time_tt, TT); +define_scale_codec!(scale_convert_tai, encode_time_tai, TAI); +define_scale_codec!(scale_convert_utc, encode_time_utc, UTC); +define_scale_codec!(scale_convert_tdb, encode_time_tdb, TDB); +define_scale_codec!(scale_convert_tcg, encode_time_tcg, TCG); +define_scale_codec!(scale_convert_tcb, encode_time_tcb, TCB); +define_scale_codec!(scale_convert_et, encode_time_et, ET); +define_scale_codec!(scale_convert_gpst, encode_time_gpst, GPST); +define_scale_codec!(scale_convert_gst, encode_time_gst, GST); +define_scale_codec!(scale_convert_bdt, encode_time_bdt, BDT); +define_scale_codec!(scale_convert_qzsst, encode_time_qzsst, QZSST); fn scale_convert_ut1( raw: TempochTime, @@ -400,64 +334,33 @@ fn scale_convert_ut1( ) -> Result { let time = split_time::(raw)?; match target { + TempochScaleTag::UT1 => Ok(TempochTime::from_time(time)), TempochScaleTag::TT => Ok(TempochTime::from_time(time.to_with::(ctx)?)), TempochScaleTag::TAI => Ok(TempochTime::from_time(time.to_with::(ctx)?)), TempochScaleTag::UTC => Ok(TempochTime::from_time(time.to_with::(ctx)?)), - TempochScaleTag::UT1 => Ok(TempochTime::from_time(time)), TempochScaleTag::TDB => Ok(TempochTime::from_time(time.to_with::(ctx)?)), TempochScaleTag::TCG => Ok(TempochTime::from_time(time.to_with::(ctx)?)), TempochScaleTag::TCB => Ok(TempochTime::from_time(time.to_with::(ctx)?)), + TempochScaleTag::ET => Ok(TempochTime::from_time(time.to_with::(ctx)?)), + TempochScaleTag::GPST => Ok(TempochTime::from_time(time.to_with::(ctx)?)), + TempochScaleTag::GST => Ok(TempochTime::from_time(time.to_with::(ctx)?)), + TempochScaleTag::BDT => Ok(TempochTime::from_time(time.to_with::(ctx)?)), + TempochScaleTag::QZSST => Ok(TempochTime::from_time(time.to_with::(ctx)?)), } } -fn scale_convert_tdb( - raw: TempochTime, - target: TempochScaleTag, - ctx: &TimeContext, -) -> Result { - let time = split_time::(raw)?; - match target { - TempochScaleTag::TT => Ok(TempochTime::from_time(time.to::())), - TempochScaleTag::TAI => Ok(TempochTime::from_time(time.to::())), - TempochScaleTag::UTC => Ok(TempochTime::from_time(time.to::())), - TempochScaleTag::UT1 => Ok(TempochTime::from_time(time.to_with::(ctx)?)), - TempochScaleTag::TDB => Ok(TempochTime::from_time(time)), - TempochScaleTag::TCG => Ok(TempochTime::from_time(time.to::())), - TempochScaleTag::TCB => Ok(TempochTime::from_time(time.to::())), - } -} - -fn scale_convert_tcg( - raw: TempochTime, - target: TempochScaleTag, - ctx: &TimeContext, -) -> Result { - let time = split_time::(raw)?; - match target { - TempochScaleTag::TT => Ok(TempochTime::from_time(time.to::())), - TempochScaleTag::TAI => Ok(TempochTime::from_time(time.to::())), - TempochScaleTag::UTC => Ok(TempochTime::from_time(time.to::())), - TempochScaleTag::UT1 => Ok(TempochTime::from_time(time.to_with::(ctx)?)), - TempochScaleTag::TDB => Ok(TempochTime::from_time(time.to::())), - TempochScaleTag::TCG => Ok(TempochTime::from_time(time)), - TempochScaleTag::TCB => Ok(TempochTime::from_time(time.to::())), - } -} - -fn scale_convert_tcb( +fn encode_time_ut1( raw: TempochTime, - target: TempochScaleTag, + format: TempochFormatTag, ctx: &TimeContext, -) -> Result { - let time = split_time::(raw)?; - match target { - TempochScaleTag::TT => Ok(TempochTime::from_time(time.to::())), - TempochScaleTag::TAI => Ok(TempochTime::from_time(time.to::())), - TempochScaleTag::UTC => Ok(TempochTime::from_time(time.to::())), - TempochScaleTag::UT1 => Ok(TempochTime::from_time(time.to_with::(ctx)?)), - TempochScaleTag::TDB => Ok(TempochTime::from_time(time.to::())), - TempochScaleTag::TCG => Ok(TempochTime::from_time(time.to::())), - TempochScaleTag::TCB => Ok(TempochTime::from_time(time)), +) -> Result { + let time = split_time::(raw)?; + match format { + TempochFormatTag::JD => Ok(time.to::().raw().value()), + TempochFormatTag::MJD => Ok(time.to::().raw().value()), + TempochFormatTag::J2000Seconds => Ok(time.to::().raw().value()), + TempochFormatTag::Unix => encode_unix_from_utc_time(time.to_with::(ctx)?, ctx), + TempochFormatTag::GPS => encode_gps_from_tai_time(time.to_with::(ctx)?), } } @@ -522,6 +425,11 @@ pub unsafe extern "C" fn tempoch_time_scale_convert( TempochScaleTag::TDB => scale_convert_tdb(value, to_scale, ctx.as_ref()), TempochScaleTag::TCG => scale_convert_tcg(value, to_scale, ctx.as_ref()), TempochScaleTag::TCB => scale_convert_tcb(value, to_scale, ctx.as_ref()), + TempochScaleTag::ET => scale_convert_et(value, to_scale, ctx.as_ref()), + TempochScaleTag::GPST => scale_convert_gpst(value, to_scale, ctx.as_ref()), + TempochScaleTag::GST => scale_convert_gst(value, to_scale, ctx.as_ref()), + TempochScaleTag::BDT => scale_convert_bdt(value, to_scale, ctx.as_ref()), + TempochScaleTag::QZSST => scale_convert_qzsst(value, to_scale, ctx.as_ref()), }; match converted { Ok(time) => { @@ -567,6 +475,11 @@ pub unsafe extern "C" fn tempoch_time_to_format( TempochScaleTag::TDB => encode_time_tdb(value, format, ctx.as_ref()), TempochScaleTag::TCG => encode_time_tcg(value, format, ctx.as_ref()), TempochScaleTag::TCB => encode_time_tcb(value, format, ctx.as_ref()), + TempochScaleTag::ET => encode_time_et(value, format, ctx.as_ref()), + TempochScaleTag::GPST => encode_time_gpst(value, format, ctx.as_ref()), + TempochScaleTag::GST => encode_time_gst(value, format, ctx.as_ref()), + TempochScaleTag::BDT => encode_time_bdt(value, format, ctx.as_ref()), + TempochScaleTag::QZSST => encode_time_qzsst(value, format, ctx.as_ref()), }; match encoded { Ok(raw) => { diff --git a/tempoch/Cargo.toml b/tempoch/Cargo.toml index 1748546..c5b3912 100644 --- a/tempoch/Cargo.toml +++ b/tempoch/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "tempoch" -version = "0.6.3" +version = "0.6.4" edition = "2021" authors = ["VPRamon "] description = "Astronomical time primitives: typed time scales, Julian dates, UTC conversion, and interval operations." @@ -20,11 +20,11 @@ serde = ["tempoch-core/serde"] runtime-data-fetch = ["tempoch-core/runtime-data-fetch"] [dependencies] -tempoch-core = { path = "../tempoch-core", version = "0.6.3" } +tempoch-core = { path = "../tempoch-core", version = "0.6.4" } [dev-dependencies] chrono = "0.4.44" -qtty = "0.8.4" +qtty = "0.8.5" serde_json = "1" [[example]]