Skip to main content

fpdec_core/
rounding.rs

1// ---------------------------------------------------------------------------
2// Copyright:   (c) 2022 ff. Michael Amrhein (michael@adrhinum.de)
3// License:     This program is part of a larger application. For license
4//              details please read the file LICENSE.TXT provided together
5//              with the application.
6// ---------------------------------------------------------------------------
7// $Source: fpdec-core/src/rounding.rs $
8// $Revision: 2026-02-03T10:35:41+01:00 $
9
10#[cfg(feature = "std")]
11use core::cell::RefCell;
12
13use crate::{
14    i128_div_mod_floor, i128_shifted_div_mod_floor, i256_div_mod_floor,
15    ten_pow,
16};
17
18/// Enum representing the different methods used when rounding a number.
19#[derive(Clone, Copy, Debug, Eq, PartialEq)]
20pub enum RoundingMode {
21    /// Round away from zero if last digit after rounding towards zero would
22    /// have been 0 or 5; otherwise round towards zero.
23    Round05Up,
24    /// Round towards Infinity.
25    RoundCeiling,
26    /// Round towards zero.
27    RoundDown,
28    /// Round towards -Infinity.
29    RoundFloor,
30    /// Round to nearest with ties going towards zero.
31    RoundHalfDown,
32    /// Round to nearest with ties going to nearest even integer.
33    RoundHalfEven,
34    /// Round to nearest with ties going away from zero.
35    RoundHalfUp,
36    /// Round away from zero.
37    RoundUp,
38}
39
40#[cfg(feature = "std")]
41thread_local!(
42    static DFLT_ROUNDING_MODE: RefCell<RoundingMode> =
43        const { RefCell::new(RoundingMode::RoundHalfEven) }
44);
45
46#[cfg(feature = "std")]
47impl Default for RoundingMode {
48    /// Returns the default RoundingMode set for the current thread.
49    ///
50    /// It is initially set to [RoundingMode::RoundHalfEven], but can be
51    /// changed using the fn [RoundingMode::set_default].
52    fn default() -> Self {
53        DFLT_ROUNDING_MODE.with(|m| *m.borrow())
54    }
55}
56
57#[cfg(feature = "std")]
58impl RoundingMode {
59    /// Sets the default RoundingMode for the current thread.
60    pub fn set_default(mode: Self) {
61        DFLT_ROUNDING_MODE.with(|m| *m.borrow_mut() = mode);
62    }
63}
64
65#[cfg(not(feature = "std"))]
66static DFLT_ROUNDING_MODE: RoundingMode = RoundingMode::RoundHalfEven;
67
68#[cfg(not(feature = "std"))]
69impl Default for RoundingMode {
70    /// Returns the current default RoundingMode.
71    ///
72    /// It is initially set to [RoundingMode::RoundHalfEven], but can be
73    /// changed using the fn [RoundingMode::set_default].
74    fn default() -> Self {
75        DFLT_ROUNDING_MODE
76    }
77}
78
79/// Rounding a number to a given number of fractional digits.
80pub trait Round
81where
82    Self: Sized,
83{
84    /// Returns a new `Self` instance with its value rounded to
85    /// `n_frac_digits` fractional digits according to the current
86    /// [RoundingMode].
87    fn round(self, n_frac_digits: i8) -> Self;
88
89    /// Returns a new `Self` instance with its value rounded to
90    /// `n_frac_digits` fractional digits according to the current
91    /// `RoundingMode`, wrapped in `Option::Some`, or `Option::None` if
92    /// the result can not be represented by `Self`.
93    fn checked_round(self, n_frac_digits: i8) -> Option<Self>;
94}
95
96// rounding helper
97
98// Round `quot` according to `mode` based on `rem` and `divisor`.
99// Pre-condition: 0 < divisor and rem <= divisor
100#[inline]
101fn round_quot(
102    quot: i128,
103    rem: u128,
104    divisor: u128,
105    mode: Option<RoundingMode>,
106) -> i128 {
107    if rem == 0 {
108        // no need for rounding
109        return quot;
110    }
111    // here: |divisor| >= 2 => rem <= |divident| / 2,
112    // therefor it's safe to use rem << 1
113    let mode = mode.unwrap_or_default();
114    match mode {
115        RoundingMode::Round05Up => {
116            // Round down unless last digit is 0 or 5:
117            // quotient not negativ and quotient divisible by 5 w/o remainder
118            // or quotient negativ and (quotient + 1) not
119            // divisible by 5 w/o rem. => add 1
120            if quot >= 0 && quot % 5 == 0 || quot < 0 && (quot + 1) % 5 != 0 {
121                return quot + 1;
122            }
123        }
124        RoundingMode::RoundCeiling => {
125            // Round towards Infinity (i. e. not away from 0 if negative):
126            // => always add 1
127            return quot + 1;
128        }
129        RoundingMode::RoundDown => {
130            // Round towards 0 (aka truncate):
131            // quotient negativ => add 1
132            if quot < 0 {
133                return quot + 1;
134            }
135        }
136        RoundingMode::RoundFloor => {
137            // Round towards -Infinity (i.e. not towards 0 if negative):
138            // => never add 1
139            return quot;
140        }
141        RoundingMode::RoundHalfDown => {
142            // Round 5 down, rest to nearest:
143            // remainder > |divisor| / 2 or
144            // remainder = |divisor| / 2 and quotient < 0
145            // => add 1
146            let rem_doubled = rem << 1;
147            if rem_doubled > divisor || rem_doubled == divisor && quot < 0 {
148                return quot + 1;
149            }
150        }
151        RoundingMode::RoundHalfEven => {
152            // Round 5 to nearest even, rest to nearest:
153            // remainder > |divisor| / 2 or
154            // remainder = |divisor| / 2 and quotient not even
155            // => add 1
156            let rem_doubled = rem << 1;
157            if rem_doubled > divisor
158                || rem_doubled == divisor && quot % 2 != 0
159            {
160                return quot + 1;
161            }
162        }
163        RoundingMode::RoundHalfUp => {
164            // Round 5 up (away from 0), rest to nearest:
165            // remainder > |divisor| / 2 or
166            // remainder = |divisor| / 2 and quotient >= 0
167            // => add 1
168            let rem_doubled = rem << 1;
169            if rem_doubled > divisor || rem_doubled == divisor && quot >= 0 {
170                return quot + 1;
171            }
172        }
173        RoundingMode::RoundUp => {
174            // Round away from 0:
175            // quotient not negative => add 1
176            if quot >= 0 {
177                return quot + 1;
178            }
179        }
180    }
181    // fall-through: round towards 0
182    quot
183}
184
185/// Divide 'divident' by 'divisor' and round result according to 'mode'.
186#[doc(hidden)]
187#[must_use]
188#[allow(clippy::cast_sign_loss)]
189pub fn i128_div_rounded(
190    mut divident: i128,
191    mut divisor: i128,
192    mode: Option<RoundingMode>,
193) -> i128 {
194    if divisor < 0 {
195        divident = -divident;
196        divisor = -divisor;
197    }
198    let (quot, rem) = i128_div_mod_floor(divident, divisor);
199    // div_mod_floor with divisor > 0 => rem >= 0
200    round_quot(quot, rem as u128, divisor as u128, mode)
201}
202
203/// Divide 'divident * 10^p' by 'divisor' and round result according to
204/// 'mode'.
205#[doc(hidden)]
206#[must_use]
207#[allow(clippy::cast_sign_loss)]
208pub fn i128_shifted_div_rounded(
209    mut divident: i128,
210    p: u8,
211    mut divisor: i128,
212    mode: Option<RoundingMode>,
213) -> Option<i128> {
214    if divisor < 0 {
215        divident = -divident;
216        divisor = -divisor;
217    }
218    let (quot, rem) = i128_shifted_div_mod_floor(divident, p, divisor)?;
219    // div_mod_floor with divisor > 0 => rem >= 0
220    Some(round_quot(quot, rem as u128, divisor as u128, mode))
221}
222
223/// Divide 'x * y' by '10^p' and round result according to 'mode'.
224#[doc(hidden)]
225#[must_use]
226#[allow(clippy::cast_sign_loss)]
227pub fn i128_mul_div_ten_pow_rounded(
228    x: i128,
229    y: i128,
230    p: u8,
231    mode: Option<RoundingMode>,
232) -> Option<i128> {
233    let divisor = ten_pow(p);
234    let (quot, rem) = i256_div_mod_floor(x, y, divisor)?;
235    // div_mod_floor with divisor > 0 => rem >= 0
236    Some(round_quot(quot, rem as u128, divisor as u128, mode))
237}
238
239#[cfg(feature = "std")]
240#[cfg(test)]
241mod rounding_mode_tests {
242    use super::*;
243
244    #[test]
245    fn test1() {
246        assert_eq!(RoundingMode::default(), RoundingMode::RoundHalfEven);
247        RoundingMode::set_default(RoundingMode::RoundUp);
248        assert_eq!(RoundingMode::default(), RoundingMode::RoundUp);
249        RoundingMode::set_default(RoundingMode::RoundHalfEven);
250        assert_eq!(RoundingMode::default(), RoundingMode::RoundHalfEven);
251    }
252
253    #[test]
254    fn test2() {
255        assert_eq!(RoundingMode::default(), RoundingMode::RoundHalfEven);
256        RoundingMode::set_default(RoundingMode::RoundHalfUp);
257        assert_eq!(RoundingMode::default(), RoundingMode::RoundHalfUp);
258        RoundingMode::set_default(RoundingMode::RoundHalfEven);
259        assert_eq!(RoundingMode::default(), RoundingMode::RoundHalfEven);
260    }
261}
262
263#[cfg(test)]
264mod helper_tests {
265    use super::*;
266
267    const TESTDATA: [(i128, i128, RoundingMode, i128); 34] = [
268        (17, 5, RoundingMode::Round05Up, 3),
269        (27, 5, RoundingMode::Round05Up, 6),
270        (-17, 5, RoundingMode::Round05Up, -3),
271        (-27, 5, RoundingMode::Round05Up, -6),
272        (17, 5, RoundingMode::RoundCeiling, 4),
273        (15, 5, RoundingMode::RoundCeiling, 3),
274        (-17, 5, RoundingMode::RoundCeiling, -3),
275        (-15, 5, RoundingMode::RoundCeiling, -3),
276        (19, 5, RoundingMode::RoundDown, 3),
277        (15, 5, RoundingMode::RoundDown, 3),
278        (-18, 5, RoundingMode::RoundDown, -3),
279        (-15, 5, RoundingMode::RoundDown, -3),
280        (19, 5, RoundingMode::RoundFloor, 3),
281        (15, 5, RoundingMode::RoundFloor, 3),
282        (-18, 5, RoundingMode::RoundFloor, -4),
283        (-15, 5, RoundingMode::RoundFloor, -3),
284        (19, 2, RoundingMode::RoundHalfDown, 9),
285        (15, 4, RoundingMode::RoundHalfDown, 4),
286        (-19, 2, RoundingMode::RoundHalfDown, -9),
287        (-15, 4, RoundingMode::RoundHalfDown, -4),
288        (19, 2, RoundingMode::RoundHalfEven, 10),
289        (15, 4, RoundingMode::RoundHalfEven, 4),
290        (-225, 50, RoundingMode::RoundHalfEven, -4),
291        (-15, 4, RoundingMode::RoundHalfEven, -4),
292        (
293            u64::MAX as i128,
294            i64::MIN as i128 * 10,
295            RoundingMode::RoundHalfEven,
296            0,
297        ),
298        (19, 2, RoundingMode::RoundHalfUp, 10),
299        (10802, 4321, RoundingMode::RoundHalfUp, 2),
300        (-19, 2, RoundingMode::RoundHalfUp, -10),
301        (-10802, 4321, RoundingMode::RoundHalfUp, -2),
302        (19, 2, RoundingMode::RoundUp, 10),
303        (10802, 4321, RoundingMode::RoundUp, 3),
304        (-19, 2, RoundingMode::RoundUp, -10),
305        (-10802, 4321, RoundingMode::RoundUp, -3),
306        (i32::MAX as i128, 1, RoundingMode::RoundUp, i32::MAX as i128),
307    ];
308
309    #[test]
310    fn test_div_rounded() {
311        for (divident, divisor, rnd_mode, result) in TESTDATA {
312            let quot = i128_div_rounded(divident, divisor, Some(rnd_mode));
313            assert_eq!(quot, result);
314        }
315    }
316}