1use crate::epoch::CivilDate;
23
24mod private {
26 pub trait Sealed {}
27}
28
29macro_rules! define_scale {
30 (
31 $(#[$meta:meta])*
32 $name:ident,
33 display = $display:literal,
34 offset = $offset:expr,
35 epoch = $epoch:expr,
36 style = $style:expr
37 ) => {
38 $(#[$meta])*
39 #[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
40 pub struct $name;
41
42 impl private::Sealed for $name {}
43
44 impl TimeScale for $name {
45 const NAME: &'static str = $display;
46 const OFFSET_TO_TAI: OffsetToTai = $offset;
47 const EPOCH_CIVIL: CivilDate = $epoch;
48 const DISPLAY_STYLE: DisplayStyle = $style;
49 }
50 };
51}
52
53pub(crate) const NANOS_PER_SECOND: i64 = 1_000_000_000;
54
55#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
63pub enum OffsetToTai {
64 Fixed(i64),
66
67 Contextual,
69}
70
71#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
74pub enum DisplayStyle {
75 WeekTow,
80
81 DayTod,
86
87 Simple,
89}
90
91pub trait TimeScale: private::Sealed + Copy + Clone + Eq + PartialEq + core::fmt::Debug {
99 const NAME: &'static str;
101
102 const OFFSET_TO_TAI: OffsetToTai;
110
111 const EPOCH_CIVIL: CivilDate;
114
115 const DISPLAY_STYLE: DisplayStyle;
117}
118
119define_scale!(
120 Glonass,
127 display = "GLO",
128 offset = OffsetToTai::Contextual,
129 epoch = CivilDate::new(1996, 1, 1),
130 style = DisplayStyle::DayTod
131);
132
133define_scale!(
134 Gps,
141 display = "GPS",
142 offset = OffsetToTai::Fixed(19 * NANOS_PER_SECOND),
143 epoch = CivilDate::new(1980, 1, 6),
144 style = DisplayStyle::WeekTow
145);
146
147define_scale!(
148 Galileo,
155 display = "GAL",
156 offset = OffsetToTai::Fixed(19 * NANOS_PER_SECOND),
157 epoch = CivilDate::new(1999, 8, 22),
158 style = DisplayStyle::WeekTow
159);
160
161define_scale!(
162 Beidou,
169 display = "BDT",
170 offset = OffsetToTai::Fixed(33 * NANOS_PER_SECOND),
171 epoch = CivilDate::new(2006, 1, 1),
172 style = DisplayStyle::WeekTow
173);
174
175define_scale!(
176 Tai,
188 display = "TAI",
189 offset = OffsetToTai::Fixed(0),
190 epoch = CivilDate::new(1958, 1, 1),
191 style = DisplayStyle::Simple
192);
193
194define_scale!(
195 Utc,
201 display = "UTC",
202 offset = OffsetToTai::Contextual,
203 epoch = CivilDate::new(1972, 1, 1),
204 style = DisplayStyle::Simple
205);
206
207impl OffsetToTai {
208 #[inline(always)]
210 #[must_use]
211 pub const fn fixed(self) -> Option<i64> {
212 match self {
213 OffsetToTai::Fixed(v) => Some(v),
214 OffsetToTai::Contextual => None,
215 }
216 }
217
218 #[inline(always)]
220 #[must_use]
221 pub const fn is_contextual(self) -> bool {
222 matches!(self, OffsetToTai::Contextual)
223 }
224
225 #[inline(always)]
227 #[must_use]
228 pub const fn is_fixed(self) -> bool {
229 matches!(self, OffsetToTai::Fixed(_))
230 }
231}
232
233#[cfg(test)]
238mod tests {
239 use std::{collections::HashSet, mem::size_of};
240
241 use super::*;
242
243 #[test]
244 fn test_name_are_correct() {
245 assert_eq!(Glonass::NAME, "GLO");
246 assert_eq!(Gps::NAME, "GPS");
247 assert_eq!(Galileo::NAME, "GAL");
248 assert_eq!(Beidou::NAME, "BDT");
249 assert_eq!(Tai::NAME, "TAI");
250 assert_eq!(Utc::NAME, "UTC");
251 }
252
253 #[test]
254 fn test_fixed_offsets() {
255 assert_eq!(
256 Gps::OFFSET_TO_TAI,
257 OffsetToTai::Fixed(19 * NANOS_PER_SECOND)
258 );
259 assert_eq!(
260 Galileo::OFFSET_TO_TAI,
261 OffsetToTai::Fixed(19 * NANOS_PER_SECOND)
262 );
263 assert_eq!(
264 Beidou::OFFSET_TO_TAI,
265 OffsetToTai::Fixed(33 * NANOS_PER_SECOND)
266 );
267 assert_eq!(Tai::OFFSET_TO_TAI, OffsetToTai::Fixed(0));
268 }
269
270 #[test]
271 fn test_contextual_offsets() {
272 assert_eq!(Utc::OFFSET_TO_TAI, OffsetToTai::Contextual);
273 assert_eq!(Glonass::OFFSET_TO_TAI, OffsetToTai::Contextual);
274 }
275
276 #[test]
277 fn test_scale_types_are_copy() {
278 fn assert_copy<T: Copy>() {}
279 assert_copy::<Glonass>();
280 assert_copy::<Gps>();
281 assert_copy::<Galileo>();
282 assert_copy::<Beidou>();
283 assert_copy::<Tai>();
284 assert_copy::<Utc>();
285 }
286
287 #[test]
288 fn test_gps_and_galileo_are_aligned() {
289 assert_eq!(Gps::OFFSET_TO_TAI, Galileo::OFFSET_TO_TAI);
291 }
292
293 #[test]
294 fn test_tai_invariant_is_valid() {
295 assert_eq!(Tai::OFFSET_TO_TAI, OffsetToTai::Fixed(0));
296 assert!(Tai::OFFSET_TO_TAI.fixed().unwrap() == 0);
297 }
298
299 #[test]
300 fn test_names_are_unique() {
301 let names = [
302 Gps::NAME,
303 Glonass::NAME,
304 Galileo::NAME,
305 Beidou::NAME,
306 Tai::NAME,
307 Utc::NAME,
308 ];
309 let set: HashSet<_> = names.iter().collect();
310
311 assert_eq!(set.len(), names.len());
312 }
313
314 #[test]
315 fn test_fixed_scales_are_really_fixed() {
316 let fixed_scales = [
317 Gps::OFFSET_TO_TAI,
318 Galileo::OFFSET_TO_TAI,
319 Beidou::OFFSET_TO_TAI,
320 Tai::OFFSET_TO_TAI,
321 ];
322
323 for scale in fixed_scales {
324 assert!(scale.fixed().is_some(), "Expected Fixed, got Contextual");
325 }
326 }
327
328 #[test]
329 fn test_contextual_only_where_expected() {
330 assert!(Utc::OFFSET_TO_TAI.is_contextual());
331 assert!(Glonass::OFFSET_TO_TAI.is_contextual());
332 }
333
334 #[test]
335 fn test_scale_is_zero_sized() {
336 assert_eq!(size_of::<Glonass>(), 0);
337 assert_eq!(size_of::<Gps>(), 0);
338 assert_eq!(size_of::<Galileo>(), 0);
339 assert_eq!(size_of::<Beidou>(), 0);
340 assert_eq!(size_of::<Tai>(), 0);
341 assert_eq!(size_of::<Utc>(), 0);
342 }
343
344 #[test]
345 fn test_scale_is_copy() {
346 fn assert_copy<T: Copy + Clone + Eq + PartialEq + core::fmt::Debug>() {}
347 assert_copy::<Glonass>();
348 assert_copy::<Gps>();
349 assert_copy::<Galileo>();
350 assert_copy::<Beidou>();
351 assert_copy::<Tai>();
352 assert_copy::<Utc>();
353 }
354
355 #[test]
356 fn test_display_styles() {
357 assert_eq!(Gps::DISPLAY_STYLE, DisplayStyle::WeekTow);
358 assert_eq!(Glonass::DISPLAY_STYLE, DisplayStyle::DayTod);
359 assert_eq!(Galileo::DISPLAY_STYLE, DisplayStyle::WeekTow);
360 assert_eq!(Beidou::DISPLAY_STYLE, DisplayStyle::WeekTow);
361 assert_eq!(Tai::DISPLAY_STYLE, DisplayStyle::Simple);
362 assert_eq!(Utc::DISPLAY_STYLE, DisplayStyle::Simple);
363 }
364
365 #[test]
366 fn test_offset_to_tai_helpers() {
367 assert!(OffsetToTai::Fixed(0).is_fixed());
368 assert!(!OffsetToTai::Fixed(0).is_contextual());
369 assert!(OffsetToTai::Contextual.is_contextual());
370 assert!(!OffsetToTai::Contextual.is_fixed());
371 assert_eq!(OffsetToTai::Fixed(42).fixed(), Some(42));
372 assert_eq!(OffsetToTai::Contextual.fixed(), None);
373 }
374
375 #[test]
376 fn test_epoch_civil_dates() {
377 assert_eq!(Gps::EPOCH_CIVIL.year, 1980);
378 assert_eq!(Glonass::EPOCH_CIVIL.year, 1996);
379 assert_eq!(Galileo::EPOCH_CIVIL.year, 1999);
380 assert_eq!(Beidou::EPOCH_CIVIL.year, 2006);
381 assert_eq!(Tai::EPOCH_CIVIL.year, 1958);
382 }
383
384 #[test]
385 fn test_tai_invariant() {
386 assert_eq!(Tai::OFFSET_TO_TAI, OffsetToTai::Fixed(0));
387 assert_eq!(Tai::OFFSET_TO_TAI.fixed(), Some(0));
388 }
389
390 #[test]
391 fn test_contract_all_scales() {
392 fn check<T: TimeScale>() {
393 match T::OFFSET_TO_TAI {
394 OffsetToTai::Fixed(0) => assert_eq!(T::NAME, "TAI"),
395 OffsetToTai::Fixed(_) => { }
396 OffsetToTai::Contextual => {
397 assert!(T::NAME == "UTC" || T::NAME == "GLO")
398 }
399 }
400 }
401 check::<Gps>();
402 check::<Glonass>();
403 check::<Galileo>();
404 check::<Beidou>();
405 check::<Tai>();
406 check::<Utc>();
407 }
408}