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::quantize_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 eight modes are
11//! the full General Decimal Arithmetic rounding set: IEEE-754's five
12//! rounding rules (`HalfToEven`, `HalfTowardZero`, `Trunc`, `Floor`,
13//! `Ceiling`), the commercial `HalfAwayFromZero` rule expected by users
14//! coming from `bigdecimal` / `rust_decimal`, and the two remaining GDA
15//! rules `AwayFromZero` (`round-up`) and `ZeroFiveUp` (`round-05up`).
16//!
17//! The default mode is `HalfToEven` (IEEE-754 default; no systematic
18//! bias). The `rounding-*` Cargo features let a downstream crate flip
19//! the crate-wide default at compile time.
20
21/// Selector for the rounding rule applied when a scale-narrowing
22/// operation discards fractional digits.
23///
24/// See the module-level documentation for when each rule applies.
25///
26/// # Precision
27///
28/// N/A: this is a tag; no arithmetic is performed by constructing
29/// or comparing variants.
30#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
31pub enum RoundingMode {
32    /// Round to nearest; on ties, round to the even neighbour.
33    /// IEEE-754 `roundTiesToEven`; also called banker's rounding.
34    /// Unbiased — repeated rounding does not drift sums. Crate default.
35    ///
36    /// Examples (truncate to integer): `0.5 -> 0`, `1.5 -> 2`,
37    /// `2.5 -> 2`, `-0.5 -> 0`, `-1.5 -> -2`.
38    HalfToEven,
39    /// Round to nearest; on ties, round away from zero. Commercial
40    /// rounding. Mildly biased in magnitude.
41    ///
42    /// Examples: `0.5 -> 1`, `1.5 -> 2`, `-0.5 -> -1`, `-1.5 -> -2`.
43    HalfAwayFromZero,
44    /// Round to nearest; on ties, round toward zero. Mildly biased
45    /// toward zero. Rare in practice; included for completeness.
46    ///
47    /// Examples: `0.5 -> 0`, `1.5 -> 1`, `-0.5 -> 0`, `-1.5 -> -1`.
48    HalfTowardZero,
49    /// Truncate toward zero. Discards the fractional part. Cheapest
50    /// in integer arithmetic; matches Rust's `as` cast for integer
51    /// narrowing.
52    ///
53    /// Examples: `0.7 -> 0`, `-0.7 -> 0`, `1.9 -> 1`, `-1.9 -> -1`.
54    Trunc,
55    /// Round toward negative infinity (floor).
56    ///
57    /// Examples: `0.7 -> 0`, `-0.7 -> -1`, `1.9 -> 1`, `-1.9 -> -2`.
58    Floor,
59    /// Round toward positive infinity (ceiling).
60    ///
61    /// Examples: `0.7 -> 1`, `-0.7 -> 0`, `1.9 -> 2`, `-1.9 -> -1`.
62    Ceiling,
63    /// Round away from zero whenever anything was discarded — the exact
64    /// mirror of [`RoundingMode::Trunc`]. An exact value is never moved.
65    /// GDA name: `round-up`.
66    ///
67    /// Examples: `0.1 -> 1`, `0.7 -> 1`, `-0.1 -> -1`, `-1.9 -> -2`,
68    /// `2.0 -> 2`.
69    AwayFromZero,
70    /// Round away from zero **iff** the last retained digit of the
71    /// toward-zero result is `0` or `5`; otherwise truncate. An exact
72    /// value is never moved.
73    ///
74    /// Reserves `0` and `5` as the only final digits that can absorb a
75    /// discarded remainder, so a later round to one fewer digit never
76    /// sees a half-way tie that the first rounding manufactured — the
77    /// "round for reround" rule. GDA name: `round-05up`.
78    ///
79    /// Examples (truncate to integer): `0.7 -> 1` (last digit `0`),
80    /// `5.7 -> 6` (last digit `5`), `1.7 -> 1`, `4.7 -> 4`, `6.7 -> 6`,
81    /// `-0.7 -> -1`, `-5.7 -> -6`, `-4.7 -> -4`, `2.0 -> 2`.
82    ZeroFiveUp,
83}
84
85/// Compile-time default `RoundingMode` for the no-arg `quantize` and
86/// future default-rounding methods.
87///
88/// Selected by Cargo feature flags (priority order: first match wins):
89/// 1. `rounding-half-away-from-zero` → `HalfAwayFromZero`
90/// 2. `rounding-half-toward-zero` → `HalfTowardZero`
91/// 3. `rounding-trunc` → `Trunc`
92/// 4. `rounding-floor` → `Floor`
93/// 5. `rounding-ceiling` → `Ceiling`
94/// 6. `rounding-away-from-zero` → `AwayFromZero`
95/// 7. `rounding-zero-five-up` → `ZeroFiveUp`
96/// 8. (none) → `HalfToEven` (IEEE-754 default; banker's rounding)
97#[cfg(feature = "rounding-half-away-from-zero")]
98pub const DEFAULT_ROUNDING_MODE: RoundingMode = RoundingMode::HalfAwayFromZero;
99
100#[cfg(all(
101    not(feature = "rounding-half-away-from-zero"),
102    feature = "rounding-half-toward-zero"
103))]
104pub const DEFAULT_ROUNDING_MODE: RoundingMode = RoundingMode::HalfTowardZero;
105
106#[cfg(all(
107    not(feature = "rounding-half-away-from-zero"),
108    not(feature = "rounding-half-toward-zero"),
109    feature = "rounding-trunc"
110))]
111pub const DEFAULT_ROUNDING_MODE: RoundingMode = RoundingMode::Trunc;
112
113#[cfg(all(
114    not(feature = "rounding-half-away-from-zero"),
115    not(feature = "rounding-half-toward-zero"),
116    not(feature = "rounding-trunc"),
117    feature = "rounding-floor"
118))]
119pub const DEFAULT_ROUNDING_MODE: RoundingMode = RoundingMode::Floor;
120
121#[cfg(all(
122    not(feature = "rounding-half-away-from-zero"),
123    not(feature = "rounding-half-toward-zero"),
124    not(feature = "rounding-trunc"),
125    not(feature = "rounding-floor"),
126    feature = "rounding-ceiling"
127))]
128pub const DEFAULT_ROUNDING_MODE: RoundingMode = RoundingMode::Ceiling;
129
130#[cfg(all(
131    not(feature = "rounding-half-away-from-zero"),
132    not(feature = "rounding-half-toward-zero"),
133    not(feature = "rounding-trunc"),
134    not(feature = "rounding-floor"),
135    not(feature = "rounding-ceiling"),
136    feature = "rounding-away-from-zero"
137))]
138pub const DEFAULT_ROUNDING_MODE: RoundingMode = RoundingMode::AwayFromZero;
139
140#[cfg(all(
141    not(feature = "rounding-half-away-from-zero"),
142    not(feature = "rounding-half-toward-zero"),
143    not(feature = "rounding-trunc"),
144    not(feature = "rounding-floor"),
145    not(feature = "rounding-ceiling"),
146    not(feature = "rounding-away-from-zero"),
147    feature = "rounding-zero-five-up"
148))]
149pub const DEFAULT_ROUNDING_MODE: RoundingMode = RoundingMode::ZeroFiveUp;
150
151#[cfg(not(any(
152    feature = "rounding-half-away-from-zero",
153    feature = "rounding-half-toward-zero",
154    feature = "rounding-trunc",
155    feature = "rounding-floor",
156    feature = "rounding-ceiling",
157    feature = "rounding-away-from-zero",
158    feature = "rounding-zero-five-up",
159)))]
160pub const DEFAULT_ROUNDING_MODE: RoundingMode = RoundingMode::HalfToEven;
161
162/// Strategy hook for the rounding-mode family.
163///
164/// Given a *truncated-toward-zero* quotient and the per-operation
165/// numerator / divisor context, returns `true` if the quotient should
166/// be bumped one step "away from zero" in the result's direction to
167/// satisfy this mode. Caller is responsible for the actual bump (it
168/// is `q + 1` when the result is positive, `q − 1` when negative).
169///
170/// The three inputs collapse the per-step numerics that every mode
171/// cares about into mode-independent booleans / orderings:
172///
173/// - `remainder_cmp` — three-way comparison of `|r|` against `|m| − |r|`.
174///   This
175///   is exactly the round-up condition (`|r| > |m| − |r|` ⇔ `2·|r| > |m|`)
176///   without the doubling-overflow risk. `Equal` flags the half-way tie,
177///   which only occurs when the divisor is even.
178/// - `q_mod_10` — the last decimal digit of the truncated quotient's
179///   *magnitude* (`|q| % 10`, so always `0..=9`). Its parity
180///   (`q_mod_10 & 1`) drives the half-to-even tie break — `10` is even,
181///   so the last digit and the whole number share a parity — and its
182///   value drives `ZeroFiveUp`, which is why the digit rather than the
183///   parity is the parameter.
184/// - `result_positive` — sign of the true result (`sign(n) == sign(m)`).
185///   Drives `Floor` / `Ceiling`.
186///
187/// Caller pre-handles the `r == 0` case (no rounding needed). That
188/// contract is what lets `AwayFromZero` be an unconditional `true` and
189/// `ZeroFiveUp` test only the digit: both are defined to leave an exact
190/// value alone, and an exact value never reaches here.
191///
192/// `#[inline(always)]` because the entire body is one match on an
193/// 8-variant enum. The hot operator path instantiates this with a
194/// const `mode` (`DEFAULT_ROUNDING_MODE`), so const-propagation can
195/// collapse the match away once inlined.
196#[inline(always)]
197pub(crate) fn should_bump(
198    mode: RoundingMode,
199    remainder_cmp: ::core::cmp::Ordering,
200    q_mod_10: u8,
201    result_positive: bool,
202) -> bool {
203    use ::core::cmp::Ordering;
204    match mode {
205        RoundingMode::HalfToEven => match remainder_cmp {
206            Ordering::Less => false,
207            Ordering::Greater => true,
208            Ordering::Equal => q_mod_10 & 1 == 1,
209        },
210        RoundingMode::HalfAwayFromZero => !matches!(remainder_cmp, Ordering::Less),
211        RoundingMode::HalfTowardZero => matches!(remainder_cmp, Ordering::Greater),
212        RoundingMode::Trunc => false,
213        RoundingMode::Floor => !result_positive,
214        RoundingMode::Ceiling => result_positive,
215        RoundingMode::AwayFromZero => true,
216        RoundingMode::ZeroFiveUp => q_mod_10 == 0 || q_mod_10 == 5,
217    }
218}
219
220/// Last decimal digit of a little-endian `u64`-limb MAGNITUDE — the
221/// `q_mod_10` input to [`should_bump`] for the kernels whose truncated
222/// quotient is a limb slice rather than a typed integer (`div_widen_scale`,
223/// `mul_schoolbook`, `newton_reciprocal`, `mg_divide`, `fixed`). Those
224/// sites read `quot[0] & 1` for the parity the tie break used to need;
225/// the last DECIMAL digit is not carried by one limb, so it needs the
226/// whole magnitude.
227///
228/// `2^64 ≡ 6 (mod 10)`, and `6^k ≡ 6 (mod 10)` for every `k >= 1`, so
229///
230/// ```text
231///   Σ limb_i · 2^(64·i)  ≡  limb_0 + 6 · Σ_{i>=1} limb_i   (mod 10)
232/// ```
233///
234/// which replaces an O(limbs) long division with one `u64 % 10` per limb
235/// — a constant divisor, so the compiler lowers each to a multiply-shift.
236/// The accumulator cannot overflow: each term is at most `6·9 = 54`.
237///
238/// `mag` must be a magnitude (the sign lives outside the limbs, as it
239/// does at every call site); an empty slice is the value `0`.
240///
241/// `const` because `consts::table`'s compile-time-baked constant
242/// accessors (`pi_const_n` and siblings) are `const fn` and need the
243/// pivot digit of their floor entry; hence the `while` loop rather than
244/// an iterator.
245#[inline]
246pub(crate) const fn limbs_mod_10(mag: &[u64]) -> u8 {
247    let mut acc: u32 = 0;
248    let mut i = 0;
249    while i < mag.len() {
250        let digit = (mag[i] % 10) as u32;
251        acc += if i == 0 { digit } else { 6 * digit };
252        i += 1;
253    }
254    (acc % 10) as u8
255}
256
257/// Last decimal digit of a little-endian `u128`-limb MAGNITUDE — the
258/// u128-packed sibling of [`limbs_mod_10`], for the kernels that carry
259/// their quotient in u128 limbs (`newton_reciprocal`'s u128 path).
260///
261/// The same fold applies unchanged: `2^128 = (2^64)^2 ≡ 6^2 = 36 ≡ 6
262/// (mod 10)`, so the base's residue is `6` here too.
263#[inline]
264pub(crate) fn limbs_u128_mod_10(mag: &[u128]) -> u8 {
265    let mut acc: u32 = 0;
266    for (i, &limb) in mag.iter().enumerate() {
267        let digit = (limb % 10) as u32;
268        acc += if i == 0 { digit } else { 6 * digit };
269    }
270    (acc % 10) as u8
271}
272
273/// `true` for the three round-to-nearest modes (`HalfToEven`,
274/// `HalfAwayFromZero`, `HalfTowardZero`), `false` for the directed
275/// modes (`Trunc`, `Floor`, `Ceiling`, `AwayFromZero`, `ZeroFiveUp`).
276///
277/// Kernels with a sub-LSB linear-approximation fast path (e.g.
278/// `ln(1 + δ)` near `δ`, `exp(δ)` near `1 + δ`) may short-circuit only
279/// under nearest rounding: those approximations land within half an LSB
280/// of the true value, which is exactly what nearest rounding needs but
281/// not enough for a directed mode, whose answer depends on which side of
282/// the boundary the true value falls. Directed modes must fall through
283/// to the full working-scale evaluation so the residual sign is known.
284#[inline(always)]
285pub(crate) const fn is_nearest_mode(mode: RoundingMode) -> bool {
286    matches!(
287        mode,
288        RoundingMode::HalfToEven | RoundingMode::HalfAwayFromZero | RoundingMode::HalfTowardZero
289    )
290}
291
292/// Correctly-rounded result of an odd, strictly-compressing function
293/// (`tanh`) at a tiny argument, for any rounding mode.
294///
295/// For `tanh` the Maclaurin series is `tanh(x) = x − x³/3 + …`, an
296/// alternating series in odd powers of `x`. Within the small-argument
297/// linear band the cubic correction `|x|³/3` is below one storage ULP
298/// yet strictly positive, so the true value `t = tanh(x)·10^SCALE`
299/// satisfies, for `raw = x·10^SCALE`:
300///
301/// ```text
302///   raw > 0 :  raw − 1 < t < raw          (just below the grid line raw)
303///   raw < 0 :  raw     < t < raw + 1      (just above the grid line raw)
304/// ```
305///
306/// i.e. `|t|` lies strictly inside `(|raw| − 1, |raw|)`. The result is
307/// therefore exactly determined by integer arithmetic on `raw` — no
308/// finite-precision kernel can resolve the sub-ULP cubic, so the
309/// directed modes must use this analytic decision rather than rounding
310/// the (grid-exact) linear approximation. The three nearest modes round
311/// to `raw` (the cubic is well under half a ULP in the band).
312///
313/// `one` is the storage value `1`; `zero` the storage value `0`. The
314/// caller guarantees `0 < |raw| <= threshold`, the band where the cubic
315/// stays under one ULP.
316///
317/// `raw_mod_10` is `|raw| % 10`, supplied by the caller because `T` here
318/// carries no division. Only `ZeroFiveUp` reads it: that mode needs the
319/// last digit of the *toward-zero* result, whose magnitude is `|raw| − 1`
320/// — hence the `+ 9` borrow step below, exact because `|raw| >= 1`.
321#[inline]
322pub(crate) fn tiny_odd_compressing_directed<T>(
323    raw: T,
324    zero: T,
325    one: T,
326    raw_mod_10: u8,
327    mode: RoundingMode,
328) -> T
329where
330    T: Copy + PartialOrd + ::core::ops::Add<Output = T> + ::core::ops::Sub<Output = T>,
331{
332    if is_nearest_mode(mode) {
333        return raw;
334    }
335    let is_positive = raw > zero;
336    // The toward-zero result: magnitude |raw| − 1.
337    let toward_zero = if is_positive { raw - one } else { raw + one };
338    match mode {
339        // Toward zero: drop the sub-ULP magnitude, landing on |raw| − 1.
340        RoundingMode::Trunc => toward_zero,
341        // Toward −∞.
342        RoundingMode::Floor => {
343            if is_positive {
344                raw - one
345            } else {
346                raw
347            }
348        }
349        // Toward +∞.
350        RoundingMode::Ceiling => {
351            if is_positive {
352                raw
353            } else {
354                raw + one
355            }
356        }
357        // The discarded part is non-zero throughout the band, so away
358        // from zero is always the full magnitude |raw|.
359        RoundingMode::AwayFromZero => raw,
360        // Last digit of |raw| − 1; bump to |raw| only on 0 or 5.
361        RoundingMode::ZeroFiveUp => {
362            let d = (raw_mod_10 + 9) % 10;
363            if d == 0 || d == 5 {
364                raw
365            } else {
366                toward_zero
367            }
368        }
369        // Nearest modes handled above.
370        _ => raw,
371    }
372}
373
374/// Directed rounding for an odd transcendental whose true value at a
375/// tiny argument sits just *above* the grid line `raw` in magnitude —
376/// e.g. `sinh(x) = x + x³/6 + …`, where the cubic is strictly positive
377/// but below one ULP. The mirror of [`tiny_odd_compressing_directed`]
378/// (which handles the just-*below* case like `tanh`).
379///
380/// `raw` is the stored argument (= the leading term `x · 10^SCALE`),
381/// `zero`/`one` the type's storage `0` / `1`. The true value lies in
382/// `(|raw|, |raw| + 1)` in magnitude, so:
383///
384/// - nearest modes round to `raw` (the excess is < 0.5 ULP);
385/// - toward-zero (`Trunc`) drops the excess → `raw`;
386/// - `Floor` (toward −∞): `raw` if positive, `raw − 1` if negative;
387/// - `Ceiling` (toward +∞): `raw + 1` if positive, `raw` if negative;
388/// - `AwayFromZero`: `raw + 1` if positive, `raw − 1` if negative;
389/// - `ZeroFiveUp`: as `AwayFromZero` when `|raw| % 10` is `0` or `5`,
390///   otherwise `raw`.
391///
392/// `raw_mod_10` is `|raw| % 10`, supplied by the caller because `T` here
393/// carries no division. Only `ZeroFiveUp` reads it, and here the
394/// toward-zero result *is* `raw`, so the digit needs no adjustment.
395#[inline]
396pub(crate) fn tiny_odd_expanding_directed<T>(
397    raw: T,
398    zero: T,
399    one: T,
400    raw_mod_10: u8,
401    mode: RoundingMode,
402) -> T
403where
404    T: Copy + PartialOrd + ::core::ops::Add<Output = T> + ::core::ops::Sub<Output = T>,
405{
406    if is_nearest_mode(mode) {
407        return raw;
408    }
409    let is_positive = raw > zero;
410    // One step away from zero from the toward-zero result `raw`.
411    let away = if is_positive { raw + one } else { raw - one };
412    match mode {
413        // Toward zero: the excess is sub-ULP, so the magnitude stays at
414        // `|raw|` — i.e. `raw` unchanged.
415        RoundingMode::Trunc => raw,
416        // Toward −∞.
417        RoundingMode::Floor => {
418            if is_positive {
419                raw
420            } else {
421                raw - one
422            }
423        }
424        // Toward +∞.
425        RoundingMode::Ceiling => {
426            if is_positive {
427                raw + one
428            } else {
429                raw
430            }
431        }
432        // The discarded excess is non-zero throughout the band.
433        RoundingMode::AwayFromZero => away,
434        // The toward-zero result is `raw` itself, so its last digit is
435        // `raw_mod_10` directly.
436        RoundingMode::ZeroFiveUp => {
437            if raw_mod_10 == 0 || raw_mod_10 == 5 {
438                away
439            } else {
440                raw
441            }
442        }
443        // Nearest modes handled above.
444        _ => raw,
445    }
446}
447
448/// Applies `mode` to integer division `raw / divisor`, returning the
449/// rounded quotient.
450///
451/// Used by `D38::quantize_with` and by the multiplier-and-divide
452/// fast paths in `mg_divide`. The whole mode-specific logic is
453/// delegated to [`should_bump`]; this function is just the i128
454/// arithmetic wrapper that builds its inputs and applies the bump.
455#[inline(always)]
456pub(crate) fn apply_rounding(raw: i128, divisor: i128, mode: RoundingMode) -> i128 {
457    let quotient = raw / divisor;
458    let remainder = raw % divisor;
459
460    if remainder == 0 {
461        return quotient;
462    }
463
464    let abs_remainder = remainder.unsigned_abs();
465    let abs_divisor = divisor.unsigned_abs();
466    let complement = abs_divisor - abs_remainder;
467    let remainder_cmp = abs_remainder.cmp(&complement);
468    // Last decimal digit of |quotient|: `%` on a negative `i128` keeps the
469    // sign, so take the magnitude of the one-digit remainder.
470    let q_mod_10 = (quotient % 10).unsigned_abs() as u8;
471    let result_positive = (raw < 0) == (divisor < 0);
472
473    if should_bump(mode, remainder_cmp, q_mod_10, result_positive) {
474        if result_positive {
475            quotient + 1
476        } else {
477            quotient - 1
478        }
479    } else {
480        quotient
481    }
482}
483
484/// `2^52` — the threshold at or above which every finite `f64` is
485/// already an exact integer (the mantissa can no longer represent a
486/// fractional bit). Used by the libm-free `f64` rounding helpers to
487/// short-circuit large magnitudes, which also keeps the `as i128`
488/// truncation inside `i128` range (`2^52 < i128::MAX`).
489const F64_INTEGER_THRESHOLD: f64 = 9_007_199_254_740_992.0_f64;
490
491/// Truncate an `f64` toward zero, libm-free.
492///
493/// Equivalent to [`f64::trunc`] but built from arithmetic and `as`
494/// casts only, so it is available in `no_std` without `libm`. For
495/// magnitudes at or above `2^52` (already integral) and for non-finite
496/// inputs the value is returned unchanged; otherwise the integral part
497/// is recovered via an `i128` round-trip, which is exact in that range.
498/// The negative-zero sign is preserved to match [`f64::trunc`] bit-for-bit.
499#[inline]
500pub(crate) fn trunc_f64(value: f64) -> f64 {
501    if value.is_nan() {
502        return value;
503    }
504    let magnitude = if value < 0.0 { -value } else { value };
505    if magnitude >= F64_INTEGER_THRESHOLD {
506        // NaN is already returned above, so `>=` is the exact complement of
507        // `< THRESHOLD` here: already-integral / too-large magnitudes pass
508        // through unchanged.
509        return value;
510    }
511    let truncated = value as i128 as f64;
512    if truncated == 0.0 && value.is_sign_negative() {
513        -0.0
514    } else {
515        truncated
516    }
517}
518
519/// Round an `f64` toward negative infinity, libm-free. Equivalent to
520/// [`f64::floor`]: drop to the truncated value, then step down by one
521/// when truncation rounded a negative value up toward zero.
522#[inline]
523pub(crate) fn floor_f64(value: f64) -> f64 {
524    let truncated = trunc_f64(value);
525    if truncated > value {
526        truncated - 1.0
527    } else {
528        truncated
529    }
530}
531
532/// Round an `f64` toward positive infinity, libm-free. Equivalent to
533/// [`f64::ceil`]: the mirror of [`floor_f64`].
534#[inline]
535pub(crate) fn ceil_f64(value: f64) -> f64 {
536    let truncated = trunc_f64(value);
537    if truncated < value {
538        truncated + 1.0
539    } else {
540        truncated
541    }
542}
543
544/// Round an `f64` to the nearest integer, ties away from zero, libm-free.
545/// Equivalent to [`f64::round`]: a fractional part with magnitude `>= 0.5`
546/// steps the truncated value one away from zero.
547#[inline]
548pub(crate) fn round_half_away_f64(value: f64) -> f64 {
549    let truncated = trunc_f64(value);
550    let fraction = value - truncated;
551    if fraction >= 0.5 {
552        truncated + 1.0
553    } else if fraction <= -0.5 {
554        truncated - 1.0
555    } else {
556        truncated
557    }
558}
559
560/// Round an `f64` to the nearest integer, ties to even, libm-free.
561/// Equivalent to [`f64::round_ties_even`]: a fractional part strictly
562/// past `0.5` in magnitude steps one away from zero; an exact half steps
563/// only when the truncated value is odd, landing on the even neighbour.
564#[inline]
565pub(crate) fn round_half_even_f64(value: f64) -> f64 {
566    let truncated = trunc_f64(value);
567    let fraction = value - truncated;
568    if fraction > 0.5 {
569        truncated + 1.0
570    } else if fraction < -0.5 {
571        truncated - 1.0
572    } else if fraction == 0.5 {
573        if (truncated as i128) & 1 == 0 {
574            truncated
575        } else {
576            truncated + 1.0
577        }
578    } else if fraction == -0.5 {
579        if (truncated as i128) & 1 == 0 {
580            truncated
581        } else {
582            truncated - 1.0
583        }
584    } else {
585        truncated
586    }
587}
588
589/// Round an `f64` to the nearest integer, ties toward zero, libm-free.
590/// Reproduces the previous `std` formulation
591/// (`(x - 0.5).ceil()` for `x >= 0`, `(x + 0.5).floor()` otherwise)
592/// using the libm-free [`ceil_f64`] / [`floor_f64`].
593#[inline]
594pub(crate) fn round_half_toward_zero_f64(value: f64) -> f64 {
595    if value >= 0.0 {
596        ceil_f64(value - 0.5)
597    } else {
598        floor_f64(value + 0.5)
599    }
600}
601
602/// Round an `f64` away from zero whenever it has a fractional part,
603/// libm-free — the `f64` form of [`RoundingMode::AwayFromZero`] and the
604/// exact mirror of [`trunc_f64`]. An already-integral value (which
605/// includes every magnitude at or above `2^52`, and the non-finite
606/// values) passes through unchanged.
607#[inline]
608pub(crate) fn away_from_zero_f64(x: f64) -> f64 {
609    let truncated = trunc_f64(x);
610    if truncated == x {
611        return truncated;
612    }
613    if x > 0.0 {
614        truncated + 1.0
615    } else {
616        truncated - 1.0
617    }
618}
619
620/// Round an `f64` away from zero iff the last decimal digit of the
621/// truncated value is `0` or `5`, libm-free — the `f64` form of
622/// [`RoundingMode::ZeroFiveUp`]. An already-integral value passes
623/// through unchanged, so the `as i128` digit read only runs where
624/// [`trunc_f64`] took its exact `i128` round-trip (magnitude below
625/// `2^52`).
626#[inline]
627pub(crate) fn zero_five_up_f64(x: f64) -> f64 {
628    let truncated = trunc_f64(x);
629    if truncated == x {
630        return truncated;
631    }
632    let last_digit = (truncated as i128 % 10).unsigned_abs();
633    if last_digit == 0 || last_digit == 5 {
634        if x > 0.0 {
635            truncated + 1.0
636        } else {
637            truncated - 1.0
638        }
639    } else {
640        truncated
641    }
642}
643
644/// `true` when the crate is built with [`DEFAULT_ROUNDING_MODE`] set to
645/// [`RoundingMode::HalfToEven`] — i.e. none of the `rounding-*` feature
646/// flags is selected. Used by tests whose expected values assume the
647/// default IEEE-754 rounding to short-circuit themselves under a
648/// non-default rounding feature build.
649#[cfg(test)]
650pub(crate) const DEFAULT_IS_HALF_TO_EVEN: bool =
651    matches!(DEFAULT_ROUNDING_MODE, RoundingMode::HalfToEven);
652
653#[cfg(test)]
654mod tests {
655    use super::*;
656
657    fn modes() -> [RoundingMode; 8] {
658        [
659            RoundingMode::HalfToEven,
660            RoundingMode::HalfAwayFromZero,
661            RoundingMode::HalfTowardZero,
662            RoundingMode::Trunc,
663            RoundingMode::Floor,
664            RoundingMode::Ceiling,
665            RoundingMode::AwayFromZero,
666            RoundingMode::ZeroFiveUp,
667        ]
668    }
669
670    /// Zero remainder is exact for every mode.
671    #[test]
672    fn zero_remainder_is_quotient_for_all_modes() {
673        for mode in modes() {
674            assert_eq!(apply_rounding(20, 10, mode), 2, "{mode:?}");
675            assert_eq!(apply_rounding(-20, 10, mode), -2, "{mode:?}");
676            assert_eq!(apply_rounding(0, 10, mode), 0, "{mode:?}");
677        }
678    }
679
680    /// Half-to-even: ties go to even neighbour.
681    #[test]
682    fn half_to_even_ties() {
683        let mode = RoundingMode::HalfToEven;
684        assert_eq!(apply_rounding(5, 10, mode), 0); // 0.5 -> 0 (even)
685        assert_eq!(apply_rounding(15, 10, mode), 2); // 1.5 -> 2
686        assert_eq!(apply_rounding(25, 10, mode), 2); // 2.5 -> 2 (even)
687        assert_eq!(apply_rounding(35, 10, mode), 4); // 3.5 -> 4
688        assert_eq!(apply_rounding(-5, 10, mode), 0); // -0.5 -> 0
689        assert_eq!(apply_rounding(-15, 10, mode), -2); // -1.5 -> -2
690        assert_eq!(apply_rounding(-25, 10, mode), -2); // -2.5 -> -2
691        assert_eq!(apply_rounding(-35, 10, mode), -4); // -3.5 -> -4
692    }
693
694    /// Half-away-from-zero: ties go away from zero.
695    #[test]
696    fn half_away_from_zero_ties() {
697        let mode = RoundingMode::HalfAwayFromZero;
698        assert_eq!(apply_rounding(5, 10, mode), 1);
699        assert_eq!(apply_rounding(15, 10, mode), 2);
700        assert_eq!(apply_rounding(25, 10, mode), 3);
701        assert_eq!(apply_rounding(-5, 10, mode), -1);
702        assert_eq!(apply_rounding(-15, 10, mode), -2);
703        assert_eq!(apply_rounding(-25, 10, mode), -3);
704    }
705
706    /// Half-toward-zero: ties go toward zero.
707    #[test]
708    fn half_toward_zero_ties() {
709        let mode = RoundingMode::HalfTowardZero;
710        assert_eq!(apply_rounding(5, 10, mode), 0);
711        assert_eq!(apply_rounding(15, 10, mode), 1);
712        assert_eq!(apply_rounding(25, 10, mode), 2);
713        assert_eq!(apply_rounding(-5, 10, mode), 0);
714        assert_eq!(apply_rounding(-15, 10, mode), -1);
715        assert_eq!(apply_rounding(-25, 10, mode), -2);
716    }
717
718    /// Trunc: always toward zero, regardless of magnitude.
719    #[test]
720    fn trunc_always_toward_zero() {
721        let mode = RoundingMode::Trunc;
722        assert_eq!(apply_rounding(7, 10, mode), 0);
723        assert_eq!(apply_rounding(9, 10, mode), 0);
724        assert_eq!(apply_rounding(19, 10, mode), 1);
725        assert_eq!(apply_rounding(-7, 10, mode), 0);
726        assert_eq!(apply_rounding(-19, 10, mode), -1);
727    }
728
729    /// Floor: always toward negative infinity.
730    #[test]
731    fn floor_toward_negative_infinity() {
732        let mode = RoundingMode::Floor;
733        assert_eq!(apply_rounding(1, 10, mode), 0);
734        assert_eq!(apply_rounding(7, 10, mode), 0);
735        assert_eq!(apply_rounding(9, 10, mode), 0);
736        assert_eq!(apply_rounding(-1, 10, mode), -1);
737        assert_eq!(apply_rounding(-7, 10, mode), -1);
738        assert_eq!(apply_rounding(-19, 10, mode), -2);
739    }
740
741    /// Ceiling: always toward positive infinity.
742    #[test]
743    fn ceiling_toward_positive_infinity() {
744        let mode = RoundingMode::Ceiling;
745        assert_eq!(apply_rounding(1, 10, mode), 1);
746        assert_eq!(apply_rounding(7, 10, mode), 1);
747        assert_eq!(apply_rounding(19, 10, mode), 2);
748        assert_eq!(apply_rounding(-1, 10, mode), 0);
749        assert_eq!(apply_rounding(-7, 10, mode), 0);
750        assert_eq!(apply_rounding(-19, 10, mode), -1);
751    }
752
753    /// Non-half values go to the nearest neighbour for every "half"
754    /// mode and ignore the half-tie rule.
755    #[test]
756    fn non_half_goes_to_nearest() {
757        for mode in [
758            RoundingMode::HalfToEven,
759            RoundingMode::HalfAwayFromZero,
760            RoundingMode::HalfTowardZero,
761        ] {
762            assert_eq!(apply_rounding(4, 10, mode), 0, "{mode:?} 0.4");
763            assert_eq!(apply_rounding(6, 10, mode), 1, "{mode:?} 0.6");
764            assert_eq!(apply_rounding(-4, 10, mode), 0, "{mode:?} -0.4");
765            assert_eq!(apply_rounding(-6, 10, mode), -1, "{mode:?} -0.6");
766        }
767    }
768
769    /// Away-from-zero (GDA `round-up`): any non-zero discard steps the
770    /// magnitude up; an exact value is left alone. The mirror of `Trunc`.
771    #[test]
772    fn away_from_zero_lifts_every_discard() {
773        let m = RoundingMode::AwayFromZero;
774        assert_eq!(apply_rounding(1, 10, m), 1); // 0.1 -> 1
775        assert_eq!(apply_rounding(5, 10, m), 1); // 0.5 -> 1
776        assert_eq!(apply_rounding(9, 10, m), 1); // 0.9 -> 1
777        assert_eq!(apply_rounding(11, 10, m), 2); // 1.1 -> 2
778        assert_eq!(apply_rounding(19, 10, m), 2); // 1.9 -> 2
779        assert_eq!(apply_rounding(-1, 10, m), -1);
780        assert_eq!(apply_rounding(-5, 10, m), -1);
781        assert_eq!(apply_rounding(-11, 10, m), -2);
782        assert_eq!(apply_rounding(-19, 10, m), -2);
783        // Exact: nothing discarded, so nothing moves.
784        assert_eq!(apply_rounding(20, 10, m), 2);
785        assert_eq!(apply_rounding(-20, 10, m), -2);
786    }
787
788    /// Zero-five-up (GDA `round-05up`): the discard lifts the magnitude
789    /// only when the last retained digit is `0` or `5`.
790    #[test]
791    fn zero_five_up_pivots_on_zero_and_five() {
792        let m = RoundingMode::ZeroFiveUp;
793        // Last retained digit 0 -> bump.
794        assert_eq!(apply_rounding(7, 10, m), 1); // q = 0
795        assert_eq!(apply_rounding(107, 10, m), 11); // q = 10
796        // Last retained digit 5 -> bump.
797        assert_eq!(apply_rounding(57, 10, m), 6); // q = 5
798        assert_eq!(apply_rounding(157, 10, m), 16); // q = 15
799        // Every other last digit truncates — 4 and 6 straddle the 5.
800        assert_eq!(apply_rounding(47, 10, m), 4);
801        assert_eq!(apply_rounding(67, 10, m), 6);
802        assert_eq!(apply_rounding(17, 10, m), 1);
803        assert_eq!(apply_rounding(97, 10, m), 9);
804        // Nothing discarded: no bump even on a 0 or 5 last digit.
805        assert_eq!(apply_rounding(50, 10, m), 5);
806        assert_eq!(apply_rounding(40, 10, m), 4);
807        assert_eq!(apply_rounding(0, 10, m), 0);
808    }
809
810    /// Zero-five-up on the negative side: the pivot is the last digit of
811    /// the MAGNITUDE, and the bump is away from zero.
812    #[test]
813    fn zero_five_up_is_sign_symmetric() {
814        let m = RoundingMode::ZeroFiveUp;
815        assert_eq!(apply_rounding(-7, 10, m), -1); // q = 0
816        assert_eq!(apply_rounding(-107, 10, m), -11); // q = -10
817        assert_eq!(apply_rounding(-57, 10, m), -6); // q = -5
818        assert_eq!(apply_rounding(-157, 10, m), -16); // q = -15
819        assert_eq!(apply_rounding(-47, 10, m), -4);
820        assert_eq!(apply_rounding(-67, 10, m), -6);
821        assert_eq!(apply_rounding(-17, 10, m), -1);
822        assert_eq!(apply_rounding(-50, 10, m), -5);
823        assert_eq!(apply_rounding(-40, 10, m), -4);
824    }
825
826    /// The half-to-even tie break reads `q_mod_10 & 1`, which must agree
827    /// with the parity of the whole quotient — `10` is even, so the last
828    /// digit carries it. Guards the `q_is_odd` -> `q_mod_10` migration at
829    /// multi-digit quotients, which the single-digit ties above miss.
830    #[test]
831    fn half_to_even_parity_holds_past_one_digit() {
832        let m = RoundingMode::HalfToEven;
833        assert_eq!(apply_rounding(105, 10, m), 10); // 10.5 -> 10 (even)
834        assert_eq!(apply_rounding(125, 10, m), 12); // 12.5 -> 12 (even)
835        assert_eq!(apply_rounding(135, 10, m), 14); // 13.5 -> 14
836        assert_eq!(apply_rounding(195, 10, m), 20); // 19.5 -> 20
837        assert_eq!(apply_rounding(-105, 10, m), -10);
838        assert_eq!(apply_rounding(-135, 10, m), -14);
839    }
840
841    /// The `f64` siblings of the two new modes must agree with the
842    /// integer path on the same values.
843    #[test]
844    fn f64_helpers_match_the_new_modes() {
845        assert_eq!(away_from_zero_f64(0.1), 1.0);
846        assert_eq!(away_from_zero_f64(1.9), 2.0);
847        assert_eq!(away_from_zero_f64(-0.1), -1.0);
848        assert_eq!(away_from_zero_f64(-1.9), -2.0);
849        assert_eq!(away_from_zero_f64(2.0), 2.0);
850        assert_eq!(away_from_zero_f64(-2.0), -2.0);
851
852        assert_eq!(zero_five_up_f64(0.7), 1.0); // last digit 0
853        assert_eq!(zero_five_up_f64(5.7), 6.0); // last digit 5
854        assert_eq!(zero_five_up_f64(4.7), 4.0);
855        assert_eq!(zero_five_up_f64(6.7), 6.0);
856        assert_eq!(zero_five_up_f64(10.7), 11.0);
857        assert_eq!(zero_five_up_f64(-0.7), -1.0);
858        assert_eq!(zero_five_up_f64(-5.7), -6.0);
859        assert_eq!(zero_five_up_f64(-4.7), -4.0);
860        assert_eq!(zero_five_up_f64(5.0), 5.0); // exact: no bump
861    }
862
863    /// The tiny-argument analytic helpers under the two new modes. The
864    /// compressing band's true value sits in `(|raw| - 1, |raw|)`, the
865    /// expanding band's in `(|raw|, |raw| + 1)`.
866    #[test]
867    fn tiny_odd_helpers_carry_the_new_modes() {
868        let away = RoundingMode::AwayFromZero;
869        let zfu = RoundingMode::ZeroFiveUp;
870
871        // Compressing: toward-zero result is |raw| - 1.
872        // raw = 7 -> toward-zero 6, last digit 6 -> no bump.
873        assert_eq!(tiny_odd_compressing_directed(7_i128, 0, 1, 7, zfu), 6);
874        // raw = 6 -> toward-zero 5, last digit 5 -> bump to 6.
875        assert_eq!(tiny_odd_compressing_directed(6_i128, 0, 1, 6, zfu), 6);
876        // raw = 1 -> toward-zero 0, last digit 0 -> bump to 1.
877        assert_eq!(tiny_odd_compressing_directed(1_i128, 0, 1, 1, zfu), 1);
878        assert_eq!(tiny_odd_compressing_directed(7_i128, 0, 1, 7, away), 7);
879        assert_eq!(tiny_odd_compressing_directed(-7_i128, 0, 1, 7, away), -7);
880        assert_eq!(tiny_odd_compressing_directed(-7_i128, 0, 1, 7, zfu), -6);
881        assert_eq!(tiny_odd_compressing_directed(-6_i128, 0, 1, 6, zfu), -6);
882
883        // Expanding: toward-zero result is |raw| itself.
884        assert_eq!(tiny_odd_expanding_directed(7_i128, 0, 1, 7, away), 8);
885        assert_eq!(tiny_odd_expanding_directed(-7_i128, 0, 1, 7, away), -8);
886        assert_eq!(tiny_odd_expanding_directed(7_i128, 0, 1, 7, zfu), 7);
887        assert_eq!(tiny_odd_expanding_directed(5_i128, 0, 1, 5, zfu), 6);
888        assert_eq!(tiny_odd_expanding_directed(10_i128, 0, 1, 0, zfu), 11);
889        assert_eq!(tiny_odd_expanding_directed(-5_i128, 0, 1, 5, zfu), -6);
890
891        // Nearest modes are untouched by the new arms.
892        for m in [
893            RoundingMode::HalfToEven,
894            RoundingMode::HalfAwayFromZero,
895            RoundingMode::HalfTowardZero,
896        ] {
897            assert_eq!(tiny_odd_compressing_directed(7_i128, 0, 1, 7, m), 7, "{m:?}");
898            assert_eq!(tiny_odd_expanding_directed(7_i128, 0, 1, 7, m), 7, "{m:?}");
899        }
900    }
901
902    /// `limbs_mod_10` must agree with `u128 % 10` across the limb
903    /// boundary, which is where the `2^64 ≡ 6 (mod 10)` fold earns its
904    /// keep — a single-limb read would answer for the low limb only.
905    #[test]
906    fn limbs_mod_10_matches_u128_remainder() {
907        for v in [
908            0_u128,
909            1,
910            5,
911            9,
912            10,
913            1234567890,
914            u64::MAX as u128,
915            (u64::MAX as u128) + 1,
916            (u64::MAX as u128) + 5,
917            1_u128 << 64,
918            (1_u128 << 64) + 7,
919            (3_u128 << 64) + 4,
920            u128::MAX,
921            u128::MAX - 3,
922        ] {
923            let limbs = [v as u64, (v >> 64) as u64];
924            assert_eq!(
925                u32::from(limbs_mod_10(&limbs)),
926                (v % 10) as u32,
927                "value {v}"
928            );
929        }
930        // A one-limb slice and the empty (zero) slice.
931        assert_eq!(limbs_mod_10(&[47]), 7);
932        assert_eq!(limbs_mod_10(&[]), 0);
933        // Three limbs: 2^128 mod 10 = 6, so the top limb folds in too.
934        assert_eq!(limbs_mod_10(&[0, 0, 1]), 6);
935        assert_eq!(limbs_mod_10(&[0, 1, 0]), 6);
936        assert_eq!(limbs_mod_10(&[0, 1, 1]), 2);
937    }
938
939    /// Both new modes are DIRECTED: they must not be granted the
940    /// sub-LSB linear-approximation short circuit.
941    #[test]
942    fn new_modes_are_not_nearest() {
943        assert!(!is_nearest_mode(RoundingMode::AwayFromZero));
944        assert!(!is_nearest_mode(RoundingMode::ZeroFiveUp));
945    }
946}