diff --git a/frontend/src/ui/common.rs b/frontend/src/ui/common.rs index 46982b7..f6d60a5 100644 --- a/frontend/src/ui/common.rs +++ b/frontend/src/ui/common.rs @@ -1136,7 +1136,7 @@ pub fn centered_moving_grouping( radius: u64, group_day: impl Fn(Vec) -> Option, group_range: impl Fn(Vec) -> Option, -) -> Vec<(NaiveDate, f32)> { +) -> Vec> { let mut date_map: BTreeMap<&NaiveDate, Vec> = BTreeMap::new(); for (date, value) in data { @@ -1155,22 +1155,40 @@ pub fn centered_moving_grouping( .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)) - }) + .fold( + vec![vec![]], + |mut result: Vec>, center| { + let value = 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::>(), + ); + if let Some(last) = result.last_mut() { + match value { + Some(v) => { + last.push((center, v)); + } + None => { + if !last.is_empty() { + result.push(vec![]); + } + } + } + } + result + }, + ) + .into_iter() + .filter(|v| !v.is_empty()) .collect::>() } @@ -1193,7 +1211,8 @@ pub fn centered_moving_total( radius, |d| Some(d.iter().sum()), |d| Some(d.iter().sum()), - ) + )[0] + .clone() } /// Calculate a series of moving averages from a given series of (date, value) pairs. @@ -1210,7 +1229,7 @@ pub fn centered_moving_average( data: &Vec<(NaiveDate, f32)>, interval: &Interval, radius: u64, -) -> Vec<(NaiveDate, f32)> { +) -> Vec> { #[allow(clippy::cast_precision_loss)] centered_moving_grouping( data, @@ -1373,21 +1392,75 @@ mod tests { } #[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)])] + #[case::empty_series( + (2020, 2, 3), + (2020, 2, 5), + 0, + &[], + vec![] + )] + #[case::value_outside_interval( + (2020, 3, 3), + (2020, 3, 5), + 0, + &[(2020, 2, 3, 1.0)], + vec![] + )] + #[case::zero_radius_single_value( + (2020, 2, 3), + (2020, 2, 5), + 0, + &[(2020, 2, 3, 1.0)], + vec![vec![(2020, 2, 3, 1.0)]] + )] + #[case::zero_radius_multiple_days( + (2020, 2, 3), + (2020, 2, 5), + 0, + &[(2020, 2, 3, 1.0), (2020, 2, 4, 1.0), (2020, 2, 5, 1.0)], + vec![vec![(2020, 2, 3, 1.0), (2020, 2, 4, 1.0), (2020, 2, 5, 1.0)]] + )] + #[case::zero_radius_multiple_values_per_day( + (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)], + vec![vec![(2020, 2, 3, 2.0), (2020, 2, 4, 1.0), (2020, 2, 5, 1.0)]] + )] + #[case::nonzero_radius_multiple_days( + (2020, 2, 3), + (2020, 2, 5), + 1, + &[(2020, 2, 3, 1.0), (2020, 2, 4, 2.0), (2020, 2, 5, 3.0)], + vec![vec![(2020, 2, 3, 1.5), (2020, 2, 4, 2.0), (2020, 2, 5, 2.5)]] + )] + #[case::nonzero_radius_missing_day( + (2020, 2, 2), + (2020, 2, 6), + 1, + &[(2020, 2, 3, 1.0), (2020, 2, 4, 2.0), (2020, 2, 5, 3.0)], + vec![vec![(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::nonzero_radius_with_gap_1( + (2020, 2, 3), + (2020, 2, 7), + 1, + &[(2020, 2, 3, 1.0), (2020, 2, 7, 1.0)], + vec![vec![(2020, 2, 3, 1.0), (2020, 2, 4, 1.0)], vec![(2020, 2, 6, 1.0), (2020, 2, 7, 1.0)]] + )] + #[case::nonzero_radius_with_gap_2( + (2020, 2, 3), + (2020, 2, 9), + 1, + &[(2020, 2, 3, 1.0), (2020, 2, 9, 1.0)], + vec![vec![(2020, 2, 3, 1.0), (2020, 2, 4, 1.0)], vec![(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)], + #[case] expected: Vec>, ) { assert_eq!( super::centered_moving_average( @@ -1403,21 +1476,78 @@ mod tests { ), expected .iter() - .map(|(y, m, d, v)| (NaiveDate::from_ymd_opt(*y, *m, *d).unwrap(), *v)) + .map(|v| v + .iter() + .map(|(y, m, d, v)| (NaiveDate::from_ymd_opt(*y, *m, *d).unwrap(), *v)) + .collect::>()) .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)])] + #[case::empty_series( + (2020, 2, 3), + (2020, 2, 5), + 0, + &[], + &[(2020, 2, 3, 0.0), (2020, 2, 4, 0.0), (2020, 2, 5, 0.0)], + )] + #[case::value_outside_interval( + (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::zero_radius_single_day( + (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::zero_radius_multiple_days( + (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::zero_radius_multiple_values_per_day( + (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::nonzero_radius_multiple_days( + (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::nonzero_radius_missing_day( + (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::nonzero_radius_multiple_missing_days_1( + (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::nonzero_radius_multiple_missing_days_2( + (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),