Skip to main content

decimal_scaled/support/
rounding.rs

1// SPDX-FileCopyrightText: 2026 John Moxley
2// SPDX-License-Identifier: MIT OR Apache-2.0
3
4//! Rounding-mode selector for scale-narrowing operations.
5//!
6//! Passed to every `*_with(mode)` sibling on every decimal width —
7//! [`crate::D38::rescale_with`], `mul_with`, `div_with`, `to_int_with`,
8//! `from_f64_with`, every `*_strict_with` on the wide tier, etc. — to
9//! control how fractional digits are discarded when the result has
10//! lower precision than the working intermediate. The six modes cover
11//! IEEE-754's five rounding rules (`HalfToEven`, `HalfTowardZero`,
12//! `Trunc`, `Floor`, `Ceiling`) plus the commercial `HalfAwayFromZero`
13//! rule expected by users coming from `bigdecimal` / `rust_decimal`.
14//!
15//! The default mode is `HalfToEven` (IEEE-754 default; no systematic
16//! bias). The `rounding-*` Cargo features let a downstream crate flip
17//! the crate-wide default at compile time.
18
19/// Selector for the rounding rule applied when a scale-narrowing
20/// operation discards fractional digits.
21///
22/// See the module-level documentation for when each rule applies.
23///
24/// # Precision
25///
26/// N/A: this is a tag; no arithmetic is performed by constructing
27/// or comparing variants.
28#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
29pub enum RoundingMode {
30    /// Round to nearest; on ties, round to the even neighbour.
31    /// IEEE-754 `roundTiesToEven`; also called banker's rounding.
32    /// Unbiased — repeated rounding does not drift sums. Crate default.
33    ///
34    /// Examples (truncate to integer): `0.5 -> 0`, `1.5 -> 2`,
35    /// `2.5 -> 2`, `-0.5 -> 0`, `-1.5 -> -2`.
36    HalfToEven,
37    /// Round to nearest; on ties, round away from zero. Commercial
38    /// rounding. Mildly biased in magnitude.
39    ///
40    /// Examples: `0.5 -> 1`, `1.5 -> 2`, `-0.5 -> -1`, `-1.5 -> -2`.
41    HalfAwayFromZero,
42    /// Round to nearest; on ties, round toward zero. Mildly biased
43    /// toward zero. Rare in practice; included for completeness.
44    ///
45    /// Examples: `0.5 -> 0`, `1.5 -> 1`, `-0.5 -> 0`, `-1.5 -> -1`.
46    HalfTowardZero,
47    /// Truncate toward zero. Discards the fractional part. Cheapest
48    /// in integer arithmetic; matches Rust's `as` cast for integer
49    /// narrowing.
50    ///
51    /// Examples: `0.7 -> 0`, `-0.7 -> 0`, `1.9 -> 1`, `-1.9 -> -1`.
52    Trunc,
53    /// Round toward negative infinity (floor).
54    ///
55    /// Examples: `0.7 -> 0`, `-0.7 -> -1`, `1.9 -> 1`, `-1.9 -> -2`.
56    Floor,
57    /// Round toward positive infinity (ceiling).
58    ///
59    /// Examples: `0.7 -> 1`, `-0.7 -> 0`, `1.9 -> 2`, `-1.9 -> -1`.
60    Ceiling,
61}
62
63/// Compile-time default `RoundingMode` for the no-arg `rescale` and
64/// future default-rounding methods.
65///
66/// Selected by Cargo feature flags (priority order: first match wins):
67/// 1. `rounding-half-away-from-zero` → `HalfAwayFromZero`
68/// 2. `rounding-half-toward-zero` → `HalfTowardZero`
69/// 3. `rounding-trunc` → `Trunc`
70/// 4. `rounding-floor` → `Floor`
71/// 5. `rounding-ceiling` → `Ceiling`
72/// 6. (none) → `HalfToEven` (IEEE-754 default; banker's rounding)
73#[cfg(feature = "rounding-half-away-from-zero")]
74pub const DEFAULT_ROUNDING_MODE: RoundingMode = RoundingMode::HalfAwayFromZero;
75
76#[cfg(all(
77    not(feature = "rounding-half-away-from-zero"),
78    feature = "rounding-half-toward-zero"
79))]
80pub const DEFAULT_ROUNDING_MODE: RoundingMode = RoundingMode::HalfTowardZero;
81
82#[cfg(all(
83    not(feature = "rounding-half-away-from-zero"),
84    not(feature = "rounding-half-toward-zero"),
85    feature = "rounding-trunc"
86))]
87pub const DEFAULT_ROUNDING_MODE: RoundingMode = RoundingMode::Trunc;
88
89#[cfg(all(
90    not(feature = "rounding-half-away-from-zero"),
91    not(feature = "rounding-half-toward-zero"),
92    not(feature = "rounding-trunc"),
93    feature = "rounding-floor"
94))]
95pub const DEFAULT_ROUNDING_MODE: RoundingMode = RoundingMode::Floor;
96
97#[cfg(all(
98    not(feature = "rounding-half-away-from-zero"),
99    not(feature = "rounding-half-toward-zero"),
100    not(feature = "rounding-trunc"),
101    not(feature = "rounding-floor"),
102    feature = "rounding-ceiling"
103))]
104pub const DEFAULT_ROUNDING_MODE: RoundingMode = RoundingMode::Ceiling;
105
106#[cfg(not(any(
107    feature = "rounding-half-away-from-zero",
108    feature = "rounding-half-toward-zero",
109    feature = "rounding-trunc",
110    feature = "rounding-floor",
111    feature = "rounding-ceiling",
112)))]
113pub const DEFAULT_ROUNDING_MODE: RoundingMode = RoundingMode::HalfToEven;
114
115/// Strategy hook for the rounding-mode family.
116///
117/// Given a *truncated-toward-zero* quotient and the per-operation
118/// numerator / divisor context, returns `true` if the quotient should
119/// be bumped one step "away from zero" in the result's direction to
120/// satisfy this mode. Caller is responsible for the actual bump (it
121/// is `q + 1` when the result is positive, `q − 1` when negative).
122///
123/// The three inputs collapse the per-step numerics that every mode
124/// cares about into mode-independent booleans / orderings:
125///
126/// - `cmp_r` — three-way comparison of `|r|` against `|m| − |r|`. This
127///   is exactly the round-up condition (`|r| > |m| − |r|` ⇔ `2·|r| > |m|`)
128///   without the doubling-overflow risk. `Equal` flags the half-way tie,
129///   which only occurs when the divisor is even.
130/// - `q_is_odd` — parity of the truncated quotient. Drives the
131///   half-to-even tie break.
132/// - `result_positive` — sign of the true result (`sign(n) == sign(m)`).
133///   Drives `Floor` / `Ceiling`.
134///
135/// Caller pre-handles the `r == 0` case (no rounding needed).
136///
137/// `#[inline(always)]` because the entire body is one match on a
138/// 6-variant enum. The hot operator path instantiates this with a
139/// const `mode` (`DEFAULT_ROUNDING_MODE`), so const-propagation can
140/// collapse the match away once inlined.
141#[inline(always)]
142pub(crate) fn should_bump(
143    mode: RoundingMode,
144    cmp_r: ::core::cmp::Ordering,
145    q_is_odd: bool,
146    result_positive: bool,
147) -> bool {
148    use ::core::cmp::Ordering;
149    match mode {
150        RoundingMode::HalfToEven => match cmp_r {
151            Ordering::Less => false,
152            Ordering::Greater => true,
153            Ordering::Equal => q_is_odd,
154        },
155        RoundingMode::HalfAwayFromZero => !matches!(cmp_r, Ordering::Less),
156        RoundingMode::HalfTowardZero => matches!(cmp_r, Ordering::Greater),
157        RoundingMode::Trunc => false,
158        RoundingMode::Floor => !result_positive,
159        RoundingMode::Ceiling => result_positive,
160    }
161}
162
163/// `true` for the three round-to-nearest modes (`HalfToEven`,
164/// `HalfAwayFromZero`, `HalfTowardZero`), `false` for the directed
165/// modes (`Trunc`, `Floor`, `Ceiling`).
166///
167/// Kernels with a sub-LSB linear-approximation fast path (e.g.
168/// `ln(1 + δ)` near `δ`, `exp(δ)` near `1 + δ`) may short-circuit only
169/// under nearest rounding: those approximations land within half an LSB
170/// of the true value, which is exactly what nearest rounding needs but
171/// not enough for a directed mode, whose answer depends on which side of
172/// the boundary the true value falls. Directed modes must fall through
173/// to the full working-scale evaluation so the residual sign is known.
174#[inline(always)]
175pub(crate) const fn is_nearest_mode(mode: RoundingMode) -> bool {
176    matches!(
177        mode,
178        RoundingMode::HalfToEven | RoundingMode::HalfAwayFromZero | RoundingMode::HalfTowardZero
179    )
180}
181
182/// Correctly-rounded result of an odd, strictly-compressing function
183/// (`tanh`) at a tiny argument, for any rounding mode.
184///
185/// For `tanh` the Maclaurin series is `tanh(x) = x − x³/3 + …`, an
186/// alternating series in odd powers of `x`. Within the small-argument
187/// linear band the cubic correction `|x|³/3` is below one storage ULP
188/// yet strictly positive, so the true value `t = tanh(x)·10^SCALE`
189/// satisfies, for `raw = x·10^SCALE`:
190///
191/// ```text
192///   raw > 0 :  raw − 1 < t < raw          (just below the grid line raw)
193///   raw < 0 :  raw     < t < raw + 1      (just above the grid line raw)
194/// ```
195///
196/// i.e. `|t|` lies strictly inside `(|raw| − 1, |raw|)`. The result is
197/// therefore exactly determined by integer arithmetic on `raw` — no
198/// finite-precision kernel can resolve the sub-ULP cubic, so the
199/// directed modes must use this analytic decision rather than rounding
200/// the (grid-exact) linear approximation. The three nearest modes round
201/// to `raw` (the cubic is well under half a ULP in the band).
202///
203/// `one` is the storage value `1`; `zero` the storage value `0`. The
204/// caller guarantees `0 < |raw| <= threshold`, the band where the cubic
205/// stays under one ULP.
206#[inline]
207pub(crate) fn tiny_odd_compressing_directed<T>(raw: T, zero: T, one: T, mode: RoundingMode) -> T
208where
209    T: Copy + PartialOrd + ::core::ops::Add<Output = T> + ::core::ops::Sub<Output = T>,
210{
211    if is_nearest_mode(mode) {
212        return raw;
213    }
214    let positive = raw > zero;
215    match mode {
216        // Toward zero: drop the sub-ULP magnitude, landing on |raw| − 1.
217        RoundingMode::Trunc => {
218            if positive {
219                raw - one
220            } else {
221                raw + one
222            }
223        }
224        // Toward −∞.
225        RoundingMode::Floor => {
226            if positive {
227                raw - one
228            } else {
229                raw
230            }
231        }
232        // Toward +∞.
233        RoundingMode::Ceiling => {
234            if positive {
235                raw
236            } else {
237                raw + one
238            }
239        }
240        // Nearest modes handled above.
241        _ => raw,
242    }
243}
244
245/// Directed rounding for an odd transcendental whose true value at a
246/// tiny argument sits just *above* the grid line `raw` in magnitude —
247/// e.g. `sinh(x) = x + x³/6 + …`, where the cubic is strictly positive
248/// but below one ULP. The mirror of [`tiny_odd_compressing_directed`]
249/// (which handles the just-*below* case like `tanh`).
250///
251/// `raw` is the stored argument (= the leading term `x · 10^SCALE`),
252/// `zero`/`one` the type's storage `0` / `1`. The true value lies in
253/// `(|raw|, |raw| + 1)` in magnitude, so:
254///
255/// - nearest modes round to `raw` (the excess is < 0.5 ULP);
256/// - toward-zero (`Trunc`) drops the excess → `raw`;
257/// - `Floor` (toward −∞): `raw` if positive, `raw − 1` if negative;
258/// - `Ceiling` (toward +∞): `raw + 1` if positive, `raw` if negative.
259#[inline]
260pub(crate) fn tiny_odd_expanding_directed<T>(raw: T, zero: T, one: T, mode: RoundingMode) -> T
261where
262    T: Copy + PartialOrd + ::core::ops::Add<Output = T> + ::core::ops::Sub<Output = T>,
263{
264    if is_nearest_mode(mode) {
265        return raw;
266    }
267    let positive = raw > zero;
268    match mode {
269        // Toward zero: the excess is sub-ULP, so the magnitude stays at
270        // `|raw|` — i.e. `raw` unchanged.
271        RoundingMode::Trunc => raw,
272        // Toward −∞.
273        RoundingMode::Floor => {
274            if positive {
275                raw
276            } else {
277                raw - one
278            }
279        }
280        // Toward +∞.
281        RoundingMode::Ceiling => {
282            if positive {
283                raw + one
284            } else {
285                raw
286            }
287        }
288        // Nearest modes handled above.
289        _ => raw,
290    }
291}
292
293/// Applies `mode` to integer division `raw / divisor`, returning the
294/// rounded quotient.
295///
296/// Used by `D38::rescale_with` and by the multiplier-and-divide
297/// fast paths in `mg_divide`. The whole mode-specific logic is
298/// delegated to [`should_bump`]; this function is just the i128
299/// arithmetic wrapper that builds its inputs and applies the bump.
300#[inline(always)]
301pub(crate) fn apply_rounding(raw: i128, divisor: i128, mode: RoundingMode) -> i128 {
302    let quotient = raw / divisor;
303    let remainder = raw % divisor;
304
305    if remainder == 0 {
306        return quotient;
307    }
308
309    let abs_rem = remainder.unsigned_abs();
310    let abs_div = divisor.unsigned_abs();
311    let comp = abs_div - abs_rem;
312    let cmp_r = abs_rem.cmp(&comp);
313    let q_is_odd = (quotient & 1) != 0;
314    let result_positive = (raw < 0) == (divisor < 0);
315
316    if should_bump(mode, cmp_r, q_is_odd, result_positive) {
317        if result_positive {
318            quotient + 1
319        } else {
320            quotient - 1
321        }
322    } else {
323        quotient
324    }
325}
326
327/// `2^52` — the threshold at or above which every finite `f64` is
328/// already an exact integer (the mantissa can no longer represent a
329/// fractional bit). Used by the libm-free `f64` rounding helpers to
330/// short-circuit large magnitudes, which also keeps the `as i128`
331/// truncation inside `i128` range (`2^52 < i128::MAX`).
332const F64_INTEGER_THRESHOLD: f64 = 9_007_199_254_740_992.0_f64;
333
334/// Truncate an `f64` toward zero, libm-free.
335///
336/// Equivalent to [`f64::trunc`] but built from arithmetic and `as`
337/// casts only, so it is available in `no_std` without `libm`. For
338/// magnitudes at or above `2^52` (already integral) and for non-finite
339/// inputs the value is returned unchanged; otherwise the integral part
340/// is recovered via an `i128` round-trip, which is exact in that range.
341/// The negative-zero sign is preserved to match [`f64::trunc`] bit-for-bit.
342#[inline]
343pub(crate) fn trunc_f64(x: f64) -> f64 {
344    if x.is_nan() {
345        return x;
346    }
347    let magnitude = if x < 0.0 { -x } else { x };
348    if magnitude >= F64_INTEGER_THRESHOLD {
349        // NaN is already returned above, so `>=` is the exact complement of
350        // `< THRESHOLD` here: already-integral / too-large magnitudes pass
351        // through unchanged.
352        return x;
353    }
354    let truncated = x as i128 as f64;
355    if truncated == 0.0 && x.is_sign_negative() {
356        -0.0
357    } else {
358        truncated
359    }
360}
361
362/// Round an `f64` toward negative infinity, libm-free. Equivalent to
363/// [`f64::floor`]: drop to the truncated value, then step down by one
364/// when truncation rounded a negative value up toward zero.
365#[inline]
366pub(crate) fn floor_f64(x: f64) -> f64 {
367    let truncated = trunc_f64(x);
368    if truncated > x {
369        truncated - 1.0
370    } else {
371        truncated
372    }
373}
374
375/// Round an `f64` toward positive infinity, libm-free. Equivalent to
376/// [`f64::ceil`]: the mirror of [`floor_f64`].
377#[inline]
378pub(crate) fn ceil_f64(x: f64) -> f64 {
379    let truncated = trunc_f64(x);
380    if truncated < x {
381        truncated + 1.0
382    } else {
383        truncated
384    }
385}
386
387/// Round an `f64` to the nearest integer, ties away from zero, libm-free.
388/// Equivalent to [`f64::round`]: a fractional part with magnitude `>= 0.5`
389/// steps the truncated value one away from zero.
390#[inline]
391pub(crate) fn round_half_away_f64(x: f64) -> f64 {
392    let truncated = trunc_f64(x);
393    let fraction = x - truncated;
394    if fraction >= 0.5 {
395        truncated + 1.0
396    } else if fraction <= -0.5 {
397        truncated - 1.0
398    } else {
399        truncated
400    }
401}
402
403/// Round an `f64` to the nearest integer, ties to even, libm-free.
404/// Equivalent to [`f64::round_ties_even`]: a fractional part strictly
405/// past `0.5` in magnitude steps one away from zero; an exact half steps
406/// only when the truncated value is odd, landing on the even neighbour.
407#[inline]
408pub(crate) fn round_half_even_f64(x: f64) -> f64 {
409    let truncated = trunc_f64(x);
410    let fraction = x - truncated;
411    if fraction > 0.5 {
412        truncated + 1.0
413    } else if fraction < -0.5 {
414        truncated - 1.0
415    } else if fraction == 0.5 {
416        if (truncated as i128) & 1 == 0 {
417            truncated
418        } else {
419            truncated + 1.0
420        }
421    } else if fraction == -0.5 {
422        if (truncated as i128) & 1 == 0 {
423            truncated
424        } else {
425            truncated - 1.0
426        }
427    } else {
428        truncated
429    }
430}
431
432/// Round an `f64` to the nearest integer, ties toward zero, libm-free.
433/// Reproduces the previous `std` formulation
434/// (`(x - 0.5).ceil()` for `x >= 0`, `(x + 0.5).floor()` otherwise)
435/// using the libm-free [`ceil_f64`] / [`floor_f64`].
436#[inline]
437pub(crate) fn round_half_toward_zero_f64(x: f64) -> f64 {
438    if x >= 0.0 {
439        ceil_f64(x - 0.5)
440    } else {
441        floor_f64(x + 0.5)
442    }
443}
444
445/// `true` when the crate is built with [`DEFAULT_ROUNDING_MODE`] set to
446/// [`RoundingMode::HalfToEven`] — i.e. none of the `rounding-*` feature
447/// flags is selected. Used by tests whose expected values assume the
448/// default IEEE-754 rounding to short-circuit themselves under a
449/// non-default rounding feature build.
450#[cfg(test)]
451pub(crate) const DEFAULT_IS_HALF_TO_EVEN: bool =
452    matches!(DEFAULT_ROUNDING_MODE, RoundingMode::HalfToEven);
453
454#[cfg(test)]
455mod tests {
456    use super::*;
457
458    fn modes() -> [RoundingMode; 6] {
459        [
460            RoundingMode::HalfToEven,
461            RoundingMode::HalfAwayFromZero,
462            RoundingMode::HalfTowardZero,
463            RoundingMode::Trunc,
464            RoundingMode::Floor,
465            RoundingMode::Ceiling,
466        ]
467    }
468
469    /// Zero remainder is exact for every mode.
470    #[test]
471    fn zero_remainder_is_quotient_for_all_modes() {
472        for m in modes() {
473            assert_eq!(apply_rounding(20, 10, m), 2, "{m:?}");
474            assert_eq!(apply_rounding(-20, 10, m), -2, "{m:?}");
475            assert_eq!(apply_rounding(0, 10, m), 0, "{m:?}");
476        }
477    }
478
479    /// Half-to-even: ties go to even neighbour.
480    #[test]
481    fn half_to_even_ties() {
482        let m = RoundingMode::HalfToEven;
483        assert_eq!(apply_rounding(5, 10, m), 0); // 0.5 -> 0 (even)
484        assert_eq!(apply_rounding(15, 10, m), 2); // 1.5 -> 2
485        assert_eq!(apply_rounding(25, 10, m), 2); // 2.5 -> 2 (even)
486        assert_eq!(apply_rounding(35, 10, m), 4); // 3.5 -> 4
487        assert_eq!(apply_rounding(-5, 10, m), 0); // -0.5 -> 0
488        assert_eq!(apply_rounding(-15, 10, m), -2); // -1.5 -> -2
489        assert_eq!(apply_rounding(-25, 10, m), -2); // -2.5 -> -2
490        assert_eq!(apply_rounding(-35, 10, m), -4); // -3.5 -> -4
491    }
492
493    /// Half-away-from-zero: ties go away from zero.
494    #[test]
495    fn half_away_from_zero_ties() {
496        let m = RoundingMode::HalfAwayFromZero;
497        assert_eq!(apply_rounding(5, 10, m), 1);
498        assert_eq!(apply_rounding(15, 10, m), 2);
499        assert_eq!(apply_rounding(25, 10, m), 3);
500        assert_eq!(apply_rounding(-5, 10, m), -1);
501        assert_eq!(apply_rounding(-15, 10, m), -2);
502        assert_eq!(apply_rounding(-25, 10, m), -3);
503    }
504
505    /// Half-toward-zero: ties go toward zero.
506    #[test]
507    fn half_toward_zero_ties() {
508        let m = RoundingMode::HalfTowardZero;
509        assert_eq!(apply_rounding(5, 10, m), 0);
510        assert_eq!(apply_rounding(15, 10, m), 1);
511        assert_eq!(apply_rounding(25, 10, m), 2);
512        assert_eq!(apply_rounding(-5, 10, m), 0);
513        assert_eq!(apply_rounding(-15, 10, m), -1);
514        assert_eq!(apply_rounding(-25, 10, m), -2);
515    }
516
517    /// Trunc: always toward zero, regardless of magnitude.
518    #[test]
519    fn trunc_always_toward_zero() {
520        let m = RoundingMode::Trunc;
521        assert_eq!(apply_rounding(7, 10, m), 0);
522        assert_eq!(apply_rounding(9, 10, m), 0);
523        assert_eq!(apply_rounding(19, 10, m), 1);
524        assert_eq!(apply_rounding(-7, 10, m), 0);
525        assert_eq!(apply_rounding(-19, 10, m), -1);
526    }
527
528    /// Floor: always toward negative infinity.
529    #[test]
530    fn floor_toward_negative_infinity() {
531        let m = RoundingMode::Floor;
532        assert_eq!(apply_rounding(1, 10, m), 0);
533        assert_eq!(apply_rounding(7, 10, m), 0);
534        assert_eq!(apply_rounding(9, 10, m), 0);
535        assert_eq!(apply_rounding(-1, 10, m), -1);
536        assert_eq!(apply_rounding(-7, 10, m), -1);
537        assert_eq!(apply_rounding(-19, 10, m), -2);
538    }
539
540    /// Ceiling: always toward positive infinity.
541    #[test]
542    fn ceiling_toward_positive_infinity() {
543        let m = RoundingMode::Ceiling;
544        assert_eq!(apply_rounding(1, 10, m), 1);
545        assert_eq!(apply_rounding(7, 10, m), 1);
546        assert_eq!(apply_rounding(19, 10, m), 2);
547        assert_eq!(apply_rounding(-1, 10, m), 0);
548        assert_eq!(apply_rounding(-7, 10, m), 0);
549        assert_eq!(apply_rounding(-19, 10, m), -1);
550    }
551
552    /// Non-half values go to the nearest neighbour for every "half"
553    /// mode and ignore the half-tie rule.
554    #[test]
555    fn non_half_goes_to_nearest() {
556        for m in [
557            RoundingMode::HalfToEven,
558            RoundingMode::HalfAwayFromZero,
559            RoundingMode::HalfTowardZero,
560        ] {
561            assert_eq!(apply_rounding(4, 10, m), 0, "{m:?} 0.4");
562            assert_eq!(apply_rounding(6, 10, m), 1, "{m:?} 0.6");
563            assert_eq!(apply_rounding(-4, 10, m), 0, "{m:?} -0.4");
564            assert_eq!(apply_rounding(-6, 10, m), -1, "{m:?} -0.6");
565        }
566    }
567}