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