diff --git a/Cargo.lock b/Cargo.lock index 08277c9..36d11a7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,15 @@ # It is not intended for manual editing. version = 3 +[[package]] +name = "aho-corasick" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" +dependencies = [ + "memchr", +] + [[package]] name = "android-tzdata" version = "0.1.1" @@ -143,6 +152,12 @@ version = "1.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1056f553da426e9c025a662efa48b52e62e0a3a7648aa2d15aeaaf7f0d329357" +[[package]] +name = "equivalent" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" + [[package]] name = "fnv" version = "1.0.7" @@ -151,9 +166,9 @@ checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" [[package]] name = "futures" -version = "0.3.29" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da0290714b38af9b4a7b094b8a37086d1b4e61f2df9122c3cad2577669145335" +checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876" dependencies = [ "futures-channel", "futures-core", @@ -166,9 +181,9 @@ dependencies = [ [[package]] name = "futures-channel" -version = "0.3.29" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff4dd66668b557604244583e3e1e1eada8c5c2e96a6d0d6653ede395b78bbacb" +checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" dependencies = [ "futures-core", "futures-sink", @@ -176,15 +191,15 @@ dependencies = [ [[package]] name = "futures-core" -version = "0.3.29" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eb1d22c66e66d9d72e1758f0bd7d4fd0bee04cad842ee34587d68c07e45d088c" +checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" [[package]] name = "futures-executor" -version = "0.3.29" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0f4fb8693db0cf099eadcca0efe2a5a22e4550f98ed16aba6c48700da29597bc" +checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f" dependencies = [ "futures-core", "futures-task", @@ -193,38 +208,44 @@ dependencies = [ [[package]] name = "futures-io" -version = "0.3.29" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8bf34a163b5c4c52d0478a4d757da8fb65cabef42ba90515efee0f6f9fa45aaa" +checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" [[package]] name = "futures-macro" -version = "0.3.29" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53b153fd91e4b0147f4aced87be237c98248656bb01050b96bf3ee89220a8ddb" +checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" dependencies = [ "proc-macro2", "quote", - "syn 2.0.41", + "syn 2.0.87", ] [[package]] name = "futures-sink" -version = "0.3.29" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e36d3378ee38c2a36ad710c5d30c2911d752cb941c00c72dbabfb786a7970817" +checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" [[package]] name = "futures-task" -version = "0.3.29" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" + +[[package]] +name = "futures-timer" +version = "3.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "efd193069b0ddadc69c46389b740bbccdd97203899b48d09c5f7969591d6bae2" +checksum = "f288b0a4f20f9a56b5d1da57e2227c661b7b16168e2f72365f57b63326e29b24" [[package]] name = "futures-util" -version = "0.3.29" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a19526d624e703a3179b3d322efec918b6246ea0fa51d41124525f00f1cc8104" +checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" dependencies = [ "futures-channel", "futures-core", @@ -273,6 +294,12 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "glob" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b" + [[package]] name = "gloo-console" version = "0.3.0" @@ -372,6 +399,12 @@ version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" +[[package]] +name = "hashbrown" +version = "0.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a9bfc1af68b1726ea47d3d5109de126281def866b33970e10fbab11b5dafab3" + [[package]] name = "http" version = "1.1.0" @@ -413,7 +446,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" dependencies = [ "autocfg", - "hashbrown", + "hashbrown 0.12.3", +] + +[[package]] +name = "indexmap" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "707907fe3c25f5424cce2cb7e1cbcafee6bdbe735ca90ef77c29e84591e5b9da" +dependencies = [ + "equivalent", + "hashbrown 0.15.1", ] [[package]] @@ -527,6 +570,15 @@ dependencies = [ "yansi", ] +[[package]] +name = "proc-macro-crate" +version = "3.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecf48c7ca261d60b74ab1a7b20da18bede46776b2e55535cb958eb595c5fa7b" +dependencies = [ + "toml_edit", +] + [[package]] name = "proc-macro-hack" version = "0.5.20+deprecated" @@ -535,9 +587,9 @@ checksum = "dc375e1527247fe1a97d8b7156678dfe7c1af2fc075c9a4db3690ecd2a148068" [[package]] name = "proc-macro2" -version = "1.0.70" +version = "1.0.89" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "39278fbbf5fb4f646ce651690877f89d1c5811a3d4acb27700c1cb3cdb78fd3b" +checksum = "f139b0662de085916d1fb67d2b4169d1addddda1919e696f3252b740b629986e" dependencies = [ "unicode-ident", ] @@ -556,9 +608,9 @@ dependencies = [ [[package]] name = "quote" -version = "1.0.33" +version = "1.0.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5267fca4496028628a95160fc423a33e8b2e6af8a5302579e322e4b520293cae" +checksum = "b5b9d34b8991d19d98081b46eacdd8eb58c6f2b201139f7c5f643cc155a633af" dependencies = [ "proc-macro2", ] @@ -614,13 +666,87 @@ dependencies = [ "rand_core", ] +[[package]] +name = "regex" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "368758f23274712b504848e9d5a6f010445cc8b87a7cdb4d7cbee666c1288da3" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" + +[[package]] +name = "relative-path" +version = "1.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba39f3699c378cd8970968dcbff9c43159ea4cfbd88d43c00b22f2ef10a435d2" + +[[package]] +name = "rstest" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a2c585be59b6b5dd66a9d2084aa1d8bd52fbdb806eafdeffb52791147862035" +dependencies = [ + "futures", + "futures-timer", + "rstest_macros", + "rustc_version 0.4.1", +] + +[[package]] +name = "rstest_macros" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "825ea780781b15345a146be27eaefb05085e337e869bff01b4306a4fd4a9ad5a" +dependencies = [ + "cfg-if", + "glob", + "proc-macro-crate", + "proc-macro2", + "quote", + "regex", + "relative-path", + "rustc_version 0.4.1", + "syn 2.0.87", + "unicode-ident", +] + [[package]] name = "rustc_version" version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "138e3e0acb6c9fb258b19b67cb8abd63c00679d2851805ea151465464fe9030a" dependencies = [ - "semver", + "semver 0.9.0", +] + +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver 1.0.23", ] [[package]] @@ -642,7 +768,7 @@ dependencies = [ "futures", "gloo-file", "gloo-timers", - "indexmap", + "indexmap 1.9.3", "js-sys", "pulldown-cmark", "rand", @@ -664,6 +790,12 @@ dependencies = [ "semver-parser", ] +[[package]] +name = "semver" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61697e0a1c7e512e84a621326239844a24d8207b4669b41bc18b32ea5cbf988b" + [[package]] name = "semver-parser" version = "0.7.0" @@ -687,7 +819,7 @@ checksum = "43576ca501357b9b071ac53cdc7da8ef0cbd9493d8df094cd821777ea6e894d3" dependencies = [ "proc-macro2", "quote", - "syn 2.0.41", + "syn 2.0.87", ] [[package]] @@ -747,7 +879,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d022496b16281348b52d0e30ae99e01a73d737b2f45d38fed4edf79f9325a1d5" dependencies = [ "discard", - "rustc_version", + "rustc_version 0.2.3", "stdweb-derive", "stdweb-internal-macros", "stdweb-internal-runtime", @@ -802,9 +934,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.41" +version = "2.0.87" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44c8b28c477cc3bf0e7966561e3460130e1255f7a1cf71931075f1c5e7a7e269" +checksum = "25aa4ce346d03a6dcd68dd8b4010bcb74e54e62c90c573f394c46eae99aba32d" dependencies = [ "proc-macro2", "quote", @@ -828,7 +960,7 @@ checksum = "01742297787513b79cf8e29d1056ede1313e2420b7b3b15d0a768b4921f549df" dependencies = [ "proc-macro2", "quote", - "syn 2.0.41", + "syn 2.0.87", ] [[package]] @@ -869,6 +1001,23 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "toml_datetime" +version = "0.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0dd7358ecb8fc2f8d014bf86f6f638ce72ba252a2c3a2572f2a795f1d23efb41" + +[[package]] +name = "toml_edit" +version = "0.22.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ae48d6208a266e853d946088ed816055e556cc6028c5e8e2b84d9fa5dd7c7f5" +dependencies = [ + "indexmap 2.6.0", + "toml_datetime", + "winnow", +] + [[package]] name = "unicase" version = "2.7.0" @@ -880,9 +1029,9 @@ dependencies = [ [[package]] name = "unicode-ident" -version = "1.0.12" +version = "1.0.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" +checksum = "e91b56cd4cadaeb79bbf1a5645f6b4f8dc5bde8834ad5894a8db35fda9efa1fe" [[package]] name = "unicode-width" @@ -910,6 +1059,7 @@ dependencies = [ "gloo-storage", "plotters", "pretty_assertions", + "rstest", "seed", "serde", "serde_json", @@ -1108,6 +1258,15 @@ version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" +[[package]] +name = "winnow" +version = "0.6.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "36c1fec1a2bb5866f07c25f68c26e565c4c200aebb96d7e55710c19d3e8ac49b" +dependencies = [ + "memchr", +] + [[package]] name = "yansi" version = "0.5.1" diff --git a/frontend/Cargo.toml b/frontend/Cargo.toml index ac7c465..96eace7 100644 --- a/frontend/Cargo.toml +++ b/frontend/Cargo.toml @@ -20,3 +20,4 @@ web-sys = { version = "0.3", features = ["AudioContext", "AudioDestinationNode", [dev-dependencies] assert_approx_eq = "1.1.0" pretty_assertions = "1.4.0" +rstest = "0.23.0" diff --git a/frontend/src/ui/common.rs b/frontend/src/ui/common.rs index f00e625..8c8e3ca 100644 --- a/frontend/src/ui/common.rs +++ b/frontend/src/ui/common.rs @@ -3,7 +3,7 @@ use std::{ collections::{BTreeMap, HashMap}, }; -use chrono::{prelude::*, Duration}; +use chrono::{prelude::*, Days, Duration}; use plotters::prelude::*; use seed::{prelude::*, *}; @@ -1109,9 +1109,129 @@ pub fn post_message_to_service_worker(message: &ServiceWorkerMessage) -> Result< } } +/// Group a series of (date, value) pairs. +/// +/// The `radius` parameter determines the number of days before and after the +/// center value to include in the calculation. +/// +/// Only values which have a date within `interval` are used as a center value +/// for the calculation. Values outside the interval are included in the +/// calculation if they fall within the radius of a center value. +/// +/// Two user-provided functions determine how values are combined: +/// +/// - `group_day` is called to combine values of the *same* day. +/// - `group_range` is called to combine values of multiple days after all +/// values for the same day have been combined by `group_day`. +/// +/// Return `None` in those functions to indicate the absence of a value. +/// +pub fn centered_moving_grouping( + data: &Vec<(NaiveDate, f32)>, + interval: &Interval, + radius: u64, + group_day: impl Fn(Vec) -> Option, + group_range: impl Fn(Vec) -> Option, +) -> Vec<(NaiveDate, f32)> { + let mut date_map: BTreeMap<&NaiveDate, Vec> = BTreeMap::new(); + + for (date, value) in data { + date_map.entry(date).or_default().push(*value); + } + + let mut grouped: BTreeMap<&NaiveDate, f32> = BTreeMap::new(); + + for (date, values) in date_map { + if let Some(result) = group_day(values) { + grouped.insert(date, result); + } + } + + interval + .first + .iter_days() + .take_while(|d| *d <= interval.last) + .filter_map(|center| { + group_range( + center + .checked_sub_days(Days::new(radius)) + .unwrap_or(center) + .iter_days() + .take_while(|d| { + *d <= interval.last + && *d <= center.checked_add_days(Days::new(radius)).unwrap_or(center) + }) + .filter_map(|d| grouped.get(&d)) + .copied() + .collect::>(), + ).map(|result| (center, result)) + }) + .collect::>() +} + +/// Calculate a series of moving totals from a given series of (date, value) pairs. +/// +/// The radius argument determines the number of days to include into the calculated +/// total before and after each value within the interval. +/// +/// Multiple values for the same date will be summed up. +/// +/// An empty result vector may be returned if there is no data within the interval. +pub fn centered_moving_total( + data: &Vec<(NaiveDate, f32)>, + interval: &Interval, + radius: u64, +) -> Vec<(NaiveDate, f32)> { + centered_moving_grouping( + data, + interval, + radius, + |d| Some(d.iter().sum()), + |d| Some(d.iter().sum()), + ) +} + +/// Calculate a series of moving averages from a given series of (date, value) pairs. +/// +/// The radius argument determines the number of days to include into the calculated +/// average before and after each value within the interval. +/// +/// Multiple values for the same date will be averaged. +/// +/// An empty result vector may be returned if there is no data within the interval. +/// Multiple result vectors may be returned in cases where there are gaps of more than +/// 2*radius+1 days in the input data within the interval. +pub fn centered_moving_average( + data: &Vec<(NaiveDate, f32)>, + interval: &Interval, + radius: u64, +) -> Vec<(NaiveDate, f32)> { + #[allow(clippy::cast_precision_loss)] + centered_moving_grouping( + data, + interval, + radius, + |d| { + if d.is_empty() { + None + } else { + Some(d.iter().sum::() / d.len() as f32) + } + }, + |d| { + if d.is_empty() { + None + } else { + Some(d.iter().sum::() / d.len() as f32) + } + }, + ) +} + #[cfg(test)] mod tests { use super::*; + use rstest::rstest; #[test] fn quartile_one() { @@ -1246,4 +1366,76 @@ mod tests { Duration::days(6) ); } + + #[rstest] + #[case((2020, 2, 3), (2020, 2, 5), 0, &[], &[])] + #[case((2020, 2, 3), (2020, 2, 5), 0, &[(2020, 2, 3, 1.0)], &[(2020, 2, 3, 1.0)])] + #[case((2020, 3, 3), (2020, 3, 5), 0, &[(2020, 2, 3, 1.0)], &[])] + #[case((2020, 2, 3), (2020, 2, 5), 0, &[(2020, 2, 3, 1.0), (2020, 2, 4, 1.0), (2020, 2, 5, 1.0)], &[(2020, 2, 3, 1.0), (2020, 2, 4, 1.0), (2020, 2, 5, 1.0)])] + #[case((2020, 2, 3), (2020, 2, 5), 0, &[(2020, 2, 3, 1.0), (2020, 2, 4, 1.0), (2020, 2, 5, 1.0), (2020, 2, 3, 3.0)], &[(2020, 2, 3, 2.0), (2020, 2, 4, 1.0), (2020, 2, 5, 1.0)])] + #[case((2020, 2, 3), (2020, 2, 5), 1, &[(2020, 2, 3, 1.0), (2020, 2, 4, 2.0), (2020, 2, 5, 3.0)], &[(2020, 2, 3, 1.5), (2020, 2, 4, 2.0), (2020, 2, 5, 2.5)])] + #[case((2020, 2, 2), (2020, 2, 6), 1, &[(2020, 2, 3, 1.0), (2020, 2, 4, 2.0), (2020, 2, 5, 3.0)], &[(2020, 2, 2, 1.0), (2020, 2, 3, 1.5), (2020, 2, 4, 2.0), (2020, 2, 5, 2.5), (2020, 2, 6, 3.0)])] + #[case((2020, 2, 3), (2020, 2, 7), 1, &[(2020, 2, 3, 1.0), (2020, 2, 7, 1.0)], &[(2020, 2, 3, 1.0), (2020, 2, 4, 1.0), (2020, 2, 6, 1.0), (2020, 2, 7, 1.0)])] + #[case((2020, 2, 3), (2020, 2, 9), 1, &[(2020, 2, 3, 1.0), (2020, 2, 9, 1.0)], &[(2020, 2, 3, 1.0), (2020, 2, 4, 1.0), (2020, 2, 8, 1.0), (2020, 2, 9, 1.0)])] + fn centered_moving_average( + #[case] start: (i32, u32, u32), + #[case] end: (i32, u32, u32), + #[case] radius: u64, + #[case] input: &[(i32, u32, u32, f32)], + #[case] expected: &[(i32, u32, u32, f32)], + ) { + assert_eq!( + super::centered_moving_average( + &input + .iter() + .map(|(y, m, d, v)| (NaiveDate::from_ymd_opt(*y, *m, *d).unwrap(), *v)) + .collect::>(), + &Interval { + first: NaiveDate::from_ymd_opt(start.0, start.1, start.2).unwrap(), + last: NaiveDate::from_ymd_opt(end.0, end.1, end.2).unwrap(), + }, + radius, + ), + expected + .iter() + .map(|(y, m, d, v)| (NaiveDate::from_ymd_opt(*y, *m, *d).unwrap(), *v)) + .collect::>(), + ); + } + + #[rstest] + #[case((2020, 2, 3), (2020, 2, 5), 0, &[], &[(2020, 2, 3, 0.0), (2020, 2, 4, 0.0), (2020, 2, 5, 0.0)])] + #[case((2020, 3, 3), (2020, 3, 5), 0, &[(2020, 2, 3, 1.0)], &[(2020, 3, 3, 0.0), (2020, 3, 4, 0.0), (2020, 3, 5, 0.0)])] + #[case((2020, 2, 3), (2020, 2, 5), 0, &[(2020, 2, 3, 1.0)], &[(2020, 2, 3, 1.0), (2020, 2, 4, 0.0), (2020, 2, 5, 0.0)])] + #[case((2020, 2, 3), (2020, 2, 5), 0, &[(2020, 2, 3, 1.0), (2020, 2, 4, 2.0), (2020, 2, 5, 3.0)], &[(2020, 2, 3, 1.0), (2020, 2, 4, 2.0), (2020, 2, 5, 3.0)])] + #[case((2020, 2, 3), (2020, 2, 5), 0, &[(2020, 2, 3, 1.0), (2020, 2, 4, 2.0), (2020, 2, 5, 3.0), (2020, 2, 3, 1.0)], &[(2020, 2, 3, 2.0), (2020, 2, 4, 2.0), (2020, 2, 5, 3.0)])] + #[case((2020, 2, 3), (2020, 2, 5), 1, &[(2020, 2, 3, 1.0), (2020, 2, 4, 2.0), (2020, 2, 5, 3.0)], &[(2020, 2, 3, 3.0), (2020, 2, 4, 6.0), (2020, 2, 5, 5.0)])] + #[case((2020, 2, 2), (2020, 2, 6), 1, &[(2020, 2, 3, 1.0), (2020, 2, 4, 2.0), (2020, 2, 5, 3.0)], &[(2020, 2, 2, 1.0), (2020, 2, 3, 3.0), (2020, 2, 4, 6.0), (2020, 2, 5, 5.0), (2020, 2, 6, 3.0)])] + #[case((2020, 2, 3), (2020, 2, 7), 1, &[(2020, 2, 3, 1.0), (2020, 2, 7, 1.0)], &[(2020, 2, 3, 1.0), (2020, 2, 4, 1.0), (2020, 2, 5, 0.0), (2020, 2, 6, 1.0), (2020, 2, 7, 1.0)])] + #[case((2020, 2, 3), (2020, 2, 9), 1, &[(2020, 2, 3, 1.0), (2020, 2, 9, 1.0)], &[(2020, 2, 3, 1.0), (2020, 2, 4, 1.0), (2020, 2, 5, 0.0), (2020, 2, 6, 0.0), (2020, 2, 7, 0.0), (2020, 2, 8, 1.0), (2020, 2, 9, 1.0)])] + fn centered_moving_total( + #[case] start: (i32, u32, u32), + #[case] end: (i32, u32, u32), + #[case] radius: u64, + #[case] input: &[(i32, u32, u32, f32)], + #[case] expected: &[(i32, u32, u32, f32)], + ) { + assert_eq!( + super::centered_moving_total( + &input + .iter() + .map(|(y, m, d, v)| (NaiveDate::from_ymd_opt(*y, *m, *d).unwrap(), *v)) + .collect::>(), + &Interval { + first: NaiveDate::from_ymd_opt(start.0, start.1, start.2).unwrap(), + last: NaiveDate::from_ymd_opt(end.0, end.1, end.2).unwrap(), + }, + radius, + ), + expected + .iter() + .map(|(y, m, d, v)| (NaiveDate::from_ymd_opt(*y, *m, *d).unwrap(), *v)) + .collect::>(), + ); + } }