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 Some(prev_val) = values[pi] else { continue };
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 Some(fv) = values[first]
115    {
116        for r in result.iter_mut().take(first) {
117            *r = fv;
118        }
119    }
120    // Edge: fill trailing NULLs with last known value.
121    if let Some(last) = values.iter().rposition(|v| v.is_some())
122        && let Some(lv) = values[last]
123    {
124        for r in result.iter_mut().skip(last + 1) {
125            *r = lv;
126        }
127    }
128
129    result
130}
131
132#[cfg(test)]
133mod tests {
134    use super::*;
135
136    fn s(v: f64) -> Option<f64> {
137        Some(v)
138    }
139
140    #[test]
141    fn zero_fill() {
142        let r = ts_interpolate(&[s(1.0), None, s(3.0)], &[0, 1, 2], InterpolateMethod::Zero);
143        assert_eq!(r, vec![1.0, 0.0, 3.0]);
144    }
145
146    #[test]
147    fn previous_fill() {
148        let r = ts_interpolate(
149            &[s(10.0), None, None, s(40.0)],
150            &[0, 1, 2, 3],
151            InterpolateMethod::Previous,
152        );
153        assert_eq!(r, vec![10.0, 10.0, 10.0, 40.0]);
154    }
155
156    #[test]
157    fn next_fill() {
158        let r = ts_interpolate(
159            &[None, None, s(30.0), s(40.0)],
160            &[0, 1, 2, 3],
161            InterpolateMethod::Next,
162        );
163        assert_eq!(r, vec![30.0, 30.0, 30.0, 40.0]);
164    }
165
166    #[test]
167    fn linear_fill() {
168        let r = ts_interpolate(
169            &[s(0.0), None, None, s(30.0)],
170            &[0, 10, 20, 30],
171            InterpolateMethod::Linear,
172        );
173        assert!((r[0] - 0.0).abs() < 1e-12);
174        assert!((r[1] - 10.0).abs() < 1e-12);
175        assert!((r[2] - 20.0).abs() < 1e-12);
176        assert!((r[3] - 30.0).abs() < 1e-12);
177    }
178
179    #[test]
180    fn linear_edge_extrapolation() {
181        // Leading and trailing NULLs → flat fill from nearest known.
182        let r = ts_interpolate(
183            &[None, s(10.0), None, s(30.0), None],
184            &[0, 10, 20, 30, 40],
185            InterpolateMethod::Linear,
186        );
187        assert!((r[0] - 10.0).abs() < 1e-12); // leading → first known
188        assert!((r[2] - 20.0).abs() < 1e-12); // interior → linear
189        assert!((r[4] - 30.0).abs() < 1e-12); // trailing → last known
190    }
191
192    #[test]
193    fn all_present() {
194        let r = ts_interpolate(
195            &[s(1.0), s(2.0), s(3.0)],
196            &[0, 1, 2],
197            InterpolateMethod::Linear,
198        );
199        assert_eq!(r, vec![1.0, 2.0, 3.0]);
200    }
201
202    #[test]
203    fn empty() {
204        assert!(ts_interpolate(&[], &[], InterpolateMethod::Linear).is_empty());
205    }
206
207    #[test]
208    fn parse_method() {
209        assert_eq!(
210            InterpolateMethod::parse("linear"),
211            Some(InterpolateMethod::Linear)
212        );
213        assert_eq!(
214            InterpolateMethod::parse("PREV"),
215            Some(InterpolateMethod::Previous)
216        );
217        assert_eq!(
218            InterpolateMethod::parse("locf"),
219            Some(InterpolateMethod::Previous)
220        );
221        assert_eq!(
222            InterpolateMethod::parse("next"),
223            Some(InterpolateMethod::Next)
224        );
225        assert_eq!(
226            InterpolateMethod::parse("nocb"),
227            Some(InterpolateMethod::Next)
228        );
229        assert_eq!(
230            InterpolateMethod::parse("zero"),
231            Some(InterpolateMethod::Zero)
232        );
233        assert_eq!(InterpolateMethod::parse("unknown"), None);
234    }
235}