Skip to main content

df_hyperliquid_core/utils/dec/
arith.rs

1//! Arithmetic on decimals: a sum or a difference exact, a product exact to eighteen places, a
2//! quotient kept to the places asked, and `None` where a figure would pass what a `Dec` holds.
3
4use crate::utils::dec::round::{Round, divide};
5use crate::utils::dec::{Dec, MAX_SCALE, ten};
6
7impl Dec {
8    #[must_use]
9    pub fn checked_add(self, other: Dec) -> Option<Dec> {
10        let (a, b, scale) = self.aligned(other)?;
11        Some(Dec { units: a.checked_add(b)?, scale }.normal())
12    }
13
14    #[must_use]
15    pub fn checked_sub(self, other: Dec) -> Option<Dec> {
16        self.checked_add(other.negate())
17    }
18
19    /// Every figure added exactly, in order; zero for none. `None` once a
20    /// running total passes what a `Dec` holds.
21    #[must_use]
22    pub fn checked_sum(figures: impl IntoIterator<Item = Dec>) -> Option<Dec> {
23        figures.into_iter().try_fold(Dec::ZERO, Dec::checked_add)
24    }
25
26    /// Exact while the product fits eighteen places; past that, the nearest.
27    #[must_use]
28    pub fn checked_mul(self, other: Dec) -> Option<Dec> {
29        let (a, b) = (self.normal(), other.normal());
30        let units = a.units.checked_mul(b.units)?;
31        let scale = a.scale + b.scale;
32        if scale <= MAX_SCALE {
33            return Some(Dec { units, scale }.normal());
34        }
35        let units = divide(units, ten(scale - MAX_SCALE)?, Round::Nearest);
36        Some(Dec { units, scale: MAX_SCALE }.normal())
37    }
38
39    /// `self / other` kept to `places` behind the point.
40    #[must_use]
41    pub fn checked_div(self, other: Dec, places: u32, round: Round) -> Option<Dec> {
42        if other.units == 0 || places > MAX_SCALE {
43            return None;
44        }
45        let (a, b) = (self.normal(), other.normal());
46        // At one scale a/b is a division of integers. Its digits behind the
47        // point come one at a time from the remainder, which stays below the
48        // divisor, so nothing grows past the quotient itself (scaling the
49        // numerator up front would overflow for figures in the hundreds of
50        // millions at ten places).
51        let scale = a.scale.max(b.scale);
52        let top = a.units.checked_mul(ten(scale - a.scale)?)?;
53        let bottom = b.units.checked_mul(ten(scale - b.scale)?)?;
54        let negative = (top < 0) != (bottom < 0);
55        let (top, bottom) = (top.unsigned_abs(), bottom.unsigned_abs());
56        let mut units = top / bottom;
57        let mut rest = top % bottom;
58        for _ in 0..places {
59            rest = rest.checked_mul(10)?;
60            units = units.checked_mul(10)?.checked_add(rest / bottom)?;
61            rest %= bottom;
62        }
63        let away = match round {
64            Round::Down => false,
65            Round::Up => rest != 0,
66            Round::Nearest => rest != 0 && rest >= bottom - rest,
67        };
68        let units = i128::try_from(units.checked_add(u128::from(away))?).ok()?;
69        let units = if negative { -units } else { units };
70        Some(Dec { units, scale: places }.normal())
71    }
72}
73
74#[cfg(test)]
75#[path = "tests/arith.rs"]
76mod tests;