use crate::traits::Abs;
use fixnum::ops::{Bounded, CheckedAdd, One, RoundMode, RoundingMul, Zero};
use fixnum::{FixedPoint, Precision};
use thiserror::Error;
#[derive(Error, Debug, Ord, PartialOrd, Eq, PartialEq)]
pub enum ApproxEqError<F> {
#[error("Expected absolute tolerance to be non-negative, got {0:?}")]
NegativeAbsoluteTolerance(F),
#[error("Expected percentage to be in interval [0, 1), got {0:?}")]
IncorrectRelativePercentage(F),
}
#[inline]
fn are_approx_eq_abs_unchecked<F>(left: F, right: F, tolerance: F) -> bool
where
F: Ord + Zero + CheckedAdd<Output = F> + Bounded + Clone,
{
left.clone() <= right.clone().saturating_add(tolerance.clone())
&& right <= left.saturating_add(tolerance)
}
pub fn are_approx_eq_abs<F>(left: F, right: F, tolerance: F) -> Result<bool, ApproxEqError<F>>
where
F: Ord + Zero + CheckedAdd<Output = F> + Bounded + Clone,
{
if tolerance >= F::ZERO {
Ok(are_approx_eq_abs_unchecked(left, right, tolerance))
} else {
Err(ApproxEqError::NegativeAbsoluteTolerance(tolerance))
}
}
fn calculate_relative_tolerance<I, P>(
a: FixedPoint<I, P>,
b: FixedPoint<I, P>,
percentage: FixedPoint<I, P>,
) -> Result<FixedPoint<I, P>, ApproxEqError<FixedPoint<I, P>>>
where
I: Ord + From<i16>,
P: Precision + Ord,
FixedPoint<I, P>: Zero
+ One
+ Abs
+ Bounded
+ CheckedAdd<Output = FixedPoint<I, P>>
+ RoundingMul<Output = FixedPoint<I, P>>,
{
let percentage_correct =
percentage >= FixedPoint::<I, P>::ZERO && percentage < FixedPoint::<I, P>::ONE;
if !percentage_correct {
return Err(ApproxEqError::IncorrectRelativePercentage(percentage));
}
let magnitude = a
.trait_abs()
.unwrap_or(FixedPoint::<I, P>::MAX)
.saturating_add(b.trait_abs().unwrap_or(FixedPoint::<I, P>::MAX));
Ok(magnitude.saturating_rmul(percentage, RoundMode::Ceil))
}
pub fn are_approx_eq_rel<I, P>(
left: FixedPoint<I, P>,
right: FixedPoint<I, P>,
percentage: FixedPoint<I, P>,
) -> Result<bool, ApproxEqError<FixedPoint<I, P>>>
where
I: Ord + From<i16>,
P: Precision + Ord,
FixedPoint<I, P>: Zero
+ One
+ Abs
+ Bounded
+ Clone
+ CheckedAdd<Output = FixedPoint<I, P>>
+ RoundingMul<Output = FixedPoint<I, P>>,
{
let tolerance = calculate_relative_tolerance(left.clone(), right.clone(), percentage)?;
are_approx_eq_abs(left, right, tolerance)
}
pub fn are_approx_eq<I, P>(
left: FixedPoint<I, P>,
right: FixedPoint<I, P>,
absolute_tolerance: FixedPoint<I, P>,
relative_percentage: FixedPoint<I, P>,
) -> Result<bool, ApproxEqError<FixedPoint<I, P>>>
where
I: Ord + From<i16>,
P: Precision + Ord,
FixedPoint<I, P>: Zero
+ One
+ Abs
+ Bounded
+ Clone
+ CheckedAdd<Output = FixedPoint<I, P>>
+ RoundingMul<Output = FixedPoint<I, P>>,
{
let relative_tolerance =
calculate_relative_tolerance(left.clone(), right.clone(), relative_percentage)?;
if absolute_tolerance >= FixedPoint::<I, P>::ZERO {
Ok(are_approx_eq_abs_unchecked(
left,
right,
absolute_tolerance.max(relative_tolerance),
))
} else {
Err(ApproxEqError::NegativeAbsoluteTolerance(absolute_tolerance))
}
}
#[cfg(test)]
mod test {
use super::{are_approx_eq, are_approx_eq_abs, are_approx_eq_rel, ApproxEqError};
use fixnum::ops::{Bounded, CheckedSub, One, Zero};
use fixnum::typenum::U18;
use fixnum::{fixnum_const, FixedPoint};
type CustomPrecision = U18;
#[test]
fn should_approx_eq_equalize_exact_numbers() {
for number in [
FixedPoint::<i128, CustomPrecision>::ZERO,
FixedPoint::<i128, CustomPrecision>::MAX,
FixedPoint::<i128, CustomPrecision>::MIN,
FixedPoint::<i128, CustomPrecision>::from_bits(1),
FixedPoint::<i128, CustomPrecision>::from_bits(-1),
] {
assert!(are_approx_eq(
number,
number,
FixedPoint::<i128, CustomPrecision>::ZERO,
FixedPoint::<i128, CustomPrecision>::ZERO
)
.unwrap());
assert!(are_approx_eq(
number,
number,
FixedPoint::<i128, CustomPrecision>::from_bits(1),
FixedPoint::<i128, CustomPrecision>::ZERO
)
.unwrap());
assert!(are_approx_eq(
number,
number,
FixedPoint::<i128, CustomPrecision>::ZERO,
FixedPoint::<i128, CustomPrecision>::from_bits(1)
)
.unwrap());
assert!(are_approx_eq(
number,
number,
FixedPoint::<i128, CustomPrecision>::from_bits(1),
FixedPoint::<i128, CustomPrecision>::from_bits(1)
)
.unwrap());
assert!(are_approx_eq(
number,
number,
FixedPoint::<i128, CustomPrecision>::MAX,
FixedPoint::<i128, CustomPrecision>::ZERO
)
.unwrap());
assert!(are_approx_eq(
number,
number,
FixedPoint::<i128, CustomPrecision>::ZERO,
FixedPoint::<i128, CustomPrecision>::ONE
.csub(FixedPoint::<i128, CustomPrecision>::from_bits(1))
.unwrap()
)
.unwrap());
assert!(are_approx_eq(
number,
number,
FixedPoint::<i128, CustomPrecision>::MAX,
FixedPoint::<i128, CustomPrecision>::ONE
.csub(FixedPoint::from_bits(1))
.unwrap()
)
.unwrap());
}
}
#[test]
fn should_approx_eq_abs_equalize_exact_numbers() {
for number in [
FixedPoint::<i128, CustomPrecision>::ZERO,
FixedPoint::<i128, CustomPrecision>::MAX,
FixedPoint::<i128, CustomPrecision>::MIN,
FixedPoint::<i128, CustomPrecision>::from_bits(1),
FixedPoint::<i128, CustomPrecision>::from_bits(-1),
] {
assert!(
are_approx_eq_abs(number, number, FixedPoint::<i128, CustomPrecision>::ZERO)
.unwrap()
);
assert!(are_approx_eq_abs(
number,
number,
FixedPoint::<i128, CustomPrecision>::from_bits(1)
)
.unwrap());
assert!(
are_approx_eq_abs(number, number, FixedPoint::<i128, CustomPrecision>::MAX)
.unwrap()
);
}
}
#[test]
fn should_approx_eq_rel_equalize_exact_numbers() {
for number in [
FixedPoint::<i128, CustomPrecision>::ZERO,
FixedPoint::<i128, CustomPrecision>::MAX,
FixedPoint::<i128, CustomPrecision>::MIN,
FixedPoint::<i128, CustomPrecision>::from_bits(1),
FixedPoint::<i128, CustomPrecision>::from_bits(-1),
] {
assert!(
are_approx_eq_rel(number, number, FixedPoint::<i128, CustomPrecision>::ZERO)
.unwrap()
);
assert!(are_approx_eq_rel(
number,
number,
FixedPoint::<i128, CustomPrecision>::from_bits(1)
)
.unwrap());
assert!(are_approx_eq_rel(
number,
number,
FixedPoint::<i128, CustomPrecision>::ONE
.csub(FixedPoint::from_bits(1))
.unwrap()
)
.unwrap());
}
}
struct ApproxEqTestCase {
left: FixedPoint<i128, CustomPrecision>,
right: FixedPoint<i128, CustomPrecision>,
absolute_tolerance: FixedPoint<i128, CustomPrecision>,
relative_percentage: FixedPoint<i128, CustomPrecision>,
}
impl ApproxEqTestCase {
const fn new(
left: FixedPoint<i128, CustomPrecision>,
right: FixedPoint<i128, CustomPrecision>,
absolute_tolerance: FixedPoint<i128, CustomPrecision>,
relative_percentage: FixedPoint<i128, CustomPrecision>,
) -> Self {
Self {
left,
right,
absolute_tolerance,
relative_percentage,
}
}
}
const APPROX_EQ_ABS_MATCH_CASES: &[ApproxEqTestCase] = &[
ApproxEqTestCase::new(
fixnum_const!(5, 18),
fixnum_const!(0, 18),
fixnum_const!(5, 18),
fixnum_const!(0.01, 18),
),
ApproxEqTestCase::new(
fixnum_const!(-5, 18),
fixnum_const!(0, 18),
fixnum_const!(5, 18),
fixnum_const!(0.01, 18),
),
ApproxEqTestCase::new(
FixedPoint::<i128, CustomPrecision>::from_bits(
fixnum::_priv::parse_fixed(stringify!(0.05), fixnum::_priv::pow10(18)) + 1,
),
fixnum_const!(0, 18),
fixnum_const!(5, 18),
fixnum_const!(0.01, 18),
),
ApproxEqTestCase::new(
FixedPoint::<i128, CustomPrecision>::from_bits(
-fixnum::_priv::parse_fixed(stringify!(0.05), fixnum::_priv::pow10(18)) - 1,
),
fixnum_const!(0, 18),
fixnum_const!(5, 18),
fixnum_const!(0.01, 18),
),
ApproxEqTestCase::new(
fixnum_const!(47, 18),
fixnum_const!(52, 18),
fixnum_const!(5, 18),
fixnum_const!(0.05, 18),
),
ApproxEqTestCase::new(
fixnum_const!(47.02, 18),
fixnum_const!(51.98, 18),
fixnum_const!(5, 18),
fixnum_const!(0.05, 18),
),
];
#[test]
fn should_approx_eq_match_abs_tolerance() {
for &ApproxEqTestCase {
left,
right,
absolute_tolerance,
relative_percentage,
} in APPROX_EQ_ABS_MATCH_CASES
{
assert!(
are_approx_eq(left, right, absolute_tolerance, relative_percentage).unwrap(),
"Expected {} = {} with absolute tolerance {} and relative tolerance (%) {}, but got '!='",
left, right, absolute_tolerance, relative_percentage
);
assert!(
are_approx_eq(right, left, absolute_tolerance, relative_percentage).unwrap(),
"Expected approx eq to be symmetrical; {} = {}, but {} != {} for abs tolerance {} rel tolerance (%) {}",
left, right, right, left, absolute_tolerance, relative_percentage
);
}
}
#[test]
fn should_approx_eq_abs_match_abs_tolerance() {
for &ApproxEqTestCase {
left,
right,
absolute_tolerance,
relative_percentage: _,
} in APPROX_EQ_ABS_MATCH_CASES
{
assert!(
are_approx_eq_abs(left, right, absolute_tolerance).unwrap(),
"Expected {} = {} with absolute tolerance {}, but got '!='",
left,
right,
absolute_tolerance
);
assert!(
are_approx_eq_abs(right, left, absolute_tolerance).unwrap(),
"Expected approx eq to be symmetrical; {} = {}, but {} != {} for abs tolerance {}",
left,
right,
right,
left,
absolute_tolerance
);
}
}
#[test]
fn should_approx_eq_rel_not_match_abs_tolerance() {
for &ApproxEqTestCase {
left,
right,
absolute_tolerance: _,
relative_percentage,
} in APPROX_EQ_ABS_MATCH_CASES
{
assert!(
!are_approx_eq_rel(left, right, relative_percentage).unwrap(),
"Expected {} != {} with relative tolerance (%) {}, but got '='",
left,
right,
relative_percentage
);
assert!(
!are_approx_eq_rel(right, left, relative_percentage).unwrap(),
"Expected approx eq to be symmetrical; {} != {}, but {} = {} for rel tolerance (%) {}",
left, right, right, left, relative_percentage
);
}
}
const APPROX_EQ_REL_MATCH_CASES: &[ApproxEqTestCase] = &[
ApproxEqTestCase::new(
fixnum_const!(6, 18),
fixnum_const!(5, 18),
fixnum_const!(0, 18),
fixnum_const!(0.1, 18),
),
ApproxEqTestCase::new(
fixnum_const!(11, 18),
fixnum_const!(9, 18),
fixnum_const!(0, 18),
fixnum_const!(0.1, 18),
),
ApproxEqTestCase::new(
fixnum_const!(11, 18),
fixnum_const!(9, 18),
fixnum_const!(1.9999, 18),
fixnum_const!(0.1, 18),
),
ApproxEqTestCase::new(
fixnum_const!(9, 18),
fixnum_const!(10.1, 18),
fixnum_const!(1, 18),
fixnum_const!(0.1, 18),
),
];
#[test]
fn should_approx_eq_match_rel_tolerance() {
for &ApproxEqTestCase {
left,
right,
absolute_tolerance,
relative_percentage,
} in APPROX_EQ_REL_MATCH_CASES
{
assert!(
are_approx_eq(left, right, absolute_tolerance, relative_percentage).unwrap(),
"Expected {} = {} with absolute tolerance {} and relative tolerance (%) {}, but got '!='",
left, right, absolute_tolerance, relative_percentage
);
assert!(
are_approx_eq(right, left, absolute_tolerance, relative_percentage).unwrap(),
"Expected approx eq to be symmetrical; {} = {}, but {} != {} for abs tolerance {} rel tolerance (%) {}",
left, right, right, left, absolute_tolerance, relative_percentage
);
}
}
#[test]
fn should_approx_eq_abs_not_match_rel_tolerance() {
for &ApproxEqTestCase {
left,
right,
absolute_tolerance,
relative_percentage: _,
} in APPROX_EQ_REL_MATCH_CASES
{
assert!(
!are_approx_eq_abs(left, right, absolute_tolerance).unwrap(),
"Expected {} != {} with absolute tolerance {}, but got '='",
left,
right,
absolute_tolerance
);
assert!(
!are_approx_eq_abs(right, left, absolute_tolerance).unwrap(),
"Expected approx eq to be symmetrical; {} != {}, but {} = {} for abs tolerance {}",
left,
right,
right,
left,
absolute_tolerance
);
}
}
#[test]
fn should_approx_eq_rel_match_rel_tolerance() {
for &ApproxEqTestCase {
left,
right,
absolute_tolerance: _,
relative_percentage,
} in APPROX_EQ_REL_MATCH_CASES
{
assert!(
are_approx_eq_rel(left, right, relative_percentage).unwrap(),
"Expected {} = {} with relative tolerance (%) {}, but got '!='",
left,
right,
relative_percentage
);
assert!(
are_approx_eq_rel(right, left, relative_percentage).unwrap(),
"Expected approx eq to be symmetrical; {} = {}, but {} != {} for rel tolerance (%) {}",
left, right, right, left, relative_percentage
);
}
}
const APPROX_EQ_BOTH_MATCH_CASES: &[ApproxEqTestCase] = &[
ApproxEqTestCase::new(
fixnum_const!(6, 18),
fixnum_const!(5, 18),
fixnum_const!(1.1, 18),
fixnum_const!(0.1, 18),
),
ApproxEqTestCase::new(
fixnum_const!(9, 18),
fixnum_const!(11, 18),
fixnum_const!(2, 18),
fixnum_const!(0.1, 18),
),
ApproxEqTestCase::new(
fixnum_const!(9, 18),
FixedPoint::<i128, CustomPrecision>::from_bits(
fixnum::_priv::parse_fixed(stringify!(9), fixnum::_priv::pow10(18)) + 1,
),
fixnum_const!(2, 18),
fixnum_const!(0.1, 18),
),
ApproxEqTestCase::new(
fixnum_const!(9, 18),
fixnum_const!(10.1, 18),
fixnum_const!(1.11, 18),
fixnum_const!(0.1, 18),
),
];
#[test]
fn should_approx_eq_match_both_tolerance() {
for &ApproxEqTestCase {
left,
right,
absolute_tolerance,
relative_percentage,
} in APPROX_EQ_BOTH_MATCH_CASES
{
assert!(
are_approx_eq(left, right, absolute_tolerance, relative_percentage).unwrap(),
"Expected {} = {} with absolute tolerance {} and relative tolerance (%) {}, but got '!='",
left, right, absolute_tolerance, relative_percentage
);
assert!(
are_approx_eq(right, left, absolute_tolerance, relative_percentage).unwrap(),
"Expected approx eq to be symmetrical; {} = {}, but {} != {} for abs tolerance {} rel tolerance (%) {}",
left, right, right, left, absolute_tolerance, relative_percentage
);
}
}
#[test]
fn should_approx_eq_abs_match_both_tolerance() {
for &ApproxEqTestCase {
left,
right,
absolute_tolerance,
relative_percentage: _,
} in APPROX_EQ_BOTH_MATCH_CASES
{
assert!(
are_approx_eq_abs(left, right, absolute_tolerance).unwrap(),
"Expected {} = {} with absolute tolerance {}, but got '!='",
left,
right,
absolute_tolerance
);
assert!(
are_approx_eq_abs(right, left, absolute_tolerance).unwrap(),
"Expected approx eq to be symmetrical; {} = {}, but {} != {} for abs tolerance {}",
left,
right,
right,
left,
absolute_tolerance
);
}
}
#[test]
fn should_approx_eq_rel_match_both_tolerance() {
for &ApproxEqTestCase {
left,
right,
absolute_tolerance: _,
relative_percentage,
} in APPROX_EQ_BOTH_MATCH_CASES
{
assert!(
are_approx_eq_rel(left, right, relative_percentage).unwrap(),
"Expected {} = {} with relative tolerance (%) {}, but got '!='",
left,
right,
relative_percentage
);
assert!(
are_approx_eq_rel(right, left, relative_percentage).unwrap(),
"Expected approx eq to be symmetrical; {} = {}, but {} != {} for rel tolerance (%) {}",
left, right, right, left, relative_percentage
);
}
}
const APPROX_EQ_NOT_MATCH_CASES: &[ApproxEqTestCase] = &[
ApproxEqTestCase::new(
FixedPoint::<i128, CustomPrecision>::from_bits(
fixnum::_priv::parse_fixed(stringify!(5), fixnum::_priv::pow10(18)) + 1,
),
fixnum_const!(0, 18),
fixnum_const!(5, 18),
fixnum_const!(0.01, 18),
),
ApproxEqTestCase::new(
FixedPoint::<i128, CustomPrecision>::from_bits(
-fixnum::_priv::parse_fixed(stringify!(5), fixnum::_priv::pow10(18)) - 1,
),
fixnum_const!(0, 18),
fixnum_const!(5, 18),
fixnum_const!(0.01, 18),
),
ApproxEqTestCase::new(
FixedPoint::<i128, CustomPrecision>::MAX,
fixnum_const!(0, 18),
fixnum_const!(5, 18),
fixnum_const!(0.01, 18),
),
ApproxEqTestCase::new(
FixedPoint::<i128, CustomPrecision>::MIN,
fixnum_const!(0, 18),
fixnum_const!(5, 18),
fixnum_const!(0.01, 18),
),
ApproxEqTestCase::new(
FixedPoint::<i128, CustomPrecision>::from_bits(fixnum::_priv::parse_fixed(
stringify!(47),
fixnum::_priv::pow10(18) - 1,
)),
FixedPoint::<i128, CustomPrecision>::from_bits(fixnum::_priv::parse_fixed(
stringify!(52),
fixnum::_priv::pow10(18) + 1,
)),
fixnum_const!(5, 18),
fixnum_const!(0.05, 18),
),
ApproxEqTestCase::new(
FixedPoint::<i128, CustomPrecision>::from_bits(fixnum::_priv::parse_fixed(
stringify!(47),
fixnum::_priv::pow10(18) - 1,
)),
FixedPoint::<i128, CustomPrecision>::from_bits(fixnum::_priv::parse_fixed(
stringify!(53),
fixnum::_priv::pow10(18) + 1,
)),
fixnum_const!(5, 18),
fixnum_const!(0.05, 18),
),
ApproxEqTestCase::new(
fixnum_const!(9, 18),
FixedPoint::<i128, CustomPrecision>::from_bits(fixnum::_priv::parse_fixed(
stringify!(11),
fixnum::_priv::pow10(18) + 10,
)),
fixnum_const!(0, 18),
fixnum_const!(0.1, 18),
),
ApproxEqTestCase::new(
fixnum_const!(9, 18),
FixedPoint::<i128, CustomPrecision>::from_bits(fixnum::_priv::parse_fixed(
stringify!(11),
fixnum::_priv::pow10(18) + 10,
)),
fixnum_const!(1.9999, 18),
fixnum_const!(0.1, 18),
),
];
#[test]
fn should_approx_eq_not_match() {
for &ApproxEqTestCase {
left,
right,
absolute_tolerance,
relative_percentage,
} in APPROX_EQ_NOT_MATCH_CASES
{
assert!(
!are_approx_eq(left, right, absolute_tolerance, relative_percentage).unwrap(),
"Expected {} != {} with absolute tolerance {} and relative tolerance (%) {}, but got '=='",
left, right, absolute_tolerance, relative_percentage
);
assert!(
!are_approx_eq(right, left, absolute_tolerance, relative_percentage).unwrap(),
"Expected approx eq to be symmetrical; {} != {}, but {} = {} for abs tolerance {} rel tolerance (%) {}",
left, right, right, left, absolute_tolerance, relative_percentage
);
}
}
#[test]
fn should_fail_incorrect_relative_percentage() {
let percentage = FixedPoint::<i128, CustomPrecision>::from_bits(-1234);
assert_eq!(
are_approx_eq(
FixedPoint::<i128, CustomPrecision>::ZERO,
FixedPoint::<i128, CustomPrecision>::ZERO,
FixedPoint::<i128, CustomPrecision>::ZERO,
percentage,
),
Err(ApproxEqError::IncorrectRelativePercentage(percentage))
);
let percentage = FixedPoint::<i128, CustomPrecision>::ONE;
assert_eq!(
are_approx_eq(
FixedPoint::<i128, CustomPrecision>::ZERO,
FixedPoint::<i128, CustomPrecision>::ZERO,
FixedPoint::<i128, CustomPrecision>::ZERO,
percentage,
),
Err(ApproxEqError::IncorrectRelativePercentage(percentage))
);
}
#[test]
fn should_fail_incorrect_absolute_percentage() {
let abs_tolerance = FixedPoint::<i128, CustomPrecision>::from_bits(-1);
assert_eq!(
are_approx_eq(
FixedPoint::<i128, CustomPrecision>::ZERO,
FixedPoint::<i128, CustomPrecision>::ZERO,
abs_tolerance,
FixedPoint::<i128, CustomPrecision>::ZERO,
),
Err(ApproxEqError::NegativeAbsoluteTolerance(abs_tolerance))
);
let abs_tolerance = FixedPoint::<i128, CustomPrecision>::from_bits(i128::MIN);
assert_eq!(
are_approx_eq(
FixedPoint::<i128, CustomPrecision>::ZERO,
FixedPoint::<i128, CustomPrecision>::ZERO,
abs_tolerance,
FixedPoint::<i128, CustomPrecision>::ZERO,
),
Err(ApproxEqError::NegativeAbsoluteTolerance(abs_tolerance))
);
}
}