use std::fmt;
use crate::description::Description;
use crate::matcher::{MatchResult, Matcher, Mismatch};
mod sealed {
pub trait Sealed {}
impl Sealed for f32 {}
impl Sealed for f64 {}
}
pub trait Float: sealed::Sealed + Copy + PartialOrd + fmt::Debug {
fn abs_diff(self, other: Self) -> Self;
fn float_is_nan(self) -> bool;
fn float_is_finite(self) -> bool;
}
impl Float for f32 {
fn abs_diff(self, other: Self) -> Self {
(self - other).abs()
}
fn float_is_nan(self) -> bool {
self.is_nan()
}
fn float_is_finite(self) -> bool {
self.is_finite()
}
}
impl Float for f64 {
fn abs_diff(self, other: Self) -> Self {
(self - other).abs()
}
fn float_is_nan(self) -> bool {
self.is_nan()
}
fn float_is_finite(self) -> bool {
self.is_finite()
}
}
struct CloseToMatcher<F> {
value: F,
tolerance: F,
}
impl<F: Float> Matcher<F> for CloseToMatcher<F> {
fn check(&self, actual: &F) -> MatchResult {
let diff = actual.abs_diff(self.value);
if diff <= self.tolerance {
MatchResult::pass()
} else {
MatchResult::fail(Mismatch::new(
self.description(),
format!("{actual:?} (off by {diff:?})"),
))
}
}
fn description(&self) -> Description {
Description::text(format!("within {:?} of {:?}", self.tolerance, self.value))
}
}
#[must_use]
pub fn close_to<F: Float>(value: F, tolerance: F) -> impl Matcher<F> {
CloseToMatcher { value, tolerance }
}
struct BetweenMatcher<F> {
low: F,
high: F,
}
impl<F: Float> Matcher<F> for BetweenMatcher<F> {
fn check(&self, actual: &F) -> MatchResult {
if self.low <= *actual && *actual <= self.high {
MatchResult::pass()
} else {
MatchResult::fail(Mismatch::new(self.description(), format!("{actual:?}")))
}
}
fn description(&self) -> Description {
Description::text(format!(
"between {:?} and {:?} (inclusive)",
self.low, self.high
))
}
}
#[must_use]
pub fn between<F: Float>(low: F, high: F) -> impl Matcher<F> {
BetweenMatcher { low, high }
}
struct IsNanMatcher;
impl<F: Float> Matcher<F> for IsNanMatcher {
fn check(&self, actual: &F) -> MatchResult {
if actual.float_is_nan() {
MatchResult::pass()
} else {
MatchResult::fail(Mismatch::new(
Description::text("NaN"),
format!("{actual:?}"),
))
}
}
fn description(&self) -> Description {
Description::text("NaN")
}
}
#[must_use]
pub fn is_nan<F: Float>() -> impl Matcher<F> {
IsNanMatcher
}
struct IsFiniteMatcher;
impl<F: Float> Matcher<F> for IsFiniteMatcher {
fn check(&self, actual: &F) -> MatchResult {
if actual.float_is_finite() {
MatchResult::pass()
} else {
MatchResult::fail(Mismatch::new(
Description::text("a finite number"),
format!("{actual:?}"),
))
}
}
fn description(&self) -> Description {
Description::text("a finite number")
}
}
#[must_use]
pub fn is_finite<F: Float>() -> impl Matcher<F> {
IsFiniteMatcher
}
#[cfg(test)]
mod tests {
use test_better_core::{OrFail, TestResult};
use super::*;
use crate::{check, eq, is_false, is_true};
#[test]
fn close_to_respects_the_tolerance() -> TestResult {
check!(close_to(0.3, 1e-9).check(&(0.1_f64 + 0.2)).matched).satisfies(is_true())?;
check!(close_to(0.3_f64, 1e-9).check(&0.4).matched).satisfies(is_false())?;
check!(close_to(1.0_f64, 0.5).check(&1.5).matched).satisfies(is_true())?;
check!(close_to(1.0_f64, 0.5).check(&1.6).matched).satisfies(is_false())?;
Ok(())
}
#[test]
fn close_to_failure_shows_the_tolerance_and_the_difference() -> TestResult {
let failure = close_to(1.0_f64, 0.1)
.check(&2.0)
.failure
.or_fail_with("2.0 is not within 0.1 of 1.0")?;
check!(failure.expected.to_string()).satisfies(eq("within 0.1 of 1.0".to_string()))?;
check!(failure.actual.contains("off by")).satisfies(is_true())?;
Ok(())
}
#[test]
fn between_is_an_inclusive_range() -> TestResult {
check!(between(0.0_f64, 5.0).check(&0.0).matched).satisfies(is_true())?;
check!(between(0.0_f64, 5.0).check(&5.0).matched).satisfies(is_true())?;
check!(between(0.0_f64, 5.0).check(&5.1).matched).satisfies(is_false())?;
check!(between(0.0_f64, 5.0).check(&-0.1).matched).satisfies(is_false())?;
Ok(())
}
#[test]
fn is_nan_matches_only_nan() -> TestResult {
check!(is_nan().check(&f64::NAN).matched).satisfies(is_true())?;
check!(is_nan().check(&1.0_f64).matched).satisfies(is_false())?;
check!(close_to(f64::NAN, 1.0).check(&f64::NAN).matched).satisfies(is_false())?;
Ok(())
}
#[test]
fn is_finite_rejects_infinities_and_nan() -> TestResult {
check!(is_finite().check(&1.5_f64).matched).satisfies(is_true())?;
check!(is_finite().check(&f64::INFINITY).matched).satisfies(is_false())?;
check!(is_finite().check(&f64::NEG_INFINITY).matched).satisfies(is_false())?;
check!(is_finite().check(&f64::NAN).matched).satisfies(is_false())?;
Ok(())
}
#[test]
fn numeric_matchers_work_for_f32_too() -> TestResult {
check!(close_to(1.0_f32, 0.01).check(&1.005).matched).satisfies(is_true())?;
check!(between(0.0_f32, 1.0).check(&0.5).matched).satisfies(is_true())?;
check!(is_nan().check(&f32::NAN).matched).satisfies(is_true())?;
Ok(())
}
}