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 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<String>,
// }
#[derive(Debug, PartialEq, Clone)]
pub struct RawCost {
pub amount: RawAmount,
pub date: Option<NaiveDate>,
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)]
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_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<Account>,
units: Vec<Unit>,
prices: Vec<Price>,
@ -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<Account> {
&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));
}
}

View File

@ -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::*;
pub use 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 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<Amount>,
cost: Option<Cost>,
price: Option<Amount>,
}
@ -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<Amount>,
cost: Option<Cost>,
price: Option<Amount>,
) -> 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());
}
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<Amount> {
pub fn get_cost(&self) -> &Option<Cost> {
&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(

View File

@ -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<IncludeDirective>,
pub options: Vec<OptionsDirective>,
pub commodities: Vec<CommodityDirective>,
pub prices: Vec<PriceDirective>,
pub transactions: Vec<TransactionDirective>,
pub balances: Vec<BalanceDirective>,
}
@ -21,6 +23,12 @@ pub struct IncludeDirective {
pub path: PathBuf,
}
#[derive(Debug, Clone)]
pub struct OptionsDirective {
pub name: String,
pub value: Vec<String>,
}
#[derive(Debug, Clone)]
pub struct CommodityDirective {
pub date: Option<NaiveDate>,
@ -29,6 +37,13 @@ pub struct CommodityDirective {
pub precision: Option<u32>,
}
#[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<PostingField>,
pub amounts: Vec<RawAmount>,
}
@ -55,7 +72,7 @@ pub struct DirectivePosting {
pub date: Option<NaiveDate>,
pub account: String,
pub amount: Option<RawAmount>,
pub cost: Option<RawAmount>,
pub cost: Option<RawCost>,
pub price: Option<RawAmount>,
}
@ -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());
}
}

View File

@ -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<Amount>,
cost: Option<Amount>,
cost: Option<Cost>,
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 })
}
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(
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;

View File

@ -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)?;
}

View File

@ -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::*;

View File

@ -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));

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::{
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<BaseDirective, Commo
))
}
fn balance_directive(directive: BaseDirective) -> IResult<BaseDirective, BalanceDirective> {
let date = if let Some(d) = directive.date {
d
fn price_directive(directive: BaseDirective) -> IResult<BaseDirective, PriceDirective> {
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<BaseDirective, Balance
}));
};
let (_, (account, bal_amount)) = if let Ok(v) =
tuple((account, opt(preceded(space1, amount)))).parse(directive.lines.get(0).unwrap_or(&""))
let (_, (symbol, amount)) = if let Ok(v) =
tuple((unit, preceded(space1, amount))).parse(directive.lines.get(0).unwrap_or(&""))
{
v
} else {
@ -144,44 +151,9 @@ fn balance_directive(directive: BaseDirective) -> IResult<BaseDirective, Balance
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((
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 directives;
mod balance;
mod transaction;
mod shared;
@ -35,6 +36,7 @@ pub fn parse_directives(input: &str) -> Result<Directives, CoreError> {
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),
}

View File

@ -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),

View File

@ -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<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 {
fn parse(input: &str) -> Option<Self> {
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,
}

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>> {
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| {

View File

@ -149,7 +149,10 @@ impl<Field: Clone + Debug> Function<Field> for SubAccountFunction<Field> {
impl<Field: Clone + Debug> RegexFunction<Field> {
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 })
}
}
@ -158,11 +161,21 @@ impl<Field: Clone + Debug> Function<Field> for RegexFunction<Field> {
fn evaluate(&self, context: &dyn Data<Field>) -> Result<DataValue, CoreError> {
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())
// }
}
}

View File

@ -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<Field: Clone + Debug> LogicalFunction<Field> {
pub fn new_op(op: LogicalOperator, left: Query<Field>, right: Query<Field>) -> Self {
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> {

View File

@ -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<PostingField> 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