1use crate::model::{Label, TimeLog, User};
4use crate::{filters, TimeDeltaExt};
5use chrono::{Duration, Local};
6use std::collections::{HashMap, HashSet};
7
8#[must_use]
10fn get_total_time_by_user_in_last_n_days(
11 time_logs: &[TimeLog],
12 days: Duration,
13) -> HashMap<&User, Duration> {
14 let filtered = filters::filter_by_last_n_days(time_logs, days);
15 filters::total_time_spent_by_user(filtered).collect()
16}
17
18#[must_use]
20fn get_total_time_by_user_yesterday(time_logs: &[TimeLog]) -> HashMap<&User, Duration> {
21 let yesterday = Local::now().date_naive() - Duration::days(1);
22 let filtered = filters::filter_by_date(time_logs, yesterday, yesterday);
23 filters::total_time_spent_by_user(filtered).collect()
24}
25
26#[must_use]
28fn get_duration_or_zero<'a>(map: &HashMap<&'a User, Duration>, user: &'a User) -> Duration {
29 map.get(user).copied().unwrap_or(Duration::zero())
30}
31
32#[must_use]
35pub fn populate_table_timelogs_in_timeframes_by_user(
36 time_logs: &[TimeLog],
37) -> (Vec<Vec<String>>, &[&str]) {
38 let today_timelogs = get_total_time_by_user_in_last_n_days(time_logs, Duration::days(1));
39 let yesterday_timelogs = get_total_time_by_user_yesterday(time_logs);
40 let this_week_timelogs = get_total_time_by_user_in_last_n_days(time_logs, Duration::weeks(1));
41 let this_month_timelogs = get_total_time_by_user_in_last_n_days(time_logs, Duration::days(30));
42
43 let total_today = today_timelogs.values().copied().sum::<Duration>();
45 let total_yesterday = yesterday_timelogs.values().copied().sum::<Duration>();
46 let total_this_week = this_week_timelogs.values().copied().sum::<Duration>();
47 let total_this_month = this_month_timelogs.values().copied().sum::<Duration>();
48 let total_time = filters::total_time_spent(time_logs);
49
50 let mut table_data: Vec<_> = filters::total_time_spent_by_user(time_logs)
51 .map(|(user, total_time_of_user)| {
52 vec![
53 user.name.clone(),
54 get_duration_or_zero(&today_timelogs, user).to_hm_string(),
55 get_duration_or_zero(&yesterday_timelogs, user).to_hm_string(),
56 get_duration_or_zero(&this_week_timelogs, user).to_hm_string(),
57 get_duration_or_zero(&this_month_timelogs, user).to_hm_string(),
58 total_time_of_user.to_hm_string(),
59 ]
60 })
61 .collect();
62
63 table_data.push(vec![
64 "Total".to_string(),
65 total_today.to_hm_string(),
66 total_yesterday.to_hm_string(),
67 total_this_week.to_hm_string(),
68 total_this_month.to_hm_string(),
69 total_time.to_hm_string(),
70 ]);
71
72 let table_header = &[
73 "User",
74 "Today",
75 "Yesterday",
76 "Last seven days",
77 "Last 30 days",
78 "Total time spent",
79 ];
80
81 (table_data, table_header)
82}
83
84#[must_use]
87pub fn populate_table_timelogs_by_label<'a>(
88 time_logs: &[TimeLog],
89 label_filter: Option<&HashSet<String>>,
90 label_others: Option<&Label>,
91) -> (Vec<Vec<String>>, &'a [&'a str]) {
92 let time_by_label = filters::group_by_label(time_logs, label_filter, label_others);
93
94 let table_data: Vec<_> = time_by_label
95 .map(|(label, timelogs)| {
96 let total_time = filters::total_time_spent(timelogs);
97 let label_name = match label {
98 Some(label) => label.title.clone(),
99 None => "No label".to_string(),
100 };
101 vec![label_name, total_time.to_hm_string()]
102 })
103 .collect();
104
105 let table_header = &["Label", "Time Spent"];
106
107 (table_data, table_header)
108}
109
110#[must_use]
113pub fn populate_table_timelogs_by_milestone<'a>(
114 time_logs: &[TimeLog],
115) -> (Vec<Vec<String>>, &'a [&'a str]) {
116 let time_by_milestone = filters::group_by_milestone(time_logs);
117
118 let table_data: Vec<_> = time_by_milestone
119 .map(|(milestone, timelogs)| {
120 let total_time = filters::total_time_spent(timelogs);
121 let title = milestone.map_or_else(|| "No milestone".to_string(), |m| m.title.clone());
122 vec![title, total_time.to_hm_string()]
123 })
124 .collect();
125 let table_header = &["Milestone", "Time Spent"];
126 (table_data, table_header)
127}
128
129#[cfg(test)]
130mod tests {
131 use super::*;
132 use crate::model::{
133 Issue, Label, Labels, MergeRequest, Milestone, TimeLog, TrackableItem, TrackableItemFields,
134 TrackableItemKind, User,
135 };
136 use chrono::{Duration, Local};
137
138 const NUMBER_OF_LOGS: usize = 4;
139
140 fn create_test_user(name: &str) -> User {
141 User {
142 username: name.to_string(),
143 name: name.to_string(),
144 }
145 }
146
147 fn get_timelogs() -> [TimeLog; NUMBER_OF_LOGS] {
148 let now = Local::now();
149 [
150 TimeLog {
151 spent_at: now - Duration::days(2),
152 time_spent: Duration::seconds(3600),
153 summary: None,
154 user: User {
155 name: "user1".to_string(),
156 username: "user1".to_string(),
157 },
158 trackable_item: TrackableItem::default(),
159 },
160 TimeLog {
161 spent_at: now - Duration::days(1),
162 time_spent: Duration::seconds(3600),
163 summary: Some("test".to_string()),
164 user: User {
165 name: "user2".to_string(),
166 username: "user2".to_string(),
167 },
168 trackable_item: TrackableItem {
169 common: TrackableItemFields {
170 milestone: Some(Milestone {
171 title: "End of Elaboration".to_string(),
172 ..Default::default()
173 }),
174 labels: Labels {
175 labels: vec![Label {
176 title: "Documentation".into(),
177 }],
178 },
179 ..Default::default()
180 },
181 kind: TrackableItemKind::Issue(Issue::default()),
182 },
183 },
184 TimeLog {
185 spent_at: now,
186 time_spent: Duration::seconds(1800),
187 summary: Some("test".to_string()),
188 user: User {
189 name: "user1".to_string(),
190 username: "user1".to_string(),
191 },
192 trackable_item: TrackableItem {
193 common: TrackableItemFields {
194 milestone: None,
195 labels: Labels {
196 labels: vec![
197 Label {
198 title: "Documentation".into(),
199 },
200 Label {
201 title: "Development".into(),
202 },
203 ],
204 },
205 ..Default::default()
206 },
207 kind: TrackableItemKind::MergeRequest(MergeRequest::default()),
208 },
209 },
210 TimeLog {
211 spent_at: now,
212 time_spent: Duration::seconds(5400),
213 summary: Some("Fix a big bug".to_string()),
214 user: User {
215 name: "user3".to_string(),
216 username: "user3".to_string(),
217 },
218 trackable_item: TrackableItem {
219 common: TrackableItemFields {
220 labels: Labels {
221 labels: vec![
222 Label {
223 title: "Bug".into(),
224 },
225 Label {
226 title: "Development".into(),
227 },
228 ],
229 },
230 ..Default::default()
231 },
232 kind: TrackableItemKind::MergeRequest(MergeRequest::default()),
233 },
234 },
235 ]
236 }
237
238 fn to_name_map(m: &HashMap<&User, Duration>) -> HashMap<String, Duration> {
240 m.iter()
241 .map(|(u, d)| (u.name.clone(), *d))
242 .collect::<HashMap<_, _>>()
243 }
244
245 #[test]
246 fn test_get_duration_or_zero_existing_user() {
247 const DURATION: Duration = Duration::seconds(1200);
248 let user = create_test_user("user1");
249 let mut map = HashMap::new();
250 map.insert(&user, DURATION);
251
252 let result = get_duration_or_zero(&map, &user);
253 assert_eq!(result, DURATION);
254 }
255
256 #[test]
257 fn test_get_duration_or_zero_missing_user() {
258 const DURATION: Duration = Duration::seconds(1200);
259 let user_in_map = create_test_user("user1");
260 let user_not_in_map = create_test_user("user4");
261 let mut map = HashMap::new();
262 map.insert(&user_in_map, DURATION);
263
264 let result = get_duration_or_zero(&map, &user_not_in_map);
265 assert_eq!(result, Duration::zero());
266 }
267
268 #[test]
269 fn test_total_time_by_user_yesterday() {
270 const NUMBER_OF_USERS: usize = 1;
271 const TIME_SPENT: Duration = Duration::seconds(3600);
272
273 let time_logs = get_timelogs();
274 let result = get_total_time_by_user_yesterday(&time_logs);
275 let name_map = to_name_map(&result);
276
277 assert_eq!(name_map.len(), NUMBER_OF_USERS);
278 assert_eq!(name_map.get("user2"), Some(&TIME_SPENT));
279 }
280
281 #[test]
282 fn test_total_time_by_user_in_last_n_days_empty() {
283 const N_DAYS: Duration = Duration::days(7);
284 let time_logs = vec![];
285 let result = get_total_time_by_user_in_last_n_days(&time_logs, N_DAYS);
286 assert!(result.is_empty());
287 }
288
289 #[test]
290 fn test_total_time_by_user_in_last_n_days_one_day() {
291 const N_DAYS: Duration = Duration::days(1);
292 const TIME_SPENT_USER_1: Duration = Duration::seconds(1800);
293 const TIME_SPENT_USER_3: Duration = Duration::seconds(5400);
294
295 let time_logs = get_timelogs();
296 let result = get_total_time_by_user_in_last_n_days(&time_logs, N_DAYS);
297 let name_map = to_name_map(&result);
298
299 assert_eq!(name_map.len(), 2);
300 assert_eq!(name_map.get("user1"), Some(&TIME_SPENT_USER_1));
301 assert_eq!(name_map.get("user3"), Some(&TIME_SPENT_USER_3));
302 assert!(!name_map.contains_key("user2"));
303 }
304
305 #[test]
306 fn test_total_time_by_user_in_last_n_days_seven_days() {
307 const N_DAYS: Duration = Duration::days(7);
308 const TIME_SPENT_USER_1: Duration = Duration::seconds(5400);
309 const TIME_SPENT_USER_2: Duration = Duration::seconds(3600);
310 const TIME_SPENT_USER_3: Duration = Duration::seconds(5400);
311
312 let time_logs = get_timelogs();
313 let result = get_total_time_by_user_in_last_n_days(&time_logs, N_DAYS);
314 let name_map = to_name_map(&result);
315
316 assert_eq!(name_map.get("user1"), Some(&TIME_SPENT_USER_1));
317 assert_eq!(name_map.get("user2"), Some(&TIME_SPENT_USER_2));
318 assert_eq!(name_map.get("user3"), Some(&TIME_SPENT_USER_3));
319 }
320
321 #[test]
322 fn test_create_table_timelogs_in_timeframes_by_user_header_and_rows() {
323 const NUMBER_OF_STATS: usize = 5;
324 const NUMBER_OF_COLUMNS: usize = NUMBER_OF_STATS + 1;
325
326 let time_logs = get_timelogs();
327 let (table, _header) = populate_table_timelogs_in_timeframes_by_user(&time_logs);
328
329 let name_column: HashSet<String> = table.iter().map(|row| row[0].clone()).collect();
331 assert!(name_column.contains("user1"));
332 assert!(name_column.contains("user2"));
333 assert!(name_column.contains("user3"));
334
335 for row in &table {
337 assert_eq!(row.len(), NUMBER_OF_COLUMNS);
338 }
339 }
340
341 #[test]
342 fn test_create_table_timelogs_by_label() {
343 let time_logs = get_timelogs();
344 let (table, _header) = populate_table_timelogs_by_label(&time_logs, None, None);
345
346 let label_time_spent_map: HashMap<String, String> = table
347 .into_iter()
348 .map(|row| (row[0].clone(), row[1].clone()))
349 .collect();
350
351 let expected_map = std::collections::HashMap::from([
352 ("Documentation".to_string(), "01h 30m".to_string()),
353 ("Development".to_string(), "02h 00m".to_string()),
354 ("Bug".to_string(), "01h 30m".to_string()),
355 ("No label".to_string(), "01h 00m".to_string()),
356 ]);
357
358 assert_eq!(label_time_spent_map, expected_map);
359 }
360
361 #[test]
362 fn test_create_table_timelogs_by_label_with_others() {
363 let time_logs = get_timelogs();
364 let label_documentation = Label {
365 title: "Documentation".into(),
366 };
367 let label_others = Label {
368 title: "Others".into(),
369 };
370
371 let selected = HashSet::from([label_documentation.title.clone()]);
372 let (table, _) =
373 populate_table_timelogs_by_label(&time_logs, Some(&selected), Some(&label_others));
374
375 let label_time_spent_map: HashMap<String, String> = table
376 .into_iter()
377 .map(|row| (row[0].clone(), row[1].clone()))
378 .collect();
379
380 let expected_map = HashMap::from([
381 ("Documentation".to_string(), "01h 30m".to_string()),
382 ("Others".to_string(), "02h 30m".to_string()),
383 ]);
384 assert_eq!(label_time_spent_map, expected_map);
385 }
386}