jobrog/
log.rs

1// stuff for
2extern crate chrono;
3extern crate clap;
4extern crate larry;
5extern crate pidgin;
6extern crate regex;
7extern crate serde_json;
8use crate::configure::Configuration;
9use crate::util::{duration_string, log_path};
10use chrono::{Datelike, Duration, Local, NaiveDate, NaiveDateTime, Timelike};
11use clap::ArgMatches;
12use larry::Larry;
13use pidgin::{Grammar, Matcher};
14use regex::{Regex, RegexSet};
15use std::fs::{File, OpenOptions};
16use std::io::{BufRead, BufReader, Lines, Write};
17use std::path::PathBuf;
18
19lazy_static! {
20    // making this public is useful for testing, but best to keep it hidden to
21    // limit complexity and commitment
22    #[doc(hidden)]
23    pub static ref LOG_LINES: Grammar = grammar!{
24
25        TOP -> r(r"\A") <log_item> r(r"\z")
26
27        log_item         -> <timestamped_item> | <blank> | <comment>
28        blank            -> r(r"\s*")
29        comment          -> r(r"\s*#.*")
30        timestamped_item -> <timestamp> <ti_continuation>
31        timestamp        -> r(r"\s*[1-9]\d{3}(?:\s+\d{1,2}){5}\s*")
32        ti_continuation  -> <taggable> | <done>
33        taggable         -> <tag_separator> <tags> (":") <description>
34        tag_separator    -> <event> | <note>
35        event            -> (":")
36        note             -> ("<NOTE>")
37        done             -> r(r":DONE\s*")
38        tags             -> r(r"(?:\\.|[^:<\\])*") // colons, spaces, and < must be escaped, so the escape character \ must also be escaped
39        description      -> r(r".*")
40    };
41    pub static ref MATCHER: Matcher = LOG_LINES.matcher().unwrap();
42}
43
44// parses a log line into an appropriate data structure preserving the line offset
45pub fn parse_line(line: &str, offset: usize) -> Item {
46    if let Some(ast) = MATCHER.parse(line) {
47        if let Some(timestamp) = ast.name("timestamp") {
48            match parse_timestamp(timestamp.as_str()) {
49                Err(s) => Item::Error(s, offset),
50                Ok(timestamp) => {
51                    if ast.has("done") {
52                        Item::Done(Done(timestamp), offset)
53                    } else {
54                        let tags = parse_tags(ast.name("tags").unwrap().as_str());
55                        let description = ast.name("description").unwrap().as_str();
56                        if ast.has("event") {
57                            Item::Event(
58                                Event {
59                                    start: timestamp,
60                                    start_overlap: false,
61                                    end: None,
62                                    end_overlap: false,
63                                    description: description.to_owned(),
64                                    tags: tags,
65                                    vacation: false,
66                                    vacation_type: None,
67                                },
68                                offset,
69                            )
70                        } else {
71                            Item::Note(
72                                Note {
73                                    time: timestamp,
74                                    description: description.to_owned(),
75                                    tags: tags,
76                                },
77                                offset,
78                            )
79                        }
80                    }
81                }
82            }
83        } else if ast.has("blank") {
84            Item::Blank(offset)
85        } else {
86            Item::Comment(offset)
87        }
88    } else {
89        Item::Error(String::from("unexpected line format"), offset)
90    }
91}
92
93pub struct LogController {
94    pub larry: Larry,
95    pub path: String,
96}
97
98impl LogController {
99    pub fn new(
100        log: Option<PathBuf>,
101        conf: &Configuration,
102    ) -> Result<LogController, std::io::Error> {
103        let log = log.unwrap_or(log_path(conf.directory()));
104        let path = log.as_path().to_str();
105        Larry::new(log.as_path()).and_then(|log| {
106            Ok(LogController {
107                larry: log,
108                path: path.unwrap().to_owned(),
109            })
110        })
111    }
112    // find best line offset for a timestamp in a log file
113    // best is the earliest instance of the line with the timestamp or, barring that, the earliest
114    // timestamped line immediately before the timestamp
115    pub fn find_line(&mut self, time: &NaiveDateTime) -> Option<Item> {
116        if let Some(start) = self.get_after(0) {
117            let end = self.get_before(self.larry.len() - 1);
118            let time = start.advance(time);
119            Some(self.narrow_in(&time, start, end))
120        } else {
121            None
122        }
123    }
124    pub fn first_timestamp(&self) -> Option<NaiveDateTime> {
125        let item = ItemsAfter::new(0, &self.path).find(|i| i.has_time());
126        item.and_then(|i| Some(i.time().unwrap().0.clone()))
127    }
128    pub fn last_timestamp(&mut self) -> Option<NaiveDateTime> {
129        let item = ItemsBefore::new(self.larry.len(), self).find(|i| i.has_time());
130        item.and_then(|i| Some(i.time().unwrap().0.clone()))
131    }
132    fn narrow_in(&mut self, time: &NaiveDateTime, start: Item, end: Item) -> Item {
133        let start = self.advance_to_first(start);
134        let (t1, mut o1) = start.time().unwrap();
135        if t1 == time {
136            return start;
137        }
138        let (t2, o2) = end.time().unwrap();
139        if t2 == time {
140            return end;
141        } else if t1 == t2 {
142            return start;
143        }
144        // we want to find an intermediate index at this point but are concerned not to
145        // get into an infinite loop where we estimate an intermediate index, loop for the timed
146        // event at or before that index, and return to our start item
147        let mut o3 = self.estimate(time, t1, o1, t2, o2);
148        if o3 == o1 {
149            return start;
150        }
151        loop {
152            let next = self.get_before(o3);
153            if next == start {
154                // the time at o3 == the time at o1, so ...
155                o1 = o3;
156                o3 = self.estimate(time, t1, o1, t2, o2);
157                if o3 == o1 {
158                    return start;
159                }
160            } else {
161                if let Some((t, _)) = next.time() {
162                    if t == time {
163                        return next;
164                    } else if t < time {
165                        return self.narrow_in(time, next, end);
166                    } else {
167                        return self.narrow_in(time, start, next);
168                    }
169                } else {
170                    unreachable!()
171                }
172            }
173        }
174    }
175    // given a time and two line and time offsets that bracket it, estimate the line
176    // offset to find the time at
177    fn estimate(
178        &self,
179        time: &NaiveDateTime,
180        t1: &NaiveDateTime,
181        o1: usize,
182        t2: &NaiveDateTime,
183        o2: usize,
184    ) -> usize {
185        let line_delta = o2 - o1;
186        match line_delta {
187            1 => o1,
188            2 => o1 + 1,
189            _ => {
190                if line_delta <= 16 {
191                    // this is an arbitrary threshold that could be optimized
192                    // switch to binary search
193                    return o1 + line_delta / 2;
194                }
195                let time_delta = t2.timestamp() - t1.timestamp();
196                let lines_per_second = (line_delta as f64) / (time_delta as f64);
197                let seconds = (time.timestamp() - t1.timestamp()) as f64;
198                let additional_lines = (lines_per_second * seconds) as usize;
199                // we've already looked at the end offsets, so make sure we don't hit those again
200                let additional_lines = if additional_lines == 0 {
201                    1
202                } else if additional_lines == line_delta {
203                    line_delta - 1
204                } else {
205                    additional_lines
206                };
207                o1 + additional_lines
208            }
209        }
210    }
211    // get an index-item pair at or before the given time starting at the given index
212    // this moves forward from earlier lines to later
213    fn get_after(&mut self, i: usize) -> Option<Item> {
214        for i in i..self.larry.len() {
215            let item = parse_line(self.larry.get(i).unwrap(), i);
216            let t = item.time();
217            if let Some((_, _)) = t {
218                return Some(item);
219            }
220        }
221        None
222    }
223    // just returns iterator from a given offset forward -- needed for validation
224    pub fn items_before(&mut self, offset: usize) -> ItemsBefore {
225        ItemsBefore::new(offset, self)
226    }
227    // get the first index-item pair at
228    // this moves in reverse from later lines to earlier
229    fn get_before(&mut self, i: usize) -> Item {
230        let mut i = i;
231        if i >= self.larry.len() {
232            i = self.larry.len() - 1;
233        }
234        loop {
235            let item = parse_line(self.larry.get(i).unwrap(), i);
236            match item {
237                Item::Done(_, _) | Item::Note(_, _) | Item::Event(_, _) => return item,
238                _ => (),
239            }
240            if i == 0 {
241                break;
242            }
243            i -= 1;
244        }
245        unreachable!()
246    }
247    // starting at the location of item, advance the pointer to the first item in the log with item's time
248    // most often timestamps will be unique, but we do this just in case
249    fn advance_to_first(&mut self, item: Item) -> Item {
250        let (time, mut i) = item.time().unwrap();
251        let mut ptr = item.clone();
252        while i > 0 {
253            i -= 1;
254            let next = parse_line(self.larry.get(i).unwrap(), i);
255            let next_time = next.time();
256            if let Some((next_time, _)) = next_time {
257                if time == next_time {
258                    ptr = next;
259                } else if time > next_time {
260                    return ptr;
261                }
262            }
263        }
264        ptr
265    }
266    pub fn events_from_the_end(&mut self) -> EventsBefore {
267        EventsBefore::new(self.larry.len(), self)
268    }
269    pub fn notes_from_the_end(&mut self) -> NotesBefore {
270        NotesBefore::new(self.larry.len(), self)
271    }
272    pub fn events_from_the_beginning(self) -> EventsAfter {
273        EventsAfter::new(0, &self)
274    }
275    pub fn notes_from_the_beginning(self) -> NotesAfter {
276        NotesAfter::new(0, &self)
277    }
278    pub fn events_in_range(&mut self, start: &NaiveDateTime, end: &NaiveDateTime) -> Vec<Event> {
279        let mut ret = vec![];
280        if let Some(item) = self.find_line(start) {
281            for e in EventsAfter::new(item.offset(), self) {
282                if &e.start < end {
283                    ret.push(e);
284                } else {
285                    break;
286                }
287            }
288        }
289        ret
290    }
291    pub fn tagable_items_in_range(
292        &mut self,
293        start: &NaiveDateTime,
294        end: &NaiveDateTime,
295    ) -> Vec<Item> {
296        let mut ret = vec![];
297        if let Some(item) = self.find_line(start) {
298            for i in ItemsAfter::new(item.offset(), &self.path) {
299                match &i {
300                    Item::Note(n, _) => {
301                        if &n.time > end {
302                            break;
303                        } else {
304                            ret.push(i);
305                        }
306                    }
307                    Item::Event(e, _) => {
308                        if &e.start > end {
309                            break;
310                        } else {
311                            ret.push(i);
312                        }
313                    }
314                    _ => (),
315                }
316            }
317        }
318        ret
319    }
320    pub fn notes_in_range(&mut self, start: &NaiveDateTime, end: &NaiveDateTime) -> Vec<Note> {
321        let mut ret = vec![];
322        if let Some(item) = self.find_line(start) {
323            let mut at_first = true;
324            for n in NotesAfter::new(item.offset(), self) {
325                if at_first && &n.time < start {
326                    at_first = false;
327                    continue;
328                } else {
329                    at_first = false
330                }
331                if &n.time < end {
332                    ret.push(n);
333                } else {
334                    break;
335                }
336            }
337        }
338        ret
339    }
340    pub fn last_event(&mut self) -> Option<Event> {
341        // because Larry caches the line, re-acquiring the last event is cheap
342        self.events_from_the_end().find(|_| true)
343    }
344    pub fn forgot_to_end_last_event(&mut self) -> bool {
345        if let Some(event) = self.last_event() {
346            if event.ongoing() {
347                let now = Local::now().naive_local();
348                event.start.date() != now.date()
349            } else {
350                false
351            }
352        } else {
353            false
354        }
355    }
356    fn needs_newline(&mut self) -> bool {
357        if self.larry.len() > 0 {
358            let last_line = self
359                .larry
360                .get(self.larry.len() - 1)
361                .expect("could not obtain last line of log");
362            let last_char = last_line.bytes().last().unwrap();
363            !(last_char == 0x0D || last_char == 0x0A)
364        } else {
365            false
366        }
367    }
368    // this method devours the reader because it invalidates the information cached in larry
369    pub fn append_event(&mut self, description: String, tags: Vec<String>) -> (Event, usize) {
370        let event = Event::coin(description, tags);
371        self.append_to_log(event, "could not append event to log")
372    }
373    // this method devours the reader because it invalidates the information cached in larry
374    pub fn append_note(&mut self, description: String, tags: Vec<String>) -> (Note, usize) {
375        let note = Note::coin(description, tags);
376        self.append_to_log(note, "could not append note to log")
377    }
378    pub fn close_event(&mut self) -> (Done, usize) {
379        let done = Done(Local::now().naive_local());
380        self.append_to_log(done, "could not append DONE line to log")
381    }
382    pub fn append_to_log<T: LogLine>(&mut self, item: T, error_message: &str) -> (T, usize) {
383        let mut log = OpenOptions::new()
384            .write(true)
385            .append(true)
386            .open(&self.path)
387            .unwrap();
388        if self.needs_newline() {
389            writeln!(log, "").expect("could not append to log file");
390        }
391        let now = Local::today().naive_local();
392        if let Some(ts) = self.last_timestamp() {
393            if ts.date() != now {
394                writeln!(log, "# {}/{}/{}", now.year(), now.month(), now.day())
395                    .expect("could not append date comment to log");
396            }
397        } else {
398            writeln!(log, "# {}/{}/{}", now.year(), now.month(), now.day())
399                .expect("could not append date comment to log");
400        }
401        writeln!(log, "{}", &item.to_line()).expect(error_message);
402        (item, self.larry.len())
403    }
404    // iterator over all items, first to last
405    pub fn items(&self) -> ItemsAfter {
406        ItemsAfter::new(0, &self.path)
407    }
408}
409
410pub struct ItemsBefore<'a> {
411    offset: Option<usize>,
412    larry: &'a mut Larry,
413}
414
415impl<'a> ItemsBefore<'a> {
416    fn new(offset: usize, reader: &mut LogController) -> ItemsBefore {
417        ItemsBefore {
418            offset: if offset == 0 { None } else { Some(offset) },
419            larry: &mut reader.larry,
420        }
421    }
422}
423
424impl<'a> Iterator for ItemsBefore<'a> {
425    type Item = Item;
426    fn next(&mut self) -> Option<Item> {
427        if let Some(o) = self.offset {
428            let o2 = o - 1;
429            let line = self.larry.get(o2).unwrap();
430            let item = parse_line(line, o);
431            self.offset = if o2 > 0 { Some(o2) } else { None };
432            Some(item)
433        } else {
434            None
435        }
436    }
437}
438
439pub struct ItemsAfter {
440    offset: usize,
441    bufreader: Lines<BufReader<File>>,
442}
443
444impl ItemsAfter {
445    pub fn new(offset: usize, path: &str) -> ItemsAfter {
446        let mut bufreader =
447            BufReader::new(File::open(path).expect("could not open log file")).lines();
448        for _ in 0..offset {
449            bufreader.next();
450        }
451        ItemsAfter { offset, bufreader }
452    }
453}
454
455impl Iterator for ItemsAfter {
456    type Item = Item;
457    fn next(&mut self) -> Option<Item> {
458        if let Some(res) = self.bufreader.next() {
459            let line = res.expect("could not read log line");
460            let item = parse_line(&line, self.offset);
461            self.offset += 1;
462            Some(item)
463        } else {
464            None
465        }
466    }
467}
468
469pub struct NotesBefore<'a> {
470    item_iterator: ItemsBefore<'a>,
471}
472
473impl<'a> NotesBefore<'a> {
474    fn new(offset: usize, reader: &mut LogController) -> NotesBefore {
475        NotesBefore {
476            item_iterator: ItemsBefore::new(offset, reader),
477        }
478    }
479}
480
481impl<'a> Iterator for NotesBefore<'a> {
482    type Item = Note;
483    fn next(&mut self) -> Option<Note> {
484        loop {
485            let item = self.item_iterator.next();
486            if let Some(item) = item {
487                match item {
488                    Item::Note(n, _) => return Some(n),
489                    _ => (),
490                }
491            } else {
492                return None;
493            }
494        }
495    }
496}
497
498pub struct NotesAfter {
499    item_iterator: ItemsAfter,
500}
501
502impl NotesAfter {
503    fn new(offset: usize, reader: &LogController) -> NotesAfter {
504        NotesAfter {
505            item_iterator: ItemsAfter::new(offset, &reader.path),
506        }
507    }
508}
509
510impl Iterator for NotesAfter {
511    type Item = Note;
512    fn next(&mut self) -> Option<Note> {
513        loop {
514            let item = self.item_iterator.next();
515            if let Some(item) = item {
516                match item {
517                    Item::Note(n, _) => return Some(n),
518                    _ => (),
519                }
520            } else {
521                return None;
522            }
523        }
524    }
525}
526
527pub struct EventsBefore<'a> {
528    last_time: Option<NaiveDateTime>,
529    item_iterator: ItemsBefore<'a>,
530}
531
532impl<'a> EventsBefore<'a> {
533    fn new(offset: usize, reader: &mut LogController) -> EventsBefore {
534        // the last event may be underway at the offset, so find out when it ends
535        let items_after = ItemsAfter::new(offset, &reader.path);
536        let timed_item = items_after
537            .filter(|i| match i {
538                Item::Event(_, _) | Item::Done(_, _) => true,
539                _ => false,
540            })
541            .find(|i| i.time().is_some());
542        let last_time = if let Some(i) = timed_item {
543            Some(i.time().unwrap().0.to_owned())
544        } else {
545            None
546        };
547        EventsBefore {
548            last_time,
549            item_iterator: ItemsBefore::new(offset, reader),
550        }
551    }
552}
553
554impl<'a> Iterator for EventsBefore<'a> {
555    type Item = Event;
556    fn next(&mut self) -> Option<Event> {
557        let mut last_time = self.last_time;
558        let mut event: Option<Event> = None;
559        loop {
560            if let Some(i) = self.item_iterator.next() {
561                match i {
562                    Item::Event(e, _) => {
563                        event = Some(e.bounded_time(last_time));
564                        break;
565                    }
566                    Item::Done(d, _) => {
567                        last_time = Some(d.0);
568                    }
569                    _ => (),
570                }
571            } else {
572                break;
573            }
574        }
575        self.last_time = if event.is_some() {
576            Some(event.as_ref().unwrap().start.clone())
577        } else {
578            last_time
579        };
580        event
581    }
582}
583
584pub struct EventsAfter {
585    next_item: Option<Event>,
586    item_iterator: ItemsAfter,
587}
588
589impl EventsAfter {
590    fn new(offset: usize, reader: &LogController) -> EventsAfter {
591        EventsAfter {
592            next_item: None,
593            item_iterator: ItemsAfter::new(offset, &reader.path),
594        }
595    }
596    fn get_end_time(&mut self) -> Option<NaiveDateTime> {
597        self.next_item = None;
598        loop {
599            if let Some(i) = self.item_iterator.next() {
600                match i {
601                    Item::Event(e, _) => {
602                        let time = e.start.clone();
603                        self.next_item = Some(e);
604                        return Some(time);
605                    }
606                    Item::Done(d, _) => return Some(d.0),
607                    _ => (),
608                }
609            } else {
610                return None;
611            }
612        }
613    }
614}
615
616impl Iterator for EventsAfter {
617    type Item = Event;
618    fn next(&mut self) -> Option<Event> {
619        if let Some(event) = &self.next_item {
620            return Some(event.clone().bounded_time(self.get_end_time()));
621        }
622        loop {
623            if let Some(i) = self.item_iterator.next() {
624                match i {
625                    Item::Event(e, _) => return Some(e.bounded_time(self.get_end_time())),
626                    _ => (),
627                }
628            } else {
629                return None;
630            }
631        }
632    }
633}
634
635#[cfg(test)]
636mod tests {
637    use super::*;
638    use chrono::Duration;
639    use rand::seq::SliceRandom;
640    use rand::{thread_rng, Rng};
641    use std::fs::File;
642    use std::io::LineWriter;
643    use std::ops::AddAssign;
644    use std::str::FromStr;
645
646    enum Need {
647        E,
648        N,
649        B,
650        C,
651        Error,
652    }
653
654    fn random_tag() -> String {
655        let choices = ["foo", "bar", "baz", "plugh", "work", "play", "tedium"];
656        choices[rand::thread_rng().gen_range(0, choices.len())].to_owned()
657    }
658
659    fn random_words(min: usize, max: usize) -> Vec<String> {
660        (0..(rand::thread_rng().gen_range(min, max + 1)))
661            .map(|_| random_tag())
662            .collect()
663    }
664
665    fn random_tags() -> Vec<String> {
666        let mut tags = random_words(0, 5);
667        tags.sort_unstable();
668        tags.dedup();
669        tags
670    }
671
672    fn random_text() -> String {
673        let mut words = random_words(5, 15);
674        let mut word = words.remove(0);
675        for w in words {
676            word += " ";
677            word.push_str(&w);
678        }
679        word
680    }
681
682    fn random_line(
683        time: &mut NaiveDateTime,
684        open_event: bool,
685        offset: usize,
686        need: Option<Need>,
687    ) -> Item {
688        let n = rand::thread_rng().gen_range(0, 100);
689        let need = if let Some(need) = need {
690            need
691        } else {
692            if n < 4 {
693                Need::B
694            } else if n < 10 {
695                Need::C
696            } else if n < 11 {
697                Need::Error
698            } else if n < 20 {
699                Need::N
700            } else {
701                Need::E
702            }
703        };
704        match need {
705            Need::B => Item::Blank(offset),
706            Need::C => {
707                let mut comment = String::from("# ");
708                comment.push_str(&random_text());
709                Item::Comment(offset)
710            }
711            Need::Error => Item::Error(random_text(), offset),
712            Need::N => {
713                time.add_assign(Duration::seconds(rand::thread_rng().gen_range(1, 1000)));
714                Item::Note(
715                    Note {
716                        time: time.clone(),
717                        description: random_text(),
718                        tags: random_tags(),
719                    },
720                    offset,
721                )
722            }
723            Need::E => {
724                time.add_assign(Duration::seconds(rand::thread_rng().gen_range(1, 1000)));
725                if open_event && n < 30 {
726                    Item::Done(Done(time.clone()), offset)
727                } else {
728                    Item::Event(
729                        Event {
730                            start: time.clone(),
731                            start_overlap: false,
732                            end: None,
733                            end_overlap: false,
734                            tags: random_tags(),
735                            description: random_text(),
736                            vacation: false,
737                            vacation_type: None,
738                        },
739                        offset,
740                    )
741                }
742            }
743        }
744    }
745
746    // the need is a set of things you need at least one of in the log
747    fn random_log(length: usize, need: Vec<Need>, disambiguator: &str) -> (Vec<Item>, String) {
748        let mut initial_time = NaiveDate::from_ymd(2019, 12, 22).and_hms(9, 39, 30);
749        let mut items: Vec<Item> = Vec::with_capacity(length);
750        let mut open_event = false;
751        // tests are run in parallel, so we need to prevent collisions, but it's nice to
752        // have the files handy to look at in case of failure
753        // this technique seems to suffice
754        let path = format!(
755            "{}-{}-{}.log",
756            disambiguator,
757            length,
758            Local::now().naive_local().timestamp_millis()
759        );
760        let file = File::create(path.clone()).unwrap();
761        let mut file = LineWriter::new(file);
762        let mut need: Vec<(usize, Need)> = if need.is_empty() {
763            vec![]
764        } else {
765            // randomly assign needs to lines
766            let mut indices: Vec<usize> = (0..length).collect();
767            indices.shuffle(&mut thread_rng());
768            let mut need = need;
769            need.shuffle(&mut thread_rng());
770            let mut need = need
771                .into_iter()
772                .map(|n| (indices.remove(0), n))
773                .collect::<Vec<_>>();
774            need.sort_unstable_by(|(a, _), (b, _)| a.cmp(b));
775            need
776        };
777        for offset in 0..length {
778            let t = if let Some((i, _)) = need.get(0) {
779                if i == &offset {
780                    let t = need.remove(0).1;
781                    Some(t)
782                } else {
783                    None
784                }
785            } else {
786                None
787            };
788            let item = random_line(&mut initial_time, open_event, offset, t);
789            open_event = match item {
790                Item::Done(_, _) => false,
791                Item::Event(_, _) => true,
792                _ => open_event,
793            };
794            let line = match &item {
795                Item::Event(e, _) => e.to_line(),
796                Item::Note(n, _) => n.to_line(),
797                Item::Done(d, _) => d.to_line(),
798                Item::Blank(_) => String::new(),
799                Item::Comment(_) => {
800                    let mut s = String::from("# ");
801                    s.push_str(&random_text());
802                    s
803                }
804                Item::Error(s, _) => s.clone(),
805            };
806            file.write_all(line.as_ref()).unwrap();
807            file.write_all("\n".as_ref()).unwrap();
808            if item.has_time() {
809                items.push(item);
810            }
811        }
812        (items, path)
813    }
814
815    fn closed_events(mut items: Vec<Item>) -> Vec<Event> {
816        items.reverse();
817        let mut ret = Vec::with_capacity(items.len());
818        let mut last_time: Option<NaiveDateTime> = None;
819        for i in items.iter() {
820            match i {
821                Item::Done(Done(t), _) => last_time = Some(t.clone()),
822                Item::Event(e, _) => {
823                    let mut e = e.clone();
824                    if last_time.is_some() {
825                        e.end = last_time;
826                    }
827                    last_time = Some(e.start.clone());
828                    ret.push(e);
829                }
830                _ => (),
831            }
832        }
833        ret.reverse();
834        ret
835    }
836
837    fn notes(items: Vec<Item>) -> Vec<Note> {
838        let mut ret = Vec::with_capacity(items.len());
839        for i in items.iter() {
840            match i {
841                Item::Note(n, _) => {
842                    ret.push(n.clone());
843                }
844                _ => (),
845            }
846        }
847        ret
848    }
849
850    fn test_configuration(path: &str) -> (String, Configuration) {
851        let conf_path = format!("{}_conf", path);
852        File::create(
853            PathBuf::from_str(&conf_path)
854                .expect(&format!("could not create path {}", conf_path))
855                .as_path(),
856        )
857        .expect(&format!("could not create file {}", conf_path));
858        let pb = PathBuf::from_str(&conf_path)
859            .expect(&format!("could not form path from {}", conf_path));
860        let conf = Configuration::read(Some(pb), None);
861        (conf_path, conf)
862    }
863
864    fn cleanup(paths: &[&str]) {
865        for p in paths {
866            let pb = PathBuf::from_str(p).expect(&format!("cannot form a path from {}", p));
867            if pb.as_path().exists() {
868                std::fs::remove_file(p).expect(&format!("failed to remove {}", p))
869            }
870        }
871    }
872
873    #[test]
874    fn test_notes_in_range() {
875        let (items, path) = random_log(100, vec![Need::N, Need::N], "test_notes_in_range");
876        let notes = notes(items);
877        assert!(notes.len() > 1, "found more than one note");
878        let (conf_path, conf) = test_configuration("test_notes_in_range");
879        let mut log_reader =
880            LogController::new(Some(PathBuf::from_str(&path).unwrap()), &conf).unwrap();
881        for i in 0..notes.len() - 1 {
882            for j in i..notes.len() {
883                let found_notes = log_reader.notes_in_range(&notes[i].time, &notes[j].time);
884                assert!(
885                    j - i == found_notes.len(),
886                    "found as many events as expected"
887                );
888                for offset in 0..found_notes.len() {
889                    let k = i + offset;
890                    assert_eq!(notes[k].time, found_notes[offset].time, "same time");
891                    assert_eq!(notes[k].tags, found_notes[offset].tags, "same tags");
892                    assert_eq!(
893                        notes[k].description, found_notes[offset].description,
894                        "same description"
895                    );
896                }
897            }
898        }
899        cleanup(&[&path, &conf_path]);
900    }
901
902    #[test]
903    fn test_events_in_range() {
904        let (items, path) = random_log(20, vec![Need::E, Need::E], "test_events_in_range");
905        let events = closed_events(items);
906        assert!(events.len() > 1, "found more than one event");
907        let (conf_path, conf) = test_configuration("test_events_in_range");
908        let mut log_reader =
909            LogController::new(Some(PathBuf::from_str(&path).unwrap()), &conf).unwrap();
910        for i in 0..events.len() - 1 {
911            for j in i..events.len() {
912                let found_events = log_reader.events_in_range(&events[i].start, &events[j].start);
913                assert!(
914                    j - i <= found_events.len(),
915                    "found at least as many events as expected"
916                );
917                for offset in 0..found_events.len() {
918                    let k = i + offset;
919                    assert_eq!(events[k].start, found_events[offset].start, "same start");
920                    assert_eq!(events[k].end, found_events[offset].end, "same end");
921                    assert_eq!(events[k].tags, found_events[offset].tags, "same tags");
922                    assert_eq!(
923                        events[k].description, found_events[offset].description,
924                        "same description"
925                    );
926                }
927            }
928        }
929        cleanup(&[&path, &conf_path]);
930    }
931
932    #[test]
933    fn test_notes_from_end() {
934        let (items, path) = random_log(100, vec![Need::N], "test_notes_from_end");
935        let mut notes = notes(items);
936        notes.reverse();
937        let (conf_path, conf) = test_configuration("test_notes_from_end");
938        let mut log_reader =
939            LogController::new(Some(PathBuf::from_str(&path).unwrap()), &conf).unwrap();
940        let found_notes = log_reader.notes_from_the_end().collect::<Vec<_>>();
941        assert_eq!(
942            notes.len(),
943            found_notes.len(),
944            "found the right number of notes"
945        );
946        for (i, e) in notes.iter().enumerate() {
947            assert_eq!(e.time, found_notes[i].time, "they occur at the same time");
948            assert_eq!(e.tags, found_notes[i].tags, "they have the same tags");
949            assert_eq!(
950                e.description, found_notes[i].description,
951                "they have the same text"
952            );
953        }
954        cleanup(&[&path, &conf_path]);
955    }
956
957    #[test]
958    fn test_notes_from_beginning() {
959        let (items, path) = random_log(103, vec![Need::N], "test_notes_from_beginning");
960        let notes = notes(items);
961        let (conf_path, conf) = test_configuration("test_notes_from_beginning");
962        let log_reader =
963            LogController::new(Some(PathBuf::from_str(&path).unwrap()), &conf).unwrap();
964        let found_notes = log_reader.notes_from_the_beginning().collect::<Vec<_>>();
965        assert_eq!(
966            notes.len(),
967            found_notes.len(),
968            "found the right number of notes"
969        );
970        for (i, n) in notes.iter().enumerate() {
971            assert_eq!(n.time, found_notes[i].time, "they occur at the same time");
972            assert_eq!(n.tags, found_notes[i].tags, "they have the same tags");
973            assert_eq!(
974                n.description, found_notes[i].description,
975                "they have the same text"
976            );
977        }
978        cleanup(&[&path, &conf_path]);
979    }
980
981    #[test]
982    fn test_events_from_end() {
983        let (items, path) = random_log(107, vec![Need::E], "test_events_from_end");
984        let mut events = closed_events(items);
985        events.reverse();
986        let (conf_path, conf) = test_configuration("test_events_from_end");
987        let mut log_reader =
988            LogController::new(Some(PathBuf::from_str(&path).unwrap()), &conf).unwrap();
989        let found_events = log_reader.events_from_the_end().collect::<Vec<_>>();
990        assert_eq!(
991            events.len(),
992            found_events.len(),
993            "found the right number of events"
994        );
995        for (i, e) in events.iter().enumerate() {
996            assert_eq!(
997                e.start, found_events[i].start,
998                "they start at the same time"
999            );
1000            assert_eq!(e.end, found_events[i].end, "they end at the same time");
1001            assert_eq!(e.tags, found_events[i].tags, "they have the same tags");
1002            assert_eq!(
1003                e.description, found_events[i].description,
1004                "they have the same description"
1005            );
1006        }
1007        cleanup(&[&path, &conf_path]);
1008    }
1009
1010    #[test]
1011    fn test_events_from_beginning() {
1012        let (items, path) = random_log(100, vec![Need::E], "test_events_from_beginning");
1013        let events = closed_events(items);
1014        let (conf_path, conf) = test_configuration("test_events_from_beginning");
1015        let log_reader =
1016            LogController::new(Some(PathBuf::from_str(&path).unwrap()), &conf).unwrap();
1017        let found_events = log_reader.events_from_the_beginning().collect::<Vec<_>>();
1018        assert_eq!(
1019            events.len(),
1020            found_events.len(),
1021            "found the right number of events"
1022        );
1023        for (i, e) in events.iter().enumerate() {
1024            assert_eq!(
1025                e.start, found_events[i].start,
1026                "they start at the same time"
1027            );
1028            assert_eq!(e.end, found_events[i].end, "they end at the same time");
1029            assert_eq!(e.tags, found_events[i].tags, "they have the same tags");
1030            assert_eq!(
1031                e.description, found_events[i].description,
1032                "they have the same description"
1033            );
1034        }
1035        cleanup(&[&path, &conf_path]);
1036    }
1037
1038    fn test_log(length: usize, disambiguator: &str) {
1039        let (items, path) = random_log(length, vec![], disambiguator);
1040        if items.is_empty() {
1041            println!("empty file; skipping...");
1042        } else {
1043            let (conf_path, conf) = test_configuration(&path);
1044            let mut log_reader =
1045                LogController::new(Some(PathBuf::from_str(&path).unwrap()), &conf).unwrap();
1046            let mut last_timed_item: Option<Item> = None;
1047            for item in items {
1048                let (time, offset) = item.time().unwrap();
1049                let found_item = log_reader.find_line(time);
1050                if let Some(found_item) = found_item {
1051                    assert_eq!(offset, found_item.offset());
1052                    if let Some(lti) = last_timed_item.clone() {
1053                        let (t1, _) = lti.time().unwrap();
1054                        let (t2, _) = found_item.time().unwrap();
1055                        let d = *t2 - *t1;
1056                        if d.num_seconds() > 1 {
1057                            let intermediate_time = t1
1058                                .checked_add_signed(Duration::seconds(d.num_seconds() / 2))
1059                                .unwrap();
1060                            let should_be_found = log_reader.find_line(&intermediate_time);
1061                            if let Some(should_be_found) = should_be_found {
1062                                assert_eq!(last_timed_item.unwrap(), should_be_found);
1063                            } else {
1064                                assert!(false, "failed to revert to found time when looking for missing intermediate time {}", intermediate_time);
1065                            }
1066                        }
1067                    }
1068                    last_timed_item = Some(found_item);
1069                } else {
1070                    assert!(false, "could not find item at offset {}", offset);
1071                }
1072                cleanup(&[&conf_path]);
1073            }
1074        }
1075        cleanup(&[&path]);
1076    }
1077
1078    #[test]
1079    fn test_empty_file() {
1080        test_log(0, "test_empty_file");
1081    }
1082
1083    #[test]
1084    fn test_100_tiny_files() {
1085        for i in 0..100 {
1086            test_log(5, &format!("test_100_tiny_files_{}", i));
1087        }
1088    }
1089
1090    #[test]
1091    fn test_10_small_files() {
1092        for i in 0..10 {
1093            test_log(100, &format!("test_10_small_files_{}", i));
1094        }
1095    }
1096
1097    #[test]
1098    fn test_large_file() {
1099        test_log(10000, "test_large_file");
1100    }
1101
1102    #[test]
1103    fn test_event() {
1104        match parse_line("2019 12 1 16 3 30::an event with no tags", 0) {
1105            Item::Event(
1106                Event {
1107                    start,
1108                    tags,
1109                    description,
1110                    ..
1111                },
1112                _,
1113            ) => {
1114                assert_eq!(2019, start.year());
1115                assert_eq!(12, start.month());
1116                assert_eq!(1, start.day());
1117                assert_eq!(16, start.hour());
1118                assert_eq!(3, start.minute());
1119                assert_eq!(30, start.second());
1120                assert!(tags.is_empty(), "there are no tags");
1121                assert_eq!(
1122                    "an event with no tags", &description,
1123                    "got correct description"
1124                )
1125            }
1126            _ => assert!(false, "failed to parse an event line"),
1127        };
1128        match parse_line("2019 12 1 16 3 30:foo bar:an event with some tags", 0) {
1129            Item::Event(
1130                Event {
1131                    start,
1132                    tags,
1133                    description,
1134                    ..
1135                },
1136                _,
1137            ) => {
1138                assert_eq!(2019, start.year());
1139                assert_eq!(12, start.month());
1140                assert_eq!(1, start.day());
1141                assert_eq!(16, start.hour());
1142                assert_eq!(3, start.minute());
1143                assert_eq!(30, start.second());
1144                assert_eq!(2, tags.len(), "there are some tags");
1145                for t in vec!["foo", "bar"] {
1146                    assert!(tags.contains(&t.to_owned()));
1147                }
1148                assert_eq!(
1149                    "an event with some tags", &description,
1150                    "got correct description"
1151                )
1152            }
1153            _ => assert!(false, "failed to parse an event line"),
1154        };
1155        // can parse tags with spaces
1156        match parse_line("2019 12 1 16 3 30:foo\\ bar:an event with some tags", 0) {
1157            Item::Event(
1158                Event {
1159                    start,
1160                    tags,
1161                    description,
1162                    ..
1163                },
1164                _,
1165            ) => {
1166                assert_eq!(2019, start.year());
1167                assert_eq!(12, start.month());
1168                assert_eq!(1, start.day());
1169                assert_eq!(16, start.hour());
1170                assert_eq!(3, start.minute());
1171                assert_eq!(30, start.second());
1172                assert_eq!(1, tags.len(), "there are some tags");
1173                for t in vec!["foo bar"] {
1174                    assert!(tags.contains(&t.to_owned()));
1175                }
1176                assert_eq!(
1177                    "an event with some tags", &description,
1178                    "got correct description"
1179                )
1180            }
1181            _ => assert!(false, "failed to parse an event line"),
1182        };
1183        //regression?
1184        match parse_line("2019 12 22 12 49 24:foo:plugh baz baz foo play play work baz tedium foo tedium foo work bar", 0) {
1185            Item::Event(
1186                Event {
1187                    start,
1188                    tags,
1189                    description,
1190                    ..
1191                },
1192                _,
1193            ) => {
1194                assert_eq!(2019, start.year());
1195                assert_eq!(12, start.month());
1196                assert_eq!(22, start.day());
1197                assert_eq!(12, start.hour());
1198                assert_eq!(49, start.minute());
1199                assert_eq!(24, start.second());
1200                assert_eq!(1, tags.len(), "there are some tags");
1201                for t in vec!["foo"] {
1202                    assert!(tags.contains(&t.to_owned()));
1203                }
1204                assert_eq!(
1205                    "plugh baz baz foo play play work baz tedium foo tedium foo work bar", &description,
1206                    "got correct description"
1207                )
1208            }
1209            _ => assert!(false, "failed to parse an event line"),
1210        };
1211    }
1212
1213    #[test]
1214    fn test_note() {
1215        match parse_line("2019 12 1 16 3 30<NOTE>:a note with no tags", 0) {
1216            Item::Note(
1217                Note {
1218                    time,
1219                    tags,
1220                    description,
1221                },
1222                _,
1223            ) => {
1224                assert_eq!(2019, time.year());
1225                assert_eq!(12, time.month());
1226                assert_eq!(1, time.day());
1227                assert_eq!(16, time.hour());
1228                assert_eq!(3, time.minute());
1229                assert_eq!(30, time.second());
1230                assert!(tags.is_empty(), "there are no tags");
1231                assert_eq!(
1232                    "a note with no tags", &description,
1233                    "got correct description"
1234                )
1235            }
1236            _ => assert!(false, "failed to parse a NOTE line"),
1237        };
1238        match parse_line("2019 12 1 16 3 30<NOTE>foo bar:a short note", 0) {
1239            Item::Note(
1240                Note {
1241                    time,
1242                    tags,
1243                    description,
1244                },
1245                _,
1246            ) => {
1247                assert_eq!(2019, time.year());
1248                assert_eq!(12, time.month());
1249                assert_eq!(1, time.day());
1250                assert_eq!(16, time.hour());
1251                assert_eq!(3, time.minute());
1252                assert_eq!(30, time.second());
1253                assert_eq!(tags.len(), 2, "there are two tags");
1254                for t in vec!["foo", "bar"] {
1255                    assert!(tags.contains(&t.to_owned()));
1256                }
1257                assert_eq!("a short note", &description, "got correct description")
1258            }
1259            _ => assert!(false, "failed to parse a NOTE line"),
1260        };
1261        match parse_line(
1262            r"2019 12 1 16 3 30<NOTE>f\:oo b\<ar b\ az pl\\ugh:a short note",
1263            0,
1264        ) {
1265            Item::Note(
1266                Note {
1267                    time,
1268                    tags,
1269                    description,
1270                },
1271                _,
1272            ) => {
1273                assert_eq!(2019, time.year());
1274                assert_eq!(12, time.month());
1275                assert_eq!(1, time.day());
1276                assert_eq!(16, time.hour());
1277                assert_eq!(3, time.minute());
1278                assert_eq!(30, time.second());
1279                assert_eq!(tags.len(), 4, "there are two tags");
1280                for t in vec!["f:oo", "b<ar", "b az", r"pl\ugh"] {
1281                    assert!(tags.contains(&t.to_owned()), "escaping worked");
1282                }
1283                assert_eq!("a short note", &description, "got correct description")
1284            }
1285            _ => assert!(false, "failed to parse a NOTE line"),
1286        };
1287        match parse_line("2019 12 1 16 3 30<NOTE>foo bar bar:a short note", 0) {
1288            Item::Note(
1289                Note {
1290                    time,
1291                    tags,
1292                    description,
1293                },
1294                _,
1295            ) => {
1296                assert_eq!(2019, time.year());
1297                assert_eq!(12, time.month());
1298                assert_eq!(1, time.day());
1299                assert_eq!(16, time.hour());
1300                assert_eq!(3, time.minute());
1301                assert_eq!(30, time.second());
1302                assert_eq!(tags.len(), 2, "there are two tags");
1303                for t in vec!["foo", "bar"] {
1304                    assert!(tags.contains(&t.to_owned()));
1305                }
1306                assert_eq!("a short note", &description, "got correct description")
1307            }
1308            _ => assert!(false, "failed to parse a NOTE line"),
1309        };
1310        match parse_line("2019 12 1 16 3 30<NOTE> foo  bar :a short note", 0) {
1311            Item::Note(
1312                Note {
1313                    time,
1314                    tags,
1315                    description,
1316                },
1317                _,
1318            ) => {
1319                assert_eq!(2019, time.year());
1320                assert_eq!(12, time.month());
1321                assert_eq!(1, time.day());
1322                assert_eq!(16, time.hour());
1323                assert_eq!(3, time.minute());
1324                assert_eq!(30, time.second());
1325                assert_eq!(tags.len(), 2, "there are two tags");
1326                for t in vec!["foo", "bar"] {
1327                    assert!(tags.contains(&t.to_owned()));
1328                }
1329                assert_eq!("a short note", &description, "got correct description")
1330            }
1331            _ => assert!(false, "failed to parse a NOTE line"),
1332        };
1333        //regression
1334        match parse_line("2019 12 22  9 59 34<NOTE>foo play tedium work:baz tedium baz tedium foo plugh bar foo bar play plugh foo baz play baz tedium work work play play bar", 0) {
1335            Item::Note(
1336                Note {
1337                    time,
1338                    tags,
1339                    description,
1340                },
1341                _,
1342            ) => {
1343                assert_eq!(2019, time.year());
1344                assert_eq!(12, time.month());
1345                assert_eq!(22, time.day());
1346                assert_eq!(9, time.hour());
1347                assert_eq!(59, time.minute());
1348                assert_eq!(34, time.second());
1349                assert_eq!(tags.len(), 4, "there are three tags");
1350                for t in vec!["foo", "play", "tedium", "work"] {
1351                    assert!(tags.contains(&t.to_owned()));
1352                }
1353                assert_eq!("baz tedium baz tedium foo plugh bar foo bar play plugh foo baz play baz tedium work work play play bar", &description, "got correct description")
1354            }
1355            _ => assert!(false, "failed to parse a NOTE line"),
1356        };
1357        //regression
1358        match parse_line(
1359            "2019 12 22 12  8  0<NOTE>bar:tedium plugh baz play tedium baz play work",
1360            0,
1361        ) {
1362            Item::Note(
1363                Note {
1364                    time,
1365                    tags,
1366                    description,
1367                },
1368                _,
1369            ) => {
1370                assert_eq!(2019, time.year());
1371                assert_eq!(12, time.month());
1372                assert_eq!(22, time.day());
1373                assert_eq!(12, time.hour());
1374                assert_eq!(8, time.minute());
1375                assert_eq!(0, time.second());
1376                assert_eq!(tags.len(), 1, "there is one tag");
1377                for t in vec!["bar"] {
1378                    assert!(tags.contains(&t.to_owned()));
1379                }
1380                assert_eq!(
1381                    "tedium plugh baz play tedium baz play work", &description,
1382                    "got correct description"
1383                )
1384            }
1385            _ => assert!(false, "failed to parse a NOTE line"),
1386        };
1387    }
1388
1389    #[test]
1390    fn test_done() {
1391        match parse_line("2019 12 1 16 3 30:DONE", 0) {
1392            Item::Done(Done(time), _) => {
1393                assert_eq!(2019, time.year());
1394                assert_eq!(12, time.month());
1395                assert_eq!(1, time.day());
1396                assert_eq!(16, time.hour());
1397                assert_eq!(3, time.minute());
1398                assert_eq!(30, time.second());
1399            }
1400            _ => assert!(false, "failed to parse a DONE line"),
1401        };
1402        match parse_line(" 2019  12   1  16  3  30 :DONE", 0) {
1403            Item::Done(Done(time), _) => {
1404                assert_eq!(2019, time.year(), "space doesn't matter");
1405                assert_eq!(12, time.month(), "space doesn't matter");
1406                assert_eq!(1, time.day(), "space doesn't matter");
1407                assert_eq!(16, time.hour(), "space doesn't matter");
1408                assert_eq!(3, time.minute(), "space doesn't matter");
1409                assert_eq!(30, time.second(), "space doesn't matter");
1410            }
1411            _ => assert!(false, "failed to parse a DONE line"),
1412        };
1413    }
1414
1415    #[test]
1416    fn test_tag_whitespace_handling() {
1417        let e = Event::coin(
1418            String::from("foo"),
1419            vec![String::from("foo bar"), String::from("baz   plugh")],
1420        );
1421        match parse_line(e.to_line().as_str(), 0) {
1422            Item::Event(Event { tags, .. }, _) => {
1423                assert_eq!(2, tags.len());
1424                assert!(tags.contains(&String::from("foo bar")));
1425                assert!(tags.contains(&String::from("baz plugh")));
1426            }
1427            _ => assert!(false, "failed to parse line as an event"),
1428        }
1429    }
1430
1431    #[test]
1432    fn test_zero_padding() {
1433        match parse_line("2019 12 01 16 03 30:DONE", 0) {
1434            Item::Done(Done(time), _) => {
1435                assert_eq!(1, time.day());
1436                assert_eq!(3, time.minute());
1437            }
1438            _ => assert!(false, "failed to parse a DONE line"),
1439        };
1440    }
1441
1442    #[test]
1443    fn test_comment() {
1444        let success = match parse_line("#foo", 0) {
1445            Item::Comment(_) => true,
1446            _ => false,
1447        };
1448        assert!(success, "recognized '#foo' as a comment line");
1449        let success = match parse_line("   #foo", 0) {
1450            Item::Comment(_) => true,
1451            _ => false,
1452        };
1453        assert!(success, "comments can have leading space");
1454    }
1455
1456    #[test]
1457    fn test_error() {
1458        let success = match parse_line("foo", 0) {
1459            Item::Error(_, _) => true,
1460            _ => false,
1461        };
1462        assert!(success, "recognized 'foo' as a malformed log line");
1463    }
1464
1465    #[test]
1466    fn test_blank() {
1467        let success = match parse_line("", 0) {
1468            Item::Blank(_) => true,
1469            _ => false,
1470        };
1471        assert!(success, "recognized an empty line as a blank");
1472        let success = match parse_line("     ", 0) {
1473            Item::Blank(_) => true,
1474            _ => false,
1475        };
1476        assert!(success, "recognized a whitespace line as a blank");
1477    }
1478
1479    #[test]
1480    fn stack_overflow_regression() {
1481        let (items, path) = random_log(23, vec![Need::E, Need::E], "stack_overflow_regression");
1482        let events = closed_events(items);
1483        assert!(events.len() > 1, "found more than one event");
1484        let (conf_path, conf) = test_configuration("stack_overflow_regression");
1485        let mut log_reader =
1486            LogController::new(Some(PathBuf::from_str(&path).unwrap()), &conf).unwrap();
1487        let e = events.first().unwrap();
1488        let false_start = e.start - Duration::days(1);
1489        let found_events = log_reader.events_in_range(&false_start, e.end.as_ref().unwrap());
1490        assert_eq!(1, found_events.len(), "found one event");
1491        assert_eq!(e.start, found_events[0].start, "same start");
1492        assert_eq!(e.end, found_events[0].end, "same end");
1493        assert_eq!(e.tags, found_events[0].tags, "same tags");
1494        assert_eq!(
1495            e.description, found_events[0].description,
1496            "same description"
1497        );
1498        cleanup(&[&path, &conf_path]);
1499    }
1500}
1501
1502// everything you could find in a stream of lines from a log
1503#[derive(Debug, Clone)]
1504pub enum Item {
1505    Event(Event, usize),
1506    Note(Note, usize),
1507    Done(Done, usize),
1508    Blank(usize),
1509    Comment(usize),
1510    Error(String, usize),
1511}
1512
1513impl Item {
1514    fn advance(&self, time: &NaiveDateTime) -> NaiveDateTime {
1515        match self {
1516            Item::Event(e, _) => {
1517                if time < &e.start {
1518                    e.start.clone()
1519                } else {
1520                    time.clone()
1521                }
1522            }
1523            Item::Note(n, _) => {
1524                if time < &n.time {
1525                    n.time.clone()
1526                } else {
1527                    time.clone()
1528                }
1529            }
1530            Item::Done(d, _) => {
1531                if time < &d.0 {
1532                    d.0.clone()
1533                } else {
1534                    time.clone()
1535                }
1536            }
1537            _ => time.clone(),
1538        }
1539    }
1540    pub fn time(&self) -> Option<(&NaiveDateTime, usize)> {
1541        match self {
1542            Item::Event(e, offset) => Some((&e.start, *offset)),
1543            Item::Note(n, offset) => Some((&n.time, *offset)),
1544            Item::Done(d, offset) => Some((&d.0, *offset)),
1545            _ => None,
1546        }
1547    }
1548    pub fn has_time(&self) -> bool {
1549        match self {
1550            Item::Event(_, _) | Item::Note(_, _) | Item::Done(_, _) => true,
1551            _ => false,
1552        }
1553    }
1554    // the line offset of the item
1555    pub fn offset(&self) -> usize {
1556        match self {
1557            Item::Event(_, i) => *i,
1558            Item::Note(_, i) => *i,
1559            Item::Done(_, i) => *i,
1560            Item::Blank(i) => *i,
1561            Item::Comment(i) => *i,
1562            Item::Error(_, i) => *i,
1563        }
1564    }
1565}
1566
1567impl PartialEq for Item {
1568    fn eq(&self, other: &Item) -> bool {
1569        self.offset() == other.offset()
1570    }
1571}
1572
1573impl PartialOrd for Item {
1574    fn partial_cmp(&self, other: &Item) -> Option<std::cmp::Ordering> {
1575        self.offset().partial_cmp(&other.offset())
1576    }
1577}
1578
1579pub fn parse_timestamp(timestamp: &str) -> Result<NaiveDateTime, String> {
1580    lazy_static! {
1581        static ref RE: Regex = Regex::new(r"\d+").unwrap();
1582    }
1583    let numbers: Vec<_> = RE.find_iter(timestamp).map(|m| m.as_str()).collect();
1584    // at this point the log lines grammar ensures all the parsing will be fine
1585    let year = numbers[0].parse::<i32>().unwrap();
1586    let month = numbers[1].parse::<u32>().unwrap();
1587    if month == 0 || month > 12 {
1588        return Err(format!("bad month: {}; must be in the range 1-12", month));
1589    }
1590    let day = numbers[2].parse::<u32>().unwrap();
1591    if day == 0 || day > 31 {
1592        return Err(format!("bad day: {}; day must be in the range 1-31", day));
1593    }
1594    let hour = numbers[3].parse::<u32>().unwrap();
1595    if hour > 23 {
1596        return Err(format!("bad hour: {}; hour must be less than 24", hour));
1597    }
1598    let minute = numbers[4].parse::<u32>().unwrap();
1599    if minute > 59 {
1600        return Err(format!(
1601            "bad minute: {}; minute must be less than 60",
1602            minute
1603        ));
1604    }
1605    let second = numbers[5].parse::<u32>().unwrap();
1606    if second > 59 {
1607        return Err(format!(
1608            "bad second: {}; second must be less than 60",
1609            second
1610        ));
1611    }
1612    match NaiveDate::from_ymd_opt(year, month, day) {
1613        Some(date) => Ok(date.and_hms(hour, minute, second)),
1614        _ => Err(String::from("impossible date")),
1615    }
1616}
1617
1618pub fn timestamp(ts: &NaiveDateTime) -> String {
1619    format!(
1620        "{} {:>2} {:>2} {:>2} {:>2} {:>2}",
1621        ts.year(),
1622        ts.month(),
1623        ts.day(),
1624        ts.hour(),
1625        ts.minute(),
1626        ts.second()
1627    )
1628}
1629
1630// converts a tag string in the log into a deduped, unescaped set of tags
1631pub fn parse_tags(tags: &str) -> Vec<String> {
1632    let mut parsed = vec![];
1633    let mut escaped = false;
1634    let mut current = String::with_capacity(tags.len());
1635    for c in tags.chars() {
1636        if c == '\\' {
1637            if escaped {
1638                current.push(c);
1639            } else {
1640                escaped = true;
1641            }
1642        } else if c == ' ' {
1643            // we expect tags to be normalized at this point so all whitespaces is ' '
1644            if escaped {
1645                current.push(c);
1646            } else {
1647                if current.len() > 0 && !parsed.contains(&current) {
1648                    parsed.push(current.clone());
1649                }
1650                current.clear();
1651            }
1652            escaped = false;
1653        } else {
1654            current.push(c);
1655            escaped = false;
1656        }
1657    }
1658    if current.len() > 0 && !parsed.contains(&current) {
1659        parsed.push(current);
1660    }
1661    parsed
1662}
1663
1664// convert tags back into a part of a log string
1665pub fn tags(tags: &Vec<String>) -> String {
1666    let mut v = tags.clone();
1667    v.sort_unstable();
1668    v.dedup(); // there may still be duplicates after we normalize whitespace below; oh, well
1669    let mut s = String::new();
1670    for (i, tag) in v.iter().enumerate() {
1671        if i > 0 {
1672            s.push(' ');
1673        }
1674        let mut ws = false;
1675        for c in tag.chars() {
1676            match c {
1677                ':' | '\\' | '<' => s.push('\\'),
1678                _ => (),
1679            }
1680            if c.is_whitespace() {
1681                if !ws {
1682                    ws = true;
1683                    s.push('\\');
1684                    s.push(' '); // normalize whitespace
1685                }
1686            } else {
1687                ws = false;
1688                s.push(c);
1689            }
1690        }
1691    }
1692    s
1693}
1694
1695#[derive(Debug, Clone)]
1696pub struct Event {
1697    pub start: NaiveDateTime,
1698    pub start_overlap: bool,
1699    pub end: Option<NaiveDateTime>,
1700    pub end_overlap: bool,
1701    pub description: String,
1702    pub tags: Vec<String>,
1703    pub vacation: bool,
1704    pub vacation_type: Option<String>,
1705}
1706
1707impl Event {
1708    pub fn coin(description: String, mut tags: Vec<String>) -> Event {
1709        tags.sort_unstable();
1710        tags.dedup();
1711        Event {
1712            start: Local::now().naive_local(),
1713            start_overlap: false,
1714            end: None,
1715            end_overlap: false,
1716            description: description,
1717            tags: tags,
1718            vacation: false,
1719            vacation_type: None,
1720        }
1721    }
1722    fn bounded_time(self, end: Option<NaiveDateTime>) -> Self {
1723        Event {
1724            start: self.start,
1725            start_overlap: self.start_overlap,
1726            end: end,
1727            end_overlap: self.end_overlap,
1728            description: self.description,
1729            tags: self.tags,
1730            vacation: self.vacation,
1731            vacation_type: self.vacation_type,
1732        }
1733    }
1734    pub fn ongoing(&self) -> bool {
1735        self.end.is_none()
1736    }
1737    // the duration of the task in seconds
1738    // the second parameter is necessary for ongoing tasks
1739    pub fn duration(&self, now: &NaiveDateTime) -> f32 {
1740        let end = self.end.as_ref().unwrap_or(now);
1741        (end.timestamp() - self.start.timestamp()) as f32
1742    }
1743    // split an event into two at a time boundary
1744    fn split(self, time: NaiveDateTime) -> (Self, Self) {
1745        assert!(time > self.start);
1746        assert!(self.end.is_none() || self.end.unwrap() > time);
1747        let mut start = self;
1748        let mut end = start.clone();
1749        start.end_overlap = true;
1750        start.end = Some(time.clone());
1751        end.start = time;
1752        end.end_overlap = true;
1753        (start, end)
1754    }
1755    // take a vector of events and convert them into sets not overlapping by day
1756    pub fn gather_by_day(events: Vec<Event>, end_date: &NaiveDateTime) -> Vec<Event> {
1757        let mut ret = vec![];
1758        let mut end_date = end_date;
1759        let now = Local::now().naive_local(); // we assume there are no future events in the log
1760        if &now < &end_date {
1761            end_date = &now;
1762        }
1763        for mut e in events {
1764            if &e.start >= end_date {
1765                break;
1766            }
1767            loop {
1768                match e.end.as_ref() {
1769                    Some(&time) => {
1770                        if time.date() == e.start.date() {
1771                            ret.push(e);
1772                            break;
1773                        }
1774                        let split_date = e.start.date().and_hms(0, 0, 0) + Duration::days(1);
1775                        let (e1, e2) = e.split(split_date);
1776                        e = e2;
1777                        ret.push(e1);
1778                    }
1779                    None => {
1780                        if e.start.date() == end_date.date() {
1781                            ret.push(e);
1782                            break;
1783                        } else {
1784                            let split_date = e.start.date().and_hms(0, 0, 0) + Duration::days(1);
1785                            let (e1, e2) = e.split(split_date);
1786                            e = e2;
1787                            ret.push(e1);
1788                        }
1789                    }
1790                }
1791            }
1792        }
1793        ret
1794    }
1795    fn mergeable(&self, other: &Self) -> bool {
1796        if self.end_overlap {
1797            // keep overlapped events separate to facilitate display
1798            return false;
1799        }
1800        if let Some(t) = self.end {
1801            t.day() == self.start.day() && // other isn't in a different day -- don't merge across day boundaries
1802            t == other.start  && self.tags == other.tags
1803        } else {
1804            false
1805        }
1806    }
1807    // this event was split off a larger one that overlapped a day boundary
1808    // it is the second part
1809    pub fn overlaps_start(&self) -> bool {
1810        self.end_overlap && self.start.hour() == 0
1811    }
1812    // this event was split off a larger one that overlapped a day boundary
1813    // it is the first part
1814    pub fn overlaps_end(&self) -> bool {
1815        if !self.end_overlap {
1816            return false;
1817        }
1818        if let Some(t) = self.end {
1819            t.day() != self.start.day()
1820        } else {
1821            false
1822        }
1823    }
1824    fn merge(&mut self, other: Self) {
1825        self.description = self.description.clone() + "; " + &other.description;
1826        self.end = other.end;
1827        self.end_overlap = other.end_overlap;
1828    }
1829    // like gather_by_day, but it also merges similar events -- similar events must have the same date and tags
1830    pub fn gather_by_day_and_merge(events: Vec<Event>, end_date: &NaiveDateTime) -> Vec<Event> {
1831        let mut events = Self::gather_by_day(events, end_date);
1832        if events.is_empty() {
1833            return events;
1834        }
1835        let mut ret = vec![];
1836        ret.push(events.remove(0));
1837        for e in events {
1838            let i = ret.len() - 1;
1839            if ret[i].mergeable(&e) {
1840                ret[i].merge(e);
1841            } else {
1842                ret.push(e);
1843            }
1844        }
1845        ret
1846    }
1847    pub fn to_json(&self, now: &NaiveDateTime, conf: &Configuration) -> String {
1848        let end = if let Some(time) = self.end {
1849            serde_json::to_string(&format!("{}", time)).unwrap()
1850        } else {
1851            "null".to_owned()
1852        };
1853        format!(
1854            r#"{{"type":"Event","start":{},"end":{},"duration":{},{}"tags":{},"description":{}}}"#,
1855            serde_json::to_string(&format!("{}", self.start)).unwrap(),
1856            end,
1857            duration_string(self.duration(now), conf),
1858            if let Some(t) = &self.vacation_type {
1859                format!("\"vacation\":\"{}\",", if t == "" { "ordinary" } else { t })
1860            } else {
1861                "".to_owned()
1862            },
1863            serde_json::to_string(&self.tags).unwrap(),
1864            serde_json::to_string(&self.description).unwrap()
1865        )
1866    }
1867}
1868
1869impl Searchable for Event {
1870    fn text(&self) -> &str {
1871        &self.description
1872    }
1873    fn tags(&self) -> Vec<&str> {
1874        self.tags.iter().map(|s| s.as_str()).collect()
1875    }
1876}
1877
1878#[derive(Debug, Clone)]
1879pub struct Note {
1880    pub time: NaiveDateTime,
1881    pub description: String,
1882    pub tags: Vec<String>,
1883}
1884
1885impl Note {
1886    pub fn coin(description: String, mut tags: Vec<String>) -> Note {
1887        tags.sort_unstable();
1888        tags.dedup();
1889        Note {
1890            time: Local::now().naive_local(),
1891            description: description,
1892            tags: tags,
1893        }
1894    }
1895    pub fn to_json(&self, _now: &NaiveDateTime, _conf: &Configuration) -> String {
1896        format!(
1897            r#"{{"type":"Note","time":{},"tags":{},"description":{}}}"#,
1898            serde_json::to_string(&format!("{}", self.time)).unwrap(),
1899            serde_json::to_string(&self.tags).unwrap(),
1900            serde_json::to_string(&self.description).unwrap()
1901        )
1902    }
1903}
1904
1905impl Searchable for Note {
1906    fn text(&self) -> &str {
1907        &self.description
1908    }
1909    fn tags(&self) -> Vec<&str> {
1910        self.tags.iter().map(|s| s.as_str()).collect()
1911    }
1912}
1913
1914#[derive(Debug, Clone)]
1915pub struct Done(pub NaiveDateTime);
1916
1917impl Done {
1918    pub fn coin() -> Done {
1919        Done(Local::now().naive_local())
1920    }
1921}
1922
1923pub enum Direction {
1924    Forward,
1925    Back,
1926}
1927
1928pub trait LogLine {
1929    fn to_line(&self) -> String;
1930}
1931
1932impl LogLine for Done {
1933    fn to_line(&self) -> String {
1934        let mut ts = timestamp(&self.0);
1935        ts += ":DONE";
1936        ts
1937    }
1938}
1939
1940impl LogLine for Note {
1941    fn to_line(&self) -> String {
1942        let mut ts = timestamp(&self.time);
1943        ts += "<NOTE>";
1944        let tags = tags(&self.tags);
1945        ts += &tags;
1946        ts.push(':');
1947        ts += &self.description;
1948        ts
1949    }
1950}
1951
1952impl LogLine for Event {
1953    fn to_line(&self) -> String {
1954        let mut ts = timestamp(&self.start);
1955        ts.push(':');
1956        let tags = tags(&self.tags);
1957        ts += &tags;
1958        ts.push(':');
1959        ts += &self.description;
1960        ts
1961    }
1962}
1963
1964pub trait Searchable {
1965    fn tags(&self) -> Vec<&str>;
1966    fn text(&self) -> &str;
1967}
1968
1969pub struct Filter<'a> {
1970    all_tags: Option<Vec<&'a str>>,
1971    no_tags: Option<Vec<&'a str>>,
1972    some_tags: Option<Vec<&'a str>>,
1973    some_patterns: Option<RegexSet>,
1974    no_patterns: Option<RegexSet>,
1975    empty: bool,
1976}
1977
1978impl<'a> Filter<'a> {
1979    pub fn dummy() -> Filter<'a> {
1980        Filter {
1981            all_tags: None,
1982            no_tags: None,
1983            some_tags: None,
1984            some_patterns: None,
1985            no_patterns: None,
1986            empty: false,
1987        }
1988    }
1989    pub fn new(matches: &'a ArgMatches) -> Filter<'a> {
1990        let all_tags = matches
1991            .values_of("tag")
1992            .and_then(|values| Some(values.collect()));
1993        let no_tags = matches
1994            .values_of("tag-none")
1995            .and_then(|values| Some(values.collect()));
1996        let some_tags = matches
1997            .values_of("tag-some")
1998            .and_then(|values| Some(values.collect()));
1999        let some_patterns = matches
2000            .values_of("rx")
2001            .and_then(|values| Some(RegexSet::new(values).unwrap()));
2002        let no_patterns = matches
2003            .values_of("rx-not")
2004            .and_then(|values| Some(RegexSet::new(values).unwrap()));
2005        let empty = matches.is_present("no-tags");
2006        Filter {
2007            all_tags,
2008            no_tags,
2009            some_tags,
2010            some_patterns,
2011            no_patterns,
2012            empty,
2013        }
2014    }
2015    pub fn matches<T: Searchable>(&self, filterable: &T) -> bool {
2016        let tags = filterable.tags();
2017        let text = filterable.text();
2018        if tags.is_empty() {
2019            if self.empty {
2020                if let Some(rx_set) = self.some_patterns.as_ref() {
2021                    if !rx_set.is_match(text) {
2022                        return false;
2023                    }
2024                }
2025                if let Some(rx_set) = self.no_patterns.as_ref() {
2026                    if rx_set.is_match(text) {
2027                        return false;
2028                    }
2029                }
2030                return true;
2031            } else if !(self.all_tags.is_none() && self.some_tags.is_none()) {
2032                return false;
2033            }
2034        } else if self.empty {
2035            return false;
2036        } else {
2037            if self.some_tags.is_some()
2038                && !self
2039                    .some_tags
2040                    .as_ref()
2041                    .unwrap()
2042                    .iter()
2043                    .any(|t| tags.contains(t))
2044            {
2045                return false;
2046            }
2047            if self.all_tags.is_some()
2048                && self
2049                    .all_tags
2050                    .as_ref()
2051                    .unwrap()
2052                    .iter()
2053                    .any(|t| !tags.contains(t))
2054            {
2055                return false;
2056            }
2057            if self.no_tags.is_some()
2058                && self
2059                    .no_tags
2060                    .as_ref()
2061                    .unwrap()
2062                    .iter()
2063                    .any(|t| tags.contains(t))
2064            {
2065                return false;
2066            }
2067        }
2068        if let Some(rx_set) = self.some_patterns.as_ref() {
2069            if !rx_set.is_match(text) {
2070                return false;
2071            }
2072        }
2073        if let Some(rx_set) = self.no_patterns.as_ref() {
2074            if rx_set.is_match(text) {
2075                return false;
2076            }
2077        }
2078        true
2079    }
2080}