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}