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