summaryrefslogtreecommitdiffstats
path: root/cli/src/search.rs
blob: fd7407f888193129927641a9c1cedffe6de8d9a8 (plain) (blame)
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
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
use chrono::{naive::NaiveDate, Duration};

use crate::transaction::{Category, Transaction};

pub struct Search<'t> {
    filtered: Vec<usize>,
    transactions: Vec<&'t Transaction>,
}

pub enum DateIsh {
    Absolute(NaiveDate),
    Relative(Duration),
}

impl DateIsh {
    pub fn parse(s: &str) -> Self {
        DateIsh::Absolute(NaiveDate::parse_from_str(s, "%Y-%m-%d").unwrap())
    }
}

pub enum Constraint {
    Category(Category),
    Before(DateIsh, bool),
    After(DateIsh, bool),
}

impl Constraint {
    fn satisfies(&self, t: &Transaction) -> bool {
        match self {
            Constraint::Category(category) => category == &t.category,
            Constraint::Before(DateIsh::Absolute(date), inclusive) => if *inclusive {
                &t.date <= date
            } else {
                &t.date < date
            },
            Constraint::After(DateIsh::Absolute(date), inclusive) => if *inclusive {
                &t.date >= date
            } else {
                &t.date > date
            }
            Constraint::Before(DateIsh::Relative(diff), inclusive) => if *inclusive {
                t.date <= chrono::offset::Local::today().naive_utc() + *diff
            } else {
                t.date < chrono::offset::Local::today().naive_utc() + *diff
            }
            Constraint::After(DateIsh::Relative(diff), inclusive) => if *inclusive {
                t.date >= chrono::offset::Local::today().naive_utc() + *diff
            } else {
                t.date > chrono::offset::Local::today().naive_utc() + *diff
            }
        }
    }
}

impl<'t> Search<'t> {
    pub fn new(transactions: Vec<&'t Transaction>) -> Self {
        Self {
            filtered: std::iter::successors(Some(0_usize), |n| Some(n.checked_add(1).unwrap()))
                .take(transactions.len())
                .collect(),
            transactions,
        }
    }

    pub fn get(&self) -> Vec<&'t Transaction> {
        self
            .filtered
            .iter()
            .map(|idx| self.transactions[*idx])
            .collect()
    }

    pub fn parse(self, rules: String) -> Self {
        let (sub, rules) = {
            if rules.chars().nth(0).unwrap() == '-' {
                (true, &rules[1..])
            } else {
                (false, &rules[..])
            }
        };

        //TODO lexing

        // +category:a
        //TODO: category:"foo bar"

        //  before:2021-01-01  =>  + (* -> 2020-12-31) n-incl
        //  after:2021-01-01   =>  + (2021-01-01 -> *) incl
        // -before:2021-01-01  =>  - (2021-01-01 -> *) incl
        // -after:2021-01-01   =>  - (* -> 2021-12-31) n-incl

        //TODO:
        // today is 2021-01-01:
        //  before:-1d  =>  + (* -> 2020-12-31) n-incl
        //  after:-1d   =>  + (2021-01-01 -> *) incl
        // -before:-1d  =>  - (2021-01-01 -> *) incl
        // -after:-1d   =>  - (* -> 2020-12-31) n-incl

        let constraint = match rules.split_once(':').unwrap() {
            ("category", category) => Constraint::Category(category.to_string()),
            ("before", date_ish) => Constraint::Before(DateIsh::parse(date_ish), sub),
            ("after", date_ish) => Constraint::After(DateIsh::parse(date_ish), !sub),
            _ => panic!(),
        };

        if sub {
            self.subtract(constraint)
        } else {
            self.filter(constraint)
        }
    }

    pub fn subtract(self, constraint: Constraint) -> Self {
        Self {
            filtered: self
                .filtered
                .iter()
                .copied()
                .filter(|idx| !constraint.satisfies(self.transactions[*idx]))
                .collect(),
            transactions: self.transactions,
        }
    }

    pub fn filter(self, constraint: Constraint) -> Self {
        Self {
            filtered: self
                .filtered
                .iter()
                .copied()
                .filter(|idx| constraint.satisfies(self.transactions[*idx]))
                .collect(),
            transactions: self.transactions,
        }
    }
}