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: 2023-05-12T15:46:50+02: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        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 = match mode {
114        None => RoundingMode::default(),
115        Some(mode) => mode,
116    };
117    match mode {
118        RoundingMode::Round05Up => {
119            // Round down unless last digit is 0 or 5:
120            // quotient not negativ and quotient divisible by 5 w/o remainder
121            // or quotient negativ and (quotient + 1) not
122            // divisible by 5 w/o rem. => add 1
123            if quot >= 0 && quot % 5 == 0 || quot < 0 && (quot + 1) % 5 != 0 {
124                return quot + 1;
125            }
126        }
127        RoundingMode::RoundCeiling => {
128            // Round towards Infinity (i. e. not away from 0 if negative):
129            // => always add 1
130            return quot + 1;
131        }
132        RoundingMode::RoundDown => {
133            // Round towards 0 (aka truncate):
134            // quotient negativ => add 1
135            if quot < 0 {
136                return quot + 1;
137            }
138        }
139        RoundingMode::RoundFloor => {
140            // Round towards -Infinity (i.e. not towards 0 if negative):
141            // => never add 1
142            return quot;
143        }
144        RoundingMode::RoundHalfDown => {
145            // Round 5 down, rest to nearest:
146            // remainder > |divisor| / 2 or
147            // remainder = |divisor| / 2 and quotient < 0
148            // => add 1
149            let rem_doubled = rem << 1;
150            if rem_doubled > divisor || rem_doubled == divisor && quot < 0 {
151                return quot + 1;
152            }
153        }
154        RoundingMode::RoundHalfEven => {
155            // Round 5 to nearest even, rest to nearest:
156            // remainder > |divisor| / 2 or
157            // remainder = |divisor| / 2 and quotient not even
158            // => add 1
159            let rem_doubled = rem << 1;
160            if rem_doubled > divisor
161                || rem_doubled == divisor && quot % 2 != 0
162            {
163                return quot + 1;
164            }
165        }
166        RoundingMode::RoundHalfUp => {
167            // Round 5 up (away from 0), rest to nearest:
168            // remainder > |divisor| / 2 or
169            // remainder = |divisor| / 2 and quotient >= 0
170            // => add 1
171            let rem_doubled = rem << 1;
172            if rem_doubled > divisor || rem_doubled == divisor && quot >= 0 {
173                return quot + 1;
174            }
175        }
176        RoundingMode::RoundUp => {
177            // Round away from 0:
178            // quotient not negative => add 1
179            if quot >= 0 {
180                return quot + 1;
181            }
182        }
183    }
184    // fall-through: round towards 0
185    quot
186}
187
188/// Divide 'divident' by 'divisor' and round result according to 'mode'.
189#[doc(hidden)]
190#[must_use]
191pub fn i128_div_rounded(
192    mut divident: i128,
193    mut divisor: i128,
194    mode: Option<RoundingMode>,
195) -> i128 {
196    if divisor < 0 {
197        divident = -divident;
198        divisor = -divisor;
199    }
200    let (quot, rem) = i128_div_mod_floor(divident, divisor);
201    // div_mod_floor with divisor > 0 => rem >= 0
202    round_quot(quot, rem as u128, divisor as u128, mode)
203}
204
205/// Divide 'divident * 10^p' by 'divisor' and round result according to
206/// 'mode'.
207#[doc(hidden)]
208#[must_use]
209pub fn i128_shifted_div_rounded(
210    mut divident: i128,
211    p: u8,
212    mut divisor: i128,
213    mode: Option<RoundingMode>,
214) -> Option<i128> {
215    if divisor < 0 {
216        divident = -divident;
217        divisor = -divisor;
218    }
219    let (quot, rem) = i128_shifted_div_mod_floor(divident, p, divisor)?;
220    // div_mod_floor with divisor > 0 => rem >= 0
221    Some(round_quot(quot, rem as u128, divisor as u128, mode))
222}
223
224/// Divide 'x * y' by '10^p' and round result according to 'mode'.
225#[doc(hidden)]
226#[must_use]
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}