use core::fmt;
use crate::ProbabilityError;
#[derive(Debug, Clone, Copy, Default, PartialEq, PartialOrd)]
pub struct Probability {
value: f64,
}
impl Probability {
#[must_use]
pub const fn new(value: f64) -> Self {
Self { value }
}
pub const fn try_new(value: f64) -> Result<Self, ProbabilityError> {
match ProbabilityError::validate_probability(value) {
Ok(value) => Ok(Self::new(value)),
Err(error) => Err(error),
}
}
#[allow(clippy::cast_precision_loss)]
pub fn from_fraction(part: u64, total: u64) -> Result<Self, ProbabilityError> {
ProbabilityError::validate_fraction(part, total)?;
Ok(Self::new(part as f64 / total as f64))
}
pub const fn validate(self) -> Result<Self, ProbabilityError> {
Self::try_new(self.value)
}
#[must_use]
pub const fn value(&self) -> f64 {
self.value
}
#[must_use]
pub const fn as_percentage(&self) -> f64 {
self.value * 100.0
}
#[must_use]
pub const fn complement(self) -> Self {
Self::new(1.0 - self.value)
}
#[must_use]
pub const fn impossible() -> Self {
Self::new(0.0)
}
#[must_use]
pub const fn certainty() -> Self {
Self::new(1.0)
}
#[must_use]
pub const fn intersection_independent(self, other: Self) -> Self {
Self::new(self.value * other.value)
}
#[must_use]
pub fn union_independent(self, other: Self) -> Self {
Self::new(self.value.mul_add(-other.value, self.value + other.value))
}
}
impl From<Probability> for f64 {
fn from(value: Probability) -> Self {
value.value
}
}
impl fmt::Display for Probability {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(formatter, "{}", self.value)
}
}
#[must_use]
pub const fn independent_intersection(left: Probability, right: Probability) -> Probability {
left.intersection_independent(right)
}
#[must_use]
pub fn independent_union(left: Probability, right: Probability) -> Probability {
left.union_independent(right)
}
#[cfg(test)]
mod tests {
use super::{Probability, ProbabilityError, independent_intersection, independent_union};
fn assert_close(left: f64, right: f64, tolerance: f64) {
assert!(
(left - right).abs() <= tolerance,
"expected {left} to be within {tolerance} of {right}"
);
}
#[test]
fn validates_probability_values() {
assert!(matches!(
Probability::try_new(f64::NAN),
Err(ProbabilityError::NonFiniteProbability(_))
));
assert!(matches!(
Probability::try_new(-0.01),
Err(ProbabilityError::ProbabilityOutOfRange(_))
));
assert!(matches!(
Probability::try_new(1.01),
Err(ProbabilityError::ProbabilityOutOfRange(_))
));
}
#[test]
fn constructs_from_fractions() -> Result<(), ProbabilityError> {
let probability = Probability::from_fraction(1, 4)?;
assert_close(probability.value(), 0.25, 1.0e-12);
Ok(())
}
#[test]
fn rejects_invalid_fractions() {
assert!(matches!(
Probability::from_fraction(1, 0),
Err(ProbabilityError::ZeroTotal)
));
assert!(matches!(
Probability::from_fraction(3, 2),
Err(ProbabilityError::PartExceedsTotal { .. })
));
}
#[test]
fn computes_complements_and_percentages() -> Result<(), ProbabilityError> {
let probability = Probability::from_fraction(1, 4)?;
assert_eq!(probability.complement(), Probability::try_new(0.75)?);
assert_close(probability.as_percentage(), 25.0, 1.0e-12);
assert_eq!(Probability::impossible(), Probability::try_new(0.0)?);
assert_eq!(Probability::certainty(), Probability::try_new(1.0)?);
Ok(())
}
#[test]
fn computes_independent_event_combinations() -> Result<(), ProbabilityError> {
let left = Probability::from_fraction(1, 4)?;
let right = Probability::try_new(0.5)?;
assert_close(
independent_intersection(left, right).value(),
0.125,
1.0e-12,
);
assert_close(independent_union(left, right).value(), 0.625, 1.0e-12);
Ok(())
}
}