Skip to main content

dinero/parser/
value_expr.rs

1use super::utils::parse_rational;
2use super::{GrammarParser, Rule};
3use crate::app;
4use crate::models::{Account, Currency, Money, Payee, Posting, Transaction};
5use crate::List;
6use chrono::NaiveDate;
7
8use num::{abs, BigRational};
9use pest::Parser;
10use regex::Regex;
11use std::borrow::Borrow;
12use std::cmp::Ordering;
13use std::collections::HashMap;
14use std::rc::Rc;
15
16/// Builds the abstract syntax tree, to be able to evaluate expressions
17///
18/// This all comes from the defined grammar.pest
19pub fn build_root_node_from_expression(
20    expression: &str,
21    regexes: &mut HashMap<String, Regex>,
22) -> Node {
23    let parsed = GrammarParser::parse(Rule::value_expr, expression)
24        .expect("unsuccessful parse") // unwrap the parse result
25        .next()
26        .unwrap()
27        .into_inner()
28        .next()
29        .unwrap();
30
31    // Build the abstract syntax tree
32    build_ast_from_expr(parsed, regexes)
33}
34
35pub fn eval_expression(
36    expression: &str,
37    posting: &Posting,
38    transaction: &Transaction<Posting>,
39    commodities: &mut List<Currency>,
40    regexes: &mut HashMap<String, Regex>,
41) -> EvalResult {
42    let parsed = GrammarParser::parse(Rule::value_expr, expression)
43        .expect("unsuccessful parse") // unwrap the parse result
44        .next()
45        .unwrap()
46        .into_inner()
47        .next()
48        .unwrap();
49
50    // Build the abstract syntax tree
51    let root = build_ast_from_expr(parsed, regexes);
52    eval(&root, posting, transaction, commodities, regexes)
53}
54
55pub fn eval_value_expression(
56    expression: &str,
57    posting: &Posting,
58    transaction: &Transaction<Posting>,
59    commodities: &mut List<Currency>,
60    regexes: &mut HashMap<String, Regex>,
61) -> Money {
62    match eval_expression(expression, posting, transaction, commodities, regexes) {
63        EvalResult::Number(n) => posting.amount.clone().unwrap() * n,
64        EvalResult::Money(m) => m,
65        x => {
66            eprintln!("Found {:?}.", x);
67            panic!("Should be money.");
68        }
69    }
70}
71
72#[derive(Clone, Debug)]
73pub enum Node {
74    Amount,
75    Account,
76    Payee,
77    Note,
78    Date,
79    Number(BigRational),
80    Money {
81        currency: String,
82        amount: BigRational,
83    },
84    UnaryExpr {
85        op: Unary,
86        child: Box<Node>,
87    },
88    BinaryExpr {
89        op: Binary,
90        lhs: Box<Node>,
91        rhs: Box<Node>,
92    },
93    Regex(Regex),
94    String(String),
95}
96
97#[derive(Debug)]
98pub enum EvalResult {
99    Number(BigRational),
100    Money(Money),
101    Boolean(bool),
102    Account(Rc<Account>),
103    Payee(Rc<Payee>),
104    Regex(Regex),
105    String(Option<String>),
106    Date(NaiveDate),
107    Note,
108}
109
110/// Only use this for >, <, >=, <=, not for equality
111impl PartialOrd for EvalResult {
112    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
113        match self {
114            EvalResult::Number(left) => match other {
115                EvalResult::Number(right) => Some(left.cmp(right)),
116                EvalResult::Money(right) => Some(left.cmp(right.get_amount().borrow())),
117                _ => None,
118            },
119            EvalResult::Money(left) => match other {
120                EvalResult::Number(right) => Some(left.get_amount().cmp(right)),
121                EvalResult::Money(right) => Some(left.cmp(right)),
122                _ => None,
123            },
124            EvalResult::String(left) => match other {
125                EvalResult::String(right) => Some(left.cmp(right)),
126                _ => None,
127            },
128            EvalResult::Date(left) => match other {
129                EvalResult::Date(right) => Some(left.cmp(right)),
130                _ => None,
131            },
132            _ => None,
133        }
134    }
135}
136
137impl PartialEq for EvalResult {
138    fn eq(&self, other: &Self) -> bool {
139        match self {
140            EvalResult::Number(left) => match other {
141                EvalResult::Number(right) => left == right,
142                EvalResult::Money(right) => left == &right.get_amount(),
143                x => panic!("Expected number, found {:?}", x),
144            },
145            EvalResult::Money(left) => match other {
146                EvalResult::Number(right) => &left.get_amount() == right,
147                EvalResult::Money(right) => left == right,
148                x => panic!("Can't compare money and {:?}", x),
149            },
150
151            EvalResult::Boolean(left) => match other {
152                EvalResult::Boolean(right) => left == right,
153                x => panic!("Expected boolean, found {:?}", x),
154            },
155            EvalResult::String(left) => match other {
156                EvalResult::String(right) => left == right,
157                x => panic!("Expected string, found {:?}", x),
158            },
159            EvalResult::Date(left) => match other {
160                EvalResult::Date(right) => left == right,
161                x => panic!("Expected string, found {:?}", x),
162            },
163            x => panic!("Can't compare {:?}", x),
164        }
165    }
166}
167
168pub fn eval(
169    node: &Node,
170    posting: &Posting,
171    transaction: &Transaction<Posting>,
172    commodities: &List<Currency>,
173    regexes: &mut HashMap<String, Regex>,
174) -> EvalResult {
175    let res = match node {
176        Node::Amount => EvalResult::Money(posting.amount.clone().unwrap()),
177        Node::Account => EvalResult::Account(posting.account.clone()),
178        Node::Payee => EvalResult::Payee(posting.payee.clone().unwrap()),
179        Node::Note => EvalResult::Note,
180        Node::Date => EvalResult::Date(posting.date),
181        Node::Regex(r) => EvalResult::Regex(r.clone()),
182        Node::String(r) => EvalResult::String(Some(r.clone())),
183        Node::Number(n) => EvalResult::Number(n.clone()),
184        Node::Money { currency, amount } => {
185            let cur = match commodities.get(currency) {
186                Ok(c) => c.clone(),
187                Err(_) => {
188                    panic!("Can't find commodity {:?}", currency);
189                }
190            };
191            EvalResult::Money(Money::from((cur, amount.clone())))
192        }
193        Node::UnaryExpr { op, child } => {
194            let res = eval(child, posting, transaction, commodities, regexes);
195            match op {
196                Unary::Not => match res {
197                    EvalResult::Boolean(b) => EvalResult::Boolean(!b),
198                    x => panic!("Can't do neg of {:?}", x),
199                },
200                Unary::Any => {
201                    let mut res = false;
202                    for p in transaction.postings.borrow().iter() {
203                        // if p.origin != PostingOrigin::FromTransaction {
204                        //     continue;
205                        // }
206                        if let EvalResult::Boolean(b) =
207                            eval(child, p, transaction, commodities, regexes)
208                        {
209                            if b {
210                                res = true;
211                                break;
212                            }
213                        } else {
214                            panic!("Should evaluate to boolean")
215                        }
216                    }
217                    EvalResult::Boolean(res)
218                }
219                Unary::Neg => match res {
220                    EvalResult::Number(n) => EvalResult::Number(-n),
221                    EvalResult::Money(money) => EvalResult::Money(-money),
222                    EvalResult::Boolean(b) => EvalResult::Boolean(!b),
223                    x => panic!("Can't do neg of {:?}", x),
224                },
225                Unary::Abs => match res {
226                    EvalResult::Number(n) => EvalResult::Number(abs(n)),
227                    EvalResult::Money(money) => EvalResult::Money(match money {
228                        Money::Zero => Money::Zero,
229                        Money::Money { amount, currency } => Money::from((currency, abs(amount))),
230                    }),
231                    EvalResult::Boolean(_b) => panic!("Can't do abs of boolean"),
232                    x => panic!("Can't do abs of {:?}", x),
233                },
234                Unary::HasTag => match res {
235                    EvalResult::Regex(r) => EvalResult::Boolean(posting.has_tag(r)),
236                    x => panic!("Expected regex. Found {:?}", x),
237                },
238                Unary::Tag => match res {
239                    EvalResult::Regex(r) => EvalResult::String(posting.get_tag(r)),
240                    EvalResult::String(r) => EvalResult::String(posting.get_exact_tag(r.unwrap())),
241                    x => panic!("Expected regex. Found {:?}", x),
242                },
243                Unary::ToDate => match res {
244                    EvalResult::String(r) => {
245                        EvalResult::Date(app::date_parser(r.unwrap().as_str()).unwrap())
246                    }
247                    x => panic!("Expected String. Found {:?}", x),
248                },
249            }
250        }
251        Node::BinaryExpr { op, lhs, rhs } => {
252            let left = eval(lhs, posting, transaction, commodities, regexes);
253            let right = eval(rhs, posting, transaction, commodities, regexes);
254            match op {
255                Binary::Eq => match right {
256                    EvalResult::Regex(rhs) => match left {
257                        EvalResult::Account(lhs) => EvalResult::Boolean(lhs.is_match(rhs)),
258                        EvalResult::Payee(lhs) => EvalResult::Boolean(lhs.is_match(rhs)),
259                        EvalResult::String(lhs) => match lhs {
260                            Some(lhs) => EvalResult::Boolean(rhs.is_match(lhs.as_str())),
261                            None => EvalResult::Boolean(false),
262                        },
263                        EvalResult::Note => {
264                            let mut result = false;
265                            for comment in transaction.comments.iter() {
266                                if rhs.is_match(comment.comment.as_str()) {
267                                    result = true;
268                                    break;
269                                }
270                            }
271                            EvalResult::Boolean(result)
272                        }
273                        x => panic!("Found {:?}", x),
274                    },
275                    _ => EvalResult::Boolean(left == right),
276                },
277                Binary::Lt => EvalResult::Boolean(left < right),
278                Binary::Gt => EvalResult::Boolean(left > right),
279                Binary::Ge => EvalResult::Boolean(left >= right),
280                Binary::Le => EvalResult::Boolean(left <= right),
281                Binary::Add | Binary::Subtract => {
282                    if let EvalResult::Number(lhs) = left {
283                        if let EvalResult::Number(rhs) = right {
284                            EvalResult::Number(match op {
285                                Binary::Add => lhs + rhs,
286                                Binary::Subtract => lhs - rhs,
287                                _ => unreachable!(),
288                            })
289                        } else {
290                            panic!("Should be numbers")
291                        }
292                    } else if let EvalResult::Money(lhs) = left {
293                        if let EvalResult::Money(rhs) = right {
294                            EvalResult::Money(match op {
295                                Binary::Add => (lhs + rhs).to_money().unwrap(),
296                                Binary::Subtract => (lhs - rhs).to_money().unwrap(),
297                                _ => unreachable!(),
298                            })
299                        } else {
300                            panic!("Should be money")
301                        }
302                    } else {
303                        panic!("Should be money")
304                    }
305                }
306                Binary::Mult | Binary::Div => {
307                    if let EvalResult::Number(lhs) = left {
308                        if let EvalResult::Number(rhs) = right {
309                            EvalResult::Number(match op {
310                                Binary::Mult => lhs * rhs,
311                                Binary::Div => lhs / rhs,
312                                _ => unreachable!(),
313                            })
314                        } else if let EvalResult::Money(rhs) = right {
315                            EvalResult::Money(match op {
316                                Binary::Mult => rhs * lhs, // the other way around is not implemented
317                                Binary::Div => panic!("Can't divide number by money"),
318                                _ => unreachable!(),
319                            })
320                        } else {
321                            panic!("Should be numbers")
322                        }
323                    } else if let EvalResult::Money(lhs) = left {
324                        if let EvalResult::Number(rhs) = right {
325                            EvalResult::Money(match op {
326                                Binary::Mult => lhs * rhs,
327                                Binary::Div => lhs / rhs,
328                                _ => unreachable!(),
329                            })
330                        } else {
331                            panic!("rhs should be a number")
332                        }
333                    } else {
334                        panic!("Should be numbers")
335                    }
336                }
337                Binary::Or | Binary::And => {
338                    if let EvalResult::Boolean(lhs) = left {
339                        EvalResult::Boolean(match op {
340                            Binary::Or => {
341                                if lhs {
342                                    true
343                                } else if let EvalResult::Boolean(rhs) = right {
344                                    rhs
345                                } else {
346                                    panic!("Should be booleans")
347                                }
348                            }
349                            Binary::And => {
350                                if !lhs {
351                                    false
352                                } else if let EvalResult::Boolean(rhs) = right {
353                                    rhs
354                                } else {
355                                    panic!("Should be booleans")
356                                }
357                            }
358                            _ => unreachable!(),
359                        })
360                    } else {
361                        panic!("Should be booleans")
362                    }
363                }
364            }
365        }
366    };
367    res
368}
369
370#[derive(Clone, Debug)]
371pub enum Unary {
372    Not,
373    Neg,
374    Abs,
375    Any,
376    HasTag,
377    Tag,
378    ToDate,
379}
380
381#[derive(Clone, Debug)]
382pub enum Binary {
383    Add,
384    Subtract,
385    Mult,
386    Div,
387    Or,
388    And,
389    Eq,
390    Ge,
391    Gt,
392    Le,
393    Lt,
394}
395
396#[derive(Clone)]
397pub enum Ternary {}
398
399fn build_ast_from_expr(
400    pair: pest::iterators::Pair<Rule>,
401    regexes: &mut HashMap<String, Regex>,
402) -> Node {
403    let rule = pair.as_rule();
404    match rule {
405        Rule::expr => build_ast_from_expr(pair.into_inner().next().unwrap(), regexes),
406        Rule::comparison_expr
407        | Rule::or_expr
408        | Rule::and_expr
409        | Rule::additive_expr
410        | Rule::multiplicative_expr => {
411            let mut pair = pair.into_inner();
412            let lhspair = pair.next().unwrap();
413            let lhs = build_ast_from_expr(lhspair, regexes);
414            match pair.next() {
415                None => lhs,
416                Some(x) => {
417                    let op = match rule {
418                        Rule::or_expr => Binary::Or,
419                        Rule::and_expr => Binary::And,
420                        _ => match x.as_str() {
421                            "+" => Binary::Add,
422                            "-" => Binary::Subtract,
423                            "*" => Binary::Mult,
424                            "/" => Binary::Div,
425                            "=~" | "==" => Binary::Eq,
426                            "<" => Binary::Lt,
427                            ">" => Binary::Gt,
428                            "<=" => Binary::Le,
429                            ">=" => Binary::Ge,
430                            x => unreachable!("{}", x),
431                        },
432                    };
433                    let rhspair = pair.next().unwrap();
434                    let rhs = build_ast_from_expr(rhspair, regexes);
435                    parse_binary_expr(op, lhs, rhs)
436                }
437            }
438        }
439        Rule::primary => {
440            let mut inner = pair.into_inner();
441            let first = inner.next().unwrap();
442            match first.as_rule() {
443                Rule::function | Rule::unary => {
444                    let op = match first.as_str() {
445                        "abs" => Unary::Abs,
446                        "-" => Unary::Neg,
447                        "has_tag" => Unary::HasTag,
448                        "tag" => Unary::Tag,
449                        "to_date" => Unary::ToDate,
450                        "not" => Unary::Not,
451                        "any" => Unary::Any,
452                        unknown => panic!("Unknown expr: {:?}", unknown),
453                    };
454                    parse_unary_expr(op, build_ast_from_expr(inner.next().unwrap(), regexes))
455                }
456                Rule::money => {
457                    let mut money = first.into_inner();
458                    let child = money.next().unwrap();
459                    match child.as_rule() {
460                        Rule::number => Node::Money {
461                            currency: money.next().unwrap().as_str().to_string(),
462                            amount: parse_rational(child),
463                        },
464                        Rule::currency => {
465                            if child.as_str().starts_with('-') {
466                                Node::Money {
467                                    currency: child.as_str().to_string(),
468                                    amount: -parse_rational(money.next().unwrap()),
469                                }
470                            } else {
471                                Node::Money {
472                                    currency: child.as_str().to_string(),
473                                    amount: parse_rational(money.next().unwrap()),
474                                }
475                            }
476                        }
477                        unknown => panic!("Unknown rule: {:?}", unknown),
478                    }
479                }
480                Rule::number => Node::Number(parse_rational(first)),
481                Rule::regex | Rule::string => {
482                    let full = first.as_str().to_string();
483                    let n = full.len() - 1;
484                    let slice = &full[1..n];
485                    match first.as_rule() {
486                        Rule::regex => match regexes.get(slice) {
487                            None => {
488                                let regex = Regex::new(slice).unwrap();
489                                regexes.insert(slice.to_string(), regex.clone());
490                                Node::Regex(regex)
491                            }
492                            Some(regex) => Node::Regex(regex.clone()),
493                        },
494                        Rule::string => Node::String(slice.to_string()),
495                        unknown => unreachable!("This cannot happen {:?}", unknown),
496                    }
497                }
498                Rule::variable => match first.as_str() {
499                    "account" => Node::Account,
500                    "amount" => Node::Amount,
501                    "payee" => Node::Payee,
502                    "note" => Node::Note,
503                    "date" => Node::Date,
504                    unknown => panic!("Unknown variable: {:?}", unknown),
505                },
506                Rule::expr => build_ast_from_expr(first, regexes),
507
508                unknown => panic!("Unknown rule: {:?}", unknown),
509            }
510        }
511        unknown => panic!("Unknown expr: {:?}", unknown),
512    }
513}
514
515fn parse_binary_expr(operation: Binary, lhs: Node, rhs: Node) -> Node {
516    Node::BinaryExpr {
517        op: operation,
518        lhs: Box::new(lhs),
519        rhs: Box::new(rhs),
520    }
521}
522
523fn parse_unary_expr(operation: Unary, child: Node) -> Node {
524    Node::UnaryExpr {
525        op: operation,
526        child: Box::new(child),
527    }
528}