many changes with balance, restructure, etc.

This commit is contained in:
Evan Peterson 2025-12-28 03:02:06 -06:00
parent 94bffebd8f
commit c163d4f8d3
Signed by: petersonev
GPG Key ID: 26BC6134519C4FC6
19 changed files with 524 additions and 190 deletions

View File

@ -1,5 +1,6 @@
use std::collections::HashMap; use std::collections::HashMap;
use chrono::NaiveDate;
use rust_decimal::Decimal; use rust_decimal::Decimal;
use rust_decimal_macros::dec; use rust_decimal_macros::dec;
@ -18,13 +19,19 @@ pub struct Amount {
pub unit_id: u32, pub unit_id: u32,
} }
// #[derive(Debug, PartialEq, Clone, Copy)] #[derive(Debug, PartialEq, Clone)]
// pub struct Cost { pub struct RawCost {
// pub value: Decimal, pub amount: RawAmount,
// pub unit_id: u32, pub date: Option<NaiveDate>,
// pub date: NativeDate, pub label: Option<String>,
// pub label: Option<String>, }
// }
#[derive(Debug, PartialEq, Clone)]
pub struct Cost {
pub amount: Amount,
pub date: Option<NaiveDate>,
pub label: Option<String>,
}
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct UnitSymbol { pub struct UnitSymbol {
@ -108,3 +115,10 @@ impl PartialOrd for Amount {
} }
} }
} }
impl From<Cost> for Amount {
fn from(cost: Cost) -> Self {
cost.amount
// StringData::Reference(value)
}
}

View File

@ -2,10 +2,11 @@ use chrono::NaiveDate;
use rust_decimal::Decimal; use rust_decimal::Decimal;
use rust_decimal_macros::dec; 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)] #[derive(Debug)]
pub struct Ledger { pub struct Ledger {
options: Options,
accounts: Vec<Account>, accounts: Vec<Account>,
units: Vec<Unit>, units: Vec<Unit>,
prices: Vec<Price>, prices: Vec<Price>,
@ -15,6 +16,7 @@ pub struct Ledger {
impl Ledger { impl Ledger {
pub fn new() -> Ledger { pub fn new() -> Ledger {
Ledger { Ledger {
options: Default::default(),
accounts: Vec::new(), accounts: Vec::new(),
units: Vec::new(), units: Vec::new(),
prices: 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<Account> { pub fn get_accounts(&self) -> &Vec<Account> {
&self.accounts &self.accounts
} }
@ -141,7 +147,7 @@ impl Ledger {
})?; })?;
} else if let Some(cost_amount) = posting.get_cost() { } else if let Some(cost_amount) = posting.get_cost() {
self.add_price(Price { self.add_price(Price {
amount: *cost_amount, amount: (*cost_amount).clone().into(),
date: transaction.get_date(), date: transaction.get_date(),
unit_id: posting.get_amount().unit_id, unit_id: posting.get_amount().unit_id,
})?; })?;
@ -160,6 +166,7 @@ impl Ledger {
} }
pub fn sort_prices(&mut self) { pub fn sort_prices(&mut self) {
println!("price length = {:?}", self.prices.len());
self.prices.sort_by(|a, b| a.date.cmp(&b.date)); self.prices.sort_by(|a, b| a.date.cmp(&b.date));
} }
} }

View File

@ -3,6 +3,7 @@ mod amounts;
mod common; mod common;
mod errors; mod errors;
mod ledger; mod ledger;
mod options;
mod price; mod price;
mod transaction; mod transaction;
mod value; mod value;

28
src/core/options.rs Normal file
View File

@ -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(),
),
],
}
}
}

View File

@ -3,7 +3,7 @@ use rust_decimal_macros::dec;
use std::collections::HashMap; use std::collections::HashMap;
use super::Amount; use super::Amount;
use crate::core::CoreError; use crate::core::{CoreError, Cost};
//////////////// ////////////////
// Directives // // Directives //
@ -32,7 +32,7 @@ pub enum TransactionFlag {
pub struct Posting { pub struct Posting {
account_id: u32, account_id: u32,
amount: Amount, amount: Amount,
cost: Option<Amount>, cost: Option<Cost>,
price: Option<Amount>, price: Option<Amount>,
} }
@ -89,7 +89,7 @@ impl Transaction {
let mut amounts = HashMap::new(); let mut amounts = HashMap::new();
for posting in &self.postings { for posting in &self.postings {
let cost = if by_cost { let cost = if by_cost {
posting.cost.or(posting.price) posting.cost.clone().map(|c| c.into()).or(posting.price)
} else { } else {
None None
}; };
@ -147,13 +147,13 @@ impl Posting {
pub fn new( pub fn new(
account_id: u32, account_id: u32,
amount: Amount, amount: Amount,
cost: Option<Amount>, cost: Option<Cost>,
price: Option<Amount>, price: Option<Amount>,
) -> Result<Posting, CoreError> { ) -> Result<Posting, CoreError> {
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()); 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()); return Err("Price cannot be negative".into());
} }
Ok(Posting { account_id, amount, cost, price }) Ok(Posting { account_id, amount, cost, price })
@ -167,7 +167,7 @@ impl Posting {
&self.amount &self.amount
} }
pub fn get_cost(&self) -> &Option<Amount> { pub fn get_cost(&self) -> &Option<Cost> {
&self.cost &self.cost
} }
@ -184,6 +184,17 @@ impl Posting {
// mod tests { // mod tests {
// use super::*; // use super::*;
// #[test]
// fn tttt() {
// }
// }
// #[cfg(test)]
// mod tests {
// use super::*;
// #[test] // #[test]
// fn balanced_transaction_simple() { // fn balanced_transaction_simple() {
// let txn = Transaction::new( // let txn = Transaction::new(

View File

@ -2,12 +2,14 @@ use std::path::PathBuf;
use chrono::NaiveDate; use chrono::NaiveDate;
use crate::core::{RawAmount, UnitSymbol}; use crate::{core::{RawAmount, RawCost, UnitSymbol}, query::{PostingField, Query}};
#[derive(Debug)] #[derive(Debug)]
pub struct Directives { pub struct Directives {
pub includes: Vec<IncludeDirective>, pub includes: Vec<IncludeDirective>,
pub options: Vec<OptionsDirective>,
pub commodities: Vec<CommodityDirective>, pub commodities: Vec<CommodityDirective>,
pub prices: Vec<PriceDirective>,
pub transactions: Vec<TransactionDirective>, pub transactions: Vec<TransactionDirective>,
pub balances: Vec<BalanceDirective>, pub balances: Vec<BalanceDirective>,
} }
@ -21,6 +23,12 @@ pub struct IncludeDirective {
pub path: PathBuf, pub path: PathBuf,
} }
#[derive(Debug, Clone)]
pub struct OptionsDirective {
pub name: String,
pub value: Vec<String>,
}
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct CommodityDirective { pub struct CommodityDirective {
pub date: Option<NaiveDate>, pub date: Option<NaiveDate>,
@ -29,6 +37,13 @@ pub struct CommodityDirective {
pub precision: Option<u32>, pub precision: Option<u32>,
} }
#[derive(Debug, Clone)]
pub struct PriceDirective {
pub date: NaiveDate,
pub symbol: String,
pub amount: RawAmount,
}
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct TransactionDirective { pub struct TransactionDirective {
pub date: NaiveDate, pub date: NaiveDate,
@ -41,8 +56,10 @@ pub struct TransactionDirective {
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct BalanceDirective { pub struct BalanceDirective {
pub date: NaiveDate, // pub date: NaiveDate,
pub account: String,
// pub account: String,
pub query: Query<PostingField>,
pub amounts: Vec<RawAmount>, pub amounts: Vec<RawAmount>,
} }
@ -55,7 +72,7 @@ pub struct DirectivePosting {
pub date: Option<NaiveDate>, pub date: Option<NaiveDate>,
pub account: String, pub account: String,
pub amount: Option<RawAmount>, pub amount: Option<RawAmount>,
pub cost: Option<RawAmount>, pub cost: Option<RawCost>,
pub price: Option<RawAmount>, pub price: Option<RawAmount>,
} }
@ -67,7 +84,9 @@ impl Directives {
pub fn new() -> Self { pub fn new() -> Self {
Directives { Directives {
includes: Vec::new(), includes: Vec::new(),
options: Vec::new(),
commodities: Vec::new(), commodities: Vec::new(),
prices: Vec::new(),
transactions: Vec::new(), transactions: Vec::new(),
balances: Vec::new(), balances: Vec::new(),
} }
@ -78,5 +97,6 @@ impl Directives {
self.transactions.extend(other.transactions.clone()); self.transactions.extend(other.transactions.clone());
self.balances.extend(other.balances.clone()); self.balances.extend(other.balances.clone());
self.commodities.extend(other.commodities.clone()); self.commodities.extend(other.commodities.clone());
self.prices.extend(other.prices.clone());
} }
} }

View File

@ -4,8 +4,11 @@ use rust_decimal_macros::dec;
use crate::{ use crate::{
core::{ core::{
Account, Amount, CoreError, DataValue, Ledger, Posting, RawAmount, Transaction, TransactionFlag, Unit, UnitSymbol Account, Amount, CoreError, Cost, DataValue, Ledger, Posting, Price, RawAmount, RawCost, Transaction, TransactionFlag, Unit, UnitSymbol
}, query::{self, AccountField, ComparisonFunction, LogicalFunction, PostingField, Query, RegexFunction, TransactionField}, }, document::PriceDirective, query::{
self, AccountField, ComparisonFunction, LogicalFunction, PostingField, Query,
RegexFunction, TransactionField,
}
// queries::{ // queries::{
// self, // self,
// base::{self, DataValue}, // base::{self, DataValue},
@ -46,7 +49,7 @@ pub fn add_transaction(
None None
}; };
let cost = if let Some(c) = &p.cost { let cost = if let Some(c) = &p.cost {
Some(create_amount(ledger, c)?) Some(create_cost(ledger, c)?)
} else { } else {
None None
}; };
@ -72,55 +75,22 @@ pub fn add_transaction(
Ok(()) 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> { 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 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 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(); let t3 = Instant::now();
@ -136,9 +106,9 @@ pub fn check_balance2(ledger: &Ledger, balance: &BalanceDirective) -> Result<(),
} }
} }
if account_count == 0 { // if account_count == 0 {
return Err("No accounts match balance account".into()); // return Err("No accounts match balance account".into());
} // }
// let balance_account = ledger // let balance_account = ledger
// .get_account_by_name(&balance.account) // .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); let balance_value = balance_amount.value.round_dp(max_scale);
if value != balance_value { 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!( return Err(format!(
"Balance amount for \"{}\" on {} does not match. Expected {} but got {}", "Balance amount does not match. Expected {} but got {}",
balance.account, balance.date, balance_value, value balance_value, value
) )
.into()); .into());
} }
@ -261,7 +236,7 @@ pub fn check_balance2(ledger: &Ledger, balance: &BalanceDirective) -> Result<(),
struct IncompletePosting { struct IncompletePosting {
account_id: u32, account_id: u32,
amount: Option<Amount>, amount: Option<Amount>,
cost: Option<Amount>, cost: Option<Cost>,
price: Option<Amount>, price: Option<Amount>,
} }
@ -271,6 +246,13 @@ fn create_amount(ledger: &mut Ledger, amount: &RawAmount) -> Result<Amount, Core
Ok(Amount { value: amount.value, unit_id }) Ok(Amount { value: amount.value, unit_id })
} }
fn create_cost(ledger: &mut Ledger, cost: &RawCost) -> Result<Cost, CoreError> {
let amount = create_amount(ledger, &cost.amount)?;
Ok(Cost { amount, date: cost.date, label: cost.label.clone() })
}
fn get_or_create_unit( fn get_or_create_unit(
ledger: &mut Ledger, ledger: &mut Ledger,
unit_symbol: &str, unit_symbol: &str,
@ -306,11 +288,11 @@ fn complete_incomplete_postings(
complete_postings.push(Posting::new( complete_postings.push(Posting::new(
posting.account_id, posting.account_id,
amount, amount,
posting.cost, posting.cost.clone(),
posting.price, 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 = amount.at_opt_price(cost);
let converted_amount = ledger.round_amount(&converted_amount); let converted_amount = ledger.round_amount(&converted_amount);
*amounts.entry(converted_amount.unit_id).or_insert(dec!(0)) += converted_amount.value; *amounts.entry(converted_amount.unit_id).or_insert(dec!(0)) += converted_amount.value;

View File

@ -4,7 +4,7 @@ mod ledger;
pub use directives::*; pub use directives::*;
use ledger::{add_transaction, check_balance2}; 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}; use std::{path::Path, time::Instant};
#[derive(Debug)] #[derive(Debug)]
@ -34,6 +34,10 @@ impl Document {
ledger.add_unit(unit)?; ledger.add_unit(unit)?;
} }
for price in &self.directives.prices {
add_price(&mut ledger, price)?;
}
for transaction in &self.directives.transactions { for transaction in &self.directives.transactions {
add_transaction(&mut ledger, transaction)?; add_transaction(&mut ledger, transaction)?;
} }

View File

@ -14,6 +14,10 @@ pub fn amount(input: &str) -> IResult<&str, RawAmount> {
alt((suffix_amount, prefix_amount)).parse(input) alt((suffix_amount, prefix_amount)).parse(input)
} }
pub fn unit(input: &str) -> IResult<&str, &str> {
recognize(many1(none_of("0123456789,+-_()*/.{} \t"))).parse(input)
}
/////////////// ///////////////
// Private // // Private //
/////////////// ///////////////
@ -45,10 +49,6 @@ fn suffix_amount(input: &str) -> IResult<&str, RawAmount> {
.parse(input) .parse(input)
} }
fn unit(input: &str) -> IResult<&str, &str> {
recognize(many1(none_of("0123456789,+-_()*/.{} \t"))).parse(input)
}
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;

View File

@ -1,13 +1,6 @@
use chrono::NaiveDate; use chrono::NaiveDate;
use nom::{ use nom::{
branch::alt, 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}
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,
}; };
use rust_decimal::Decimal; 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> { 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> { pub fn parse_iso_date(input: &str) -> IResult<&str, NaiveDate> {
@ -99,6 +93,11 @@ mod tests {
use super::*; use super::*;
use rust_decimal_macros::dec; use rust_decimal_macros::dec;
#[test]
fn parse_number_int() {
assert_eq!(number_int("1").unwrap().1, "1");
}
#[test] #[test]
fn parse_decimal_good() { fn parse_decimal_good() {
assert_eq!(decimal("1").unwrap().1, dec!(1)); assert_eq!(decimal("1").unwrap().1, dec!(1));

View File

@ -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<BaseDirective, BalanceDirective> {
// 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::<PostingField>(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);
}
}

View File

@ -3,7 +3,7 @@ use std::path::PathBuf;
use nom::{ use nom::{
bytes::complete::{is_not, tag}, bytes::complete::{is_not, tag},
character::complete::space1, character::complete::space1,
combinator::{opt, rest}, combinator::rest,
error::{Error, ErrorKind}, error::{Error, ErrorKind},
sequence::{preceded, terminated, tuple}, sequence::{preceded, terminated, tuple},
IResult, Parser, IResult, Parser,
@ -11,11 +11,14 @@ use nom::{
use crate::{ use crate::{
core::UnitSymbol, core::UnitSymbol,
document::{BalanceDirective, CommodityDirective, IncludeDirective, TransactionDirective}, document::{
parser::amount, 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 // // Public //
@ -24,6 +27,7 @@ use super::{base_directive::BaseDirective, shared::account, transaction::transac
pub enum Directive { pub enum Directive {
Include(IncludeDirective), Include(IncludeDirective),
Commodity(CommodityDirective), Commodity(CommodityDirective),
Price(PriceDirective),
Balance(BalanceDirective), Balance(BalanceDirective),
Transaction(TransactionDirective), Transaction(TransactionDirective),
} }
@ -34,6 +38,9 @@ pub fn specific_directive<'a>(
match directive.directive_name.to_lowercase().as_str() { match directive.directive_name.to_lowercase().as_str() {
"txn" => transaction(directive).map(|(i, v)| (i, Directive::Transaction(v))), "txn" => transaction(directive).map(|(i, v)| (i, Directive::Transaction(v))),
// Assume transaction flag if length of one // Assume transaction flag if length of one
"p" | "price" => price_directive
.map(|v| Directive::Price(v))
.parse(directive),
n if (n.len() == 1) => transaction n if (n.len() == 1) => transaction
.map(|v| Directive::Transaction(v)) .map(|v| Directive::Transaction(v))
.parse(directive), .parse(directive),
@ -124,9 +131,9 @@ fn commodity_directive(directive: BaseDirective) -> IResult<BaseDirective, Commo
)) ))
} }
fn balance_directive(directive: BaseDirective) -> IResult<BaseDirective, BalanceDirective> { fn price_directive(directive: BaseDirective) -> IResult<BaseDirective, PriceDirective> {
let date = if let Some(d) = directive.date { let date = if let Some(date) = directive.date {
d date
} else { } else {
return Err(nom::Err::Failure(Error { return Err(nom::Err::Failure(Error {
input: directive, input: directive,
@ -134,8 +141,8 @@ fn balance_directive(directive: BaseDirective) -> IResult<BaseDirective, Balance
})); }));
}; };
let (_, (account, bal_amount)) = if let Ok(v) = let (_, (symbol, amount)) = if let Ok(v) =
tuple((account, opt(preceded(space1, amount)))).parse(directive.lines.get(0).unwrap_or(&"")) tuple((unit, preceded(space1, amount))).parse(directive.lines.get(0).unwrap_or(&""))
{ {
v v
} else { } else {
@ -144,44 +151,9 @@ fn balance_directive(directive: BaseDirective) -> IResult<BaseDirective, Balance
code: ErrorKind::Eof, code: ErrorKind::Eof,
})); }));
}; };
let amounts = if let Some(a) = bal_amount {
vec![a]
} else {
let mut amounts = Vec::with_capacity(directive.lines.len());
for &line in directive.lines.get(1..).unwrap_or(&[]) {
let bal_amount = if let Ok(a) = amount(line) {
a
} else {
return Err(nom::Err::Failure(Error {
input: directive,
code: ErrorKind::Fail,
}));
};
amounts.push(bal_amount.1);
}
amounts
};
Ok(( Ok((
directive, directive,
BalanceDirective { date, account: account.to_string(), amounts }, PriceDirective { date, symbol: symbol.into(), amount },
)) ))
} }
#[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: "txn",
lines: vec!["Account", "$10", "20 UNIT"],
};
let balance = balance_directive(directive).unwrap();
println!("{:?}", balance);
}
}

View File

@ -1,5 +1,6 @@
mod base_directive; mod base_directive;
mod directives; mod directives;
mod balance;
mod transaction; mod transaction;
mod shared; mod shared;
@ -35,6 +36,7 @@ pub fn parse_directives(input: &str) -> Result<Directives, CoreError> {
match parsed_directive { match parsed_directive {
Directive::Include(d) => directives.includes.push(d), Directive::Include(d) => directives.includes.push(d),
Directive::Balance(d) => directives.balances.push(d), Directive::Balance(d) => directives.balances.push(d),
Directive::Price(d) => directives.prices.push(d),
Directive::Transaction(d) => directives.transactions.push(d), Directive::Transaction(d) => directives.transactions.push(d),
Directive::Commodity(d) => directives.commodities.push(d), Directive::Commodity(d) => directives.commodities.push(d),
} }

View File

@ -1,17 +1,12 @@
use chrono::NaiveDate;
use nom::{ use nom::{
branch::alt, 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}
bytes::complete::{is_not, tag},
character::complete::space1,
combinator::{eof, opt, rest},
error::{Error, ErrorKind},
sequence::{delimited, preceded, terminated, tuple},
Err, IResult, Parser,
}; };
use crate::{ use crate::{
core::RawAmount, core::{RawAmount, RawCost},
document::{DirectivePosting, TransactionDirective}, document::{DirectivePosting, TransactionDirective},
parser::amount, parser::{amount, quoted_string, ws},
}; };
use super::{ use super::{
@ -104,8 +99,10 @@ fn posting(input: &str) -> IResult<&str, DirectivePosting> {
account, account,
opt(tuple(( opt(tuple((
preceded(space1, amount), preceded(space1, amount),
permutation((
opt(preceded(space1, parse_cost)), opt(preceded(space1, parse_cost)),
opt(preceded(space1, parse_price)), opt(preceded(space1, parse_price)),
))
))), ))),
eof, eof,
)) ))
@ -115,18 +112,22 @@ fn posting(input: &str) -> IResult<&str, DirectivePosting> {
let mut price = None; let mut price = None;
if let Some(v) = value { if let Some(v) = value {
amount = Some(v.0); amount = Some(v.0);
if let Some(c) = v.1 { if let Some(c) = v.1.0 {
if c.1 { if c.1 {
cost = Some(RawAmount { cost = Some(RawCost {
value: c.0.value / amount.as_ref().unwrap().value.abs(), amount: RawAmount {
unit_symbol: c.0.unit_symbol, value: c.0.amount.value / amount.as_ref().unwrap().value.abs(),
is_unit_prefix: c.0.is_unit_prefix, 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 { } else {
cost = Some(c.0); cost = Some(c.0);
} }
} }
if let Some(p) = v.2 { if let Some(p) = v.1.1 {
if p.1 { if p.1 {
price = Some(RawAmount { price = Some(RawAmount {
value: p.0.value / amount.as_ref().unwrap().value.abs(), value: p.0.value / amount.as_ref().unwrap().value.abs(),
@ -149,10 +150,49 @@ fn posting(input: &str) -> IResult<&str, DirectivePosting> {
.parse(input) .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(( alt((
delimited(tag("{"), amount, tag("}")).map(|amount| (amount, false)), delimited(tag("{{"), cost_internal, tag("}}")).map(|amount| (amount, true)),
delimited(tag("{{"), amount, tag("}}")).map(|amount| (amount, true)), delimited(tag("{"), cost_internal, tag("}")).map(|amount| (amount, false)),
)) ))
.parse(input) .parse(input)
} }
@ -176,6 +216,13 @@ mod tests {
use super::*; use super::*;
// #[test]
// fn aaa() {
// let cost = parse_cost_internal("$10");
// println!("{:?}", cost);
// assert!(false);
// }
#[test] #[test]
fn test_parse_payee_narration() { fn test_parse_payee_narration() {
assert_eq!(payee_narration("").unwrap().1, (None, None)); assert_eq!(payee_narration("").unwrap().1, (None, None));
@ -214,10 +261,14 @@ mod tests {
unit_symbol: "SHARE".into(), unit_symbol: "SHARE".into(),
is_unit_prefix: false is_unit_prefix: false
}), }),
cost: Some(RawAmount { cost: Some(RawCost {
amount: RawAmount {
value: dec!(100), value: dec!(100),
unit_symbol: "$".into(), unit_symbol: "$".into(),
is_unit_prefix: true is_unit_prefix: true
},
date: None,
label: None
}), }),
price: None, price: None,
} }
@ -232,10 +283,14 @@ mod tests {
unit_symbol: "SHARE".into(), unit_symbol: "SHARE".into(),
is_unit_prefix: false is_unit_prefix: false
}), }),
cost: Some(RawAmount { cost: Some(RawCost {
amount: RawAmount {
value: dec!(100), value: dec!(100),
unit_symbol: "USD".into(), unit_symbol: "USD".into(),
is_unit_prefix: false is_unit_prefix: false
},
date: None,
label: None
}), }),
price: None, price: None,
} }
@ -294,10 +349,14 @@ mod tests {
unit_symbol: "SHARE".into(), unit_symbol: "SHARE".into(),
is_unit_prefix: false is_unit_prefix: false
}), }),
cost: Some(RawAmount { cost: Some(RawCost {
amount: RawAmount {
value: dec!(100), value: dec!(100),
unit_symbol: "$".into(), unit_symbol: "$".into(),
is_unit_prefix: true is_unit_prefix: true
},
date: None,
label: None
}), }),
price: Some(RawAmount { price: Some(RawAmount {
value: dec!(110), value: dec!(110),
@ -318,10 +377,14 @@ mod tests {
unit_symbol: "SHARE".into(), unit_symbol: "SHARE".into(),
is_unit_prefix: false is_unit_prefix: false
}), }),
cost: Some(RawAmount { cost: Some(RawCost {
amount: RawAmount {
value: dec!(100), value: dec!(100),
unit_symbol: "USD".into(), unit_symbol: "USD".into(),
is_unit_prefix: false is_unit_prefix: false
},
date: None,
label: None
}), }),
price: Some(RawAmount { price: Some(RawAmount {
value: dec!(110), value: dec!(110),
@ -342,10 +405,14 @@ mod tests {
unit_symbol: "SHARE".into(), unit_symbol: "SHARE".into(),
is_unit_prefix: false is_unit_prefix: false
}), }),
cost: Some(RawAmount { cost: Some(RawCost {
amount: RawAmount {
value: dec!(100), value: dec!(100),
unit_symbol: "USD".into(), unit_symbol: "USD".into(),
is_unit_prefix: false is_unit_prefix: false
},
date: None,
label: None
}), }),
price: Some(RawAmount { price: Some(RawAmount {
value: dec!(110), value: dec!(110),
@ -370,10 +437,14 @@ mod tests {
unit_symbol: "SHARE".into(), unit_symbol: "SHARE".into(),
is_unit_prefix: false is_unit_prefix: false
}), }),
cost: Some(RawAmount { cost: Some(RawCost {
amount: RawAmount {
value: dec!(100), value: dec!(100),
unit_symbol: "$".into(), unit_symbol: "$".into(),
is_unit_prefix: true is_unit_prefix: true
},
date: None,
label: None
}), }),
price: Some(RawAmount { price: Some(RawAmount {
value: dec!(110), value: dec!(110),

View File

@ -1,6 +1,6 @@
use std::fmt::Debug; use std::fmt::Debug;
use crate::query::{AccountField, PostingField, TransactionField}; use crate::query::{AccountField, PostingField, PostingRelated, TransactionField};
pub trait ParseField: Debug + Sized + Clone { pub trait ParseField: Debug + Sized + Clone {
fn parse(input: &str) -> Option<Self>; fn parse(input: &str) -> Option<Self>;
@ -18,6 +18,15 @@ impl ParseField for TransactionField {
} }
} }
impl ParseField for PostingRelated {
fn parse(input: &str) -> Option<Self> {
match input.to_lowercase().as_str() {
"account_name" => Some(PostingRelated::AccountName),
_ => None,
}
}
}
impl ParseField for AccountField { impl ParseField for AccountField {
fn parse(input: &str) -> Option<Self> { fn parse(input: &str) -> Option<Self> {
match input.to_lowercase().as_str() { match input.to_lowercase().as_str() {
@ -41,6 +50,7 @@ impl ParseField for PostingField {
["cost"] => Some(PostingField::Cost), ["cost"] => Some(PostingField::Cost),
["price"] => Some(PostingField::Price), ["price"] => Some(PostingField::Price),
["transaction", t] => TransactionField::parse(t).map(|v| PostingField::Transaction(v)), ["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)), ["account", t] => AccountField::parse(t).map(|v| PostingField::Account(v)),
_ => None, _ => None,
} }

View File

@ -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<Field>> { fn function_regex<'a, Field: ParseField>(input: &'a str) -> IResult<&'a str, Query<Field>> {
let (new_input, result) = tuple((factor, tag("~"), ws(quoted_string))) let (new_input, result) = tuple((factor, alt((tag("~"), (tag("!~")))), ws(quoted_string)))
.map(|(left, _, right)| RegexFunction::new(left, right, true)) // TODO: case sensitive? .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)?; .parse(input)?;
match result { 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))), 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> { fn field<'a, Field: ParseField>(input: &str) -> IResult<&str, Field> {
input input
.split_at_position1_complete( .split_at_position1_complete(
|item| !item.is_alphanum() && item != '.', |item| !item.is_alphanum() && item != '.' && item != '_',
ErrorKind::AlphaNumeric, ErrorKind::AlphaNumeric,
) )
.and_then(|v| { .and_then(|v| {

View File

@ -149,7 +149,10 @@ impl<Field: Clone + Debug> Function<Field> for SubAccountFunction<Field> {
impl<Field: Clone + Debug> RegexFunction<Field> { impl<Field: Clone + Debug> RegexFunction<Field> {
pub fn new(left: Query<Field>, regex: &str, case_insensitive: bool) -> Result<Self, CoreError> { pub fn new(left: Query<Field>, regex: &str, case_insensitive: bool) -> Result<Self, CoreError> {
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 }) Ok(RegexFunction { left, regex })
} }
} }
@ -158,11 +161,21 @@ impl<Field: Clone + Debug> Function<Field> for RegexFunction<Field> {
fn evaluate(&self, context: &dyn Data<Field>) -> Result<DataValue, CoreError> { fn evaluate(&self, context: &dyn Data<Field>) -> Result<DataValue, CoreError> {
let left = self.left.evaluate(context)?; let left = self.left.evaluate(context)?;
if let DataValue::String(left) = left { match left {
Ok(DataValue::Boolean(self.regex.is_match(left.as_ref()))) DataValue::String(left) => Ok(DataValue::Boolean(self.regex.is_match(left.as_ref()))),
} else { // TODO: better string matching?
Err("Cannot use REGEX operation on non string types".into()) 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())
// }
} }
} }

View File

@ -2,7 +2,7 @@ use super::{Data, Function, Query};
use crate::core::{CoreError, DataValue}; use crate::core::{CoreError, DataValue};
use std::fmt::Debug; use std::fmt::Debug;
#[derive(PartialEq, Debug, Clone)] #[derive(PartialEq, Debug, Copy, Clone)]
pub enum LogicalOperator { pub enum LogicalOperator {
AND, AND,
OR, OR,
@ -38,6 +38,18 @@ impl<Field: Clone + Debug> LogicalFunction<Field> {
pub fn new_op(op: LogicalOperator, left: Query<Field>, right: Query<Field>) -> Self { pub fn new_op(op: LogicalOperator, left: Query<Field>, right: Query<Field>) -> Self {
LogicalFunction { op, left, right } LogicalFunction { op, left, right }
} }
pub fn new_from_vec(
op: LogicalOperator,
queries: impl IntoIterator<Item = Query<Field>>,
) -> Option<Query<Field>> {
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<Field: Clone + Debug> Function<Field> for LogicalFunction<Field> { impl<Field: Clone + Debug> Function<Field> for LogicalFunction<Field> {

View File

@ -8,9 +8,15 @@ pub enum AccountField {
CloseDate, CloseDate,
} }
#[derive(Debug, Clone)]
pub enum PostingRelated {
AccountName,
}
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub enum PostingField { pub enum PostingField {
Transaction(TransactionField), Transaction(TransactionField),
Related(PostingRelated),
Account(AccountField), Account(AccountField),
Amount, Amount,
Cost, Cost,
@ -37,6 +43,24 @@ impl<'a> Data<PostingField> for PostingData<'a> {
PostingField::Transaction(transaction_field) => { PostingField::Transaction(transaction_field) => {
get_transaction_value(transaction_field, &self.parent_transaction) 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) => { PostingField::Account(account_field) => {
let account = self let account = self
.ledger .ledger