Skip to main content

df_hyperliquid_core/utils/
dec.rs

1use std::{cmp::Ordering, fmt, str::FromStr};
2
3use serde::{Deserialize, Deserializer, Serialize, Serializer, de};
4
5/// The finest figure this type keeps. The venue's own ceiling is eight
6/// places for a spot amount; marks, funding and ETH in wei run deeper.
7pub const MAX_SCALE: u32 = 18;
8
9/// An exact decimal, `units / 10^scale`, for every amount the venue prices
10/// in. The venue sends and takes these as strings, and a float between the
11/// two would change the figure that gets signed.
12#[derive(Debug, Clone, Copy, Default)]
13pub struct Dec {
14    units: i128,
15    scale: u32,
16}
17
18/// Which way a figure moves when it has more digits than it may keep.
19#[derive(Debug, Clone, Copy, PartialEq, Eq)]
20pub enum Round {
21    /// Toward zero, which is what the venue's own clients do to an amount.
22    Down,
23    /// Away from zero.
24    Up,
25    /// To the nearer, and away from zero on a tie.
26    Nearest,
27}
28
29#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
30pub enum BadDec {
31    #[error("`{0}` is not a decimal number")]
32    NotANumber(String),
33    #[error("`{0}` has more than eighteen decimal places")]
34    TooFine(String),
35    #[error("`{0}` is larger than this build can count")]
36    OutOfRange(String),
37}
38
39impl Dec {
40    pub const ZERO: Dec = Dec { units: 0, scale: 0 };
41    pub const ONE: Dec = Dec { units: 1, scale: 0 };
42
43    /// `units / 10^scale`. A scale past [`MAX_SCALE`] is refused.
44    #[must_use]
45    pub const fn new(units: i128, scale: u32) -> Option<Dec> {
46        if scale > MAX_SCALE {
47            return None;
48        }
49        Some(Dec { units, scale })
50    }
51
52    /// `units / 10^SCALE` for a figure written into the code: `Dec::lit::<2>(475)`
53    /// is 4.75. The scale is checked when the program is built, so a constant
54    /// needs no fallback; one past [`MAX_SCALE`] does not compile.
55    ///
56    /// ```compile_fail,E0080
57    /// const TOO_FINE: df_hyperliquid_core::utils::Dec = df_hyperliquid_core::utils::Dec::lit::<19>(1);
58    /// ```
59    #[must_use]
60    pub const fn lit<const SCALE: u32>(units: i128) -> Dec {
61        const { assert!(SCALE <= MAX_SCALE, "a Dec keeps at most eighteen places") };
62        Dec { units, scale: SCALE }
63    }
64
65    #[must_use]
66    pub const fn from_int(whole: i64) -> Dec {
67        Dec { units: whole as i128, scale: 0 }
68    }
69
70    /// Reads what the venue writes: an optional sign, digits, and at most one
71    /// point. No exponent, since the venue never sends one.
72    pub fn parse(text: &str) -> Result<Dec, BadDec> {
73        let bad = || BadDec::NotANumber(text.to_owned());
74        let body = text.trim();
75        let (negative, body) = match body.strip_prefix('-') {
76            Some(rest) => (true, rest),
77            None => (false, body.strip_prefix('+').unwrap_or(body)),
78        };
79        let (whole, fraction) = body.split_once('.').unwrap_or((body, ""));
80        if whole.is_empty() && fraction.is_empty() {
81            return Err(bad());
82        }
83        if !whole.bytes().all(|b| b.is_ascii_digit()) || !fraction.bytes().all(|b| b.is_ascii_digit()) {
84            return Err(bad());
85        }
86        let fraction = fraction.trim_end_matches('0');
87        let scale = u32::try_from(fraction.len()).map_err(|_| BadDec::TooFine(text.to_owned()))?;
88        if scale > MAX_SCALE {
89            return Err(BadDec::TooFine(text.to_owned()));
90        }
91        let mut units: i128 = 0;
92        for digit in whole.bytes().chain(fraction.bytes()) {
93            units = units
94                .checked_mul(10)
95                .and_then(|u| u.checked_add(i128::from(digit - b'0')))
96                .ok_or_else(|| BadDec::OutOfRange(text.to_owned()))?;
97        }
98        Ok(Dec { units: if negative { -units } else { units }, scale }.normal())
99    }
100
101    /// The same value with no trailing zeros behind the point.
102    #[must_use]
103    fn normal(mut self) -> Dec {
104        while self.scale > 0 && self.units % 10 == 0 {
105            self.units /= 10;
106            self.scale -= 1;
107        }
108        if self.units == 0 {
109            self.scale = 0;
110        }
111        self
112    }
113
114    #[must_use]
115    pub const fn is_zero(self) -> bool {
116        self.units == 0
117    }
118
119    #[must_use]
120    pub const fn is_negative(self) -> bool {
121        self.units < 0
122    }
123
124    #[must_use]
125    pub const fn is_positive(self) -> bool {
126        self.units > 0
127    }
128
129    #[must_use]
130    pub fn is_integer(self) -> bool {
131        self.normal().scale == 0
132    }
133
134    /// How many places behind the point the value needs.
135    #[must_use]
136    pub fn places(self) -> u32 {
137        self.normal().scale
138    }
139
140    #[must_use]
141    pub const fn abs(self) -> Dec {
142        Dec { units: self.units.abs(), scale: self.scale }
143    }
144
145    #[must_use]
146    pub const fn negate(self) -> Dec {
147        Dec { units: -self.units, scale: self.scale }
148    }
149
150    /// The value as a count of `10^-places`, rounded as asked. An integer
151    /// amount on a chain (USDC in millionths, say) comes out of here.
152    #[must_use]
153    pub fn units_at(self, places: u32, round: Round) -> Option<i128> {
154        match places.cmp(&self.scale) {
155            Ordering::Equal => Some(self.units),
156            Ordering::Greater => self.units.checked_mul(ten(places - self.scale)?),
157            Ordering::Less => Some(divide(self.units, ten(self.scale - places)?, round)),
158        }
159    }
160
161    /// Kept to at most `places` behind the point.
162    #[must_use]
163    pub fn round(self, places: u32, round: Round) -> Dec {
164        if places >= self.scale {
165            return self.normal();
166        }
167        self.units_at(places, round).map_or(self, |units| Dec { units, scale: places }.normal())
168    }
169
170    /// The places `figures` significant digits reach for this value: 2 for
171    /// `123.456` at five figures, 6 for `0.01234567`; 0 when they end in the
172    /// whole part (`123456.7`).
173    #[must_use]
174    pub fn sig_places(self, figures: u32) -> u32 {
175        let places = i64::from(figures) - i64::from(digits(self.units)) + i64::from(self.scale);
176        u32::try_from(places.max(0)).unwrap_or(0)
177    }
178
179    /// Kept to at most `figures` significant digits. Digits dropped before the
180    /// point become zeros, so `123456.7` to five figures is `123450`.
181    #[must_use]
182    pub fn round_sig(self, figures: u32, round: Round) -> Dec {
183        let digits = digits(self.units);
184        if figures == 0 || digits <= figures {
185            return self.normal();
186        }
187        let dropped = digits - figures;
188        let Some(step) = ten(dropped) else {
189            return self;
190        };
191        let kept = divide(self.units, step, round);
192        match kept.checked_mul(step) {
193            Some(units) => Dec { units, scale: self.scale }.normal(),
194            None => self,
195        }
196    }
197
198    fn aligned(self, other: Dec) -> Option<(i128, i128, u32)> {
199        let scale = self.scale.max(other.scale);
200        Some((
201            self.units.checked_mul(ten(scale - self.scale)?)?,
202            other.units.checked_mul(ten(scale - other.scale)?)?,
203            scale,
204        ))
205    }
206
207    #[must_use]
208    pub fn checked_add(self, other: Dec) -> Option<Dec> {
209        let (a, b, scale) = self.aligned(other)?;
210        Some(Dec { units: a.checked_add(b)?, scale }.normal())
211    }
212
213    #[must_use]
214    pub fn checked_sub(self, other: Dec) -> Option<Dec> {
215        self.checked_add(other.negate())
216    }
217
218    /// Every figure added exactly, in order; zero for none. `None` once a
219    /// running total passes what a `Dec` holds.
220    #[must_use]
221    pub fn checked_sum(figures: impl IntoIterator<Item = Dec>) -> Option<Dec> {
222        figures.into_iter().try_fold(Dec::ZERO, Dec::checked_add)
223    }
224
225    /// Exact while the product fits eighteen places; past that, the nearest.
226    #[must_use]
227    pub fn checked_mul(self, other: Dec) -> Option<Dec> {
228        let (a, b) = (self.normal(), other.normal());
229        let units = a.units.checked_mul(b.units)?;
230        let scale = a.scale + b.scale;
231        if scale <= MAX_SCALE {
232            return Some(Dec { units, scale }.normal());
233        }
234        let units = divide(units, ten(scale - MAX_SCALE)?, Round::Nearest);
235        Some(Dec { units, scale: MAX_SCALE }.normal())
236    }
237
238    /// `self / other` kept to `places` behind the point.
239    #[must_use]
240    pub fn checked_div(self, other: Dec, places: u32, round: Round) -> Option<Dec> {
241        if other.units == 0 || places > MAX_SCALE {
242            return None;
243        }
244        let (a, b) = (self.normal(), other.normal());
245        // At one scale a/b is a division of integers. Its digits behind the
246        // point come one at a time from the remainder, which stays below the
247        // divisor, so nothing grows past the quotient itself (scaling the
248        // numerator up front would overflow for figures in the hundreds of
249        // millions at ten places).
250        let scale = a.scale.max(b.scale);
251        let top = a.units.checked_mul(ten(scale - a.scale)?)?;
252        let bottom = b.units.checked_mul(ten(scale - b.scale)?)?;
253        let negative = (top < 0) != (bottom < 0);
254        let (top, bottom) = (top.unsigned_abs(), bottom.unsigned_abs());
255        let mut units = top / bottom;
256        let mut rest = top % bottom;
257        for _ in 0..places {
258            rest = rest.checked_mul(10)?;
259            units = units.checked_mul(10)?.checked_add(rest / bottom)?;
260            rest %= bottom;
261        }
262        let away = match round {
263            Round::Down => false,
264            Round::Up => rest != 0,
265            Round::Nearest => rest != 0 && rest >= bottom - rest,
266        };
267        let units = i128::try_from(units.checked_add(u128::from(away))?).ok()?;
268        let units = if negative { -units } else { units };
269        Some(Dec { units, scale: places }.normal())
270    }
271
272    /// The canonical spelling: no trailing zeros, no exponent, `0` for zero.
273    /// This is the string that is hashed, so it must match the venue's clients.
274    #[must_use]
275    pub fn to_wire(self) -> String {
276        self.to_string()
277    }
278}
279
280/// `10^power`, or `None` past what an `i128` holds.
281fn ten(power: u32) -> Option<i128> {
282    10i128.checked_pow(power)
283}
284
285fn digits(units: i128) -> u32 {
286    let mut left = units.unsigned_abs();
287    let mut count = 0;
288    while left > 0 {
289        left /= 10;
290        count += 1;
291    }
292    count
293}
294
295/// Integer division with the rounding asked for; `by` is positive.
296fn divide(value: i128, by: i128, round: Round) -> i128 {
297    let quotient = value / by;
298    let rest = value % by;
299    if rest == 0 {
300        return quotient;
301    }
302    let away = if value < 0 { -1 } else { 1 };
303    match round {
304        Round::Down => quotient,
305        Round::Up => quotient + away,
306        Round::Nearest => match (rest.unsigned_abs() * 2).cmp(&by.unsigned_abs()) {
307            Ordering::Less => quotient,
308            _ => quotient + away,
309        },
310    }
311}
312
313impl PartialEq for Dec {
314    fn eq(&self, other: &Dec) -> bool {
315        self.cmp(other) == Ordering::Equal
316    }
317}
318
319impl Eq for Dec {}
320
321impl PartialOrd for Dec {
322    fn partial_cmp(&self, other: &Dec) -> Option<Ordering> {
323        Some(self.cmp(other))
324    }
325}
326
327impl Ord for Dec {
328    fn cmp(&self, other: &Dec) -> Ordering {
329        match self.aligned(*other) {
330            Some((a, b, _)) => a.cmp(&b),
331            // Only a figure near the i128 ceiling lands here; its sign and
332            // integer part decide it.
333            None => (self.units / ten(self.scale).unwrap_or(1)).cmp(&(other.units / ten(other.scale).unwrap_or(1))),
334        }
335    }
336}
337
338impl fmt::Display for Dec {
339    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
340        let normal = self.normal();
341        let sign = if normal.units < 0 { "-" } else { "" };
342        let magnitude = normal.units.unsigned_abs();
343        if normal.scale == 0 {
344            return write!(f, "{sign}{magnitude}");
345        }
346        let step = 10u128.pow(normal.scale);
347        let places = normal.scale as usize;
348        write!(f, "{sign}{}.{:0places$}", magnitude / step, magnitude % step)
349    }
350}
351
352impl FromStr for Dec {
353    type Err = BadDec;
354
355    fn from_str(text: &str) -> Result<Self, Self::Err> {
356        Dec::parse(text)
357    }
358}
359
360impl Serialize for Dec {
361    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
362        serializer.collect_str(self)
363    }
364}
365
366/// From the venue's decimal strings, or a JSON integer. A JSON fraction is
367/// refused: it has already been through a float.
368impl<'de> Deserialize<'de> for Dec {
369    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
370        struct Visitor;
371
372        impl de::Visitor<'_> for Visitor {
373            type Value = Dec;
374
375            fn expecting(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
376                f.write_str("a decimal string or an integer")
377            }
378
379            fn visit_str<E: de::Error>(self, text: &str) -> Result<Dec, E> {
380                Dec::parse(text).map_err(E::custom)
381            }
382
383            fn visit_u64<E: de::Error>(self, value: u64) -> Result<Dec, E> {
384                Ok(Dec { units: i128::from(value), scale: 0 })
385            }
386
387            fn visit_i64<E: de::Error>(self, value: i64) -> Result<Dec, E> {
388                Ok(Dec { units: i128::from(value), scale: 0 })
389            }
390        }
391
392        deserializer.deserialize_any(Visitor)
393    }
394}
395
396#[cfg(test)]
397#[path = "tests/dec.rs"]
398mod tests;