Skip to main content

timelog/
entry.rs

1//! Module representing an entry in the timelog.
2//!
3//! # Examples
4//!
5//! ```rust
6//! use timelog::entry::Entry;
7//! use std::fs::File;
8//! use std::io::{BufRead, BufReader};
9//!
10//! fn day_entrys(date: &str, file: &mut File) -> Vec<Entry> {
11//!     let mut reader = BufReader::new(file);
12//!     reader.lines()
13//!           .filter_map(|line| Entry::from_line(&line.ok()?).ok())
14//!           .filter(|ev| ev.stamp() == String::from(date))
15//!           .collect::<Vec<Entry>>()
16//! }
17//! ```
18//!
19//! # Description
20//!
21//! Objects of this type represent the individual lines in the `timelog.txt` file.
22//! Each [`Entry`] has a date and time stamp, an optional project, and a task.
23
24use std::fmt::{self, Debug, Display};
25
26use once_cell::sync::Lazy;
27use regex::Regex;
28
29const STOP_CMD: &str = "stop";
30
31// These should not be able to fail, hardcoded input strings.
32// Still using expect() in case the regex strings ever get changed.
33
34/// Regular expression to match a time stamp
35static TIMESTAMP_RE: Lazy<Regex> = Lazy::new(|| {
36    Regex::new(r"\A(\d{4}[-/](?:0[1-9]|1[0-2])[-/](?:0[1-9]|[12][0-9]|3[01]) (?:[01][0-9]|2[0-3]):[0-5][0-9]:[0-6][0-9])").expect("Date time Regex failed.")
37});
38/// A somewhat lax regular expression to match an entry line.
39static LAX_LINE_RE: Lazy<Regex> = Lazy::new(|| {
40    Regex::new(r"\A(\d{4}[-/](?:0[1-9]|1[0-2])[-/](?:0[1-9]|[12][0-9]|3[01]) (?:[01][0-9]|2[0-3]):[0-5][0-9]:[0-6][0-9])(.)(.+)").expect("Entry line Regex failed.")
41});
42/// A regular expression matching the project part of an entry line.
43pub static PROJECT_RE: Lazy<Regex> =
44    Lazy::new(|| Regex::new(r"\+(\S+)").expect("Entry project regex failed."));
45/// A regular expression matching the task part of an entry line.
46static TASKNAME_RE: Lazy<Regex> =
47    Lazy::new(|| Regex::new(r"@(\S+)").expect("Task name Regex failed."));
48/// A regular expression matching a stop line
49static STOP_LINE: Lazy<Regex> = Lazy::new(|| {
50    Regex::new(r"\A(\d{4}[-/](?:0[1-9]|1[0-2])[-/](?:0[1-9]|[12][0-9]|3[01]) (?:[01][0-9]|2[0-3]):[0-5][0-9]:[0-6][0-9]) stop").expect("Stop line Regex failed")
51});
52/// A regular expression matching a stop line
53static EVENT_LINE: Lazy<Regex> = Lazy::new(|| {
54    Regex::new(r"\A(\d{4}[-/](?:0[1-9]|1[0-2])[-/](?:0[1-9]|[12][0-9]|3[01]) (?:[01][0-9]|2[0-3]):[0-5][0-9]:[0-6][0-9])\^.+").expect("Event line Regex failed")
55});
56/// Regular expression matching the year portion of an entry line.
57pub static YEAR_RE: Lazy<Regex> =
58    Lazy::new(|| Regex::new(r"^(\d\d\d\d)").expect("Date regex failed"));
59/// Regular expression extracting the marker from the line.
60pub static MARKER_RE: Lazy<Regex> =
61    Lazy::new(|| Regex::new(r"^\d{4}-\d\d-\d\d \d\d:\d\d:\d\d(.)").expect("Marker regex failed"));
62
63#[doc(inline)]
64use crate::date::{Date, DateTime};
65
66pub mod error;
67pub mod kind;
68
69/// Errors associated with the entry.
70pub use error::EntryError;
71/// The kind of entry
72pub use kind::EntryKind;
73
74/// Representation of an entry in the log
75///
76/// Objects of this type represent individual lines in the `timelog.txt` file.
77/// Each [`Entry`] has a date and time stamp, an optional project, and a task.
78#[derive(Debug, Clone, Eq, PartialEq)]
79#[must_use]
80pub struct Entry {
81    /// Time that this entry began
82    time:    DateTime,
83    /// An optional project name
84    project: Option<String>,
85    /// The text of the entry from the entry line
86    text:    String,
87    /// The type of the entry
88    kind:    EntryKind
89}
90
91/// # Line parsing tools
92impl Entry {
93    /// Parse the entry line text into the task name and detail parts if they exist.
94    pub fn task_breakdown(entry_text: &str) -> (Option<String>, Option<String>) {
95        if entry_text.is_empty() {
96            return (None, None);
97        }
98
99        let task = PROJECT_RE.replace(entry_text, "").trim().to_string();
100        if let Some(caps) = TASKNAME_RE.captures(&task) {
101            if let Some(tname) = caps.get(1) {
102                let detail = TASKNAME_RE.replace(&task, "").trim().to_string();
103                let tname = tname.as_str().to_string();
104                return (Some(tname), (!detail.is_empty()).then_some(detail));
105            }
106        }
107        (None, (!task.is_empty()).then_some(task))
108    }
109
110    /// Return `true` if the supplied string looks like a stop line.
111    pub fn is_stop_line(line: &str) -> bool { STOP_LINE.is_match(line) }
112
113    /// Return `true` if the supplied string looks like an event line.
114    pub fn is_event_line(line: &str) -> bool { EVENT_LINE.is_match(line) }
115
116    /// Extract a date/time string from a task line
117    pub fn datetime_from_line(line: &str) -> Option<&str> {
118        if line.is_empty() || Self::is_comment_line(line) {
119            return None;
120        }
121
122        if let Some(caps) = LAX_LINE_RE.captures(line) {
123            return caps.get(1).map(|s| s.as_str());
124        }
125        None
126    }
127
128    /// Extract a date string from a task line
129    pub fn date_from_line(line: &str) -> Option<&str> {
130        Self::datetime_from_line(line).and_then(|s| s.split_whitespace().next())
131    }
132
133    /// Return the year for the supplied entry line, if any.
134    pub fn extract_year(line: &str) -> Option<i32> {
135        if Self::is_comment_line(line) {
136            return None;
137        }
138
139        YEAR_RE
140            .captures(line)
141            .and_then(|cap| cap[0].parse::<i32>().ok())
142    }
143
144    /// Return `true` if the supplied line is a comment.
145    pub fn is_comment_line(line: &str) -> bool { line.starts_with('#') }
146}
147
148/// # Constructors
149impl Entry {
150    /// Create a new [`Entry`] representing the supplied task at the supplied time.
151    pub fn new(entry_text: &str, time: DateTime) -> Self {
152        Self::new_marked(entry_text, time, EntryKind::Start)
153    }
154
155    /// Create a new [`Entry`] representing the supplied task at the supplied time and optional
156    /// mark.
157    pub fn new_marked(entry_text: &str, time: DateTime, kind: EntryKind) -> Self {
158        let kind = if kind == EntryKind::Start && entry_text == STOP_CMD {
159            EntryKind::Stop
160        }
161        else {
162            kind
163        };
164        let oproject = PROJECT_RE.captures(entry_text)
165            .and_then(|caps| caps.get(1).map(|m| String::from(m.as_str())));
166        Self { time, project: oproject, text: entry_text.into(), kind }
167    }
168
169    /// Create a new [`Entry`] representing a stop entry for the supplied [`DateTime`]
170    pub fn new_stop(time: DateTime) -> Self { Self::new_marked(STOP_CMD, time, EntryKind::Stop) }
171
172    /// Create a new [`Entry`] representing the entry from the supplied line.
173    ///
174    /// This entry must be formatted as described in Format.md.
175    ///
176    /// # Errors
177    ///
178    /// Return an [`EntryError`] if the line is empty or formatted incorrectly.
179    pub fn from_line(line: &str) -> Result<Self, EntryError> {
180        if line.is_empty() {
181            return Err(EntryError::BlankLine);
182        }
183
184        match LAX_LINE_RE.captures(line) {
185            Some(caps) => {
186                let Some(stamp) = caps.get(1) else { return Err(EntryError::InvalidTimeStamp); };
187                let Ok(time) = stamp.as_str().parse::<DateTime>() else {
188                    return Err(EntryError::InvalidTimeStamp);
189                };
190                let kind = EntryKind::try_new(caps.get(2).and_then(|m| m.as_str().chars().next()))?;
191                Ok(Entry::new_marked(
192                    caps.get(3).map_or("", |m| m.as_str()),
193                    time,
194                    kind
195                ))
196            }
197            None => Err(if TIMESTAMP_RE.is_match(line) {
198                EntryError::MissingTask
199            }
200            else {
201                EntryError::InvalidTimeStamp
202            })
203        }
204    }
205}
206
207/// # Accessors
208impl Entry {
209    /// Return the [`&str`] designated as the project, if any, from the [`Entry`].
210    pub fn project(&self) -> Option<&str> { self.project.as_deref() }
211
212    /// Return the [`&str`] containing all of the [`Entry`] except the time and date.
213    pub fn entry_text(&self) -> &str { &self.text }
214
215    /// Return the [`String`] containing all of the [`Entry`] except the time and date.
216    pub fn task(&self) -> Option<String> { Self::task_breakdown(&self.text).0 }
217
218    /// Return the [`String`] containing all of the [`Entry`] except the time and date.
219    pub fn detail(&self) -> Option<String> { Self::task_breakdown(&self.text).1 }
220
221    /// Return the [`String`] containing all of the [`Entry`] except the time and date.
222    pub fn task_and_detail(&self) -> (Option<String>, Option<String>) {
223        Self::task_breakdown(&self.text)
224    }
225
226    /// Return the time for the start of the [`Entry`] in epoch seconds.
227    pub fn epoch(&self) -> i64 { self.time.timestamp() }
228
229    /// Return the date for the start of the [`Entry`] as a [`Date`]
230    pub fn date(&self) -> Date { self.time.date() }
231
232    /// Return the time for the start of the [`Entry`] as a [`DateTime`]
233    pub fn date_time(&self) -> DateTime { self.time }
234
235    /// Return the time for the start of the [`Entry`] as a [`DateTime`]
236    #[rustfmt::skip]
237    pub fn timestamp(&self) -> String {
238        format!("{} {:02}:{:02}", self.time.date(), self.time.hour(), self.time.minute())
239    }
240
241    /// Return the date stamp of the [`Entry`] in 'YYYY-MM-DD' format.
242    pub fn stamp(&self) -> String { self.date().to_string() }
243
244    /// Return `true` if this a start [`Entry`].
245    pub fn is_start(&self) -> bool { self.kind == EntryKind::Start }
246
247    /// Return `true` if this was a stop [`Entry`].
248    pub fn is_stop(&self) -> bool { self.kind == EntryKind::Stop }
249
250    /// Return `true` if this was an ignored [`Entry`].
251    pub fn is_ignore(&self) -> bool { self.kind == EntryKind::Ignored }
252
253    /// Return an ignored [`Entry`] converted from this one.
254    pub fn ignore(&self) -> Self { Self { kind: EntryKind::Ignored, ..self.clone() } }
255
256    /// Return `true` if this was a event [`Entry`].
257    pub fn is_event(&self) -> bool { self.kind == EntryKind::Event }
258}
259
260/// # Mutators
261impl Entry {
262    /// Return a new copy of the current [`Entry`] with the date and time
263    /// reset the the supplied value.
264    pub fn change_date_time(&self, date_time: DateTime) -> Self {
265        let mut entry = self.clone();
266        entry.time = date_time;
267        entry
268    }
269
270    /// Return a new copy of the current [`Entry`] with the date and time
271    /// reset the the supplied value.
272    pub fn change_text(&self, task: &str) -> Self {
273        if self.is_stop() { return self.clone(); }
274        Self::new_marked(task, self.time, self.kind)
275    }
276
277    /// Return a new [`Entry`] timestamped as the end of this date
278    pub fn to_day_end(&self) -> Self { Self { time: self.date().day_end(), ..self.clone() } }
279}
280
281impl Display for Entry {
282    /// Format the [`Entry`] formatted as described in Format.md.
283    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
284        let mark = match self.kind {
285            EntryKind::Ignored => '!',
286            EntryKind::Event => '^',
287            _ => ' '
288        };
289        write!(f, "{}{mark}{}", self.time, self.text)
290    }
291}
292
293impl PartialOrd for Entry {
294    /// This method returns an ordering between self and other values if one exists.
295    #[rustfmt::skip]
296    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
297        Some(self.time.cmp(&other.time)
298            .then_with(|| self.text.cmp(&other.text)))
299    }
300}
301
302#[cfg(test)]
303mod tests {
304    use assert2::{assert, let_assert};
305    use rstest::rstest;
306
307    use super::*;
308
309    const CANONICAL_LINE: &str = "2013-06-05 10:00:02 +proj1 @do something";
310    const IGNORED_LINE: &str = "2013-06-05 10:00:02!+proj1 @do something";
311    const EVENT_LINE: &str = "2013-06-05 10:00:02^+proj1 @do something";
312    const STOP_LINE: &str = "2013-06-05 10:00:02 stop";
313
314    fn reference_time() -> i64 { DateTime::new((2013, 6, 5), (10, 0, 2)).expect("Hardcoded value").timestamp() }
315
316    #[test]
317    fn from_line_error_if_empty() {
318        assert!(Err(EntryError::BlankLine) == Entry::from_line(""));
319    }
320
321    #[rstest]
322    #[case("# Random comment", "simple comment")]
323    #[case("#2013-06-05 10:00:02 +test @Commented", "commented entry")]
324    fn is_comment_found(#[case]input: &str, #[case]msg: &str) {
325        assert!(Entry::is_comment_line(input), "{msg}");
326    }
327
328    #[rstest]
329    #[case("", "empty line")]
330    #[case("2013-06-05 10:00:02 +test @Commented", "entry line")]
331    fn is_comment_not_found(#[case]input: &str, #[case]msg: &str) {
332        assert!(!Entry::is_comment_line(input), "{msg}");
333    }
334
335    #[rstest]
336    #[case("", "empty line")]
337    #[case("# Random comment", "simple comment")]
338    #[case("#2013-06-05 10:00:02 +test @Commented", "commented entry")]
339    fn test_datetime_not_found(#[case]input: &str, #[case]msg: &str) {
340        assert!(None == Entry::datetime_from_line(input), "{msg}");
341    }
342
343    #[rstest]
344    #[case(CANONICAL_LINE, "2013-06-05 10:00:02", "entry line")]
345    #[case(IGNORED_LINE,  "2013-06-05 10:00:02", "ignored line")]
346    #[case(EVENT_LINE,  "2013-06-05 10:00:02", "event line")]
347    #[case(STOP_LINE,  "2013-06-05 10:00:02", "stop line")]
348    fn test_datetime_from_line(#[case]input: &str, #[case]expected: &str, #[case]msg: &str) {
349        let_assert!(Some(dt) = Entry::datetime_from_line(input));
350        assert!(dt == expected, "{msg}");
351    }
352
353    #[rstest]
354    #[case("", "empty line")]
355    #[case("# Random comment", "simple comment")]
356    #[case("#2013-06-05 10:00:02 +test @Commented", "entry line")]
357    fn test_date_not_found(#[case]input: &str, #[case]msg: &str) {
358        assert!(None == Entry::date_from_line(input), "{msg}");
359    }
360
361    #[rstest]
362    #[case(CANONICAL_LINE, "2013-06-05", "entry line")]
363    #[case(IGNORED_LINE,  "2013-06-05", "ignored line")]
364    #[case(EVENT_LINE,  "2013-06-05", "event line")]
365    #[case(STOP_LINE,  "2013-06-05", "stop line")]
366    fn test_date_from_line(#[case]input: &str, #[case]expected: &str, #[case]msg: &str) {
367        let_assert!(Some(dt) = Entry::date_from_line(input));
368        assert!(dt == expected, "{msg}");
369    }
370
371    #[test]
372    fn from_line_error_if_not_entry() {
373        assert!(Err(EntryError::InvalidTimeStamp) == Entry::from_line("This is not an entry"));
374    }
375
376    #[test]
377    fn from_line_canonical_entry() {
378        let_assert!(Ok(entry) = Entry::from_line(CANONICAL_LINE));
379        assert!(entry.stamp() == String::from("2013-06-05"));
380        assert!(entry.project() == Some("proj1"));
381        assert!(entry.entry_text() == "+proj1 @do something");
382        assert!(entry.task() == Some(String::from("do")));
383        assert!(entry.detail() == Some(String::from("something")));
384        assert!(entry.task_and_detail() == (Some(String::from("do")), Some(String::from("something"))));
385        assert!(entry.epoch() == reference_time());
386        assert!(entry.to_string().as_str() == CANONICAL_LINE);
387        assert!(!entry.is_stop());
388        assert!(!entry.is_ignore());
389        assert!(!entry.is_event());
390    }
391
392    #[test]
393    fn new_canonical_entry() {
394        let_assert!(Ok(canonical_time) = "2013-06-05 10:00:02".parse::<DateTime>());
395        let entry = Entry::new("+proj1 @do something", canonical_time);
396        assert!(entry.stamp() == String::from("2013-06-05"));
397        assert!(entry.project() == Some("proj1"));
398        assert!(entry.entry_text() == "+proj1 @do something");
399        assert!(entry.task() == Some(String::from("do")));
400        assert!(entry.detail() == Some(String::from("something")));
401        assert!(entry.task_and_detail() == (Some(String::from("do")), Some(String::from("something"))));
402        assert!(entry.epoch() == reference_time());
403        assert!(entry.to_string().as_str() == CANONICAL_LINE);
404        assert!(!entry.is_stop());
405        assert!(!entry.is_ignore());
406        assert!(!entry.is_event());
407    }
408
409    #[test]
410    fn from_line_no_task_entry() {
411        const LINE: &str = "2013-06-05 10:00:02 +proj1 do something";
412        let_assert!(Ok(entry) = Entry::from_line(LINE));
413        assert!(entry.stamp() == String::from("2013-06-05"));
414        assert!(entry.project() == Some("proj1"));
415        assert!(entry.entry_text() == "+proj1 do something");
416        assert!(entry.task() == None);
417        assert!(entry.detail() == Some(String::from("do something")));
418        assert!(entry.task_and_detail() == (None, Some(String::from("do something"))));
419        assert!(entry.epoch() == reference_time());
420        assert!(entry.to_string().as_str() == LINE);
421        assert!(!entry.is_stop());
422        assert!(!entry.is_ignore());
423        assert!(!entry.is_event());
424    }
425
426    #[test]
427    fn from_line_no_detail_entry() {
428        const LINE: &str = "2013-06-05 10:00:02 +proj1 @something";
429        let_assert!(Ok(entry) = Entry::from_line(LINE));
430        assert!(entry.stamp() == String::from("2013-06-05"));
431        assert!(entry.project() == Some("proj1"));
432        assert!(entry.entry_text() == "+proj1 @something");
433        assert!(entry.task() == Some(String::from("something")));
434        assert!(entry.detail() == None);
435        assert!(entry.task_and_detail() == (Some(String::from("something")), None));
436        assert!(entry.epoch() == reference_time());
437        assert!(entry.to_string().as_str() == LINE);
438        assert!(!entry.is_stop());
439        assert!(!entry.is_ignore());
440        assert!(!entry.is_event());
441    }
442
443    #[test]
444    fn from_line_no_entry_text() {
445        const LINE: &str = "2013-06-05 10:00:02 +proj1";
446        let_assert!(Ok(entry) = Entry::from_line(LINE));
447        assert!(entry.stamp() == String::from("2013-06-05"));
448        assert!(entry.project() == Some("proj1"));
449        assert!(entry.entry_text() == "+proj1");
450        assert!(entry.task() == None);
451        assert!(entry.detail() == None);
452        assert!(entry.task_and_detail() == (None, None));
453        assert!(entry.epoch() == reference_time());
454        assert!(entry.to_string().as_str() == LINE);
455        assert!(!entry.is_stop());
456        assert!(!entry.is_ignore());
457        assert!(!entry.is_event());
458    }
459
460    #[test]
461    fn from_line_stop_entry() {
462        let_assert!(Ok(entry) = Entry::from_line(STOP_LINE));
463        assert!(entry.stamp() == String::from("2013-06-05"));
464        assert!(entry.project() == None);
465        assert!(entry.entry_text() == "stop");
466        assert!(entry.task() == None);
467        assert!(entry.detail() == Some(String::from("stop")));
468        assert!(entry.task_and_detail() == (None, Some(String::from("stop"))));
469        assert!(entry.epoch() == reference_time());
470        assert!(entry.to_string().as_str() == STOP_LINE);
471        assert!(entry.is_stop());
472        assert!(!entry.is_ignore());
473        assert!(!entry.is_event());
474    }
475
476    #[test]
477    fn test_extract_year() {
478        let line = "2018-11-20 12:34:43 +test @Event";
479        assert!(Some(2018) == Entry::extract_year(line));
480    }
481
482    #[test]
483    fn test_extract_year_fail() {
484        let line = "xyzzy 2018-11-20 12:34:43 +test @Event";
485        assert!(Entry::extract_year(line) == None);
486    }
487
488    #[test]
489    fn new_stop_entry() {
490        let_assert!(Ok(canonical_time) = "2013-06-05 10:00:02".parse::<DateTime>());
491        let entry = Entry::new("stop", canonical_time);
492        assert!(entry.stamp() == String::from("2013-06-05"));
493        assert!(entry.project() == None);
494        assert!(entry.entry_text() == "stop");
495        assert!(entry.task() == None);
496        assert!(entry.detail() == Some(String::from("stop")));
497        assert!(entry.task_and_detail() == (None, Some(String::from("stop"))));
498        assert!(entry.epoch() == reference_time());
499        assert!(entry.to_string().as_str() == STOP_LINE);
500        assert!(entry.is_stop());
501        assert!(!entry.is_ignore());
502        assert!(!entry.is_event());
503    }
504
505    #[test]
506    fn from_line_ignored_entry() {
507        let_assert!(Ok(entry) = Entry::from_line(IGNORED_LINE));
508        assert!(entry.stamp() == String::from("2013-06-05"));
509        assert!(entry.project() == Some("proj1"));
510        assert!(entry.entry_text() == "+proj1 @do something");
511        assert!(entry.task() == Some(String::from("do")));
512        assert!(entry.detail() == Some(String::from("something")));
513        assert!(entry.task_and_detail() == (Some(String::from("do")), Some(String::from("something"))));
514        assert!(entry.epoch() == reference_time());
515        assert!(entry.to_string().as_str() == IGNORED_LINE);
516        assert!(!entry.is_stop());
517        assert!(entry.is_ignore());
518        assert!(!entry.is_event());
519    }
520
521    #[test]
522    fn new_ignored_entry() {
523        let_assert!(Ok(canonical_time) = "2013-06-05 10:00:02".parse::<DateTime>());
524        let entry = Entry::new_marked("+proj1 @do something", canonical_time, EntryKind::Ignored);
525        assert!(entry.stamp() == String::from("2013-06-05"));
526        assert!(entry.project() == Some("proj1"));
527        assert!(entry.entry_text() == "+proj1 @do something");
528        assert!(entry.task() == Some(String::from("do")));
529        assert!(entry.detail() == Some(String::from("something")));
530        assert!(entry.task_and_detail() == (Some(String::from("do")), Some(String::from("something"))));
531        assert!(entry.epoch() == reference_time());
532        assert!(entry.to_string().as_str() == IGNORED_LINE);
533        assert!(!entry.is_stop());
534        assert!(entry.is_ignore());
535        assert!(!entry.is_event());
536    }
537
538    #[test]
539    fn from_line_event_entry() {
540        let_assert!(Ok(entry) = Entry::from_line(EVENT_LINE));
541        assert!(entry.stamp() == String::from("2013-06-05"));
542        assert!(entry.project() == Some("proj1"));
543        assert!(entry.entry_text() == "+proj1 @do something");
544        assert!(entry.task() == Some(String::from("do")));
545        assert!(entry.detail() == Some(String::from("something")));
546        assert!(entry.task_and_detail() == (Some(String::from("do")), Some(String::from("something"))));
547        assert!(entry.epoch() == reference_time());
548        assert!(entry.to_string().as_str() == EVENT_LINE);
549        assert!(!entry.is_stop());
550        assert!(!entry.is_ignore());
551        assert!(entry.is_event());
552    }
553
554    #[test]
555    fn new_event_entry() {
556        let_assert!(Ok(canonical_time) = "2013-06-05 10:00:02".parse::<DateTime>());
557        let entry = Entry::new_marked("+proj1 @do something", canonical_time, EntryKind::Event);
558        assert!(entry.stamp() == String::from("2013-06-05"));
559        assert!(entry.project() == Some("proj1"));
560        assert!(entry.entry_text() == "+proj1 @do something");
561        assert!(entry.task() == Some(String::from("do")));
562        assert!(entry.detail() == Some(String::from("something")));
563        assert!(entry.task_and_detail() == (Some(String::from("do")), Some(String::from("something"))));
564        assert!(entry.epoch() == reference_time());
565        assert!(entry.to_string().as_str() == EVENT_LINE);
566        assert!(!entry.is_stop());
567        assert!(!entry.is_ignore());
568        assert!(entry.is_event());
569    }
570
571    #[test]
572    fn compare_entry() {
573        let_assert!(Ok(entry1) = Entry::from_line("2013-06-05 10:00:02 +proj1"));
574        let_assert!(Ok(entry2) = Entry::from_line("2013-06-05 11:00:02 +proj1"));
575        assert!(entry2 > entry1);
576        assert!(entry1 < entry2);
577        assert!(entry1 == entry1);
578    }
579
580    #[test]
581    fn test_change_date_time_start() {
582        let_assert!(Ok(entry) = Entry::from_line("2022-12-27 10:00:00 +proj1"));
583        let_assert!(Ok(dt) = DateTime::new((2022, 12, 27), (9, 50, 00)));
584        let new_entry = entry.change_date_time(dt);
585
586        assert!(new_entry != entry);
587        let_assert!(Ok(dt) = DateTime::new((2022, 12, 27), (9, 50, 00)));
588        assert!(new_entry.date_time() == dt);
589        assert!(new_entry.entry_text() == entry.entry_text());
590        assert!(new_entry.is_start());
591    }
592
593    #[test]
594    fn test_change_date_time_event() {
595        let_assert!(Ok(entry) = Entry::from_line("2022-12-27 10:00:00^+event"));
596        let_assert!(Ok(dt) = DateTime::new((2022, 12, 27), (9, 50, 00)));
597        let new_entry = entry.change_date_time(dt);
598
599        assert!(new_entry != entry);
600        let_assert!(Ok(dt) = DateTime::new((2022, 12, 27), (9, 50, 00)));
601        assert!(new_entry.date_time() == dt);
602        assert!(new_entry.entry_text() == entry.entry_text());
603        assert!(new_entry.is_event());
604    }
605
606    #[test]
607    fn test_change_date_time_ignored() {
608        let_assert!(Ok(entry) = Entry::from_line("2022-12-27 10:00:00!+proj1"));
609        let_assert!(Ok(dt) = DateTime::new((2022, 12, 27), (9, 50, 00)));
610        let new_entry = entry.change_date_time(dt);
611
612        assert!(new_entry != entry);
613        let_assert!(Ok(dt) = DateTime::new((2022, 12, 27), (9, 50, 00)));
614        assert!(new_entry.date_time() == dt);
615        assert!(new_entry.entry_text() == entry.entry_text());
616        assert!(new_entry.is_ignore());
617    }
618
619    #[test]
620    fn test_change_date_time_stop() {
621        let_assert!(Ok(entry) = Entry::from_line("2022-12-27 10:00:00 stop"));
622        let_assert!(Ok(dt) = DateTime::new((2022, 12, 27), (9, 50, 00)));
623        let new_entry = entry.change_date_time(dt);
624
625        assert!(new_entry != entry);
626        let_assert!(Ok(dt) = DateTime::new((2022, 12, 27), (9, 50, 00)));
627        assert!(new_entry.date_time() == dt);
628        assert!(new_entry.entry_text() == entry.entry_text());
629        assert!(new_entry.is_stop());
630    }
631
632    #[test]
633    fn test_change_text_start() {
634        let_assert!(Ok(entry) = Entry::from_line("2022-12-27 10:00:00 +proj1"));
635        let new_entry = entry.change_text("+proj2 @Changed");
636
637        assert!(new_entry != entry);
638        assert!(new_entry.date_time() == entry.date_time());
639        assert!(new_entry.entry_text() == "+proj2 @Changed");
640        assert!(new_entry.is_start());
641    }
642
643    #[test]
644    fn test_change_text_event() {
645        let_assert!(Ok(entry) = Entry::from_line("2022-12-27 10:00:00^+event"));
646        let new_entry = entry.change_text("+proj2 @Changed");
647
648        assert!(new_entry != entry);
649        assert!(new_entry.date_time() == entry.date_time());
650        assert!(new_entry.entry_text() == "+proj2 @Changed");
651        assert!(new_entry.is_event());
652    }
653
654    #[test]
655    fn test_change_text_ignored() {
656        let_assert!(Ok(entry) = Entry::from_line("2022-12-27 10:00:00!+proj1"));
657        let new_entry = entry.change_text("+proj2 @Changed");
658
659        assert!(new_entry != entry);
660        assert!(new_entry.date_time() == entry.date_time());
661        assert!(new_entry.entry_text() == "+proj2 @Changed");
662        assert!(new_entry.is_ignore());
663    }
664
665    #[test]
666    fn test_change_text_stop() {
667        let_assert!(Ok(entry) = Entry::from_line("2022-12-27 10:00:00 stop"));
668        let new_entry = entry.change_text("+proj2 @Changed");
669
670        assert!(new_entry == entry);
671        assert!(new_entry.date_time() == entry.date_time());
672        assert!(new_entry.entry_text() == "stop");
673        assert!(new_entry.is_stop());
674    }
675}