Skip to main content

use_simple_filter/
lib.rs

1#![forbid(unsafe_code)]
2//! Primitive filtering helpers.
3//!
4//! The crate intentionally keeps filtering narrow: a causal moving average plus
5//! first-order low-pass and high-pass filters.
6//!
7//! # Examples
8//!
9//! ```rust
10//! use use_simple_filter::{first_order_low_pass, moving_average_filter};
11//!
12//! assert_eq!(moving_average_filter(&[1.0, 3.0, 5.0], 2).unwrap(), vec![1.0, 2.0, 4.0]);
13//! assert_eq!(first_order_low_pass(&[0.0, 1.0, 1.0], 0.5).unwrap(), vec![0.0, 0.5, 0.75]);
14//! ```
15
16#[derive(Debug, Clone, Copy, PartialEq, Eq)]
17pub enum FilterError {
18    EmptySamples,
19    InvalidSample,
20    InvalidWindowSize,
21    InvalidAlpha,
22}
23
24fn validate_samples(samples: &[f64]) -> Result<(), FilterError> {
25    if samples.is_empty() {
26        return Err(FilterError::EmptySamples);
27    }
28
29    if samples.iter().any(|sample| !sample.is_finite()) {
30        return Err(FilterError::InvalidSample);
31    }
32
33    Ok(())
34}
35
36fn validate_alpha(alpha: f64) -> Result<(), FilterError> {
37    if !alpha.is_finite() || !(0.0..=1.0).contains(&alpha) {
38        Err(FilterError::InvalidAlpha)
39    } else {
40        Ok(())
41    }
42}
43
44pub fn moving_average_filter(samples: &[f64], window_size: usize) -> Result<Vec<f64>, FilterError> {
45    validate_samples(samples)?;
46
47    if window_size == 0 {
48        return Err(FilterError::InvalidWindowSize);
49    }
50
51    let mut running_sum = 0.0;
52    let mut output = Vec::with_capacity(samples.len());
53
54    for (index, sample) in samples.iter().copied().enumerate() {
55        running_sum += sample;
56
57        if index >= window_size {
58            running_sum -= samples[index - window_size];
59        }
60
61        let divisor = usize::min(index + 1, window_size) as f64;
62        output.push(running_sum / divisor);
63    }
64
65    Ok(output)
66}
67
68pub fn first_order_low_pass(samples: &[f64], alpha: f64) -> Result<Vec<f64>, FilterError> {
69    validate_samples(samples)?;
70    validate_alpha(alpha)?;
71
72    let mut output = Vec::with_capacity(samples.len());
73    let mut previous = samples[0];
74    output.push(previous);
75
76    for sample in &samples[1..] {
77        previous = alpha * sample + (1.0 - alpha) * previous;
78        output.push(previous);
79    }
80
81    Ok(output)
82}
83
84pub fn first_order_high_pass(samples: &[f64], alpha: f64) -> Result<Vec<f64>, FilterError> {
85    validate_samples(samples)?;
86    validate_alpha(alpha)?;
87
88    let mut output = Vec::with_capacity(samples.len());
89    let mut previous_input = samples[0];
90    let mut previous_output = samples[0];
91    output.push(previous_output);
92
93    for sample in &samples[1..] {
94        let current = alpha * (previous_output + sample - previous_input);
95        output.push(current);
96        previous_input = *sample;
97        previous_output = current;
98    }
99
100    Ok(output)
101}
102
103#[cfg(test)]
104mod tests {
105    use super::{FilterError, first_order_high_pass, first_order_low_pass, moving_average_filter};
106
107    #[test]
108    fn applies_moving_average_filter() {
109        assert_eq!(
110            moving_average_filter(&[1.0, 3.0, 5.0, 7.0], 2).unwrap(),
111            vec![1.0, 2.0, 4.0, 6.0]
112        );
113    }
114
115    #[test]
116    fn applies_first_order_low_pass_filter() {
117        assert_eq!(
118            first_order_low_pass(&[0.0, 1.0, 1.0], 0.5).unwrap(),
119            vec![0.0, 0.5, 0.75]
120        );
121    }
122
123    #[test]
124    fn applies_first_order_high_pass_filter() {
125        let filtered = first_order_high_pass(&[1.0, 1.0, 1.0], 0.5).unwrap();
126
127        assert_eq!(filtered, vec![1.0, 0.5, 0.25]);
128    }
129
130    #[test]
131    fn rejects_invalid_inputs() {
132        assert_eq!(
133            moving_average_filter(&[], 2),
134            Err(FilterError::EmptySamples)
135        );
136        assert_eq!(
137            moving_average_filter(&[1.0], 0),
138            Err(FilterError::InvalidWindowSize)
139        );
140        assert_eq!(
141            first_order_low_pass(&[1.0, f64::NAN], 0.5),
142            Err(FilterError::InvalidSample)
143        );
144        assert_eq!(
145            first_order_high_pass(&[1.0], 1.5),
146            Err(FilterError::InvalidAlpha)
147        );
148    }
149
150    #[test]
151    fn handles_single_value_inputs() {
152        assert_eq!(moving_average_filter(&[2.0], 4).unwrap(), vec![2.0]);
153        assert_eq!(first_order_low_pass(&[2.0], 0.2).unwrap(), vec![2.0]);
154        assert_eq!(first_order_high_pass(&[2.0], 0.2).unwrap(), vec![2.0]);
155    }
156}