rust_fuzzylogic/
sampler.rs

1use crate::{error::FuzzyError, prelude::*, Float};
2
3pub trait Sampler {
4    //Trait shape: Returning Result<Vec<Float>> is fine.
5    //If you later need performance, consider an iterator or a small wrapper type, but not necessary now.
6    fn sample(&self, min: Float, max: Float) -> Result<Vec<Float>>;
7}
8
9pub struct UniformSampler {
10    pub n: usize,
11}
12
13impl Default for UniformSampler {
14    fn default() -> Self {
15        Self { n: Self::DEFAULT_N }
16    }
17}
18
19impl UniformSampler {
20    pub const DEFAULT_N: usize = 101;
21
22    pub fn new(n: usize) -> Result<Self> {
23        if n < 2 {
24            return Err(FuzzyError::OutOfBounds);
25        }
26        Ok(Self { n: n })
27    }
28}
29
30impl Sampler for UniformSampler {
31    fn sample(&self, min: Float, max: Float) -> Result<Vec<Float>> {
32        if min >= max {
33            return Err(FuzzyError::BadArity);
34        }
35
36        if !(min.is_finite() && max.is_finite()) {
37            return Err(FuzzyError::BadArity);
38        }
39
40        let n = self.n;
41        let mut sample: Vec<Float> = Vec::with_capacity(n);
42        let step = (max - min) / (n as Float - 1.0);
43
44        for i in 0..n {
45            sample.push(min + i as Float * step)
46        }
47        sample[n - 1] = max;
48
49        Ok(sample)
50    }
51}
52
53#[cfg(test)]
54mod tests {
55    use crate::error::FuzzyError;
56    use crate::sampler::{Sampler, UniformSampler};
57    use crate::Float;
58
59    #[test]
60    fn uniform_sampler_two_points_inclusive_endpoints() {
61        let s = UniformSampler::new(2).unwrap();
62        let min: Float = -3.5;
63        let max: Float = 4.5;
64        let pts = s.sample(min, max).unwrap();
65        assert_eq!(pts.len(), 2);
66        assert_eq!(pts[0], min);
67        assert_eq!(pts[1], max, "Last point must equal max for n=2");
68    }
69
70    #[test]
71    fn uniform_sampler_inclusive_endpoints_default() {
72        let s = UniformSampler::default();
73        let n = UniformSampler::DEFAULT_N;
74        let min: Float = -5.0;
75        let max: Float = 5.0;
76        let pts = s.sample(min, max).unwrap();
77        assert_eq!(pts.len(), n);
78        assert_eq!(pts.first().copied().unwrap(), min);
79        assert_eq!(
80            pts.last().copied().unwrap(),
81            max,
82            "Sampler should include max exactly"
83        );
84    }
85
86    #[test]
87    fn uniform_sampler_spacing_monotonic() {
88        let s = UniformSampler::default();
89        let min: Float = 0.0;
90        let max: Float = 10.0;
91        let pts = s.sample(min, max).unwrap();
92        assert!(pts.windows(2).all(|w| w[1] >= w[0]));
93
94        // Check approximate uniform spacing consistency across interior points
95        let eps = Float::EPSILON * 10.0;
96        let base_step = pts[1] - pts[0];
97        for i in 2..pts.len() {
98            let step = pts[i] - pts[i - 1];
99            assert!((step - base_step).abs() <= eps, "Non-uniform step at i={i}");
100        }
101    }
102
103    #[test]
104    fn uniform_sampler_invalid_points_rejected() {
105        assert!(matches!(
106            UniformSampler::new(0),
107            Err(FuzzyError::OutOfBounds)
108        ));
109        assert!(matches!(
110            UniformSampler::new(1),
111            Err(FuzzyError::OutOfBounds)
112        ));
113    }
114
115    #[test]
116    fn uniform_sampler_invalid_range_rejected() {
117        let s = UniformSampler::default();
118        // min > max must error
119        assert!(matches!(s.sample(1.0, 0.0), Err(FuzzyError::BadArity)));
120        // Degenerate range should be rejected for a sampler that requires >=2 distinct points
121        assert!(matches!(s.sample(1.0, 1.0), Err(_)));
122    }
123}