timelog/chart/
day.rs

1//! Type holding the data for generating the hourly bar chart.
2//!
3//! # Description
4//!
5//! The [`DayHours`] type represents the time spent in events during each hour of a day.
6//! The [`Hour`] type represents the work events applied to a particular hour.
7
8use std::time::Duration;
9
10use crate::TaskEvent;
11
12fn hour() -> Duration { Duration::from_secs(3600) }
13
14/// [`DayHours`] type tracks the hours containing [`TaskEvent`]s.
15pub struct DayHours {
16    // Starting hour
17    start: usize,
18    // Vector containing [`Hour`] object starting from `start` through all used hours.
19    hours: Vec<Hour>
20}
21
22impl DayHours {
23    /// Return the start hour
24    pub fn start(&self) -> usize { self.start }
25
26    /// Return the end hour
27    pub fn end(&self) -> usize { self.start() + self.hours.len() - 1 }
28
29    /// Return the number of hours covered by the [`DayHours`]
30    pub fn num_hours(&self) -> usize { self.hours.len() }
31
32    /// Add a new [`TaskEvent`]
33    pub fn add(&mut self, event: TaskEvent) {
34        let hour = event.hour();
35        if self.hours.is_empty() {
36            self.start = hour;
37            self.hours.push(Hour::default());
38        }
39        if hour > self.end() {
40            (0..(hour - self.end())).for_each(|_| self.hours.push(Hour::default()));
41        }
42        let start = self.start();
43        if let Some(the_hour) = self.hours.get_mut(hour - start) {
44            if let Some(ev) = the_hour.add(event) {
45                self.add(ev);
46            }
47        }
48    }
49
50    /// Return an iterator over the [`Hour`]s
51    pub fn iter(&self) -> impl Iterator<Item = &'_ Hour> { self.hours.iter() }
52}
53
54impl Default for DayHours {
55    /// Create a [`DayHours`] type
56    fn default() -> Self { Self { start: 0, hours: Vec::new() } }
57}
58
59/// A type representing the [`TaskEvent`]s in the hour.
60#[derive(Debug, Clone, Eq, PartialEq, PartialOrd)]
61pub struct Hour {
62    events: Vec<TaskEvent>,
63    offset: Duration
64}
65
66impl Hour {
67    /// Return `true` if the [`Hour`] contains no [`TaskEvent`]s.
68    pub fn is_empty(&self) -> bool { self.events.is_empty() }
69
70    /// Return an iterator over the [`TaskEvent`]s
71    pub fn iter(&self) -> impl Iterator<Item = &'_ TaskEvent> { self.events.iter() }
72
73    /// Return a [`Duration`] representing the time remaining in the hour
74    /// after the [`TaskEvent`]s are accounted for.
75    pub fn remain(&self) -> Duration { hour() - self.offset }
76
77    /// Add a new [`TaskEvent`], returning a new [`TaskEvent`] if there
78    /// is any time left over after the hour is full.
79    pub fn add(&mut self, event: TaskEvent) -> Option<TaskEvent> {
80        self.offset = Duration::from_secs(u64::from(event.second_offset()));
81        if event.duration() >= self.remain() {
82            let (first, next) = event.split(self.remain())?;
83            self.events.push(first);
84            self.offset = hour();
85            Some(next)
86        }
87        else {
88            self.offset += event.duration();
89            self.events.push(event);
90            None
91        }
92    }
93}
94
95impl Default for Hour {
96    /// Create a [`Hour`]
97    fn default() -> Self { Self { events: vec![], offset: Duration::default() } }
98}
99
100#[cfg(test)]
101mod tests {
102    use spectral::prelude::*;
103
104    use super::*;
105    use crate::date::DateTime;
106
107    fn task_event(time: (u32, u32, u32), proj: &str, secs: u64) -> TaskEvent {
108        TaskEvent::new(
109            DateTime::new((2022, 2, 17), time).unwrap(),
110            proj,
111            Duration::from_secs(secs)
112        )
113    }
114
115    #[test]
116    fn test_hour_new() {
117        let hour = Hour::default();
118        assert_that!(hour.is_empty()).is_equal_to(true);
119        assert_that!(hour.iter().next()).is_none();
120        assert_that!(hour.remain()).is_equal_to(Duration::from_secs(3600));
121    }
122
123    #[test]
124    fn test_hour_add_partial() {
125        let mut hour = Hour::default();
126        let dur = task_event((8, 0, 0), "foo", 300);
127
128        assert_that!(hour.add(dur.clone())).is_none();
129
130        assert_that!(hour.is_empty()).is_equal_to(false);
131        assert_that!(hour.iter().next()).contains(&dur);
132    }
133
134    #[test]
135    fn test_hour_add_partial_remain() {
136        let mut hour = Hour::default();
137        let dur = task_event((8, 0, 0), "foo", 300);
138
139        assert_that!(hour.add(dur.clone())).is_none();
140
141        assert_that!(hour.remain()).is_equal_to(Duration::from_secs(3300));
142        let used: Duration = hour.iter().map(|d| d.duration()).sum();
143        assert_that!(hour.remain()).is_equal_to(Duration::from_secs(3600) - used);
144    }
145
146    #[test]
147    fn test_hour_add_partial_again() {
148        let mut hour = Hour::default();
149        let durs = [
150            task_event((8, 0, 0), "foo", 300),
151            task_event((8, 5, 0), "bar", 600)
152        ];
153
154        for dur in durs.iter() {
155            assert_that!(hour.add(dur.clone())).is_none();
156        }
157
158        assert_that!(hour.is_empty()).is_equal_to(false);
159
160        for (dur, hdur) in durs.iter().zip(hour.iter()) {
161            assert_that!(hdur).is_equal_to(&dur);
162        }
163    }
164
165    #[test]
166    fn test_hour_add_partial_again_remain() {
167        let mut hour = Hour::default();
168        let durs = [
169            task_event((8, 0, 0), "foo", 300),
170            task_event((8, 5, 0), "bar", 600)
171        ];
172
173        for dur in durs.iter() {
174            assert_that!(hour.add(dur.clone())).is_none();
175        }
176
177        assert_that!(hour.remain()).is_equal_to(Duration::from_secs(2700));
178        let used: Duration = hour.iter().map(|d| d.duration()).sum();
179        assert_that!(hour.remain()).is_equal_to(Duration::from_secs(3600) - used);
180    }
181
182    #[test]
183    fn test_hour_add_overflow() {
184        let mut hour = Hour::default();
185        let dur1 = task_event((8, 0, 0), "foo", 3300);
186        let dur2 = task_event((8, 55, 0), "bar", 650);
187
188        assert_that!(hour.add(dur1.clone())).is_none();
189        assert_that!(hour.add(dur2.clone())).is_some();
190
191        let expect = [dur1, task_event((8, 55, 0), "bar", 300)];
192        for (dur, hdur) in expect.iter().zip(hour.iter()) {
193            assert_that!(hdur).is_equal_to(&dur);
194        }
195    }
196
197    #[test]
198    fn test_hour_add_overflow_remain() {
199        let mut hour = Hour::default();
200        let dur1 = task_event((8, 0, 0), "foo", 3300);
201        let dur2 = task_event((8, 55, 0), "bar", 650);
202        let remain = task_event((9, 0, 0), "bar", 350);
203
204        assert_that!(hour.add(dur1.clone())).is_none();
205        assert_that!(hour.add(dur2.clone())).contains(remain);
206
207        assert_that!(hour.remain()).is_equal_to(Duration::from_secs(0));
208        let used: Duration = hour.iter().map(|d| d.duration()).sum();
209        assert_that!(used).is_equal_to(Duration::from_secs(3600));
210    }
211}