use super::core::{Dec, MAX_RAW, NAN_RAW, ONE_U, POW10, dec_or_nan};
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum RoundingStrategy {
#[default]
MidpointAwayFromZero,
MidpointNearestEven,
ToZero,
AwayFromZero,
ToNegativeInfinity,
ToPositiveInfinity,
}
impl Dec {
#[inline(always)]
pub fn from_f64_round(value: f64, dp: u32) -> Option<Self> {
Self::from_f64(value).map(|value| value.round_dp(dp))
}
#[inline(always)]
pub const fn round_dp(self, dp: u32) -> Self {
self.round_dp_with(dp, RoundingStrategy::MidpointAwayFromZero)
}
#[inline(always)]
pub const fn round_dp_with(self, dp: u32, strategy: RoundingStrategy) -> Self {
if dp >= Dec::SCALE {
return self;
}
if self.is_nan() {
return Self::NAN;
}
let factor = POW10[(Dec::SCALE - dp) as usize];
dec_or_nan(div_strategy(self.0, factor, strategy).checked_mul(factor))
}
#[inline(always)]
pub const fn round_to_step(self, step: Self) -> Self {
self.round_to_step_with(step, RoundingStrategy::MidpointAwayFromZero)
}
#[inline(always)]
pub const fn round_to_step_with(self, step: Self, strategy: RoundingStrategy) -> Self {
if self.is_nan() || step.is_nan() {
return Self::NAN;
}
if step.0 <= 0 {
return self;
}
match pow10_exponent(step.0) {
Some(exponent) if exponent <= Dec::SCALE => {
Self(round_to_places(self.0, Dec::SCALE - exponent, strategy))
}
_ => dec_or_nan(div_strategy(self.0, step.0, strategy).checked_mul(step.0)),
}
}
}
#[inline(always)]
pub(crate) const fn div_strategy(
numerator: i128,
divisor: i128,
strategy: RoundingStrategy,
) -> i128 {
let quotient = numerator / divisor;
let remainder = numerator % divisor;
if remainder == 0 {
return quotient;
}
let magnitude = if remainder < 0 { -remainder } else { remainder };
let half = divisor / 2;
let away = match strategy {
RoundingStrategy::MidpointAwayFromZero => {
magnitude > half || (magnitude == half && divisor & 1 == 0)
}
RoundingStrategy::MidpointNearestEven => {
magnitude > half || (magnitude == half && divisor & 1 == 0 && quotient & 1 != 0)
}
RoundingStrategy::ToZero => false,
RoundingStrategy::AwayFromZero => true,
RoundingStrategy::ToNegativeInfinity => numerator < 0,
RoundingStrategy::ToPositiveInfinity => numerator > 0,
};
if !away {
return quotient;
}
if numerator < 0 {
quotient.saturating_sub(1)
} else {
quotient.saturating_add(1)
}
}
const fn pow10_exponent(value: i128) -> Option<u32> {
let bits = 128 - value.leading_zeros();
let exponent = (bits * 1233) >> 12;
if POW10[exponent as usize] == value {
return Some(exponent);
}
if exponent + 1 < POW10.len() as u32 && POW10[(exponent + 1) as usize] == value {
return Some(exponent + 1);
}
None
}
#[inline(always)]
const fn round_to_places(raw: i128, dp: u32, strategy: RoundingStrategy) -> i128 {
if dp >= Dec::SCALE {
return raw;
}
let negative = raw < 0;
let magnitude = raw.unsigned_abs();
let fraction = round_fraction((magnitude % ONE_U) as u64, dp, negative, strategy) as u128;
let scaled = match (magnitude / ONE_U).checked_mul(ONE_U) {
Some(integer) => match integer.checked_add(fraction) {
Some(value) => value,
None => u128::MAX,
},
None => u128::MAX,
};
if scaled > MAX_RAW as u128 {
return NAN_RAW;
}
match negative {
true => -(scaled as i128),
false => scaled as i128,
}
}
#[inline(always)]
const fn round_fraction(fraction: u64, dp: u32, negative: bool, strategy: RoundingStrategy) -> u64 {
match dp {
0 => round_u64::<1_000_000_000_000_000_000>(fraction, negative, strategy),
1 => round_u64::<100_000_000_000_000_000>(fraction, negative, strategy),
2 => round_u64::<10_000_000_000_000_000>(fraction, negative, strategy),
3 => round_u64::<1_000_000_000_000_000>(fraction, negative, strategy),
4 => round_u64::<100_000_000_000_000>(fraction, negative, strategy),
5 => round_u64::<10_000_000_000_000>(fraction, negative, strategy),
6 => round_u64::<1_000_000_000_000>(fraction, negative, strategy),
7 => round_u64::<100_000_000_000>(fraction, negative, strategy),
8 => round_u64::<10_000_000_000>(fraction, negative, strategy),
9 => round_u64::<1_000_000_000>(fraction, negative, strategy),
10 => round_u64::<100_000_000>(fraction, negative, strategy),
11 => round_u64::<10_000_000>(fraction, negative, strategy),
12 => round_u64::<1_000_000>(fraction, negative, strategy),
13 => round_u64::<100_000>(fraction, negative, strategy),
14 => round_u64::<10_000>(fraction, negative, strategy),
15 => round_u64::<1_000>(fraction, negative, strategy),
16 => round_u64::<100>(fraction, negative, strategy),
17 => round_u64::<10>(fraction, negative, strategy),
_ => fraction,
}
}
#[inline(always)]
const fn round_u64<const STEP: u64>(
fraction: u64,
negative: bool,
strategy: RoundingStrategy,
) -> u64 {
let quotient = fraction / STEP;
let remainder = fraction % STEP;
let half = STEP / 2;
let away = remainder != 0
&& match strategy {
RoundingStrategy::MidpointAwayFromZero => remainder >= half,
RoundingStrategy::MidpointNearestEven => {
remainder > half || (remainder == half && quotient & 1 != 0)
}
RoundingStrategy::ToZero => false,
RoundingStrategy::AwayFromZero => true,
RoundingStrategy::ToNegativeInfinity => negative,
RoundingStrategy::ToPositiveInfinity => !negative,
};
match away {
true => (quotient + 1) * STEP,
false => quotient * STEP,
}
}
#[cfg(test)]
mod round_to_step {
#![allow(clippy::unwrap_used)]
use super::*;
use crate::Dec;
use crate::dec;
const VALUES: [Dec; 9] = [
Dec::ZERO,
dec!(0.5),
dec!(-0.5),
dec!(2.5),
dec!(1.234567),
dec!(-1.234567),
dec!(1234.98765),
Dec::MAX,
Dec::MIN,
];
#[test]
fn test_powers_of_ten_match_round_dp() {
for value in VALUES {
for dp in 0..=Dec::SCALE {
let step = Dec::from_raw(POW10[(Dec::SCALE - dp) as usize]);
assert_eq!(
value.round_to_step(step),
value.round_dp(dp),
"value {value} step {step}"
);
}
}
}
#[test]
fn test_arbitrary_steps() {
assert_eq!(dec!(1.6543).round_to_step(dec!(0.25)), dec!(1.75));
assert_eq!(dec!(-1.6543).round_to_step(dec!(0.25)), dec!(-1.75));
assert_eq!(dec!(103).round_to_step(dec!(5)), dec!(105));
assert_eq!(dec!(102).round_to_step(dec!(5)), dec!(100));
}
#[test]
fn test_halves_round_away_from_zero() {
assert_eq!(dec!(1.125).round_to_step(dec!(0.01)), dec!(1.13));
assert_eq!(dec!(-1.125).round_to_step(dec!(0.01)), dec!(-1.13));
assert_eq!(dec!(0.5).round_to_step(Dec::ONE), Dec::ONE);
assert_eq!(dec!(-0.5).round_to_step(Dec::ONE), Dec::NEG_ONE);
}
#[test]
fn test_a_rounded_up_fraction_carries() {
assert_eq!(dec!(1.999).round_to_step(dec!(0.01)), dec!(2));
assert_eq!(dec!(-1.999).round_to_step(dec!(0.01)), dec!(-2));
assert_eq!(dec!(0.9999).round_to_step(dec!(0.001)), Dec::ONE);
}
#[test]
fn test_a_step_of_ten_or_more_rounds_the_integer_part() {
assert_eq!(dec!(104).round_to_step(dec!(10)), dec!(100));
assert_eq!(dec!(105).round_to_step(dec!(10)), dec!(110));
assert_eq!(dec!(1449).round_to_step(dec!(100)), dec!(1400));
}
#[test]
fn test_a_non_positive_step_is_a_no_op() {
assert_eq!(dec!(1.5).round_to_step(Dec::ZERO), dec!(1.5));
assert_eq!(dec!(1.5).round_to_step(dec!(-0.01)), dec!(1.5));
}
#[test]
fn test_the_extremes_become_nan() {
assert!(Dec::MAX.round_to_step(Dec::ONE).is_nan());
assert!(Dec::MIN.round_to_step(Dec::ONE).is_nan());
assert!(Dec::MAX.round_to_step(dec!(0.25)).is_nan());
assert!(Dec::MIN.round_to_step(dec!(0.25)).is_nan());
}
#[test]
fn test_nan_survives_both_rounding_paths() {
assert!(Dec::NAN.round_to_step(Dec::ONE).is_nan());
assert!(Dec::NAN.round_to_step(dec!(0.25)).is_nan());
assert!(dec!(1.5).round_to_step(Dec::NAN).is_nan());
assert!(Dec::NAN.round_dp(2).is_nan());
assert!(Dec::NAN.round_dp(Dec::SCALE).is_nan());
}
#[test]
fn test_pow10_exponent_finds_every_power() {
for (exponent, power) in POW10.iter().enumerate() {
assert_eq!(pow10_exponent(*power), Some(exponent as u32));
}
assert_eq!(pow10_exponent(25), None);
assert_eq!(pow10_exponent(999), None);
assert_eq!(pow10_exponent(i128::MAX), None);
}
}
#[cfg(test)]
mod strategies {
#![allow(clippy::unwrap_used)]
use super::*;
use crate::Dec;
use crate::dec;
use RoundingStrategy::*;
const ALL: [RoundingStrategy; 6] = [
MidpointAwayFromZero,
MidpointNearestEven,
ToZero,
AwayFromZero,
ToNegativeInfinity,
ToPositiveInfinity,
];
#[test]
fn test_each_strategy_resolves_a_fraction_its_own_way() {
for (value, expected) in [
(
dec!(1.284),
[
dec!(1.28),
dec!(1.28),
dec!(1.28),
dec!(1.29),
dec!(1.28),
dec!(1.29),
],
),
(
dec!(1.286),
[
dec!(1.29),
dec!(1.29),
dec!(1.28),
dec!(1.29),
dec!(1.28),
dec!(1.29),
],
),
(
dec!(-1.284),
[
dec!(-1.28),
dec!(-1.28),
dec!(-1.28),
dec!(-1.29),
dec!(-1.29),
dec!(-1.28),
],
),
(
dec!(-1.286),
[
dec!(-1.29),
dec!(-1.29),
dec!(-1.28),
dec!(-1.29),
dec!(-1.29),
dec!(-1.28),
],
),
] {
for (strategy, expected) in ALL.into_iter().zip(expected) {
assert_eq!(
value.round_dp_with(2, strategy),
expected,
"{value} at 2 places {strategy:?}"
);
}
}
}
#[test]
fn test_a_tie_splits_the_two_nearest_strategies() {
assert_eq!(dec!(0.5).round_dp_with(0, MidpointNearestEven), Dec::ZERO);
assert_eq!(dec!(1.5).round_dp_with(0, MidpointNearestEven), dec!(2));
assert_eq!(dec!(2.5).round_dp_with(0, MidpointNearestEven), dec!(2));
assert_eq!(dec!(3.5).round_dp_with(0, MidpointNearestEven), dec!(4));
assert_eq!(dec!(-2.5).round_dp_with(0, MidpointNearestEven), dec!(-2));
assert_eq!(dec!(-3.5).round_dp_with(0, MidpointNearestEven), dec!(-4));
assert_eq!(dec!(2.5).round_dp_with(0, MidpointAwayFromZero), dec!(3));
assert_eq!(dec!(-2.5).round_dp_with(0, MidpointAwayFromZero), dec!(-3));
}
#[test]
fn test_nearest_even_holds_on_an_odd_step() {
for raw in -40_i128..=40 {
let value = Dec::from_raw(raw);
let step = Dec::from_raw(3);
assert_eq!(
value.round_to_step_with(step, MidpointNearestEven),
value.round_to_step_with(step, MidpointAwayFromZero),
"raw {raw} to a step of 3"
);
}
}
#[test]
fn test_the_zero_place_strategies_are_floor_ceil_and_trunc() {
for value in [
dec!(2.5),
dec!(-2.5),
dec!(1.0001),
dec!(-1.0001),
dec!(7),
Dec::ZERO,
] {
assert_eq!(
value.round_dp_with(0, ToNegativeInfinity),
value.floor(),
"{value}"
);
assert_eq!(
value.round_dp_with(0, ToPositiveInfinity),
value.ceil(),
"{value}"
);
assert_eq!(value.round_dp_with(0, ToZero), value.trunc(), "{value}");
}
}
#[test]
fn test_the_default_is_what_the_plain_methods_do() {
assert_eq!(RoundingStrategy::default(), MidpointAwayFromZero);
for value in [
dec!(1.125),
dec!(-1.125),
dec!(104_237.286),
Dec::MAX,
Dec::MIN,
] {
for dp in 0..=Dec::SCALE {
assert_eq!(
value.round_dp_with(dp, RoundingStrategy::default()),
value.round_dp(dp),
"{value} at {dp} places"
);
}
assert_eq!(
value.round_to_step_with(dec!(0.25), RoundingStrategy::default()),
value.round_to_step(dec!(0.25)),
"{value} to a step of 0.25"
);
}
}
#[test]
fn test_an_exact_value_never_moves() {
for strategy in ALL {
for value in [dec!(1.25), dec!(-1.25), dec!(100), Dec::ZERO] {
assert_eq!(
value.round_to_step_with(dec!(0.25), strategy),
value,
"{value} {strategy:?}"
);
}
}
}
#[test]
fn test_a_directed_strategy_carries_into_the_integer_part() {
assert_eq!(dec!(1.991).round_dp_with(2, ToPositiveInfinity), dec!(2));
assert_eq!(dec!(-1.991).round_dp_with(2, ToNegativeInfinity), dec!(-2));
assert_eq!(dec!(0.9991).round_dp_with(3, AwayFromZero), Dec::ONE);
assert_eq!(dec!(-0.9991).round_dp_with(3, AwayFromZero), Dec::NEG_ONE);
}
#[test]
fn test_towards_zero_never_leaves_the_range() {
for value in [Dec::MAX, Dec::MIN] {
assert!(value.round_dp_with(0, ToZero).is_finite(), "{value}");
assert!(value.round_dp_with(0, AwayFromZero).is_nan(), "{value}");
}
assert!(Dec::MAX.round_dp_with(0, ToPositiveInfinity).is_nan());
assert!(Dec::MAX.round_dp_with(0, ToNegativeInfinity).is_finite());
assert!(Dec::MIN.round_dp_with(0, ToNegativeInfinity).is_nan());
assert!(Dec::MIN.round_dp_with(0, ToPositiveInfinity).is_finite());
}
#[test]
fn test_nan_survives_every_strategy() {
for strategy in ALL {
assert!(Dec::NAN.round_dp_with(2, strategy).is_nan(), "{strategy:?}");
assert!(
Dec::NAN.round_dp_with(Dec::SCALE, strategy).is_nan(),
"{strategy:?}"
);
assert!(
Dec::NAN.round_to_step_with(Dec::ONE, strategy).is_nan(),
"{strategy:?}"
);
assert!(
Dec::NAN.round_to_step_with(dec!(0.25), strategy).is_nan(),
"{strategy:?}"
);
assert!(
dec!(1.5).round_to_step_with(Dec::NAN, strategy).is_nan(),
"{strategy:?}"
);
}
}
#[test]
fn test_a_non_positive_step_stays_a_no_op() {
for strategy in ALL {
assert_eq!(dec!(1.5).round_to_step_with(Dec::ZERO, strategy), dec!(1.5));
assert_eq!(
dec!(1.5).round_to_step_with(dec!(-0.01), strategy),
dec!(1.5)
);
}
}
}