diff --git a/src/core/amounts.rs b/src/core/amounts.rs index eae91fb..f296d87 100644 --- a/src/core/amounts.rs +++ b/src/core/amounts.rs @@ -1,5 +1,6 @@ use std::collections::HashMap; +use chrono::NaiveDate; use rust_decimal::Decimal; use rust_decimal_macros::dec; @@ -18,13 +19,19 @@ pub struct Amount { pub unit_id: u32, } -// #[derive(Debug, PartialEq, Clone, Copy)] -// pub struct Cost { -// pub value: Decimal, -// pub unit_id: u32, -// pub date: NativeDate, -// pub label: Option, -// } +#[derive(Debug, PartialEq, Clone)] +pub struct RawCost { + pub amount: RawAmount, + pub date: Option, + pub label: Option, +} + +#[derive(Debug, PartialEq, Clone)] +pub struct Cost { + pub amount: Amount, + pub date: Option, + pub label: Option, +} #[derive(Debug, Clone)] pub struct UnitSymbol { @@ -108,3 +115,10 @@ impl PartialOrd for Amount { } } } + +impl From for Amount { + fn from(cost: Cost) -> Self { + cost.amount + // StringData::Reference(value) + } +} diff --git a/src/core/ledger.rs b/src/core/ledger.rs index b5bff28..5fe24f5 100644 --- a/src/core/ledger.rs +++ b/src/core/ledger.rs @@ -2,10 +2,11 @@ use chrono::NaiveDate; use rust_decimal::Decimal; use rust_decimal_macros::dec; -use super::{Account, Amount, CoreError, Price, Transaction, Unit}; +use super::{Account, Amount, CoreError, Price, Transaction, Unit, options::Options}; #[derive(Debug)] pub struct Ledger { + options: Options, accounts: Vec, units: Vec, prices: Vec, @@ -15,6 +16,7 @@ pub struct Ledger { impl Ledger { pub fn new() -> Ledger { Ledger { + options: Default::default(), accounts: Vec::new(), units: Vec::new(), prices: Vec::new(), @@ -22,6 +24,10 @@ impl Ledger { } } + pub fn get_options(&self) -> &Options { + &self.options + } + pub fn get_accounts(&self) -> &Vec { &self.accounts } @@ -141,7 +147,7 @@ impl Ledger { })?; } else if let Some(cost_amount) = posting.get_cost() { self.add_price(Price { - amount: *cost_amount, + amount: (*cost_amount).clone().into(), date: transaction.get_date(), unit_id: posting.get_amount().unit_id, })?; @@ -160,6 +166,7 @@ impl Ledger { } pub fn sort_prices(&mut self) { + println!("price length = {:?}", self.prices.len()); self.prices.sort_by(|a, b| a.date.cmp(&b.date)); } } diff --git a/src/core/mod.rs b/src/core/mod.rs index 8f742a8..f303a5a 100644 --- a/src/core/mod.rs +++ b/src/core/mod.rs @@ -3,6 +3,7 @@ mod amounts; mod common; mod errors; mod ledger; +mod options; mod price; mod transaction; mod value; @@ -13,4 +14,4 @@ pub use errors::*; pub use ledger::*; pub use price::*; pub use transaction::*; -pub use value::*; \ No newline at end of file +pub use value::*; diff --git a/src/core/options.rs b/src/core/options.rs new file mode 100644 index 0000000..efb7d40 --- /dev/null +++ b/src/core/options.rs @@ -0,0 +1,28 @@ +use regex::{Regex, RegexBuilder}; + +#[derive(Debug)] +pub struct Options { + // Regex - account regex to match for temporally last account + // bool - whether the sign to match is positive (true) or negative (false) + // u32 - account ID + default_clearing_accounts: Vec<(Regex, bool, String)>, +} + +impl Default for Options { + fn default() -> Self { + Self { + default_clearing_accounts: vec![ + ( + RegexBuilder::new("^(Assets|Liabilities)").build().unwrap(), + true, + "Assets:Clearing".into(), + ), + ( + RegexBuilder::new("^(Assets|Liabilities)").build().unwrap(), + false, + "Liablities:Clearing".into(), + ), + ], + } + } +} diff --git a/src/core/transaction.rs b/src/core/transaction.rs index 0202933..9948004 100644 --- a/src/core/transaction.rs +++ b/src/core/transaction.rs @@ -3,7 +3,7 @@ use rust_decimal_macros::dec; use std::collections::HashMap; use super::Amount; -use crate::core::CoreError; +use crate::core::{CoreError, Cost}; //////////////// // Directives // @@ -32,7 +32,7 @@ pub enum TransactionFlag { pub struct Posting { account_id: u32, amount: Amount, - cost: Option, + cost: Option, price: Option, } @@ -89,7 +89,7 @@ impl Transaction { let mut amounts = HashMap::new(); for posting in &self.postings { let cost = if by_cost { - posting.cost.or(posting.price) + posting.cost.clone().map(|c| c.into()).or(posting.price) } else { None }; @@ -147,13 +147,13 @@ impl Posting { pub fn new( account_id: u32, amount: Amount, - cost: Option, + cost: Option, price: Option, ) -> Result { - if cost.is_some() && cost.unwrap().value.is_sign_negative() { + if cost.as_ref().is_some_and(|c| c.amount.value.is_sign_negative()) { return Err("Cost cannot be negative".into()); } - if price.is_some() && price.unwrap().value.is_sign_negative() { + if price.is_some_and(|p| p.value.is_sign_negative()) { return Err("Price cannot be negative".into()); } Ok(Posting { account_id, amount, cost, price }) @@ -167,7 +167,7 @@ impl Posting { &self.amount } - pub fn get_cost(&self) -> &Option { + pub fn get_cost(&self) -> &Option { &self.cost } @@ -184,6 +184,17 @@ impl Posting { // mod tests { // use super::*; +// #[test] +// fn tttt() { + +// } + +// } + +// #[cfg(test)] +// mod tests { +// use super::*; + // #[test] // fn balanced_transaction_simple() { // let txn = Transaction::new( diff --git a/src/document/directives.rs b/src/document/directives.rs index 1872bae..067d69a 100644 --- a/src/document/directives.rs +++ b/src/document/directives.rs @@ -2,12 +2,14 @@ use std::path::PathBuf; use chrono::NaiveDate; -use crate::core::{RawAmount, UnitSymbol}; +use crate::{core::{RawAmount, RawCost, UnitSymbol}, query::{PostingField, Query}}; #[derive(Debug)] pub struct Directives { pub includes: Vec, + pub options: Vec, pub commodities: Vec, + pub prices: Vec, pub transactions: Vec, pub balances: Vec, } @@ -21,6 +23,12 @@ pub struct IncludeDirective { pub path: PathBuf, } +#[derive(Debug, Clone)] +pub struct OptionsDirective { + pub name: String, + pub value: Vec, +} + #[derive(Debug, Clone)] pub struct CommodityDirective { pub date: Option, @@ -29,6 +37,13 @@ pub struct CommodityDirective { pub precision: Option, } +#[derive(Debug, Clone)] +pub struct PriceDirective { + pub date: NaiveDate, + pub symbol: String, + pub amount: RawAmount, +} + #[derive(Debug, Clone)] pub struct TransactionDirective { pub date: NaiveDate, @@ -41,8 +56,10 @@ pub struct TransactionDirective { #[derive(Debug, Clone)] pub struct BalanceDirective { - pub date: NaiveDate, - pub account: String, + // pub date: NaiveDate, + + // pub account: String, + pub query: Query, pub amounts: Vec, } @@ -55,7 +72,7 @@ pub struct DirectivePosting { pub date: Option, pub account: String, pub amount: Option, - pub cost: Option, + pub cost: Option, pub price: Option, } @@ -67,7 +84,9 @@ impl Directives { pub fn new() -> Self { Directives { includes: Vec::new(), + options: Vec::new(), commodities: Vec::new(), + prices: Vec::new(), transactions: Vec::new(), balances: Vec::new(), } @@ -78,5 +97,6 @@ impl Directives { self.transactions.extend(other.transactions.clone()); self.balances.extend(other.balances.clone()); self.commodities.extend(other.commodities.clone()); + self.prices.extend(other.prices.clone()); } } diff --git a/src/document/ledger.rs b/src/document/ledger.rs index c643129..b583986 100644 --- a/src/document/ledger.rs +++ b/src/document/ledger.rs @@ -4,8 +4,11 @@ use rust_decimal_macros::dec; use crate::{ core::{ - Account, Amount, CoreError, DataValue, Ledger, Posting, RawAmount, Transaction, TransactionFlag, Unit, UnitSymbol - }, query::{self, AccountField, ComparisonFunction, LogicalFunction, PostingField, Query, RegexFunction, TransactionField}, + Account, Amount, CoreError, Cost, DataValue, Ledger, Posting, Price, RawAmount, RawCost, Transaction, TransactionFlag, Unit, UnitSymbol + }, document::PriceDirective, query::{ + self, AccountField, ComparisonFunction, LogicalFunction, PostingField, Query, + RegexFunction, TransactionField, + } // queries::{ // self, // base::{self, DataValue}, @@ -46,7 +49,7 @@ pub fn add_transaction( None }; let cost = if let Some(c) = &p.cost { - Some(create_amount(ledger, c)?) + Some(create_cost(ledger, c)?) } else { None }; @@ -72,55 +75,22 @@ pub fn add_transaction( Ok(()) } +pub fn add_price(ledger: &mut Ledger, price: &PriceDirective) -> Result<(), CoreError> { + let unit_id = get_or_create_unit(ledger, &price.symbol, false)?; + let amount = create_amount(ledger, &price.amount)?; + ledger.add_price(Price { + date: price.date, + unit_id, + amount: amount, + }) +} + pub fn check_balance2(ledger: &Ledger, balance: &BalanceDirective) -> Result<(), CoreError> { - let date_query = ComparisonFunction::new( - "<=", - Query::from_field(PostingField::Transaction(TransactionField::Date)), - Query::from(DataValue::from(balance.date)), - ) - .unwrap(); - // let account_fn = |str: &str| { - // str.strip_prefix("abc") - // .map(|n| n.is_empty() || n.starts_with(":")) - // .unwrap_or(false) - // }; - // let account_query = StringComparisonFunction::new_func( - // base::Query::from_field(PostingField::Account(AccountField::Name)), - // &account_fn, - // )?; - // let account_regex = format!("^{}($|:.+)", balance.account); - // let account_query = RegexFunction::new( - // base::Query::from_field(PostingField::Account(AccountField::Name)), - // &account_regex, - // )?; - - - // let account_query = SubAccountFunction::new( - // balance.account.clone().into(), - // base::Query::from_field(PostingField::Account(AccountField::Name)), - // ); - - // TODO: is this efficient enough? - let account_query = RegexFunction::new( - Query::from_field(PostingField::Account(AccountField::Name)), - format!("^{}", balance.account).as_str(), - true - // "^" + balance.account.clone(), - ).unwrap(); - - let start = Instant::now(); - let total_query = LogicalFunction::new( - "and", - Query::from_fn(date_query), - Query::from_fn(account_query), - ) - .unwrap(); - let t2 = Instant::now(); - let accounts = query::balance(&ledger, Some(&Query::from_fn(total_query)), None); + let accounts = query::balance(&ledger, Some(&balance.query), None); let t3 = Instant::now(); @@ -136,9 +106,9 @@ pub fn check_balance2(ledger: &Ledger, balance: &BalanceDirective) -> Result<(), } } - if account_count == 0 { - return Err("No accounts match balance account".into()); - } + // if account_count == 0 { + // return Err("No accounts match balance account".into()); + // } // let balance_account = ledger // .get_account_by_name(&balance.account) @@ -175,9 +145,14 @@ pub fn check_balance2(ledger: &Ledger, balance: &BalanceDirective) -> Result<(), let balance_value = balance_amount.value.round_dp(max_scale); if value != balance_value { + // return Err(format!( + // "Balance amount for \"{}\" on {} does not match. Expected {} but got {}", + // balance.account, balance.date, balance_value, value + // ) + // TODO: better error message return Err(format!( - "Balance amount for \"{}\" on {} does not match. Expected {} but got {}", - balance.account, balance.date, balance_value, value + "Balance amount does not match. Expected {} but got {}", + balance_value, value ) .into()); } @@ -261,7 +236,7 @@ pub fn check_balance2(ledger: &Ledger, balance: &BalanceDirective) -> Result<(), struct IncompletePosting { account_id: u32, amount: Option, - cost: Option, + cost: Option, price: Option, } @@ -271,6 +246,13 @@ fn create_amount(ledger: &mut Ledger, amount: &RawAmount) -> Result Result { + let amount = create_amount(ledger, &cost.amount)?; + + Ok(Cost { amount, date: cost.date, label: cost.label.clone() }) +} + + fn get_or_create_unit( ledger: &mut Ledger, unit_symbol: &str, @@ -306,11 +288,11 @@ fn complete_incomplete_postings( complete_postings.push(Posting::new( posting.account_id, amount, - posting.cost, + posting.cost.clone(), posting.price, )?); - let cost = posting.cost.or(posting.price); + let cost = posting.cost.clone().map(|p| p.into()).or(posting.price); let converted_amount = amount.at_opt_price(cost); let converted_amount = ledger.round_amount(&converted_amount); *amounts.entry(converted_amount.unit_id).or_insert(dec!(0)) += converted_amount.value; diff --git a/src/document/mod.rs b/src/document/mod.rs index 276b360..fac3921 100644 --- a/src/document/mod.rs +++ b/src/document/mod.rs @@ -4,7 +4,7 @@ mod ledger; pub use directives::*; use ledger::{add_transaction, check_balance2}; -use crate::{core::{CoreError, Ledger, Unit}, parser::parse_directives}; +use crate::{core::{CoreError, Ledger, Unit}, document::ledger::add_price, parser::parse_directives}; use std::{path::Path, time::Instant}; #[derive(Debug)] @@ -34,6 +34,10 @@ impl Document { ledger.add_unit(unit)?; } + for price in &self.directives.prices { + add_price(&mut ledger, price)?; + } + for transaction in &self.directives.transactions { add_transaction(&mut ledger, transaction)?; } diff --git a/src/parser/amount.rs b/src/parser/amount.rs index 16494e3..98fc2e5 100644 --- a/src/parser/amount.rs +++ b/src/parser/amount.rs @@ -14,6 +14,10 @@ pub fn amount(input: &str) -> IResult<&str, RawAmount> { alt((suffix_amount, prefix_amount)).parse(input) } +pub fn unit(input: &str) -> IResult<&str, &str> { + recognize(many1(none_of("0123456789,+-_()*/.{} \t"))).parse(input) +} + /////////////// // Private // /////////////// @@ -45,10 +49,6 @@ fn suffix_amount(input: &str) -> IResult<&str, RawAmount> { .parse(input) } -fn unit(input: &str) -> IResult<&str, &str> { - recognize(many1(none_of("0123456789,+-_()*/.{} \t"))).parse(input) -} - #[cfg(test)] mod tests { use super::*; diff --git a/src/parser/core.rs b/src/parser/core.rs index 3929e92..89ee7e6 100644 --- a/src/parser/core.rs +++ b/src/parser/core.rs @@ -1,13 +1,6 @@ use chrono::NaiveDate; use nom::{ - branch::alt, - bytes::complete::{escaped, tag, take_while_m_n}, - character::complete::{char, none_of, one_of, space0}, - combinator::{opt, recognize}, - error::{Error, ErrorKind}, - multi::{many0, many1}, - sequence::{delimited, terminated, tuple}, - AsChar, Err, IResult, Parser, + AsChar, Err, IResult, Parser, branch::alt, bytes::complete::{escaped, tag, take_while_m_n}, character::complete::{char, none_of, one_of, space0}, combinator::{opt, recognize}, error::{Error, ErrorKind}, multi::{many0, many1, separated_list0, separated_list1}, sequence::{delimited, terminated, tuple} }; use rust_decimal::Decimal; @@ -34,7 +27,8 @@ pub fn decimal(input: &str) -> IResult<&str, Decimal> { } pub fn number_int(input: &str) -> IResult<&str, &str> { - recognize(many1(terminated(one_of("0123456789"), many0(one_of("_,")))))(input) + recognize(separated_list1(one_of("_,"), many1(one_of("0123456789"))))(input) + // recognize(many1(terminated(one_of("0123456789"), many0(one_of("_,")))))(input) } pub fn parse_iso_date(input: &str) -> IResult<&str, NaiveDate> { @@ -99,6 +93,11 @@ mod tests { use super::*; use rust_decimal_macros::dec; + #[test] + fn parse_number_int() { + assert_eq!(number_int("1").unwrap().1, "1"); + } + #[test] fn parse_decimal_good() { assert_eq!(decimal("1").unwrap().1, dec!(1)); diff --git a/src/parser/document/balance.rs b/src/parser/document/balance.rs new file mode 100644 index 0000000..71e0d8e --- /dev/null +++ b/src/parser/document/balance.rs @@ -0,0 +1,153 @@ +use nom::{ + character::complete::space1, + combinator::opt, + error::{Error, ErrorKind}, + sequence::{preceded, tuple}, + IResult, Parser, +}; + +use crate::{ + core::DataValue, + document::BalanceDirective, + parser::{amount, document::{base_directive::parse_iso_date, shared::metadatum}, query}, + query::{ + AccountField, ComparisonFunction, LogicalFunction, LogicalOperator, PostingField, Query, + RegexFunction, TransactionField, + }, +}; + +use super::{base_directive::BaseDirective, shared::account}; + +pub fn balance_directive(directive: BaseDirective) -> IResult { + // TODO: doesn't work without account + let (_, (single_account, single_amount)) = if let Ok(v) = + tuple((opt(account), opt(preceded(space1, amount)))) + .parse(directive.lines.get(0).unwrap_or(&"")) + { + v + } else { + return Err(nom::Err::Failure(Error { + input: directive, + code: ErrorKind::Eof, + })); + }; + + let mut amounts = if let Some(a) = single_amount { + vec![a] + } else { + Vec::new() + }; + let mut metadata = Vec::new(); + + for &line in directive.lines.get(1..).unwrap_or(&[]) { + if let Ok(m) = metadatum(line) { + metadata.push((m.1 .0.to_string(), m.1 .1.to_string())); + continue; + } + + let amount = if let Ok(a) = amount(line) { + a + } else { + return Err(nom::Err::Failure(Error { + input: directive, + code: ErrorKind::Fail, + })); + }; + amounts.push(amount.1); + } + + let mut queries = Vec::new(); + + if let Some(date) = directive.date { + let date_query = ComparisonFunction::new( + "<=", + Query::from_field(PostingField::Transaction(TransactionField::Date)), + Query::from(DataValue::from(date)), + ) + .unwrap(); + queries.push(Query::from_fn(date_query)); + } + + if let Some(account) = single_account { + let account_query = RegexFunction::new( + Query::from_field(PostingField::Account(AccountField::Name)), + format!("^{}", account).as_str(), + true, + ) + .unwrap(); + queries.push(Query::from_fn(account_query)); + } + + if let Some(start_date) = metadata + .iter() + .find(|m| m.0.to_lowercase() == "start") + .map(|m| m.1.clone()) + { + let start_date = parse_iso_date(&start_date).unwrap().1; + let start_query = ComparisonFunction::new( + ">=", + Query::from_field(PostingField::Transaction(TransactionField::Date)), + Query::from(DataValue::from(start_date)), + ) + .unwrap(); + queries.push(Query::from_fn(start_query)); + } + if let Some(end_date) = metadata + .iter() + .find(|m| m.0.to_lowercase() == "end") + .map(|m| m.1.clone()) + { + let end_date = parse_iso_date(&end_date).unwrap().1; + let end_query = ComparisonFunction::new( + "<=", + Query::from_field(PostingField::Transaction(TransactionField::Date)), + Query::from(DataValue::from(end_date)), + ) + .unwrap(); + queries.push(Query::from_fn(end_query)); + } + if let Some(general_query) = metadata + .iter() + .find(|m| m.0.to_lowercase() == "where") + .map(|m| &m.1) + { + let general_query = query::(general_query).unwrap().1; + queries.push(general_query); + } + + let total_query = LogicalFunction::new_from_vec(LogicalOperator::AND, queries).unwrap(); + + Ok(( + directive, + BalanceDirective { query: total_query, amounts }, + )) +} + +#[cfg(test)] +mod tests { + use chrono::NaiveDate; + + use super::*; + + // #[test] + // fn parse_balance() { + // let directive = BaseDirective { + // date: NaiveDate::from_ymd_opt(2000, 01, 01), + // directive_name: "balance", + // lines: vec!["Account", "$10", "20 UNIT"], + // }; + // let balance = balance_directive(directive).unwrap(); + // println!("{:?}", balance); + // } + + #[test] + fn parse_balance_start() { + let directive = BaseDirective { + date: NaiveDate::from_ymd_opt(2000, 01, 01), + directive_name: "balance", + lines: vec!["Account", "$10", "20 UNIT"], + }; + let balance = balance_directive(directive).unwrap(); + println!("{:?}", balance); + } +} diff --git a/src/parser/document/directives.rs b/src/parser/document/directives.rs index f68839b..63610c9 100644 --- a/src/parser/document/directives.rs +++ b/src/parser/document/directives.rs @@ -3,7 +3,7 @@ use std::path::PathBuf; use nom::{ bytes::complete::{is_not, tag}, character::complete::space1, - combinator::{opt, rest}, + combinator::rest, error::{Error, ErrorKind}, sequence::{preceded, terminated, tuple}, IResult, Parser, @@ -11,11 +11,14 @@ use nom::{ use crate::{ core::UnitSymbol, - document::{BalanceDirective, CommodityDirective, IncludeDirective, TransactionDirective}, - parser::amount, + document::{ + BalanceDirective, CommodityDirective, IncludeDirective, PriceDirective, + TransactionDirective, + }, + parser::{amount, document::balance::balance_directive, unit}, }; -use super::{base_directive::BaseDirective, shared::account, transaction::transaction}; +use super::{base_directive::BaseDirective, transaction::transaction}; ////////////// // Public // @@ -24,6 +27,7 @@ use super::{base_directive::BaseDirective, shared::account, transaction::transac pub enum Directive { Include(IncludeDirective), Commodity(CommodityDirective), + Price(PriceDirective), Balance(BalanceDirective), Transaction(TransactionDirective), } @@ -34,6 +38,9 @@ pub fn specific_directive<'a>( match directive.directive_name.to_lowercase().as_str() { "txn" => transaction(directive).map(|(i, v)| (i, Directive::Transaction(v))), // Assume transaction flag if length of one + "p" | "price" => price_directive + .map(|v| Directive::Price(v)) + .parse(directive), n if (n.len() == 1) => transaction .map(|v| Directive::Transaction(v)) .parse(directive), @@ -124,9 +131,9 @@ fn commodity_directive(directive: BaseDirective) -> IResult IResult { - let date = if let Some(d) = directive.date { - d +fn price_directive(directive: BaseDirective) -> IResult { + let date = if let Some(date) = directive.date { + date } else { return Err(nom::Err::Failure(Error { input: directive, @@ -134,8 +141,8 @@ fn balance_directive(directive: BaseDirective) -> IResult IResult Result { match parsed_directive { Directive::Include(d) => directives.includes.push(d), Directive::Balance(d) => directives.balances.push(d), + Directive::Price(d) => directives.prices.push(d), Directive::Transaction(d) => directives.transactions.push(d), Directive::Commodity(d) => directives.commodities.push(d), } diff --git a/src/parser/document/transaction.rs b/src/parser/document/transaction.rs index b4e228c..c65354f 100644 --- a/src/parser/document/transaction.rs +++ b/src/parser/document/transaction.rs @@ -1,17 +1,12 @@ +use chrono::NaiveDate; use nom::{ - branch::alt, - bytes::complete::{is_not, tag}, - character::complete::space1, - combinator::{eof, opt, rest}, - error::{Error, ErrorKind}, - sequence::{delimited, preceded, terminated, tuple}, - Err, IResult, Parser, + Err, IResult, Parser, branch::{alt, permutation}, bytes::complete::{is_not, tag}, character::complete::space1, combinator::{eof, opt, rest}, error::{Error, ErrorKind}, multi::separated_list0, sequence::{delimited, preceded, terminated, tuple} }; use crate::{ - core::RawAmount, + core::{RawAmount, RawCost}, document::{DirectivePosting, TransactionDirective}, - parser::amount, + parser::{amount, quoted_string, ws}, }; use super::{ @@ -104,8 +99,10 @@ fn posting(input: &str) -> IResult<&str, DirectivePosting> { account, opt(tuple(( preceded(space1, amount), - opt(preceded(space1, parse_cost)), - opt(preceded(space1, parse_price)), + permutation(( + opt(preceded(space1, parse_cost)), + opt(preceded(space1, parse_price)), + )) ))), eof, )) @@ -115,18 +112,22 @@ fn posting(input: &str) -> IResult<&str, DirectivePosting> { let mut price = None; if let Some(v) = value { amount = Some(v.0); - if let Some(c) = v.1 { + if let Some(c) = v.1.0 { if c.1 { - cost = Some(RawAmount { - value: c.0.value / amount.as_ref().unwrap().value.abs(), - unit_symbol: c.0.unit_symbol, - is_unit_prefix: c.0.is_unit_prefix, + cost = Some(RawCost { + amount: RawAmount { + value: c.0.amount.value / amount.as_ref().unwrap().value.abs(), + unit_symbol: c.0.amount.unit_symbol, + is_unit_prefix: c.0.amount.is_unit_prefix, + }, + date: c.0.date, + label: c.0.label, }); } else { cost = Some(c.0); } } - if let Some(p) = v.2 { + if let Some(p) = v.1.1 { if p.1 { price = Some(RawAmount { value: p.0.value / amount.as_ref().unwrap().value.abs(), @@ -149,10 +150,49 @@ fn posting(input: &str) -> IResult<&str, DirectivePosting> { .parse(input) } -fn parse_cost(input: &str) -> IResult<&str, (RawAmount, bool)> { +#[derive(Debug)] +enum CostInfo<'a> { + Amount(RawAmount), + Date(NaiveDate), + String(&'a str), +} + +fn cost_internal(input: &str) -> IResult<&str, RawCost> { + let (input, cost_info) = separated_list0( + ws(tag(",")), + ws(alt(( + parse_iso_date.map(|v| CostInfo::Date(v)), + quoted_string.map(|v| CostInfo::String(v)), + amount.map(|v| CostInfo::Amount(v)), + ))), + ) + .parse(input)?; + + let mut amount = None; + let mut date = None; + let mut label = None; + for i in cost_info { + match i { + CostInfo::Amount(a) => amount = Some(a), + CostInfo::Date(d) => date = Some(d), + CostInfo::String(s) => label = Some(s.into()), + } + } + + let amount = if let Some(a) = amount { + a + } else { + return Result::Err(Err::Failure(Error { input: input, code: ErrorKind::Fail })); + }; + + return Ok((input, RawCost { amount, date, label })); +} + +fn parse_cost(input: &str) -> IResult<&str, (RawCost, bool)> { + // TODO: allow dates alt(( - delimited(tag("{"), amount, tag("}")).map(|amount| (amount, false)), - delimited(tag("{{"), amount, tag("}}")).map(|amount| (amount, true)), + delimited(tag("{{"), cost_internal, tag("}}")).map(|amount| (amount, true)), + delimited(tag("{"), cost_internal, tag("}")).map(|amount| (amount, false)), )) .parse(input) } @@ -176,6 +216,13 @@ mod tests { use super::*; + // #[test] + // fn aaa() { + // let cost = parse_cost_internal("$10"); + // println!("{:?}", cost); + // assert!(false); + // } + #[test] fn test_parse_payee_narration() { assert_eq!(payee_narration("").unwrap().1, (None, None)); @@ -214,10 +261,14 @@ mod tests { unit_symbol: "SHARE".into(), is_unit_prefix: false }), - cost: Some(RawAmount { - value: dec!(100), - unit_symbol: "$".into(), - is_unit_prefix: true + cost: Some(RawCost { + amount: RawAmount { + value: dec!(100), + unit_symbol: "$".into(), + is_unit_prefix: true + }, + date: None, + label: None }), price: None, } @@ -232,10 +283,14 @@ mod tests { unit_symbol: "SHARE".into(), is_unit_prefix: false }), - cost: Some(RawAmount { - value: dec!(100), - unit_symbol: "USD".into(), - is_unit_prefix: false + cost: Some(RawCost { + amount: RawAmount { + value: dec!(100), + unit_symbol: "USD".into(), + is_unit_prefix: false + }, + date: None, + label: None }), price: None, } @@ -294,10 +349,14 @@ mod tests { unit_symbol: "SHARE".into(), is_unit_prefix: false }), - cost: Some(RawAmount { - value: dec!(100), - unit_symbol: "$".into(), - is_unit_prefix: true + cost: Some(RawCost { + amount: RawAmount { + value: dec!(100), + unit_symbol: "$".into(), + is_unit_prefix: true + }, + date: None, + label: None }), price: Some(RawAmount { value: dec!(110), @@ -318,10 +377,14 @@ mod tests { unit_symbol: "SHARE".into(), is_unit_prefix: false }), - cost: Some(RawAmount { - value: dec!(100), - unit_symbol: "USD".into(), - is_unit_prefix: false + cost: Some(RawCost { + amount: RawAmount { + value: dec!(100), + unit_symbol: "USD".into(), + is_unit_prefix: false + }, + date: None, + label: None }), price: Some(RawAmount { value: dec!(110), @@ -342,10 +405,14 @@ mod tests { unit_symbol: "SHARE".into(), is_unit_prefix: false }), - cost: Some(RawAmount { - value: dec!(100), - unit_symbol: "USD".into(), - is_unit_prefix: false + cost: Some(RawCost { + amount: RawAmount { + value: dec!(100), + unit_symbol: "USD".into(), + is_unit_prefix: false + }, + date: None, + label: None }), price: Some(RawAmount { value: dec!(110), @@ -370,10 +437,14 @@ mod tests { unit_symbol: "SHARE".into(), is_unit_prefix: false }), - cost: Some(RawAmount { - value: dec!(100), - unit_symbol: "$".into(), - is_unit_prefix: true + cost: Some(RawCost { + amount: RawAmount { + value: dec!(100), + unit_symbol: "$".into(), + is_unit_prefix: true + }, + date: None, + label: None }), price: Some(RawAmount { value: dec!(110), diff --git a/src/parser/fields.rs b/src/parser/fields.rs index 0266800..5ea2a3f 100644 --- a/src/parser/fields.rs +++ b/src/parser/fields.rs @@ -1,6 +1,6 @@ use std::fmt::Debug; -use crate::query::{AccountField, PostingField, TransactionField}; +use crate::query::{AccountField, PostingField, PostingRelated, TransactionField}; pub trait ParseField: Debug + Sized + Clone { fn parse(input: &str) -> Option; @@ -18,6 +18,15 @@ impl ParseField for TransactionField { } } +impl ParseField for PostingRelated { + fn parse(input: &str) -> Option { + match input.to_lowercase().as_str() { + "account_name" => Some(PostingRelated::AccountName), + _ => None, + } + } +} + impl ParseField for AccountField { fn parse(input: &str) -> Option { match input.to_lowercase().as_str() { @@ -41,6 +50,7 @@ impl ParseField for PostingField { ["cost"] => Some(PostingField::Cost), ["price"] => Some(PostingField::Price), ["transaction", t] => TransactionField::parse(t).map(|v| PostingField::Transaction(v)), + ["related", t] => PostingRelated::parse(t).map(|v| PostingField::Related(v)), ["account", t] => AccountField::parse(t).map(|v| PostingField::Account(v)), _ => None, } diff --git a/src/parser/query.rs b/src/parser/query.rs index b10ee73..5d6316d 100644 --- a/src/parser/query.rs +++ b/src/parser/query.rs @@ -52,11 +52,22 @@ fn term<'a, Field: ParseField + 'static>(input: &'a str) -> IResult<&'a str, Que } fn function_regex<'a, Field: ParseField>(input: &'a str) -> IResult<&'a str, Query> { - let (new_input, result) = tuple((factor, tag("~"), ws(quoted_string))) - .map(|(left, _, right)| RegexFunction::new(left, right, true)) // TODO: case sensitive? + let (new_input, result) = tuple((factor, alt((tag("~"), (tag("!~")))), ws(quoted_string))) + .map(|(left, op, right)| { + let regex = RegexFunction::new(left, right, true); + let regex = match regex { + Ok(r) => Query::from_fn(r), + Err(e) => return Err(e), + }; + if op == "~" { + Ok(regex) + } else { + Ok(Query::from_fn(NotFunction::new(regex))) + } + }) // TODO: case sensitive? .parse(input)?; match result { - Ok(regex_function) => Ok((new_input, Query::from_fn(regex_function))), + Ok(regex_function) => Ok((new_input, regex_function)), Err(_) => Err(Err::Error(Error::new(input, ErrorKind::Eof))), } } @@ -99,7 +110,7 @@ fn function_unary<'a, Field: ParseField + 'static>( fn field<'a, Field: ParseField>(input: &str) -> IResult<&str, Field> { input .split_at_position1_complete( - |item| !item.is_alphanum() && item != '.', + |item| !item.is_alphanum() && item != '.' && item != '_', ErrorKind::AlphaNumeric, ) .and_then(|v| { diff --git a/src/query/functions_comparison.rs b/src/query/functions_comparison.rs index 08d6414..570d1d7 100644 --- a/src/query/functions_comparison.rs +++ b/src/query/functions_comparison.rs @@ -149,7 +149,10 @@ impl Function for SubAccountFunction { impl RegexFunction { pub fn new(left: Query, regex: &str, case_insensitive: bool) -> Result { - let regex = RegexBuilder::new(regex).case_insensitive(case_insensitive).build().map_err(|_| CoreError::from("Unable to parse regex"))?; + let regex = RegexBuilder::new(regex) + .case_insensitive(case_insensitive) + .build() + .map_err(|_| CoreError::from("Unable to parse regex"))?; Ok(RegexFunction { left, regex }) } } @@ -158,11 +161,21 @@ impl Function for RegexFunction { fn evaluate(&self, context: &dyn Data) -> Result { let left = self.left.evaluate(context)?; - if let DataValue::String(left) = left { - Ok(DataValue::Boolean(self.regex.is_match(left.as_ref()))) - } else { - Err("Cannot use REGEX operation on non string types".into()) + match left { + DataValue::String(left) => Ok(DataValue::Boolean(self.regex.is_match(left.as_ref()))), + // TODO: better string matching? + DataValue::List(left) => Ok(DataValue::Boolean(left.iter().any(|val| match val { + DataValue::String(v) => self.regex.is_match(v.as_ref()), + _ => false, + }))), + _ => Err("Cannot use REGEX operation on non string types".into()), } + + // if let DataValue::String(left) = left { + // Ok(DataValue::Boolean(self.regex.is_match(left.as_ref()))) + // } else { + // Err("Cannot use REGEX operation on non string types".into()) + // } } } diff --git a/src/query/functions_logical.rs b/src/query/functions_logical.rs index c5552ad..9ba0319 100644 --- a/src/query/functions_logical.rs +++ b/src/query/functions_logical.rs @@ -2,7 +2,7 @@ use super::{Data, Function, Query}; use crate::core::{CoreError, DataValue}; use std::fmt::Debug; -#[derive(PartialEq, Debug, Clone)] +#[derive(PartialEq, Debug, Copy, Clone)] pub enum LogicalOperator { AND, OR, @@ -38,6 +38,18 @@ impl LogicalFunction { pub fn new_op(op: LogicalOperator, left: Query, right: Query) -> Self { LogicalFunction { op, left, right } } + + pub fn new_from_vec( + op: LogicalOperator, + queries: impl IntoIterator>, + ) -> Option> { + let mut iter = queries.into_iter(); + let first = iter.next()?; + + Some(iter.fold(first, |left, right| { + Query::from_fn(LogicalFunction { op, left: left, right: right }) + })) + } } impl Function for LogicalFunction { diff --git a/src/query/transaction.rs b/src/query/transaction.rs index b61e9d3..bdafc32 100644 --- a/src/query/transaction.rs +++ b/src/query/transaction.rs @@ -8,9 +8,15 @@ pub enum AccountField { CloseDate, } +#[derive(Debug, Clone)] +pub enum PostingRelated { + AccountName, +} + #[derive(Debug, Clone)] pub enum PostingField { Transaction(TransactionField), + Related(PostingRelated), Account(AccountField), Amount, Cost, @@ -37,6 +43,24 @@ impl<'a> Data for PostingData<'a> { PostingField::Transaction(transaction_field) => { get_transaction_value(transaction_field, &self.parent_transaction) } + PostingField::Related(related_field) => { + let values = self + .parent_transaction + .get_postings() + .iter() + .map(|posting| match related_field { + PostingRelated::AccountName => { + let account = self + .ledger + .get_account(posting.get_account_id()) + .ok_or_else(|| CoreError::from("Unable to find account")) + .unwrap(); + account.get_name().as_str().into() + } + }) + .collect(); + Ok(DataValue::List(values)) + } PostingField::Account(account_field) => { let account = self .ledger