Skip to main content

graphcal_compiler/registry/
time_scale.rs

1//! Time scale definitions for the `Datetime` primitive type.
2//!
3//! Maps Graphcal's `TimeScale` enum to `hifitime::TimeScale`.
4//! UTC is the default for civil use; aerospace users opt into TAI, TT, TDB, etc.
5
6use std::fmt;
7use std::str::FromStr;
8
9/// Time scales supported by Graphcal.
10///
11/// Each variant maps 1:1 to a [`hifitime::TimeScale`] variant.
12/// `UTC` is the default for civil datetime values.
13#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
14pub enum TimeScale {
15    /// Coordinated Universal Time — default for civil use.
16    UTC,
17    /// International Atomic Time — continuous, the internal reference.
18    TAI,
19    /// Terrestrial Time — TAI + 32.184 s, used in orbital mechanics.
20    TT,
21    /// Barycentric Dynamical Time — used for solar system ephemerides.
22    TDB,
23    /// Ephemeris Time (NAIF/SPICE variant, ≈ TDB).
24    ET,
25    /// GPS Time — TAI − 19 s.
26    GPST,
27    /// Galileo System Time.
28    GST,
29    /// `BeiDou` Time.
30    BDT,
31    /// QZSS Time.
32    QZSST,
33}
34
35impl TimeScale {
36    /// All supported time scale names, for error messages and validation.
37    pub const ALL_NAMES: &[&str] = &[
38        "UTC", "TAI", "TT", "TDB", "ET", "GPST", "GST", "BDT", "QZSST",
39    ];
40
41    /// Returns `true` if this is the default civil time scale (UTC).
42    #[must_use]
43    pub const fn is_utc(self) -> bool {
44        matches!(self, Self::UTC)
45    }
46
47    /// Returns the string name of this time scale.
48    #[must_use]
49    pub const fn name(self) -> &'static str {
50        match self {
51            Self::UTC => "UTC",
52            Self::TAI => "TAI",
53            Self::TT => "TT",
54            Self::TDB => "TDB",
55            Self::ET => "ET",
56            Self::GPST => "GPST",
57            Self::GST => "GST",
58            Self::BDT => "BDT",
59            Self::QZSST => "QZSST",
60        }
61    }
62
63    /// Convert to the corresponding `hifitime::TimeScale`.
64    #[must_use]
65    pub const fn to_hifitime(self) -> hifitime::TimeScale {
66        match self {
67            Self::UTC => hifitime::TimeScale::UTC,
68            Self::TAI => hifitime::TimeScale::TAI,
69            Self::TT => hifitime::TimeScale::TT,
70            Self::TDB => hifitime::TimeScale::TDB,
71            Self::ET => hifitime::TimeScale::ET,
72            Self::GPST => hifitime::TimeScale::GPST,
73            Self::GST => hifitime::TimeScale::GST,
74            Self::BDT => hifitime::TimeScale::BDT,
75            Self::QZSST => hifitime::TimeScale::QZSST,
76        }
77    }
78
79    /// Convert from `hifitime::TimeScale`.
80    #[must_use]
81    pub const fn from_hifitime(ts: hifitime::TimeScale) -> Self {
82        match ts {
83            hifitime::TimeScale::TAI => Self::TAI,
84            hifitime::TimeScale::TT => Self::TT,
85            hifitime::TimeScale::TDB => Self::TDB,
86            hifitime::TimeScale::ET => Self::ET,
87            hifitime::TimeScale::GPST => Self::GPST,
88            hifitime::TimeScale::GST => Self::GST,
89            hifitime::TimeScale::BDT => Self::BDT,
90            hifitime::TimeScale::QZSST => Self::QZSST,
91            // hifitime::TimeScale is non_exhaustive; UTC and unknown variants map to UTC
92            _ => Self::UTC,
93        }
94    }
95}
96
97/// Returns the target `TimeScale` for a conversion function name, if it matches.
98///
99/// Maps `"to_utc"` → `UTC`, `"to_tai"` → `TAI`, etc.
100/// Returns `None` if the name is not a time scale conversion function.
101#[must_use]
102pub fn time_scale_from_conversion_fn(name: &str) -> Option<TimeScale> {
103    match name {
104        "to_utc" => Some(TimeScale::UTC),
105        "to_tai" => Some(TimeScale::TAI),
106        "to_tt" => Some(TimeScale::TT),
107        "to_tdb" => Some(TimeScale::TDB),
108        "to_et" => Some(TimeScale::ET),
109        "to_gpst" => Some(TimeScale::GPST),
110        "to_gst" => Some(TimeScale::GST),
111        "to_bdt" => Some(TimeScale::BDT),
112        "to_qzsst" => Some(TimeScale::QZSST),
113        _ => None,
114    }
115}
116
117impl fmt::Display for TimeScale {
118    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
119        f.write_str(self.name())
120    }
121}
122
123/// Error returned when parsing an unknown time scale name.
124#[derive(Debug, Clone, PartialEq, Eq)]
125pub struct ParseTimeScaleError {
126    /// The unrecognized input string.
127    pub input: String,
128}
129
130impl fmt::Display for ParseTimeScaleError {
131    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
132        write!(
133            f,
134            "unknown time scale `{}`; expected one of: {}",
135            self.input,
136            TimeScale::ALL_NAMES.join(", ")
137        )
138    }
139}
140
141impl std::error::Error for ParseTimeScaleError {}
142
143impl FromStr for TimeScale {
144    type Err = ParseTimeScaleError;
145
146    fn from_str(s: &str) -> Result<Self, Self::Err> {
147        // Try matching against all variants
148        let variants = [
149            Self::UTC,
150            Self::TAI,
151            Self::TT,
152            Self::TDB,
153            Self::ET,
154            Self::GPST,
155            Self::GST,
156            Self::BDT,
157            Self::QZSST,
158        ];
159
160        for variant in variants {
161            if variant.name() == s {
162                return Ok(variant);
163            }
164        }
165
166        Err(ParseTimeScaleError {
167            input: s.to_string(),
168        })
169    }
170}
171
172#[cfg(test)]
173mod tests {
174    use super::*;
175
176    #[test]
177    fn display_roundtrip() {
178        let scales = [
179            TimeScale::UTC,
180            TimeScale::TAI,
181            TimeScale::TT,
182            TimeScale::TDB,
183            TimeScale::ET,
184            TimeScale::GPST,
185            TimeScale::GST,
186            TimeScale::BDT,
187            TimeScale::QZSST,
188        ];
189        for scale in &scales {
190            let s = scale.to_string();
191            let parsed: TimeScale = s.parse().unwrap();
192            assert_eq!(*scale, parsed);
193        }
194    }
195
196    #[test]
197    fn from_str_unknown() {
198        let err = "INVALID".parse::<TimeScale>().unwrap_err();
199        assert_eq!(err.input, "INVALID");
200        assert!(err.to_string().contains("unknown time scale"));
201    }
202
203    #[test]
204    fn hifitime_roundtrip() {
205        let scales = [
206            TimeScale::UTC,
207            TimeScale::TAI,
208            TimeScale::TT,
209            TimeScale::TDB,
210            TimeScale::ET,
211            TimeScale::GPST,
212            TimeScale::GST,
213            TimeScale::BDT,
214            TimeScale::QZSST,
215        ];
216        for scale in &scales {
217            let hf = scale.to_hifitime();
218            let back = TimeScale::from_hifitime(hf);
219            assert_eq!(*scale, back);
220        }
221    }
222
223    #[test]
224    fn utc_is_default() {
225        assert!(TimeScale::UTC.is_utc());
226        assert!(!TimeScale::TT.is_utc());
227    }
228}