Skip to main content

work_tuimer/models/
day_data.rs

1use super::WorkRecord;
2use serde::{Deserialize, Serialize};
3use std::collections::HashMap;
4use time::Date;
5
6#[derive(Debug, Clone, Serialize, Deserialize)]
7pub struct DayData {
8    pub date: Date,
9    pub last_id: u32,
10    pub work_records: HashMap<u32, WorkRecord>,
11}
12
13impl DayData {
14    pub fn new(date: Date) -> Self {
15        DayData {
16            date,
17            last_id: 0,
18            work_records: HashMap::new(),
19        }
20    }
21
22    pub fn add_record(&mut self, record: WorkRecord) {
23        if record.id > self.last_id {
24            self.last_id = record.id;
25        }
26        self.work_records.insert(record.id, record);
27    }
28
29    pub fn remove_record(&mut self, id: u32) -> Option<WorkRecord> {
30        self.work_records.remove(&id)
31    }
32
33    pub fn next_id(&mut self) -> u32 {
34        self.last_id += 1;
35        self.last_id
36    }
37
38    pub fn get_sorted_records(&self) -> Vec<&WorkRecord> {
39        let mut records: Vec<&WorkRecord> = self.work_records.values().collect();
40        records.sort_by_key(|r| r.start);
41        records
42    }
43
44    pub fn get_grouped_totals(&self) -> Vec<(String, u32)> {
45        let mut totals: HashMap<String, u32> = HashMap::new();
46
47        for record in self.work_records.values() {
48            *totals.entry(record.name.clone()).or_insert(0) += record.total_minutes;
49        }
50
51        let mut result: Vec<(String, u32)> = totals.into_iter().collect();
52        // Sort by duration (descending), then by task name (ascending) for stable ordering
53        result.sort_by(|a, b| b.1.cmp(&a.1).then_with(|| a.0.cmp(&b.0)));
54        result
55    }
56}
57
58#[cfg(test)]
59mod tests {
60    use super::*;
61    use crate::models::TimePoint;
62
63    fn create_test_date() -> Date {
64        Date::from_calendar_date(2025, time::Month::November, 6).unwrap()
65    }
66
67    fn create_test_record(id: u32, name: &str, start_hour: u8, end_hour: u8) -> WorkRecord {
68        let start = TimePoint::new(start_hour, 0).unwrap();
69        let end = TimePoint::new(end_hour, 0).unwrap();
70        WorkRecord::new(id, name.to_string(), start, end)
71    }
72
73    #[test]
74    fn test_new_day_data() {
75        let date = create_test_date();
76        let day = DayData::new(date);
77
78        assert_eq!(day.date, date);
79        assert_eq!(day.last_id, 0);
80        assert_eq!(day.work_records.len(), 0);
81    }
82
83    #[test]
84    fn test_add_record() {
85        let mut day = DayData::new(create_test_date());
86        let record = create_test_record(1, "Coding", 9, 17);
87
88        day.add_record(record.clone());
89
90        assert_eq!(day.work_records.len(), 1);
91        assert_eq!(day.last_id, 1);
92        assert!(day.work_records.contains_key(&1));
93    }
94
95    #[test]
96    fn test_add_multiple_records() {
97        let mut day = DayData::new(create_test_date());
98
99        day.add_record(create_test_record(1, "Coding", 9, 12));
100        day.add_record(create_test_record(2, "Meeting", 13, 14));
101        day.add_record(create_test_record(3, "Code Review", 14, 16));
102
103        assert_eq!(day.work_records.len(), 3);
104        assert_eq!(day.last_id, 3);
105    }
106
107    #[test]
108    fn test_add_record_updates_last_id() {
109        let mut day = DayData::new(create_test_date());
110
111        day.add_record(create_test_record(5, "Task", 9, 10));
112        assert_eq!(day.last_id, 5);
113
114        day.add_record(create_test_record(2, "Task2", 10, 11));
115        assert_eq!(day.last_id, 5); // Should not decrease
116
117        day.add_record(create_test_record(10, "Task3", 11, 12));
118        assert_eq!(day.last_id, 10);
119    }
120
121    #[test]
122    fn test_remove_record() {
123        let mut day = DayData::new(create_test_date());
124        day.add_record(create_test_record(1, "Coding", 9, 17));
125
126        let removed = day.remove_record(1);
127        assert!(removed.is_some());
128        assert_eq!(removed.unwrap().name, "Coding");
129        assert_eq!(day.work_records.len(), 0);
130    }
131
132    #[test]
133    fn test_remove_nonexistent_record() {
134        let mut day = DayData::new(create_test_date());
135        day.add_record(create_test_record(1, "Coding", 9, 17));
136
137        let removed = day.remove_record(999);
138        assert!(removed.is_none());
139        assert_eq!(day.work_records.len(), 1);
140    }
141
142    #[test]
143    fn test_next_id() {
144        let mut day = DayData::new(create_test_date());
145
146        assert_eq!(day.next_id(), 1);
147        assert_eq!(day.next_id(), 2);
148        assert_eq!(day.next_id(), 3);
149        assert_eq!(day.last_id, 3);
150    }
151
152    #[test]
153    fn test_next_id_after_add_record() {
154        let mut day = DayData::new(create_test_date());
155        day.add_record(create_test_record(5, "Task", 9, 10));
156
157        assert_eq!(day.last_id, 5);
158        assert_eq!(day.next_id(), 6);
159        assert_eq!(day.next_id(), 7);
160    }
161
162    #[test]
163    fn test_get_sorted_records_empty() {
164        let day = DayData::new(create_test_date());
165        let sorted = day.get_sorted_records();
166        assert_eq!(sorted.len(), 0);
167    }
168
169    #[test]
170    fn test_get_sorted_records_single() {
171        let mut day = DayData::new(create_test_date());
172        day.add_record(create_test_record(1, "Coding", 9, 17));
173
174        let sorted = day.get_sorted_records();
175        assert_eq!(sorted.len(), 1);
176        assert_eq!(sorted[0].name, "Coding");
177    }
178
179    #[test]
180    fn test_get_sorted_records_already_sorted() {
181        let mut day = DayData::new(create_test_date());
182        day.add_record(create_test_record(1, "Morning", 9, 12));
183        day.add_record(create_test_record(2, "Afternoon", 13, 17));
184
185        let sorted = day.get_sorted_records();
186        assert_eq!(sorted.len(), 2);
187        assert_eq!(sorted[0].name, "Morning");
188        assert_eq!(sorted[1].name, "Afternoon");
189    }
190
191    #[test]
192    fn test_get_sorted_records_unsorted() {
193        let mut day = DayData::new(create_test_date());
194        day.add_record(create_test_record(1, "Afternoon", 13, 17));
195        day.add_record(create_test_record(2, "Morning", 9, 12));
196        day.add_record(create_test_record(3, "Evening", 18, 20));
197
198        let sorted = day.get_sorted_records();
199        assert_eq!(sorted.len(), 3);
200        assert_eq!(sorted[0].name, "Morning");
201        assert_eq!(sorted[1].name, "Afternoon");
202        assert_eq!(sorted[2].name, "Evening");
203    }
204
205    #[test]
206    fn test_get_sorted_records_same_start_time() {
207        let mut day = DayData::new(create_test_date());
208        let start = TimePoint::new(9, 0).unwrap();
209        let end1 = TimePoint::new(10, 0).unwrap();
210        let end2 = TimePoint::new(11, 0).unwrap();
211
212        day.add_record(WorkRecord::new(1, "Task1".to_string(), start, end1));
213        day.add_record(WorkRecord::new(2, "Task2".to_string(), start, end2));
214
215        let sorted = day.get_sorted_records();
216        assert_eq!(sorted.len(), 2);
217        // Both start at 9:00, order doesn't matter but both should be present
218        assert!(sorted.iter().any(|r| r.name == "Task1"));
219        assert!(sorted.iter().any(|r| r.name == "Task2"));
220    }
221
222    #[test]
223    fn test_get_grouped_totals_empty() {
224        let day = DayData::new(create_test_date());
225        let totals = day.get_grouped_totals();
226        assert_eq!(totals.len(), 0);
227    }
228
229    #[test]
230    fn test_get_grouped_totals_single_task() {
231        let mut day = DayData::new(create_test_date());
232        day.add_record(create_test_record(1, "Coding", 9, 17)); // 8 hours
233
234        let totals = day.get_grouped_totals();
235        assert_eq!(totals.len(), 1);
236        assert_eq!(totals[0].0, "Coding");
237        assert_eq!(totals[0].1, 480); // 8 * 60 minutes
238    }
239
240    #[test]
241    fn test_get_grouped_totals_multiple_different_tasks() {
242        let mut day = DayData::new(create_test_date());
243        day.add_record(create_test_record(1, "Coding", 9, 12)); // 3 hours
244        day.add_record(create_test_record(2, "Meeting", 13, 14)); // 1 hour
245        day.add_record(create_test_record(3, "Code Review", 14, 16)); // 2 hours
246
247        let totals = day.get_grouped_totals();
248        assert_eq!(totals.len(), 3);
249
250        // Should be sorted by duration (descending)
251        assert_eq!(totals[0].0, "Coding");
252        assert_eq!(totals[0].1, 180);
253        assert_eq!(totals[1].0, "Code Review");
254        assert_eq!(totals[1].1, 120);
255        assert_eq!(totals[2].0, "Meeting");
256        assert_eq!(totals[2].1, 60);
257    }
258
259    #[test]
260    fn test_get_grouped_totals_same_task_multiple_times() {
261        let mut day = DayData::new(create_test_date());
262        day.add_record(create_test_record(1, "Coding", 9, 11)); // 2 hours
263        day.add_record(create_test_record(2, "Meeting", 11, 12)); // 1 hour
264        day.add_record(create_test_record(3, "Coding", 13, 16)); // 3 hours
265        day.add_record(create_test_record(4, "Coding", 16, 17)); // 1 hour
266
267        let totals = day.get_grouped_totals();
268        assert_eq!(totals.len(), 2);
269
270        // Coding should be grouped: 2 + 3 + 1 = 6 hours
271        assert_eq!(totals[0].0, "Coding");
272        assert_eq!(totals[0].1, 360);
273        assert_eq!(totals[1].0, "Meeting");
274        assert_eq!(totals[1].1, 60);
275    }
276
277    #[test]
278    fn test_get_grouped_totals_sorted_by_duration() {
279        let mut day = DayData::new(create_test_date());
280        day.add_record(create_test_record(1, "Short", 9, 10)); // 1 hour
281        day.add_record(create_test_record(2, "Long", 10, 15)); // 5 hours
282        day.add_record(create_test_record(3, "Medium", 15, 17)); // 2 hours
283
284        let totals = day.get_grouped_totals();
285
286        // Should be sorted by duration descending
287        assert_eq!(totals[0].0, "Long");
288        assert_eq!(totals[1].0, "Medium");
289        assert_eq!(totals[2].0, "Short");
290    }
291
292    #[test]
293    fn test_get_grouped_totals_stable_sort_on_tied_durations() {
294        let mut day = DayData::new(create_test_date());
295        // Create tasks with same duration to test stable sorting
296        day.add_record(create_test_record(1, "Zebra Task", 9, 11)); // 2 hours
297        day.add_record(create_test_record(2, "Alpha Task", 11, 13)); // 2 hours
298        day.add_record(create_test_record(3, "Beta Task", 13, 15)); // 2 hours
299
300        let totals = day.get_grouped_totals();
301
302        // All have same duration, should be sorted alphabetically by name
303        assert_eq!(totals.len(), 3);
304        assert_eq!(totals[0].0, "Alpha Task");
305        assert_eq!(totals[0].1, 120);
306        assert_eq!(totals[1].0, "Beta Task");
307        assert_eq!(totals[1].1, 120);
308        assert_eq!(totals[2].0, "Zebra Task");
309        assert_eq!(totals[2].1, 120);
310
311        // Test multiple times to ensure sort is stable (non-blinking)
312        for _ in 0..10 {
313            let totals_repeat = day.get_grouped_totals();
314            assert_eq!(totals_repeat[0].0, "Alpha Task");
315            assert_eq!(totals_repeat[1].0, "Beta Task");
316            assert_eq!(totals_repeat[2].0, "Zebra Task");
317        }
318    }
319
320    #[test]
321    fn test_clone() {
322        let mut day1 = DayData::new(create_test_date());
323        day1.add_record(create_test_record(1, "Coding", 9, 17));
324
325        let day2 = day1.clone();
326
327        assert_eq!(day1.date, day2.date);
328        assert_eq!(day1.last_id, day2.last_id);
329        assert_eq!(day1.work_records.len(), day2.work_records.len());
330    }
331}