1use crate::epoch::CivilDate;
37
38mod private {
40 pub trait Sealed {}
41}
42
43macro_rules! define_scale {
44 (
45 $(#[$meta:meta])*
46 $name:ident,
47 display = $display:literal,
48 offset = $offset:expr,
49 epoch = $epoch:expr,
50 style = $style:expr
51 ) => {
52 $(#[$meta])*
53 #[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
54 pub struct $name;
55
56 impl private::Sealed for $name {}
57
58 impl TimeScale for $name {
59 const NAME: &'static str = $display;
60 const OFFSET_TO_TAI: OffsetToTai = $offset;
61 const EPOCH_CIVIL: CivilDate = $epoch;
62 const DISPLAY_STYLE: DisplayStyle = $style;
63 }
64 };
65}
66
67pub(crate) const NANOS_PER_SECOND: i64 = 1_000_000_000;
68
69#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
84pub enum OffsetToTai {
85 Fixed(i64),
87
88 Contextual,
90}
91
92#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
96pub enum DisplayStyle {
97 WeekTow,
102
103 DayTod,
108
109 Simple,
114}
115
116pub trait TimeScale: private::Sealed + Copy + Clone + Eq + PartialEq + core::fmt::Debug {
129 const NAME: &'static str;
131
132 const OFFSET_TO_TAI: OffsetToTai;
138
139 const EPOCH_CIVIL: CivilDate;
141
142 const DISPLAY_STYLE: DisplayStyle;
144}
145
146define_scale!(
147 Glonass,
154 display = "GLO",
155 offset = OffsetToTai::Contextual,
156 epoch = CivilDate::new(1996, 1, 1),
157 style = DisplayStyle::DayTod
158);
159
160define_scale!(
161 Gps,
168 display = "GPS",
169 offset = OffsetToTai::Fixed(19 * NANOS_PER_SECOND),
170 epoch = CivilDate::new(1980, 1, 6),
171 style = DisplayStyle::WeekTow
172);
173
174define_scale!(
175 Galileo,
182 display = "GAL",
183 offset = OffsetToTai::Fixed(19 * NANOS_PER_SECOND),
184 epoch = CivilDate::new(1999, 8, 22),
185 style = DisplayStyle::WeekTow
186);
187
188define_scale!(
189 Beidou,
196 display = "BDT",
197 offset = OffsetToTai::Fixed(33 * NANOS_PER_SECOND),
198 epoch = CivilDate::new(2006, 1, 1),
199 style = DisplayStyle::WeekTow
200);
201
202define_scale!(
203 Tai,
215 display = "TAI",
216 offset = OffsetToTai::Fixed(0),
217 epoch = CivilDate::new(1958, 1, 1),
218 style = DisplayStyle::Simple
219);
220
221define_scale!(
222 Utc,
228 display = "UTC",
229 offset = OffsetToTai::Contextual,
230 epoch = CivilDate::new(1972, 1, 1),
231 style = DisplayStyle::Simple
232);
233
234impl OffsetToTai {
235 #[inline(always)]
237 #[must_use]
238 pub const fn fixed(self) -> Option<i64> {
239 match self {
240 OffsetToTai::Fixed(v) => Some(v),
241 OffsetToTai::Contextual => None,
242 }
243 }
244
245 #[inline(always)]
247 #[must_use]
248 pub const fn is_contextual(self) -> bool {
249 matches!(self, OffsetToTai::Contextual)
250 }
251
252 #[inline(always)]
254 #[must_use]
255 pub const fn is_fixed(self) -> bool {
256 matches!(self, OffsetToTai::Fixed(_))
257 }
258}
259
260#[cfg(test)]
265mod tests {
266 use std::{collections::HashSet, mem::size_of};
267
268 use super::*;
269
270 #[test]
271 fn test_name_are_correct() {
272 assert_eq!(Glonass::NAME, "GLO");
273 assert_eq!(Gps::NAME, "GPS");
274 assert_eq!(Galileo::NAME, "GAL");
275 assert_eq!(Beidou::NAME, "BDT");
276 assert_eq!(Tai::NAME, "TAI");
277 assert_eq!(Utc::NAME, "UTC");
278 }
279
280 #[test]
281 fn test_fixed_offsets() {
282 assert_eq!(
283 Gps::OFFSET_TO_TAI,
284 OffsetToTai::Fixed(19 * NANOS_PER_SECOND)
285 );
286 assert_eq!(
287 Galileo::OFFSET_TO_TAI,
288 OffsetToTai::Fixed(19 * NANOS_PER_SECOND)
289 );
290 assert_eq!(
291 Beidou::OFFSET_TO_TAI,
292 OffsetToTai::Fixed(33 * NANOS_PER_SECOND)
293 );
294 assert_eq!(Tai::OFFSET_TO_TAI, OffsetToTai::Fixed(0));
295 }
296
297 #[test]
298 fn test_contextual_offsets() {
299 assert_eq!(Utc::OFFSET_TO_TAI, OffsetToTai::Contextual);
300 assert_eq!(Glonass::OFFSET_TO_TAI, OffsetToTai::Contextual);
301 }
302
303 #[test]
304 fn test_scale_types_are_copy() {
305 fn assert_copy<T: Copy>() {}
306 assert_copy::<Glonass>();
307 assert_copy::<Gps>();
308 assert_copy::<Galileo>();
309 assert_copy::<Beidou>();
310 assert_copy::<Tai>();
311 assert_copy::<Utc>();
312 }
313
314 #[test]
315 fn test_gps_and_galileo_are_aligned() {
316 assert_eq!(Gps::OFFSET_TO_TAI, Galileo::OFFSET_TO_TAI);
318 }
319
320 #[test]
321 fn test_tai_invariant_is_valid() {
322 assert_eq!(Tai::OFFSET_TO_TAI, OffsetToTai::Fixed(0));
323 assert!(Tai::OFFSET_TO_TAI.fixed().unwrap() == 0);
324 }
325
326 #[test]
327 fn test_names_are_unique() {
328 let names = [
329 Gps::NAME,
330 Glonass::NAME,
331 Galileo::NAME,
332 Beidou::NAME,
333 Tai::NAME,
334 Utc::NAME,
335 ];
336 let set: HashSet<_> = names.iter().collect();
337
338 assert_eq!(set.len(), names.len());
339 }
340
341 #[test]
342 fn test_fixed_scales_are_really_fixed() {
343 let fixed_scales = [
344 Gps::OFFSET_TO_TAI,
345 Galileo::OFFSET_TO_TAI,
346 Beidou::OFFSET_TO_TAI,
347 Tai::OFFSET_TO_TAI,
348 ];
349
350 for scale in fixed_scales {
351 assert!(scale.fixed().is_some(), "Expected Fixed, got Contextual");
352 }
353 }
354
355 #[test]
356 fn test_contextual_only_where_expected() {
357 assert!(Utc::OFFSET_TO_TAI.is_contextual());
358 assert!(Glonass::OFFSET_TO_TAI.is_contextual());
359 }
360
361 #[test]
362 fn test_scale_is_zero_sized() {
363 assert_eq!(size_of::<Glonass>(), 0);
364 assert_eq!(size_of::<Gps>(), 0);
365 assert_eq!(size_of::<Galileo>(), 0);
366 assert_eq!(size_of::<Beidou>(), 0);
367 assert_eq!(size_of::<Tai>(), 0);
368 assert_eq!(size_of::<Utc>(), 0);
369 }
370
371 #[test]
372 fn test_scale_is_copy() {
373 fn assert_copy<T: Copy + Clone + Eq + PartialEq + core::fmt::Debug>() {}
374 assert_copy::<Glonass>();
375 assert_copy::<Gps>();
376 assert_copy::<Galileo>();
377 assert_copy::<Beidou>();
378 assert_copy::<Tai>();
379 assert_copy::<Utc>();
380 }
381
382 #[test]
383 fn test_display_styles() {
384 assert_eq!(Gps::DISPLAY_STYLE, DisplayStyle::WeekTow);
385 assert_eq!(Glonass::DISPLAY_STYLE, DisplayStyle::DayTod);
386 assert_eq!(Galileo::DISPLAY_STYLE, DisplayStyle::WeekTow);
387 assert_eq!(Beidou::DISPLAY_STYLE, DisplayStyle::WeekTow);
388 assert_eq!(Tai::DISPLAY_STYLE, DisplayStyle::Simple);
389 assert_eq!(Utc::DISPLAY_STYLE, DisplayStyle::Simple);
390 }
391
392 #[test]
393 fn test_offset_to_tai_helpers() {
394 assert!(OffsetToTai::Fixed(0).is_fixed());
395 assert!(!OffsetToTai::Fixed(0).is_contextual());
396 assert!(OffsetToTai::Contextual.is_contextual());
397 assert!(!OffsetToTai::Contextual.is_fixed());
398 assert_eq!(OffsetToTai::Fixed(42).fixed(), Some(42));
399 assert_eq!(OffsetToTai::Contextual.fixed(), None);
400 }
401
402 #[test]
403 fn test_epoch_civil_dates() {
404 assert_eq!(Gps::EPOCH_CIVIL.year, 1980);
405 assert_eq!(Glonass::EPOCH_CIVIL.year, 1996);
406 assert_eq!(Galileo::EPOCH_CIVIL.year, 1999);
407 assert_eq!(Beidou::EPOCH_CIVIL.year, 2006);
408 assert_eq!(Tai::EPOCH_CIVIL.year, 1958);
409 }
410
411 #[test]
412 fn test_tai_invariant() {
413 assert_eq!(Tai::OFFSET_TO_TAI, OffsetToTai::Fixed(0));
414 assert_eq!(Tai::OFFSET_TO_TAI.fixed(), Some(0));
415 }
416
417 #[test]
418 fn test_contract_all_scales() {
419 fn check<T: TimeScale>() {
420 match T::OFFSET_TO_TAI {
421 OffsetToTai::Fixed(0) => assert_eq!(T::NAME, "TAI"),
422 OffsetToTai::Fixed(_) => { }
423 OffsetToTai::Contextual => {
424 assert!(T::NAME == "UTC" || T::NAME == "GLO")
425 }
426 }
427 }
428 check::<Gps>();
429 check::<Glonass>();
430 check::<Galileo>();
431 check::<Beidou>();
432 check::<Tai>();
433 check::<Utc>();
434 }
435}