gitlab_time_report/charts/
estimates.rs

1//! Calculates the estimates from [`crate::model::TrackableItem`] and prepares the data for chart creation.
2
3use super::SeriesData;
4use super::charming_extensions::MultiSeries;
5use crate::TimeDeltaExt;
6use crate::filters::group_by_trackable_item;
7use crate::model::TimeLog;
8use chrono::Duration;
9use std::collections::BTreeMap;
10
11/// Contains the tracked time per each `TrackableItem`
12struct TrackedTime {
13    /// The total time spent per `TrackableItems`
14    time_spent: Duration,
15    /// The estimate on the `TrackableItem`
16    estimate: Duration,
17}
18
19/// Calculates the estimate and the actual time spent of all items of type `T`.
20/// Data can be converted into a [`charming`] chart.
21pub(super) fn calculate_estimate_data<'a, T, Series>(
22    grouped_time_log: BTreeMap<impl Into<Option<&'a T>> + Clone, Vec<&'a TimeLog>>,
23) -> (SeriesData, Vec<String>)
24where
25    T: std::fmt::Display + 'a,
26    Series: MultiSeries,
27{
28    let capacity = grouped_time_log.len();
29    let mut axis_labels = Vec::with_capacity(capacity);
30    let mut estimate_sums = Vec::with_capacity(capacity);
31    let mut actual_sums = Vec::with_capacity(capacity);
32
33    for (outer_key, time_logs) in grouped_time_log {
34        axis_labels.push(Series::option_to_string(outer_key));
35
36        // Get the estimate and actual time per Issue/MR and store it into TrackedTime
37        let tracked_time_by_item = group_by_trackable_item(time_logs)
38            .map(|(trackable_item, _)| TrackedTime {
39                estimate: trackable_item.common.time_estimate,
40                time_spent: trackable_item.common.total_time_spent,
41            })
42            .collect::<Vec<_>>();
43
44        // Sum up all the times from every TrackableItem of this grouping (i.e., Label)
45        let (estimate_sum, actual_sum) = tracked_time_by_item.iter().fold(
46            (Duration::zero(), Duration::zero()),
47            |(est, act), tracked| (est + tracked.estimate, act + tracked.time_spent),
48        );
49
50        estimate_sums.push(estimate_sum.total_hours());
51        actual_sums.push(actual_sum.total_hours());
52    }
53
54    let series = vec![
55        ("Estimates".into(), estimate_sums),
56        ("Actual Time".into(), actual_sums),
57    ];
58
59    (series, axis_labels)
60}
61
62#[cfg(test)]
63mod tests {
64    use super::*;
65    use crate::charts::tests::*;
66    use crate::filters::group_by_milestone;
67    use crate::model::Milestone;
68    use charming::series::Bar;
69
70    #[test]
71    fn test_calculate_estimate_data() {
72        const AXIS_LABELS: &[&str] = &["None", "M1", "M2"];
73        const NUMBER_OF_SERIES: usize = 2;
74        const NUMBER_OF_DATA_POINTS: usize = 3;
75        const ESTIMATES_DATA: [f32; NUMBER_OF_DATA_POINTS] = [2.0, 7.0, 2.0];
76        const ACTUAL_DATA: [f32; NUMBER_OF_DATA_POINTS] = [5.0, 7.75, 1.5];
77
78        let time_logs = get_time_logs();
79        let by_milestone = group_by_milestone(&time_logs).collect();
80        let (series, axis_labels) = calculate_estimate_data::<Milestone, Bar>(by_milestone);
81
82        assert_eq!(axis_labels, AXIS_LABELS);
83        assert_eq!(series.len(), NUMBER_OF_SERIES);
84
85        let estimate_data = series.first().unwrap();
86        assert_eq!(estimate_data.0, "Estimates");
87        assert_eq!(estimate_data.1, ESTIMATES_DATA);
88
89        let actual_data = series.last().unwrap();
90        assert_eq!(actual_data.0, "Actual Time");
91        assert_eq!(actual_data.1, ACTUAL_DATA);
92    }
93}