timelog/
task.rs

1//! Represention of a task event.
2//!
3//! # Examples
4//!
5//! ```rust
6//! use std::time::Duration;
7//! use timelog::{DateTime, TaskEvent};
8//!
9//! # fn main() {
10//! let timestamp = DateTime::new((2022, 03, 14), (10, 0, 0)).expect("Invalid date or time");
11//! let mut event = TaskEvent::new(
12//!     timestamp,
13//!     "project_1",
14//!     Duration::from_secs(300)
15//! );
16//! println!("{}", event.project());
17//! event.add_dur(Duration::from_secs(3000));
18//! println!("{:?}", event.duration());
19//! # }
20//! ```
21//!
22//! # Description
23//!
24//! The [`TaskEvent`] type represents a single task event containing a
25//! start time and duration.
26
27use std::time::Duration;
28
29#[doc(inline)]
30use crate::date::DateTime;
31#[doc(inline)]
32use crate::entry::Entry;
33
34/// Structure representing a single task event.
35#[derive(Debug, Clone, Eq, Ord, PartialOrd, PartialEq)]
36pub struct TaskEvent {
37    /// Timestamp of the start of this event
38    start: DateTime,
39    /// Optional [`String`] representing the project
40    proj:  Option<String>,
41    /// Calculated total [`Duration`] of the event
42    dur:   Duration
43}
44
45impl TaskEvent {
46    /// Create a [`TaskEvent`] with the supplied parameters.
47    pub fn new<'a, OS>(start: DateTime, proj: OS, dur: Duration) -> Self
48    where
49        OS: Into<Option<&'a str>>
50    {
51        Self { start, proj: proj.into().map(ToString::to_string), dur }
52    }
53
54    /// Create a [`TaskEvent`] from the supplied entry
55    pub fn from_entry(entry: &Entry) -> Self {
56        Self::new(entry.date_time(), entry.project(), Duration::default())
57    }
58
59    /// Return a tuple with two [`TaskEvent`]s, split at the supplied [`Duration`].
60    /// The first starts at the same starting point as this [`TaskEvent`] and
61    /// stopping at the supplied [`Duration`]. The second starts [`Duration`] after
62    /// the start, containg the rest of the time.
63    #[rustfmt::skip]
64    pub fn split(&self, dur: Duration) -> Option<(Self, Self)> {
65        Some((
66            Self {
67                start: self.start,
68                proj:  self.proj(),
69                dur
70            },
71            Self {
72                start: (self.start + dur).ok()?,
73                proj:  self.proj(),
74                dur:   self.dur - dur
75            }
76        ))
77    }
78
79    /// Return the project as a String if one exists.
80    pub fn proj(&self) -> Option<String> { self.proj.as_ref().map(|s| s.clone()) }
81
82    /// Return the project as a String defaulting to an empty string.
83    pub fn project(&self) -> String { self.proj().unwrap_or_default() }
84
85    /// Return the hour of the start time.
86    pub fn hour(&self) -> usize { self.start.hour() as usize }
87
88    /// Return the starting [`DateTime`].
89    pub fn start(&self) -> &DateTime { &self.start }
90
91    /// Extend the [`TaskEvent`]'s duration by the supplied amount.
92    pub fn add_dur(&mut self, dur: Duration) { self.dur += dur; }
93
94    /// Return the [`Duration`] of this [`TaskEvent`].
95    pub fn duration(&self) -> Duration { self.dur }
96
97    /// Return the number of seconds this [`TaskEvent`] has lasted.
98    pub fn as_secs(&self) -> u64 { self.dur.as_secs() }
99
100    /// Return the number of seconds after the hour of the starting time.
101    pub fn second_offset(&self) -> u32 { self.start.second_offset() }
102}
103
104#[cfg(test)]
105mod tests {
106    use spectral::prelude::*;
107
108    use super::*;
109    use crate::DateTime;
110
111    #[test]
112    fn test_new_with_project() {
113        let task = TaskEvent::new(
114            DateTime::new((2022, 3, 20), (12, 34, 56)).expect("Failed to create DateTime"),
115            "proja",
116            Duration::from_secs(305)
117        );
118        assert_that!(task.proj()).is_some().contains(String::from("proja").as_str());
119        assert_that!(task.project()).is_equal_to(String::from("proja"));
120        let expect_start =
121            DateTime::new((2022, 3, 20), (12, 34, 56)).expect("Failed to create DateTime");
122        assert_that!(task.start()).is_equal_to(&expect_start);
123        assert_that!(task.hour()).is_equal_to(12);
124        assert_that!(task.duration()).is_equal_to(&Duration::from_secs(305));
125    }
126
127    #[test]
128    fn test_new_without_project() {
129        let task = TaskEvent::new(
130            DateTime::new((2022, 3, 20), (13, 24, 56)).expect("Failed to create DateTime"),
131            None,
132            Duration::from_secs(255)
133        );
134        assert_that!(task.proj()).is_none();
135        assert_that!(task.project()).is_equal_to(String::new());
136        let expect_start =
137            DateTime::new((2022, 3, 20), (13, 24, 56)).expect("Failed to create DateTime");
138        assert_that!(task.start()).is_equal_to(&expect_start);
139        assert_that!(task.hour()).is_equal_to(13);
140        assert_that!(task.duration()).is_equal_to(&Duration::from_secs(255));
141    }
142
143    #[test]
144    fn test_add_dur() {
145        let mut task = TaskEvent::new(
146            DateTime::new((2022, 3, 20), (12, 34, 56)).expect("Failed to create DateTime"),
147            "proja",
148            Duration::from_secs(305)
149        );
150
151        assert_that!(task.as_secs()).named("Initial value").is_equal_to(305);
152
153        task.add_dur(Duration::from_secs(3600));
154        assert_that!(task.as_secs()).named("After add_dur").is_equal_to(3905);
155        assert_that!(task.duration()).is_equal_to(Duration::from_secs(3905));
156    }
157
158    #[test]
159    fn test_second_offset() {
160        let task = TaskEvent::new(
161            DateTime::new((2022, 3, 20), (12, 34, 56)).expect("Failed to create DateTime"),
162            "proja",
163            Duration::from_secs(305)
164        );
165
166        assert_that!(task.second_offset()).is_equal_to(34 * 60 + 56);
167    }
168
169    #[test]
170    fn test_split() {
171        let task = TaskEvent::new(
172            DateTime::new((2022, 3, 20), (12, 34, 56)).expect("Failed to create DateTime"),
173            "proja",
174            Duration::from_secs(600)
175        );
176
177        let pair = task.split(Duration::from_secs(250));
178        assert_that!(pair).is_some();
179        let (first, second) = pair.unwrap();
180
181        assert_that!(first.start()).is_equal_to(
182            &DateTime::new((2022, 3, 20), (12, 34, 56)).expect("Failed to create DateTime")
183        );
184        assert_that!(first.duration()).is_equal_to(Duration::from_secs(250));
185
186        assert_that!(second.start()).is_equal_to(
187            &DateTime::new((2022, 3, 20), (12, 39, 6)).expect("Failed to create DateTime")
188        );
189        assert_that!(second.duration()).is_equal_to(Duration::from_secs(350));
190    }
191}