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}