deep_time/dt/arithmetic.rs
1use crate::{
2 ATTOS_PER_FS_I128, ATTOS_PER_HOUR, ATTOS_PER_MIN, ATTOS_PER_MS_I128, ATTOS_PER_NS_I128,
3 ATTOS_PER_PS_I128, ATTOS_PER_SEC_I128, ATTOS_PER_US_I128, Dt, Real, floor_f,
4};
5
6impl Dt {
7 /// Computes the signed duration between this [`Dt`] and another [`Dt`].
8 ///
9 /// Does **not** perform any time scale conversion.
10 #[inline(always)]
11 pub const fn to_diff_raw(&self, other: Dt) -> Dt {
12 Dt::new(
13 self.attos.saturating_sub(other.attos),
14 self.scale,
15 self.target,
16 )
17 }
18
19 /// Computes the signed duration between this [`Dt`] and another [`Dt`] as a float.
20 ///
21 /// Does **not** perform any time scale conversion.
22 #[inline(always)]
23 pub const fn to_diff_raw_f(&self, other: Dt) -> Real {
24 self.to_sec_f() - other.to_sec_f()
25 }
26
27 /// Saturating add, keeps `self`'s `scale` and `target`.
28 ///
29 /// Does **not** perform any time scale conversion.
30 #[inline]
31 pub const fn add(&self, dt: Dt) -> Dt {
32 if !dt.is_zero() {
33 Dt::new(self.attos.saturating_add(dt.attos), self.scale, self.target)
34 } else {
35 *self
36 }
37 }
38
39 /// Saturating sub, keeps `self`'s `scale` and `target`.
40 ///
41 /// Does **not** perform any time scale conversion.
42 #[inline]
43 pub const fn sub(&self, dt: Dt) -> Dt {
44 if !dt.is_zero() {
45 Dt::new(self.attos.saturating_sub(dt.attos), self.scale, self.target)
46 } else {
47 *self
48 }
49 }
50
51 /// Adds the specified number of attoseconds to this time value.
52 ///
53 /// ## Examples
54 ///
55 /// ```rust
56 /// use deep_time::Dt;
57 ///
58 /// let dt = Dt::ZERO;
59 /// let sub_5 = dt.add_attos(-5);
60 /// assert_eq!(sub_5.to_attos(), -5);
61 /// ```
62 #[inline(always)]
63 pub const fn add_attos(&self, n: i128) -> Dt {
64 Dt::new(self.attos.saturating_add(n), self.scale, self.target)
65 }
66
67 /// Adds the specified number of femtoseconds to this time value.
68 ///
69 /// ## Examples
70 ///
71 /// ```rust
72 /// use deep_time::Dt;
73 ///
74 /// let dt = Dt::ZERO;
75 /// let sub_5 = dt.add_fs(-5);
76 /// assert_eq!(sub_5.to_fs().0, -5);
77 /// ```
78 #[inline(always)]
79 pub const fn add_fs(&self, n: i128) -> Dt {
80 self.add_attos(n.saturating_mul(ATTOS_PER_FS_I128))
81 }
82
83 /// Adds the specified number of picoseconds to this time value.
84 ///
85 /// ## Examples
86 ///
87 /// ```rust
88 /// use deep_time::Dt;
89 ///
90 /// let dt = Dt::ZERO;
91 /// let sub_5 = dt.add_ps(-5);
92 /// assert_eq!(sub_5.to_ps().0, -5);
93 /// ```
94 #[inline(always)]
95 pub const fn add_ps(&self, n: i128) -> Dt {
96 self.add_attos(n.saturating_mul(ATTOS_PER_PS_I128))
97 }
98
99 /// Adds the specified number of nanoseconds to this time value.
100 ///
101 /// ## Examples
102 ///
103 /// ```rust
104 /// use deep_time::Dt;
105 ///
106 /// let dt = Dt::ZERO;
107 /// let sub_5 = dt.add_ns(-5);
108 /// assert_eq!(sub_5.to_ns().0, -5);
109 /// ```
110 #[inline(always)]
111 pub const fn add_ns(&self, n: i128) -> Dt {
112 self.add_attos(n.saturating_mul(ATTOS_PER_NS_I128))
113 }
114
115 /// Adds the specified number of microseconds to this time value.
116 ///
117 /// ## Examples
118 ///
119 /// ```rust
120 /// use deep_time::Dt;
121 ///
122 /// let dt = Dt::ZERO;
123 /// let sub_5 = dt.add_us(-5);
124 /// assert_eq!(sub_5.to_us().0, -5);
125 /// ```
126 #[inline(always)]
127 pub const fn add_us(&self, n: i128) -> Dt {
128 self.add_attos(n.saturating_mul(ATTOS_PER_US_I128))
129 }
130
131 /// Adds the specified number of milliseconds to this time value.
132 ///
133 /// ## Examples
134 ///
135 /// ```rust
136 /// use deep_time::Dt;
137 ///
138 /// let dt = Dt::ZERO;
139 /// let sub_5 = dt.add_ms(-5);
140 /// assert_eq!(sub_5.to_ms().0, -5);
141 /// ```
142 #[inline(always)]
143 pub const fn add_ms(&self, n: i128) -> Dt {
144 self.add_attos(n.saturating_mul(ATTOS_PER_MS_I128))
145 }
146
147 /// Adds the specified number of seconds to this time value using saturating arithmetic.
148 ///
149 /// ## Examples
150 ///
151 /// ```rust
152 /// use deep_time::Dt;
153 ///
154 /// let dt = Dt::ZERO;
155 /// let sub_5 = dt.add_sec(-5);
156 /// assert_eq!(sub_5.to_sec(), -5);
157 /// ```
158 #[inline(always)]
159 pub const fn add_sec(&self, n: i128) -> Dt {
160 self.add_attos(n.saturating_mul(ATTOS_PER_SEC_I128))
161 }
162
163 /// Adds the specified number of minutes to this time value using saturating arithmetic.
164 ///
165 /// ## Examples
166 ///
167 /// ```rust
168 /// use deep_time::Dt;
169 ///
170 /// let dt = Dt::ZERO;
171 /// let sub_5 = dt.add_mins(-5);
172 /// assert_eq!(sub_5.to_mins_floor().0, -5);
173 /// ```
174 #[inline(always)]
175 pub const fn add_mins(&self, n: i128) -> Dt {
176 self.add_attos(n.saturating_mul(ATTOS_PER_MIN))
177 }
178
179 /// Adds the specified number of hours to this time value using saturating arithmetic.
180 ///
181 /// ## Examples
182 ///
183 /// ```rust
184 /// use deep_time::Dt;
185 ///
186 /// let dt = Dt::ZERO;
187 /// let sub_5 = dt.add_hours(-5);
188 /// assert_eq!(sub_5.to_hours_floor().0, -5);
189 /// ```
190 #[inline(always)]
191 pub const fn add_hours(&self, n: i128) -> Dt {
192 self.add_attos(n.saturating_mul(ATTOS_PER_HOUR))
193 }
194
195 /// Returns `true` if this time is zero.
196 ///
197 /// Does **not** perform any time scale conversion.
198 #[inline(always)]
199 pub const fn is_zero(&self) -> bool {
200 self.attos == 0
201 }
202
203 /// Returns `true` if this time is strictly positive **> 0**.
204 ///
205 /// Does **not** perform any time scale conversion.
206 #[inline(always)]
207 pub const fn is_positive(&self) -> bool {
208 self.attos > 0
209 }
210
211 /// Multiplies this time by an integer scalar.
212 ///
213 /// Uses 128-bit arithmetic internally.
214 pub const fn mul(self, rhs: i64) -> Dt {
215 if rhs == 0 || self.is_zero() {
216 return Self::ZERO;
217 }
218 let total = self.attos.saturating_mul(rhs as i128);
219 Dt::new(total, self.scale, self.target)
220 }
221
222 /// Divides this `Dt` by an integer scalar.
223 ///
224 /// Uses truncating division (rounds toward zero), same as normal integer division.
225 /// Returns `ZERO` if `rhs == 0`.
226 pub const fn div(self, rhs: i64) -> Dt {
227 if rhs == 0 || self.is_zero() {
228 return Self::ZERO;
229 }
230 let result = self.attos / (rhs as i128);
231 Dt::new(result, self.scale, self.target)
232 }
233
234 /// Returns the **largest** multiple of `unit` that is ≤ `self`.
235 /// If `unit` is zero, returns `self` unchanged (exact, full precision).
236 pub const fn floor(&self, unit: Dt) -> Dt {
237 if unit.is_zero() {
238 return *self;
239 }
240 let a = self.attos;
241 let b = unit.attos;
242 let q = safe_div_euc!(a, b, 0i128);
243 let result = q.wrapping_mul(b);
244 Dt::new(result, self.scale, self.target)
245 }
246
247 /// Returns the **smallest** multiple of `unit` that is ≥ `self`.
248 /// If `unit` is zero, returns `self` unchanged (exact, full precision).
249 pub const fn ceil(&self, unit: Dt) -> Dt {
250 if unit.is_zero() {
251 return *self;
252 }
253 let a = self.attos;
254 let b = unit.attos;
255 // ceil(a/b) ≡ −floor(−a/b)
256 let neg_a = a.wrapping_neg();
257 let q = safe_div_euc!(neg_a, b, 0i128);
258 let q_ceil = q.wrapping_neg();
259 let result = q_ceil.wrapping_mul(b);
260 Dt::new(result, self.scale, self.target)
261 }
262
263 /// ## Examples
264 ///
265 /// ```rust
266 /// use deep_time::{Dt, TimeTraits};
267 ///
268 /// // Round to nearest second
269 /// let dt = 1.3.sec();
270 /// assert_eq!(dt.round(1.sec()), 1.sec());
271 ///
272 /// let dt = 1.6.sec();
273 /// assert_eq!(dt.round(1.sec()), 2.sec());
274 ///
275 /// // Negative values
276 /// let dt = (-1.3).sec();
277 /// assert_eq!(dt.round(1.sec()), (-1).sec());
278 ///
279 /// // Halfway cases round *away from zero*
280 /// assert_eq!(0.5.sec().round(1.sec()), 1.sec());
281 /// assert_eq!((-0.5).sec().round(1.sec()), (-1).sec());
282 ///
283 /// assert_eq!(1.5.sec().round(1.sec()), 2.sec());
284 /// assert_eq!((-1.5).sec().round(1.sec()), (-2).sec());
285 ///
286 /// // Round to nearest minute
287 /// let dt = (1.mins() + 40.sec()).round(1.mins());
288 /// assert_eq!(dt, 2.mins());
289 ///
290 /// // Round to nearest hour
291 /// let dt = 1.6.hours().round(1.hours());
292 /// assert_eq!(dt, 2.hours());
293 /// ```
294 pub const fn round(&self, unit: Dt) -> Dt {
295 if unit.is_zero() {
296 return *self;
297 }
298
299 let a = self.attos;
300 let b = unit.attos;
301
302 let abs_a = a.wrapping_abs();
303 let abs_b = b.wrapping_abs();
304
305 let q = safe_div_euc!(abs_a, abs_b, 0i128);
306 let r = safe_rem_euc!(abs_a, abs_b, 0i128);
307
308 let half = (abs_b + 1) / 2;
309
310 let q_rounded = if r >= half { q + 1 } else { q };
311
312 let rounded_abs = q_rounded.wrapping_mul(abs_b);
313
314 let result = if a < 0 { -rounded_abs } else { rounded_abs };
315
316 Dt::new(result, self.scale, self.target)
317 }
318
319 /// Returns `floor(|self| / |unit|)` as `usize`, saturating at `usize::MAX`.
320 ///
321 /// Fully exact integer arithmetic using 128-bit intermediaries. Used by `TimeRange::len`.
322 pub const fn abs_div_floor(&self, unit: Dt) -> usize {
323 if unit.is_zero() {
324 return 0;
325 }
326 let a = self.attos.wrapping_abs();
327 let b = unit.attos.wrapping_abs();
328 let q = safe_div_euc!(a, b, 0i128);
329
330 if q > (usize::MAX as i128) {
331 usize::MAX
332 } else {
333 q as usize
334 }
335 }
336
337 /// Multiplies this [`Dt`] by a floating-point scalar using saturating attosecond arithmetic.
338 ///
339 /// ## Algorithm
340 ///
341 /// - `rhs` is split into an **integer part** ([`floor_f`]) and a **fractional part** in `[0, 1)`.
342 /// - The integer part is multiplied exactly via [`i128::checked_mul`], saturating to
343 /// [`Dt::MAX`] / [`Dt::MIN`] on overflow.
344 /// - The fractional part is applied via a `10¹⁵`-scaled decomposition that avoids
345 /// intermediate `i128` overflow.
346 /// - The two parts are combined with [`i128::saturating_add`] and clamped to the
347 /// representable attosecond range.
348 ///
349 /// ## Precision
350 ///
351 /// - Integer scalars (e.g. `2.0`, `-3.0`) use exact integer arithmetic for their whole part.
352 /// - General `f64` scalars are limited by IEEE-754 precision (~15 decimal digits) and the
353 /// `10¹⁵` fractional quantization.
354 ///
355 /// ## Special cases
356 ///
357 /// | Condition | Result |
358 /// |---|---|
359 /// | `rhs` is NaN | [`Dt::ZERO`] |
360 /// | `rhs` is ±∞ and `self` is zero | [`Dt::ZERO`] |
361 /// | `rhs` is ±∞ and `self` is non-zero | [`Dt::MAX`] or [`Dt::MIN`] (sign of product) |
362 /// | `rhs == 0.0` or `self` is zero | [`Dt::ZERO`] |
363 /// | Product exceeds `i128` range | [`Dt::MAX`] or [`Dt::MIN`] (sign of product) |
364 ///
365 /// `NaN` maps to zero rather than poisoning the result: [`Dt`] has no NaN state, and zero
366 /// is the additive identity (a safe, non-saturating default for invalid scale factors).
367 pub const fn mul_by_f(&self, rhs: Real) -> Dt {
368 if rhs.is_nan() {
369 return Self::ZERO;
370 }
371 if rhs.is_infinite() {
372 if self.is_zero() {
373 return Self::ZERO;
374 }
375 let self_pos = self.attos > 0;
376 return if (rhs > 0.0) == self_pos {
377 Self::MAX
378 } else {
379 Self::MIN
380 };
381 }
382 if self.is_zero() || rhs == 0.0 {
383 return Self::ZERO;
384 }
385
386 let self_attos = self.attos;
387 let max_attos = Self::MAX.to_attos();
388 let min_attos = Self::MIN.to_attos();
389
390 // Safe extraction of integer part (handles huge |rhs| without UB)
391 let int_part = if rhs >= (i128::MAX as Real) {
392 i128::MAX
393 } else if rhs <= (i128::MIN as Real) {
394 i128::MIN
395 } else {
396 floor_f(rhs) as i128
397 };
398
399 // Huge |rhs| integer → product cannot fit; saturate immediately.
400 if int_part == i128::MAX || int_part == i128::MIN {
401 let self_pos = self.attos > 0;
402 return if (rhs > 0.0) == self_pos {
403 Self::MAX
404 } else {
405 Self::MIN
406 };
407 }
408
409 let frac_part = rhs - f!(int_part); // always in [0, 1)
410
411 let int_attos = if int_part == 0 {
412 0
413 } else {
414 Self::saturating_mul_attos(int_part, self_attos, max_attos, min_attos)
415 };
416
417 // Fractional part: decomposed exact computation (never overflows i128)
418 const SCALE: i128 = 1_000_000_000_000_000; // 10¹⁵
419 let frac_scaled = (frac_part * (SCALE as Real)) as i128;
420
421 let frac_attos = if self_attos >= 0 {
422 let high = self_attos / SCALE;
423 let low = self_attos % SCALE;
424 let high_part = high * frac_scaled;
425 let low_part = (low * frac_scaled) / SCALE;
426 high_part + low_part
427 } else {
428 let abs_self = self_attos.wrapping_neg();
429 let high = abs_self / SCALE;
430 let low = abs_self % SCALE;
431 let high_part = high * frac_scaled;
432 let low_part = (low * frac_scaled) / SCALE;
433 let pos = high_part + low_part;
434 pos.wrapping_neg()
435 };
436
437 let total_attos = int_attos.saturating_add(frac_attos);
438 let clamped = if total_attos > max_attos {
439 max_attos
440 } else if total_attos < min_attos {
441 min_attos
442 } else {
443 total_attos
444 };
445
446 Dt::new(clamped, self.scale, self.target)
447 }
448
449 /// `a * b` as attoseconds, saturating to `[min_attos, max_attos]` when not representable.
450 #[inline(always)]
451 pub(crate) const fn saturating_mul_attos(
452 a: i128,
453 b: i128,
454 max_attos: i128,
455 min_attos: i128,
456 ) -> i128 {
457 match a.checked_mul(b) {
458 Some(product) => product,
459 None => {
460 let a_neg = a < 0;
461 let b_neg = b < 0;
462 if a_neg == b_neg { max_attos } else { min_attos }
463 }
464 }
465 }
466
467 /// Divides by a real number (routes through the high-precision `mul_by_f`).
468 #[inline]
469 pub const fn div_by_f(&self, rhs: Real) -> Dt {
470 if rhs == 0.0 || rhs.is_nan() {
471 return if self.attos >= 0 {
472 Self::MAX
473 } else {
474 Self::MIN
475 };
476 }
477 self.mul_by_f(1.0 / rhs)
478 }
479
480 /// Divides this Dt by 2 (convenience wrapper).
481 #[inline]
482 pub const fn div_by_2(&self) -> Dt {
483 self.div_by_f(2.0)
484 }
485
486 /// Returns the scalar ratio `self / rhs` expressed in seconds (as `Real`).
487 ///
488 /// This is the floating-point equivalent of `self.to_sec_f() / rhs.to_sec_f()`.
489 ///
490 /// # Special cases (chosen for safety and usability in time arithmetic)
491 /// - `non-zero / ZERO` returns `±Real::INFINITY` (sign matches `self`)
492 /// - `ZERO / non-zero` returns `0.0`
493 /// - `ZERO / ZERO` returns `1.0` (the two durations are identical)
494 ///
495 /// These rules avoid `NaN` entirely while remaining predictable and useful
496 /// in simulations, rate calculations, and control code.
497 ///
498 /// Negative durations are supported (e.g. `(-5 s) / (2 s) == -2.5`).
499 ///
500 /// This method is `const fn` and can be used in const contexts.
501 #[inline]
502 pub const fn div_dt(self, rhs: Dt) -> Real {
503 let a = self.to_sec_f();
504 let b = rhs.to_sec_f();
505
506 if b == 0.0 {
507 if a == 0.0 {
508 1.0
509 } else {
510 Real::INFINITY.copysign(a)
511 }
512 } else {
513 a / b
514 }
515 }
516}