decimal_scaled/rounding.rs
1//! Rounding-mode selector for scale-narrowing operations.
2//!
3//! Passed to every `*_with(mode)` sibling on every decimal width —
4//! [`crate::D38::rescale_with`], `mul_with`, `div_with`, `to_int_with`,
5//! `from_f64_with`, every `*_strict_with` on the wide tier, etc. — to
6//! control how fractional digits are discarded when the result has
7//! lower precision than the working intermediate. The six modes cover
8//! IEEE-754's five rounding rules (`HalfToEven`, `HalfTowardZero`,
9//! `Trunc`, `Floor`, `Ceiling`) plus the commercial `HalfAwayFromZero`
10//! rule expected by users coming from `bigdecimal` / `rust_decimal`.
11//!
12//! The default mode is `HalfToEven` (IEEE-754 default; no systematic
13//! bias). The `rounding-*` Cargo features let a downstream crate flip
14//! the crate-wide default at compile time.
15
16/// Selector for the rounding rule applied when a scale-narrowing
17/// operation discards fractional digits.
18///
19/// See the module-level documentation for when each rule applies.
20///
21/// # Precision
22///
23/// N/A: this is a tag; no arithmetic is performed by constructing
24/// or comparing variants.
25#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
26pub enum RoundingMode {
27 /// Round to nearest; on ties, round to the even neighbour.
28 /// IEEE-754 `roundTiesToEven`; also called banker's rounding.
29 /// Unbiased — repeated rounding does not drift sums. Crate default.
30 ///
31 /// Examples (truncate to integer): `0.5 -> 0`, `1.5 -> 2`,
32 /// `2.5 -> 2`, `-0.5 -> 0`, `-1.5 -> -2`.
33 HalfToEven,
34 /// Round to nearest; on ties, round away from zero. Commercial
35 /// rounding. Mildly biased in magnitude.
36 ///
37 /// Examples: `0.5 -> 1`, `1.5 -> 2`, `-0.5 -> -1`, `-1.5 -> -2`.
38 HalfAwayFromZero,
39 /// Round to nearest; on ties, round toward zero. Mildly biased
40 /// toward zero. Rare in practice; included for completeness.
41 ///
42 /// Examples: `0.5 -> 0`, `1.5 -> 1`, `-0.5 -> 0`, `-1.5 -> -1`.
43 HalfTowardZero,
44 /// Truncate toward zero. Discards the fractional part. Cheapest
45 /// in integer arithmetic; matches Rust's `as` cast for integer
46 /// narrowing.
47 ///
48 /// Examples: `0.7 -> 0`, `-0.7 -> 0`, `1.9 -> 1`, `-1.9 -> -1`.
49 Trunc,
50 /// Round toward negative infinity (floor).
51 ///
52 /// Examples: `0.7 -> 0`, `-0.7 -> -1`, `1.9 -> 1`, `-1.9 -> -2`.
53 Floor,
54 /// Round toward positive infinity (ceiling).
55 ///
56 /// Examples: `0.7 -> 1`, `-0.7 -> 0`, `1.9 -> 2`, `-1.9 -> -1`.
57 Ceiling,
58}
59
60/// Compile-time default `RoundingMode` for the no-arg `rescale` and
61/// future default-rounding methods.
62///
63/// Selected by Cargo feature flags (priority order: first match wins):
64/// 1. `rounding-half-away-from-zero` → `HalfAwayFromZero`
65/// 2. `rounding-half-toward-zero` → `HalfTowardZero`
66/// 3. `rounding-trunc` → `Trunc`
67/// 4. `rounding-floor` → `Floor`
68/// 5. `rounding-ceiling` → `Ceiling`
69/// 6. (none) → `HalfToEven` (IEEE-754 default; banker's rounding)
70#[cfg(feature = "rounding-half-away-from-zero")]
71pub const DEFAULT_ROUNDING_MODE: RoundingMode = RoundingMode::HalfAwayFromZero;
72
73#[cfg(all(
74 not(feature = "rounding-half-away-from-zero"),
75 feature = "rounding-half-toward-zero"
76))]
77pub const DEFAULT_ROUNDING_MODE: RoundingMode = RoundingMode::HalfTowardZero;
78
79#[cfg(all(
80 not(feature = "rounding-half-away-from-zero"),
81 not(feature = "rounding-half-toward-zero"),
82 feature = "rounding-trunc"
83))]
84pub const DEFAULT_ROUNDING_MODE: RoundingMode = RoundingMode::Trunc;
85
86#[cfg(all(
87 not(feature = "rounding-half-away-from-zero"),
88 not(feature = "rounding-half-toward-zero"),
89 not(feature = "rounding-trunc"),
90 feature = "rounding-floor"
91))]
92pub const DEFAULT_ROUNDING_MODE: RoundingMode = RoundingMode::Floor;
93
94#[cfg(all(
95 not(feature = "rounding-half-away-from-zero"),
96 not(feature = "rounding-half-toward-zero"),
97 not(feature = "rounding-trunc"),
98 not(feature = "rounding-floor"),
99 feature = "rounding-ceiling"
100))]
101pub const DEFAULT_ROUNDING_MODE: RoundingMode = RoundingMode::Ceiling;
102
103#[cfg(not(any(
104 feature = "rounding-half-away-from-zero",
105 feature = "rounding-half-toward-zero",
106 feature = "rounding-trunc",
107 feature = "rounding-floor",
108 feature = "rounding-ceiling",
109)))]
110pub const DEFAULT_ROUNDING_MODE: RoundingMode = RoundingMode::HalfToEven;
111
112/// Strategy hook for the rounding-mode family.
113///
114/// Given a *truncated-toward-zero* quotient and the per-operation
115/// numerator / divisor context, returns `true` if the quotient should
116/// be bumped one step "away from zero" in the result's direction to
117/// satisfy this mode. Caller is responsible for the actual bump (it
118/// is `q + 1` when the result is positive, `q − 1` when negative).
119///
120/// The three inputs collapse the per-step numerics that every mode
121/// cares about into mode-independent booleans / orderings:
122///
123/// - `cmp_r` — three-way comparison of `|r|` against `|m| − |r|`. This
124/// is exactly the round-up condition (`|r| > |m| − |r|` ⇔ `2·|r| > |m|`)
125/// without the doubling-overflow risk. `Equal` flags the half-way tie,
126/// which only occurs when the divisor is even.
127/// - `q_is_odd` — parity of the truncated quotient. Drives the
128/// half-to-even tie break.
129/// - `result_positive` — sign of the true result (`sign(n) == sign(m)`).
130/// Drives `Floor` / `Ceiling`.
131///
132/// Caller pre-handles the `r == 0` case (no rounding needed).
133///
134/// `#[inline(always)]` because the entire body is one match on a
135/// 6-variant enum. The hot operator path instantiates this with a
136/// const `mode` (`DEFAULT_ROUNDING_MODE`), so const-propagation can
137/// collapse the match away once inlined.
138#[inline(always)]
139pub(crate) fn should_bump(
140 mode: RoundingMode,
141 cmp_r: ::core::cmp::Ordering,
142 q_is_odd: bool,
143 result_positive: bool,
144) -> bool {
145 use ::core::cmp::Ordering;
146 match mode {
147 RoundingMode::HalfToEven => match cmp_r {
148 Ordering::Less => false,
149 Ordering::Greater => true,
150 Ordering::Equal => q_is_odd,
151 },
152 RoundingMode::HalfAwayFromZero => !matches!(cmp_r, Ordering::Less),
153 RoundingMode::HalfTowardZero => matches!(cmp_r, Ordering::Greater),
154 RoundingMode::Trunc => false,
155 RoundingMode::Floor => !result_positive,
156 RoundingMode::Ceiling => result_positive,
157 }
158}
159
160/// Applies `mode` to integer division `raw / divisor`, returning the
161/// rounded quotient.
162///
163/// Used by `D38::rescale_with` and by the multiplier-and-divide
164/// fast paths in `mg_divide`. The whole mode-specific logic is
165/// delegated to [`should_bump`]; this function is just the i128
166/// arithmetic wrapper that builds its inputs and applies the bump.
167#[inline(always)]
168pub(crate) fn apply_rounding(raw: i128, divisor: i128, mode: RoundingMode) -> i128 {
169 let quotient = raw / divisor;
170 let remainder = raw % divisor;
171
172 if remainder == 0 {
173 return quotient;
174 }
175
176 let abs_rem = remainder.unsigned_abs();
177 let abs_div = divisor.unsigned_abs();
178 let comp = abs_div - abs_rem;
179 let cmp_r = abs_rem.cmp(&comp);
180 let q_is_odd = (quotient & 1) != 0;
181 let result_positive = (raw < 0) == (divisor < 0);
182
183 if should_bump(mode, cmp_r, q_is_odd, result_positive) {
184 if result_positive { quotient + 1 } else { quotient - 1 }
185 } else {
186 quotient
187 }
188}
189
190/// `true` when the crate is built with [`DEFAULT_ROUNDING_MODE`] set to
191/// [`RoundingMode::HalfToEven`] — i.e. none of the `rounding-*` feature
192/// flags is selected. Used by tests whose expected values assume the
193/// default IEEE-754 rounding to short-circuit themselves under a
194/// non-default rounding feature build.
195#[cfg(test)]
196pub(crate) const DEFAULT_IS_HALF_TO_EVEN: bool = matches!(
197 DEFAULT_ROUNDING_MODE,
198 RoundingMode::HalfToEven
199);
200
201#[cfg(test)]
202mod tests {
203 use super::*;
204
205 fn modes() -> [RoundingMode; 6] {
206 [
207 RoundingMode::HalfToEven,
208 RoundingMode::HalfAwayFromZero,
209 RoundingMode::HalfTowardZero,
210 RoundingMode::Trunc,
211 RoundingMode::Floor,
212 RoundingMode::Ceiling,
213 ]
214 }
215
216 /// Zero remainder is exact for every mode.
217 #[test]
218 fn zero_remainder_is_quotient_for_all_modes() {
219 for m in modes() {
220 assert_eq!(apply_rounding(20, 10, m), 2, "{m:?}");
221 assert_eq!(apply_rounding(-20, 10, m), -2, "{m:?}");
222 assert_eq!(apply_rounding(0, 10, m), 0, "{m:?}");
223 }
224 }
225
226 /// Half-to-even: ties go to even neighbour.
227 #[test]
228 fn half_to_even_ties() {
229 let m = RoundingMode::HalfToEven;
230 assert_eq!(apply_rounding(5, 10, m), 0); // 0.5 -> 0 (even)
231 assert_eq!(apply_rounding(15, 10, m), 2); // 1.5 -> 2
232 assert_eq!(apply_rounding(25, 10, m), 2); // 2.5 -> 2 (even)
233 assert_eq!(apply_rounding(35, 10, m), 4); // 3.5 -> 4
234 assert_eq!(apply_rounding(-5, 10, m), 0); // -0.5 -> 0
235 assert_eq!(apply_rounding(-15, 10, m), -2); // -1.5 -> -2
236 assert_eq!(apply_rounding(-25, 10, m), -2); // -2.5 -> -2
237 assert_eq!(apply_rounding(-35, 10, m), -4); // -3.5 -> -4
238 }
239
240 /// Half-away-from-zero: ties go away from zero.
241 #[test]
242 fn half_away_from_zero_ties() {
243 let m = RoundingMode::HalfAwayFromZero;
244 assert_eq!(apply_rounding(5, 10, m), 1);
245 assert_eq!(apply_rounding(15, 10, m), 2);
246 assert_eq!(apply_rounding(25, 10, m), 3);
247 assert_eq!(apply_rounding(-5, 10, m), -1);
248 assert_eq!(apply_rounding(-15, 10, m), -2);
249 assert_eq!(apply_rounding(-25, 10, m), -3);
250 }
251
252 /// Half-toward-zero: ties go toward zero.
253 #[test]
254 fn half_toward_zero_ties() {
255 let m = RoundingMode::HalfTowardZero;
256 assert_eq!(apply_rounding(5, 10, m), 0);
257 assert_eq!(apply_rounding(15, 10, m), 1);
258 assert_eq!(apply_rounding(25, 10, m), 2);
259 assert_eq!(apply_rounding(-5, 10, m), 0);
260 assert_eq!(apply_rounding(-15, 10, m), -1);
261 assert_eq!(apply_rounding(-25, 10, m), -2);
262 }
263
264 /// Trunc: always toward zero, regardless of magnitude.
265 #[test]
266 fn trunc_always_toward_zero() {
267 let m = RoundingMode::Trunc;
268 assert_eq!(apply_rounding(7, 10, m), 0);
269 assert_eq!(apply_rounding(9, 10, m), 0);
270 assert_eq!(apply_rounding(19, 10, m), 1);
271 assert_eq!(apply_rounding(-7, 10, m), 0);
272 assert_eq!(apply_rounding(-19, 10, m), -1);
273 }
274
275 /// Floor: always toward negative infinity.
276 #[test]
277 fn floor_toward_negative_infinity() {
278 let m = RoundingMode::Floor;
279 assert_eq!(apply_rounding(1, 10, m), 0);
280 assert_eq!(apply_rounding(7, 10, m), 0);
281 assert_eq!(apply_rounding(9, 10, m), 0);
282 assert_eq!(apply_rounding(-1, 10, m), -1);
283 assert_eq!(apply_rounding(-7, 10, m), -1);
284 assert_eq!(apply_rounding(-19, 10, m), -2);
285 }
286
287 /// Ceiling: always toward positive infinity.
288 #[test]
289 fn ceiling_toward_positive_infinity() {
290 let m = RoundingMode::Ceiling;
291 assert_eq!(apply_rounding(1, 10, m), 1);
292 assert_eq!(apply_rounding(7, 10, m), 1);
293 assert_eq!(apply_rounding(19, 10, m), 2);
294 assert_eq!(apply_rounding(-1, 10, m), 0);
295 assert_eq!(apply_rounding(-7, 10, m), 0);
296 assert_eq!(apply_rounding(-19, 10, m), -1);
297 }
298
299 /// Non-half values go to the nearest neighbour for every "half"
300 /// mode and ignore the half-tie rule.
301 #[test]
302 fn non_half_goes_to_nearest() {
303 for m in [
304 RoundingMode::HalfToEven,
305 RoundingMode::HalfAwayFromZero,
306 RoundingMode::HalfTowardZero,
307 ] {
308 assert_eq!(apply_rounding(4, 10, m), 0, "{m:?} 0.4");
309 assert_eq!(apply_rounding(6, 10, m), 1, "{m:?} 0.6");
310 assert_eq!(apply_rounding(-4, 10, m), 0, "{m:?} -0.4");
311 assert_eq!(apply_rounding(-6, 10, m), -1, "{m:?} -0.6");
312 }
313 }
314}
315