Skip to main content

df_hyperliquid_core/utils/dec/
round.rs

1//! Rounding a decimal the way asked: to places behind the point, to significant figures, or to a
2//! whole count of a smaller unit.
3
4use std::cmp::Ordering;
5
6use crate::utils::dec::{Dec, ten};
7
8/// Which way a figure moves when it has more digits than it may keep.
9#[derive(Debug, Clone, Copy, PartialEq, Eq)]
10pub enum Round {
11    /// Toward zero, which is what the venue's own clients do to an amount.
12    Down,
13    /// Away from zero.
14    Up,
15    /// To the nearer, and away from zero on a tie.
16    Nearest,
17}
18
19impl Dec {
20    /// The value as a count of `10^-places`, rounded as asked. An integer
21    /// amount on a chain (USDC in millionths, say) comes out of here.
22    #[must_use]
23    pub fn units_at(self, places: u32, round: Round) -> Option<i128> {
24        match places.cmp(&self.scale) {
25            Ordering::Equal => Some(self.units),
26            Ordering::Greater => self.units.checked_mul(ten(places - self.scale)?),
27            Ordering::Less => Some(divide(self.units, ten(self.scale - places)?, round)),
28        }
29    }
30
31    /// Kept to at most `places` behind the point.
32    #[must_use]
33    pub fn round(self, places: u32, round: Round) -> Dec {
34        if places >= self.scale {
35            return self.normal();
36        }
37        self.units_at(places, round).map_or(self, |units| Dec { units, scale: places }.normal())
38    }
39
40    /// The places `figures` significant digits reach for this value: 2 for
41    /// `123.456` at five figures, 6 for `0.01234567`; 0 when they end in the
42    /// whole part (`123456.7`).
43    #[must_use]
44    pub fn sig_places(self, figures: u32) -> u32 {
45        let places = i64::from(figures) - i64::from(digits(self.units)) + i64::from(self.scale);
46        u32::try_from(places.max(0)).unwrap_or(0)
47    }
48
49    /// Kept to at most `figures` significant digits. Digits dropped before the
50    /// point become zeros, so `123456.7` to five figures is `123450`.
51    #[must_use]
52    pub fn round_sig(self, figures: u32, round: Round) -> Dec {
53        let digits = digits(self.units);
54        if figures == 0 || digits <= figures {
55            return self.normal();
56        }
57        let dropped = digits - figures;
58        let Some(step) = ten(dropped) else {
59            return self;
60        };
61        let kept = divide(self.units, step, round);
62        match kept.checked_mul(step) {
63            Some(units) => Dec { units, scale: self.scale }.normal(),
64            None => self,
65        }
66    }
67}
68
69fn digits(units: i128) -> u32 {
70    let mut left = units.unsigned_abs();
71    let mut count = 0;
72    while left > 0 {
73        left /= 10;
74        count += 1;
75    }
76    count
77}
78
79/// Integer division with the rounding asked for; `by` is positive.
80pub(super) fn divide(value: i128, by: i128, round: Round) -> i128 {
81    let quotient = value / by;
82    let rest = value % by;
83    if rest == 0 {
84        return quotient;
85    }
86    let away = if value < 0 { -1 } else { 1 };
87    match round {
88        Round::Down => quotient,
89        Round::Up => quotient + away,
90        Round::Nearest => match (rest.unsigned_abs() * 2).cmp(&by.unsigned_abs()) {
91            Ordering::Less => quotient,
92            _ => quotient + away,
93        },
94    }
95}
96
97#[cfg(test)]
98#[path = "tests/round.rs"]
99mod tests;