Skip to content

Commit

Permalink
Merge pull request #39 from frosklis/release/0.12
Browse files Browse the repository at this point in the history
Release/0.12
  • Loading branch information
frosklis authored Feb 24, 2021
2 parents 19e9554 + e95758e commit ef6ec4d
Show file tree
Hide file tree
Showing 42 changed files with 871 additions and 273 deletions.
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
# Changelog
Changelog file for dinero-rs project, a command line application for managing finances.

## [0.12.0] - 2021-02-xx (planned)
### Added
- support for (some of the) automated transaction syntax, what Claudio uses in his personal ledger
### Fixed
- speed bump (from 44 seconds to 7 seconds) in a big personal ledger
## [0.11.1] - 2021-02-22
### Fixed
- Fixed bug in balance report
4 changes: 2 additions & 2 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,10 @@ repository = "https://github.com/frosklis/dinero-rs"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[lib]
name = "dinero"
path = "src/lib/mod.rs"
path = "src/mod.rs"
[[bin]]
name = "dinero"
test = false
test = false
bench = false
path = "src/main.rs"

Expand Down
6 changes: 5 additions & 1 deletion examples/demo.ledger
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@

2021-01-15 * Flights
; destination: spain
Expenses:Travel 200 EUR
Expenses:Unknown 200 EUR ; this will get translated to Expenses:Flights
Assets:Bank:Checking account $-210.12

2021-01-16 * Alphabet Inc.
Expand All @@ -34,3 +34,7 @@ commodity €
commodity USD
alias $
P 2021-01-23 AAPL 139.07 USD
payee ACME, inc.
alias (?i)(.*acme.*)
account Expenses:Travel
payee Flights
14 changes: 11 additions & 3 deletions src/lib/main.rs → src/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ use two_timer;
use lazy_static::lazy_static;
use regex::Regex;

use crate::commands::{accounts, balance, check, commodities, prices, register};
use crate::commands::{accounts, balance, check, commodities, payees, prices, register};
use crate::Error;
use chrono::NaiveDate;
use colored::Colorize;
Expand All @@ -35,7 +35,8 @@ enum Command {
/// List the accounts
Accounts(CommonOpts),
// Codes,
// Payees,
/// List the payees
Payees(CommonOpts),
/// Show the exchange rates
Prices(CommonOpts),
/// List commodities
Expand Down Expand Up @@ -203,6 +204,13 @@ pub fn run_app(mut args: Vec<String>) -> Result<(), ()> {

commodities::execute(options.input_file, options.no_balance_check)
}
Command::Payees(options) => {
if options.force_color {
env::set_var("CLICOLOR_FORCE", "1");
}

payees::execute(options.input_file, options.no_balance_check)
}
Command::Prices(options) => prices::execute(options.input_file, options.no_balance_check),
Command::Accounts(options) => {
if options.force_color {
Expand All @@ -223,7 +231,7 @@ pub fn run_app(mut args: Vec<String>) -> Result<(), ()> {
}

/// A parser for date expressions
fn date_parser(date: &str) -> Result<NaiveDate, Error> {
pub fn date_parser(date: &str) -> Result<NaiveDate, Error> {
lazy_static! {
static ref RE_MONTH: Regex = Regex::new(r"(\d{4})[/-](\d\d?)$").unwrap();
static ref RE_DATE: Regex = Regex::new(r"(\d{4})[/-](\d\d?)[/-](\d\d?)$").unwrap();
Expand Down
File renamed without changes.
8 changes: 4 additions & 4 deletions src/lib/commands/balance.rs → src/commands/balance.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,13 +20,13 @@ pub fn execute(options: &CommonOpts, flat: bool, show_total: bool) -> Result<(),
let no_balance_check = options.no_balance_check;
let mut tokenizer: Tokenizer = Tokenizer::from(&path);
let items = tokenizer.tokenize()?;
let ledger = items.to_ledger(no_balance_check)?;
let mut ledger = items.to_ledger(no_balance_check)?;

let mut balances: HashMap<Rc<Account>, Balance> = HashMap::new();

for t in ledger.transactions.iter() {
for p in t.postings_iter() {
if !filter::filter(&options, t, p) {
if !filter::filter(&options, t, p, &mut ledger.commodities)? {
continue;
}
let mut cur_bal = balances
Expand Down Expand Up @@ -140,7 +140,7 @@ pub fn execute(options: &CommonOpts, flat: bool, show_total: bool) -> Result<(),
let mut text = account.split(":").last().unwrap().to_string();
// This is where it gets tricky, we need to collapse while we can
let mut collapse = true;
'outer: loop {
loop {
if (index + 1) >= num_bal {
break;
}
Expand All @@ -153,7 +153,7 @@ pub fn execute(options: &CommonOpts, flat: bool, show_total: bool) -> Result<(),
break;
}
let this_depth = name.split(":").count();
if (this_depth == n + 1) {
if this_depth == n + 1 {
collapse = false;
break;
}
Expand Down
File renamed without changes.
File renamed without changes.
1 change: 1 addition & 0 deletions src/lib/commands/mod.rs → src/commands/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,6 @@ pub mod accounts;
pub mod balance;
pub mod check;
pub mod commodities;
pub mod payees;
pub mod prices;
pub mod register;
21 changes: 21 additions & 0 deletions src/commands/payees.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
use crate::models::{HasName, Payee};
use crate::parser::Tokenizer;
use crate::Error;
use std::ops::Deref;
use std::path::PathBuf;

pub fn execute(path: PathBuf, no_balance_check: bool) -> Result<(), Error> {
let mut tokenizer: Tokenizer = Tokenizer::from(&path);
let items = tokenizer.tokenize()?;
let ledger = items.to_ledger(no_balance_check)?;
let mut payees = ledger
.payees
.iter()
.map(|x| x.1.deref().to_owned())
.collect::<Vec<Payee>>();
payees.sort_by(|a, b| a.get_name().cmp(b.get_name()));
for payee in payees.iter() {
println!("{}", payee);
}
Ok(())
}
File renamed without changes.
9 changes: 6 additions & 3 deletions src/lib/commands/register.rs → src/commands/register.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ pub fn execute(options: &CommonOpts) -> Result<(), Error> {
// Now work
let mut tokenizer: Tokenizer = Tokenizer::from(&path);
let items = tokenizer.tokenize()?;
let ledger = items.to_ledger(no_balance_check)?;
let mut ledger = items.to_ledger(no_balance_check)?;

let mut balance = Balance::new();

Expand All @@ -36,15 +36,18 @@ pub fn execute(options: &CommonOpts) -> Result<(), Error> {
for t in ledger.transactions.iter() {
let mut counter = 0;
for p in t.postings_iter() {
if !filter::filter(&options, t, p) {
if !filter::filter(&options, t, p, &mut ledger.commodities)? {
continue;
}
counter += 1;
if counter == 1 {
print!(
"{:w1$}{:width$}",
format!("{}", t.date.unwrap()),
clip(&t.description, w_description),
clip(
&format!("{} ", t.get_payee(&mut ledger.payees)),
w_description
),
width = w_description,
w1 = w_date
);
Expand Down
File renamed without changes.
140 changes: 140 additions & 0 deletions src/filter.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
use crate::models::{Currency, Posting, PostingType, Transaction};
use crate::parser::value_expr::{eval_expression, EvalResult};
use crate::{CommonOpts, Error, List};
use colored::Colorize;
use regex::Regex;
use std::collections::HashMap;

pub fn filter(
options: &CommonOpts,
transaction: &Transaction<Posting>,
posting: &Posting,
commodities: &mut List<Currency>,
) -> Result<bool, Error> {
// Get what's needed
let predicate = preprocess_query(&options.query);
let real = options.real;

// Check for real postings
if real {
if let PostingType::Real = posting.kind {
} else {
return Ok(false);
}
}

// Check for dates at the transaction level
// todo should do this at the posting level
if let Some(date) = options.end {
if transaction.date.unwrap() >= date {
return Ok(false);
}
}
if let Some(date) = options.begin {
if transaction.date.unwrap() < date {
return Ok(false);
}
}

filter_predicate(
predicate.as_str(),
posting,
transaction,
commodities,
&mut HashMap::new(),
)
}

pub fn filter_predicate(
predicate: &str,
posting: &Posting,
transaction: &Transaction<Posting>,
commodities: &mut List<Currency>,
regexes: &mut HashMap<String, Regex>,
) -> Result<bool, Error> {
if (predicate.len() == 0) | (predicate == "()") {
return Ok(true);
}
let result = eval_expression(predicate, posting, transaction, commodities, regexes);
match result {
EvalResult::Boolean(b) => Ok(b),
_ => Err(Error {
message: vec![predicate.red().bold(), "should return a boolean".normal()],
}),
}
}

/// Create search expression from Strings
///
/// The command line arguments provide syntactic sugar which save time when querying the journal.
/// This expands it to an actual query
///
/// # Examples
/// ```rust
/// # use dinero::filter::preprocess_query;
/// let params:Vec<String> = vec!["@payee", "savings" , "and", "checking", "and", "expr", "/aeiou/"].iter().map(|x| x.to_string()).collect();
/// let processed = preprocess_query(&params);
/// assert_eq!(processed, "((payee =~ /(?i)payee/) or (account =~ /(?i)savings/) and (account =~ /(?i)checking/) and (/aeiou/))")
/// ```
pub fn preprocess_query(query: &Vec<String>) -> String {
let mut expression = String::new();
let mut and = false;
let mut first = true;
let mut expr = false;
for raw_term in query.iter() {
let term = raw_term.trim();
if term.len() == 0 {
continue;
}
if term == "and" {
and = true;
continue;
} else if term == "or" {
and = false;
continue;
} else if term == "expr" {
expr = true;
continue;
}
let join_term = if !first {
if and {
" and ("
} else {
" or ("
}
} else {
"("
};
expression.push_str(join_term);
if expr {
expression.push_str(term);
} else if let Some(c) = term.chars().next() {
match c {
'@' => {
expression.push_str("payee =~ /(?i)"); // case insensitive
expression.push_str(&term.to_string()[1..]);
expression.push_str("/")
}
'%' => {
expression.push_str("has_tag(/(?i)"); // case insensitive
expression.push_str(&term.to_string()[1..]);
expression.push_str("/)")
}
'/' => {
expression.push_str("account =~ "); // case insensitive
expression.push_str(term);
}
_ => {
expression.push_str("account =~ /(?i)"); // case insensitive
expression.push_str(term);
expression.push_str("/")
}
}
}
expression.push_str(")");
and = false;
expr = false;
first = false;
}
format!("({})", expression)
}
68 changes: 68 additions & 0 deletions src/grammar/expressions.pest
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
// Grammar specification for value expressions

// A value expression is an expression between parenthesis
value_expr = {"(" ~ ws* ~ expr ~ ws* ~ ")"}

// Then the expression builds up in terms of increasing preference
expr = { or_expr }
or_expr = { and_expr ~ ws* ~ ( or ~ ws* ~ and_expr ) * }
and_expr = { comparison_expr ~ ws* ~ ( and ~ ws* ~ comparison_expr ) * }
comparison_expr = { additive_expr ~ ws* ~ ( comparison ~ ws* ~ additive_expr ) * }
additive_expr = { multiplicative_expr ~ ws* ~ ( add ~ ws* ~ multiplicative_expr ) * }
multiplicative_expr = { primary ~ ws* ~ ( mult ~ ws* ~ primary )* }
primary = {
("(" ~ ws* ~ expr ~ ws* ~ ")") |
(unary ~ ws* ~ expr) |
term |
(function ~ ws* ~ "(" ~ ws* ~ expr ~ ws* ~ ("," ~ ws* ~ expr ~ ws*)* ~ ")")
}


term = _{ variable | money | number | regex | string }
money = { (number ~ ws* ~ currency) | (currency ~ ws* ~ number) }
currency = { LETTER+ | ("\"" ~ (!"\"" ~ ANY)+ ~ "\"")}
regex = { "/" ~ (!"/" ~ ANY)* ~ "/"}
string = { "'" ~ (!"'" ~ ANY)* ~ "'"}
variable = {
"account" |
"payee" |
"date" |
"note" |
"amount" |
"total_amount" |
"cost" |
"value" |
"gain" |
"depth" |
"posting_number" |
"posting_count" |
"cleared" |
"real" |
"not_automated" |
"running_total" |
"note" |
// Abbreviations go later
"T" | "N" | "O" | "Z" | "R" | "X" |
"n" | "l" | "g" | "v" | "b"
}


// helpers
number = { "-"? ~ bigint ~ ("." ~ bigint)? }
bigint = _{ ASCII_DIGIT+ }
ws = _{" "}


add = { "+" | "-" }
mult = { "*" | "/" }
and = {"&" | "and"}
or = {"|" | "or" }
unary = { "-" | "!" | "not" }
function = { "abs" | "has_tag" | "to_date" | "any" | "tag" }
comparison = { eq | ne | ge | gt | le | lt }
eq = { "=~" | "=="}
ne = { "!=" }
gt = { ">" }
ge = { ">=" }
le = { "<=" }
lt = { "<" }
Loading

0 comments on commit ef6ec4d

Please sign in to comment.