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