Skip to main content

decimal_scaled/support/
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/// `true` for the three round-to-nearest modes (`HalfToEven`,
161/// `HalfAwayFromZero`, `HalfTowardZero`), `false` for the directed
162/// modes (`Trunc`, `Floor`, `Ceiling`).
163///
164/// Kernels with a sub-LSB linear-approximation fast path (e.g.
165/// `ln(1 + δ)` near `δ`, `exp(δ)` near `1 + δ`) may short-circuit only
166/// under nearest rounding: those approximations land within half an LSB
167/// of the true value, which is exactly what nearest rounding needs but
168/// not enough for a directed mode, whose answer depends on which side of
169/// the boundary the true value falls. Directed modes must fall through
170/// to the full working-scale evaluation so the residual sign is known.
171#[inline(always)]
172pub(crate) const fn is_nearest_mode(mode: RoundingMode) -> bool {
173    matches!(
174        mode,
175        RoundingMode::HalfToEven
176            | RoundingMode::HalfAwayFromZero
177            | RoundingMode::HalfTowardZero
178    )
179}
180
181/// Correctly-rounded result of an odd, strictly-compressing function
182/// (`tanh`) at a tiny argument, for any rounding mode.
183///
184/// For `tanh` the Maclaurin series is `tanh(x) = x − x³/3 + …`, an
185/// alternating series in odd powers of `x`. Within the small-argument
186/// linear band the cubic correction `|x|³/3` is below one storage ULP
187/// yet strictly positive, so the true value `t = tanh(x)·10^SCALE`
188/// satisfies, for `raw = x·10^SCALE`:
189///
190/// ```text
191///   raw > 0 :  raw − 1 < t < raw          (just below the grid line raw)
192///   raw < 0 :  raw     < t < raw + 1      (just above the grid line raw)
193/// ```
194///
195/// i.e. `|t|` lies strictly inside `(|raw| − 1, |raw|)`. The result is
196/// therefore exactly determined by integer arithmetic on `raw` — no
197/// finite-precision kernel can resolve the sub-ULP cubic, so the
198/// directed modes must use this analytic decision rather than rounding
199/// the (grid-exact) linear approximation. The three nearest modes round
200/// to `raw` (the cubic is well under half a ULP in the band).
201///
202/// `one` is the storage value `1`; `zero` the storage value `0`. The
203/// caller guarantees `0 < |raw| <= threshold`, the band where the cubic
204/// stays under one ULP.
205#[inline]
206pub(crate) fn tiny_odd_compressing_directed<T>(
207    raw: T,
208    zero: T,
209    one: T,
210    mode: RoundingMode,
211) -> T
212where
213    T: Copy
214        + PartialOrd
215        + ::core::ops::Add<Output = T>
216        + ::core::ops::Sub<Output = T>,
217{
218    if is_nearest_mode(mode) {
219        return raw;
220    }
221    let positive = raw > zero;
222    match mode {
223        // Toward zero: drop the sub-ULP magnitude, landing on |raw| − 1.
224        RoundingMode::Trunc => {
225            if positive {
226                raw - one
227            } else {
228                raw + one
229            }
230        }
231        // Toward −∞.
232        RoundingMode::Floor => {
233            if positive {
234                raw - one
235            } else {
236                raw
237            }
238        }
239        // Toward +∞.
240        RoundingMode::Ceiling => {
241            if positive {
242                raw
243            } else {
244                raw + one
245            }
246        }
247        // Nearest modes handled above.
248        _ => raw,
249    }
250}
251
252/// Directed rounding for an odd transcendental whose true value at a
253/// tiny argument sits just *above* the grid line `raw` in magnitude —
254/// e.g. `sinh(x) = x + x³/6 + …`, where the cubic is strictly positive
255/// but below one ULP. The mirror of [`tiny_odd_compressing_directed`]
256/// (which handles the just-*below* case like `tanh`).
257///
258/// `raw` is the stored argument (= the leading term `x · 10^SCALE`),
259/// `zero`/`one` the type's storage `0` / `1`. The true value lies in
260/// `(|raw|, |raw| + 1)` in magnitude, so:
261///
262/// - nearest modes round to `raw` (the excess is < 0.5 ULP);
263/// - toward-zero (`Trunc`) drops the excess → `raw`;
264/// - `Floor` (toward −∞): `raw` if positive, `raw − 1` if negative;
265/// - `Ceiling` (toward +∞): `raw + 1` if positive, `raw` if negative.
266#[inline]
267pub(crate) fn tiny_odd_expanding_directed<T>(
268    raw: T,
269    zero: T,
270    one: T,
271    mode: RoundingMode,
272) -> T
273where
274    T: Copy
275        + PartialOrd
276        + ::core::ops::Add<Output = T>
277        + ::core::ops::Sub<Output = T>,
278{
279    if is_nearest_mode(mode) {
280        return raw;
281    }
282    let positive = raw > zero;
283    match mode {
284        // Toward zero: the excess is sub-ULP, so the magnitude stays at
285        // `|raw|` — i.e. `raw` unchanged.
286        RoundingMode::Trunc => raw,
287        // Toward −∞.
288        RoundingMode::Floor => {
289            if positive {
290                raw
291            } else {
292                raw - one
293            }
294        }
295        // Toward +∞.
296        RoundingMode::Ceiling => {
297            if positive {
298                raw + one
299            } else {
300                raw
301            }
302        }
303        // Nearest modes handled above.
304        _ => raw,
305    }
306}
307
308/// Applies `mode` to integer division `raw / divisor`, returning the
309/// rounded quotient.
310///
311/// Used by `D38::rescale_with` and by the multiplier-and-divide
312/// fast paths in `mg_divide`. The whole mode-specific logic is
313/// delegated to [`should_bump`]; this function is just the i128
314/// arithmetic wrapper that builds its inputs and applies the bump.
315#[inline(always)]
316pub(crate) fn apply_rounding(raw: i128, divisor: i128, mode: RoundingMode) -> i128 {
317    let quotient = raw / divisor;
318    let remainder = raw % divisor;
319
320    if remainder == 0 {
321        return quotient;
322    }
323
324    let abs_rem = remainder.unsigned_abs();
325    let abs_div = divisor.unsigned_abs();
326    let comp = abs_div - abs_rem;
327    let cmp_r = abs_rem.cmp(&comp);
328    let q_is_odd = (quotient & 1) != 0;
329    let result_positive = (raw < 0) == (divisor < 0);
330
331    if should_bump(mode, cmp_r, q_is_odd, result_positive) {
332        if result_positive { quotient + 1 } else { quotient - 1 }
333    } else {
334        quotient
335    }
336}
337
338/// `true` when the crate is built with [`DEFAULT_ROUNDING_MODE`] set to
339/// [`RoundingMode::HalfToEven`] — i.e. none of the `rounding-*` feature
340/// flags is selected. Used by tests whose expected values assume the
341/// default IEEE-754 rounding to short-circuit themselves under a
342/// non-default rounding feature build.
343#[cfg(test)]
344pub(crate) const DEFAULT_IS_HALF_TO_EVEN: bool = matches!(
345    DEFAULT_ROUNDING_MODE,
346    RoundingMode::HalfToEven
347);
348
349#[cfg(test)]
350mod tests {
351    use super::*;
352
353    fn modes() -> [RoundingMode; 6] {
354        [
355            RoundingMode::HalfToEven,
356            RoundingMode::HalfAwayFromZero,
357            RoundingMode::HalfTowardZero,
358            RoundingMode::Trunc,
359            RoundingMode::Floor,
360            RoundingMode::Ceiling,
361        ]
362    }
363
364    /// Zero remainder is exact for every mode.
365    #[test]
366    fn zero_remainder_is_quotient_for_all_modes() {
367        for m in modes() {
368            assert_eq!(apply_rounding(20, 10, m), 2, "{m:?}");
369            assert_eq!(apply_rounding(-20, 10, m), -2, "{m:?}");
370            assert_eq!(apply_rounding(0, 10, m), 0, "{m:?}");
371        }
372    }
373
374    /// Half-to-even: ties go to even neighbour.
375    #[test]
376    fn half_to_even_ties() {
377        let m = RoundingMode::HalfToEven;
378        assert_eq!(apply_rounding(5, 10, m), 0);     // 0.5 -> 0 (even)
379        assert_eq!(apply_rounding(15, 10, m), 2);    // 1.5 -> 2
380        assert_eq!(apply_rounding(25, 10, m), 2);    // 2.5 -> 2 (even)
381        assert_eq!(apply_rounding(35, 10, m), 4);    // 3.5 -> 4
382        assert_eq!(apply_rounding(-5, 10, m), 0);    // -0.5 -> 0
383        assert_eq!(apply_rounding(-15, 10, m), -2);  // -1.5 -> -2
384        assert_eq!(apply_rounding(-25, 10, m), -2);  // -2.5 -> -2
385        assert_eq!(apply_rounding(-35, 10, m), -4);  // -3.5 -> -4
386    }
387
388    /// Half-away-from-zero: ties go away from zero.
389    #[test]
390    fn half_away_from_zero_ties() {
391        let m = RoundingMode::HalfAwayFromZero;
392        assert_eq!(apply_rounding(5, 10, m), 1);
393        assert_eq!(apply_rounding(15, 10, m), 2);
394        assert_eq!(apply_rounding(25, 10, m), 3);
395        assert_eq!(apply_rounding(-5, 10, m), -1);
396        assert_eq!(apply_rounding(-15, 10, m), -2);
397        assert_eq!(apply_rounding(-25, 10, m), -3);
398    }
399
400    /// Half-toward-zero: ties go toward zero.
401    #[test]
402    fn half_toward_zero_ties() {
403        let m = RoundingMode::HalfTowardZero;
404        assert_eq!(apply_rounding(5, 10, m), 0);
405        assert_eq!(apply_rounding(15, 10, m), 1);
406        assert_eq!(apply_rounding(25, 10, m), 2);
407        assert_eq!(apply_rounding(-5, 10, m), 0);
408        assert_eq!(apply_rounding(-15, 10, m), -1);
409        assert_eq!(apply_rounding(-25, 10, m), -2);
410    }
411
412    /// Trunc: always toward zero, regardless of magnitude.
413    #[test]
414    fn trunc_always_toward_zero() {
415        let m = RoundingMode::Trunc;
416        assert_eq!(apply_rounding(7, 10, m), 0);
417        assert_eq!(apply_rounding(9, 10, m), 0);
418        assert_eq!(apply_rounding(19, 10, m), 1);
419        assert_eq!(apply_rounding(-7, 10, m), 0);
420        assert_eq!(apply_rounding(-19, 10, m), -1);
421    }
422
423    /// Floor: always toward negative infinity.
424    #[test]
425    fn floor_toward_negative_infinity() {
426        let m = RoundingMode::Floor;
427        assert_eq!(apply_rounding(1, 10, m), 0);
428        assert_eq!(apply_rounding(7, 10, m), 0);
429        assert_eq!(apply_rounding(9, 10, m), 0);
430        assert_eq!(apply_rounding(-1, 10, m), -1);
431        assert_eq!(apply_rounding(-7, 10, m), -1);
432        assert_eq!(apply_rounding(-19, 10, m), -2);
433    }
434
435    /// Ceiling: always toward positive infinity.
436    #[test]
437    fn ceiling_toward_positive_infinity() {
438        let m = RoundingMode::Ceiling;
439        assert_eq!(apply_rounding(1, 10, m), 1);
440        assert_eq!(apply_rounding(7, 10, m), 1);
441        assert_eq!(apply_rounding(19, 10, m), 2);
442        assert_eq!(apply_rounding(-1, 10, m), 0);
443        assert_eq!(apply_rounding(-7, 10, m), 0);
444        assert_eq!(apply_rounding(-19, 10, m), -1);
445    }
446
447    /// Non-half values go to the nearest neighbour for every "half"
448    /// mode and ignore the half-tie rule.
449    #[test]
450    fn non_half_goes_to_nearest() {
451        for m in [
452            RoundingMode::HalfToEven,
453            RoundingMode::HalfAwayFromZero,
454            RoundingMode::HalfTowardZero,
455        ] {
456            assert_eq!(apply_rounding(4, 10, m), 0, "{m:?} 0.4");
457            assert_eq!(apply_rounding(6, 10, m), 1, "{m:?} 0.6");
458            assert_eq!(apply_rounding(-4, 10, m), 0, "{m:?} -0.4");
459            assert_eq!(apply_rounding(-6, 10, m), -1, "{m:?} -0.6");
460        }
461    }
462}
463