Skip to main content

df_hyperliquid_core/utils/dec/
mod.rs

1//! Exact decimals for every amount the venue prices in: read from and written as the venue's strings,
2//! added, multiplied, divided and rounded, never through a float.
3
4mod arith;
5mod round;
6mod wire;
7
8pub use round::Round;
9pub use wire::BadDec;
10
11use std::cmp::Ordering;
12
13/// The finest figure this type keeps. The venue's own ceiling is eight
14/// places for a spot amount; marks, funding and ETH in wei run deeper.
15pub const MAX_SCALE: u32 = 18;
16
17/// An exact decimal, `units / 10^scale`, for every amount the venue prices
18/// in. The venue sends and takes these as strings, and a float between the
19/// two would change the figure that gets signed.
20#[derive(Debug, Clone, Copy, Default)]
21pub struct Dec {
22    units: i128,
23    scale: u32,
24}
25
26impl Dec {
27    pub const ZERO: Dec = Dec { units: 0, scale: 0 };
28    pub const ONE: Dec = Dec { units: 1, scale: 0 };
29
30    /// `units / 10^scale`. A scale past [`MAX_SCALE`] is refused.
31    #[must_use]
32    pub const fn new(units: i128, scale: u32) -> Option<Dec> {
33        if scale > MAX_SCALE {
34            return None;
35        }
36        Some(Dec { units, scale })
37    }
38
39    /// `units / 10^SCALE` for a figure written into the code: `Dec::lit::<2>(475)`
40    /// is 4.75. The scale is checked when the program is built, so a constant
41    /// needs no fallback; one past [`MAX_SCALE`] does not compile.
42    ///
43    /// ```compile_fail,E0080
44    /// const TOO_FINE: df_hyperliquid_core::utils::Dec = df_hyperliquid_core::utils::Dec::lit::<19>(1);
45    /// ```
46    #[must_use]
47    pub const fn lit<const SCALE: u32>(units: i128) -> Dec {
48        const { assert!(SCALE <= MAX_SCALE, "a Dec keeps at most eighteen places") };
49        Dec { units, scale: SCALE }
50    }
51
52    #[must_use]
53    pub const fn from_int(whole: i64) -> Dec {
54        Dec { units: whole as i128, scale: 0 }
55    }
56
57    /// The same value with no trailing zeros behind the point.
58    #[must_use]
59    fn normal(mut self) -> Dec {
60        while self.scale > 0 && self.units % 10 == 0 {
61            self.units /= 10;
62            self.scale -= 1;
63        }
64        if self.units == 0 {
65            self.scale = 0;
66        }
67        self
68    }
69
70    #[must_use]
71    pub const fn is_zero(self) -> bool {
72        self.units == 0
73    }
74
75    #[must_use]
76    pub const fn is_negative(self) -> bool {
77        self.units < 0
78    }
79
80    #[must_use]
81    pub const fn is_positive(self) -> bool {
82        self.units > 0
83    }
84
85    #[must_use]
86    pub fn is_integer(self) -> bool {
87        self.normal().scale == 0
88    }
89
90    /// How many places behind the point the value needs.
91    #[must_use]
92    pub fn places(self) -> u32 {
93        self.normal().scale
94    }
95
96    #[must_use]
97    pub const fn abs(self) -> Dec {
98        Dec { units: self.units.abs(), scale: self.scale }
99    }
100
101    #[must_use]
102    pub const fn negate(self) -> Dec {
103        Dec { units: -self.units, scale: self.scale }
104    }
105
106    fn aligned(self, other: Dec) -> Option<(i128, i128, u32)> {
107        let scale = self.scale.max(other.scale);
108        Some((
109            self.units.checked_mul(ten(scale - self.scale)?)?,
110            other.units.checked_mul(ten(scale - other.scale)?)?,
111            scale,
112        ))
113    }
114}
115
116/// `10^power`, or `None` past what an `i128` holds.
117fn ten(power: u32) -> Option<i128> {
118    10i128.checked_pow(power)
119}
120
121impl PartialEq for Dec {
122    fn eq(&self, other: &Dec) -> bool {
123        self.cmp(other) == Ordering::Equal
124    }
125}
126
127impl Eq for Dec {}
128
129impl PartialOrd for Dec {
130    fn partial_cmp(&self, other: &Dec) -> Option<Ordering> {
131        Some(self.cmp(other))
132    }
133}
134
135impl Ord for Dec {
136    fn cmp(&self, other: &Dec) -> Ordering {
137        match self.aligned(*other) {
138            Some((a, b, _)) => a.cmp(&b),
139            // Only a figure near the i128 ceiling lands here; its sign and
140            // integer part decide it.
141            None => (self.units / ten(self.scale).unwrap_or(1)).cmp(&(other.units / ten(other.scale).unwrap_or(1))),
142        }
143    }
144}
145
146#[cfg(test)]
147#[path = "tests/dec.rs"]
148mod tests;