Skip to main content

nexus_decimal/
decimal.rs

1//! Core `Decimal<B, D>` type definition and constructors.
2
3use crate::backing::Backing;
4
5/// Fixed-point decimal with compile-time backing type and precision.
6///
7/// `B` is the backing integer type (`i32`, `i64`, `i128`).
8/// `DECIMALS` is the number of fractional digits. Any combination
9/// where `10^DECIMALS` fits in `B` is valid — `Decimal<i64, 2>` for
10/// USD or `Decimal<i64, 8>` for BTC without any macro invocation.
11///
12/// The scale factor `10^DECIMALS` is validated at compile time.
13/// Invalid combinations (e.g., `Decimal<i32, 10>`) fail to compile
14/// when any associated constant or method is used.
15///
16/// # Examples
17///
18/// ```
19/// use nexus_decimal::Decimal;
20/// type D64 = Decimal<i64, 8>;
21///
22/// const PRICE: D64 = D64::new(100, 50_000_000); // 100.50
23/// const FEE: D64 = D64::from_raw(500_000);       // 0.005
24/// const TOTAL: D64 = match PRICE.checked_add(FEE) {
25///     Some(v) => v,
26///     None => panic!("overflow"),
27/// };
28/// ```
29#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
30#[repr(transparent)]
31pub struct Decimal<B: Backing, const DECIMALS: u8> {
32    pub(crate) value: B,
33}
34
35/// Generates constructors and query methods for a concrete backing type.
36macro_rules! impl_decimal_core {
37    ($backing:ty, $pow10_fn:path, $max_exp:expr) => {
38        impl<const D: u8> Decimal<$backing, D> {
39            /// The scale factor `10^DECIMALS`.
40            ///
41            /// Validated at compile time: panics if `DECIMALS` is too
42            /// large for the backing type.
43            pub const SCALE: $backing = {
44                assert!(
45                    (D as u32) <= $max_exp,
46                    "DECIMALS too large for backing type"
47                );
48                $pow10_fn(D)
49            };
50
51            /// The number of fractional digits.
52            pub const DECIMALS: u8 = D;
53
54            /// Creates a `Decimal` from a raw pre-scaled value.
55            ///
56            /// No validation — the caller is responsible for ensuring
57            /// the value is in the expected scale.
58            #[inline(always)]
59            pub const fn from_raw(value: $backing) -> Self {
60                Self { value }
61            }
62
63            /// Returns the raw internal value (scaled by `10^DECIMALS`).
64            #[inline(always)]
65            pub const fn to_raw(self) -> $backing {
66                self.value
67            }
68
69            /// Creates a `Decimal` from integer and fractional parts.
70            ///
71            /// The fractional part is combined with the integer part as
72            /// `integer * SCALE + fractional`. For conventional usage,
73            /// pass a non-negative `fractional` less than `SCALE`.
74            /// For negative values, negate the integer part:
75            /// `new(-123, 45_000_000)` → `-123.45` (for `DECIMALS=8`).
76            ///
77            /// # Panics
78            ///
79            /// Panics if the result overflows the backing type.
80            ///
81            /// # Examples
82            ///
83            /// ```
84            /// use nexus_decimal::Decimal;
85            /// type D64 = Decimal<i64, 8>;
86            ///
87            /// const PRICE: D64 = D64::new(100, 50_000_000); // 100.50
88            /// const NEG: D64 = D64::new(-50, 25_000_000);   // -50.25
89            /// ```
90            pub const fn new(integer: $backing, fractional: $backing) -> Self {
91                let Some(scaled) = integer.checked_mul(Self::SCALE) else {
92                    panic!("overflow in Decimal::new: integer part too large")
93                };
94
95                let value = if integer >= 0 {
96                    let Some(v) = scaled.checked_add(fractional) else {
97                        panic!("overflow in Decimal::new")
98                    };
99                    v
100                } else {
101                    let Some(v) = scaled.checked_sub(fractional) else {
102                        panic!("overflow in Decimal::new")
103                    };
104                    v
105                };
106
107                Self { value }
108            }
109
110            /// Construct from integer part, fractional part, and sign.
111            ///
112            /// The fractional part is always positive (represents digits
113            /// after the decimal point). Use `negative` to control sign.
114            ///
115            /// This handles the `-0.5` case that `new()` cannot express
116            /// (because `-0 == 0` for integers).
117            ///
118            /// # Examples
119            ///
120            /// ```
121            /// use nexus_decimal::Decimal;
122            /// type D64 = Decimal<i64, 8>;
123            ///
124            /// let neg_half = D64::from_parts(0, 50_000_000, true);
125            /// assert_eq!(neg_half.unwrap().to_raw(), -50_000_000);
126            ///
127            /// let pos = D64::from_parts(1, 25_000_000, false);
128            /// assert_eq!(pos.unwrap().to_raw(), 125_000_000);
129            /// ```
130            pub const fn from_parts(
131                integer: $backing,
132                fractional: $backing,
133                negative: bool,
134            ) -> Option<Self> {
135                let Some(scaled) = integer.checked_mul(Self::SCALE) else {
136                    return None;
137                };
138                let Some(abs) = scaled.checked_add(fractional) else {
139                    return None;
140                };
141                if negative {
142                    match abs.checked_neg() {
143                        Some(v) => Some(Self { value: v }),
144                        None => None,
145                    }
146                } else {
147                    Some(Self { value: abs })
148                }
149            }
150
151            /// Returns `true` if the value is zero.
152            #[inline(always)]
153            pub const fn is_zero(self) -> bool {
154                self.value == 0
155            }
156
157            /// Returns `true` if the value is strictly positive.
158            #[inline(always)]
159            pub const fn is_positive(self) -> bool {
160                self.value > 0
161            }
162
163            /// Returns `true` if the value is strictly negative.
164            #[inline(always)]
165            pub const fn is_negative(self) -> bool {
166                self.value < 0
167            }
168
169            /// Returns the signum: `-1`, `0`, or `1`.
170            #[inline(always)]
171            pub const fn signum(self) -> $backing {
172                self.value.signum()
173            }
174        }
175
176        impl<const D: u8> Default for Decimal<$backing, D> {
177            #[inline]
178            fn default() -> Self {
179                Self::ZERO
180            }
181        }
182    };
183}
184
185use crate::pow10::{pow10_i32, pow10_i64, pow10_i128};
186
187impl_decimal_core!(i32, pow10_i32, 9);
188impl_decimal_core!(i64, pow10_i64, 18);
189impl_decimal_core!(i128, pow10_i128, 38);