1#![forbid(unsafe_code)]
2fn 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 && previous != sign {
46 count += 1;
47 }
48
49 previous_sign = Some(sign);
50 }
51 None if !sample.is_finite() => previous_sign = None,
52 None => {}
53 }
54 }
55
56 count
57}
58
59pub fn zero_crossing_rate(samples: &[f64]) -> Option<f64> {
60 if samples.len() < 2 || samples.iter().any(|sample| !sample.is_finite()) {
61 return None;
62 }
63
64 Some(zero_crossing_count(samples) as f64 / (samples.len() - 1) as f64)
65}
66
67#[cfg(test)]
68mod tests {
69 use super::{crosses_zero, zero_crossing_count, zero_crossing_rate};
70
71 #[test]
72 fn detects_direct_crossings() {
73 assert!(crosses_zero(-1.0, 1.0));
74 assert!(crosses_zero(1.0, -1.0));
75 assert!(!crosses_zero(-1.0, 0.0));
76 assert!(!crosses_zero(0.0, 1.0));
77 }
78
79 #[test]
80 fn counts_crossings_while_skipping_exact_zero_samples() {
81 assert_eq!(zero_crossing_count(&[-1.0, 0.0, 1.0, 0.0, -1.0]), 2);
82 assert_eq!(zero_crossing_count(&[1.0]), 0);
83 }
84
85 #[test]
86 fn computes_zero_crossing_rate() {
87 assert_eq!(zero_crossing_rate(&[-1.0, 0.0, 1.0, -1.0]), Some(2.0 / 3.0));
88 }
89
90 #[test]
91 fn rejects_invalid_rate_inputs() {
92 assert_eq!(zero_crossing_rate(&[]), None);
93 assert_eq!(zero_crossing_rate(&[1.0]), None);
94 assert_eq!(zero_crossing_rate(&[1.0, f64::NAN]), None);
95 }
96}