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