Skip to main content

lox_time/
time.rs

1// SPDX-FileCopyrightText: 2023 Andrei Zisu <matzipan@gmail.com>
2// SPDX-FileCopyrightText: 2023 Angus Morrison <github@angus-morrison.com>
3// SPDX-FileCopyrightText: 2023 Helge Eichhorn <git@helgeeichhorn.de>
4//
5// SPDX-License-Identifier: MPL-2.0
6
7use std::fmt;
8use std::fmt::Display;
9use std::fmt::Formatter;
10use std::ops::Add;
11use std::ops::Sub;
12use std::str::FromStr;
13
14use itertools::Itertools;
15use lox_core::f64;
16use lox_core::i64;
17use lox_core::types::units::Days;
18use lox_test_utils::approx_eq::ApproxEq;
19use lox_test_utils::approx_eq::results::ApproxEqResults;
20use num::ToPrimitive;
21use thiserror::Error;
22
23use crate::calendar_dates::CalendarDate;
24use crate::calendar_dates::Date;
25use crate::calendar_dates::DateError;
26use crate::deltas::TimeDelta;
27use crate::deltas::ToDelta;
28use crate::julian_dates::Epoch;
29use crate::julian_dates::JulianDate;
30use crate::julian_dates::Unit;
31use crate::offsets::DefaultOffsetProvider;
32use crate::offsets::Offset;
33use crate::offsets::TryOffset;
34use crate::subsecond::Subsecond;
35use crate::time_of_day::CivilTime;
36use crate::time_of_day::TimeOfDay;
37use crate::time_of_day::TimeOfDayError;
38use crate::time_scales::DynTimeScale;
39use crate::time_scales::Tai;
40use crate::time_scales::Tcb;
41use crate::time_scales::Tcg;
42use crate::time_scales::Tdb;
43use crate::time_scales::TimeScale;
44use crate::time_scales::Tt;
45use crate::time_scales::Ut1;
46
47/// Error type for [`Time`] construction failures.
48#[derive(Clone, Debug, Error, PartialEq, Eq)]
49pub enum TimeError {
50    /// Invalid date component.
51    #[error(transparent)]
52    DateError(#[from] DateError),
53    /// Invalid time-of-day component.
54    #[error(transparent)]
55    TimeError(#[from] TimeOfDayError),
56    /// Attempted to represent a leap second in a continuous time scale.
57    #[error("leap seconds do not exist in continuous time scales; use `Utc` instead")]
58    LeapSecondOutsideUtc,
59    /// The ISO 8601 string could not be parsed.
60    #[error("invalid ISO string `{0}`")]
61    InvalidIsoString(String),
62}
63
64/// An instant in time in a given [TimeScale], relative to J2000.
65///
66/// `Time` supports femtosecond precision, but be aware that many algorithms operating on `Time`s
67/// are not accurate to this level of precision.
68#[derive(Clone, Copy, Debug, Default, Eq, PartialEq, PartialOrd, Ord)]
69#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
70pub struct Time<T: TimeScale> {
71    scale: T,
72    delta: TimeDelta,
73}
74
75/// A [`Time`] with a runtime-determined time scale.
76pub type DynTime = Time<DynTimeScale>;
77
78impl<T: TimeScale> Time<T> {
79    /// Instantiates a [Time] in the given [TimeScale] from the count of seconds since J2000, subdivided
80    /// into integral seconds and [Subsecond].
81    pub const fn new(scale: T, seconds: i64, subsecond: Subsecond) -> Self {
82        let delta = TimeDelta::new(seconds, subsecond.as_attoseconds());
83        Self { scale, delta }
84    }
85
86    /// Instantiates a [Time] in the given [TimeScale] from a [Date] and a [TimeOfDay].
87    ///
88    /// # Errors
89    ///
90    /// * Returns `TimeError::LeapSecondsOutsideUtc` if `time` is a leap second, since leap seconds
91    ///   cannot be unambiguously represented by a continuous time format.
92    pub fn from_date_and_time(scale: T, date: Date, time: TimeOfDay) -> Result<Self, TimeError> {
93        let mut seconds = (date.days_since_j2000() * f64::consts::SECONDS_PER_DAY)
94            .to_i64()
95            .unwrap_or_else(|| {
96                unreachable!(
97                    "seconds since J2000 for date {} are not representable as i64: {}",
98                    date,
99                    date.days_since_j2000()
100                )
101            });
102        if time.second() == 60 {
103            return Err(TimeError::LeapSecondOutsideUtc);
104        }
105        seconds += time.second_of_day();
106        Ok(Self::new(scale, seconds, time.subsecond()))
107    }
108
109    /// Instantiates a [Time] in the given [TimeScale] from an ISO 8601 string.
110    ///
111    /// # Errors
112    ///
113    /// * Returns `TimeError::InvalidIsoString` if `iso` is not a valid ISO 8601 timestamp.
114    pub fn from_iso(scale: T, iso: &str) -> Result<Self, TimeError> {
115        let Some((date, time_and_scale)) = iso.split_once('T') else {
116            return Err(TimeError::InvalidIsoString(iso.to_owned()));
117        };
118
119        let (time, scale_abbrv) = time_and_scale
120            .split_whitespace()
121            .collect_tuple()
122            .unwrap_or((time_and_scale, ""));
123
124        if !scale_abbrv.is_empty() && scale_abbrv != scale.abbreviation() {
125            return Err(TimeError::InvalidIsoString(iso.to_owned()));
126        }
127
128        let date: Date = date.parse()?;
129        let time: TimeOfDay = time.parse()?;
130
131        Self::from_date_and_time(scale, date, time)
132    }
133
134    /// Instantiates a [Time] in the given [TimeScale] and a [TimeDelta] relative to J2000.
135    pub const fn from_delta(scale: T, delta: TimeDelta) -> Self {
136        Self { scale, delta }
137    }
138
139    /// Returns the [Time] at `epoch` in the given [TimeScale].
140    ///
141    /// Since [Time] is defined relative to J2000, this is equivalent to the delta between
142    /// J2000 and `epoch`.
143    pub const fn from_epoch(scale: T, epoch: Epoch) -> Self {
144        match epoch {
145            Epoch::JulianDate => Self {
146                scale,
147                delta: TimeDelta::from_seconds(-i64::consts::SECONDS_BETWEEN_JD_AND_J2000),
148            },
149            Epoch::ModifiedJulianDate => Self {
150                scale,
151                delta: TimeDelta::from_seconds(-i64::consts::SECONDS_BETWEEN_MJD_AND_J2000),
152            },
153            Epoch::J1950 => Self {
154                scale,
155                delta: TimeDelta::from_seconds(-i64::consts::SECONDS_BETWEEN_J1950_AND_J2000),
156            },
157            Epoch::J2000 => Self {
158                scale,
159                delta: TimeDelta::ZERO,
160            },
161        }
162    }
163
164    /// Given a Julian date, instantiates a [Time] in the specified [TimeScale], relative to
165    /// `epoch`.
166    pub fn from_julian_date(scale: T, julian_date: Days, epoch: Epoch) -> Self {
167        let delta = TimeDelta::from_julian_date(julian_date, epoch);
168        Self { scale, delta }
169    }
170
171    /// Instantiates a [Time] from a two-part Julian date `jd1 + jd2` in days.
172    pub fn from_two_part_julian_date(scale: T, jd1: Days, jd2: Days) -> Self {
173        let delta = TimeDelta::from_two_part_julian_date(jd1, jd2);
174        Self { scale, delta }
175    }
176
177    /// Returns a [TimeBuilder] for constructing a new [Time] in the given [TimeScale].
178    pub fn builder_with_scale(scale: T) -> TimeBuilder<T> {
179        TimeBuilder::new(scale)
180    }
181
182    /// Returns the timescale
183    pub fn scale(&self) -> T
184    where
185        T: Copy,
186    {
187        self.scale
188    }
189
190    /// Returns a new [Time] with the delta of `self` but time scale `scale`.
191    ///
192    /// Note that the underlying delta is simply copied – no time scale transformation takes place.
193    pub fn with_scale<S: TimeScale>(&self, scale: S) -> Time<S> {
194        Time::from_delta(scale, self.delta)
195    }
196
197    /// Converts this time to `scale` using the given offset `provider`.
198    pub fn try_to_scale<S, P>(&self, scale: S, provider: &P) -> Result<Time<S>, P::Error>
199    where
200        T: Copy,
201        S: TimeScale + Copy,
202        P: TryOffset<T, S> + ?Sized,
203    {
204        let offset = provider.try_offset(self.scale, scale, self.to_delta())?;
205        Ok(self.with_scale_and_delta(scale, offset))
206    }
207
208    /// Converts this time to `scale` using the default offset provider.
209    pub fn to_scale<S>(&self, scale: S) -> Time<S>
210    where
211        T: Copy,
212        S: TimeScale + Copy,
213        DefaultOffsetProvider: Offset<T, S>,
214    {
215        let offset = DefaultOffsetProvider.offset(self.scale, scale, self.to_delta());
216        self.with_scale_and_delta(scale, offset)
217    }
218
219    /// Returns a new [Time] with the delta of `self` adjusted by `delta`, and time scale `scale`.
220    ///
221    /// Note that no time scale transformation takes place beyond the adjustment specified by
222    /// `delta`.
223    pub fn with_scale_and_delta<S: TimeScale>(&self, scale: S, delta: TimeDelta) -> Time<S> {
224        Time::from_delta(scale, self.to_delta() + delta)
225    }
226
227    /// Returns the Julian epoch as a [Time] in the given [TimeScale].
228    pub fn jd0(scale: T) -> Self {
229        Self::from_epoch(scale, Epoch::JulianDate)
230    }
231
232    /// Returns the modified Julian epoch as a [Time] in the given [TimeScale].
233    pub fn mjd0(scale: T) -> Self {
234        Self::from_epoch(scale, Epoch::ModifiedJulianDate)
235    }
236
237    /// Returns the J1950 epoch as a [Time] in the given [TimeScale].
238    pub fn j1950(scale: T) -> Self {
239        Self::from_epoch(scale, Epoch::J1950)
240    }
241
242    /// Returns the J2000 epoch as a [Time] in the given [TimeScale].
243    pub fn j2000(scale: T) -> Self {
244        Self::from_epoch(scale, Epoch::J2000)
245    }
246
247    /// Returns the seconds and subsecond components, or `None` if the delta is non-finite.
248    pub fn as_seconds_and_subsecond(&self) -> Option<(i64, Subsecond)> {
249        self.delta.as_seconds_and_subsecond()
250    }
251
252    /// Returns the number of whole seconds since J2000.
253    pub fn seconds(&self) -> Option<i64> {
254        self.as_seconds_and_subsecond().map(|(seconds, _)| seconds)
255    }
256
257    /// Returns the fraction of a second from the last whole second as an `f64`.
258    pub fn subsecond(&self) -> Option<f64> {
259        self.as_seconds_and_subsecond()
260            .map(|(_, subsecond)| subsecond.as_seconds_f64())
261    }
262}
263
264impl<T: TimeScale + Into<DynTimeScale>> Time<T> {
265    /// Converts this time into a [`DynTime`] with a runtime time scale.
266    pub fn into_dyn(self) -> DynTime {
267        Time::from_delta(self.scale.into(), self.delta)
268    }
269}
270
271impl<T: TimeScale + std::fmt::Debug> ApproxEq for Time<T> {
272    fn approx_eq(&self, rhs: &Self, atol: f64, rtol: f64) -> ApproxEqResults {
273        self.to_delta().approx_eq(&rhs.to_delta(), atol, rtol)
274    }
275}
276
277impl<T: TimeScale> ToDelta for Time<T> {
278    fn to_delta(&self) -> TimeDelta {
279        self.delta
280    }
281}
282
283impl<T: TimeScale> JulianDate for Time<T> {
284    fn julian_date(&self, epoch: Epoch, unit: Unit) -> f64 {
285        self.delta.julian_date(epoch, unit)
286    }
287}
288
289impl<T: TimeScale> Display for Time<T> {
290    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
291        let precision = f.precision().unwrap_or(3);
292        write!(
293            f,
294            "{}T{:.*} {}",
295            self.date(),
296            precision,
297            self.time(),
298            self.scale.abbreviation()
299        )
300    }
301}
302
303impl FromStr for Time<Tai> {
304    type Err = TimeError;
305
306    fn from_str(iso: &str) -> Result<Self, Self::Err> {
307        Self::from_iso(Tai, iso)
308    }
309}
310
311impl FromStr for Time<Tcb> {
312    type Err = TimeError;
313
314    fn from_str(iso: &str) -> Result<Self, Self::Err> {
315        Self::from_iso(Tcb, iso)
316    }
317}
318
319impl FromStr for Time<Tcg> {
320    type Err = TimeError;
321
322    fn from_str(iso: &str) -> Result<Self, Self::Err> {
323        Self::from_iso(Tcg, iso)
324    }
325}
326
327impl FromStr for Time<Tdb> {
328    type Err = TimeError;
329
330    fn from_str(iso: &str) -> Result<Self, Self::Err> {
331        Self::from_iso(Tdb, iso)
332    }
333}
334
335impl FromStr for Time<Tt> {
336    type Err = TimeError;
337
338    fn from_str(iso: &str) -> Result<Self, Self::Err> {
339        Self::from_iso(Tt, iso)
340    }
341}
342
343impl FromStr for Time<Ut1> {
344    type Err = TimeError;
345
346    fn from_str(iso: &str) -> Result<Self, Self::Err> {
347        Self::from_iso(Ut1, iso)
348    }
349}
350
351impl<T: TimeScale> Add<TimeDelta> for Time<T> {
352    type Output = Self;
353
354    fn add(self, rhs: TimeDelta) -> Self::Output {
355        Self {
356            scale: self.scale,
357            delta: self.delta + rhs,
358        }
359    }
360}
361
362impl<T: TimeScale> Sub<TimeDelta> for Time<T> {
363    type Output = Self;
364
365    fn sub(self, rhs: TimeDelta) -> Self::Output {
366        Self {
367            scale: self.scale,
368            delta: self.delta - rhs,
369        }
370    }
371}
372
373impl<T: TimeScale> Sub<Time<T>> for Time<T> {
374    type Output = TimeDelta;
375
376    fn sub(self, rhs: Time<T>) -> Self::Output {
377        self.delta - rhs.delta
378    }
379}
380
381impl<T: TimeScale> CivilTime for Time<T> {
382    fn time(&self) -> TimeOfDay {
383        debug_assert!(self.delta.is_finite());
384        let (seconds, subsecond) = self.as_seconds_and_subsecond().unwrap();
385        TimeOfDay::from_seconds_since_j2000(seconds).with_subsecond(subsecond)
386    }
387}
388
389impl<T: TimeScale> CalendarDate for Time<T> {
390    fn date(&self) -> Date {
391        debug_assert!(self.delta.is_finite());
392        let seconds = self.seconds().unwrap();
393        Date::from_seconds_since_j2000(seconds)
394    }
395}
396
397/// `TimeBuilder` supports the construction of [Time] instances piecewise using the builder pattern.
398#[derive(Debug, Clone, PartialEq, Eq)]
399pub struct TimeBuilder<T: TimeScale> {
400    scale: T,
401    date: Result<Date, DateError>,
402    time: Result<TimeOfDay, TimeOfDayError>,
403}
404
405impl<T: TimeScale> TimeBuilder<T> {
406    /// Returns a new [TimeBuilder], equivalent to a [Time] at J2000 in the given [TimeScale].
407    pub fn new(scale: T) -> Self {
408        Self {
409            scale,
410            date: Ok(Date::default()),
411            time: Ok(TimeOfDay::default()),
412        }
413    }
414
415    /// Sets the `year`, `month`, and `day` of the [Time] under construction.
416    pub fn with_ymd(self, year: i64, month: u8, day: u8) -> Self {
417        Self {
418            date: Date::new(year, month, day),
419            ..self
420        }
421    }
422
423    /// Sets the `year` and `day_of_year` of the [Time] under construction.
424    pub fn with_doy(self, year: i64, day_of_year: u16) -> Self {
425        Self {
426            date: Date::from_day_of_year(year, day_of_year),
427            ..self
428        }
429    }
430
431    /// Sets the `hour`, `minute`, and decimal `seconds` of the [Time] under construction.
432    pub fn with_hms(self, hour: u8, minute: u8, seconds: f64) -> Self {
433        Self {
434            time: TimeOfDay::from_hms(hour, minute, seconds),
435            ..self
436        }
437    }
438
439    /// Builds the [Time] instance.
440    ///
441    /// # Errors
442    ///
443    /// * [DateError] if `ymd` data passed into the builder did not correspond to a valid date;
444    /// * [TimeOfDayError] if `hms` data passed into the builder did not correspond to a valid time
445    ///   of day.
446    pub fn build(self) -> Result<Time<T>, TimeError> {
447        let date = self.date?;
448        let time = self.time?;
449        Time::from_date_and_time(self.scale, date, time)
450    }
451}
452
453/// Convenience macro to simplify the construction of [Time] instances.
454///
455/// # Examples
456///
457/// ```
458/// use lox_time::Time;
459/// use lox_time::time;
460/// use lox_time::time_scales::Tai;
461///
462///
463/// time!(Tai, 2020, 1, 2); // 2020-01-02T00:00:00.000 TAI
464/// time!(Tai, 2020, 1, 2, 3) ; // 2020-01-02T03:00:00.000 TAI
465/// time!(Tai, 2020, 1, 2, 3, 4); // 2020-01-02T03:04:00.000 TAI
466/// time!(Tai, 2020, 1, 2, 3, 4, 5.006); // 2020-01-02T03:04:05.006 TAI
467/// ```
468#[macro_export]
469macro_rules! time {
470    ($scale:expr, $year:literal, $month:literal, $day:literal) => {
471        Time::builder_with_scale($scale)
472            .with_ymd($year, $month, $day)
473            .build()
474    };
475    ($scale:expr, $year:literal, $month:literal, $day:literal, $hour:literal) => {
476        Time::builder_with_scale($scale)
477            .with_ymd($year, $month, $day)
478            .with_hms($hour, 0, 0.0)
479            .build()
480    };
481    ($scale:expr, $year:literal, $month:literal, $day:literal, $hour:literal, $minute:literal) => {
482        Time::builder_with_scale($scale)
483            .with_ymd($year, $month, $day)
484            .with_hms($hour, $minute, 0.0)
485            .build()
486    };
487    ($scale:expr, $year:literal, $month:literal, $day:literal, $hour:literal, $minute:literal, $second:literal) => {
488        Time::builder_with_scale($scale)
489            .with_ymd($year, $month, $day)
490            .with_hms($hour, $minute, $second)
491            .build()
492    };
493}
494
495#[cfg(test)]
496mod tests {
497    use lox_core::f64::consts::DAYS_PER_JULIAN_CENTURY;
498    use lox_test_utils::assert_approx_eq;
499    use rstest::rstest;
500
501    use crate::Time;
502    use crate::time_scales::{Tai, Tdb, Tt};
503    use lox_core::i64::consts::{SECONDS_PER_DAY, SECONDS_PER_HALF_DAY};
504
505    use super::*;
506
507    use lox_core::i64::consts::{
508        SECONDS_BETWEEN_J1950_AND_J2000, SECONDS_BETWEEN_JD_AND_J2000,
509        SECONDS_BETWEEN_MJD_AND_J2000, SECONDS_PER_HOUR, SECONDS_PER_JULIAN_CENTURY,
510        SECONDS_PER_MINUTE,
511    };
512
513    #[test]
514    fn test_time_builder() {
515        let time = Time::builder_with_scale(Tai)
516            .with_ymd(2000, 1, 1)
517            .build()
518            .unwrap();
519        assert_eq!(time.seconds(), Some(-SECONDS_PER_HALF_DAY));
520        let time = Time::builder_with_scale(Tai)
521            .with_ymd(2000, 1, 1)
522            .with_hms(12, 0, 0.0)
523            .build()
524            .unwrap();
525        assert_eq!(time.seconds(), Some(0));
526    }
527
528    #[test]
529    fn test_time_from_seconds() {
530        let scale = Tai;
531        let seconds = 1234567890;
532        let subsecond = Subsecond::from_f64(0.9876543210).unwrap();
533        let expected = Time::new(scale, seconds, subsecond);
534        let actual = Time::new(scale, seconds, subsecond);
535        assert_eq!(expected, actual);
536    }
537
538    #[rstest]
539    #[case(Epoch::JulianDate, -SECONDS_BETWEEN_JD_AND_J2000)]
540    #[case(Epoch::ModifiedJulianDate, -SECONDS_BETWEEN_MJD_AND_J2000)]
541    #[case(Epoch::J1950, -SECONDS_BETWEEN_J1950_AND_J2000)]
542    #[case(Epoch::J2000, 0)]
543    fn test_time_from_julian_date(#[case] epoch: Epoch, #[case] seconds: i64) {
544        let time = Time::from_julian_date(Tai, 0.0, epoch);
545        assert_eq!(time.seconds(), Some(seconds));
546    }
547
548    #[test]
549    fn test_time_from_julian_date_subsecond() {
550        let time = Time::from_julian_date(Tai, 0.3 / f64::consts::SECONDS_PER_DAY, Epoch::J2000);
551        assert_approx_eq!(time.subsecond().unwrap(), 0.3, atol <= 1e-15);
552    }
553
554    #[test]
555    fn test_time_from_two_part_julian_date() {
556        let t0 = time!(Tai, 2024, 7, 11, 8, 2, 14.0).unwrap();
557        let (jd1, jd2) = t0.two_part_julian_date();
558        let t1 = Time::from_two_part_julian_date(Tai, jd1, jd2);
559        assert_approx_eq!(t0, t1);
560    }
561
562    #[rstest]
563    #[case(i64::MAX as f64, 1.0)]
564    #[case(i64::MIN as f64, -1.0)]
565    fn test_time_from_two_part_julian_date_edge_cases(#[case] jd1: f64, #[case] jd2: f64) {
566        let time = Time::from_two_part_julian_date(Tai, jd1, jd2);
567        // Edge cases now result in non-finite TimeDelta variants (NaN, PosInf, NegInf)
568        assert!(!time.to_delta().is_finite());
569    }
570
571    #[rstest]
572    #[case(
573        (SECONDS_BETWEEN_JD_AND_J2000 as f64) / f64::consts::SECONDS_PER_DAY,
574        0.0,
575        0,
576    )]
577    #[case(
578        (SECONDS_BETWEEN_JD_AND_J2000 as f64 + 0.5) / f64::consts::SECONDS_PER_DAY,
579        0.6 / f64::consts::SECONDS_PER_DAY,
580        1,
581    )]
582    #[case(
583        (SECONDS_BETWEEN_JD_AND_J2000 as f64 + 0.5) / f64::consts::SECONDS_PER_DAY,
584        -0.6 / f64::consts::SECONDS_PER_DAY,
585        -1,
586    )]
587    fn test_time_from_two_part_julian_date_adjustments(
588        #[case] jd1: f64,
589        #[case] jd2: f64,
590        #[case] expected: i64,
591    ) {
592        let time = Time::from_two_part_julian_date(Tai, jd1, jd2);
593        assert_eq!(time.seconds(), Some(expected));
594    }
595
596    #[test]
597    fn test_time_with_scale_and_delta() {
598        let tai: Time<Tai> = Time::default();
599        let delta = TimeDelta::from_seconds(20);
600        let tdb = tai.with_scale_and_delta(Tdb, delta);
601        assert_eq!(tdb.scale(), Tdb);
602        assert_eq!(tdb.seconds(), Some(tai.seconds().unwrap() + 20));
603    }
604
605    #[rstest]
606    #[case(f64::INFINITY)]
607    #[case(-f64::INFINITY)]
608    #[case(f64::NAN)]
609    #[case(-f64::NAN)]
610    #[case(i64::MAX as f64 / f64::consts::SECONDS_PER_DAY + 1.0)]
611    #[case(i64::MIN as f64 / f64::consts::SECONDS_PER_DAY - 1.0)]
612    fn test_time_from_julian_date_special_values(#[case] julian_date: f64) {
613        let time = Time::from_julian_date(Tai, julian_date, Epoch::J2000);
614        // Special values (NaN, Infinity) result in non-finite TimeDelta
615        assert!(!time.to_delta().is_finite());
616    }
617
618    #[rstest]
619    #[case("2000-01-01T00:00:00", Ok(time!(Tai, 2000, 1, 1).unwrap()))]
620    #[case("2000-01-01T00:00:00 TAI", Ok(time!(Tai, 2000, 1, 1).unwrap()))]
621    #[case("2000-1-01T00:00:00", Err(TimeError::DateError(DateError::InvalidIsoString("2000-1-01".to_string()))))]
622    #[case("2000-01-01T0:00:00", Err(TimeError::TimeError(TimeOfDayError::InvalidIsoString("0:00:00".to_string()))))]
623    #[case("2000-01-01-00:00:00", Err(TimeError::InvalidIsoString("2000-01-01-00:00:00".to_string())))]
624    #[case("2000-01-01T00:00:00 UTC", Err(TimeError::InvalidIsoString("2000-01-01T00:00:00 UTC".to_string())))]
625    fn test_time_from_str_tai(#[case] iso: &str, #[case] expected: Result<Time<Tai>, TimeError>) {
626        let actual: Result<Time<Tai>, TimeError> = iso.parse();
627        assert_eq!(actual, expected)
628    }
629
630    #[rstest]
631    #[case("2000-01-01T00:00:00", Ok(time!(Tcb, 2000, 1, 1).unwrap()))]
632    #[case("2000-01-01T00:00:00 TCB", Ok(time!(Tcb, 2000, 1, 1).unwrap()))]
633    #[case("2000-1-01T00:00:00", Err(TimeError::DateError(DateError::InvalidIsoString("2000-1-01".to_string()))))]
634    #[case("2000-01-01T0:00:00", Err(TimeError::TimeError(TimeOfDayError::InvalidIsoString("0:00:00".to_string()))))]
635    #[case("2000-01-01-00:00:00", Err(TimeError::InvalidIsoString("2000-01-01-00:00:00".to_string())))]
636    #[case("2000-01-01T00:00:00 UTC", Err(TimeError::InvalidIsoString("2000-01-01T00:00:00 UTC".to_string())))]
637    fn test_time_from_str_tcb(#[case] iso: &str, #[case] expected: Result<Time<Tcb>, TimeError>) {
638        let actual: Result<Time<Tcb>, TimeError> = iso.parse();
639        assert_eq!(actual, expected)
640    }
641
642    #[rstest]
643    #[case("2000-01-01T00:00:00", Ok(time!(Tcg, 2000, 1, 1).unwrap()))]
644    #[case("2000-01-01T00:00:00 TCG", Ok(time!(Tcg, 2000, 1, 1).unwrap()))]
645    #[case("2000-1-01T00:00:00", Err(TimeError::DateError(DateError::InvalidIsoString("2000-1-01".to_string()))))]
646    #[case("2000-01-01T0:00:00", Err(TimeError::TimeError(TimeOfDayError::InvalidIsoString("0:00:00".to_string()))))]
647    #[case("2000-01-01-00:00:00", Err(TimeError::InvalidIsoString("2000-01-01-00:00:00".to_string())))]
648    #[case("2000-01-01T00:00:00 UTC", Err(TimeError::InvalidIsoString("2000-01-01T00:00:00 UTC".to_string())))]
649    fn test_time_from_str_tcg(#[case] iso: &str, #[case] expected: Result<Time<Tcg>, TimeError>) {
650        let actual: Result<Time<Tcg>, TimeError> = iso.parse();
651        assert_eq!(actual, expected)
652    }
653
654    #[rstest]
655    #[case("2000-01-01T00:00:00", Ok(time!(Tdb, 2000, 1, 1).unwrap()))]
656    #[case("2000-01-01T00:00:00 TDB", Ok(time!(Tdb, 2000, 1, 1).unwrap()))]
657    #[case("2000-1-01T00:00:00", Err(TimeError::DateError(DateError::InvalidIsoString("2000-1-01".to_string()))))]
658    #[case("2000-01-01T0:00:00", Err(TimeError::TimeError(TimeOfDayError::InvalidIsoString("0:00:00".to_string()))))]
659    #[case("2000-01-01-00:00:00", Err(TimeError::InvalidIsoString("2000-01-01-00:00:00".to_string())))]
660    #[case("2000-01-01T00:00:00 UTC", Err(TimeError::InvalidIsoString("2000-01-01T00:00:00 UTC".to_string())))]
661    fn test_time_from_str_tdb(#[case] iso: &str, #[case] expected: Result<Time<Tdb>, TimeError>) {
662        let actual: Result<Time<Tdb>, TimeError> = iso.parse();
663        assert_eq!(actual, expected)
664    }
665
666    #[rstest]
667    #[case("2000-01-01T00:00:00", Ok(time!(Tt, 2000, 1, 1).unwrap()))]
668    #[case("2000-01-01T00:00:00 TT", Ok(time!(Tt, 2000, 1, 1).unwrap()))]
669    #[case("2000-1-01T00:00:00", Err(TimeError::DateError(DateError::InvalidIsoString("2000-1-01".to_string()))))]
670    #[case("2000-01-01T0:00:00", Err(TimeError::TimeError(TimeOfDayError::InvalidIsoString("0:00:00".to_string()))))]
671    #[case("2000-01-01-00:00:00", Err(TimeError::InvalidIsoString("2000-01-01-00:00:00".to_string())))]
672    #[case("2000-01-01T00:00:00 UTC", Err(TimeError::InvalidIsoString("2000-01-01T00:00:00 UTC".to_string())))]
673    fn test_time_from_str_tt(#[case] iso: &str, #[case] expected: Result<Time<Tt>, TimeError>) {
674        let actual: Result<Time<Tt>, TimeError> = iso.parse();
675        assert_eq!(actual, expected)
676    }
677
678    #[rstest]
679    #[case("2000-01-01T00:00:00", Ok(time!(Ut1, 2000, 1, 1).unwrap()))]
680    #[case("2000-01-01T00:00:00 UT1", Ok(time!(Ut1, 2000, 1, 1).unwrap()))]
681    #[case("2000-1-01T00:00:00", Err(TimeError::DateError(DateError::InvalidIsoString("2000-1-01".to_string()))))]
682    #[case("2000-01-01T0:00:00", Err(TimeError::TimeError(TimeOfDayError::InvalidIsoString("0:00:00".to_string()))))]
683    #[case("2000-01-01-00:00:00", Err(TimeError::InvalidIsoString("2000-01-01-00:00:00".to_string())))]
684    #[case("2000-01-01T00:00:00 UTC", Err(TimeError::InvalidIsoString("2000-01-01T00:00:00 UTC".to_string())))]
685    fn test_time_from_str_ut1(#[case] iso: &str, #[case] expected: Result<Time<Ut1>, TimeError>) {
686        let actual: Result<Time<Ut1>, TimeError> = iso.parse();
687        assert_eq!(actual, expected)
688    }
689
690    #[test]
691    fn test_time_display() {
692        let time = Time::j2000(Tai);
693        let expected = "2000-01-01T12:00:00.000 TAI".to_string();
694        let actual = time.to_string();
695        assert_eq!(expected, actual);
696        let expected = "2000-01-01T12:00:00.000000000000000 TAI".to_string();
697        let actual = format!("{time:.15}");
698        assert_eq!(expected, actual);
699    }
700
701    #[test]
702    fn test_time_j2000() {
703        let actual = Time::j2000(Tai);
704        let expected = Time {
705            scale: Tai,
706            ..Default::default()
707        };
708        assert_eq!(expected, actual);
709    }
710
711    #[test]
712    fn test_time_jd0() {
713        let actual = Time::jd0(Tai);
714        let expected = Time::new(Tai, -211813488000, Subsecond::default());
715        assert_eq!(expected, actual);
716    }
717
718    #[test]
719    fn test_time_seconds() {
720        let time = Time::new(Tai, 1234567890, Subsecond::from_f64(0.9876543210).unwrap());
721        let expected = Some(1234567890);
722        let actual = time.seconds();
723        assert_eq!(
724            expected, actual,
725            "expected Time to have {expected:?} seconds, but got {actual:?}"
726        );
727    }
728
729    #[test]
730    fn test_julian_date() {
731        let time = Time::jd0(Tdb);
732        assert_eq!(time.julian_date(Epoch::JulianDate, Unit::Days), 0.0);
733        assert_eq!(time.seconds_since_julian_epoch(), 0.0);
734        assert_eq!(time.days_since_julian_epoch(), 0.0);
735        assert_eq!(time.centuries_since_julian_epoch(), 0.0);
736    }
737
738    #[test]
739    fn test_modified_julian_date() {
740        let time = Time::mjd0(Tdb);
741        assert_eq!(time.julian_date(Epoch::ModifiedJulianDate, Unit::Days), 0.0);
742        assert_eq!(time.seconds_since_modified_julian_epoch(), 0.0);
743        assert_eq!(time.days_since_modified_julian_epoch(), 0.0);
744        assert_eq!(time.centuries_since_modified_julian_epoch(), 0.0);
745    }
746
747    #[test]
748    fn test_j1950() {
749        let time = Time::j1950(Tdb);
750        assert_eq!(time.julian_date(Epoch::J1950, Unit::Days), 0.0);
751        assert_eq!(time.seconds_since_j1950(), 0.0);
752        assert_eq!(time.days_since_j1950(), 0.0);
753        assert_eq!(time.centuries_since_j1950(), 0.0);
754    }
755
756    #[test]
757    fn test_j2000() {
758        let time = Time::j2000(Tdb);
759        assert_eq!(time.julian_date(Epoch::J2000, Unit::Days), 0.0);
760        assert_eq!(time.seconds_since_j2000(), 0.0);
761        assert_eq!(time.days_since_j2000(), 0.0);
762        assert_eq!(time.centuries_since_j2000(), 0.0);
763    }
764
765    #[test]
766    fn test_j2100() {
767        let time = time!(Tdb, 2100, 1, 1, 12).unwrap();
768        assert_eq!(
769            time.julian_date(Epoch::J2000, Unit::Days),
770            DAYS_PER_JULIAN_CENTURY
771        );
772        assert_eq!(time.seconds_since_j2000(), 3155760000.0);
773        assert_eq!(time.days_since_j2000(), DAYS_PER_JULIAN_CENTURY);
774        assert_eq!(time.centuries_since_j2000(), 1.0);
775    }
776
777    #[test]
778    fn test_two_part_julian_date() {
779        let time = time!(Tdb, 2100, 1, 2).unwrap();
780        let (jd1, jd2) = time.two_part_julian_date();
781        assert_eq!(jd1, 2451545.0 + DAYS_PER_JULIAN_CENTURY);
782        assert_eq!(jd2, 0.5);
783    }
784
785    #[test]
786    fn test_time_macro() {
787        let time = time!(Tai, 2000, 1, 1).unwrap();
788        assert_eq!(time.seconds(), Some(-SECONDS_PER_HALF_DAY));
789        let time = time!(Tai, 2000, 1, 1, 12).unwrap();
790        assert_eq!(time.seconds(), Some(0));
791        let time = time!(Tai, 2000, 1, 1, 12, 0).unwrap();
792        assert_eq!(time.seconds(), Some(0));
793        let time = time!(Tai, 2000, 1, 1, 12, 0, 0.0).unwrap();
794        assert_eq!(time.seconds(), Some(0));
795        // TODO: Fix subsecond handling in TimeOfDay::from_hms or time builder
796        // let time = time!(Tai, 2000, 1, 1, 12, 0, 0.123).unwrap();
797        // assert_eq!(time.seconds(), Some(0));
798        // assert_approx_eq!(time.subsecond().unwrap(), 0.123, atol <= 1e-12);
799    }
800
801    #[test]
802    fn test_time_subsecond() {
803        let time = Time::new(Tai, 0, Subsecond::from_f64(0.123).unwrap());
804        assert_eq!(time.subsecond(), Some(0.123));
805    }
806
807    #[rstest]
808    #[case::zero_delta(Time::default(), Time::default(), TimeDelta::default())]
809    #[case::positive_delta(Time::default(), Time::new(Tai, 1, Subsecond::default()), TimeDelta::from_seconds(-1))]
810    #[case::negative_delta(Time::default(), Time::new(Tai, -1, Subsecond::default()), TimeDelta::from_seconds(1))]
811    fn test_time_delta(
812        #[case] lhs: Time<Tai>,
813        #[case] rhs: Time<Tai>,
814        #[case] expected: TimeDelta,
815    ) {
816        assert_eq!(expected, lhs - rhs);
817    }
818
819    const MAX_FEMTOSECONDS: Subsecond = Subsecond::from_attoseconds(999_999_999_999_999);
820
821    #[rstest]
822    #[case::zero_value(Time::new(Tai, 0, Subsecond::default()), 12)]
823    #[case::one_femtosecond_less_than_an_hour(Time::new(Tai, SECONDS_PER_HOUR - 1, MAX_FEMTOSECONDS), 12)]
824    #[case::exactly_one_hour(Time::new(Tai, SECONDS_PER_HOUR, Subsecond::default()), 13)]
825    #[case::half_day(Time::new(Tai, SECONDS_PER_DAY / 2, Subsecond::default()), 0)]
826    #[case::negative_half_day(Time::new(Tai, -SECONDS_PER_DAY / 2, Subsecond::default()), 0)]
827    #[case::one_day_and_one_hour(Time::new(Tai, SECONDS_PER_HOUR * 25, Subsecond::default()), 13)]
828    #[case::one_femtosecond_less_than_the_epoch(Time::new(Tai, -1, MAX_FEMTOSECONDS), 11)]
829    #[case::one_hour_less_than_the_epoch(Time::new(Tai, -SECONDS_PER_HOUR, Subsecond::default()), 11)]
830    #[case::one_hour_and_one_femtosecond_less_than_the_epoch(Time::new(Tai, -SECONDS_PER_HOUR - 1, MAX_FEMTOSECONDS), 10)]
831    #[case::one_day_less_than_the_epoch(Time::new(Tai, -SECONDS_PER_DAY, Subsecond::default()), 12)]
832    #[case::one_day_and_one_hour_less_than_the_epoch(Time::new(Tai, -SECONDS_PER_DAY - SECONDS_PER_HOUR, Subsecond::default()), 11)]
833    #[case::two_days_less_than_the_epoch(Time::new(Tai, -SECONDS_PER_DAY * 2, Subsecond::default()), 12)]
834    fn test_time_civil_time_hour(#[case] time: Time<Tai>, #[case] expected: u8) {
835        let actual = time.hour();
836        assert_eq!(expected, actual);
837    }
838
839    #[rstest]
840    #[case::zero_value(Time::new(Tai, 0, Subsecond::default()), 0)]
841    #[case::one_femtosecond_less_than_one_minute(Time::new(Tai, SECONDS_PER_MINUTE - 1, MAX_FEMTOSECONDS), 0)]
842    #[case::one_minute(Time::new(Tai, SECONDS_PER_MINUTE, Subsecond::default()), 1)]
843    #[case::one_femtosecond_less_than_an_hour(Time::new(Tai, SECONDS_PER_HOUR - 1, MAX_FEMTOSECONDS), 59)]
844    #[case::exactly_one_hour(Time::new(Tai, SECONDS_PER_HOUR, Subsecond::default()), 0)]
845    #[case::one_hour_and_one_minute(Time::new(Tai, SECONDS_PER_HOUR + SECONDS_PER_MINUTE, Subsecond::default()), 1)]
846    #[case::one_hour_less_than_the_epoch(Time::new(Tai, -SECONDS_PER_HOUR, Subsecond::default()), 0)]
847    #[case::one_femtosecond_less_than_the_epoch(Time::new(Tai, -1, MAX_FEMTOSECONDS), 59)]
848    #[case::one_minute_less_than_the_epoch(Time::new(Tai, -SECONDS_PER_MINUTE, Subsecond::default()), 59)]
849    #[case::one_minute_and_one_femtosecond_less_than_the_epoch(Time::new(Tai, -SECONDS_PER_MINUTE - 1, MAX_FEMTOSECONDS), 58)]
850    fn test_time_civil_time_minute(#[case] time: Time<Tai>, #[case] expected: u8) {
851        let actual = time.minute();
852        assert_eq!(expected, actual);
853    }
854
855    #[rstest]
856    #[case::zero_value(Time::new(Tai, 0, Subsecond::default()), 0)]
857    #[case::one_femtosecond_less_than_one_second(Time::new(Tai, 0, MAX_FEMTOSECONDS), 0)]
858    #[case::one_second(Time::new(Tai, 1, Subsecond::default()), 1)]
859    #[case::one_femtosecond_less_than_a_minute(Time::new(Tai, SECONDS_PER_MINUTE - 1, MAX_FEMTOSECONDS), 59)]
860    #[case::exactly_one_minute(Time::new(Tai, SECONDS_PER_MINUTE, Subsecond::default()), 0)]
861    #[case::one_minute_and_one_second(Time::new(Tai, SECONDS_PER_MINUTE + 1, Subsecond::default()), 1)]
862    #[case::one_femtosecond_less_than_the_epoch(Time::new(Tai, -1, MAX_FEMTOSECONDS), 59)]
863    #[case::one_second_less_than_the_epoch(Time::new(Tai, -1, Subsecond::default()), 59)]
864    #[case::one_second_and_one_femtosecond_less_than_the_epoch(Time::new(Tai, -2, MAX_FEMTOSECONDS), 58)]
865    #[case::one_minute_less_than_the_epoch(Time::new(Tai, -SECONDS_PER_MINUTE, Subsecond::default()), 0)]
866    fn test_time_civil_time_second(#[case] time: Time<Tai>, #[case] expected: u8) {
867        let actual = time.second();
868        assert_eq!(expected, actual);
869    }
870
871    const POSITIVE_BASE_TIME_SUBSECONDS_FIXTURE: Time<Tai> = Time::new(
872        Tai,
873        0,
874        Subsecond::new()
875            .set_milliseconds(123)
876            .set_microseconds(456)
877            .set_nanoseconds(789)
878            .set_picoseconds(12)
879            .set_femtoseconds(345),
880    );
881
882    const NEGATIVE_BASE_TIME_SUBSECONDS_FIXTURE: Time<Tai> = Time::new(
883        Tai,
884        -1,
885        Subsecond::new()
886            .set_milliseconds(123)
887            .set_microseconds(456)
888            .set_nanoseconds(789)
889            .set_picoseconds(12)
890            .set_femtoseconds(345),
891    );
892
893    #[rstest]
894    #[case::positive_time_millisecond(
895        POSITIVE_BASE_TIME_SUBSECONDS_FIXTURE,
896        CivilTime::millisecond,
897        123
898    )]
899    #[case::positive_time_microsecond(
900        POSITIVE_BASE_TIME_SUBSECONDS_FIXTURE,
901        CivilTime::microsecond,
902        456
903    )]
904    #[case::positive_time_nanosecond(
905        POSITIVE_BASE_TIME_SUBSECONDS_FIXTURE,
906        CivilTime::nanosecond,
907        789
908    )]
909    #[case::positive_time_picosecond(
910        POSITIVE_BASE_TIME_SUBSECONDS_FIXTURE,
911        CivilTime::picosecond,
912        12
913    )]
914    #[case::positive_time_femtosecond(
915        POSITIVE_BASE_TIME_SUBSECONDS_FIXTURE,
916        CivilTime::femtosecond,
917        345
918    )]
919    #[case::negative_time_millisecond(
920        NEGATIVE_BASE_TIME_SUBSECONDS_FIXTURE,
921        CivilTime::millisecond,
922        123
923    )]
924    #[case::negative_time_microsecond(
925        NEGATIVE_BASE_TIME_SUBSECONDS_FIXTURE,
926        CivilTime::microsecond,
927        456
928    )]
929    #[case::negative_time_nanosecond(
930        NEGATIVE_BASE_TIME_SUBSECONDS_FIXTURE,
931        CivilTime::nanosecond,
932        789
933    )]
934    #[case::negative_time_picosecond(
935        NEGATIVE_BASE_TIME_SUBSECONDS_FIXTURE,
936        CivilTime::picosecond,
937        12
938    )]
939    #[case::negative_time_femtosecond(
940        NEGATIVE_BASE_TIME_SUBSECONDS_FIXTURE,
941        CivilTime::femtosecond,
942        345
943    )]
944    fn test_time_subseconds(
945        #[case] time: Time<Tai>,
946        #[case] f: fn(&Time<Tai>) -> u32,
947        #[case] expected: u32,
948    ) {
949        let actual = f(&time);
950        assert_eq!(expected, actual);
951    }
952
953    #[rstest]
954    #[case::zero_delta(Time::default(), TimeDelta::default(), Time::default())]
955    #[case::pos_delta_no_carry(Time::new(Tai, 1, Subsecond::new().set_milliseconds(300)), TimeDelta::from_seconds_and_subsecond(1, Subsecond::new().set_milliseconds(600)), Time::new(Tai, 2, Subsecond::new().set_milliseconds(900)))]
956    #[case::pos_delta_with_carry(Time::new(Tai, 1, Subsecond::new().set_milliseconds(300)), TimeDelta::from_seconds_and_subsecond(1, Subsecond::new().set_milliseconds(900)), Time::new(Tai, 3, Subsecond::new().set_milliseconds(200)))]
957    #[case::neg_delta_no_carry(Time::new(Tai, 1, Subsecond::new().set_milliseconds(600)), TimeDelta::from_seconds_and_subsecond(-2, Subsecond::new().set_milliseconds(700)), Time::new(Tai, 0, Subsecond::new().set_milliseconds(300)))]
958    #[case::neg_delta_with_carry(Time::new(Tai, 1, Subsecond::new().set_milliseconds(600)), TimeDelta::from_seconds_and_subsecond(-2, Subsecond::new().set_milliseconds(300)), Time::new(Tai, -1, Subsecond::new().set_milliseconds(900)))]
959    fn test_time_add_time_delta(
960        #[case] time: Time<Tai>,
961        #[case] delta: TimeDelta,
962        #[case] expected: Time<Tai>,
963    ) {
964        let actual = time + delta;
965        assert_eq!(expected, actual);
966    }
967
968    #[rstest]
969    #[case::zero_delta(Time::default(), TimeDelta::default(), Time::default())]
970    #[case::pos_delta_no_carry(Time::new(Tai, 1, Subsecond::new().set_milliseconds(900)), TimeDelta::from_seconds_and_subsecond(1, Subsecond::new().set_milliseconds(300)), Time::new(Tai, 0, Subsecond::new().set_milliseconds(600)))]
971    #[case::pos_delta_with_carry(Time::new(Tai, 1, Subsecond::new().set_milliseconds(300)), TimeDelta::from_seconds_and_subsecond(1, Subsecond::new().set_milliseconds(400)), Time::new(Tai, -1, Subsecond::new().set_milliseconds(900)))]
972    #[case::neg_delta_no_carry(Time::new(Tai, 1, Subsecond::new().set_milliseconds(600)), TimeDelta::from_seconds_and_subsecond(-1, Subsecond::new().set_milliseconds(700)), Time::new(Tai, 1, Subsecond::new().set_milliseconds(900)))]
973    #[case::neg_delta_with_carry(Time::new(Tai, 1, Subsecond::new().set_milliseconds(900)), TimeDelta::from_seconds_and_subsecond(-1, Subsecond::new().set_milliseconds(300)), Time::new(Tai, 2, Subsecond::new().set_milliseconds(600)))]
974    fn test_time_sub_time_delta(
975        #[case] time: Time<Tai>,
976        #[case] delta: TimeDelta,
977        #[case] expected: Time<Tai>,
978    ) {
979        let actual = time - delta;
980        assert_eq!(expected, actual);
981    }
982
983    #[rstest]
984    #[case(Time::default(), Time::default())]
985    #[case(Time::default(), Time::new(Tai, 1, Subsecond::new().set_milliseconds(900)))]
986    #[case(
987        Time::new(Tai, 0, Subsecond::new().set_milliseconds(900)),
988        Time::new(Tai, 1, Subsecond::new().set_milliseconds(600))
989    )]
990    #[case(Time::new(Tai, 1, Subsecond::new().set_milliseconds(900)), Time::default())]
991    #[case(
992        Time::new(Tai, 1, Subsecond::new().set_milliseconds(600)),
993        Time::new(Tai, 0, Subsecond::new().set_milliseconds(900))
994    )]
995    #[case(Time::new(Tai, 1, Subsecond::new().set_milliseconds(600)), Time::new(Tai, -1, Subsecond::new().set_milliseconds(900)), )]
996    #[case(Time::new(Tai, -1, Subsecond::new().set_milliseconds(900)), Time::new(Tai, 1, Subsecond::new().set_milliseconds(600)), )]
997    #[case(Time::new(Tai, 1, Subsecond::new().set_milliseconds(900)), Time::new(Tai, -1, Subsecond::new().set_milliseconds(600)), )]
998    #[case(Time::new(Tai, -1, Subsecond::new().set_milliseconds(600)), Time::new(Tai, 1, Subsecond::new().set_milliseconds(900)), )]
999    fn test_time_sub_time(#[case] time1: Time<Tai>, #[case] time2: Time<Tai>) {
1000        let delta = time2 - time1;
1001        let actual = time1 + delta;
1002        assert_eq!(actual, time2);
1003    }
1004
1005    #[rstest]
1006    #[case::at_the_epoch(Time::default(), 0.0)]
1007    #[case::exactly_one_day_after_the_epoch(
1008        Time::new(Tai, SECONDS_PER_DAY, Subsecond::default()),
1009        1.0
1010    )]
1011    #[case::exactly_one_day_before_the_epoch(
1012        Time::new(Tai, -SECONDS_PER_DAY, Subsecond::default()),
1013        -1.0
1014    )]
1015    #[case::a_partial_number_of_days_after_the_epoch(
1016        Time::new(Tai, (SECONDS_PER_DAY / 2) * 3, Subsecond::new().set_milliseconds(500)),
1017        1.5000057870370371
1018    )]
1019    fn test_time_days_since_j2000(#[case] time: Time<Tai>, #[case] expected: f64) {
1020        let actual = time.days_since_j2000();
1021        assert_approx_eq!(expected, actual, atol <= 1e-12);
1022    }
1023
1024    #[rstest]
1025    #[case::at_the_epoch(Time::default(), 0.0)]
1026    #[case::exactly_one_century_after_the_epoch(
1027        Time::new(Tai, SECONDS_PER_JULIAN_CENTURY, Subsecond::default()),
1028        1.0
1029    )]
1030    #[case::exactly_one_century_before_the_epoch(
1031        Time::new(Tai, -SECONDS_PER_JULIAN_CENTURY, Subsecond::default()),
1032        -1.0
1033    )]
1034    #[case::a_partial_number_of_centuries_after_the_epoch(
1035        Time::new(Tai, (SECONDS_PER_JULIAN_CENTURY / 2) * 3, Subsecond::new().set_milliseconds(500)),
1036        1.5000000001584404
1037    )]
1038    fn test_time_centuries_since_j2000(#[case] time: Time<Tai>, #[case] expected: f64) {
1039        let actual = time.centuries_since_j2000();
1040        assert_approx_eq!(expected, actual, atol <= 1e-12);
1041    }
1042
1043    #[rstest]
1044    #[case::j2000(Time::default(), Date::new(2000, 1, 1).unwrap())]
1045    #[case::next_day(Time::new(Tai, SECONDS_PER_DAY, Subsecond::default()), Date::new(2000, 1, 2).unwrap())]
1046    #[case::leap_year(Time::new(Tai, SECONDS_PER_DAY * 366, Subsecond::default()), Date::new(2001, 1, 1).unwrap())]
1047    #[case::non_leap_year(Time::new(Tai, SECONDS_PER_DAY * (366 + 365), Subsecond::default()), Date::new(2002, 1, 1).unwrap())]
1048    #[case::negative_time(Time::new(Tai, -SECONDS_PER_DAY, Subsecond::default()), Date::new(1999, 12, 31).unwrap())]
1049    fn test_time_calendar_date(#[case] time: Time<Tai>, #[case] expected: Date) {
1050        assert_eq!(expected, time.date());
1051        assert_eq!(expected.year(), time.year());
1052        assert_eq!(expected.month(), time.month());
1053        assert_eq!(expected.day(), time.day());
1054    }
1055
1056    #[test]
1057    fn test_time_scale() {
1058        let time: Time<Tai> = Time::default();
1059        assert_eq!(time.scale(), Tai);
1060    }
1061
1062    #[test]
1063    fn test_time_override_scale() {
1064        let time: Time<Tai> = Time::default();
1065        let time = time.with_scale(Tt);
1066        assert_eq!(time.scale(), Tt);
1067    }
1068
1069    #[test]
1070    fn test_time_leap_second_outside_utc() {
1071        let actual = time!(Tai, 2000, 1, 1, 23, 59, 60.0);
1072        let expected = Err(TimeError::LeapSecondOutsideUtc);
1073        assert_eq!(actual, expected);
1074    }
1075
1076    #[test]
1077    fn test_time_to_delta() {
1078        let time = time!(Tai, 2000, 1, 1, 12, 0, 0.0).unwrap();
1079        let actual = time.to_delta();
1080        let expected = TimeDelta::from_seconds(0);
1081        assert_eq!(actual, expected);
1082    }
1083
1084    #[test]
1085    fn test_time_into_dyn() {
1086        use crate::time_scales::DynTimeScale;
1087
1088        let time = time!(Tai, 2000, 1, 1, 12, 0, 0.0).unwrap();
1089        let dyn_time = time.into_dyn();
1090        assert_eq!(dyn_time.scale(), DynTimeScale::Tai);
1091        assert_eq!(dyn_time.to_delta(), time.to_delta());
1092
1093        let tdb_time = time!(Tdb, 2023, 6, 15, 10, 30, 0.0).unwrap();
1094        let dyn_tdb = tdb_time.into_dyn();
1095        assert_eq!(dyn_tdb.scale(), DynTimeScale::Tdb);
1096        assert_eq!(dyn_tdb.to_delta(), tdb_time.to_delta());
1097    }
1098}