Skip to main content

nodedb_query/ts_functions/
interpolate.rs

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