use crate::error::{Interval, NonFinite, ValueError};
pub(crate) fn finite(parameter: &'static str, value: f64) -> Result<f64, ValueError> {
match NonFinite::classify(value) {
None => Ok(value),
Some(class) => Err(ValueError::NotFinite {
parameter,
value: class,
}),
}
}
pub(crate) fn within(
parameter: &'static str,
value: f64,
domain: Interval,
) -> Result<f64, ValueError> {
let value = finite(parameter, value)?;
if domain.contains(value) {
Ok(value)
} else {
Err(ValueError::OutOfDomain {
parameter,
value,
domain,
})
}
}
pub(crate) fn finite_result(quantity: &'static str, value: f64) -> Result<f64, ValueError> {
match NonFinite::classify(value) {
None => Ok(value),
Some(class) => Err(ValueError::ResultNotFinite {
quantity,
value: class,
}),
}
}
pub(crate) fn result_within(
quantity: &'static str,
value: f64,
domain: Interval,
) -> Result<f64, ValueError> {
let value = finite_result(quantity, value)?;
if domain.contains(value) {
Ok(value)
} else {
Err(ValueError::ResultOutOfDomain {
quantity,
value,
domain,
})
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::support::prng;
use std::ops::Bound;
const EVERYTHING: Interval = Interval::new(Bound::Unbounded, Bound::Unbounded);
const NON_FINITE: [(f64, NonFinite); 3] = [
(f64::NAN, NonFinite::NaN),
(f64::INFINITY, NonFinite::PositiveInfinity),
(f64::NEG_INFINITY, NonFinite::NegativeInfinity),
];
const FINITE_EDGES: [f64; 8] = [
0.0,
-0.0,
f64::MIN_POSITIVE,
f64::MAX,
f64::MIN,
f64::EPSILON,
1.0,
-1.0,
];
#[test]
fn non_finite_inputs_are_rejected() {
for (value, class) in NON_FINITE {
let expected = ValueError::NotFinite {
parameter: "rate",
value: class,
};
assert_eq!(finite("rate", value), Err(expected.clone()));
assert_eq!(
within("rate", value, Interval::at_least(0.0)),
Err(expected)
);
}
}
#[test]
fn finite_inputs_are_accepted_unchanged() {
let subnormal = f64::from_bits(1);
for value in FINITE_EDGES.into_iter().chain([subnormal, -subnormal]) {
let accepted = finite("x", value).expect("finite input");
assert_eq!(accepted.to_bits(), value.to_bits());
let accepted = within("x", value, EVERYTHING).expect("in domain");
assert_eq!(accepted.to_bits(), value.to_bits());
}
}
#[test]
fn out_of_domain_inputs_are_rejected() {
let positive = Interval::greater_than(0.0);
assert_eq!(
within("time", -0.25, positive),
Err(ValueError::OutOfDomain {
parameter: "time",
value: -0.25,
domain: positive,
})
);
for zero in [0.0, -0.0] {
assert!(within("time", zero, positive).is_err());
assert!(within("time", zero, Interval::at_least(0.0)).is_ok());
}
let below = Interval::less_than(1.0);
assert!(within("x", 1.0, below).is_err());
assert!(within("x", 1.0_f64.next_down(), below).is_ok());
assert!(within("x", 1.0, Interval::at_most(1.0)).is_ok());
}
#[test]
fn non_finite_results_are_rejected() {
let overflowed = f64::MAX * 2.0;
assert_eq!(
finite_result("compound factor", overflowed),
Err(ValueError::ResultNotFinite {
quantity: "compound factor",
value: NonFinite::PositiveInfinity,
})
);
for (value, class) in NON_FINITE {
let expected = ValueError::ResultNotFinite {
quantity: "discount factor",
value: class,
};
assert_eq!(
finite_result("discount factor", value),
Err(expected.clone())
);
assert_eq!(
result_within("discount factor", value, Interval::greater_than(0.0)),
Err(expected)
);
}
}
#[test]
fn out_of_domain_results_are_rejected() {
let positive = Interval::greater_than(0.0);
let underflowed = f64::MIN_POSITIVE * f64::EPSILON * f64::EPSILON;
assert_eq!(underflowed.to_bits(), 0, "the product underflows to +0.0");
assert_eq!(
result_within("compound factor", underflowed, positive),
Err(ValueError::ResultOutOfDomain {
quantity: "compound factor",
value: underflowed,
domain: positive,
})
);
for value in FINITE_EDGES {
let accepted = finite_result("x", value).expect("finite result");
assert_eq!(accepted.to_bits(), value.to_bits());
}
assert!(result_within("x", 0.5, positive).is_ok());
}
#[test]
fn validators_agree_with_the_definition_on_arbitrary_bit_patterns() {
let domains = [
Interval::greater_than(-2.0),
Interval::at_least(0.0),
Interval::less_than(1.0),
Interval::at_most(0.0),
EVERYTHING,
];
prng::check("validators match definition", 0x5AB1_0001, 20_000, |rng| {
let value = if rng.below(2) == 0 {
rng.any_f64()
} else {
(rng.next_f64() - 0.5) * 8.0
};
let accepted = finite("x", value).is_ok();
if accepted != value.is_finite() {
return Err(format!("finite({value:?}) accepted = {accepted}"));
}
for domain in domains {
let accepted = within("x", value, domain).is_ok();
let expected = value.is_finite() && domain.contains(value);
if accepted != expected {
return Err(format!("within({value:?}, {domain}) accepted = {accepted}"));
}
let accepted = result_within("x", value, domain).is_ok();
if accepted != expected {
return Err(format!(
"result_within({value:?}, {domain}) accepted = {accepted}"
));
}
}
Ok(())
});
}
}