Skip to main content

devela/num/prob/
probability.rs

1// devela/src/num/prob/probability.rs
2//
3//! Defines [`Probability`].
4//
5
6use crate::{_impl_init, RatioU64, is, unwrap};
7
8#[doc = crate::_tags!(num)]
9/// An exact probability represented by a canonical rational value.
10#[doc = crate::_doc_meta!{
11    location("num/prob", struct Probability),
12    test_size_of(Probability = 16|128; niche Option),
13}]
14/// A probability is a value in the closed unit interval:
15/// $$
16/// 0 \le P(A) \le 1
17/// $$
18///
19/// This representation stores an exact reduced ratio
20/// $$
21/// P(A) = \frac{n}{d}, \qquad 0 \le n \le d, \qquad d > 0.
22/// $$
23///
24/// Equivalent ratios have the same canonical representation.
25///
26/// If $g = \gcd(n,d)$, construction reduces the terms as
27/// $$
28/// \frac{n}{d} = \frac{n/g}{d/g}
29/// $$
30///
31/// Therefore `1/2`, `2/4`, and `50/100` construct equal values.
32///
33/// [`ZERO`](Self::ZERO) represents impossibility and
34/// [`ONE`](Self::ONE) represents certainty.
35#[must_use]
36#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
37pub struct Probability {
38    ratio: RatioU64,
39}
40
41_impl_init! { Self::ZERO => Probability }
42impl Default for Probability {
43    fn default() -> Self {
44        Self::ZERO
45    }
46}
47
48#[rustfmt::skip]
49impl Probability {
50    /// The impossible probability, $P = 0$.
51    pub const ZERO: Self = Self { ratio: RatioU64::ZERO };
52    /// The certain probability, $P = 1$.
53    pub const ONE: Self = Self { ratio: RatioU64::ONE };
54
55    /// Constructs an exact probability from the ratio `num / den`.
56    ///
57    /// Equivalent ratios are reduced to one canonical representation.
58    ///
59    /// Returns `None` if `den == 0` or `num > den`.
60    #[must_use]
61    pub const fn new(num: u64, den: u64) -> Option<Self> {
62        let ratio = unwrap![some? RatioU64::new(num, den)];
63        Self::from_ratio(ratio)
64    }
65    /// Constructs an exact probability from a [`RatioU64`].
66    ///
67    /// The ratio is reduced to its canonical representation.
68    ///
69    /// Returns `None` if the ratio is greater than one.
70    #[must_use]
71    pub const fn from_ratio(ratio: RatioU64) -> Option<Self> {
72        is! { ratio.num() > ratio.den(), return None }
73        Some(Self { ratio: ratio.reduced() })
74    }
75    /// Returns the canonical ratio representation.
76    #[must_use]
77    pub const fn as_ratio(&self) -> &RatioU64 { &self.ratio }
78
79    /// Returns the canonical ratio representation by value.
80    #[must_use]
81    pub const fn into_ratio(self) -> RatioU64 { self.ratio }
82
83    /// Returns the canonical numerator.
84    #[must_use]
85    pub const fn num(self) -> u64 { self.ratio.num() }
86
87    /// Returns the canonical denominator.
88    #[must_use]
89    pub const fn den(self) -> u64 { self.ratio.den() }
90
91    /// Returns the canonical `(numerator, denominator)` pair.
92    #[must_use]
93    pub const fn num_den(self) -> (u64, u64) { self.ratio.num_den() }
94
95    /// Returns whether this probability is impossible, $P = 0$.
96    #[must_use]
97    pub const fn is_zero(self) -> bool { self.ratio.is_zero() }
98
99    /// Returns whether this probability is certain, $P = 1$.
100    #[must_use]
101    pub const fn is_one(self) -> bool { self.ratio.is_one() }
102
103    /// Returns the complementary probability, $1 - P$.
104    ///
105    /// If
106    /// $$
107    /// P(A) = \frac{n}{d},
108    /// $$
109    ///
110    /// then
111    /// $$
112    /// P(A^\complement) = 1 - P(A) = \frac{d-n}{d}.
113    /// $$
114    pub const fn complement(self) -> Self {
115        let (num, den) = self.ratio.num_den();
116        // `num <= den` is a Probability invariant, and the existing denominator is nonzero.
117        let ratio = RatioU64::new_nonzero(den - num, self.ratio.den_nonzero());
118        Self { ratio: ratio.reduced() }
119    }
120}
121
122#[cfg(test)]
123mod _test {
124    use super::*;
125
126    const HALF: Probability = match Probability::new(2, 4) {
127        Some(p) => p,
128        None => panic!("valid probability"),
129    };
130
131    #[test]
132    fn probability_const_construction() {
133        assert_eq!(HALF.num_den(), (1, 2));
134    }
135    #[test]
136    fn probability_accepts_unit_interval() {
137        assert_eq!(Probability::new(0, 1), Some(Probability::ZERO));
138        assert_eq!(Probability::new(1, 1), Some(Probability::ONE));
139        assert!(Probability::new(1, 2).is_some());
140        assert!(Probability::new(5, 7).is_some());
141    }
142    #[test]
143    fn probability_rejects_invalid_terms() {
144        assert_eq!(Probability::new(0, 0), None);
145        assert_eq!(Probability::new(1, 0), None);
146        assert_eq!(Probability::new(2, 1), None);
147        assert_eq!(Probability::new(u64::MAX, 1), None);
148    }
149    #[test]
150    fn probability_canonicalizes_equivalent_ratios() {
151        let a = Probability::new(1, 2).unwrap();
152        let b = Probability::new(2, 4).unwrap();
153        let c = Probability::new(50, 100).unwrap();
154        assert_eq!(a, b);
155        assert_eq!(b, c);
156        assert_eq!(a.num_den(), (1, 2));
157        assert_eq!(b.num_den(), (1, 2));
158        assert_eq!(c.num_den(), (1, 2));
159    }
160    #[test]
161    fn probability_canonicalizes_zero_and_one() {
162        assert_eq!(Probability::new(0, 999).unwrap(), Probability::ZERO);
163        assert_eq!(Probability::new(999, 999).unwrap(), Probability::ONE);
164        assert_eq!(Probability::ZERO.num_den(), (0, 1));
165        assert_eq!(Probability::ONE.num_den(), (1, 1));
166    }
167    #[test]
168    fn probability_constructs_from_ratio() {
169        let ratio = RatioU64::new(6, 8).unwrap();
170        let probability = Probability::from_ratio(ratio).unwrap();
171        assert_eq!(probability.num_den(), (3, 4));
172        let invalid = RatioU64::new(5, 4).unwrap();
173        assert_eq!(Probability::from_ratio(invalid), None);
174    }
175    #[test]
176    fn probability_complement_is_exact() {
177        assert_eq!(Probability::new(1, 4).unwrap().complement(), Probability::new(3, 4).unwrap(),);
178        assert_eq!(Probability::ZERO.complement(), Probability::ONE);
179        assert_eq!(Probability::ONE.complement(), Probability::ZERO);
180        assert_eq!(HALF.complement(), HALF);
181    }
182}