many changes with balance, restructure, etc.
This commit is contained in:
parent
94bffebd8f
commit
c163d4f8d3
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@ -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));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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
28
src/core/options.rs
Normal 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(),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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(
|
||||||
|
|||||||
@ -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());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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;
|
||||||
|
|||||||
@ -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)?;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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::*;
|
||||||
|
|||||||
@ -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));
|
||||||
|
|||||||
153
src/parser/document/balance.rs
Normal file
153
src/parser/document/balance.rs
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@ -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),
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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),
|
||||||
opt(preceded(space1, parse_cost)),
|
permutation((
|
||||||
opt(preceded(space1, parse_price)),
|
opt(preceded(space1, parse_cost)),
|
||||||
|
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 {
|
||||||
value: dec!(100),
|
amount: RawAmount {
|
||||||
unit_symbol: "$".into(),
|
value: dec!(100),
|
||||||
is_unit_prefix: true
|
unit_symbol: "$".into(),
|
||||||
|
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 {
|
||||||
value: dec!(100),
|
amount: RawAmount {
|
||||||
unit_symbol: "USD".into(),
|
value: dec!(100),
|
||||||
is_unit_prefix: false
|
unit_symbol: "USD".into(),
|
||||||
|
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 {
|
||||||
value: dec!(100),
|
amount: RawAmount {
|
||||||
unit_symbol: "$".into(),
|
value: dec!(100),
|
||||||
is_unit_prefix: true
|
unit_symbol: "$".into(),
|
||||||
|
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 {
|
||||||
value: dec!(100),
|
amount: RawAmount {
|
||||||
unit_symbol: "USD".into(),
|
value: dec!(100),
|
||||||
is_unit_prefix: false
|
unit_symbol: "USD".into(),
|
||||||
|
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 {
|
||||||
value: dec!(100),
|
amount: RawAmount {
|
||||||
unit_symbol: "USD".into(),
|
value: dec!(100),
|
||||||
is_unit_prefix: false
|
unit_symbol: "USD".into(),
|
||||||
|
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 {
|
||||||
value: dec!(100),
|
amount: RawAmount {
|
||||||
unit_symbol: "$".into(),
|
value: dec!(100),
|
||||||
is_unit_prefix: true
|
unit_symbol: "$".into(),
|
||||||
|
is_unit_prefix: true
|
||||||
|
},
|
||||||
|
date: None,
|
||||||
|
label: None
|
||||||
}),
|
}),
|
||||||
price: Some(RawAmount {
|
price: Some(RawAmount {
|
||||||
value: dec!(110),
|
value: dec!(110),
|
||||||
|
|||||||
@ -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,
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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| {
|
||||||
|
|||||||
@ -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())
|
||||||
|
// }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -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> {
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user