Skip to main content

deep_time/dt/
arithmetic.rs

1use crate::{
2    ATTOS_PER_FS_I128, ATTOS_PER_MS_I128, ATTOS_PER_NS_I128, ATTOS_PER_PS_I128, ATTOS_PER_SEC_I128,
3    ATTOS_PER_SECF, ATTOS_PER_US_I128, Drift, Dt, Real, Scale, Spacetime, floor_f,
4};
5
6impl Dt {
7    /// Saturating add, keeps `self`'s `scale` and `target`.
8    #[inline]
9    pub const fn add(&self, dt: Dt) -> Dt {
10        if !dt.is_zero() {
11            Dt::new(self.attos.saturating_add(dt.attos), self.scale, self.target)
12        } else {
13            *self
14        }
15    }
16
17    /// Saturating sub, keeps `self`'s `scale` and `target`.
18    #[inline]
19    pub const fn sub(&self, dt: Dt) -> Dt {
20        if !dt.is_zero() {
21            Dt::new(self.attos.saturating_sub(dt.attos), self.scale, self.target)
22        } else {
23            *self
24        }
25    }
26
27    /// If this time were turned into [`i128`] seconds and [`u64`] (always
28    /// pushing to the positive) fractional attoseconds, this returns the
29    /// whole seconds part.
30    ///
31    /// To just get seconds rounded to the nearest second use
32    /// [`Dt::to_sec_rounded`](../struct.Dt.html#method.to_sec_rounded)
33    /// instead.
34    ///
35    /// ## Examples
36    ///
37    /// ```
38    /// use deep_time::{Dt, Scale};
39    ///
40    /// // negative 1.3 seconds
41    /// let dt = Dt::span(-1_300_000_000_000_000_000);
42    ///
43    /// // becomes positive 700ms
44    /// let frac = dt.to_sec_ufrac();
45    /// assert_eq!(frac, 700_000_000_000_000_000);
46    ///
47    /// // becomes negative 2 seconds
48    /// let sec = dt.to_sec();
49    /// assert_eq!(sec, -2);
50    ///
51    /// let dt = Dt::span(1_300_000_000_000_000_000);
52    ///
53    /// assert_eq!(dt.to_sec(), 1);
54    /// assert_eq!(dt.to_sec_ufrac(), 300_000_000_000_000_000);
55    ///
56    /// // if you just want rounded seconds
57    /// // use to_sec_rounded() instead
58    /// let dt = Dt::span(-1_300_000_000_000_000_000);
59    /// let sec = dt.to_sec_rounded();
60    /// assert_eq!(sec, -1);
61    /// ```
62    #[inline(always)]
63    pub const fn to_sec(&self) -> i128 {
64        self.attos.div_euclid(ATTOS_PER_SEC_I128)
65    }
66
67    #[inline(always)]
68    pub const fn to_sec_rounded(&self) -> i128 {
69        self.round_to_sec().to_sec()
70    }
71
72    /// If this time were turned into [`i64`] seconds and [`u64`] (always
73    /// pushing to the positive) fractional attoseconds, this returns the
74    /// whole seconds part.
75    ///
76    /// To just get seconds rounded to the nearest second use
77    /// [`Dt::to_sec_rounded`](../struct.Dt.html#method.to_sec_rounded)
78    /// instead.
79    ///
80    /// ## Examples
81    ///
82    /// ```
83    /// use deep_time::{Dt, Scale};
84    ///
85    /// // negative 1.3 seconds
86    /// let dt = Dt::span(-1_300_000_000_000_000_000);
87    ///
88    /// // becomes positive 700ms
89    /// let frac = dt.to_sec_ufrac();
90    /// assert_eq!(frac, 700_000_000_000_000_000);
91    ///
92    /// // becomes negative 2 seconds
93    /// let sec = dt.to_sec64();
94    /// assert_eq!(sec, -2);
95    ///
96    /// let dt = Dt::span(1_300_000_000_000_000_000);
97    ///
98    /// assert_eq!(dt.to_sec64(), 1);
99    /// assert_eq!(dt.to_sec_ufrac(), 300_000_000_000_000_000);
100    ///
101    /// // if you just want rounded seconds
102    /// // use to_sec_rounded() instead
103    /// let dt = Dt::span(-1_300_000_000_000_000_000);
104    /// let sec = dt.to_sec_rounded();
105    /// assert_eq!(sec, -1);
106    /// ```
107    #[inline(always)]
108    pub const fn to_sec64(&self) -> i64 {
109        Self::i128_to_i64(self.attos.div_euclid(ATTOS_PER_SEC_I128))
110    }
111
112    /// Converts this `Dt` to a floating-point number of seconds since the reference
113    /// epoch of its associated scale.
114    ///
115    /// - The conversion is lossy, as [`Real`] provides approximately 15.95 decimal
116    ///   digits of precision.
117    pub const fn to_sec_f(&self) -> Real {
118        let attos = self.attos;
119
120        if attos == 0 {
121            return 0.0;
122        }
123        let sec = attos.div_euclid(ATTOS_PER_SEC_I128);
124        let rem = attos.rem_euclid(ATTOS_PER_SEC_I128); // always in [0, aps)
125
126        if sec < 0 && rem > ATTOS_PER_SEC_I128 / 2 {
127            // original cancellation-avoidance path
128            let small = ATTOS_PER_SEC_I128 - rem;
129            let small_f = f!(small as u64) / ATTOS_PER_SECF;
130            (sec as f64) + 1.0 - small_f
131        } else {
132            (sec as f64) + f!(rem as u64) / ATTOS_PER_SECF
133        }
134    }
135
136    /// If this time were turned into seconds, this returns the fractional attoseconds part.
137    #[inline(always)]
138    pub const fn to_sec_frac(&self) -> i64 {
139        (self.attos % ATTOS_PER_SEC_I128) as i64
140    }
141
142    /// If this time were turned into i64 seconds and u64 (always pushing to the positive)
143    /// fractional attoseconds, this returns the fractional attoseconds part.
144    ///
145    /// - Always returns a value in the range `0 ≤ x < ATTOS_PER_SEC`.
146    /// - For negative [`Dt`]s this is **not** simply the decimal part of the time in seconds.
147    ///
148    /// ## Examples
149    ///
150    /// ```
151    /// use deep_time::{Dt, Scale};
152    ///
153    /// // negative 1.3 seconds
154    /// let dt = Dt::span(-1_300_000_000_000_000_000);
155    ///
156    /// // becomes positive 700ms
157    /// let frac = dt.to_sec_ufrac();
158    /// assert_eq!(frac, 700_000_000_000_000_000);
159    ///
160    /// // becomes -2 seconds
161    /// let sec = dt.to_sec64();
162    /// assert_eq!(sec, -2);
163    ///
164    /// let dt = Dt::span(1_300_000_000_000_000_000);
165    ///
166    /// assert_eq!(dt.to_sec64(), 1);
167    /// assert_eq!(dt.to_sec_ufrac(), 300_000_000_000_000_000);
168    /// ```
169    #[inline(always)]
170    pub const fn to_sec_ufrac(&self) -> u64 {
171        self.attos.rem_euclid(ATTOS_PER_SEC_I128) as u64
172    }
173
174    /// Returns a new [`Dt`] rounded to the nearest second.
175    #[inline(always)]
176    pub const fn round_to_sec(&self) -> Dt {
177        self.round(Dt::span(ATTOS_PER_SEC_I128))
178    }
179
180    /// Computes the signed duration between this [`Dt`] and another [`Dt`].
181    #[inline]
182    pub const fn to_diff_raw(&self, other: Dt) -> Dt {
183        Dt::new(
184            self.attos.saturating_sub(other.attos),
185            self.scale,
186            self.target,
187        )
188    }
189
190    /// Computes the signed duration between this [`Dt`] and another [`Dt`] as a float.
191    #[inline]
192    pub const fn to_diff_raw_f(&self, other: Dt) -> Real {
193        self.to_sec_f() - other.to_sec_f()
194    }
195
196    /// Low level constructor from total attoseconds since a given epoch.
197    ///
198    /// Simply adds the total attoseconds to the epoch. Does not perform
199    /// any time scale conversions.
200    ///
201    /// ## Examples
202    ///
203    /// ```rust
204    /// use deep_time::{Dt, Scale};
205    ///
206    /// // A leap second from the middle of the table (36 leap seconds accumulated)
207    /// let original = Dt::from_ymd(2015, 6, 30, 23, 59, 60, 123_456_789_000_000_000, Scale::UTC);
208    ///
209    /// // Round-trip through canonical attoseconds
210    /// let canon = original.to_diff_raw(Dt::UNIX_EPOCH).to_attos();
211    /// let roundtrip1 = Dt::from_diff_raw(canon, Dt::UNIX_EPOCH);
212    ///
213    /// assert_eq!(original, roundtrip1, "Canonical round-trip failed");
214    /// ```
215    #[inline]
216    pub const fn from_diff_raw(attos: i128, epoch: Dt) -> Dt {
217        epoch.add(Dt::new(attos, epoch.scale, epoch.target))
218    }
219
220    /// Adds the specified number of attoseconds to this time value.
221    #[inline(always)]
222    pub const fn add_attos(&self, n: i128) -> Dt {
223        Dt::new(self.attos.saturating_add(n), self.scale, self.target)
224    }
225
226    /// Adds the specified number of seconds to this time value using saturating arithmetic.
227    #[inline(always)]
228    pub const fn add_sec(&self, n: i128) -> Dt {
229        self.add_attos(n.saturating_mul(ATTOS_PER_SEC_I128))
230    }
231
232    /// Adds the specified number of milliseconds to this time value.
233    #[inline(always)]
234    pub const fn add_ms(&self, n: i128) -> Dt {
235        self.add_attos(n.saturating_mul(ATTOS_PER_MS_I128))
236    }
237
238    /// Adds the specified number of microseconds to this time value.
239    #[inline(always)]
240    pub const fn add_us(&self, n: i128) -> Dt {
241        self.add_attos(n.saturating_mul(ATTOS_PER_US_I128))
242    }
243
244    /// Adds the specified number of nanoseconds to this time value.
245    #[inline(always)]
246    pub const fn add_ns(&self, n: i128) -> Dt {
247        self.add_attos(n.saturating_mul(ATTOS_PER_NS_I128))
248    }
249
250    /// Adds the specified number of picoseconds to this time value.
251    #[inline(always)]
252    pub const fn add_ps(&self, n: i128) -> Dt {
253        self.add_attos(n.saturating_mul(ATTOS_PER_PS_I128))
254    }
255
256    /// Adds the specified number of femtoseconds to this time value.
257    #[inline(always)]
258    pub const fn add_fs(&self, n: i128) -> Dt {
259        self.add_attos(n.saturating_mul(ATTOS_PER_FS_I128))
260    }
261
262    /// Adds the specified number of minutes to this time value using saturating arithmetic.
263    #[inline]
264    pub const fn add_min(&self, n: i64) -> Dt {
265        Dt::new(
266            self.attos
267                .saturating_add((n as i128) * 60 * ATTOS_PER_SEC_I128),
268            self.scale,
269            self.target,
270        )
271    }
272
273    /// Adds the specified number of hours to this time value using saturating arithmetic.
274    #[inline]
275    pub const fn add_hr(&self, n: i64) -> Dt {
276        Dt::new(
277            self.attos
278                .saturating_add((n as i128) * 3600 * ATTOS_PER_SEC_I128),
279            self.scale,
280            self.target,
281        )
282    }
283
284    /// Returns the total time in attoseconds.
285    #[inline(always)]
286    pub const fn to_attos(&self) -> i128 {
287        self.attos
288    }
289
290    /// Returns the total time in milliseconds.
291    #[inline(always)]
292    pub const fn to_ms(&self) -> i128 {
293        self.attos / ATTOS_PER_MS_I128
294    }
295
296    /// Returns the total time in microseconds.
297    #[inline(always)]
298    pub const fn to_us(&self) -> i128 {
299        self.attos / ATTOS_PER_US_I128
300    }
301
302    /// Returns the total time in nanoseconds.
303    #[inline(always)]
304    pub const fn to_ns(&self) -> i128 {
305        self.attos / ATTOS_PER_NS_I128
306    }
307
308    /// Returns the total time in picoseconds.
309    #[inline(always)]
310    pub const fn to_ps(&self) -> i128 {
311        self.attos / ATTOS_PER_PS_I128
312    }
313
314    /// Returns the total time in femtoseconds.
315    #[inline(always)]
316    pub const fn to_fs(&self) -> i128 {
317        self.attos / ATTOS_PER_FS_I128
318    }
319
320    /// Returns `true` if this time is zero.
321    #[inline(always)]
322    pub const fn is_zero(&self) -> bool {
323        self.attos == 0
324    }
325
326    /// Returns `true` if this time is strictly positive **> 0**.
327    #[inline(always)]
328    pub const fn is_positive(&self) -> bool {
329        self.attos > 0
330    }
331
332    /// Multiplies this time by an integer scalar.
333    ///
334    /// Uses 128-bit arithmetic internally.
335    pub const fn mul(self, rhs: i64) -> Dt {
336        if rhs == 0 || self.is_zero() {
337            return Self::ZERO;
338        }
339        let total = self.attos.saturating_mul(rhs as i128);
340        Self::from_attos(total, Scale::TAI)
341    }
342
343    /// Divides this `Dt` by an integer scalar.
344    ///
345    /// Uses truncating division (rounds toward zero), same as normal integer division.
346    /// Returns `ZERO` if `rhs == 0`.
347    pub const fn div(self, rhs: i64) -> Dt {
348        if rhs == 0 || self.is_zero() {
349            return Self::ZERO;
350        }
351        let result = self.attos / (rhs as i128);
352        Self::from_attos(result, Scale::TAI)
353    }
354
355    /// Returns the **largest** multiple of `unit` that is ≤ `self`.
356    /// If `unit` is zero, returns `self` unchanged (exact, full precision).
357    pub const fn floor(&self, unit: Dt) -> Dt {
358        if unit.is_zero() {
359            return *self;
360        }
361        let a = self.attos;
362        let b = unit.attos;
363        let q = safe_div_euc!(a, b, 0i128);
364        let result = q.wrapping_mul(b);
365        Self::from_attos(result, Scale::TAI)
366    }
367
368    /// Returns the **smallest** multiple of `unit` that is ≥ `self`.
369    /// If `unit` is zero, returns `self` unchanged (exact, full precision).
370    pub const fn ceil(&self, unit: Dt) -> Dt {
371        if unit.is_zero() {
372            return *self;
373        }
374        let a = self.attos;
375        let b = unit.attos;
376        // ceil(a/b) ≡ −floor(−a/b)
377        let neg_a = a.wrapping_neg();
378        let q = safe_div_euc!(neg_a, b, 0i128);
379        let q_ceil = q.wrapping_neg();
380        let result = q_ceil.wrapping_mul(b);
381        Self::from_attos(result, Scale::TAI)
382    }
383
384    /// Returns the nearest multiple of `unit`.
385    ///
386    /// Halfway cases round **away from zero** (e.g. `2.5 → 3.0`, `-2.5 → -3.0`),
387    /// matching the behavior of the old `f64::round()`.
388    ///
389    /// - If `unit` is zero, returns `self` unchanged (preserves full precision).
390    /// - Uses Euclidean division internally for correct behavior on negative values.
391    /// - The result is always a multiple of `unit`.
392    pub const fn round(&self, unit: Dt) -> Dt {
393        if unit.is_zero() {
394            return *self;
395        }
396
397        let a = self.attos;
398        let b = unit.attos;
399
400        let abs_a = a.wrapping_abs();
401        let abs_b = b.wrapping_abs();
402
403        let q = safe_div_euc!(abs_a, abs_b, 0i128);
404        let r = safe_rem_euc!(abs_a, abs_b, 0i128);
405
406        let half = (abs_b + 1) / 2;
407
408        let q_rounded = if r >= half { q + 1 } else { q };
409
410        let rounded_abs = q_rounded.wrapping_mul(abs_b);
411
412        let result = if a < 0 { -rounded_abs } else { rounded_abs };
413
414        Self::from_attos(result, Scale::TAI)
415    }
416
417    /// Returns `floor(|self| / |unit|)` as `usize`, saturating at `usize::MAX`.
418    ///
419    /// Fully exact integer arithmetic using 128-bit intermediaries. Used by `TimeRange::len`.
420    pub const fn abs_div_floor(&self, unit: Dt) -> usize {
421        if unit.is_zero() {
422            return 0;
423        }
424        let a = self.attos.wrapping_abs();
425        let b = unit.attos.wrapping_abs();
426        let q = safe_div_euc!(a, b, 0i128);
427
428        if q > (usize::MAX as i128) {
429            usize::MAX
430        } else {
431            q as usize
432        }
433    }
434
435    /// - Integer part of `rhs` is multiplied **exactly** (pure i128 arithmetic).
436    /// - Fractional part (|frac| < 1) uses the 10¹⁵ scaling.
437    pub const fn mul_by_f(&self, rhs: Real) -> Dt {
438        if rhs.is_nan() {
439            return Self::ZERO;
440        }
441        if rhs.is_infinite() {
442            if self.is_zero() {
443                return Self::ZERO;
444            }
445            let self_pos = self.attos > 0;
446            return if (rhs > 0.0) == self_pos {
447                Self::MAX
448            } else {
449                Self::MIN
450            };
451        }
452        if self.is_zero() || rhs == 0.0 {
453            return Self::ZERO;
454        }
455
456        let self_attos = self.attos;
457        let max_attos = Self::MAX.to_attos();
458        let min_attos = Self::MIN.to_attos();
459
460        // Safe extraction of integer part (handles huge |rhs| without UB)
461        let int_part = if rhs >= (i128::MAX as Real) {
462            i128::MAX
463        } else if rhs <= (i128::MIN as Real) {
464            i128::MIN
465        } else {
466            floor_f(rhs) as i128
467        };
468
469        // Huge |rhs| → definitely saturates the type
470        if int_part == i128::MAX || int_part == i128::MIN {
471            let self_pos = self.attos > 0;
472            return if (rhs > 0.0) == self_pos {
473                Self::MAX
474            } else {
475                Self::MIN
476            };
477        }
478
479        let frac_part = rhs - f!(int_part); // always in [0, 1)
480
481        // Integer part with explicit type-range saturation
482        let int_attos = if int_part == 0 {
483            0
484        } else if int_part > 0 {
485            if self_attos > 0 {
486                if int_part > max_attos / self_attos {
487                    max_attos
488                } else {
489                    self_attos.saturating_mul(int_part)
490                }
491            } else {
492                let abs_self = self_attos.wrapping_neg();
493                let abs_min = min_attos.wrapping_neg();
494                if int_part > abs_min / abs_self {
495                    min_attos
496                } else {
497                    self_attos.saturating_mul(int_part)
498                }
499            }
500        } else {
501            // int_part < 0
502            if self_attos > 0 {
503                let abs_int = int_part.wrapping_neg();
504                let abs_min = min_attos.wrapping_neg();
505                if abs_int > abs_min / self_attos {
506                    min_attos
507                } else {
508                    self_attos.saturating_mul(int_part)
509                }
510            } else {
511                let abs_self = self_attos.wrapping_neg();
512                let abs_int = int_part.wrapping_neg();
513                if abs_int > max_attos / abs_self {
514                    max_attos
515                } else {
516                    self_attos.saturating_mul(int_part)
517                }
518            }
519        };
520
521        // --- Fractional part: decomposed exact computation (never overflows i128) ---
522        const SCALE: i128 = 1_000_000_000_000_000; // 10¹⁵
523        let frac_scaled = (frac_part * (SCALE as Real)) as i128;
524
525        let frac_attos = if self_attos >= 0 {
526            let high = self_attos / SCALE;
527            let low = self_attos % SCALE;
528            let high_part = high * frac_scaled;
529            let low_part = (low * frac_scaled) / SCALE;
530            high_part + low_part
531        } else {
532            let abs_self = self_attos.wrapping_neg();
533            let high = abs_self / SCALE;
534            let low = abs_self % SCALE;
535            let high_part = high * frac_scaled;
536            let low_part = (low * frac_scaled) / SCALE;
537            let pos = high_part + low_part;
538            pos.wrapping_neg()
539        };
540
541        // Combine + final clamp
542        let total_attos = int_attos.saturating_add(frac_attos);
543        let clamped = if total_attos > max_attos {
544            max_attos
545        } else if total_attos < min_attos {
546            min_attos
547        } else {
548            total_attos
549        };
550
551        Self::from_attos(clamped, Scale::TAI)
552    }
553
554    /// Divides by a real number (routes through the high-precision `mul_by_f`).
555    #[inline]
556    pub const fn div_by_f(&self, rhs: Real) -> Dt {
557        if rhs == 0.0 || rhs.is_nan() {
558            return if self.attos >= 0 {
559                Self::MAX
560            } else {
561                Self::MIN
562            };
563        }
564        self.mul_by_f(1.0 / rhs)
565    }
566
567    /// Divides this Dt by 2 (convenience wrapper).
568    #[inline]
569    pub const fn div_by_2(&self) -> Dt {
570        self.div_by_f(2.0)
571    }
572
573    /// Clamps an `i128` to the representable range of `i64`.
574    #[inline(always)]
575    pub(crate) const fn i128_to_i64(x: i128) -> i64 {
576        let y = x as i64;
577        if x == y as i128 {
578            y
579        } else if x > 0 {
580            i64::MAX
581        } else {
582            i64::MIN
583        }
584    }
585
586    /// Converts seconds (i64) → total attoseconds (i128)
587    #[inline(always)]
588    pub const fn sec_to_attos(sec: i128) -> i128 {
589        sec.saturating_mul(ATTOS_PER_SEC_I128)
590    }
591
592    /// Converts total attoseconds → whole seconds as i64
593    #[inline(always)]
594    pub const fn attos_to_sec_i64(attos: i128) -> i64 {
595        Self::i128_to_i64(attos / ATTOS_PER_SEC_I128)
596    }
597
598    /// Clamps `value` to the range `[min, max]`.
599    ///
600    /// This is a `const fn`, so it can be used in const contexts
601    /// (e.g. const generics, statics, const evaluation, etc.).
602    ///
603    /// If `min > max`, the result is equivalent to clamping to `[max, min]`.
604    pub(crate) const fn clamp_u8(value: u8, min: u8, max: u8) -> u8 {
605        if value < min {
606            min
607        } else if value > max {
608            max
609        } else {
610            value
611        }
612    }
613
614    /// Clamps `value` to the range `[min, max]`.
615    ///
616    /// This is a `const fn`, so it can be used in const contexts
617    /// (e.g. const generics, statics, const evaluation, etc.).
618    ///
619    /// If `min > max`, the result is equivalent to clamping to `[max, min]`.
620    pub(crate) const fn clamp_u64(value: u64, min: u64, max: u64) -> u64 {
621        if value < min {
622            min
623        } else if value > max {
624            max
625        } else {
626            value
627        }
628    }
629
630    /// **Lossy** conversion of u128 attoseconds to → float seconds (s).
631    #[inline(always)]
632    pub const fn attos_to_sec_f(attos: u128) -> Real {
633        f!(attos) / ATTOS_PER_SECF
634    }
635
636    /// Converts i128 attoseconds → seconds (s)
637    #[inline(always)]
638    pub const fn attos_to_sec(attos: i128) -> i128 {
639        attos / ATTOS_PER_SEC_I128
640    }
641
642    /// Converts i128 attoseconds → milliseconds (ms)
643    #[inline(always)]
644    pub const fn attos_to_ms(attos: i128) -> i128 {
645        attos / ATTOS_PER_MS_I128
646    }
647
648    /// Converts i128 attoseconds → microseconds (us)
649    #[inline(always)]
650    pub const fn attos_to_us(attos: i128) -> i128 {
651        attos / ATTOS_PER_US_I128
652    }
653
654    /// Converts i128 attoseconds → nanoseconds (ns)
655    #[inline(always)]
656    pub const fn attos_to_ns(attos: i128) -> i128 {
657        attos / ATTOS_PER_NS_I128
658    }
659
660    /// Converts i128 attoseconds → picoseconds (ps)
661    #[inline(always)]
662    pub const fn attos_to_ps(attos: i128) -> i128 {
663        attos / ATTOS_PER_PS_I128
664    }
665
666    /// Converts i128 attoseconds → femtoseconds (fs)
667    #[inline(always)]
668    pub const fn attos_to_fs(attos: i128) -> i128 {
669        attos / ATTOS_PER_FS_I128
670    }
671
672    /// Returns the scalar ratio `self / rhs` expressed in seconds (as `Real`).
673    ///
674    /// This is the floating-point equivalent of `self.to_sec_f() / rhs.to_sec_f()`.
675    ///
676    /// # Special cases (chosen for safety and usability in time arithmetic)
677    /// - `non-zero / ZERO` returns `±Real::INFINITY` (sign matches `self`)
678    /// - `ZERO / non-zero` returns `0.0`
679    /// - `ZERO / ZERO` returns `1.0` (the two durations are identical)
680    ///
681    /// These rules avoid `NaN` entirely while remaining predictable and useful
682    /// in simulations, rate calculations, and control code.
683    ///
684    /// Negative durations are handled correctly (e.g. `(-5 s) / (2 s) == -2.5`).
685    ///
686    /// This method is `const fn` and can be used in const contexts.
687    #[inline]
688    pub const fn div_dt(self, rhs: Dt) -> Real {
689        let a = self.to_sec_f();
690        let b = rhs.to_sec_f();
691
692        if b == 0.0 {
693            if a == 0.0 {
694                1.0
695            } else {
696                Real::INFINITY.copysign(a)
697            }
698        } else {
699            a / b
700        }
701    }
702
703    /// Advances this `Dt` by the given elapsed duration while applying the relativistic proper-time correction
704    /// derived from the supplied `Spacetime` model.
705    ///
706    /// - This method is intended for simulation of remote clocks (e.g., Earth time as observed from a spacecraft).
707    /// - For a local hardware proper-time clock, use the plain `add` methods instead.
708    #[inline]
709    pub const fn adjusted_advance(&mut self, elapsed: &Dt, spacetime: &Spacetime) {
710        let dtau = elapsed.add(Drift::from_spacetime(spacetime).time_diff_after(elapsed));
711        *self = self.add(dtau);
712    }
713
714    /// Advances this `Dt` by the given elapsed duration while applying the relativistic proper-time correction
715    /// from a pre-computed `Drift` value.
716    ///
717    /// - This is an optimized variant of [`Dt::adjusted_advance`](../struct.Dt.html#method.adjusted_advance)
718    ///   for callers that already hold a [`Drift`] instance.
719    /// - This method is intended for simulation of remote clocks (e.g., Earth time as observed from a spacecraft).
720    /// - For a local hardware proper-time clock, use the plain `add` methods instead.
721    #[inline]
722    pub const fn adjusted_advance_using_drift(&mut self, elapsed: &Dt, drift: &Drift) {
723        let dtau = elapsed.add(drift.time_diff_after(elapsed));
724        *self = self.add(dtau);
725    }
726}