use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
#[serde(try_from = "f32", into = "f32")]
pub struct Probability(f32);
impl Probability {
#[must_use]
pub const fn new(p: f32) -> Self {
assert!(
p >= 0.0 && p <= 1.0,
"Probability::new: value outside [0, 1] (or NaN)"
);
Self(p)
}
pub fn try_new(p: f32) -> Result<Self, ProbabilityError> {
if (0.0..=1.0).contains(&p) {
Ok(Self(p))
} else {
Err(ProbabilityError { got: p })
}
}
#[must_use]
pub const fn get(self) -> f32 {
self.0
}
}
impl TryFrom<f32> for Probability {
type Error = ProbabilityError;
fn try_from(p: f32) -> Result<Self, Self::Error> {
Self::try_new(p)
}
}
impl From<Probability> for f32 {
fn from(p: Probability) -> Self {
p.0
}
}
#[derive(Debug, Clone, Copy, PartialEq, thiserror::Error)]
#[error(
"invalid probability: {got} is outside the closed unit interval [0, 1] (and must not be NaN)"
)]
pub struct ProbabilityError {
pub got: f32,
}
#[cfg(test)]
mod tests {
use super::{Probability, ProbabilityError};
#[test]
fn new_accepts_interval_including_endpoints() {
assert_eq!(Probability::new(0.0).get(), 0.0);
assert_eq!(Probability::new(1.0).get(), 1.0);
assert_eq!(Probability::new(0.25).get(), 0.25);
}
#[test]
#[should_panic(expected = "outside [0, 1]")]
fn new_panics_above_one() {
let _ = Probability::new(1.5);
}
#[test]
#[should_panic(expected = "outside [0, 1]")]
fn new_panics_below_zero() {
let _ = Probability::new(-0.1);
}
#[test]
#[should_panic(expected = "outside [0, 1]")]
fn new_panics_on_nan() {
let _ = Probability::new(f32::NAN);
}
#[test]
fn try_new_accepts_valid() {
assert!(Probability::try_new(0.0).is_ok());
assert!(Probability::try_new(1.0).is_ok());
assert!(Probability::try_new(0.5).is_ok());
}
#[test]
fn try_new_rejects_out_of_range_nan_and_inf() {
assert_eq!(
Probability::try_new(1.5),
Err(ProbabilityError { got: 1.5 })
);
assert!(Probability::try_new(-0.1).is_err());
assert!(Probability::try_new(f32::NAN).is_err());
assert!(Probability::try_new(f32::INFINITY).is_err());
assert!(Probability::try_new(f32::NEG_INFINITY).is_err());
}
#[test]
fn round_trip_via_try_from_and_into() {
let p = Probability::try_from(0.7).unwrap();
let back: f32 = p.into();
assert_eq!(back, 0.7);
assert!(Probability::try_from(2.0).is_err());
}
#[test]
fn error_display_names_the_value() {
let s = ProbabilityError { got: 1.5 }.to_string();
assert!(s.contains("1.5"));
assert!(s.contains("[0, 1]"));
}
}