Skip to main content

use_probability/
probability.rs

1use core::fmt;
2
3use crate::ProbabilityError;
4
5/// A validated probability value in the closed interval `[0, 1]`.
6#[derive(Debug, Clone, Copy, Default, PartialEq, PartialOrd)]
7pub struct Probability {
8    value: f64,
9}
10
11impl Probability {
12    /// Creates a probability without validation.
13    #[must_use]
14    pub const fn new(value: f64) -> Self {
15        Self { value }
16    }
17
18    /// Creates a probability from a finite value in `[0, 1]`.
19    ///
20    /// # Errors
21    ///
22    /// Returns [`ProbabilityError::NonFiniteProbability`] when `value` is
23    /// `NaN` or infinite, and [`ProbabilityError::ProbabilityOutOfRange`] when
24    /// `value` is outside `[0, 1]`.
25    ///
26    /// # Examples
27    ///
28    /// ```
29    /// use use_probability::{Probability, ProbabilityError};
30    ///
31    /// let probability = Probability::try_new(0.25)?;
32    /// assert_eq!(probability, Probability::new(0.25));
33    ///
34    /// assert!(matches!(
35    ///     Probability::try_new(1.5),
36    ///     Err(ProbabilityError::ProbabilityOutOfRange(1.5))
37    /// ));
38    /// # Ok::<(), ProbabilityError>(())
39    /// ```
40    pub const fn try_new(value: f64) -> Result<Self, ProbabilityError> {
41        match ProbabilityError::validate_probability(value) {
42            Ok(value) => Ok(Self::new(value)),
43            Err(error) => Err(error),
44        }
45    }
46
47    /// Creates a probability from `part / total`.
48    ///
49    /// # Errors
50    ///
51    /// Returns [`ProbabilityError::ZeroTotal`] when `total == 0`, and
52    /// [`ProbabilityError::PartExceedsTotal`] when `part > total`.
53    ///
54    /// # Examples
55    ///
56    /// ```
57    /// use use_probability::Probability;
58    ///
59    /// let probability = Probability::from_fraction(1, 4)?;
60    /// assert!((probability.value() - 0.25).abs() < 1.0e-12);
61    /// # Ok::<(), use_probability::ProbabilityError>(())
62    /// ```
63    #[allow(clippy::cast_precision_loss)]
64    pub fn from_fraction(part: u64, total: u64) -> Result<Self, ProbabilityError> {
65        ProbabilityError::validate_fraction(part, total)?;
66        Ok(Self::new(part as f64 / total as f64))
67    }
68
69    /// Validates that an existing probability remains normalized.
70    ///
71    /// # Errors
72    ///
73    /// Returns the same error variants as [`Self::try_new`].
74    pub const fn validate(self) -> Result<Self, ProbabilityError> {
75        Self::try_new(self.value)
76    }
77
78    /// Returns the stored probability value.
79    #[must_use]
80    pub const fn value(&self) -> f64 {
81        self.value
82    }
83
84    /// Returns the stored probability as a percentage.
85    #[must_use]
86    pub const fn as_percentage(&self) -> f64 {
87        self.value * 100.0
88    }
89
90    /// Returns the complementary probability `1 - p`.
91    #[must_use]
92    pub const fn complement(self) -> Self {
93        Self::new(1.0 - self.value)
94    }
95
96    /// Returns the impossible event probability `0`.
97    #[must_use]
98    pub const fn impossible() -> Self {
99        Self::new(0.0)
100    }
101
102    /// Returns the certain event probability `1`.
103    #[must_use]
104    pub const fn certainty() -> Self {
105        Self::new(1.0)
106    }
107
108    /// Returns the probability of two independent events both happening.
109    #[must_use]
110    pub const fn intersection_independent(self, other: Self) -> Self {
111        Self::new(self.value * other.value)
112    }
113
114    /// Returns the probability of at least one of two independent events
115    /// happening.
116    #[must_use]
117    pub fn union_independent(self, other: Self) -> Self {
118        Self::new(self.value.mul_add(-other.value, self.value + other.value))
119    }
120}
121
122impl From<Probability> for f64 {
123    fn from(value: Probability) -> Self {
124        value.value
125    }
126}
127
128impl fmt::Display for Probability {
129    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
130        write!(formatter, "{}", self.value)
131    }
132}
133
134/// Returns the probability of two independent events both happening.
135#[must_use]
136pub const fn independent_intersection(left: Probability, right: Probability) -> Probability {
137    left.intersection_independent(right)
138}
139
140/// Returns the probability of at least one of two independent events
141/// happening.
142#[must_use]
143pub fn independent_union(left: Probability, right: Probability) -> Probability {
144    left.union_independent(right)
145}
146
147#[cfg(test)]
148mod tests {
149    use super::{Probability, ProbabilityError, independent_intersection, independent_union};
150
151    fn assert_close(left: f64, right: f64, tolerance: f64) {
152        assert!(
153            (left - right).abs() <= tolerance,
154            "expected {left} to be within {tolerance} of {right}"
155        );
156    }
157
158    #[test]
159    fn validates_probability_values() {
160        assert!(matches!(
161            Probability::try_new(f64::NAN),
162            Err(ProbabilityError::NonFiniteProbability(_))
163        ));
164        assert!(matches!(
165            Probability::try_new(-0.01),
166            Err(ProbabilityError::ProbabilityOutOfRange(_))
167        ));
168        assert!(matches!(
169            Probability::try_new(1.01),
170            Err(ProbabilityError::ProbabilityOutOfRange(_))
171        ));
172    }
173
174    #[test]
175    fn constructs_from_fractions() -> Result<(), ProbabilityError> {
176        let probability = Probability::from_fraction(1, 4)?;
177
178        assert_close(probability.value(), 0.25, 1.0e-12);
179        Ok(())
180    }
181
182    #[test]
183    fn rejects_invalid_fractions() {
184        assert!(matches!(
185            Probability::from_fraction(1, 0),
186            Err(ProbabilityError::ZeroTotal)
187        ));
188        assert!(matches!(
189            Probability::from_fraction(3, 2),
190            Err(ProbabilityError::PartExceedsTotal { .. })
191        ));
192    }
193
194    #[test]
195    fn computes_complements_and_percentages() -> Result<(), ProbabilityError> {
196        let probability = Probability::from_fraction(1, 4)?;
197
198        assert_eq!(probability.complement(), Probability::try_new(0.75)?);
199        assert_close(probability.as_percentage(), 25.0, 1.0e-12);
200        assert_eq!(Probability::impossible(), Probability::try_new(0.0)?);
201        assert_eq!(Probability::certainty(), Probability::try_new(1.0)?);
202
203        Ok(())
204    }
205
206    #[test]
207    fn computes_independent_event_combinations() -> Result<(), ProbabilityError> {
208        let left = Probability::from_fraction(1, 4)?;
209        let right = Probability::try_new(0.5)?;
210
211        assert_close(
212            independent_intersection(left, right).value(),
213            0.125,
214            1.0e-12,
215        );
216        assert_close(independent_union(left, right).value(), 0.625, 1.0e-12);
217
218        Ok(())
219    }
220}