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}