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
16pub 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") .next()
26 .unwrap()
27 .into_inner()
28 .next()
29 .unwrap();
30
31 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") .next()
45 .unwrap()
46 .into_inner()
47 .next()
48 .unwrap();
49
50 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
110impl 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 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, 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}