use_probability/
probability.rs1use core::fmt;
2
3use crate::ProbabilityError;
4
5#[derive(Debug, Clone, Copy, Default, PartialEq, PartialOrd)]
7pub struct Probability {
8 value: f64,
9}
10
11impl Probability {
12 #[must_use]
14 pub const fn new(value: f64) -> Self {
15 Self { value }
16 }
17
18 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 #[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 pub const fn validate(self) -> Result<Self, ProbabilityError> {
75 Self::try_new(self.value)
76 }
77
78 #[must_use]
80 pub const fn value(&self) -> f64 {
81 self.value
82 }
83
84 #[must_use]
86 pub const fn as_percentage(&self) -> f64 {
87 self.value * 100.0
88 }
89
90 #[must_use]
92 pub const fn complement(self) -> Self {
93 Self::new(1.0 - self.value)
94 }
95
96 #[must_use]
98 pub const fn impossible() -> Self {
99 Self::new(0.0)
100 }
101
102 #[must_use]
104 pub const fn certainty() -> Self {
105 Self::new(1.0)
106 }
107
108 #[must_use]
110 pub const fn intersection_independent(self, other: Self) -> Self {
111 Self::new(self.value * other.value)
112 }
113
114 #[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#[must_use]
136pub const fn independent_intersection(left: Probability, right: Probability) -> Probability {
137 left.intersection_independent(right)
138}
139
140#[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}