Skip to content

Commit

Permalink
Rename increment to coarseness
Browse files Browse the repository at this point in the history
  • Loading branch information
jedel1043 committed Oct 25, 2023
1 parent 08c6454 commit dba796b
Show file tree
Hide file tree
Showing 2 changed files with 116 additions and 103 deletions.
217 changes: 115 additions & 102 deletions utils/fixed_decimal/src/decimal.rs
Original file line number Diff line number Diff line change
Expand Up @@ -159,21 +159,23 @@ pub enum SignDisplay {
Negative,
}

/// Increment value for rounding operations.
/// Coarseness in a rounding operation.
///
/// Forces a rounding operation to round to only multiples of the specified coarseness.
#[non_exhaustive]
#[derive(Debug, Eq, PartialEq, Clone, Copy)]
pub enum RoundingIncrement {
pub enum RoundingCoarseness {
/// Round the last digit to any digit (0-9).
///
/// This is the default rounding increment for all the methods that don't take a
/// `RoundingIncrement` as an argument.
I1,
/// This is the default rounding coarseness for all the methods that don't take a
/// `RoundingCoarseness` as an argument.
MultiplesOf1,
/// Round the last digit to only multiples of two (0, 2, 4, 6, 8).
I2,
MultiplesOf2,
/// Round the last digit to only multiples of five (0, 5).
I5,
MultiplesOf5,
/// Round the last two digits to only multiples of twenty-five (0, 25, 50, 75).
I25,
MultiplesOf25,
}

impl Default for FixedDecimal {
Expand Down Expand Up @@ -1006,11 +1008,11 @@ impl FixedDecimal {
/// assert_eq!("1", dec.to_string());
/// ```
pub fn trunc(&mut self, position: i16) {
self.trunc_with_increment(position, RoundingIncrement::I1)
self.trunc_coarse(position, RoundingCoarseness::MultiplesOf1)
}

/// Truncates the number on the right to a particular position and rounding increment,
/// deleting digits if necessary.
/// Truncates the number on the right to a particular position and rounding
/// coarseness, deleting digits if necessary.
///
/// # Examples
///
Expand All @@ -1034,7 +1036,7 @@ impl FixedDecimal {
/// dec.trunc_with_increment(-2, RoundingIncrement::I2);
/// assert_eq!("9.98", dec.to_string());
/// ```
pub fn trunc_with_increment(&mut self, position: i16, increment: RoundingIncrement) {
pub fn trunc_coarse(&mut self, position: i16, coarseness: RoundingCoarseness) {
// 1. Set upper and lower magnitude
self.lower_magnitude = cmp::min(position, 0);
if position == i16::MIN {
Expand All @@ -1051,21 +1053,94 @@ impl FixedDecimal {
// Don't exit if the increment is not `1`, because the algorithm could need to truncate the
// last non-zero digit.
if self.is_zero()
|| (magnitude < self.nonzero_magnitude_end() && increment == RoundingIncrement::I1)
|| (magnitude < self.nonzero_magnitude_end()
&& coarseness == RoundingCoarseness::MultiplesOf1)
{
#[cfg(debug_assertions)]
self.check_invariants();
return;
}

// 3. If the rounding position is *in the middle* of the nonzero digits

if magnitude < self.magnitude {
// 3a. Calculate the number of digits to retain and remove the rest
let digits_to_retain = crate::ops::i16_abs_sub(self.magnitude, magnitude);
self.digits.truncate(digits_to_retain as usize);
// 3b. Remove trailing zeros from self.digits to retain invariants
// Note: this does not affect visible trailing zeros,
// which is tracked by self.lower_magnitude
// 3-a. Calculate the number of digits to retain and remove the rest
let digits_to_retain = crate::ops::i16_abs_sub(self.magnitude, magnitude) as usize;
self.digits.truncate(digits_to_retain);

// 3-b. Truncate to the next coarseness.
'coarseness: {
match coarseness {
RoundingCoarseness::MultiplesOf1 => {
// No need to do more work, trailing zeroes are removed below.
}
RoundingCoarseness::MultiplesOf2 | RoundingCoarseness::MultiplesOf5 => {
if digits_to_retain != self.digits.len() {
// A trailing zero is always a multiple of any coarseness.
break 'coarseness;
}

let Some((last_digit, _)) = self.digits.split_last_mut() else {
// Unreachable
break 'coarseness;
};

let num = match coarseness {
RoundingCoarseness::MultiplesOf2 => {
// Equivalent to (n / 2) * 2, which truncates to the next
// multiple of two
*last_digit & 0xFE
}
RoundingCoarseness::MultiplesOf5 if *last_digit < 5 => 0,
RoundingCoarseness::MultiplesOf5 => 5,
_ => 0, // Unreachable
};

if num == 0 {
self.digits.pop();
} else {
*last_digit = num;
}
}
RoundingCoarseness::MultiplesOf25 => {
// Temporarily extend the digits to have the correct
// trailing digits. The remaining zeroes will be removed
// by the end of the algorithm.
self.digits.resize(digits_to_retain, 0);

let Some((last_digit, digits)) = self.digits.split_last_mut() else {
// Unreachable.
break 'coarseness;
};

let Some((second_last_digit, _)) = digits.split_last_mut() else {
// The number has no other digits aside from the last,
// making it strictly less than 25.
self.digits.pop();
break 'coarseness;
};

let number = *second_last_digit * 10 + *last_digit;

if number < 25 {
self.digits.truncate(self.digits.len() - 2);
} else if number < 50 {
*second_last_digit = 2;
*last_digit = 5;
} else if number < 75 {
*second_last_digit = 5;
// Last digit can just be removed since it will be zero.
self.digits.pop();
} else {
*second_last_digit = 7;
*last_digit = 5;
}
}
}
}

// 3-c. Handle the case where `digits` has leading zeros after
// truncating to the next coarseness.
let position_last_nonzero_digit = self
.digits
.iter()
Expand All @@ -1074,81 +1149,7 @@ impl FixedDecimal {
.unwrap_or(0);
self.digits.truncate(position_last_nonzero_digit);

// 3c. By the invariant, there should still be at least 1 nonzero digit
debug_assert!(!self.digits.is_empty());

let Some((last_digit, digits)) = self.digits.split_last_mut() else {
// Shouldn't be reachable anyways
#[cfg(debug_assertions)]
self.check_invariants();
return;
};

// 3d. Truncate to the next increment.
match increment {
RoundingIncrement::I1 => {
// skip additional work if the increment is 1, since
// it could be costly for the common case.
#[cfg(debug_assertions)]
self.check_invariants();
return;
}
RoundingIncrement::I2 => {
// Equivalent to (n / 2) * 2, which truncates to the next
// multiple of two
let num = *last_digit & 0xFE;

if num == 0 {
self.digits.pop();
} else {
*last_digit = num;
}
}
RoundingIncrement::I5 => {
if *last_digit < 5 {
self.digits.pop();
} else {
*last_digit = 5;
}
}
RoundingIncrement::I25 => {
let Some((second_last_digit, _)) = digits.split_last_mut() else {
// The number has no other digits aside from the last,
// making it strictly less than 25.
self.digits.clear();
self.magnitude = 0;

#[cfg(debug_assertions)]
self.check_invariants();

return;
};

let number = *second_last_digit * 10 + *last_digit;

if number < 25 {
self.digits.truncate(self.digits.len() - 2);
} else if number < 50 {
*second_last_digit = 2;
*last_digit = 5;
} else if number < 75 {
*second_last_digit = 5;
// Last digit can just be removed since it will be zero.
self.digits.pop();
} else {
*second_last_digit = 7;
*last_digit = 5;
}
}
}

// 3e. Handle the case where `digits` has leading zeros after
// truncating to the next increment.
let position_last_nonzero_digit =
self.digits.iter().rposition(|x| *x != 0).unwrap_or(0);
self.digits.truncate(position_last_nonzero_digit + 1);

// 3f. If `digits` had only trailing zeroes after truncating,
// 3-d. If `digits` had only trailing zeroes after truncating,
// reset to zero.
if self.digits.is_empty() {
self.magnitude = 0;
Expand Down Expand Up @@ -3600,30 +3601,42 @@ fn test_rounding_increments() {
let mut dec = FixedDecimal::from(4235970).multiplied_pow10(-3);
assert_eq!("4235.970", dec.to_string());

dec.trunc_with_increment(-2, RoundingIncrement::I2);
dec.trunc_coarse(-2, RoundingCoarseness::MultiplesOf2);
assert_eq!("4235.96", dec.to_string());

dec.trunc_with_increment(-1, RoundingIncrement::I5);
dec.trunc_coarse(-1, RoundingCoarseness::MultiplesOf5);
assert_eq!("4235.5", dec.to_string());

dec.trunc_with_increment(0, RoundingIncrement::I25);
dec.trunc_coarse(0, RoundingCoarseness::MultiplesOf25);
assert_eq!("4225", dec.to_string());

dec.trunc_with_increment(5, RoundingIncrement::I5);
dec.trunc_coarse(5, RoundingCoarseness::MultiplesOf5);
assert_eq!("00000", dec.to_string());

dec.trunc_with_increment(2, RoundingIncrement::I2);
dec.trunc_coarse(2, RoundingCoarseness::MultiplesOf2);
assert_eq!("00000", dec.to_string());

let mut dec = FixedDecimal::from_str("-99.999").unwrap();
dec.trunc_with_increment(-2, RoundingIncrement::I25);
dec.trunc_coarse(-2, RoundingCoarseness::MultiplesOf25);
assert_eq!("-99.75", dec.to_string());

let mut dec = FixedDecimal::from_str("1234.56").unwrap();
dec.trunc_with_increment(-1, RoundingIncrement::I2);
dec.trunc_coarse(-1, RoundingCoarseness::MultiplesOf2);
assert_eq!("1234.4", dec.to_string());

let mut dec = FixedDecimal::from_str("0.009").unwrap();
dec.trunc_with_increment(-1, RoundingIncrement::I5);
dec.trunc_coarse(-1, RoundingCoarseness::MultiplesOf5);
assert_eq!("0.0", dec.to_string());

let mut dec = FixedDecimal::from_str("0.60").unwrap();
dec.trunc_coarse(-2, RoundingCoarseness::MultiplesOf25);
assert_eq!("0.50", dec.to_string());

let mut dec = FixedDecimal::from_str("0.40").unwrap();
dec.trunc_coarse(-2, RoundingCoarseness::MultiplesOf25);
assert_eq!("0.25", dec.to_string());

let mut dec = FixedDecimal::from_str("0.7000000099").unwrap();
dec.trunc_coarse(-3, RoundingCoarseness::MultiplesOf2);
assert_eq!("0.700", dec.to_string());
}
2 changes: 1 addition & 1 deletion utils/fixed_decimal/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ pub use FloatPrecision as DoublePrecision;

pub use compact::CompactDecimal;
pub use decimal::FixedDecimal;
pub use decimal::RoundingIncrement;
pub use decimal::RoundingCoarseness;
pub use decimal::Sign;
pub use decimal::SignDisplay;
use displaydoc::Display;
Expand Down

0 comments on commit dba796b

Please sign in to comment.