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);