Skip to main content

timewarrior_rs/
data.rs

1use anyhow::{bail, ensure, Result};
2use chrono::{DateTime, Datelike, Duration, Local, LocalResult, NaiveDateTime, TimeZone, Utc, Weekday, NaiveDate};
3use regex::Regex;
4use std::cmp::Ordering;
5use std::fmt::{Display, Formatter};
6use std::fs::{read_dir, File};
7use std::io;
8use std::io::BufRead;
9use std::ops::Add;
10use std::path::Path;
11use std::str::FromStr;
12
13use nom::branch::alt;
14use nom::bytes::complete::{tag, take_while1};
15use nom::character::complete::{alphanumeric0, char as nom_char, char};
16use nom::combinator::{map, map_opt, map_res};
17use nom::multi::separated_list0;
18use nom::sequence::{delimited, preceded, separated_pair};
19use nom::IResult as NomResult;
20
21fn parse_tags(text: &str) -> NomResult<&str, Vec<&str>> {
22    let sep = ' ';
23    let quote = '"';
24    let x = separated_list0(
25        nom_char(sep),
26        alt((
27            delimited(
28                nom_char(quote),
29                take_while1(|c| c != quote),
30                nom_char(quote),
31            ),
32            take_while1(|c| c != quote && c != sep),
33        )),
34    )(text);
35
36    x
37}
38
39fn parse_date(input: &str) -> NomResult<&str, DateTime<Utc>> {
40    map_opt(take_while1(|c| c != ' '), |v: &str| {
41        let parsed = NaiveDateTime::parse_from_str(v.trim(), "%Y%m%dT%H%M%SZ");
42        return if let Ok(d) = parsed {
43            Some(DateTime::<Utc>::from_utc(d, Utc))
44        } else {
45            None
46        };
47    })(input)
48}
49
50/*
51Parse a range.
52Ranges can have multiple formats:
53 <datetime> - <datetime>
54 <datetime>
55 :<period>
56where:
57  datetime is in the format "%Y%m%dT%H%M%SZ" (e.g.: 20220711T133312Z)
58  period is one of
59   - today
60   - yesterday
61   - week
62   - lastweek
63   - month
64   - lastmonth
65 */
66fn parse_range(input: &str) -> NomResult<&str, Range> {
67    alt((
68        map_res(
69            separated_pair(parse_date, tag(" - "), parse_date),
70            |(from, to)| Range::new(from, Some(to)),
71        ),
72        map_res(parse_date, |r| Range::new(r, None)),
73        preceded(char(':'), map_res(alphanumeric0, Range::from_period_str)),
74    ))(input)
75}
76
77fn parse_entry(input: &str) -> NomResult<&str, TimeEntry> {
78    preceded(
79        tag("inc "),
80        map(separated_pair(parse_range, tag(" # "), parse_tags), |t| {
81            TimeEntry {
82                range: t.0,
83                tags: t
84                    .1
85                    .into_iter()
86                    .map(|s| s.to_string().clone())
87                    .collect::<Vec<String>>(),
88                id: 0,
89            }
90        }),
91    )(input)
92}
93/// Specify an optionally opened range of time. Times are stored in UTC and expected to be given in
94/// UTC time.
95#[derive(Copy, Clone, PartialEq, Debug)]
96pub struct Range {
97    from: DateTime<Utc>,
98    to: Option<DateTime<Utc>>,
99}
100
101impl Range {
102    fn from_period_str(period: &str) -> Result<Range> {
103        match period {
104            "today" => Range::today(),
105            "yesterday" => Range::yesterday(),
106            "week" => Range::current_week(),
107            "lastweek" => Range::last_week(),
108            "month" => Range::current_month(),
109            "lastmonth" => Range::last_month(),
110            _ => bail!(""),
111        }
112    }
113
114    /// Print the duration in a HH:MM:SS format
115    pub fn pretty_duration(d: &Duration) -> String {
116        format!(
117            "{:02}:{:02}:{:02}",
118            d.num_hours(),
119            d.num_minutes() % 60,
120            d.num_seconds() % 60
121        )
122    }
123
124    /// Create a new Range with the specified `from` and `to`
125    pub fn new(from: DateTime<Utc>, to: Option<DateTime<Utc>>) -> Result<Range> {
126        if to.is_none() {
127            return Ok(Range { from, to });
128        }
129
130        return if from + Duration::seconds(1) <= to.unwrap() {
131            Ok(Range { from, to })
132        } else {
133            bail!("Range is invalid: from cannot be greater that to");
134        };
135    }
136
137    /// Create a new Range representing the day containing the given date/time
138    pub fn day(day: &DateTime<Local>) -> Result<Range> {
139        let morning = match Utc.from_local_datetime(&day.naive_utc().date().and_hms(0, 0, 0)) {
140            LocalResult::Single(t) => t,
141            _ => bail!("Cannot determine morning"),
142        };
143        let evening = match Utc.from_local_datetime(&day.naive_utc().date().and_hms(23, 59, 59)) {
144            LocalResult::Single(t) => Some(t),
145            _ => bail!("Cannot determine evening"),
146        };
147
148        Range::new(morning, evening)
149    }
150
151    /// Create a Range representing today
152    pub fn today() -> Result<Range> {
153        Self::day(&Local::now())
154    }
155
156    /// Create a Range representing yesterday
157    pub fn yesterday() -> Result<Range> {
158        let day = Local::today() - Duration::days(1);
159        Self::day(&day.and_hms(0,0,0))
160    }
161
162    /// Create a new Range representing the week containing the given date/time
163    pub fn week(day: &DateTime<Local>) -> Result<Range> {
164        let mut current = day.clone();
165        while current.weekday() != Weekday::Mon {
166            current = current - Duration::days(1);
167        }
168
169        let monday = match Utc.from_local_datetime(&current.naive_utc().date().and_hms(0, 0, 0)) {
170            LocalResult::Single(t) => t,
171            _ => bail!("Cannot determine morning"),
172        };
173
174        let sunday = match Utc.from_local_datetime(
175            &(current + Duration::days(6)).naive_utc().date().and_hms(23, 59, 59),
176        ) {
177            LocalResult::Single(t) => Some(t),
178            _ => bail!("Cannot determine morning"),
179        };
180
181        Range::new(monday, sunday)
182    }
183
184    /// Create a Range representing the current week
185    pub fn current_week() -> Result<Range> {
186        Self::week(&Local::today().and_hms(0,0,0))
187    }
188
189    /// Create a Range representing last week
190    pub fn last_week() -> Result<Range> {
191        let day = Local::today() - Duration::days(7);
192        Self::week(&day.and_hms(0,0,0))
193    }
194
195    /// Create a new Range representing the month containing the given date/time
196    pub fn month(day: &DateTime<Local>) -> Result<Range> {
197        let mut current = day.clone();
198        while current.day() != 1 {
199            current = current - Duration::days(1);
200        }
201
202        let first = match Utc.from_local_datetime(&current.naive_utc().date().and_hms(0, 0, 0)) {
203            LocalResult::Single(t) => t,
204            _ => bail!("Cannot determine morning"),
205        };
206
207        current = current + Duration::days(26);
208        while current.day() != 1 {
209            current = current + Duration::days(1);
210        }
211
212        let last = match Utc.from_local_datetime(
213            &(current - Duration::days(1)).naive_utc().date()
214                .and_hms(23, 59, 59),
215        ) {
216            LocalResult::Single(t) => Some(t),
217            _ => bail!("Cannot determine morning"),
218        };
219
220        Range::new(first, last)
221    }
222
223    /// Create a Range representing the current month
224    pub fn current_month() -> Result<Range> {
225        Self::month(&Local::today().and_hms(0,0,0))
226    }
227
228    /// Create a Range representing the last month
229    pub fn last_month() -> Result<Range> {
230        let mut current = Local::today();
231        let this_month = current.month();
232        while current.month() != this_month - 1 {
233            current = current - Duration::days(15);
234        }
235        Self::month(&current.and_hms(0,0,0))
236    }
237
238    /// Return true if the range is open. An open range is a Range that has no end set.
239    pub fn is_open(&self) -> bool {
240        self.to.is_none()
241    }
242
243    /// Return the intersection with another Range, if any.
244    pub fn intersection(&self, other: &Range) -> Option<Range> {
245        if self.to.is_none() && other.to.is_none() {
246            let from = if self.from < other.from {
247                other.from
248            } else {
249                self.from
250            };
251            return match Range::new(from, None) {
252                Ok(r) => Some(r),
253                Err(_) => None,
254            };
255        }
256
257        let from_a = self.from;
258        let to_a = self.to;
259
260        let from_b = other.from;
261        let to_b = other.to;
262
263        if let Some(to) = to_a {
264            if from_b > to {
265                return None;
266            }
267        } else {
268            let from = if self.from < other.from {
269                other.from
270            } else {
271                self.from
272            };
273            return match Range::new(from, to_b) {
274                Ok(r) => Some(r),
275                Err(_) => None,
276            };
277        }
278
279        if let Some(to) = to_b {
280            if from_a > to {
281                return None;
282            }
283        } else {
284            let from = if self.from < other.from {
285                other.from
286            } else {
287                self.from
288            };
289            return match Range::new(from, to_a) {
290                Ok(r) => Some(r),
291                Err(_) => None,
292            };
293        }
294
295        if let Some(to) = to_b {
296            if from_a > to {
297                return None;
298            }
299        }
300
301        let from = if from_a > from_b { from_a } else { from_b };
302        let to = if to_a.unwrap() > to_b.unwrap() {
303            to_b
304        } else {
305            to_a
306        };
307
308        match Range::new(from, to) {
309            Ok(r) => Some(r),
310            Err(_) => None,
311        }
312    }
313
314    /// Return the duration of a Range
315    pub fn duration(&self) -> Duration {
316        let to = match self.to {
317            Some(t) => t,
318            None => Utc::now(),
319        };
320
321        to - self.from
322    }
323
324    /// Return the number of days in the Range
325    pub fn days(&self) -> Vec<DateTime<Utc>> {
326        let mut current = self.from;
327        let end = self.to.unwrap_or(Utc::now());
328        let mut days = vec![];
329        while current <= end {
330            days.push(current);
331            current = current.add(Duration::days(1));
332        }
333
334        days
335    }
336
337    /// Split the Range at the given date/time in 2 new Range elements
338    pub fn split_at(&self, date: DateTime<Utc>) -> Result<(Range, Range)> {
339        let to = match self.to {
340            Some(t) => t,
341            None => Utc::now(),
342        };
343
344        let duration_from = date - self.from;
345        let duration_to = to - date;
346
347        Ok((
348            Range::new(self.from, Some(self.from + duration_from))?,
349            Range::new(to - duration_to, self.to)?,
350        ))
351    }
352
353    /// Split the Range at middle in 2 new Range elements
354    pub fn split(&self) -> Result<(Range, Range)> {
355        let to = match self.to {
356            Some(t) => t,
357            None => Utc::now(),
358        };
359
360        let duration = (to - self.from) / 2;
361
362        self.split_at(self.from + duration)
363    }
364}
365
366impl FromStr for Range {
367    type Err = anyhow::Error;
368
369    fn from_str(s: &str) -> Result<Self> {
370        let range = match parse_range(s) {
371            Ok((st, r)) => {
372                if !st.is_empty() {
373                    bail!("Cannot parse range {}", s)
374                }
375                r
376            }
377            Err(_) => bail!("Cannot parse range {}", s),
378        };
379
380        if let None = range.to {
381            ensure!(
382                Utc::now() - range.from >= Duration::seconds(1),
383                "From must be less than \"now - 1s\""
384            )
385        }
386
387        Ok(range)
388    }
389}
390
391impl Display for Range {
392    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
393        let from = Local.from_utc_datetime(&self.from.naive_utc());
394        if self.is_open() {
395            write!(
396                f,
397                "{} - ... [{}]",
398                from,
399                Range::pretty_duration(&self.duration())
400            )
401        } else {
402            let to = Local.from_utc_datetime(&self.to.unwrap().naive_utc());
403            write!(
404                f,
405                "{} - {} [{}]",
406                from,
407                to,
408                Range::pretty_duration(&self.duration())
409            )
410        }
411    }
412}
413
414/// Represent a time entry in timewarrior. It stores the time Range, the tags and the id of the
415/// entry.
416#[derive(Clone)]
417pub struct TimeEntry {
418    range: Range,
419    tags: Vec<String>,
420    id: usize,
421}
422
423impl TimeEntry {
424    /// Return the time Range of the entry. It can be open if the entry is currently being logged.
425    pub fn range(&self) -> &Range {
426        &self.range
427    }
428
429    /// Return a slice of the entry's tags.
430    pub fn tags(&self) -> &[String] {
431        &self.tags
432    }
433
434    /// Return the day of this entry. Note that this is the day of the start of the entry.
435    pub fn day(&self) -> NaiveDate {
436        self.range.from.naive_local().date()
437    }
438
439    /// Return the ID of the entry. IDs are not fixed for a specific entry. IDs are always counting
440    /// up from one, starting at the most recent entry.
441    pub fn id(&self) -> usize {
442        self.id
443    }
444}
445
446impl FromStr for TimeEntry {
447    type Err = anyhow::Error;
448
449    fn from_str(s: &str) -> Result<Self> {
450        match parse_entry(s) {
451            Ok((_, e)) => Ok(e),
452            Err(_) => bail!("Cannot parse \"{}\"", s),
453        }
454    }
455}
456
457impl Display for TimeEntry {
458    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
459        write!(f, "{}: {:?}", self.range(), self.tags())
460    }
461}
462
463/// Represent the work done, providing a list of time entries.
464pub struct Work {
465    entries: Vec<TimeEntry>,
466}
467
468impl Work {
469    fn load_entries_from_file(file: &Path) -> Result<Vec<TimeEntry>> {
470        let mut entries = vec![];
471        let data = File::open(file)?;
472
473        for line in io::BufReader::new(data).lines() {
474            entries.push(line?.parse()?);
475        }
476
477        Ok(entries)
478    }
479
480    /// Load entries from the given timewarrior database at data_path.
481    /// If Range is given, only the entries in that range are added to the Work.
482    pub fn load_range(data_path: &Path, range: Option<Range>) -> Result<Work> {
483        let mut entries = vec![];
484
485        let file_re = Regex::new(r"^(?P<y>\d{4})-(?P<m>\d{2}).data$").unwrap();
486
487        for file in read_dir(data_path)? {
488            let file = file?;
489            for _ in file_re.captures_iter(&file.file_name().to_string_lossy()) {
490                entries.append(&mut Work::load_entries_from_file(&file.path())?);
491            }
492        }
493
494        entries.sort_by(|a, b| {
495            if a.range.from > b.range.from {
496                Ordering::Less
497            } else if a.range.from < b.range.from {
498                Ordering::Greater
499            } else {
500                Ordering::Equal
501            }
502        });
503
504        let mut i = 1;
505        for e in &mut entries {
506            e.id = i;
507            i += 1;
508        }
509
510        return if let Some(r) = range {
511            Ok(Work {
512                entries: entries
513                    .into_iter()
514                    .filter(|e| e.range.intersection(&r).is_some())
515                    .collect(),
516            })
517        } else {
518            Ok(Work { entries })
519        };
520    }
521
522    /// Same as `load_range` but loads all entries.
523    pub fn load_all(data_path: &Path) -> Result<Work> {
524        Work::load_range(data_path, None)
525    }
526
527    /// Return a slice of the entries
528    pub fn entries(&self) -> &[TimeEntry] {
529        &self.entries
530    }
531
532    /// Return the duration of all the entries.
533    pub fn duration(&self) -> Duration {
534        self.entries()
535            .iter()
536            .fold(Duration::zero(), |a, t| a + t.range.duration())
537    }
538}
539
540impl Display for Work {
541    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
542        write!(f, "{} entries loaded", self.entries.len())
543    }
544}
545
546#[cfg(test)]
547mod range_tests {
548    use crate::data::Range;
549    use chrono::{DateTime, Duration, NaiveDateTime, Utc};
550
551    fn parse_date_time(date: &str) -> DateTime<Utc> {
552        let d = NaiveDateTime::parse_from_str(date, "%Y%m%dT%H%M%SZ").unwrap();
553        DateTime::<Utc>::from_utc(d, Utc)
554    }
555
556    #[test]
557    fn test_range_parse() {
558        // Correct range
559        assert_eq!(
560            "20220101T120000Z - 20220101T130000Z"
561                .parse::<Range>()
562                .unwrap(),
563            Range {
564                from: parse_date_time("20220101T120000Z"),
565                to: Some(parse_date_time("20220101T130000Z"))
566            }
567        );
568
569        // from > to
570        assert!("20220101T130000Z - 20220101T120000Z"
571            .parse::<Range>()
572            .is_err());
573
574        // to not specified
575        assert_eq!(
576            "20220101T120000Z".parse::<Range>().unwrap(),
577            Range {
578                from: parse_date_time("20220101T120000Z"),
579                to: None
580            }
581        );
582
583        // to not specified but from > now
584        let now = Utc::now();
585        let tomorrow = now + Duration::days(1);
586        let range = tomorrow.format("%Y%m%dT%H%M%SZ").to_string();
587        assert!(range.parse::<Range>().is_err(),);
588
589        // Wrong format
590        assert!("1日1月2022年".parse::<Range>().is_err())
591    }
592
593    #[test]
594    fn test_range_split() {
595        let input1: Range = "20220101T120000Z - 20220101T130000Z".parse().unwrap();
596        let output1: Range = "20220101T120000Z - 20220101T123000Z".parse().unwrap();
597        let output2: Range = "20220101T123000Z - 20220101T130000Z".parse().unwrap();
598
599        assert_eq!(input1.split().unwrap(), (output1, output2));
600
601        // Check that 1s duration is not split
602        let input1: Range = "20220101T120000Z - 20220101T120001Z".parse().unwrap();
603
604        assert!(input1.split().is_err());
605    }
606
607    #[test]
608    fn test_range_split_at() {
609        let input1: Range = "20220101T120000Z - 20220101T130000Z".parse().unwrap();
610        let output1: Range = "20220101T120000Z - 20220101T123000Z".parse().unwrap();
611        let output2: Range = "20220101T123000Z - 20220101T130000Z".parse().unwrap();
612
613        // Correct split
614        assert_eq!(
615            input1
616                .split_at(parse_date_time("20220101T123000Z"))
617                .unwrap(),
618            (output1, output2)
619        );
620
621        // Split gives range < 1s
622        assert!(input1
623            .split_at(parse_date_time("20220101T120000Z"))
624            .is_err());
625        assert!(input1
626            .split_at(parse_date_time("20220101T130000Z"))
627            .is_err());
628
629        // Split out of bounds
630        assert!(input1
631            .split_at(parse_date_time("20220101T110000Z"))
632            .is_err());
633        assert!(input1
634            .split_at(parse_date_time("20220101T150000Z"))
635            .is_err());
636    }
637
638    #[test]
639    fn test_range_intersect() {
640        // Check working intersection
641        let input1: Range = "20220101T120000Z - 20220101T124500Z".parse().unwrap();
642        let input2: Range = "20220101T123000Z - 20220101T130000Z".parse().unwrap();
643        let output2: Range = "20220101T123000Z - 20220101T124500Z".parse().unwrap();
644
645        assert_eq!(input1.intersection(&input2), Some(output2));
646
647        // Check close to intersection
648        let input1: Range = "20220101T120000Z - 20220101T123000Z".parse().unwrap();
649        let input2: Range = "20220101T123000Z - 20220101T130000Z".parse().unwrap();
650
651        assert!(input1.intersection(&input2).is_none());
652
653        // Check no intersection
654        let input1: Range = "20220101T120000Z - 20220101T121500Z".parse().unwrap();
655        let input2: Range = "20220101T124500Z - 20220101T130000Z".parse().unwrap();
656
657        assert!(input1.intersection(&input2).is_none());
658
659        // Check intersection with 1 opened range
660        let input1: Range = "20220711T120000Z".parse().unwrap();
661        let input2: Range = "20220711T000000Z - 20220712T000000Z".parse().unwrap();
662        let output: Range = "20220711T120000Z - 20220712T000000Z".parse().unwrap();
663
664        assert_eq!(input1.intersection(&input2).unwrap(), output);
665        assert_eq!(input2.intersection(&input1).unwrap(), output);
666
667        // Check intersection of non-crossing range with 1 opened range
668        let input1: Range = "20220718T095832Z".parse().unwrap();
669        let input2: Range = "20220711T000000Z - 20220717T235959Z".parse().unwrap();
670
671        assert!(input1.intersection(&input2).is_none());
672        assert!(input2.intersection(&input1).is_none());
673
674        // Check intersection of 2 opened ranges
675        let input1: Range = "20220718T104206Z".parse().unwrap();
676        let input2: Range = "20220717T000000Z".parse().unwrap();
677
678        assert_eq!(input1.intersection(&input2).unwrap(), input1);
679        assert_eq!(input2.intersection(&input1).unwrap(), input1);
680    }
681
682    #[test]
683    fn test_range_duration() {
684        let input1: Range = "20220101T120000Z - 20220101T124500Z".parse().unwrap();
685
686        assert_eq!(input1.duration(), Duration::minutes(45));
687    }
688}
689
690#[cfg(test)]
691mod timeentry_tests {
692    use crate::data::TimeEntry;
693    use chrono::{DateTime, Duration, NaiveDateTime, Utc};
694
695    fn parse_date_time(date: &str) -> DateTime<Utc> {
696        let d = NaiveDateTime::parse_from_str(date, "%Y%m%dT%H%M%SZ").unwrap();
697        DateTime::<Utc>::from_utc(d, Utc)
698    }
699
700    #[test]
701    fn test_timeentry_parse() {
702        let input1: TimeEntry = "inc 20220101T120000Z - 20220101T124500Z # tag1 \"tag 2\" tag3"
703            .parse()
704            .unwrap();
705
706        assert_eq!(input1.range().duration(), Duration::minutes(45));
707        assert_eq!(input1.tags(), vec!["tag1", "tag 2", "tag3"]);
708        assert!(!input1.range().is_open());
709
710        let input1: TimeEntry = "inc 20220101T120000Z # tag1 \"tag 2  \" \" t a g 3 \""
711            .parse()
712            .unwrap();
713
714        assert_eq!(input1.range().from, parse_date_time("20220101T120000Z"));
715        assert_eq!(input1.tags(), vec!["tag1", "tag 2  ", " t a g 3 "]);
716        assert!(input1.range().is_open());
717    }
718}