Skip to main content

tempoch/
instant.rs

1// SPDX-License-Identifier: AGPL-3.0-or-later
2// Copyright (C) 2026 Vallés Puig, Ramon
3
4//! Generic time–scale parameterised instant.
5//!
6//! [`Time<S>`] is the core type of the time module.  It stores a scalar
7//! quantity in [`Days`] whose *meaning* is determined by the compile-time
8//! marker `S: TimeScale`.  All arithmetic (addition/subtraction of
9//! durations, difference between instants), UTC conversion, serialisation,
10//! and display are implemented generically — no code duplication.
11//!
12//! Domain-specific methods that only make sense for a particular scale
13//! (e.g. [`Time::<JD>::julian_centuries()`]) are placed in inherent `impl`
14//! blocks gated on the concrete marker type.
15
16use chrono::{DateTime, Utc};
17use qtty::*;
18use std::marker::PhantomData;
19use std::ops::{Add, AddAssign, Sub, SubAssign};
20
21#[cfg(feature = "serde")]
22use serde::{Deserialize, Deserializer, Serialize, Serializer};
23
24// ═══════════════════════════════════════════════════════════════════════════
25// TimeScale trait
26// ═══════════════════════════════════════════════════════════════════════════
27
28/// Marker trait for time scales.
29///
30/// A **time scale** defines:
31///
32/// 1. A human-readable **label** (e.g. `"JD"`, `"MJD"`, `"TAI"`).
33/// 2. A pair of conversion functions between the scale's native quantity
34///    (in [`Days`]) and **Julian Date in TT** (JD(TT)) — the canonical
35///    internal representation used throughout the crate.
36///
37/// For pure *epoch counters* (JD, MJD, Unix Time, GPS) the conversions are
38/// trivial constant offsets that the compiler will inline and fold away.
39///
40/// For *physical scales* (TT, TDB, TAI) the conversions may include
41/// function-based corrections (e.g. the ≈1.7 ms TDB↔TT periodic term).
42pub trait TimeScale: Copy + Clone + std::fmt::Debug + PartialEq + PartialOrd + 'static {
43    /// Display label used by [`Time`] formatting.
44    const LABEL: &'static str;
45
46    /// Convert a quantity in this scale's native unit to an absolute JD(TT).
47    fn to_jd_tt(value: Days) -> Days;
48
49    /// Convert an absolute JD(TT) back to this scale's native quantity.
50    fn from_jd_tt(jd_tt: Days) -> Days;
51}
52
53// ═══════════════════════════════════════════════════════════════════════════
54// Time<S> — the generic instant
55// ═══════════════════════════════════════════════════════════════════════════
56
57/// A point on time scale `S`.
58///
59/// Internally stores a single `Days` quantity whose interpretation depends on
60/// `S: TimeScale`.  The struct is `Copy` and zero-cost: `PhantomData` is
61/// zero-sized, so `Time<S>` is layout-identical to `Days` (a single `f64`).
62#[derive(Debug, Copy, Clone, PartialEq, PartialOrd)]
63pub struct Time<S: TimeScale> {
64    quantity: Days,
65    _scale: PhantomData<S>,
66}
67
68impl<S: TimeScale> Time<S> {
69    // ── constructors ──────────────────────────────────────────────────
70
71    /// Create from a raw scalar (days since the scale's epoch).
72    #[inline]
73    pub const fn new(value: f64) -> Self {
74        Self {
75            quantity: Days::new(value),
76            _scale: PhantomData,
77        }
78    }
79
80    /// Create from a [`Days`] quantity.
81    #[inline]
82    pub const fn from_days(days: Days) -> Self {
83        Self {
84            quantity: days,
85            _scale: PhantomData,
86        }
87    }
88
89    // ── accessors ─────────────────────────────────────────────────────
90
91    /// The underlying quantity in days.
92    #[inline]
93    pub const fn quantity(&self) -> Days {
94        self.quantity
95    }
96
97    /// The underlying scalar value in days.
98    #[inline]
99    pub const fn value(&self) -> f64 {
100        self.quantity.value()
101    }
102
103    /// Absolute Julian Day (TT) corresponding to this instant.
104    #[inline]
105    pub fn julian_day(&self) -> Days {
106        S::to_jd_tt(self.quantity)
107    }
108
109    /// Absolute Julian Day (TT) as scalar.
110    #[inline]
111    pub fn julian_day_value(&self) -> f64 {
112        self.julian_day().value()
113    }
114
115    /// Build an instant from an absolute Julian Day (TT).
116    #[inline]
117    pub fn from_julian_day(jd: Days) -> Self {
118        Self::from_days(S::from_jd_tt(jd))
119    }
120
121    // ── cross-scale conversion (mirroring qtty's .to::<T>()) ─────────
122
123    /// Convert this instant to another time scale.
124    ///
125    /// The conversion routes through the canonical JD(TT) intermediate:
126    ///
127    /// ```text
128    /// self → JD(TT) → target
129    /// ```
130    ///
131    /// For pure epoch-offset scales this compiles down to a single
132    /// addition/subtraction.
133    #[inline]
134    pub fn to<T: TimeScale>(&self) -> Time<T> {
135        Time::<T>::from_julian_day(S::to_jd_tt(self.quantity))
136    }
137
138    // ── UTC helpers ───────────────────────────────────────────────────
139
140    /// Convert to a `chrono::DateTime<Utc>`.
141    ///
142    /// Inverts the ΔT correction to recover the UTC / UT timestamp.
143    /// Returns `None` if the value falls outside chrono's representable range.
144    pub fn to_utc(&self) -> Option<DateTime<Utc>> {
145        use super::scales::UT;
146        const UNIX_EPOCH_JD: f64 = 2_440_587.5;
147        let jd_ut = self.to::<UT>().quantity();
148        let seconds_since_epoch = (jd_ut - Days::new(UNIX_EPOCH_JD)).to::<Second>().value();
149        let secs = seconds_since_epoch.floor() as i64;
150        let nanos = ((seconds_since_epoch - secs as f64) * 1e9) as u32;
151        DateTime::<Utc>::from_timestamp(secs, nanos)
152    }
153
154    /// Build an instant from a `chrono::DateTime<Utc>`.
155    ///
156    /// The UTC timestamp is interpreted as Universal Time (≈ UT1) and the
157    /// epoch-dependent **ΔT** correction is applied automatically, so the
158    /// resulting `Time<S>` is on the target scale's axis.
159    pub fn from_utc(datetime: DateTime<Utc>) -> Self {
160        use super::scales::UT;
161        const UNIX_EPOCH_JD: f64 = 2_440_587.5;
162        let seconds_since_epoch = Seconds::new(datetime.timestamp() as f64);
163        let nanos = Seconds::new(datetime.timestamp_subsec_nanos() as f64 / 1e9);
164        let jd_ut = Days::new(UNIX_EPOCH_JD) + (seconds_since_epoch + nanos).to::<Day>();
165        Time::<UT>::from_days(jd_ut).to::<S>()
166    }
167
168    // ── min / max ─────────────────────────────────────────────────────
169
170    /// Element-wise minimum.
171    #[inline]
172    pub const fn min(self, other: Self) -> Self {
173        Self::from_days(self.quantity.min_const(other.quantity))
174    }
175
176    /// Element-wise maximum.
177    #[inline]
178    pub const fn max(self, other: Self) -> Self {
179        Self::from_days(self.quantity.max_const(other.quantity))
180    }
181
182    /// Mean (midpoint) between two instants on the same time scale.
183    #[inline]
184    pub const fn mean(self, other: Self) -> Self {
185        Self::from_days(self.quantity.const_add(other.quantity).const_div(2.0))
186    }
187}
188
189// ═══════════════════════════════════════════════════════════════════════════
190// Generic trait implementations
191// ═══════════════════════════════════════════════════════════════════════════
192
193// ── Display ───────────────────────────────────────────────────────────────
194
195impl<S: TimeScale> std::fmt::Display for Time<S> {
196    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
197        write!(f, "{} {}", S::LABEL, self.quantity)
198    }
199}
200
201// ── Serde ─────────────────────────────────────────────────────────────────
202
203#[cfg(feature = "serde")]
204impl<S: TimeScale> Serialize for Time<S> {
205    fn serialize<Ser>(&self, serializer: Ser) -> Result<Ser::Ok, Ser::Error>
206    where
207        Ser: Serializer,
208    {
209        serializer.serialize_f64(self.value())
210    }
211}
212
213#[cfg(feature = "serde")]
214impl<'de, S: TimeScale> Deserialize<'de> for Time<S> {
215    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
216    where
217        D: Deserializer<'de>,
218    {
219        let v = f64::deserialize(deserializer)?;
220        Ok(Self::new(v))
221    }
222}
223
224// ── Arithmetic ────────────────────────────────────────────────────────────
225
226impl<S: TimeScale> Add<Days> for Time<S> {
227    type Output = Self;
228    #[inline]
229    fn add(self, rhs: Days) -> Self::Output {
230        Self::from_days(self.quantity + rhs)
231    }
232}
233
234impl<S: TimeScale> AddAssign<Days> for Time<S> {
235    #[inline]
236    fn add_assign(&mut self, rhs: Days) {
237        self.quantity += rhs;
238    }
239}
240
241impl<S: TimeScale> Sub<Days> for Time<S> {
242    type Output = Self;
243    #[inline]
244    fn sub(self, rhs: Days) -> Self::Output {
245        Self::from_days(self.quantity - rhs)
246    }
247}
248
249impl<S: TimeScale> SubAssign<Days> for Time<S> {
250    #[inline]
251    fn sub_assign(&mut self, rhs: Days) {
252        self.quantity -= rhs;
253    }
254}
255
256impl<S: TimeScale> Sub for Time<S> {
257    type Output = Days;
258    #[inline]
259    fn sub(self, rhs: Self) -> Self::Output {
260        self.quantity - rhs.quantity
261    }
262}
263
264impl<S: TimeScale> std::ops::Div<Days> for Time<S> {
265    type Output = f64;
266    #[inline]
267    fn div(self, rhs: Days) -> Self::Output {
268        (self.quantity / rhs).simplify().value()
269    }
270}
271
272impl<S: TimeScale> std::ops::Div<f64> for Time<S> {
273    type Output = f64;
274    #[inline]
275    fn div(self, rhs: f64) -> Self::Output {
276        (self.quantity / rhs).value()
277    }
278}
279
280// ── From/Into Days ────────────────────────────────────────────────────────
281
282impl<S: TimeScale> From<Days> for Time<S> {
283    #[inline]
284    fn from(days: Days) -> Self {
285        Self::from_days(days)
286    }
287}
288
289impl<S: TimeScale> From<Time<S>> for Days {
290    #[inline]
291    fn from(time: Time<S>) -> Self {
292        time.quantity
293    }
294}
295
296// ═══════════════════════════════════════════════════════════════════════════
297// TimeInstant trait
298// ═══════════════════════════════════════════════════════════════════════════
299
300/// Trait for types that represent a point in time.
301///
302/// Types implementing this trait can be used as time instants in `Interval<T>`
303/// and provide conversions to/from UTC plus basic arithmetic operations.
304pub trait TimeInstant: Copy + Clone + PartialEq + PartialOrd + Sized {
305    /// The duration type used for arithmetic operations.
306    type Duration;
307
308    /// Convert this time instant to UTC DateTime.
309    fn to_utc(&self) -> Option<DateTime<Utc>>;
310
311    /// Create a time instant from UTC DateTime.
312    fn from_utc(datetime: DateTime<Utc>) -> Self;
313
314    /// Compute the difference between two time instants.
315    fn difference(&self, other: &Self) -> Self::Duration;
316
317    /// Add a duration to this time instant.
318    fn add_duration(&self, duration: Self::Duration) -> Self;
319
320    /// Subtract a duration from this time instant.
321    fn sub_duration(&self, duration: Self::Duration) -> Self;
322}
323
324impl<S: TimeScale> TimeInstant for Time<S> {
325    type Duration = Days;
326
327    #[inline]
328    fn to_utc(&self) -> Option<DateTime<Utc>> {
329        Time::to_utc(self)
330    }
331
332    #[inline]
333    fn from_utc(datetime: DateTime<Utc>) -> Self {
334        Time::from_utc(datetime)
335    }
336
337    #[inline]
338    fn difference(&self, other: &Self) -> Self::Duration {
339        *self - *other
340    }
341
342    #[inline]
343    fn add_duration(&self, duration: Self::Duration) -> Self {
344        *self + duration
345    }
346
347    #[inline]
348    fn sub_duration(&self, duration: Self::Duration) -> Self {
349        *self - duration
350    }
351}
352
353impl TimeInstant for DateTime<Utc> {
354    type Duration = chrono::Duration;
355
356    fn to_utc(&self) -> Option<DateTime<Utc>> {
357        Some(*self)
358    }
359
360    fn from_utc(datetime: DateTime<Utc>) -> Self {
361        datetime
362    }
363
364    fn difference(&self, other: &Self) -> Self::Duration {
365        *self - *other
366    }
367
368    fn add_duration(&self, duration: Self::Duration) -> Self {
369        *self + duration
370    }
371
372    fn sub_duration(&self, duration: Self::Duration) -> Self {
373        *self - duration
374    }
375}
376
377// ═══════════════════════════════════════════════════════════════════════════
378// Tests
379// ═══════════════════════════════════════════════════════════════════════════
380
381#[cfg(test)]
382mod tests {
383    use super::super::scales::{JD, MJD};
384    use super::*;
385    use chrono::TimeZone;
386
387    #[test]
388    fn test_julian_day_creation() {
389        let jd = Time::<JD>::new(2_451_545.0);
390        assert_eq!(jd.quantity(), Days::new(2_451_545.0));
391    }
392
393    #[test]
394    fn test_jd_utc_roundtrip() {
395        // from_utc applies ΔT (UT→TT); to_utc inverts it (TT→UT).
396        let datetime = DateTime::from_timestamp(946_728_000, 0).unwrap();
397        let jd = Time::<JD>::from_utc(datetime);
398        let back = jd.to_utc().expect("to_utc");
399        let delta_ns =
400            back.timestamp_nanos_opt().unwrap() - datetime.timestamp_nanos_opt().unwrap();
401        assert!(delta_ns.abs() < 1_000, "roundtrip error: {} ns", delta_ns);
402    }
403
404    #[test]
405    fn test_from_utc_applies_delta_t() {
406        // 2000-01-01 12:00:00 UTC → JD(UT)=2451545.0; ΔT≈63.83 s
407        let datetime = DateTime::from_timestamp(946_728_000, 0).unwrap();
408        let jd = Time::<JD>::from_utc(datetime);
409        let delta_t_secs = (jd.quantity() - Days::new(2_451_545.0)).to::<Second>();
410        assert!(
411            (delta_t_secs - Seconds::new(63.83)).abs() < Seconds::new(1.0),
412            "ΔT correction = {} s, expected ~63.83 s",
413            delta_t_secs
414        );
415    }
416
417    #[test]
418    fn test_julian_conversions() {
419        let jd = Time::<JD>::J2000 + Days::new(365_250.0);
420        assert!((jd.julian_millennias() - Millennia::new(1.0)).abs() < 1e-12);
421        assert!((jd.julian_centuries() - Centuries::new(10.0)).abs() < Centuries::new(1e-12));
422        assert!((jd.julian_years() - JulianYears::new(1000.0)).abs() < JulianYears::new(1e-9));
423    }
424
425    #[test]
426    fn test_tt_to_tdb_and_min_max() {
427        let jd_tdb = Time::<JD>::tt_to_tdb(Time::<JD>::J2000);
428        assert!((jd_tdb - Time::<JD>::J2000).abs() < 1e-6);
429
430        let earlier = Time::<JD>::J2000;
431        let later = earlier + Days::new(1.0);
432        assert_eq!(earlier.min(later), earlier);
433        assert_eq!(earlier.max(later), later);
434    }
435
436    #[test]
437    fn test_const_min_max() {
438        const A: Time<JD> = Time::<JD>::new(10.0);
439        const B: Time<JD> = Time::<JD>::new(14.0);
440        const MIN: Time<JD> = A.min(B);
441        const MAX: Time<JD> = A.max(B);
442        assert_eq!(MIN.quantity(), Days::new(10.0));
443        assert_eq!(MAX.quantity(), Days::new(14.0));
444    }
445
446    #[test]
447    fn test_mean_and_const_mean() {
448        let a = Time::<JD>::new(10.0);
449        let b = Time::<JD>::new(14.0);
450        assert_eq!(a.mean(b).quantity(), Days::new(12.0));
451        assert_eq!(b.mean(a).quantity(), Days::new(12.0));
452
453        const MID: Time<JD> = Time::<JD>::new(10.0).mean(Time::<JD>::new(14.0));
454        assert_eq!(MID.quantity(), Days::new(12.0));
455    }
456
457    #[test]
458    fn test_into_days() {
459        let jd = Time::<JD>::new(2_451_547.5);
460        let days: Days = jd.into();
461        assert_eq!(days, 2_451_547.5);
462
463        let roundtrip = Time::<JD>::from(days);
464        assert_eq!(roundtrip, jd);
465    }
466
467    #[test]
468    fn test_into_julian_years() {
469        let jd = Time::<JD>::J2000 + Days::new(365.25 * 2.0);
470        let years: JulianYears = jd.into();
471        assert!((years - JulianYears::new(2.0)).abs() < JulianYears::new(1e-12));
472
473        let roundtrip = Time::<JD>::from(years);
474        assert!((roundtrip.quantity() - jd.quantity()).abs() < Days::new(1e-12));
475    }
476
477    #[test]
478    fn test_into_centuries() {
479        let jd = Time::<JD>::J2000 + Days::new(36_525.0 * 3.0);
480        let centuries: Centuries = jd.into();
481        assert!((centuries - Centuries::new(3.0)).abs() < Centuries::new(1e-12));
482
483        let roundtrip = Time::<JD>::from(centuries);
484        assert!((roundtrip.quantity() - jd.quantity()).abs() < Days::new(1e-12));
485    }
486
487    #[test]
488    fn test_into_millennia() {
489        let jd = Time::<JD>::J2000 + Days::new(365_250.0 * 1.5);
490        let millennia: Millennia = jd.into();
491        assert!((millennia - Millennia::new(1.5)).abs() < Millennia::new(1e-12));
492
493        let roundtrip = Time::<JD>::from(millennia);
494        assert!((roundtrip.quantity() - jd.quantity()).abs() < Days::new(1e-9));
495    }
496
497    #[test]
498    fn test_mjd_creation() {
499        let mjd = Time::<MJD>::new(51_544.5);
500        assert_eq!(mjd.quantity(), Days::new(51_544.5));
501    }
502
503    #[test]
504    fn test_mjd_into_jd() {
505        let mjd = Time::<MJD>::new(51_544.5);
506        let jd: Time<JD> = mjd.into();
507        assert_eq!(jd.quantity(), Days::new(2_451_545.0));
508    }
509
510    #[test]
511    fn test_mjd_utc_roundtrip() {
512        let datetime = DateTime::from_timestamp(946_728_000, 0).unwrap();
513        let mjd = Time::<MJD>::from_utc(datetime);
514        let back = mjd.to_utc().expect("to_utc");
515        let delta_ns =
516            back.timestamp_nanos_opt().unwrap() - datetime.timestamp_nanos_opt().unwrap();
517        assert!(delta_ns.abs() < 1_000, "roundtrip error: {} ns", delta_ns);
518    }
519
520    #[test]
521    fn test_mjd_from_utc_applies_delta_t() {
522        // MJD epoch is JD − 2400000.5; ΔT should shift value by ~63.83/86400 days
523        let datetime = DateTime::from_timestamp(946_728_000, 0).unwrap();
524        let mjd = Time::<MJD>::from_utc(datetime);
525        let delta_t_secs = (mjd.quantity() - Days::new(51_544.5)).to::<Second>();
526        assert!(
527            (delta_t_secs - Seconds::new(63.83)).abs() < Seconds::new(1.0),
528            "ΔT correction = {} s, expected ~63.83 s",
529            delta_t_secs
530        );
531    }
532
533    #[test]
534    fn test_mjd_add_days() {
535        let mjd = Time::<MJD>::new(59_000.0);
536        let result = mjd + Days::new(1.5);
537        assert_eq!(result.quantity(), Days::new(59_001.5));
538    }
539
540    #[test]
541    fn test_mjd_sub_days() {
542        let mjd = Time::<MJD>::new(59_000.0);
543        let result = mjd - Days::new(1.5);
544        assert_eq!(result.quantity(), Days::new(58_998.5));
545    }
546
547    #[test]
548    fn test_mjd_sub_mjd() {
549        let mjd1 = Time::<MJD>::new(59_001.0);
550        let mjd2 = Time::<MJD>::new(59_000.0);
551        let diff = mjd1 - mjd2;
552        assert_eq!(diff, 1.0);
553    }
554
555    #[test]
556    fn test_mjd_comparison() {
557        let mjd1 = Time::<MJD>::new(59_000.0);
558        let mjd2 = Time::<MJD>::new(59_001.0);
559        assert!(mjd1 < mjd2);
560        assert!(mjd2 > mjd1);
561    }
562
563    #[test]
564    fn test_display_jd() {
565        let jd = Time::<JD>::new(2_451_545.0);
566        let s = format!("{jd}");
567        assert!(s.contains("Julian Day"));
568    }
569
570    #[test]
571    fn test_display_mjd() {
572        let mjd = Time::<MJD>::new(51_544.5);
573        let s = format!("{mjd}");
574        assert!(s.contains("MJD"));
575    }
576
577    #[test]
578    fn test_add_assign_sub_assign() {
579        let mut jd = Time::<JD>::new(2_451_545.0);
580        jd += Days::new(1.0);
581        assert_eq!(jd.quantity(), Days::new(2_451_546.0));
582        jd -= Days::new(0.5);
583        assert_eq!(jd.quantity(), Days::new(2_451_545.5));
584    }
585
586    #[test]
587    fn test_add_years() {
588        let jd = Time::<JD>::new(2_450_000.0);
589        let with_years = jd + Years::new(1.0);
590        let span: Days = with_years - jd;
591        assert!((span - Time::<JD>::JULIAN_YEAR).abs() < Days::new(1e-9));
592    }
593
594    #[test]
595    fn test_div_days_and_f64() {
596        let jd = Time::<JD>::new(100.0);
597        assert!((jd / Days::new(2.0) - 50.0).abs() < 1e-12);
598        assert!((jd / 4.0 - 25.0).abs() < 1e-12);
599    }
600
601    #[test]
602    fn test_to_method_jd_mjd() {
603        let jd = Time::<JD>::new(2_451_545.0);
604        let mjd = jd.to::<MJD>();
605        assert!((mjd.quantity() - Days::new(51_544.5)).abs() < Days::new(1e-10));
606    }
607
608    #[test]
609    fn timeinstant_for_julian_date_handles_arithmetic() {
610        let jd = Time::<JD>::new(2_451_545.0);
611        let other = jd + Days::new(2.0);
612
613        assert_eq!(jd.difference(&other), Days::new(-2.0));
614        assert_eq!(
615            jd.add_duration(Days::new(1.5)).quantity(),
616            Days::new(2_451_546.5)
617        );
618        assert_eq!(
619            other.sub_duration(Days::new(0.5)).quantity(),
620            Days::new(2_451_546.5)
621        );
622    }
623
624    #[test]
625    fn timeinstant_for_modified_julian_date_roundtrips_utc() {
626        let dt = DateTime::from_timestamp(946_684_800, 123_000_000).unwrap(); // 2000-01-01T00:00:00.123Z
627        let mjd = Time::<MJD>::from_utc(dt);
628        let back = mjd.to_utc().expect("mjd to utc");
629
630        assert_eq!(mjd.difference(&mjd), Days::new(0.0));
631        assert_eq!(
632            mjd.add_duration(Days::new(1.0)).quantity(),
633            mjd.quantity() + Days::new(1.0)
634        );
635        assert_eq!(
636            mjd.sub_duration(Days::new(0.5)).quantity(),
637            mjd.quantity() - Days::new(0.5)
638        );
639        let delta_ns = back.timestamp_nanos_opt().unwrap() - dt.timestamp_nanos_opt().unwrap();
640        assert!(delta_ns.abs() < 10_000, "nanos differ by {}", delta_ns);
641    }
642
643    #[test]
644    fn timeinstant_for_datetime_uses_chrono_durations() {
645        let base = Utc.with_ymd_and_hms(2024, 1, 1, 0, 0, 0).unwrap();
646        let later = Utc.with_ymd_and_hms(2024, 1, 2, 6, 0, 0).unwrap();
647        let diff = later.difference(&base);
648
649        assert_eq!(diff.num_hours(), 30);
650        assert_eq!(
651            base.add_duration(diff + chrono::Duration::hours(6)),
652            later + chrono::Duration::hours(6)
653        );
654        assert_eq!(later.sub_duration(diff), base);
655        assert_eq!(TimeInstant::to_utc(&later), Some(later));
656    }
657}