df_hyperliquid_core/utils/
dec.rs1use std::{cmp::Ordering, fmt, str::FromStr};
2
3use serde::{Deserialize, Deserializer, Serialize, Serializer, de};
4
5pub const MAX_SCALE: u32 = 18;
8
9#[derive(Debug, Clone, Copy, Default)]
13pub struct Dec {
14 units: i128,
15 scale: u32,
16}
17
18#[derive(Debug, Clone, Copy, PartialEq, Eq)]
20pub enum Round {
21 Down,
23 Up,
25 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 #[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 #[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 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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 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 #[must_use]
275 pub fn to_wire(self) -> String {
276 self.to_string()
277 }
278}
279
280fn 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
295fn 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 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
366impl<'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;