Skip to content

Commit 49595ae

Browse files
committed
Add rounding module
2 parents 91c78d3 + 04d6ec0 commit 49595ae

File tree

2 files changed

+368
-0
lines changed

2 files changed

+368
-0
lines changed

src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,7 @@ mod macros;
9090
extern crate paste;
9191

9292
mod parsing;
93+
pub mod rounding;
9394

9495
#[inline(always)]
9596
fn ten_to_the(pow: u64) -> BigInt {

src/rounding.rs

Lines changed: 367 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,367 @@
1+
//! Rounding structures and subroutines
2+
3+
use crate::Sign;
4+
use stdlib;
5+
6+
/// Determines how to calculate the last digit of the number
7+
///
8+
/// Default rounding mode is HalfUp
9+
///
10+
#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq)]
11+
pub enum RoundingMode {
12+
/// Always round away from zero
13+
///
14+
///
15+
/// * 5.5 → 6.0
16+
/// * 2.5 → 3.0
17+
/// * 1.6 → 2.0
18+
/// * 1.1 → 2.0
19+
/// * -1.1 → -2.0
20+
/// * -1.6 → -2.0
21+
/// * -2.5 → -3.0
22+
/// * -5.5 → -6.0
23+
Up,
24+
25+
/// Always round towards zero
26+
///
27+
/// * 5.5 → 5.0
28+
/// * 2.5 → 2.0
29+
/// * 1.6 → 1.0
30+
/// * 1.1 → 1.0
31+
/// * -1.1 → -1.0
32+
/// * -1.6 → -1.0
33+
/// * -2.5 → -2.0
34+
/// * -5.5 → -5.0
35+
Down,
36+
37+
/// Towards +∞
38+
///
39+
/// * 5.5 → 6.0
40+
/// * 2.5 → 3.0
41+
/// * 1.6 → 2.0
42+
/// * 1.1 → 2.0
43+
/// * -1.1 → -1.0
44+
/// * -1.6 → -1.0
45+
/// * -2.5 → -2.0
46+
/// * -5.5 → -5.0
47+
Ceiling,
48+
49+
/// Towards -∞
50+
///
51+
/// * 5.5 → 5.0
52+
/// * 2.5 → 2.0
53+
/// * 1.6 → 1.0
54+
/// * 1.1 → 1.0
55+
/// * -1.1 → -2.0
56+
/// * -1.6 → -2.0
57+
/// * -2.5 → -3.0
58+
/// * -5.5 → -6.0
59+
Floor,
60+
61+
/// Round to 'nearest neighbor', or up if ending decimal is 5
62+
///
63+
/// * 5.5 → 6.0
64+
/// * 2.5 → 3.0
65+
/// * 1.6 → 2.0
66+
/// * 1.1 → 1.0
67+
/// * -1.1 → -1.0
68+
/// * -1.6 → -2.0
69+
/// * -2.5 → -3.0
70+
/// * -5.5 → -6.0
71+
HalfUp,
72+
73+
/// Round to 'nearest neighbor', or down if ending decimal is 5
74+
///
75+
/// * 5.5 → 5.0
76+
/// * 2.5 → 2.0
77+
/// * 1.6 → 2.0
78+
/// * 1.1 → 1.0
79+
/// * -1.1 → -1.0
80+
/// * -1.6 → -2.0
81+
/// * -2.5 → -2.0
82+
/// * -5.5 → -5.0
83+
HalfDown,
84+
85+
/// Round to 'nearest neighbor', if equidistant, round towards
86+
/// nearest even digit
87+
///
88+
/// * 5.5 → 6.0
89+
/// * 2.5 → 2.0
90+
/// * 1.6 → 2.0
91+
/// * 1.1 → 1.0
92+
/// * -1.1 → -1.0
93+
/// * -1.6 → -2.0
94+
/// * -2.5 → -2.0
95+
/// * -5.5 → -6.0
96+
///
97+
HalfEven,
98+
}
99+
100+
101+
impl RoundingMode {
102+
/// Perform the rounding operation
103+
///
104+
/// Parameters
105+
/// ----------
106+
/// * sign (Sign) - Sign of the number to be rounded
107+
/// * pair (u8, u8) - The two digits in question to be rounded.
108+
/// i.e. to round 0.345 to two places, you would pass (4, 5).
109+
/// As decimal digits, they
110+
/// must be less than ten!
111+
/// * trailing_zeros (bool) - True if all digits after the pair are zero.
112+
/// This has an effect if the right hand digit is 0 or 5.
113+
///
114+
/// Returns
115+
/// -------
116+
/// Returns the first number of the pair, rounded. The sign is not preserved.
117+
///
118+
/// Examples
119+
/// --------
120+
/// - To round 2341, pass in `Plus, (4, 1), true` → get 4 or 5 depending on scheme
121+
/// - To round -0.1051, to two places: `Minus, (0, 5), false` → returns either 0 or 1
122+
/// - To round -0.1, pass in `true, (0, 1)` → returns either 0 or 1
123+
///
124+
/// Calculation of pair of digits from full number, and the replacement of that number
125+
/// should be handled separately
126+
///
127+
pub fn round_pair(&self, sign: Sign, pair: (u8, u8), trailing_zeros: bool) -> u8 {
128+
use self::RoundingMode::*;
129+
use stdlib::cmp::Ordering::*;
130+
131+
let (lhs, rhs) = pair;
132+
// if all zero after digit, never round
133+
if rhs == 0 && trailing_zeros {
134+
return lhs;
135+
}
136+
let up = lhs + 1;
137+
let down = lhs;
138+
match (*self, rhs.cmp(&5)) {
139+
(Up, _) => up,
140+
(Down, _) => down,
141+
(Floor, _) => if sign == Sign::Minus { up } else { down },
142+
(Ceiling, _) => if sign == Sign::Minus { down } else { up },
143+
(_, Less) => down,
144+
(_, Greater) => up,
145+
(_, Equal) if !trailing_zeros => up,
146+
(HalfUp, Equal) => up,
147+
(HalfDown, Equal) => down,
148+
(HalfEven, Equal) => if lhs % 2 == 0 { down } else { up },
149+
}
150+
}
151+
152+
/// Round value at particular digit, returning replacement digit
153+
///
154+
/// Parameters
155+
/// ----------
156+
/// * at_digit (NonZeroU8) - 0-based index of digit at which to round.
157+
/// 0 would be the first digit, and would
158+
///
159+
/// * sign (Sign) - Sign of the number to be rounded
160+
/// * value (u32) - The number containing digits to be rounded.
161+
/// * trailing_zeros (bool) - True if all digits after the value are zero.
162+
///
163+
/// Returns
164+
/// -------
165+
/// Returns the first number of the pair, rounded. The sign is not preserved.
166+
///
167+
/// Examples
168+
/// --------
169+
/// - To round 823418, at digit-index 3: `3, Plus, 823418, true` → 823000 or 824000, depending on scheme
170+
/// - To round -100205, at digit-index 1: `1, Minus, 100205, true` → 100200 or 100210
171+
///
172+
/// Calculation of pair of digits from full number, and the replacement of that number
173+
/// should be handled separately
174+
///
175+
pub fn round_u32(&self, at_digit: stdlib::num::NonZeroU8, sign: Sign, value: u32, trailing_zeros: bool) -> u32 {
176+
let shift = 10u32.pow(at_digit.get() as u32 - 1);
177+
let splitter = shift * 10;
178+
179+
// split 'value' into high and low
180+
let (top, bottom) = num_integer::div_rem(value, splitter);
181+
let lhs = (top % 10) as u8;
182+
let (rhs, remainder) = num_integer::div_rem(bottom, shift);
183+
let pair = (lhs, rhs as u8);
184+
let rounded = self.round_pair(sign, pair, trailing_zeros && remainder == 0);
185+
186+
// replace low digit with rounded value
187+
let full = top - lhs as u32 + rounded as u32;
188+
189+
// shift rounded value back to position
190+
full * splitter
191+
}
192+
}
193+
194+
195+
#[cfg(test)]
196+
#[allow(non_snake_case)]
197+
mod test_round_pair {
198+
use paste::paste;
199+
use super::*;
200+
201+
macro_rules! impl_test {
202+
( $($mode:ident),+ => $expected:literal) => {
203+
$(
204+
paste! {
205+
#[test]
206+
fn [< mode_ $mode >]() {
207+
let (pair, sign, trailing_zeros) = test_input();
208+
let mode = self::RoundingMode::$mode;
209+
let result = mode.round_pair(sign, pair, trailing_zeros);
210+
assert_eq!(result, $expected);
211+
}
212+
}
213+
)*
214+
}
215+
}
216+
217+
macro_rules! define_test_input {
218+
( - $lhs:literal, $rhs:literal $($t:tt)* ) => {
219+
define_test_input!(sign=Sign::Minus, pair=($lhs, $rhs) $($t)*);
220+
};
221+
( $lhs:literal, $rhs:literal $($t:tt)*) => {
222+
define_test_input!(sign=Sign::Plus, pair=($lhs, $rhs) $($t)*);
223+
};
224+
( sign=$sign:expr, $lhs:literal, $rhs:literal $($t:tt)* ) => {
225+
define_test_input!(sign=$sign, pair=($lhs, $rhs) $($t)*);
226+
};
227+
( sign=$sign:expr, pair=$pair:expr) => {
228+
define_test_input!(sign=$sign, pair=$pair, trailing_zeros=false);
229+
};
230+
( sign=$sign:expr, pair=$pair:expr, 000...) => {
231+
define_test_input!(sign=$sign, pair=$pair, trailing_zeros=true);
232+
};
233+
( sign=$sign:expr, pair=$pair:expr, trailing_zeros=$trailing_zeros:literal) => {
234+
fn test_input() -> ((u8, u8), Sign, bool) { ($pair, $sign, $trailing_zeros) }
235+
};
236+
}
237+
238+
mod case_0_1 {
239+
use super::*;
240+
241+
define_test_input!( 0, 1 );
242+
243+
impl_test!(Up, Ceiling => 1);
244+
impl_test!(Down, Floor, HalfUp, HalfDown, HalfEven => 0);
245+
}
246+
247+
mod case_9_5 {
248+
use super::*;
249+
250+
define_test_input!( 9, 5 );
251+
252+
impl_test!(Up, Ceiling, HalfDown, HalfUp, HalfEven => 10);
253+
impl_test!(Down, Floor => 9);
254+
}
255+
256+
mod case_9_5_0000 {
257+
use super::*;
258+
259+
define_test_input!( 9, 5, 000...);
260+
261+
impl_test!(Up, Ceiling, HalfUp, HalfEven => 10);
262+
impl_test!(Down, Floor, HalfDown => 9);
263+
}
264+
265+
mod case_8_5_0000 {
266+
use super::*;
267+
268+
define_test_input!( 8, 5, 000...);
269+
270+
impl_test!(Up, Ceiling, HalfUp => 9);
271+
impl_test!(Down, Floor, HalfDown, HalfEven => 8);
272+
}
273+
274+
mod case_neg_4_3 {
275+
use super::*;
276+
277+
define_test_input!(- 4, 3 );
278+
279+
impl_test!(Up, Floor => 5);
280+
impl_test!(Down, Ceiling, HalfUp, HalfDown, HalfEven => 4);
281+
}
282+
283+
mod case_neg_6_5_000 {
284+
use super::*;
285+
286+
define_test_input!( -6, 5, 000... );
287+
288+
impl_test!(Up, Floor, HalfUp => 7);
289+
impl_test!(Down, Ceiling, HalfDown, HalfEven => 6);
290+
}
291+
292+
mod case_neg_2_0_000 {
293+
use super::*;
294+
295+
define_test_input!( -2, 0, 000... );
296+
297+
impl_test!(Up, Down, Ceiling, Floor, HalfUp, HalfDown, HalfEven => 2);
298+
}
299+
}
300+
301+
302+
#[cfg(test)]
303+
#[allow(non_snake_case)]
304+
mod test_round_u32 {
305+
use paste::paste;
306+
use super::*;
307+
308+
macro_rules! impl_test {
309+
( $pos:literal :: $($mode:ident),+ => $expected:literal) => {
310+
$(
311+
paste! {
312+
#[test]
313+
fn [< digit_ $pos _mode_ $mode >]() {
314+
let (value, sign, trailing_zeros) = test_input();
315+
let mode = self::RoundingMode::$mode;
316+
let pos = stdlib::num::NonZeroU8::new($pos as u8).unwrap();
317+
let result = mode.round_u32(pos, sign, value, trailing_zeros);
318+
assert_eq!(result, $expected);
319+
}
320+
}
321+
)*
322+
}
323+
}
324+
325+
macro_rules! define_test_input {
326+
( - $value:literal $($t:tt)* ) => {
327+
define_test_input!(sign=Sign::Minus, value=$value $($t)*);
328+
};
329+
( $value:literal $($t:tt)* ) => {
330+
define_test_input!(sign=Sign::Plus, value=$value $($t)*);
331+
};
332+
( sign=$sign:expr, value=$value:literal ) => {
333+
define_test_input!(sign=$sign, value=$value, trailing_zeros=false);
334+
};
335+
( sign=$sign:expr, value=$value:literal 000... ) => {
336+
define_test_input!(sign=$sign, value=$value, trailing_zeros=true);
337+
};
338+
( sign=$sign:expr, value=$value:expr, trailing_zeros=$trailing_zeros:literal ) => {
339+
fn test_input() -> (u32, Sign, bool) { ($value, $sign, $trailing_zeros) }
340+
};
341+
}
342+
343+
mod case_13950000_000 {
344+
use super::*;
345+
346+
define_test_input!( 13950000 000... );
347+
348+
impl_test!(3 :: Up => 13950000);
349+
impl_test!(5 :: Up, Ceiling, HalfUp, HalfEven => 14000000);
350+
impl_test!(5 :: Down, HalfDown => 13900000);
351+
}
352+
353+
mod case_neg_35488622 {
354+
use super::*;
355+
356+
define_test_input!( - 35488622 );
357+
358+
impl_test!(1 :: Up => 35488630);
359+
impl_test!(1 :: Down => 35488620);
360+
impl_test!(2 :: Up => 35488700);
361+
impl_test!(2 :: Down => 35488600);
362+
impl_test!(7 :: Up, Floor => 40000000);
363+
impl_test!(7 :: Down, Ceiling => 30000000);
364+
impl_test!(8 :: Up => 100000000);
365+
impl_test!(8 :: Down => 0);
366+
}
367+
}

0 commit comments

Comments
 (0)