Skip to main content

use_zero_crossing/
lib.rs

1#![forbid(unsafe_code)]
2//! Primitive zero-crossing helpers.
3//!
4//! Exact zero values are treated as neutral samples: they do not create a
5//! crossing on their own, but they also do not break the sign continuity
6//! between surrounding non-zero finite samples.
7//!
8//! # Examples
9//!
10//! ```rust
11//! use use_zero_crossing::{crosses_zero, zero_crossing_count, zero_crossing_rate};
12//!
13//! let samples = [-1.0, 0.0, 1.0, -1.0];
14//!
15//! assert!(crosses_zero(-1.0, 1.0));
16//! assert_eq!(zero_crossing_count(&samples), 2);
17//! assert_eq!(zero_crossing_rate(&samples), Some(2.0 / 3.0));
18//! ```
19
20fn non_zero_sign(sample: f64) -> Option<i8> {
21    if !sample.is_finite() || sample == 0.0 {
22        None
23    } else if sample.is_sign_positive() {
24        Some(1)
25    } else {
26        Some(-1)
27    }
28}
29
30#[must_use]
31pub fn crosses_zero(previous: f64, current: f64) -> bool {
32    previous.is_finite()
33        && current.is_finite()
34        && ((previous < 0.0 && current > 0.0) || (previous > 0.0 && current < 0.0))
35}
36
37#[must_use]
38pub fn zero_crossing_count(samples: &[f64]) -> usize {
39    let mut previous_sign = None;
40    let mut count = 0;
41
42    for sample in samples.iter().copied() {
43        match non_zero_sign(sample) {
44            Some(sign) => {
45                if let Some(previous) = previous_sign
46                    && previous != sign
47                {
48                    count += 1;
49                }
50
51                previous_sign = Some(sign);
52            },
53            None if !sample.is_finite() => previous_sign = None,
54            None => {},
55        }
56    }
57
58    count
59}
60
61pub fn zero_crossing_rate(samples: &[f64]) -> Option<f64> {
62    if samples.len() < 2 || samples.iter().any(|sample| !sample.is_finite()) {
63        return None;
64    }
65
66    Some(zero_crossing_count(samples) as f64 / (samples.len() - 1) as f64)
67}
68
69#[cfg(test)]
70mod tests {
71    use super::{crosses_zero, zero_crossing_count, zero_crossing_rate};
72
73    #[test]
74    fn detects_direct_crossings() {
75        assert!(crosses_zero(-1.0, 1.0));
76        assert!(crosses_zero(1.0, -1.0));
77        assert!(!crosses_zero(-1.0, 0.0));
78        assert!(!crosses_zero(0.0, 1.0));
79    }
80
81    #[test]
82    fn counts_crossings_while_skipping_exact_zero_samples() {
83        assert_eq!(zero_crossing_count(&[-1.0, 0.0, 1.0, 0.0, -1.0]), 2);
84        assert_eq!(zero_crossing_count(&[1.0]), 0);
85    }
86
87    #[test]
88    fn computes_zero_crossing_rate() {
89        assert_eq!(zero_crossing_rate(&[-1.0, 0.0, 1.0, -1.0]), Some(2.0 / 3.0));
90    }
91
92    #[test]
93    fn rejects_invalid_rate_inputs() {
94        assert_eq!(zero_crossing_rate(&[]), None);
95        assert_eq!(zero_crossing_rate(&[1.0]), None);
96        assert_eq!(zero_crossing_rate(&[1.0, f64::NAN]), None);
97    }
98}