Skip to main content

nodedb_query/ts_functions/
interpolate.rs

1//! Time-series gap filling (interpolation).
2
3/// Interpolation method for filling NULL gaps.
4#[derive(Debug, Clone, Copy, PartialEq, Eq)]
5pub enum InterpolateMethod {
6    /// Linear interpolation using surrounding non-NULL values and timestamps.
7    Linear,
8    /// Carry forward the last known value (LOCF).
9    Previous,
10    /// Carry backward the next known value (NOCB).
11    Next,
12    /// Replace NULLs with zero.
13    Zero,
14}
15
16impl InterpolateMethod {
17    /// Parse from a case-insensitive string.
18    pub fn parse(s: &str) -> Option<Self> {
19        match s.to_ascii_lowercase().as_str() {
20            "linear" => Some(Self::Linear),
21            "prev" | "previous" | "locf" => Some(Self::Previous),
22            "next" | "nocb" => Some(Self::Next),
23            "zero" => Some(Self::Zero),
24            _ => None,
25        }
26    }
27}
28
29/// Fill NULL gaps in a time series.
30///
31/// `values` — `Some(v)` for present samples, `None` for gaps.
32/// `timestamps_ns` — epoch nanoseconds, same length as `values`.
33///
34/// Edge handling for `Linear`: gaps before the first known value or
35/// after the last known value are filled with the nearest known value
36/// (flat extrapolation, not unbounded linear extrapolation).
37pub fn ts_interpolate(
38    values: &[Option<f64>],
39    timestamps_ns: &[i64],
40    method: InterpolateMethod,
41) -> Vec<f64> {
42    debug_assert_eq!(values.len(), timestamps_ns.len());
43    let n = values.len();
44    if n == 0 {
45        return vec![];
46    }
47
48    match method {
49        InterpolateMethod::Zero => values.iter().map(|v| v.unwrap_or(0.0)).collect(),
50        InterpolateMethod::Previous => fill_previous(values),
51        InterpolateMethod::Next => fill_next(values),
52        InterpolateMethod::Linear => fill_linear(values, timestamps_ns),
53    }
54}
55
56fn fill_previous(values: &[Option<f64>]) -> Vec<f64> {
57    let mut result = Vec::with_capacity(values.len());
58    let mut last = 0.0;
59    for v in values {
60        match v {
61            Some(val) => {
62                last = *val;
63                result.push(*val);
64            }
65            None => result.push(last),
66        }
67    }
68    result
69}
70
71fn fill_next(values: &[Option<f64>]) -> Vec<f64> {
72    let n = values.len();
73    let mut result = vec![0.0; n];
74    let mut next_val = 0.0;
75    for i in (0..n).rev() {
76        match values[i] {
77            Some(val) => {
78                next_val = val;
79                result[i] = val;
80            }
81            None => result[i] = next_val,
82        }
83    }
84    result
85}
86
87fn fill_linear(values: &[Option<f64>], timestamps_ns: &[i64]) -> Vec<f64> {
88    let n = values.len();
89    let mut result = vec![0.0; n];
90
91    // Copy known values and linearly interpolate gaps.
92    let mut prev_idx: Option<usize> = None;
93
94    for i in 0..n {
95        if let Some(val) = values[i] {
96            // Fill the gap between prev_idx and i.
97            if let Some(pi) = prev_idx {
98                let prev_val = values[pi].unwrap();
99                let dt = (timestamps_ns[i] - timestamps_ns[pi]) as f64;
100                if dt > 0.0 && pi + 1 < i {
101                    for j in (pi + 1)..i {
102                        let frac = (timestamps_ns[j] - timestamps_ns[pi]) as f64 / dt;
103                        result[j] = prev_val + frac * (val - prev_val);
104                    }
105                }
106            }
107            result[i] = val;
108            prev_idx = Some(i);
109        }
110    }
111
112    // Edge: fill leading NULLs with first known value.
113    if let Some(first) = values.iter().position(|v| v.is_some()) {
114        let fv = values[first].unwrap();
115        for r in result.iter_mut().take(first) {
116            *r = fv;
117        }
118    }
119    // Edge: fill trailing NULLs with last known value.
120    if let Some(last) = values.iter().rposition(|v| v.is_some()) {
121        let lv = values[last].unwrap();
122        for r in result.iter_mut().skip(last + 1) {
123            *r = lv;
124        }
125    }
126
127    result
128}
129
130#[cfg(test)]
131mod tests {
132    use super::*;
133
134    fn s(v: f64) -> Option<f64> {
135        Some(v)
136    }
137
138    #[test]
139    fn zero_fill() {
140        let r = ts_interpolate(&[s(1.0), None, s(3.0)], &[0, 1, 2], InterpolateMethod::Zero);
141        assert_eq!(r, vec![1.0, 0.0, 3.0]);
142    }
143
144    #[test]
145    fn previous_fill() {
146        let r = ts_interpolate(
147            &[s(10.0), None, None, s(40.0)],
148            &[0, 1, 2, 3],
149            InterpolateMethod::Previous,
150        );
151        assert_eq!(r, vec![10.0, 10.0, 10.0, 40.0]);
152    }
153
154    #[test]
155    fn next_fill() {
156        let r = ts_interpolate(
157            &[None, None, s(30.0), s(40.0)],
158            &[0, 1, 2, 3],
159            InterpolateMethod::Next,
160        );
161        assert_eq!(r, vec![30.0, 30.0, 30.0, 40.0]);
162    }
163
164    #[test]
165    fn linear_fill() {
166        let r = ts_interpolate(
167            &[s(0.0), None, None, s(30.0)],
168            &[0, 10, 20, 30],
169            InterpolateMethod::Linear,
170        );
171        assert!((r[0] - 0.0).abs() < 1e-12);
172        assert!((r[1] - 10.0).abs() < 1e-12);
173        assert!((r[2] - 20.0).abs() < 1e-12);
174        assert!((r[3] - 30.0).abs() < 1e-12);
175    }
176
177    #[test]
178    fn linear_edge_extrapolation() {
179        // Leading and trailing NULLs → flat fill from nearest known.
180        let r = ts_interpolate(
181            &[None, s(10.0), None, s(30.0), None],
182            &[0, 10, 20, 30, 40],
183            InterpolateMethod::Linear,
184        );
185        assert!((r[0] - 10.0).abs() < 1e-12); // leading → first known
186        assert!((r[2] - 20.0).abs() < 1e-12); // interior → linear
187        assert!((r[4] - 30.0).abs() < 1e-12); // trailing → last known
188    }
189
190    #[test]
191    fn all_present() {
192        let r = ts_interpolate(
193            &[s(1.0), s(2.0), s(3.0)],
194            &[0, 1, 2],
195            InterpolateMethod::Linear,
196        );
197        assert_eq!(r, vec![1.0, 2.0, 3.0]);
198    }
199
200    #[test]
201    fn empty() {
202        assert!(ts_interpolate(&[], &[], InterpolateMethod::Linear).is_empty());
203    }
204
205    #[test]
206    fn parse_method() {
207        assert_eq!(
208            InterpolateMethod::parse("linear"),
209            Some(InterpolateMethod::Linear)
210        );
211        assert_eq!(
212            InterpolateMethod::parse("PREV"),
213            Some(InterpolateMethod::Previous)
214        );
215        assert_eq!(
216            InterpolateMethod::parse("locf"),
217            Some(InterpolateMethod::Previous)
218        );
219        assert_eq!(
220            InterpolateMethod::parse("next"),
221            Some(InterpolateMethod::Next)
222        );
223        assert_eq!(
224            InterpolateMethod::parse("nocb"),
225            Some(InterpolateMethod::Next)
226        );
227        assert_eq!(
228            InterpolateMethod::parse("zero"),
229            Some(InterpolateMethod::Zero)
230        );
231        assert_eq!(InterpolateMethod::parse("unknown"), None);
232    }
233}