Skip to main content

lox_time/
time_scales.rs

1// SPDX-FileCopyrightText: 2024 Angus Morrison <github@angus-morrison.com>
2// SPDX-FileCopyrightText: 2024 Helge Eichhorn <git@helgeeichhorn.de>
3//
4// SPDX-License-Identifier: MPL-2.0
5
6/*!
7    Module `time_scales` provides a marker trait denoting a continuous astronomical time scale,
8    along with zero-sized implementations for the most commonly used scales.
9
10    # Utc
11
12    As a discontinuous time scale, [Utc] does not implement [TimeScale] and is treated by Lox
13    exclusively as an IO format.
14*/
15
16use std::fmt::Display;
17use std::str::FromStr;
18
19use thiserror::Error;
20
21/// Marker trait denoting a continuous astronomical time scale.
22pub trait TimeScale {
23    /// Returns the standard abbreviation of this time scale (e.g. `"TAI"`).
24    fn abbreviation(&self) -> &'static str;
25    /// Returns the full name of this time scale (e.g. `"International Atomic Time"`).
26    fn name(&self) -> &'static str;
27}
28
29/// International Atomic Time.
30#[derive(Debug, Default, Copy, Clone, Eq, PartialEq, PartialOrd, Ord)]
31#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
32pub struct Tai;
33
34impl TimeScale for Tai {
35    fn abbreviation(&self) -> &'static str {
36        "TAI"
37    }
38    fn name(&self) -> &'static str {
39        "International Atomic Time"
40    }
41}
42
43impl Display for Tai {
44    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
45        write!(f, "{}", self.abbreviation())
46    }
47}
48
49/// Barycentric Coordinate Time.
50#[derive(Debug, Default, Copy, Clone, Eq, PartialEq, PartialOrd, Ord)]
51#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
52pub struct Tcb;
53
54impl TimeScale for Tcb {
55    fn abbreviation(&self) -> &'static str {
56        "TCB"
57    }
58    fn name(&self) -> &'static str {
59        "Barycentric Coordinate Time"
60    }
61}
62
63impl Display for Tcb {
64    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
65        write!(f, "{}", self.abbreviation())
66    }
67}
68
69/// Geocentric Coordinate Time.
70#[derive(Debug, Default, Copy, Clone, Eq, PartialEq, PartialOrd, Ord)]
71#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
72pub struct Tcg;
73
74impl TimeScale for Tcg {
75    fn abbreviation(&self) -> &'static str {
76        "TCG"
77    }
78    fn name(&self) -> &'static str {
79        "Geocentric Coordinate Time"
80    }
81}
82
83impl Display for Tcg {
84    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
85        write!(f, "{}", self.abbreviation())
86    }
87}
88
89/// Barycentric Dynamical Time.
90#[derive(Debug, Default, Copy, Clone, Eq, PartialEq, PartialOrd, Ord)]
91#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
92pub struct Tdb;
93
94impl TimeScale for Tdb {
95    fn abbreviation(&self) -> &'static str {
96        "TDB"
97    }
98    fn name(&self) -> &'static str {
99        "Barycentric Dynamical Time"
100    }
101}
102
103impl Display for Tdb {
104    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
105        write!(f, "{}", self.abbreviation())
106    }
107}
108
109/// Terrestrial Time.
110#[derive(Debug, Default, Copy, Clone, Eq, PartialEq, PartialOrd, Ord)]
111#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
112pub struct Tt;
113
114impl TimeScale for Tt {
115    fn abbreviation(&self) -> &'static str {
116        "TT"
117    }
118    fn name(&self) -> &'static str {
119        "Terrestrial Time"
120    }
121}
122
123impl Display for Tt {
124    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
125        write!(f, "{}", self.abbreviation())
126    }
127}
128
129/// Universal Time.
130#[derive(Debug, Default, Copy, Clone, Eq, PartialEq, PartialOrd, Ord)]
131#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
132pub struct Ut1;
133
134impl TimeScale for Ut1 {
135    fn abbreviation(&self) -> &'static str {
136        "UT1"
137    }
138    fn name(&self) -> &'static str {
139        "Universal Time"
140    }
141}
142
143impl Display for Ut1 {
144    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
145        write!(f, "{}", self.abbreviation())
146    }
147}
148
149/// Dynamic time scale selector for runtime-determined time scales.
150#[derive(Copy, Clone, Debug, Default, Eq, PartialEq, PartialOrd, Ord)]
151#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
152pub enum DynTimeScale {
153    /// International Atomic Time.
154    #[default]
155    Tai,
156    /// Barycentric Coordinate Time.
157    Tcb,
158    /// Geocentric Coordinate Time.
159    Tcg,
160    /// Barycentric Dynamical Time.
161    Tdb,
162    /// Terrestrial Time.
163    Tt,
164    /// Universal Time.
165    Ut1,
166}
167
168impl TimeScale for DynTimeScale {
169    fn abbreviation(&self) -> &'static str {
170        match self {
171            DynTimeScale::Tai => Tai.abbreviation(),
172            DynTimeScale::Tcb => Tcb.abbreviation(),
173            DynTimeScale::Tcg => Tcg.abbreviation(),
174            DynTimeScale::Tdb => Tdb.abbreviation(),
175            DynTimeScale::Tt => Tt.abbreviation(),
176            DynTimeScale::Ut1 => Ut1.abbreviation(),
177        }
178    }
179
180    fn name(&self) -> &'static str {
181        match self {
182            DynTimeScale::Tai => Tai.name(),
183            DynTimeScale::Tcb => Tcb.name(),
184            DynTimeScale::Tcg => Tcg.name(),
185            DynTimeScale::Tdb => Tdb.name(),
186            DynTimeScale::Tt => Tt.name(),
187            DynTimeScale::Ut1 => Ut1.name(),
188        }
189    }
190}
191
192impl From<Tai> for DynTimeScale {
193    fn from(_: Tai) -> Self {
194        Self::Tai
195    }
196}
197
198impl From<Tcb> for DynTimeScale {
199    fn from(_: Tcb) -> Self {
200        Self::Tcb
201    }
202}
203
204impl From<Tcg> for DynTimeScale {
205    fn from(_: Tcg) -> Self {
206        Self::Tcg
207    }
208}
209
210impl From<Tdb> for DynTimeScale {
211    fn from(_: Tdb) -> Self {
212        Self::Tdb
213    }
214}
215
216impl From<Tt> for DynTimeScale {
217    fn from(_: Tt) -> Self {
218        Self::Tt
219    }
220}
221
222impl From<Ut1> for DynTimeScale {
223    fn from(_: Ut1) -> Self {
224        Self::Ut1
225    }
226}
227
228/// Error returned when parsing an unknown time scale abbreviation.
229#[derive(Clone, Debug, Error, Eq, PartialEq)]
230#[error("unknown time scale: {0}")]
231pub struct UnknownTimeScaleError(String);
232
233impl FromStr for DynTimeScale {
234    type Err = UnknownTimeScaleError;
235
236    fn from_str(s: &str) -> Result<Self, Self::Err> {
237        match s {
238            "tai" | "TAI" => Ok(DynTimeScale::Tai),
239            "tcb" | "TCB" => Ok(DynTimeScale::Tcb),
240            "tcg" | "TCG" => Ok(DynTimeScale::Tcg),
241            "tdb" | "TDB" => Ok(DynTimeScale::Tdb),
242            "tt" | "TT" => Ok(DynTimeScale::Tt),
243            "ut1" | "UT1" => Ok(DynTimeScale::Ut1),
244            _ => Err(UnknownTimeScaleError(s.to_owned())),
245        }
246    }
247}
248
249impl Display for DynTimeScale {
250    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
251        write!(f, "{}", self.abbreviation())
252    }
253}
254
255#[cfg(test)]
256mod tests {
257    use super::*;
258    use rstest::rstest;
259
260    #[rstest]
261    #[case(Tai, "TAI", "International Atomic Time")]
262    #[case(Tcb, "TCB", "Barycentric Coordinate Time")]
263    #[case(Tcg, "TCG", "Geocentric Coordinate Time")]
264    #[case(Tdb, "TDB", "Barycentric Dynamical Time")]
265    #[case(Tt, "TT", "Terrestrial Time")]
266    #[case(Ut1, "UT1", "Universal Time")]
267    fn test_time_scales<T: TimeScale + ToString>(
268        #[case] scale: T,
269        #[case] abbreviation: &'static str,
270        #[case] name: &'static str,
271    ) {
272        assert_eq!(scale.abbreviation(), abbreviation);
273        assert_eq!(scale.to_string(), abbreviation);
274        assert_eq!(scale.name(), name);
275    }
276
277    #[rstest]
278    #[case("TAI", "International Atomic Time")]
279    #[case("TCB", "Barycentric Coordinate Time")]
280    #[case("TCG", "Geocentric Coordinate Time")]
281    #[case("TDB", "Barycentric Dynamical Time")]
282    #[case("TT", "Terrestrial Time")]
283    #[case("UT1", "Universal Time")]
284    fn test_dyn_time_scale(#[case] abbreviation: &str, #[case] name: &str) {
285        let scale: DynTimeScale = abbreviation.parse().unwrap();
286        assert_eq!(scale.abbreviation(), abbreviation);
287        assert_eq!(scale.to_string(), abbreviation);
288        assert_eq!(scale.name(), name);
289    }
290
291    #[test]
292    fn test_dyn_time_scale_invalid() {
293        let scale: Result<DynTimeScale, UnknownTimeScaleError> = "NTS".parse();
294        assert_eq!(scale, Err(UnknownTimeScaleError("NTS".to_owned())))
295    }
296}