The situation
The ridge sensor has been logging every five minutes for twelve hours. Most of it is a slow, boring climb in temperature, which is exactly what everyone hoped for. Buried in it are two dropouts โ moments where the sensor didn't answer and the logger wrote -999.0 rather than nothing โ and one reading that claims the ridge briefly hit 21.8 ยฐC, which it did not.
Build the pipeline that turns that trace into something you'd put in a report.
The house rule: no for, no while, no index arithmetic. Every function is an iterator chain. The tests only check results, so you can cheat โ but the whole exercise is the chain, and the chain is shorter than the loop.
Your mission
1. clean(raw: &[f64]) -> Vec<f64>
Drop every reading equal to DROPOUT (-999.0), keep the rest in order. Only the exact sentinel counts โ -998.0 is a real (if alarming) reading.
[1.0, -999.0, 2.0, 3.0] -> [1.0, 2.0, 3.0]
[-999.0, -999.0] -> []2. smooth(readings: &[f64], window: usize) -> Vec<f64>
The average of every consecutive run of window readings. n readings with a window of w give you n - w + 1 averages.
smooth([1.0, 2.0, 3.0, 4.0], 2) -> [1.5, 2.5, 3.5]
smooth([1.0, 2.0], 3) -> [] window longer than the data
smooth([1.0, 2.0], 0) -> [] and this one must not panicThat last row is a trap worth knowing about: the obvious slice method panics on a window of zero rather than returning nothing. Guard it.
3. spikes(readings: &[f64], threshold: f64) -> Vec<usize>
Indices where the step from the previous reading is bigger than threshold in absolute terms. The index reported is the later reading โ the one that jumped.
spikes([10.0, 10.5, 20.0, 20.2], 5.0) -> [2]
spikes([10.0, 2.0, 10.0], 5.0) -> [1, 2] down counts as much as up
spikes([0.0, 5.0], 5.0) -> [] exactly the threshold isn't over it4. summary(readings: &[f64]) -> Option<(f64, f64, f64)>
(min, max, mean), or None for an empty slice. Note what you can't write here: f64 doesn't implement Ord โ because NaN refuses to compare โ so .max() on an iterator of f64 won't compile. Fold instead.
How you're graded
clean_drops_only_the_dropoutssmooth_averages_each_windowsmooth_handles_windows_that_dont_fitโ the zero windowspikes_report_the_later_readingspikes_threshold_is_exclusivesummary_reports_min_max_meansummary_of_nothing_is_nonethe_pipeline_fits_togetherโ runs all four over the real trace
Run draws both series and marks the spikes, so you can see the 21.8 ยฐC outlier and watch the smoothed line refuse to be impressed by it.
Stretch goals
- Make
smoothreturn an iterator instead of aVec. What does the signature have to become, and why isimpl Iterator<Item = f64> + '_the interesting part? - Interpolate across dropouts instead of deleting them โ average of the neighbours.
- Add
fn trend(readings: &[f64]) -> f64: the least-squares slope, as one fold. - Time it: does
smoothallocate more than it needs to? What doeswith_capacitychange?