Skip to main content

nexus_decimal/
rounding.rs

1//! Rounding operations for `Decimal`.
2//!
3//! All methods are `const fn`, generated per backing type via macro.
4
5use crate::Decimal;
6
7macro_rules! impl_decimal_rounding {
8    ($backing:ty, $pow10_fn:path) => {
9        impl<const D: u8> Decimal<$backing, D> {
10            /// Rounds toward negative infinity.
11            ///
12            /// # Examples
13            ///
14            /// ```
15            /// use nexus_decimal::Decimal;
16            /// type D64 = Decimal<i64, 8>;
17            ///
18            /// let pos = D64::new(1, 75_000_000); // 1.75
19            /// assert_eq!(pos.floor().to_raw(), D64::new(1, 0).to_raw());
20            ///
21            /// let neg = D64::new(-1, 75_000_000); // -1.75
22            /// assert_eq!(neg.floor().to_raw(), D64::new(-2, 0).to_raw());
23            /// ```
24            #[inline(always)]
25            pub const fn floor(self) -> Self {
26                let remainder = self.value % Self::SCALE;
27                if remainder >= 0 {
28                    Self {
29                        value: self.value - remainder,
30                    }
31                } else {
32                    // self.value - remainder gives next integer toward zero.
33                    // Subtract SCALE to go one step negative. Saturate on underflow.
34                    let toward_zero = self.value - remainder;
35                    match toward_zero.checked_sub(Self::SCALE) {
36                        Some(v) => Self { value: v },
37                        None => Self::MIN,
38                    }
39                }
40            }
41
42            /// Rounds toward positive infinity.
43            ///
44            /// # Examples
45            ///
46            /// ```
47            /// use nexus_decimal::Decimal;
48            /// type D64 = Decimal<i64, 8>;
49            ///
50            /// let pos = D64::new(1, 25_000_000); // 1.25
51            /// assert_eq!(pos.ceil().to_raw(), D64::new(2, 0).to_raw());
52            ///
53            /// let neg = D64::new(-1, 25_000_000); // -1.25
54            /// assert_eq!(neg.ceil().to_raw(), D64::new(-1, 0).to_raw());
55            /// ```
56            #[inline(always)]
57            pub const fn ceil(self) -> Self {
58                let remainder = self.value % Self::SCALE;
59                if remainder > 0 {
60                    let toward_zero = self.value - remainder;
61                    match toward_zero.checked_add(Self::SCALE) {
62                        Some(v) => Self { value: v },
63                        None => Self::MAX,
64                    }
65                } else {
66                    Self {
67                        value: self.value - remainder,
68                    }
69                }
70            }
71
72            /// Truncates toward zero (removes fractional part).
73            ///
74            /// # Examples
75            ///
76            /// ```
77            /// use nexus_decimal::Decimal;
78            /// type D64 = Decimal<i64, 8>;
79            ///
80            /// let pos = D64::new(1, 99_000_000); // 1.99
81            /// assert_eq!(pos.trunc().to_raw(), D64::new(1, 0).to_raw());
82            ///
83            /// let neg = D64::new(-1, 99_000_000); // -1.99
84            /// assert_eq!(neg.trunc().to_raw(), D64::new(-1, 0).to_raw());
85            /// ```
86            #[inline(always)]
87            pub const fn trunc(self) -> Self {
88                Self {
89                    value: (self.value / Self::SCALE) * Self::SCALE,
90                }
91            }
92
93            /// Returns the fractional part (same sign as `self`).
94            ///
95            /// Invariant: `self == self.trunc() + self.fract()`.
96            #[inline(always)]
97            pub const fn fract(self) -> Self {
98                Self {
99                    value: self.value % Self::SCALE,
100                }
101            }
102
103            /// Returns the integer part as the backing type.
104            ///
105            /// Equivalent to `self.trunc().to_raw() / SCALE`.
106            #[inline(always)]
107            pub const fn to_integer(self) -> $backing {
108                self.value / Self::SCALE
109            }
110
111            /// Rounds to the nearest integer using banker's rounding
112            /// (round half to even).
113            ///
114            /// # Examples
115            ///
116            /// ```
117            /// use nexus_decimal::Decimal;
118            /// type D64 = Decimal<i64, 8>;
119            ///
120            /// // Half rounds to even
121            /// let half_even = D64::new(2, 50_000_000); // 2.5
122            /// assert_eq!(half_even.round().to_raw(), D64::new(2, 0).to_raw());
123            ///
124            /// let half_odd = D64::new(3, 50_000_000); // 3.5
125            /// assert_eq!(half_odd.round().to_raw(), D64::new(4, 0).to_raw());
126            /// ```
127            #[inline(always)]
128            pub const fn round(self) -> Self {
129                let quotient = self.value / Self::SCALE;
130                let remainder = self.value % Self::SCALE;
131                let half = Self::SCALE / 2;
132
133                let rounded = if remainder > half {
134                    quotient + 1
135                } else if remainder < -half {
136                    quotient - 1
137                } else if remainder == half {
138                    // Banker's rounding: round to even
139                    if quotient & 1 != 0 {
140                        quotient + 1
141                    } else {
142                        quotient
143                    }
144                } else if remainder == -half {
145                    if quotient & 1 != 0 {
146                        quotient - 1
147                    } else {
148                        quotient
149                    }
150                } else {
151                    quotient
152                };
153
154                match rounded.checked_mul(Self::SCALE) {
155                    Some(v) => Self { value: v },
156                    None => {
157                        if rounded > 0 {
158                            Self::MAX
159                        } else {
160                            Self::MIN
161                        }
162                    }
163                }
164            }
165
166            /// Rounds to `dp` decimal places using banker's rounding.
167            ///
168            /// # Panics
169            ///
170            /// Panics if `dp >= DECIMALS`.
171            ///
172            /// # Examples
173            ///
174            /// ```
175            /// use nexus_decimal::Decimal;
176            /// type D64 = Decimal<i64, 8>;
177            ///
178            /// let price = D64::new(1, 23_456_789); // 1.23456789
179            /// let rounded = price.round_dp(2);       // 1.23
180            /// assert_eq!(rounded.to_raw(), D64::new(1, 23_000_000).to_raw());
181            /// ```
182            #[inline]
183            pub const fn round_dp(self, dp: u8) -> Self {
184                assert!(dp < D, "round_dp: dp must be less than DECIMALS");
185
186                let sub_scale = $pow10_fn(D - dp);
187                let half = sub_scale / 2;
188                let quotient = self.value / sub_scale;
189                let remainder = self.value % sub_scale;
190
191                let rounded = if remainder > half {
192                    quotient + 1
193                } else if remainder < -half {
194                    quotient - 1
195                } else if remainder == half {
196                    if quotient & 1 != 0 {
197                        quotient + 1
198                    } else {
199                        quotient
200                    }
201                } else if remainder == -half {
202                    if quotient & 1 != 0 {
203                        quotient - 1
204                    } else {
205                        quotient
206                    }
207                } else {
208                    quotient
209                };
210
211                match rounded.checked_mul(sub_scale) {
212                    Some(v) => Self { value: v },
213                    None => {
214                        if rounded > 0 {
215                            Self::MAX
216                        } else {
217                            Self::MIN
218                        }
219                    }
220                }
221            }
222        }
223    };
224}
225
226use crate::pow10::{pow10_i32, pow10_i64, pow10_i128};
227
228impl_decimal_rounding!(i32, pow10_i32);
229impl_decimal_rounding!(i64, pow10_i64);
230impl_decimal_rounding!(i128, pow10_i128);