Skip to main content

deep_time/dt/
arithmetic.rs

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