uncertain_rs/operations/
comparison.rs

1#![allow(clippy::cast_precision_loss)]
2
3use crate::Uncertain;
4use crate::traits::Shareable;
5
6/// Trait for comparison operations that return uncertain boolean evidence
7///
8/// The key insight from the paper: comparisons return `Uncertain<bool>` (evidence),
9/// not `bool` (boolean facts). This prevents uncertainty bugs.
10///
11/// **Note**: For most use cases, prefer the method-based API (e.g., `value.gt(threshold)`)
12/// over the trait-based API (e.g., `Comparison::gt(&value, threshold)`). The trait is
13/// primarily useful for generic programming and advanced scenarios.
14pub trait Comparison<T> {
15    /// Returns uncertain boolean evidence that this value is greater than threshold
16    #[must_use]
17    fn gt(&self, threshold: T) -> Uncertain<bool>;
18
19    /// Returns uncertain boolean evidence that this value is less than threshold
20    #[must_use]
21    fn lt(&self, threshold: T) -> Uncertain<bool>;
22
23    /// Returns uncertain boolean evidence that this value is greater than or equal to threshold
24    #[must_use]
25    fn ge(&self, threshold: T) -> Uncertain<bool>;
26
27    /// Returns uncertain boolean evidence that this value is less than or equal to threshold
28    #[must_use]
29    fn le(&self, threshold: T) -> Uncertain<bool>;
30
31    /// Returns uncertain boolean evidence that this value equals threshold
32    #[must_use]
33    fn eq(&self, threshold: T) -> Uncertain<bool>;
34
35    /// Returns uncertain boolean evidence that this value does not equal threshold
36    #[must_use]
37    fn ne(&self, threshold: T) -> Uncertain<bool>;
38}
39
40impl<T> Comparison<T> for Uncertain<T>
41where
42    T: PartialOrd + PartialEq + Shareable,
43{
44    /// Greater than comparison
45    ///
46    /// # Example
47    /// ```rust
48    /// use uncertain_rs::Uncertain;
49    ///
50    /// let speed = Uncertain::normal(55.0, 5.0);
51    /// let speeding_evidence = speed.gt(60.0);
52    ///
53    /// if speeding_evidence.probability_exceeds(0.95) {
54    ///     println!("95% confident speeding");
55    /// }
56    /// ```
57    fn gt(&self, threshold: T) -> Uncertain<bool> {
58        let sample_fn = self.sample_fn.clone();
59        Uncertain::new(move || sample_fn() > threshold)
60    }
61
62    /// Less than comparison
63    ///
64    /// # Example
65    /// ```rust
66    /// use uncertain_rs::Uncertain;
67    ///
68    /// let temperature = Uncertain::normal(1.0, 2.0);
69    /// let freezing_evidence = temperature.lt(0.0);
70    ///
71    /// if freezing_evidence.probability_exceeds(0.8) {
72    ///     println!("Likely freezing");
73    /// }
74    /// ```
75    fn lt(&self, threshold: T) -> Uncertain<bool> {
76        let sample_fn = self.sample_fn.clone();
77        Uncertain::new(move || sample_fn() < threshold)
78    }
79
80    /// Greater than or equal comparison
81    fn ge(&self, threshold: T) -> Uncertain<bool> {
82        let sample_fn = self.sample_fn.clone();
83        Uncertain::new(move || sample_fn() >= threshold)
84    }
85
86    /// Less than or equal comparison
87    fn le(&self, threshold: T) -> Uncertain<bool> {
88        let sample_fn = self.sample_fn.clone();
89        Uncertain::new(move || sample_fn() <= threshold)
90    }
91
92    /// Equality comparison
93    ///
94    /// Note: For floating point types, exact equality is rarely meaningful.
95    /// Consider using range-based comparisons instead.
96    fn eq(&self, threshold: T) -> Uncertain<bool> {
97        let sample_fn = self.sample_fn.clone();
98        Uncertain::new(move || sample_fn() == threshold)
99    }
100
101    /// Inequality comparison
102    fn ne(&self, threshold: T) -> Uncertain<bool> {
103        let sample_fn = self.sample_fn.clone();
104        Uncertain::new(move || sample_fn() != threshold)
105    }
106}
107
108// Comparisons between two uncertain values
109impl<T> Uncertain<T>
110where
111    T: PartialOrd + PartialEq + Shareable,
112{
113    /// Compare two uncertain values for greater than
114    ///
115    /// # Example
116    /// ```rust
117    /// use uncertain_rs::Uncertain;
118    ///
119    /// let sensor1 = Uncertain::normal(10.0, 1.0);
120    /// let sensor2 = Uncertain::normal(12.0, 1.0);
121    /// let evidence = sensor2.gt_uncertain(&sensor1);
122    /// ```
123    #[must_use]
124    pub fn gt_uncertain(&self, other: &Self) -> Uncertain<bool> {
125        let sample_fn1 = self.sample_fn.clone();
126        let sample_fn2 = other.sample_fn.clone();
127        Uncertain::new(move || sample_fn1() > sample_fn2())
128    }
129
130    /// Compare two uncertain values for less than
131    #[must_use]
132    pub fn lt_uncertain(&self, other: &Self) -> Uncertain<bool> {
133        let sample_fn1 = self.sample_fn.clone();
134        let sample_fn2 = other.sample_fn.clone();
135        Uncertain::new(move || sample_fn1() < sample_fn2())
136    }
137
138    /// Compare two uncertain values for equality
139    #[must_use]
140    pub fn eq_uncertain(&self, other: &Self) -> Uncertain<bool> {
141        let sample_fn1 = self.sample_fn.clone();
142        let sample_fn2 = other.sample_fn.clone();
143        Uncertain::new(move || sample_fn1() == sample_fn2())
144    }
145}
146
147// Floating point specific comparisons
148impl Uncertain<f64> {
149    /// Check if value is approximately equal within tolerance
150    ///
151    /// # Example
152    /// ```rust
153    /// use uncertain_rs::Uncertain;
154    ///
155    /// let measurement = Uncertain::normal(10.0, 0.1);
156    /// let target = 10.0;
157    /// let tolerance = 0.5;
158    ///
159    /// let close_evidence = measurement.approx_eq(target, tolerance);
160    /// ```
161    #[must_use]
162    pub fn approx_eq(&self, target: f64, tolerance: f64) -> Uncertain<bool> {
163        self.map(move |x| (x - target).abs() <= tolerance)
164    }
165
166    /// Check if value is within a range
167    ///
168    /// # Example
169    /// ```rust
170    /// use uncertain_rs::Uncertain;
171    ///
172    /// let measurement = Uncertain::normal(10.0, 2.0);
173    /// let in_range = measurement.within_range(8.0, 12.0);
174    /// ```
175    #[must_use]
176    pub fn within_range(&self, min: f64, max: f64) -> Uncertain<bool> {
177        self.map(move |x| x >= min && x <= max)
178    }
179}
180
181#[cfg(test)]
182mod tests {
183    use super::*;
184
185    #[test]
186    fn test_comparison_returns_uncertain_bool() {
187        let value = Uncertain::point(5.0);
188        let evidence = Comparison::gt(&value, 3.0);
189
190        // Should return Uncertain<bool>, not bool
191        assert!(evidence.sample()); // 5 > 3 is always true
192    }
193
194    #[test]
195    fn test_comparison_with_uncertainty() {
196        let value = Uncertain::normal(5.0, 1.0);
197        let evidence = Comparison::gt(&value, 4.0);
198
199        // With normal(5, 1), most samples should be > 4
200        let samples: Vec<bool> = evidence.take_samples(1000);
201        let true_ratio = samples.iter().filter(|&&x| x).count() as f64 / samples.len() as f64;
202        assert!(true_ratio > 0.8); // Should be high probability
203    }
204
205    #[test]
206    fn test_approximate_equality() {
207        let measurement = Uncertain::normal(10.0, 0.1);
208        let close = measurement.approx_eq(10.0, 0.5);
209
210        // With small std dev, should almost always be close to 10
211        let samples: Vec<bool> = close.take_samples(100);
212        let true_ratio = samples.iter().filter(|&&x| x).count() as f64 / samples.len() as f64;
213        assert!(true_ratio > 0.95);
214    }
215
216    #[test]
217    fn test_within_range() {
218        let value = Uncertain::uniform(0.0, 10.0);
219        let in_range = value.within_range(2.0, 8.0);
220
221        // About 60% of uniform[0,10] should be in [2,8]
222        let samples: Vec<bool> = in_range.take_samples(1000);
223        let true_ratio = samples.iter().filter(|&&x| x).count() as f64 / samples.len() as f64;
224        assert!((true_ratio - 0.6).abs() < 0.1);
225    }
226
227    #[test]
228    fn test_uncertain_vs_uncertain_comparison() {
229        let x = Uncertain::normal(5.0, 1.0);
230        let y = Uncertain::normal(3.0, 1.0);
231        let evidence = x.gt_uncertain(&y);
232
233        // x should usually be greater than y
234        let samples: Vec<bool> = evidence.take_samples(1000);
235        let true_ratio = samples.iter().filter(|&&x| x).count() as f64 / samples.len() as f64;
236        assert!(true_ratio > 0.8);
237    }
238}