aboutsummaryrefslogtreecommitdiff
path: root/apps/cassiopeia/src/format/parser.rs
blob: cc4b1b7c77dff72cd1fdd5d275d40fdc07a0d250 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
//! cassiopeia parser
//!
//! Takes a lexer's token stream as an input, and outputs a fully
//! parsed time file.

use crate::format::{LineLexer, LineToken, Token};
use chrono::{DateTime, FixedOffset as Offset};
use logos::Lexer;
use std::collections::BTreeMap;
use std::iter::Iterator;

/// A type-parsed line in a time file
#[derive(Debug)]
pub enum LineCfg {
    /// A header line with a set of keys and values
    Header(BTreeMap<String, String>),
    /// A session start line with a date and time
    Start(Option<DateTime<Offset>>),
    /// A session stop line with a date and time
    Stop(Option<DateTime<Offset>>),
    /// An invoice line with a date
    Invoice(Option<DateTime<Offset>>),
    /// An empty line
    Empty,
    /// A temporary value that is invalid
    #[doc(hidden)]
    Ignore,
}

impl LineCfg {
    pub(crate) fn valid(&self) -> bool {
        match self {
            LineCfg::Ignore => false,
            _ => true,
        }
    }
}

pub(crate) fn parse<'l>(lex: LineLexer<'l>) -> LineCfg {
    use LineCfg::*;
    use Token as T;

    #[cfg_attr(rustfmt, rustfmt_skip)]
    lex.get_all().into_iter().fold(Ignore, |cfg, tok| match (cfg, tok) {
        // If the first token is a comment, we ignore it
        (Ignore, LineToken { tt: T::Comment, .. }, ) => Ignore,
        // If the first token is a keyword, we wait for more data
        (Ignore, LineToken { tt: T::Header, .. }) => Header(Default::default()),
        (Ignore, LineToken { tt: T::Start, .. }) => Start(None),
        (Ignore, LineToken { tt: T::Stop, .. }) => Stop(None),
        (Ignore, LineToken { tt: T::Invoice, .. }) => Invoice(None),

        // If the first token _was_ a keyword, fill in the data
        (Header(map), LineToken { tt: T::HeaderData, slice }) => Header(append_data(map, slice)),
        (Start(_), LineToken { tt: T::Date, slice }) => Start(parse_date(slice)),
        (Stop(_), LineToken { tt: T::Date, slice }) => Stop(parse_date(slice)),
        (Invoice(_), LineToken { tt: T::Date, slice }) => Invoice(parse_date(slice)),

        // Pass empty lines through,
        (Empty, _) => Empty,

        // Ignore everything else (which will be filtered)
        _ => Ignore,
    })
}

fn append_data(mut map: BTreeMap<String, String>, slice: &str) -> BTreeMap<String, String> {
    let split = slice.split("=").collect::<Vec<_>>();
    map.insert(split[0].into(), split[1].into());
    map
}

fn parse_date(slice: &str) -> Option<DateTime<Offset>> {
    Some(
        DateTime::parse_from_str(slice, "%Y-%m-%d %H:%M:%S%:z")
            .expect("Failed to parse date; invalid format!"),
    )
}