Skip to main content

high_roller/
decimal.rs

1//! # Decimal32
2//!
3//! Decimal32 is a transparent wrapper over an i32.
4//! A const generic declares the number of places
5//! after the decimal point.
6//!
7//! The motivation for such a type is providing lossless
8//! arithmetic guarantees like in the example below.
9//!
10//! ```
11//! use high_roller::decimal::D9;
12//! use num_traits::{CheckedAdd, WrappingAdd, WrappingSub};
13//!
14//! const SMALL: f64 = 0.111000111;
15//! const LARGE: f64 = 2.147483647;
16//!
17//! const CHECKED_SMALL: D9 = D9::checked(SMALL).unwrap();
18//! const CHECKED_LARGE: D9 = D9::checked(LARGE).unwrap();
19//!
20//! // Parity with lossless operations
21//! let sum = const { D9::checked(1.).unwrap() }.checked_add(&CHECKED_SMALL);
22//! assert_eq!(sum.unwrap().get(), 1. + SMALL, "Result fits in f64");
23//!
24//! // Checked operations prevent overflow
25//! let lossy = CHECKED_LARGE.checked_add(&CHECKED_SMALL);
26//! assert_eq!(lossy, None, "Result overflows i32");
27//! assert_ne!(LARGE + SMALL - LARGE, SMALL);
28//!
29//! // Wrapping operations enable loss recovery
30//! let wrapped = CHECKED_LARGE.wrapping_add(&CHECKED_SMALL);
31//! assert_eq!(wrapped.wrapping_sub(&CHECKED_LARGE), CHECKED_SMALL);
32//! ```
33
34use core::f32;
35use core::fmt::Debug;
36use core::fmt::Display;
37use core::ops::Add;
38use core::ops::Neg;
39use core::ops::Sub;
40
41use num_traits::CheckedAdd;
42use num_traits::CheckedSub;
43use num_traits::WrappingAdd;
44use num_traits::WrappingSub;
45use thiserror::Error;
46
47/// A `Decimal32` type with one significant figure
48/// after the decimal point.
49///
50/// ```
51/// use high_roller::decimal::D1;
52///
53/// assert_eq!(D1::MAX.get(), 214748364.7_f64);
54/// assert_eq!(D1::MIN.get(), -214748364.8_f64);
55/// assert_eq!(D1::MIN_UNIT.get(), 0.1_f64);
56/// ```
57pub type D1 = Decimal32<1>;
58
59/// A `Decimal32` type with two significant figures
60/// after the decimal point.
61///
62/// ```
63/// use high_roller::decimal::D2;
64///
65/// assert_eq!(D2::MAX.get(), 21474836.47_f64);
66/// assert_eq!(D2::MIN.get(), -21474836.48_f64);
67/// assert_eq!(D2::MIN_UNIT.get(), 0.01_f64);
68/// ```
69pub type D2 = Decimal32<2>;
70
71/// A `Decimal32` type with three significant figures
72/// after the decimal point.
73///
74/// ```
75/// use high_roller::decimal::D3;
76///
77/// assert_eq!(D3::MAX.get(), 2147483.647_f64);
78/// assert_eq!(D3::MIN.get(), -2147483.648_f64);
79/// assert_eq!(D3::MIN_UNIT.get(), 0.001_f64);
80/// ```
81pub type D3 = Decimal32<3>;
82
83/// A `Decimal32` type with four significant figures
84/// after the decimal point.
85///
86/// ```
87/// use high_roller::decimal::D4;
88///
89/// assert_eq!(D4::MAX.get(), 214748.3647_f64);
90/// assert_eq!(D4::MIN.get(), -214748.3648_f64);
91/// assert_eq!(D4::MIN_UNIT.get(), 0.0001_f64);
92/// ```
93pub type D4 = Decimal32<4>;
94
95/// A `Decimal32` type with five significant figures
96/// after the decimal point.
97///
98/// ```
99/// use high_roller::decimal::D5;
100///
101/// assert_eq!(D5::MAX.get(), 21474.83647_f64);
102/// assert_eq!(D5::MIN.get(), -21474.83648_f64);
103/// assert_eq!(D5::MIN_UNIT.get(), 0.00001_f64);
104/// ```
105pub type D5 = Decimal32<5>;
106
107/// A `Decimal32` type with six significant figures
108/// after the decimal point.
109///
110/// ```
111/// use high_roller::decimal::D6;
112///
113/// assert_eq!(D6::MAX.get(), 2147.483647_f64);
114/// assert_eq!(D6::MIN.get(), -2147.483648_f64);
115/// assert_eq!(D6::MIN_UNIT.get(), 0.000001_f64);
116/// ```
117pub type D6 = Decimal32<6>;
118
119/// A `Decimal32` type with seven significant figures
120/// after the decimal point.
121///
122/// ```
123/// use high_roller::decimal::D7;
124///
125/// assert_eq!(D7::MAX.get(), 214.7483647_f64);
126/// assert_eq!(D7::MIN.get(), -214.7483648_f64);
127/// assert_eq!(D7::MIN_UNIT.get(), 0.0000001_f64);
128/// ```
129pub type D7 = Decimal32<7>;
130
131/// A `Decimal32` type with eight significant figures
132/// after the decimal point.
133///
134/// ```
135/// use high_roller::decimal::D8;
136///
137/// assert_eq!(D8::MAX.get(), 21.47483647_f64);
138/// assert_eq!(D8::MIN.get(), -21.47483648_f64);
139/// assert_eq!(D8::MIN_UNIT.get(), 0.00000001_f64);
140/// ```
141pub type D8 = Decimal32<8>;
142
143/// A `Decimal32` type with nine significant figures
144/// after the decimal point.
145///
146/// ```
147/// use high_roller::decimal::D9;
148///
149/// assert_eq!(D9::MAX.get(), 2.147483647_f64);
150/// assert_eq!(D9::MIN.get(), -2.147483648_f64);
151/// assert_eq!(D9::MIN_UNIT.get(), 0.000000001_f64);
152/// ```
153pub type D9 = Decimal32<9>;
154
155/// A 32-bit lossless number with const-defined decimal precision.
156///
157/// # Design
158///
159/// There are different ways to represent a floating point
160/// number with wrapping and saturating semantics.
161/// This design basically takes the bounds of an i32 and
162/// sticks a decimal point somewhere. So the type itself
163/// serves primarily for self-documentation and convenience.
164///
165/// IEEE 754 floating point debauchery keeps the lossless
166/// range of an f32 in signed 2^24.
167/// So an equally valid design could lock the inner value within
168/// that range and use that bound for wrapping, saturating, and
169/// checked operations.
170/// The benefit is that `get` could return an f32 without loss
171/// of precision. The cost is hardware support for arithmetic.
172/// Since wrapping arithmetic is the primary motivation for
173/// Decimal32, this was not chosen.
174///
175/// A Decimal64 type is not (yet) exposed because at that point,
176/// the [Decimal](https://crates.io/crates/rust_decimal) crate
177/// might be a better fit for your use case.
178#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
179#[repr(transparent)]
180pub struct Decimal32<const PRECISION: u32>(i32);
181
182/// Enumerates the possible errors `Decimal` operations may return.
183#[derive(Error, Debug)]
184#[non_exhaustive]
185pub enum DecimalErr {
186    #[error("The attempted operation would cause a loss of precision.")]
187    Lossy,
188}
189
190impl<const PRECISION: u32> Decimal32<PRECISION> {
191    const _PRECISION_CHECK: () = assert!(
192        PRECISION <= 9,
193        "PRECISION must be <= 9; 10ePRECISION would overflow u32"
194    );
195
196    pub const ZERO: Self = Self(0);
197
198    /// The greatest positive value this type can contain.
199    pub const MAX: Self = Self(i32::MAX);
200
201    /// The most negative value this type can contain.
202    pub const MIN: Self = Self(i32::MIN);
203
204    /// The smallest positive value this type can contain.
205    pub const MIN_UNIT: Self = Self(1);
206
207    /// Constructor that accepts any input. Truncates toward zero when
208    /// the input has more decimal places than `PRECISION`. Use [`Self::checked`]
209    /// to detect when precision is lost.
210    ///
211    /// This function takes an `f64` to prevent sneaky loss of precision.
212    /// `f32` only has a 24-bit mantissa, so values between 2^24 and
213    /// and 2^31 require an f64 to be constructed.
214    ///
215    /// ```
216    /// use high_roller::decimal::D2;
217    ///
218    /// let num = D2::cast(0.125_f64);
219    /// assert_eq!(num.get(), 0.12_f64);
220    /// ```
221    ///
222    /// If this `f64` situation is annoying for your use case,
223    /// you can still escape it entirely at compile time.
224    ///
225    /// ```
226    /// use high_roller::decimal::D3;
227    ///
228    /// const MY_F32: f32 = D3::cast(0.321_f64).get() as f32;
229    /// assert_eq!(MY_F32, 0.321);
230    /// ```
231    #[must_use]
232    #[inline]
233    pub const fn cast(value: f64) -> Self {
234        Self((value * self::scalar(PRECISION) as f64) as i32)
235    }
236
237    /// Const constructor that prevents loss of precision
238    /// from the input value.
239    ///
240    /// ### Succeeds
241    ///
242    /// ```
243    /// use high_roller::decimal::D5;
244    ///
245    /// const GOOD: D5 = D5::checked(-100.12345).unwrap();
246    /// ```
247    ///
248    /// ### Fails
249    ///
250    /// ```compile_fail
251    /// use high_roller::decimal::D9;
252    ///
253    /// const BAD: D9 = D9::checked(-100.)
254    ///     .expect("There isn't space in 32 bits for 9 decimal places after -100");
255    /// ```
256    #[must_use]
257    pub const fn checked(value: f64) -> Option<Self> {
258        let dec = Self::cast(value);
259        if dec.get() != value {
260            return None;
261        }
262        Some(dec)
263    }
264
265    /// Returns the inner value as an f64. This conversion is lossless
266    /// because f64's 53-bit mantissa can represent every i32 value exactly.
267    ///
268    /// For f32 output use [`f32::try_from`], which returns `Err(Lossy)` when
269    /// the inner value exceeds f32's 24-bit mantissa.
270    ///
271    /// ```
272    /// use high_roller::decimal::D1;
273    ///
274    /// assert_eq!(D1::cast(1.0_f64).get(), 1.0_f64);
275    /// ```
276    #[must_use]
277    #[inline]
278    pub const fn get(self) -> f64 {
279        self.0 as f64 / self::scalar(PRECISION) as f64
280    }
281}
282
283/// Helper function for the scaling constant on an
284/// inner Decimal value.
285#[inline]
286const fn scalar(precision: u32) -> u32 {
287    10u32.pow(precision)
288}
289
290impl<const P: u32> Debug for Decimal32<P> {
291    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
292        write!(f, "Decimal32<{}>({})", P, self.get())
293    }
294}
295
296impl<const P: u32> Display for Decimal32<P> {
297    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
298        write!(f, "{}", self.get())
299    }
300}
301
302impl<const P: u32> TryFrom<f32> for Decimal32<P> {
303    type Error = DecimalErr;
304
305    /// Constructs a `Decimal32` from an `f32` or returns `Err(DecimalErr::Lossy)`
306    /// if the conversion would lose precision. This might occur if the input
307    /// literal specifies more decimal places than the underlying`Decimal32` type.
308    ///
309    /// ```should_panic
310    /// use high_roller::decimal::D2;
311    ///
312    /// D2::try_from(0.123).expect("resulting decimal is 0.12");
313    /// ```
314    ///
315    /// But take care not to lose precision when constructing the
316    /// f32 input into this function. Since f32 has a 24-bit mantissa,
317    /// it cannot represent some values that Decimal32 can.
318    ///
319    /// In the example below, rustc abbreviates the float before this
320    /// function even sees it. [`Decimal32::cast`] solves this case by
321    /// using an f64 constructor.
322    ///
323    /// ```
324    /// use high_roller::decimal::D9;
325    ///
326    /// const INPUT: f32 = 2.147483647;
327    /// let expected = D9::try_from(INPUT).unwrap();
328    ///
329    /// assert_eq!(INPUT, f32::try_from(expected).unwrap());
330    /// assert_eq!(INPUT, 2.1474836, "two places were dropped");
331    /// ```
332    ///
333    fn try_from(value: f32) -> Result<Self, Self::Error> {
334        // Use f32 arithmetic for the scale step: the caller's value is already
335        // an f32, and f32 multiplication may round (e.g. 7.12f32 * 1000 → 7120)
336        // in ways that f64 arithmetic would not.
337        let dec = Self((value * self::scalar(P) as f32) as i32);
338        if dec.get() as f32 != value {
339            return Err(DecimalErr::Lossy);
340        }
341        Ok(dec)
342    }
343}
344
345impl<const P: u32> TryFrom<Decimal32<P>> for f32 {
346    type Error = DecimalErr;
347
348    /// Converts to f32. Returns `Err(Lossy)` when the inner value exceeds
349    /// f32's 24-bit mantissa. Use [`Decimal32::get`] if unchecked lossless
350    /// output is required.
351    fn try_from(value: Decimal32<P>) -> Result<Self, Self::Error> {
352        if value.0 as Self as i32 != value.0 {
353            return Err(DecimalErr::Lossy);
354        }
355        Ok(value.get() as Self)
356    }
357}
358
359impl<const P: u32> From<Decimal32<P>> for f64 {
360    fn from(val: Decimal32<P>) -> Self {
361        val.get()
362    }
363}
364
365impl<const P: u32> Add for Decimal32<P> {
366    type Output = Self;
367
368    /// Uses wrapping addition.
369    fn add(self, rhs: Self) -> Self::Output {
370        Self(self.0.wrapping_add(rhs.0))
371    }
372}
373
374impl<const P: u32> CheckedAdd for Decimal32<P> {
375    fn checked_add(&self, v: &Self) -> Option<Self> {
376        self.0.checked_add(v.0).map(Self)
377    }
378}
379
380impl<const P: u32> WrappingAdd for Decimal32<P> {
381    fn wrapping_add(&self, v: &Self) -> Self {
382        Self(self.0.wrapping_add(v.0))
383    }
384}
385
386impl<const P: u32> Default for Decimal32<P> {
387    fn default() -> Self {
388        Self::ZERO
389    }
390}
391
392impl<const P: u32> Sub for Decimal32<P> {
393    type Output = Self;
394
395    /// Uses wrapping subtraction.
396    fn sub(self, rhs: Self) -> Self::Output {
397        Self(self.0.wrapping_sub(rhs.0))
398    }
399}
400
401impl<const P: u32> CheckedSub for Decimal32<P> {
402    fn checked_sub(&self, v: &Self) -> Option<Self> {
403        self.0.checked_sub(v.0).map(Self)
404    }
405}
406
407impl<const P: u32> WrappingSub for Decimal32<P> {
408    fn wrapping_sub(&self, v: &Self) -> Self {
409        Self(self.0.wrapping_sub(v.0))
410    }
411}
412
413impl<const P: u32> Neg for Decimal32<P> {
414    type Output = Self;
415
416    /// Uses wrapping negation.
417    fn neg(self) -> Self::Output {
418        Self(self.0.wrapping_neg())
419    }
420}
421
422#[cfg(test)]
423impl<const P: u32> From<Decimal32<P>> for num_bigint::BigInt {
424    fn from(val: Decimal32<P>) -> Self {
425        Self::from(val.0)
426    }
427}
428
429#[cfg(test)]
430impl<const P: u32> TryFrom<&num_bigint::BigInt> for Decimal32<P> {
431    type Error = ();
432
433    fn try_from(val: &num_bigint::BigInt) -> Result<Self, Self::Error> {
434        let n: i32 = val.try_into().map_err(|_| ())?;
435        Ok(Self(n))
436    }
437}
438
439#[allow(clippy::missing_panics_doc)]
440#[allow(clippy::expect_used)]
441#[cfg(test)]
442pub mod decimal_tests {
443    use num_traits::CheckedAdd;
444    use num_traits::CheckedSub;
445    use num_traits::WrappingAdd;
446    use num_traits::WrappingSub;
447
448    use crate::decimal::Decimal32;
449    use crate::decimal::DecimalErr;
450    use crate::decimal::D3;
451
452    #[test]
453    fn basic_math() {
454        assert_eq_f64((D3::cast(0.001) + D3::cast(7.120)).get(), 7.121, 3);
455        assert_eq_f64((D3::cast(1.000) + D3::cast(-3.500)).get(), -2.500, 3);
456
457        // Checked addition.
458        assert_eq!(
459            D3::cast(1.000).checked_add(&D3::cast(2.000)),
460            Some(D3::cast(3.000))
461        );
462        assert_eq!(
463            Decimal32::<0>(i32::MAX).checked_add(&Decimal32::<0>(1)),
464            None
465        );
466
467        // Wrapping addition.
468        assert_eq!(
469            Decimal32::<0>(i32::MAX).wrapping_add(&Decimal32::<0>(1)),
470            Decimal32::<0>(i32::MIN)
471        );
472
473        // Subtraction
474        assert_eq_f64((D3::cast(7.121) - D3::cast(0.001)).get(), 7.120, 3);
475        assert_eq_f64((D3::cast(1.000) - D3::cast(3.500)).get(), -2.500, 3);
476
477        // Checked subtraction
478        assert_eq!(
479            D3::cast(5.000).checked_sub(&D3::cast(2.000)),
480            Some(D3::cast(3.000))
481        );
482        assert_eq!(
483            Decimal32::<0>(i32::MIN).checked_sub(&Decimal32::<0>(1)),
484            None
485        );
486
487        // Wrapping subtraction
488        assert_eq!(
489            Decimal32::<0>(i32::MIN).wrapping_sub(&Decimal32::<0>(1)),
490            Decimal32::<0>(i32::MAX)
491        );
492    }
493
494    #[test]
495    fn identities() {
496        assert_eq!(D3::default(), D3::ZERO);
497
498        // Commutative
499        assert_eq!(D3::cast(3.400) + D3::ZERO, D3::cast(3.400));
500        assert_eq!(D3::ZERO + D3::cast(3.400), D3::cast(3.400));
501
502        // Zero
503        assert_eq!(D3::cast(1.200) - D3::ZERO, D3::cast(1.200));
504    }
505
506    #[test]
507    fn ordering() {
508        let neg = D3::cast(-1.000);
509        let zero = D3::ZERO;
510        let pos = D3::cast(1.000);
511        assert!(neg < zero);
512        assert!(zero < pos);
513        assert!(neg < pos);
514        assert_eq!(zero, D3::cast(0.000));
515    }
516
517    #[test]
518    fn negation() {
519        let num = D3::cast(1.234);
520        assert_eq!(-num, D3::cast(-1.234));
521        assert_eq!(-(-num), num);
522
523        assert_eq!(-Decimal32::<0>(i32::MIN), Decimal32::<0>(i32::MIN));
524    }
525
526    #[test]
527    fn casting() {
528        // Exact
529        assert_eq_f64(D3::cast(0.001).get(), 0.001, 3);
530        assert_eq_f64(D3::cast(7.120).get(), 7.120, 3);
531        assert_eq_f64(D3::cast(-3.500).get(), -3.500, 3);
532
533        // Truncates
534        assert_eq!(Decimal32::<0>::cast(0.9999_f64).get(), 0.0_f64);
535    }
536
537    #[test]
538    fn try_from() {
539        assert!(D3::try_from(0.001_f32).is_ok());
540        assert!(D3::try_from(7.120_f32).is_ok());
541        assert!(D3::try_from(0.0_f32).is_ok());
542
543        // 1/3 is not representable at any finite decimal precision
544        assert!(matches!(
545            D3::try_from(1.0_f32 / 3.0_f32),
546            Err(DecimalErr::Lossy)
547        ));
548        // But cast accepts it
549        assert_eq_f64(D3::cast(1. / 3.).get(), 0.333, 3);
550
551        // f32::try_from permits lossless.
552        let d = D3::cast(7.120);
553        let f: f32 = f32::try_from(d).expect("lossless into f32");
554        assert_eq_f64(f.into(), 7.120, 3);
555
556        // f32::try_from does not permit loss.
557        assert!(f32::try_from(Decimal32::<1>(16_777_217_i32)).is_err());
558        assert!(f32::try_from(Decimal32::<1>(-16_777_217_i32)).is_err());
559    }
560
561    /// Checks equality within the given precision.
562    pub fn assert_eq_f64(left: f64, right: f64, precision: u32) {
563        let tolerance = 1.0 / f64::from(super::scalar(precision));
564        assert!(
565            (left - right).abs() < tolerance,
566            "equality failed: {left:?} != {right:?} (tolerance {tolerance})"
567        );
568    }
569}