significant cleanup, restructuring, query parsing
This commit is contained in:
parent
38fcb89343
commit
94bffebd8f
@ -5,6 +5,13 @@ use rust_decimal_macros::dec;
|
|||||||
|
|
||||||
use super::{common::generate_id, CoreError};
|
use super::{common::generate_id, CoreError};
|
||||||
|
|
||||||
|
#[derive(Debug, PartialEq, Clone)]
|
||||||
|
pub struct RawAmount {
|
||||||
|
pub value: Decimal,
|
||||||
|
pub unit_symbol: String,
|
||||||
|
pub is_unit_prefix: bool,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, PartialEq, Clone, Copy)]
|
#[derive(Debug, PartialEq, Clone, Copy)]
|
||||||
pub struct Amount {
|
pub struct Amount {
|
||||||
pub value: Decimal,
|
pub value: Decimal,
|
||||||
@ -35,10 +42,7 @@ pub struct Unit {
|
|||||||
impl Amount {
|
impl Amount {
|
||||||
pub fn at_opt_price(&self, price: Option<Amount>) -> Amount {
|
pub fn at_opt_price(&self, price: Option<Amount>) -> Amount {
|
||||||
if let Some(p) = price {
|
if let Some(p) = price {
|
||||||
Amount {
|
Amount { value: self.value * p.value, unit_id: p.unit_id }
|
||||||
value: self.value * p.value,
|
|
||||||
unit_id: p.unit_id,
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
*self
|
*self
|
||||||
}
|
}
|
||||||
@ -89,7 +93,10 @@ pub fn combine_amounts(amounts: impl Iterator<Item = Amount>) -> Vec<Amount> {
|
|||||||
*output_amounts.entry(amount.unit_id).or_insert(dec!(0)) += amount.value;
|
*output_amounts.entry(amount.unit_id).or_insert(dec!(0)) += amount.value;
|
||||||
}
|
}
|
||||||
|
|
||||||
output_amounts.iter().map(|(&unit_id, &value)| Amount {value, unit_id}).collect()
|
output_amounts
|
||||||
|
.iter()
|
||||||
|
.map(|(&unit_id, &value)| Amount { value, unit_id })
|
||||||
|
.collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
impl PartialOrd for Amount {
|
impl PartialOrd for Amount {
|
||||||
@ -100,4 +107,4 @@ impl PartialOrd for Amount {
|
|||||||
self.value.partial_cmp(&other.value)
|
self.value.partial_cmp(&other.value)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -5,6 +5,7 @@ mod errors;
|
|||||||
mod ledger;
|
mod ledger;
|
||||||
mod price;
|
mod price;
|
||||||
mod transaction;
|
mod transaction;
|
||||||
|
mod value;
|
||||||
|
|
||||||
pub use account::*;
|
pub use account::*;
|
||||||
pub use amounts::*;
|
pub use amounts::*;
|
||||||
@ -12,3 +13,4 @@ pub use errors::*;
|
|||||||
pub use ledger::*;
|
pub use ledger::*;
|
||||||
pub use price::*;
|
pub use price::*;
|
||||||
pub use transaction::*;
|
pub use transaction::*;
|
||||||
|
pub use value::*;
|
||||||
@ -1,57 +1,29 @@
|
|||||||
use std::collections::HashMap;
|
|
||||||
|
|
||||||
use chrono::NaiveDate;
|
use chrono::NaiveDate;
|
||||||
use rust_decimal::{prelude::Zero, Decimal};
|
use rust_decimal::{prelude::Zero, Decimal};
|
||||||
|
use std::collections::HashMap;
|
||||||
use crate::core::{Amount, CoreError};
|
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub enum StringData<'a> {
|
pub enum StringData<'a> {
|
||||||
Owned(String),
|
Owned(String),
|
||||||
Reference(&'a str)
|
Reference(&'a str),
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq)]
|
#[derive(Debug, Clone, PartialEq)]
|
||||||
pub enum DataValue<'a> {
|
pub enum DataValue {
|
||||||
Null,
|
Null,
|
||||||
Integer(u32),
|
Integer(u32),
|
||||||
Decimal(Decimal),
|
Decimal(Decimal),
|
||||||
Boolean(bool),
|
Boolean(bool),
|
||||||
String(StringData<'a>),
|
String(StringData<'static>),
|
||||||
Date(NaiveDate),
|
Date(NaiveDate),
|
||||||
Amount(Amount),
|
List(Vec<DataValue>),
|
||||||
List(Vec<DataValue<'a>>),
|
Map(HashMap<&'static str, DataValue>),
|
||||||
Map(HashMap<&'static str, DataValue<'a>>),
|
// Amount(Amount),
|
||||||
}
|
}
|
||||||
|
|
||||||
pub enum Query<'a, T> {
|
/////////////////////
|
||||||
Field(T),
|
// Implementations //
|
||||||
Value(DataValue<'a>),
|
/////////////////////
|
||||||
Function(Box<dyn Function<T>>),
|
|
||||||
}
|
|
||||||
|
|
||||||
pub trait Data<T> {
|
|
||||||
fn get_field(&self, field: &T) -> Result<DataValue, CoreError>;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub trait Function<T> {
|
|
||||||
fn evaluate(&self, context: &dyn Data<T>) -> Result<DataValue, CoreError>;
|
|
||||||
}
|
|
||||||
|
|
||||||
// impl ConstantValue {
|
|
||||||
// pub fn to_bool(&self) -> bool {
|
|
||||||
// match self {
|
|
||||||
// ConstantValue::Integer(val) => !val.is_zero(),
|
|
||||||
// ConstantValue::Decimal(val) => !val.is_zero(),
|
|
||||||
// ConstantValue::Boolean(val) => *val,
|
|
||||||
// ConstantValue::String(val) => val.is_empty(),
|
|
||||||
// ConstantValue::Date(_) => true,
|
|
||||||
// ConstantValue::Amount(val) => !val.value.is_zero(),
|
|
||||||
// ConstantValue::List(list) => !list.is_empty(),
|
|
||||||
// ConstantValue::Map(map) => !map.is_empty(),
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
impl<'a> StringData<'a> {
|
impl<'a> StringData<'a> {
|
||||||
pub fn as_ref(&'a self) -> &'a str {
|
pub fn as_ref(&'a self) -> &'a str {
|
||||||
@ -60,6 +32,13 @@ impl<'a> StringData<'a> {
|
|||||||
StringData::Reference(val) => val,
|
StringData::Reference(val) => val,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn into_owned(self) -> StringData<'static> {
|
||||||
|
match self {
|
||||||
|
StringData::Owned(s) => StringData::Owned(s),
|
||||||
|
StringData::Reference(s) => StringData::Owned(s.to_string()),
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> PartialEq for StringData<'a> {
|
impl<'a> PartialEq for StringData<'a> {
|
||||||
@ -90,7 +69,7 @@ impl<'a> PartialOrd for StringData<'a> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> PartialOrd for DataValue<'a> {
|
impl PartialOrd for DataValue {
|
||||||
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
|
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
|
||||||
match (self, other) {
|
match (self, other) {
|
||||||
(DataValue::Null, DataValue::Null) => Some(std::cmp::Ordering::Equal),
|
(DataValue::Null, DataValue::Null) => Some(std::cmp::Ordering::Equal),
|
||||||
@ -99,7 +78,7 @@ impl<'a> PartialOrd for DataValue<'a> {
|
|||||||
(DataValue::Boolean(val1), DataValue::Boolean(val2)) => val1.partial_cmp(val2),
|
(DataValue::Boolean(val1), DataValue::Boolean(val2)) => val1.partial_cmp(val2),
|
||||||
(DataValue::String(val1), DataValue::String(val2)) => val1.partial_cmp(val2),
|
(DataValue::String(val1), DataValue::String(val2)) => val1.partial_cmp(val2),
|
||||||
(DataValue::Date(val1), DataValue::Date(val2)) => val1.partial_cmp(val2),
|
(DataValue::Date(val1), DataValue::Date(val2)) => val1.partial_cmp(val2),
|
||||||
(DataValue::Amount(val1), DataValue::Amount(val2)) => val1.partial_cmp(val2),
|
// (DataValue::Amount(val1), DataValue::Amount(val2)) => val1.partial_cmp(val2),
|
||||||
(DataValue::List(val1), DataValue::List(val2)) => val1.partial_cmp(val2),
|
(DataValue::List(val1), DataValue::List(val2)) => val1.partial_cmp(val2),
|
||||||
_ => None,
|
_ => None,
|
||||||
}
|
}
|
||||||
@ -118,50 +97,49 @@ impl<'a> From<String> for StringData<'a> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> From<u32> for DataValue<'a> {
|
impl From<u32> for DataValue {
|
||||||
fn from(value: u32) -> Self {
|
fn from(value: u32) -> Self {
|
||||||
DataValue::Integer(value)
|
DataValue::Integer(value)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> From<Decimal> for DataValue<'a> {
|
impl From<Decimal> for DataValue {
|
||||||
fn from(value: Decimal) -> Self {
|
fn from(value: Decimal) -> Self {
|
||||||
DataValue::Decimal(value)
|
DataValue::Decimal(value)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> From<bool> for DataValue<'a> {
|
impl From<bool> for DataValue {
|
||||||
fn from(value: bool) -> Self {
|
fn from(value: bool) -> Self {
|
||||||
DataValue::Boolean(value)
|
DataValue::Boolean(value)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> From<&'a str> for DataValue<'a> {
|
impl<'a> From<&'a str> for DataValue {
|
||||||
fn from(value: &'a str) -> Self {
|
fn from(value: &'a str) -> Self {
|
||||||
DataValue::String(value.into())
|
DataValue::String(StringData::from(value).into_owned())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl From<String> for DataValue {
|
||||||
impl<'a> From<String> for DataValue<'a> {
|
|
||||||
fn from(value: String) -> Self {
|
fn from(value: String) -> Self {
|
||||||
DataValue::String(value.into())
|
DataValue::String(value.into())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> From<NaiveDate> for DataValue<'a> {
|
impl From<NaiveDate> for DataValue {
|
||||||
fn from(value: NaiveDate) -> Self {
|
fn from(value: NaiveDate) -> Self {
|
||||||
DataValue::Date(value)
|
DataValue::Date(value)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> From<Amount> for DataValue<'a> {
|
// impl<'a> From<Amount> for DataValue<'a> {
|
||||||
fn from(value: Amount) -> Self {
|
// fn from(value: Amount) -> Self {
|
||||||
DataValue::Amount(value)
|
// DataValue::Amount(value)
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
|
|
||||||
impl<'a> From<DataValue<'a>> for bool {
|
impl From<DataValue> for bool {
|
||||||
fn from(value: DataValue) -> Self {
|
fn from(value: DataValue) -> Self {
|
||||||
match value {
|
match value {
|
||||||
DataValue::Null => false,
|
DataValue::Null => false,
|
||||||
@ -170,49 +148,9 @@ impl<'a> From<DataValue<'a>> for bool {
|
|||||||
DataValue::Boolean(val) => val,
|
DataValue::Boolean(val) => val,
|
||||||
DataValue::String(val) => val.as_ref().is_empty(),
|
DataValue::String(val) => val.as_ref().is_empty(),
|
||||||
DataValue::Date(_) => true,
|
DataValue::Date(_) => true,
|
||||||
DataValue::Amount(val) => !val.value.is_zero(),
|
// DataValue::Amount(val) => !val.value.is_zero(),
|
||||||
DataValue::List(list) => !list.is_empty(),
|
DataValue::List(list) => !list.is_empty(),
|
||||||
DataValue::Map(map) => !map.is_empty(),
|
DataValue::Map(map) => !map.is_empty(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a, T> Query<'a, T> {
|
|
||||||
pub fn evaluate(&self, context: &'a dyn Data<T>) -> Result<DataValue, CoreError> {
|
|
||||||
match self {
|
|
||||||
Query::Field(field) => context.get_field(field),
|
|
||||||
Query::Value(constant) => Ok(constant.clone()),
|
|
||||||
Query::Function(function) => function.evaluate(context),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn from_field(field: T) -> Self {
|
|
||||||
Query::Field(field)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn from_fn<F: Function<T> + Sized + 'static>(function: F) -> Self {
|
|
||||||
Query::Function(Box::new(function))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'a, T> From<DataValue<'a>> for Query<'a, T> {
|
|
||||||
fn from(constant: DataValue<'a>) -> Self {
|
|
||||||
Query::Value(constant)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// impl<T: Sized + 'static, Field: Sized> Function<Field> for T {
|
|
||||||
// fn to_value(self) -> Value<T> {
|
|
||||||
// Value::Function(Box::new(self))
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
// impl<Field, T: Function<Field>> T {
|
|
||||||
|
|
||||||
// }
|
|
||||||
|
|
||||||
// impl<Field, T: Function<Field> + Sized + 'static> From<T> for Value<Field> {
|
|
||||||
// fn from(function: T) -> Self {
|
|
||||||
// Value::Function(Box::new(function))
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
@ -1,9 +1,8 @@
|
|||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
|
||||||
use chrono::NaiveDate;
|
use chrono::NaiveDate;
|
||||||
use rust_decimal::Decimal;
|
|
||||||
|
|
||||||
use crate::core::UnitSymbol;
|
use crate::core::{RawAmount, UnitSymbol};
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct Directives {
|
pub struct Directives {
|
||||||
@ -44,7 +43,7 @@ pub struct TransactionDirective {
|
|||||||
pub struct BalanceDirective {
|
pub struct BalanceDirective {
|
||||||
pub date: NaiveDate,
|
pub date: NaiveDate,
|
||||||
pub account: String,
|
pub account: String,
|
||||||
pub amounts: Vec<DirectiveAmount>,
|
pub amounts: Vec<RawAmount>,
|
||||||
}
|
}
|
||||||
|
|
||||||
///////////////
|
///////////////
|
||||||
@ -55,16 +54,9 @@ pub struct BalanceDirective {
|
|||||||
pub struct DirectivePosting {
|
pub struct DirectivePosting {
|
||||||
pub date: Option<NaiveDate>,
|
pub date: Option<NaiveDate>,
|
||||||
pub account: String,
|
pub account: String,
|
||||||
pub amount: Option<DirectiveAmount>,
|
pub amount: Option<RawAmount>,
|
||||||
pub cost: Option<DirectiveAmount>,
|
pub cost: Option<RawAmount>,
|
||||||
pub price: Option<DirectiveAmount>,
|
pub price: Option<RawAmount>,
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, PartialEq, Clone)]
|
|
||||||
pub struct DirectiveAmount {
|
|
||||||
pub value: Decimal,
|
|
||||||
pub unit_symbol: String,
|
|
||||||
pub is_unit_prefix: bool,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/////////////////////
|
/////////////////////
|
||||||
@ -73,7 +65,12 @@ pub struct DirectiveAmount {
|
|||||||
|
|
||||||
impl Directives {
|
impl Directives {
|
||||||
pub fn new() -> Self {
|
pub fn new() -> Self {
|
||||||
Directives{includes: Vec::new(), commodities: Vec::new(), transactions: Vec::new(), balances: Vec::new()}
|
Directives {
|
||||||
|
includes: Vec::new(),
|
||||||
|
commodities: Vec::new(),
|
||||||
|
transactions: Vec::new(),
|
||||||
|
balances: Vec::new(),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn add_directives(&mut self, other: &Directives) {
|
pub fn add_directives(&mut self, other: &Directives) {
|
||||||
@ -82,4 +79,4 @@ impl Directives {
|
|||||||
self.balances.extend(other.balances.clone());
|
self.balances.extend(other.balances.clone());
|
||||||
self.commodities.extend(other.commodities.clone());
|
self.commodities.extend(other.commodities.clone());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -4,21 +4,21 @@ use rust_decimal_macros::dec;
|
|||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
core::{
|
core::{
|
||||||
Account, Amount, CoreError, Ledger, Posting, Transaction, TransactionFlag, Unit, UnitSymbol,
|
Account, Amount, CoreError, DataValue, Ledger, Posting, RawAmount, Transaction, TransactionFlag, Unit, UnitSymbol
|
||||||
},
|
}, query::{self, AccountField, ComparisonFunction, LogicalFunction, PostingField, Query, RegexFunction, TransactionField},
|
||||||
queries::{
|
// queries::{
|
||||||
self,
|
// self,
|
||||||
base::{self, DataValue},
|
// base::{self, DataValue},
|
||||||
functions::{
|
// functions::{
|
||||||
ComparisonFunction, LogicalFunction, RegexFunction, StringComparisonFunction,
|
// ComparisonFunction, LogicalFunction, RegexFunction, StringComparisonFunction,
|
||||||
SubAccountFunction,
|
// SubAccountFunction,
|
||||||
},
|
// },
|
||||||
transaction::{AccountField, PostingField, TransactionField},
|
// transaction::{AccountField, PostingField, TransactionField},
|
||||||
Query,
|
// Query,
|
||||||
},
|
// },
|
||||||
};
|
};
|
||||||
|
|
||||||
use super::{BalanceDirective, DirectiveAmount, TransactionDirective};
|
use super::{BalanceDirective, TransactionDirective};
|
||||||
|
|
||||||
pub fn add_transaction(
|
pub fn add_transaction(
|
||||||
ledger: &mut Ledger,
|
ledger: &mut Ledger,
|
||||||
@ -75,8 +75,8 @@ pub fn add_transaction(
|
|||||||
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(
|
let date_query = ComparisonFunction::new(
|
||||||
"<=",
|
"<=",
|
||||||
base::Query::from_field(PostingField::Transaction(TransactionField::Date)),
|
Query::from_field(PostingField::Transaction(TransactionField::Date)),
|
||||||
base::Query::from(DataValue::from(balance.date)),
|
Query::from(DataValue::from(balance.date)),
|
||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
// let account_fn = |str: &str| {
|
// let account_fn = |str: &str| {
|
||||||
@ -93,23 +93,34 @@ pub fn check_balance2(ledger: &Ledger, balance: &BalanceDirective) -> Result<(),
|
|||||||
// base::Query::from_field(PostingField::Account(AccountField::Name)),
|
// base::Query::from_field(PostingField::Account(AccountField::Name)),
|
||||||
// &account_regex,
|
// &account_regex,
|
||||||
// )?;
|
// )?;
|
||||||
let account_query = SubAccountFunction::new(
|
|
||||||
balance.account.clone().into(),
|
|
||||||
base::Query::from_field(PostingField::Account(AccountField::Name)),
|
// 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(
|
let total_query = LogicalFunction::new(
|
||||||
"and",
|
"and",
|
||||||
base::Query::from_fn(date_query),
|
Query::from_fn(date_query),
|
||||||
base::Query::from_fn(account_query),
|
Query::from_fn(account_query),
|
||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
let t2 = Instant::now();
|
let t2 = Instant::now();
|
||||||
|
|
||||||
let accounts = queries::balance3(&ledger, &base::Query::from_fn(total_query));
|
let accounts = query::balance(&ledger, Some(&Query::from_fn(total_query)), None);
|
||||||
|
|
||||||
let t3 = Instant::now();
|
let t3 = Instant::now();
|
||||||
|
|
||||||
@ -175,76 +186,77 @@ pub fn check_balance2(ledger: &Ledger, balance: &BalanceDirective) -> Result<(),
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn check_balance(ledger: &Ledger, balance: &BalanceDirective) -> Result<(), CoreError> {
|
// pub fn check_balance(ledger: &Ledger, balance: &BalanceDirective) -> Result<(), CoreError> {
|
||||||
let accounts = queries::balance(&ledger, &[Query::EndDate(balance.date)]);
|
// let accounts = query::balance(&ledger, &[Query::EndDate(balance.date)]);
|
||||||
// let accounts = queries::balance2(&ledger, balance.date);
|
|
||||||
|
|
||||||
let accounts = accounts.iter().filter(|(&account_id, val)| {
|
// // let accounts = queries::balance2(&ledger, balance.date);
|
||||||
let account = ledger.get_account(account_id).unwrap();
|
|
||||||
account.is_under_account(&balance.account)
|
|
||||||
});
|
|
||||||
|
|
||||||
if accounts.clone().count() == 0 {}
|
// let accounts = accounts.iter().filter(|(&account_id, val)| {
|
||||||
|
// let account = ledger.get_account(account_id).unwrap();
|
||||||
|
// account.is_under_account(&balance.account)
|
||||||
|
// });
|
||||||
|
|
||||||
let mut total_amounts = HashMap::new();
|
// if accounts.clone().count() == 0 {}
|
||||||
let mut account_count = 0;
|
|
||||||
|
|
||||||
for (_, amounts) in accounts {
|
// let mut total_amounts = HashMap::new();
|
||||||
account_count += 1;
|
// let mut account_count = 0;
|
||||||
for amount in amounts {
|
|
||||||
*total_amounts.entry(amount.unit_id).or_insert(dec!(0)) += amount.value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if account_count == 0 {
|
// for (_, amounts) in accounts {
|
||||||
return Err("No accounts match balance account".into());
|
// account_count += 1;
|
||||||
}
|
// for amount in amounts {
|
||||||
|
// *total_amounts.entry(amount.unit_id).or_insert(dec!(0)) += amount.value;
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
// let balance_account = ledger
|
// if account_count == 0 {
|
||||||
// .get_account_by_name(&balance.account)
|
// return Err("No accounts match balance account".into());
|
||||||
// .ok_or("Account not found")?;
|
// }
|
||||||
|
|
||||||
// let amounts = accounts
|
// // let balance_account = ledger
|
||||||
// .get(&balance_account.get_id())
|
// // .get_account_by_name(&balance.account)
|
||||||
// .map(|v| v.as_slice())
|
// // .ok_or("Account not found")?;
|
||||||
// .unwrap_or(&[]);
|
|
||||||
|
|
||||||
// if amounts.len() > balance.amounts.len() {
|
// // let amounts = accounts
|
||||||
// return Err("".into());
|
// // .get(&balance_account.get_id())
|
||||||
// } else if amounts.len() < balance.amounts.len() {
|
// // .map(|v| v.as_slice())
|
||||||
// return Err("".into());
|
// // .unwrap_or(&[]);
|
||||||
// }
|
|
||||||
|
|
||||||
for balance_amount in &balance.amounts {
|
// // if amounts.len() > balance.amounts.len() {
|
||||||
let unit = ledger
|
// // return Err("".into());
|
||||||
.get_unit_by_symbol(&balance_amount.unit_symbol)
|
// // } else if amounts.len() < balance.amounts.len() {
|
||||||
.ok_or("Unit not found")?;
|
// // return Err("".into());
|
||||||
let value = total_amounts
|
// // }
|
||||||
.get(&unit.get_id())
|
|
||||||
.map(|v| *v)
|
|
||||||
.unwrap_or(dec!(0));
|
|
||||||
|
|
||||||
// let value = amounts
|
// for balance_amount in &balance.amounts {
|
||||||
// .iter()
|
// let unit = ledger
|
||||||
// .find(|a| a.unit_id == unit.get_id())
|
// .get_unit_by_symbol(&balance_amount.unit_symbol)
|
||||||
// .map(|a| a.value)
|
// .ok_or("Unit not found")?;
|
||||||
// .unwrap_or(dec!(0));
|
// let value = total_amounts
|
||||||
let max_scale = max(value.scale(), balance_amount.value.scale());
|
// .get(&unit.get_id())
|
||||||
|
// .map(|v| *v)
|
||||||
|
// .unwrap_or(dec!(0));
|
||||||
|
|
||||||
let value = value.round_dp(max_scale);
|
// // let value = amounts
|
||||||
let balance_value = balance_amount.value.round_dp(max_scale);
|
// // .iter()
|
||||||
|
// // .find(|a| a.unit_id == unit.get_id())
|
||||||
|
// // .map(|a| a.value)
|
||||||
|
// // .unwrap_or(dec!(0));
|
||||||
|
// let max_scale = max(value.scale(), balance_amount.value.scale());
|
||||||
|
|
||||||
if value != balance_value {
|
// let value = value.round_dp(max_scale);
|
||||||
return Err(format!(
|
// let balance_value = balance_amount.value.round_dp(max_scale);
|
||||||
"Balance amount for \"{}\" on {} does not match. Expected {} but got {}",
|
|
||||||
balance.account, balance.date, balance_value, value
|
|
||||||
)
|
|
||||||
.into());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
// if value != balance_value {
|
||||||
}
|
// return Err(format!(
|
||||||
|
// "Balance amount for \"{}\" on {} does not match. Expected {} but got {}",
|
||||||
|
// balance.account, balance.date, balance_value, value
|
||||||
|
// )
|
||||||
|
// .into());
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
// Ok(())
|
||||||
|
// }
|
||||||
|
|
||||||
struct IncompletePosting {
|
struct IncompletePosting {
|
||||||
account_id: u32,
|
account_id: u32,
|
||||||
@ -253,7 +265,7 @@ struct IncompletePosting {
|
|||||||
price: Option<Amount>,
|
price: Option<Amount>,
|
||||||
}
|
}
|
||||||
|
|
||||||
fn create_amount(ledger: &mut Ledger, amount: &DirectiveAmount) -> Result<Amount, CoreError> {
|
fn create_amount(ledger: &mut Ledger, amount: &RawAmount) -> Result<Amount, CoreError> {
|
||||||
let unit_id = get_or_create_unit(ledger, &amount.unit_symbol, amount.is_unit_prefix)?;
|
let unit_id = get_or_create_unit(ledger, &amount.unit_symbol, amount.is_unit_prefix)?;
|
||||||
|
|
||||||
Ok(Amount { value: amount.value, unit_id })
|
Ok(Amount { value: amount.value, unit_id })
|
||||||
|
|||||||
@ -1,12 +1,10 @@
|
|||||||
mod directives;
|
mod directives;
|
||||||
mod ledger;
|
mod ledger;
|
||||||
mod parser;
|
|
||||||
|
|
||||||
pub use directives::*;
|
pub use directives::*;
|
||||||
use ledger::{add_transaction, check_balance2};
|
use ledger::{add_transaction, check_balance2};
|
||||||
use parser::parse_directives;
|
|
||||||
|
|
||||||
use crate::core::{CoreError, Ledger, Unit};
|
use crate::{core::{CoreError, Ledger, Unit}, parser::parse_directives};
|
||||||
use std::{path::Path, time::Instant};
|
use std::{path::Path, time::Instant};
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
|
|||||||
@ -1,156 +0,0 @@
|
|||||||
use nom::{
|
|
||||||
branch::alt,
|
|
||||||
character::complete::{char, none_of, one_of, space0},
|
|
||||||
combinator::{opt, recognize},
|
|
||||||
error::{Error, ErrorKind},
|
|
||||||
multi::{many0, many1},
|
|
||||||
sequence::{preceded, terminated, tuple},
|
|
||||||
Err, IResult, InputTakeAtPosition, Parser,
|
|
||||||
};
|
|
||||||
use rust_decimal::Decimal;
|
|
||||||
use rust_decimal_macros::dec;
|
|
||||||
|
|
||||||
use crate::document::DirectiveAmount;
|
|
||||||
|
|
||||||
pub fn account(input: &str) -> IResult<&str, &str> {
|
|
||||||
input.split_at_position1_complete(|item| item == ' ' || item == '\t', ErrorKind::AlphaNumeric)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn amount(input: &str) -> IResult<&str, DirectiveAmount> {
|
|
||||||
alt((suffix_amount, prefix_amount)).parse(input)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn decimal(input: &str) -> IResult<&str, Decimal> {
|
|
||||||
let (new_input, decimal_str) = recognize(tuple((
|
|
||||||
opt(one_of("+-")),
|
|
||||||
opt(number_int),
|
|
||||||
opt(char('.')),
|
|
||||||
opt(number_int),
|
|
||||||
)))
|
|
||||||
.parse(input)?;
|
|
||||||
|
|
||||||
if decimal_str.contains(',') {
|
|
||||||
match Decimal::from_str_exact(&decimal_str.replace(",", "")) {
|
|
||||||
Ok(decimal) => Ok((new_input, decimal)),
|
|
||||||
Err(_) => Err(Err::Error(Error::new(input, ErrorKind::Eof))),
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
match Decimal::from_str_exact(decimal_str) {
|
|
||||||
Ok(decimal) => Ok((new_input, decimal)),
|
|
||||||
Err(_) => Err(Err::Error(Error::new(input, ErrorKind::Eof))),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
///////////////
|
|
||||||
// Private //
|
|
||||||
///////////////
|
|
||||||
|
|
||||||
fn prefix_amount(input: &str) -> IResult<&str, DirectiveAmount> {
|
|
||||||
tuple((
|
|
||||||
opt(one_of("+-")),
|
|
||||||
unit,
|
|
||||||
preceded(space0, decimal),
|
|
||||||
))
|
|
||||||
.map(|(sign, unit_symbol, mut value)| {
|
|
||||||
if let Some(s) = sign {
|
|
||||||
if s == '-' {
|
|
||||||
value = value * dec!(-1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
DirectiveAmount {
|
|
||||||
value,
|
|
||||||
unit_symbol: unit_symbol.to_string(),
|
|
||||||
is_unit_prefix: true,
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.parse(input)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn suffix_amount(input: &str) -> IResult<&str, DirectiveAmount> {
|
|
||||||
tuple((
|
|
||||||
decimal,
|
|
||||||
preceded(space0, unit),
|
|
||||||
))
|
|
||||||
.map(|(value, unit_symbol)| DirectiveAmount {
|
|
||||||
value,
|
|
||||||
unit_symbol: unit_symbol.to_string(),
|
|
||||||
is_unit_prefix: false,
|
|
||||||
})
|
|
||||||
.parse(input)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn unit(input: &str) -> IResult<&str, &str> {
|
|
||||||
recognize(many1(none_of("0123456789,+-_()*/.{} \t"))).parse(input)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn number_int(input: &str) -> IResult<&str, &str> {
|
|
||||||
recognize(many1(terminated(one_of("0123456789"), many0(one_of("_,")))))(input)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use rust_decimal_macros::dec;
|
|
||||||
|
|
||||||
use super::*;
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn parse_decimal_good() {
|
|
||||||
assert_eq!(decimal("1").unwrap().1, dec!(1));
|
|
||||||
assert_eq!(decimal("+10").unwrap().1, dec!(10));
|
|
||||||
assert_eq!(decimal("-10").unwrap().1, dec!(-10));
|
|
||||||
assert_eq!(decimal("10.1").unwrap().1, dec!(10.1));
|
|
||||||
assert_eq!(decimal("100_000.01").unwrap().1, dec!(100000.01));
|
|
||||||
assert_eq!(decimal(".1").unwrap().1, dec!(0.1));
|
|
||||||
assert_eq!(decimal("-.1").unwrap().1, dec!(-0.1));
|
|
||||||
assert_eq!(decimal("2.").unwrap().1, dec!(2.));
|
|
||||||
assert_eq!(decimal("1,000").unwrap().1, dec!(1000));
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn amount_good() {
|
|
||||||
assert_eq!(
|
|
||||||
amount("$10").unwrap().1,
|
|
||||||
DirectiveAmount {
|
|
||||||
value: dec!(10),
|
|
||||||
unit_symbol: "$".into(),
|
|
||||||
is_unit_prefix: true
|
|
||||||
}
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
amount("10 USD").unwrap().1,
|
|
||||||
DirectiveAmount {
|
|
||||||
value: dec!(10),
|
|
||||||
unit_symbol: "USD".into(),
|
|
||||||
is_unit_prefix: false
|
|
||||||
}
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
amount("-$10.01").unwrap().1,
|
|
||||||
DirectiveAmount {
|
|
||||||
value: dec!(-10.01),
|
|
||||||
unit_symbol: "$".into(),
|
|
||||||
is_unit_prefix: true
|
|
||||||
}
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
amount("-10€").unwrap().1,
|
|
||||||
DirectiveAmount {
|
|
||||||
value: dec!(-10),
|
|
||||||
unit_symbol: "€".into(),
|
|
||||||
is_unit_prefix: false
|
|
||||||
}
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
amount("-€10").unwrap().1,
|
|
||||||
DirectiveAmount {
|
|
||||||
value: dec!(-10),
|
|
||||||
unit_symbol: "€".into(),
|
|
||||||
is_unit_prefix: true
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@ -38,12 +38,14 @@
|
|||||||
// struct Quantity {}
|
// struct Quantity {}
|
||||||
|
|
||||||
pub mod core;
|
pub mod core;
|
||||||
pub mod queries;
|
// pub mod queries;
|
||||||
// pub mod parser;
|
// pub mod parser;
|
||||||
// pub mod create_ledger;
|
// pub mod create_ledger;
|
||||||
pub mod document;
|
pub mod document;
|
||||||
pub mod output;
|
pub mod output;
|
||||||
mod parser;
|
pub mod parser;
|
||||||
|
|
||||||
|
pub mod query;
|
||||||
|
|
||||||
// pub struct Account {
|
// pub struct Account {
|
||||||
// // TODO
|
// // TODO
|
||||||
|
|||||||
51
src/main.rs
51
src/main.rs
@ -7,9 +7,11 @@ use std::{
|
|||||||
use accounting_rust::{
|
use accounting_rust::{
|
||||||
document::Document,
|
document::Document,
|
||||||
output::cli::{format_balance, tui_to_ansi::text_to_ansi},
|
output::cli::{format_balance, tui_to_ansi::text_to_ansi},
|
||||||
queries::{self, Query},
|
parser::{self, query},
|
||||||
|
query::{self, PostingField},
|
||||||
|
// queries::{self, base::{self, DataValue, Query}, functions::{ComparisonFunction, LogicalFunction, SubAccountFunction}, transaction::{AccountField, PostingField, TransactionField}},
|
||||||
};
|
};
|
||||||
use chrono::NaiveDate;
|
use chrono::{NaiveDate, Utc};
|
||||||
use ratatui::{
|
use ratatui::{
|
||||||
crossterm::{self, style::PrintStyledContent},
|
crossterm::{self, style::PrintStyledContent},
|
||||||
layout::Rect,
|
layout::Rect,
|
||||||
@ -105,11 +107,46 @@ pub fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
// &[],
|
// &[],
|
||||||
// );
|
// );
|
||||||
|
|
||||||
let balance = queries::balance2(
|
// let balance = queries::balance2(
|
||||||
&ledger,
|
// &ledger,
|
||||||
NaiveDate::from_ymd_opt(2100, 01, 01).unwrap(),
|
// NaiveDate::from_ymd_opt(2100, 01, 01).unwrap(),
|
||||||
Some("$")
|
// Some("$")
|
||||||
);
|
// );
|
||||||
|
|
||||||
|
// let balance_query = "transaction.date < 2100-01-01";
|
||||||
|
let balance_query = "account.name ~ 'Assets' OR account.name ~ 'Liabilities'";
|
||||||
|
|
||||||
|
let parsed_query = parser::query::<PostingField>(balance_query).unwrap();
|
||||||
|
if parsed_query.0.trim().len() != 0 {
|
||||||
|
panic!("Full string not consumed")
|
||||||
|
}
|
||||||
|
let balance_query = parsed_query.1;
|
||||||
|
|
||||||
|
let current_date = Utc::now().date_naive();
|
||||||
|
|
||||||
|
let balance = query::balance(&ledger, Some(&balance_query), Some(("$", current_date)));
|
||||||
|
|
||||||
|
// let date_query = ComparisonFunction::new(
|
||||||
|
// "<=",
|
||||||
|
// Query::from_field(PostingField::Transaction(TransactionField::Date)),
|
||||||
|
// Query::from(DataValue::from(NaiveDate::from_ymd_opt(2100, 01, 01).unwrap())),
|
||||||
|
// ).unwrap();
|
||||||
|
// let account_query = SubAccountFunction::new(
|
||||||
|
// "Assets".into(),
|
||||||
|
// base::Query::from_field(PostingField::Account(AccountField::Name)),
|
||||||
|
// );
|
||||||
|
|
||||||
|
// let total_query = LogicalFunction::new(
|
||||||
|
// "and",
|
||||||
|
// base::Query::from_fn(date_query),
|
||||||
|
// base::Query::from_fn(account_query),
|
||||||
|
// ).unwrap();
|
||||||
|
|
||||||
|
// let balance = queries::balance3(
|
||||||
|
// &ledger,
|
||||||
|
// &base::Query::from_fn(total_query),
|
||||||
|
// );
|
||||||
|
|
||||||
|
|
||||||
let t4 = Instant::now();
|
let t4 = Instant::now();
|
||||||
|
|
||||||
|
|||||||
100
src/parser/amount.rs
Normal file
100
src/parser/amount.rs
Normal file
@ -0,0 +1,100 @@
|
|||||||
|
use nom::{
|
||||||
|
branch::alt,
|
||||||
|
character::complete::{none_of, one_of, space0},
|
||||||
|
combinator::{opt, recognize},
|
||||||
|
multi::many1,
|
||||||
|
sequence::{preceded, tuple}, IResult, Parser,
|
||||||
|
};
|
||||||
|
use rust_decimal_macros::dec;
|
||||||
|
|
||||||
|
use crate::core::RawAmount;
|
||||||
|
use super::decimal;
|
||||||
|
|
||||||
|
pub fn amount(input: &str) -> IResult<&str, RawAmount> {
|
||||||
|
alt((suffix_amount, prefix_amount)).parse(input)
|
||||||
|
}
|
||||||
|
|
||||||
|
///////////////
|
||||||
|
// Private //
|
||||||
|
///////////////
|
||||||
|
|
||||||
|
fn prefix_amount(input: &str) -> IResult<&str, RawAmount> {
|
||||||
|
tuple((opt(one_of("+-")), unit, preceded(space0, decimal)))
|
||||||
|
.map(|(sign, unit_symbol, mut value)| {
|
||||||
|
if let Some(s) = sign {
|
||||||
|
if s == '-' {
|
||||||
|
value = value * dec!(-1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
RawAmount {
|
||||||
|
value,
|
||||||
|
unit_symbol: unit_symbol.to_string(),
|
||||||
|
is_unit_prefix: true,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.parse(input)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn suffix_amount(input: &str) -> IResult<&str, RawAmount> {
|
||||||
|
tuple((decimal, preceded(space0, unit)))
|
||||||
|
.map(|(value, unit_symbol)| RawAmount {
|
||||||
|
value,
|
||||||
|
unit_symbol: unit_symbol.to_string(),
|
||||||
|
is_unit_prefix: false,
|
||||||
|
})
|
||||||
|
.parse(input)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn unit(input: &str) -> IResult<&str, &str> {
|
||||||
|
recognize(many1(none_of("0123456789,+-_()*/.{} \t"))).parse(input)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use rust_decimal_macros::dec;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn amount_good() {
|
||||||
|
assert_eq!(
|
||||||
|
amount("$10").unwrap().1,
|
||||||
|
RawAmount {
|
||||||
|
value: dec!(10),
|
||||||
|
unit_symbol: "$".into(),
|
||||||
|
is_unit_prefix: true
|
||||||
|
}
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
amount("10 USD").unwrap().1,
|
||||||
|
RawAmount {
|
||||||
|
value: dec!(10),
|
||||||
|
unit_symbol: "USD".into(),
|
||||||
|
is_unit_prefix: false
|
||||||
|
}
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
amount("-$10.01").unwrap().1,
|
||||||
|
RawAmount {
|
||||||
|
value: dec!(-10.01),
|
||||||
|
unit_symbol: "$".into(),
|
||||||
|
is_unit_prefix: true
|
||||||
|
}
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
amount("-10€").unwrap().1,
|
||||||
|
RawAmount {
|
||||||
|
value: dec!(-10),
|
||||||
|
unit_symbol: "€".into(),
|
||||||
|
is_unit_prefix: false
|
||||||
|
}
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
amount("-€10").unwrap().1,
|
||||||
|
RawAmount {
|
||||||
|
value: dec!(-10),
|
||||||
|
unit_symbol: "€".into(),
|
||||||
|
is_unit_prefix: true
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
143
src/parser/core.rs
Normal file
143
src/parser/core.rs
Normal file
@ -0,0 +1,143 @@
|
|||||||
|
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,
|
||||||
|
};
|
||||||
|
use rust_decimal::Decimal;
|
||||||
|
|
||||||
|
pub fn decimal(input: &str) -> IResult<&str, Decimal> {
|
||||||
|
let (new_input, decimal_str) = recognize(tuple((
|
||||||
|
opt(one_of("+-")),
|
||||||
|
opt(number_int),
|
||||||
|
opt(char('.')),
|
||||||
|
opt(number_int),
|
||||||
|
)))
|
||||||
|
.parse(input)?;
|
||||||
|
|
||||||
|
if decimal_str.contains(',') {
|
||||||
|
match Decimal::from_str_exact(&decimal_str.replace(",", "")) {
|
||||||
|
Ok(decimal) => Ok((new_input, decimal)),
|
||||||
|
Err(_) => Err(Err::Error(Error::new(input, ErrorKind::Eof))),
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
match Decimal::from_str_exact(decimal_str) {
|
||||||
|
Ok(decimal) => Ok((new_input, decimal)),
|
||||||
|
Err(_) => Err(Err::Error(Error::new(input, ErrorKind::Eof))),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn number_int(input: &str) -> IResult<&str, &str> {
|
||||||
|
recognize(many1(terminated(one_of("0123456789"), many0(one_of("_,")))))(input)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn parse_iso_date(input: &str) -> IResult<&str, NaiveDate> {
|
||||||
|
let (new_input, (year, _, month, _, day)) =
|
||||||
|
tuple((date_year, tag("-"), date_month, tag("-"), date_day)).parse(input)?;
|
||||||
|
|
||||||
|
match NaiveDate::from_ymd_opt(year, month, day) {
|
||||||
|
Some(date) => Ok((new_input, date)),
|
||||||
|
None => Err(nom::Err::Error(Error::new(input, ErrorKind::Eof))),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn quoted_string(input: &str) -> IResult<&str, &str> {
|
||||||
|
alt((
|
||||||
|
delimited(
|
||||||
|
tag("\""),
|
||||||
|
escaped(none_of("\\\""), '\\', tag("\"")),
|
||||||
|
tag("\""),
|
||||||
|
),
|
||||||
|
delimited(
|
||||||
|
tag("'"),
|
||||||
|
escaped(none_of("\\\'"), '\\', tag("\'")),
|
||||||
|
tag("'"),
|
||||||
|
),
|
||||||
|
))
|
||||||
|
.parse(input)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn ws<'a, F: 'a, O>(inner: F) -> impl FnMut(&'a str) -> IResult<&'a str, O>
|
||||||
|
where
|
||||||
|
F: FnMut(&'a str) -> IResult<&'a str, O>,
|
||||||
|
{
|
||||||
|
delimited(space0, inner, space0)
|
||||||
|
}
|
||||||
|
|
||||||
|
///////////////
|
||||||
|
// Private //
|
||||||
|
///////////////
|
||||||
|
|
||||||
|
fn take_n_digits(i: &str, n: usize) -> IResult<&str, u32> {
|
||||||
|
let (i, digits) = take_while_m_n(n, n, AsChar::is_dec_digit)(i)?;
|
||||||
|
|
||||||
|
let res = digits.parse().expect("Invalid ASCII number");
|
||||||
|
|
||||||
|
Ok((i, res))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn date_year(input: &str) -> IResult<&str, i32> {
|
||||||
|
take_n_digits(input, 4).map(|(str, year)| (str, i32::try_from(year).unwrap()))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn date_month(input: &str) -> IResult<&str, u32> {
|
||||||
|
take_n_digits(input, 2)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn date_day(input: &str) -> IResult<&str, u32> {
|
||||||
|
take_n_digits(input, 2)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use rust_decimal_macros::dec;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_decimal_good() {
|
||||||
|
assert_eq!(decimal("1").unwrap().1, dec!(1));
|
||||||
|
assert_eq!(decimal("+10").unwrap().1, dec!(10));
|
||||||
|
assert_eq!(decimal("-10").unwrap().1, dec!(-10));
|
||||||
|
assert_eq!(decimal("10.1").unwrap().1, dec!(10.1));
|
||||||
|
assert_eq!(decimal("100_000.01").unwrap().1, dec!(100000.01));
|
||||||
|
assert_eq!(decimal(".1").unwrap().1, dec!(0.1));
|
||||||
|
assert_eq!(decimal("-.1").unwrap().1, dec!(-0.1));
|
||||||
|
assert_eq!(decimal("2.").unwrap().1, dec!(2.));
|
||||||
|
assert_eq!(decimal("1,000").unwrap().1, dec!(1000));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn correct_date() {
|
||||||
|
assert_eq!(
|
||||||
|
parse_iso_date("2000-01-01"),
|
||||||
|
Ok(("", NaiveDate::from_ymd_opt(2000, 01, 01).unwrap()))
|
||||||
|
);
|
||||||
|
// assert_eq!(
|
||||||
|
// parse_iso_date("20000101"),
|
||||||
|
// Ok(("", NaiveDate::from_ymd_opt(2000, 01, 01).unwrap()))
|
||||||
|
// );
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn incomplete_date() {
|
||||||
|
assert_eq!(parse_iso_date("200-01-01").is_err(), true);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn invalid_date() {
|
||||||
|
assert_eq!(parse_iso_date("2000-02-30").is_err(), true);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_string() {
|
||||||
|
assert_eq!(quoted_string("\"test\"").unwrap().1, "test");
|
||||||
|
assert_eq!(quoted_string("'test'").unwrap().1, "test");
|
||||||
|
assert_eq!(quoted_string("\"te\\\"st\"").unwrap().1, "te\\\"st");
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -61,9 +61,9 @@ pub fn empty_lines(input: &str) -> IResult<&str, ()> {
|
|||||||
pub fn parse_iso_date(input: &str) -> IResult<&str, NaiveDate> {
|
pub fn parse_iso_date(input: &str) -> IResult<&str, NaiveDate> {
|
||||||
let (new_input, (year, _, month, _, day)) = tuple((
|
let (new_input, (year, _, month, _, day)) = tuple((
|
||||||
date_year,
|
date_year,
|
||||||
opt(tag("-")),
|
tag("-"),
|
||||||
date_month,
|
date_month,
|
||||||
opt(tag("-")),
|
tag("-"),
|
||||||
date_day,
|
date_day,
|
||||||
))
|
))
|
||||||
.parse(input)?;
|
.parse(input)?;
|
||||||
@ -136,10 +136,10 @@ mod tests {
|
|||||||
parse_iso_date("2000-01-01"),
|
parse_iso_date("2000-01-01"),
|
||||||
Ok(("", NaiveDate::from_ymd_opt(2000, 01, 01).unwrap()))
|
Ok(("", NaiveDate::from_ymd_opt(2000, 01, 01).unwrap()))
|
||||||
);
|
);
|
||||||
assert_eq!(
|
// assert_eq!(
|
||||||
parse_iso_date("20000101"),
|
// parse_iso_date("20000101"),
|
||||||
Ok(("", NaiveDate::from_ymd_opt(2000, 01, 01).unwrap()))
|
// Ok(("", NaiveDate::from_ymd_opt(2000, 01, 01).unwrap()))
|
||||||
);
|
// );
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@ -2,29 +2,20 @@ use std::path::PathBuf;
|
|||||||
|
|
||||||
use nom::{
|
use nom::{
|
||||||
bytes::complete::{is_not, tag},
|
bytes::complete::{is_not, tag},
|
||||||
character::complete::{none_of, space1},
|
character::complete::space1,
|
||||||
combinator::{opt, rest},
|
combinator::{opt, rest},
|
||||||
error::{Error, ErrorKind, ParseError},
|
error::{Error, ErrorKind},
|
||||||
sequence::{preceded, terminated, tuple},
|
sequence::{preceded, terminated, tuple},
|
||||||
IResult, Parser,
|
IResult, Parser,
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::{core::UnitSymbol, document::{
|
use crate::{
|
||||||
BalanceDirective, CommodityDirective, IncludeDirective, TransactionDirective,
|
core::UnitSymbol,
|
||||||
}};
|
document::{BalanceDirective, CommodityDirective, IncludeDirective, TransactionDirective},
|
||||||
|
parser::amount,
|
||||||
use super::{
|
|
||||||
amounts::{account, amount},
|
|
||||||
base_directive::BaseDirective,
|
|
||||||
transaction::transaction,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// use super::{
|
use super::{base_directive::BaseDirective, shared::account, transaction::transaction};
|
||||||
// base::ParsedBaseDirective,
|
|
||||||
// shared::{parse_account, parse_amount},
|
|
||||||
// transaction::parse_transaction,
|
|
||||||
// types::{ParseError, ParsedBalanceDirective, ParsedDirectives, ParsedIncludeDirective},
|
|
||||||
// };
|
|
||||||
|
|
||||||
//////////////
|
//////////////
|
||||||
// Public //
|
// Public //
|
||||||
@ -115,13 +106,15 @@ fn commodity_directive(directive: BaseDirective) -> IResult<BaseDirective, Commo
|
|||||||
}
|
}
|
||||||
|
|
||||||
match key {
|
match key {
|
||||||
"symbol_prefix" => symbols.push(UnitSymbol {symbol: value.into(), is_prefix: true}),
|
"symbol_prefix" => symbols.push(UnitSymbol { symbol: value.into(), is_prefix: true }),
|
||||||
"symbol" => symbols.push(UnitSymbol {symbol: value.into(), is_prefix: false}),
|
"symbol" => symbols.push(UnitSymbol { symbol: value.into(), is_prefix: false }),
|
||||||
"precision" => precision = Some(value.trim().parse::<u32>().unwrap()), // TODO: unwrap
|
"precision" => precision = Some(value.trim().parse::<u32>().unwrap()), // TODO: unwrap
|
||||||
_ => return Err(nom::Err::Failure(Error {
|
_ => {
|
||||||
input: directive,
|
return Err(nom::Err::Failure(Error {
|
||||||
code: ErrorKind::Fail,
|
input: directive,
|
||||||
})),
|
code: ErrorKind::Fail,
|
||||||
|
}))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1,4 +1,3 @@
|
|||||||
mod amounts;
|
|
||||||
mod base_directive;
|
mod base_directive;
|
||||||
mod directives;
|
mod directives;
|
||||||
mod transaction;
|
mod transaction;
|
||||||
@ -14,9 +13,7 @@ use nom::{
|
|||||||
Parser,
|
Parser,
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::core::CoreError;
|
use crate::{core::CoreError, document::Directives};
|
||||||
|
|
||||||
use super::Directives;
|
|
||||||
|
|
||||||
pub fn parse_directives(input: &str) -> Result<Directives, CoreError> {
|
pub fn parse_directives(input: &str) -> Result<Directives, CoreError> {
|
||||||
let parsed_directives = terminated(
|
let parsed_directives = terminated(
|
||||||
@ -6,6 +6,10 @@ use nom::{
|
|||||||
IResult, InputTakeAtPosition, Parser,
|
IResult, InputTakeAtPosition, Parser,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
pub fn account(input: &str) -> IResult<&str, &str> {
|
||||||
|
input.split_at_position1_complete(|item| item == ' ' || item == '\t', ErrorKind::AlphaNumeric)
|
||||||
|
}
|
||||||
|
|
||||||
pub fn metadatum(input: &str) -> IResult<&str, (&str, &str)> {
|
pub fn metadatum(input: &str) -> IResult<&str, (&str, &str)> {
|
||||||
tuple((
|
tuple((
|
||||||
delimited(tag("-"), delimited(space0, key, space0), tag(":")),
|
delimited(tag("-"), delimited(space0, key, space0), tag(":")),
|
||||||
@ -1,25 +1,24 @@
|
|||||||
use nom::{
|
use nom::{
|
||||||
branch::alt,
|
branch::alt,
|
||||||
bytes::complete::{is_not, tag},
|
bytes::complete::{is_not, tag},
|
||||||
character::complete::{space0, space1},
|
character::complete::space1,
|
||||||
combinator::{eof, opt, rest},
|
combinator::{eof, opt, rest},
|
||||||
error::{Error, ErrorKind},
|
error::{Error, ErrorKind},
|
||||||
sequence::{delimited, preceded, terminated, tuple},
|
sequence::{delimited, preceded, terminated, tuple},
|
||||||
Err, IResult, InputTakeAtPosition, Parser,
|
Err, IResult, Parser,
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::document::{DirectiveAmount, DirectivePosting, TransactionDirective};
|
use crate::{
|
||||||
|
core::RawAmount,
|
||||||
|
document::{DirectivePosting, TransactionDirective},
|
||||||
|
parser::amount,
|
||||||
|
};
|
||||||
|
|
||||||
use super::{
|
use super::{
|
||||||
amounts::{account, amount},
|
|
||||||
base_directive::{parse_iso_date, BaseDirective},
|
base_directive::{parse_iso_date, BaseDirective},
|
||||||
shared::metadatum,
|
shared::{account, metadatum},
|
||||||
};
|
};
|
||||||
|
|
||||||
// use super::{
|
|
||||||
// base::ParsedBaseDirective, directives::BaseDirective, shared::{parse_account, amount}, types::{ParseError, DirectiveAmount, DirectivePosting, ParsedTransactionDirective}
|
|
||||||
// };
|
|
||||||
|
|
||||||
//////////////
|
//////////////
|
||||||
// Public //
|
// Public //
|
||||||
//////////////
|
//////////////
|
||||||
@ -56,7 +55,7 @@ pub fn transaction<'a>(
|
|||||||
let mut metadata = Vec::new();
|
let mut metadata = Vec::new();
|
||||||
for &line in directive.lines.get(1..).unwrap_or(&[]) {
|
for &line in directive.lines.get(1..).unwrap_or(&[]) {
|
||||||
if let Ok(m) = metadatum(line) {
|
if let Ok(m) = metadatum(line) {
|
||||||
metadata.push((m.1.0.to_string(), m.1.1.to_string()));
|
metadata.push((m.1 .0.to_string(), m.1 .1.to_string()));
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
let posting = if let Ok(v) = posting(line) {
|
let posting = if let Ok(v) = posting(line) {
|
||||||
@ -118,7 +117,7 @@ fn posting(input: &str) -> IResult<&str, DirectivePosting> {
|
|||||||
amount = Some(v.0);
|
amount = Some(v.0);
|
||||||
if let Some(c) = v.1 {
|
if let Some(c) = v.1 {
|
||||||
if c.1 {
|
if c.1 {
|
||||||
cost = Some(DirectiveAmount {
|
cost = Some(RawAmount {
|
||||||
value: c.0.value / amount.as_ref().unwrap().value.abs(),
|
value: c.0.value / amount.as_ref().unwrap().value.abs(),
|
||||||
unit_symbol: c.0.unit_symbol,
|
unit_symbol: c.0.unit_symbol,
|
||||||
is_unit_prefix: c.0.is_unit_prefix,
|
is_unit_prefix: c.0.is_unit_prefix,
|
||||||
@ -129,7 +128,7 @@ fn posting(input: &str) -> IResult<&str, DirectivePosting> {
|
|||||||
}
|
}
|
||||||
if let Some(p) = v.2 {
|
if let Some(p) = v.2 {
|
||||||
if p.1 {
|
if p.1 {
|
||||||
price = Some(DirectiveAmount {
|
price = Some(RawAmount {
|
||||||
value: p.0.value / amount.as_ref().unwrap().value.abs(),
|
value: p.0.value / amount.as_ref().unwrap().value.abs(),
|
||||||
unit_symbol: p.0.unit_symbol,
|
unit_symbol: p.0.unit_symbol,
|
||||||
is_unit_prefix: p.0.is_unit_prefix,
|
is_unit_prefix: p.0.is_unit_prefix,
|
||||||
@ -150,7 +149,7 @@ fn posting(input: &str) -> IResult<&str, DirectivePosting> {
|
|||||||
.parse(input)
|
.parse(input)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn parse_cost(input: &str) -> IResult<&str, (DirectiveAmount, bool)> {
|
fn parse_cost(input: &str) -> IResult<&str, (RawAmount, bool)> {
|
||||||
alt((
|
alt((
|
||||||
delimited(tag("{"), amount, tag("}")).map(|amount| (amount, false)),
|
delimited(tag("{"), amount, tag("}")).map(|amount| (amount, false)),
|
||||||
delimited(tag("{{"), amount, tag("}}")).map(|amount| (amount, true)),
|
delimited(tag("{{"), amount, tag("}}")).map(|amount| (amount, true)),
|
||||||
@ -158,7 +157,7 @@ fn parse_cost(input: &str) -> IResult<&str, (DirectiveAmount, bool)> {
|
|||||||
.parse(input)
|
.parse(input)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn parse_price(input: &str) -> IResult<&str, (DirectiveAmount, bool)> {
|
fn parse_price(input: &str) -> IResult<&str, (RawAmount, bool)> {
|
||||||
alt((
|
alt((
|
||||||
preceded(tuple((tag("@"), space1)), amount).map(|amount| (amount, false)),
|
preceded(tuple((tag("@"), space1)), amount).map(|amount| (amount, false)),
|
||||||
preceded(tuple((tag("@@"), space1)), amount).map(|amount| (amount, true)),
|
preceded(tuple((tag("@@"), space1)), amount).map(|amount| (amount, true)),
|
||||||
@ -210,12 +209,12 @@ mod tests {
|
|||||||
DirectivePosting {
|
DirectivePosting {
|
||||||
date: None,
|
date: None,
|
||||||
account: "Account1".into(),
|
account: "Account1".into(),
|
||||||
amount: Some(DirectiveAmount {
|
amount: Some(RawAmount {
|
||||||
value: dec!(10),
|
value: dec!(10),
|
||||||
unit_symbol: "SHARE".into(),
|
unit_symbol: "SHARE".into(),
|
||||||
is_unit_prefix: false
|
is_unit_prefix: false
|
||||||
}),
|
}),
|
||||||
cost: Some(DirectiveAmount {
|
cost: Some(RawAmount {
|
||||||
value: dec!(100),
|
value: dec!(100),
|
||||||
unit_symbol: "$".into(),
|
unit_symbol: "$".into(),
|
||||||
is_unit_prefix: true
|
is_unit_prefix: true
|
||||||
@ -228,12 +227,12 @@ mod tests {
|
|||||||
DirectivePosting {
|
DirectivePosting {
|
||||||
date: None,
|
date: None,
|
||||||
account: "Account1".into(),
|
account: "Account1".into(),
|
||||||
amount: Some(DirectiveAmount {
|
amount: Some(RawAmount {
|
||||||
value: dec!(10),
|
value: dec!(10),
|
||||||
unit_symbol: "SHARE".into(),
|
unit_symbol: "SHARE".into(),
|
||||||
is_unit_prefix: false
|
is_unit_prefix: false
|
||||||
}),
|
}),
|
||||||
cost: Some(DirectiveAmount {
|
cost: Some(RawAmount {
|
||||||
value: dec!(100),
|
value: dec!(100),
|
||||||
unit_symbol: "USD".into(),
|
unit_symbol: "USD".into(),
|
||||||
is_unit_prefix: false
|
is_unit_prefix: false
|
||||||
@ -250,13 +249,13 @@ mod tests {
|
|||||||
DirectivePosting {
|
DirectivePosting {
|
||||||
date: None,
|
date: None,
|
||||||
account: "Account1".into(),
|
account: "Account1".into(),
|
||||||
amount: Some(DirectiveAmount {
|
amount: Some(RawAmount {
|
||||||
value: dec!(10),
|
value: dec!(10),
|
||||||
unit_symbol: "SHARE".into(),
|
unit_symbol: "SHARE".into(),
|
||||||
is_unit_prefix: false
|
is_unit_prefix: false
|
||||||
}),
|
}),
|
||||||
cost: None,
|
cost: None,
|
||||||
price: Some(DirectiveAmount {
|
price: Some(RawAmount {
|
||||||
value: dec!(100),
|
value: dec!(100),
|
||||||
unit_symbol: "$".into(),
|
unit_symbol: "$".into(),
|
||||||
is_unit_prefix: true
|
is_unit_prefix: true
|
||||||
@ -268,13 +267,13 @@ mod tests {
|
|||||||
DirectivePosting {
|
DirectivePosting {
|
||||||
date: None,
|
date: None,
|
||||||
account: "Account1".into(),
|
account: "Account1".into(),
|
||||||
amount: Some(DirectiveAmount {
|
amount: Some(RawAmount {
|
||||||
value: dec!(10),
|
value: dec!(10),
|
||||||
unit_symbol: "SHARE".into(),
|
unit_symbol: "SHARE".into(),
|
||||||
is_unit_prefix: false
|
is_unit_prefix: false
|
||||||
}),
|
}),
|
||||||
cost: None,
|
cost: None,
|
||||||
price: Some(DirectiveAmount {
|
price: Some(RawAmount {
|
||||||
value: dec!(100),
|
value: dec!(100),
|
||||||
unit_symbol: "USD".into(),
|
unit_symbol: "USD".into(),
|
||||||
is_unit_prefix: false
|
is_unit_prefix: false
|
||||||
@ -290,17 +289,17 @@ mod tests {
|
|||||||
DirectivePosting {
|
DirectivePosting {
|
||||||
date: None,
|
date: None,
|
||||||
account: "Account1".into(),
|
account: "Account1".into(),
|
||||||
amount: Some(DirectiveAmount {
|
amount: Some(RawAmount {
|
||||||
value: dec!(10),
|
value: dec!(10),
|
||||||
unit_symbol: "SHARE".into(),
|
unit_symbol: "SHARE".into(),
|
||||||
is_unit_prefix: false
|
is_unit_prefix: false
|
||||||
}),
|
}),
|
||||||
cost: Some(DirectiveAmount {
|
cost: Some(RawAmount {
|
||||||
value: dec!(100),
|
value: dec!(100),
|
||||||
unit_symbol: "$".into(),
|
unit_symbol: "$".into(),
|
||||||
is_unit_prefix: true
|
is_unit_prefix: true
|
||||||
}),
|
}),
|
||||||
price: Some(DirectiveAmount {
|
price: Some(RawAmount {
|
||||||
value: dec!(110),
|
value: dec!(110),
|
||||||
unit_symbol: "$".into(),
|
unit_symbol: "$".into(),
|
||||||
is_unit_prefix: true
|
is_unit_prefix: true
|
||||||
@ -314,17 +313,17 @@ mod tests {
|
|||||||
DirectivePosting {
|
DirectivePosting {
|
||||||
date: None,
|
date: None,
|
||||||
account: "Account1".into(),
|
account: "Account1".into(),
|
||||||
amount: Some(DirectiveAmount {
|
amount: Some(RawAmount {
|
||||||
value: dec!(10),
|
value: dec!(10),
|
||||||
unit_symbol: "SHARE".into(),
|
unit_symbol: "SHARE".into(),
|
||||||
is_unit_prefix: false
|
is_unit_prefix: false
|
||||||
}),
|
}),
|
||||||
cost: Some(DirectiveAmount {
|
cost: Some(RawAmount {
|
||||||
value: dec!(100),
|
value: dec!(100),
|
||||||
unit_symbol: "USD".into(),
|
unit_symbol: "USD".into(),
|
||||||
is_unit_prefix: false
|
is_unit_prefix: false
|
||||||
}),
|
}),
|
||||||
price: Some(DirectiveAmount {
|
price: Some(RawAmount {
|
||||||
value: dec!(110),
|
value: dec!(110),
|
||||||
unit_symbol: "USD".into(),
|
unit_symbol: "USD".into(),
|
||||||
is_unit_prefix: false
|
is_unit_prefix: false
|
||||||
@ -338,17 +337,17 @@ mod tests {
|
|||||||
DirectivePosting {
|
DirectivePosting {
|
||||||
date: None,
|
date: None,
|
||||||
account: "Account1".into(),
|
account: "Account1".into(),
|
||||||
amount: Some(DirectiveAmount {
|
amount: Some(RawAmount {
|
||||||
value: dec!(10),
|
value: dec!(10),
|
||||||
unit_symbol: "SHARE".into(),
|
unit_symbol: "SHARE".into(),
|
||||||
is_unit_prefix: false
|
is_unit_prefix: false
|
||||||
}),
|
}),
|
||||||
cost: Some(DirectiveAmount {
|
cost: Some(RawAmount {
|
||||||
value: dec!(100),
|
value: dec!(100),
|
||||||
unit_symbol: "USD".into(),
|
unit_symbol: "USD".into(),
|
||||||
is_unit_prefix: false
|
is_unit_prefix: false
|
||||||
}),
|
}),
|
||||||
price: Some(DirectiveAmount {
|
price: Some(RawAmount {
|
||||||
value: dec!(110),
|
value: dec!(110),
|
||||||
unit_symbol: "USD".into(),
|
unit_symbol: "USD".into(),
|
||||||
is_unit_prefix: false
|
is_unit_prefix: false
|
||||||
@ -366,17 +365,17 @@ mod tests {
|
|||||||
DirectivePosting {
|
DirectivePosting {
|
||||||
date: Some(NaiveDate::from_ymd_opt(2000, 01, 01).unwrap()),
|
date: Some(NaiveDate::from_ymd_opt(2000, 01, 01).unwrap()),
|
||||||
account: "Account1".into(),
|
account: "Account1".into(),
|
||||||
amount: Some(DirectiveAmount {
|
amount: Some(RawAmount {
|
||||||
value: dec!(10),
|
value: dec!(10),
|
||||||
unit_symbol: "SHARE".into(),
|
unit_symbol: "SHARE".into(),
|
||||||
is_unit_prefix: false
|
is_unit_prefix: false
|
||||||
}),
|
}),
|
||||||
cost: Some(DirectiveAmount {
|
cost: Some(RawAmount {
|
||||||
value: dec!(100),
|
value: dec!(100),
|
||||||
unit_symbol: "$".into(),
|
unit_symbol: "$".into(),
|
||||||
is_unit_prefix: true
|
is_unit_prefix: true
|
||||||
}),
|
}),
|
||||||
price: Some(DirectiveAmount {
|
price: Some(RawAmount {
|
||||||
value: dec!(110),
|
value: dec!(110),
|
||||||
unit_symbol: "$".into(),
|
unit_symbol: "$".into(),
|
||||||
is_unit_prefix: true
|
is_unit_prefix: true
|
||||||
@ -402,7 +401,7 @@ mod tests {
|
|||||||
assert_eq!(transaction.postings[0].account, "Account1:Account2");
|
assert_eq!(transaction.postings[0].account, "Account1:Account2");
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
transaction.postings[0].amount,
|
transaction.postings[0].amount,
|
||||||
Some(DirectiveAmount {
|
Some(RawAmount {
|
||||||
value: dec!(10.01),
|
value: dec!(10.01),
|
||||||
unit_symbol: "$".into(),
|
unit_symbol: "$".into(),
|
||||||
is_unit_prefix: true
|
is_unit_prefix: true
|
||||||
48
src/parser/fields.rs
Normal file
48
src/parser/fields.rs
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
use std::fmt::Debug;
|
||||||
|
|
||||||
|
use crate::query::{AccountField, PostingField, TransactionField};
|
||||||
|
|
||||||
|
pub trait ParseField: Debug + Sized + Clone {
|
||||||
|
fn parse(input: &str) -> Option<Self>;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ParseField for TransactionField {
|
||||||
|
fn parse(input: &str) -> Option<Self> {
|
||||||
|
match input.to_lowercase().as_str() {
|
||||||
|
"date" => Some(TransactionField::Date),
|
||||||
|
"flag" => Some(TransactionField::Flag),
|
||||||
|
"payee" => Some(TransactionField::Payee),
|
||||||
|
"narration" => Some(TransactionField::Narration),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ParseField for AccountField {
|
||||||
|
fn parse(input: &str) -> Option<Self> {
|
||||||
|
match input.to_lowercase().as_str() {
|
||||||
|
"name" => Some(AccountField::Name),
|
||||||
|
"open" => Some(AccountField::OpenDate),
|
||||||
|
"close" => Some(AccountField::CloseDate),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ParseField for PostingField {
|
||||||
|
fn parse(input: &str) -> Option<Self> {
|
||||||
|
match input
|
||||||
|
.to_lowercase()
|
||||||
|
.split('.')
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
.as_slice()
|
||||||
|
{
|
||||||
|
["amount"] => Some(PostingField::Amount),
|
||||||
|
["cost"] => Some(PostingField::Cost),
|
||||||
|
["price"] => Some(PostingField::Price),
|
||||||
|
["transaction", t] => TransactionField::parse(t).map(|v| PostingField::Transaction(v)),
|
||||||
|
["account", t] => AccountField::parse(t).map(|v| PostingField::Account(v)),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,99 +1,14 @@
|
|||||||
use chrono::NaiveDate;
|
|
||||||
use nom::{
|
|
||||||
bytes::complete::{escaped, tag, take_while_m_n},
|
|
||||||
character::complete::{char, none_of, one_of},
|
|
||||||
combinator::{opt, recognize},
|
|
||||||
error::{Error, ErrorKind},
|
|
||||||
multi::{many0, many1},
|
|
||||||
sequence::{delimited, terminated, tuple},
|
|
||||||
AsChar, Err, IResult, Parser,
|
|
||||||
};
|
|
||||||
use rust_decimal::Decimal;
|
|
||||||
|
|
||||||
pub fn decimal(input: &str) -> IResult<&str, Decimal> {
|
mod core;
|
||||||
let (new_input, decimal_str) = recognize(tuple((
|
mod amount;
|
||||||
opt(one_of("+-")),
|
mod value;
|
||||||
opt(number_int),
|
mod query;
|
||||||
opt(char('.')),
|
mod document;
|
||||||
opt(number_int),
|
mod fields;
|
||||||
)))
|
|
||||||
.parse(input)?;
|
|
||||||
|
|
||||||
if decimal_str.contains(',') {
|
pub use core::*;
|
||||||
match Decimal::from_str_exact(&decimal_str.replace(",", "")) {
|
pub use amount::*;
|
||||||
Ok(decimal) => Ok((new_input, decimal)),
|
pub use value::*;
|
||||||
Err(_) => Err(Err::Error(Error::new(input, ErrorKind::Eof))),
|
pub use query::*;
|
||||||
}
|
pub use document::*;
|
||||||
} else {
|
pub use fields::*;
|
||||||
match Decimal::from_str_exact(decimal_str) {
|
|
||||||
Ok(decimal) => Ok((new_input, decimal)),
|
|
||||||
Err(_) => Err(Err::Error(Error::new(input, ErrorKind::Eof))),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn number_int(input: &str) -> IResult<&str, &str> {
|
|
||||||
recognize(many1(terminated(one_of("0123456789"), many0(one_of("_,")))))(input)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn parse_iso_date(input: &str) -> IResult<&str, NaiveDate> {
|
|
||||||
let (new_input, (year, _, month, _, day)) = tuple((
|
|
||||||
date_year,
|
|
||||||
opt(tag("-")),
|
|
||||||
date_month,
|
|
||||||
opt(tag("-")),
|
|
||||||
date_day,
|
|
||||||
))
|
|
||||||
.parse(input)?;
|
|
||||||
|
|
||||||
match NaiveDate::from_ymd_opt(year, month, day) {
|
|
||||||
Some(date) => Ok((new_input, date)),
|
|
||||||
None => Err(nom::Err::Error(Error::new(input, ErrorKind::Eof))),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn quoted_string(input: &str) -> IResult<&str, &str> {
|
|
||||||
delimited(
|
|
||||||
tag("\""),
|
|
||||||
escaped(none_of("\\\""), '\\', tag("\"")),
|
|
||||||
tag("\""),
|
|
||||||
)
|
|
||||||
.parse(input)
|
|
||||||
}
|
|
||||||
|
|
||||||
///////////////
|
|
||||||
// Private //
|
|
||||||
///////////////
|
|
||||||
|
|
||||||
fn take_n_digits(i: &str, n: usize) -> IResult<&str, u32> {
|
|
||||||
let (i, digits) = take_while_m_n(n, n, AsChar::is_dec_digit)(i)?;
|
|
||||||
|
|
||||||
let res = digits.parse().expect("Invalid ASCII number");
|
|
||||||
|
|
||||||
Ok((i, res))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn date_year(input: &str) -> IResult<&str, i32> {
|
|
||||||
take_n_digits(input, 4).map(|(str, year)| (str, i32::try_from(year).unwrap()))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn date_month(input: &str) -> IResult<&str, u32> {
|
|
||||||
take_n_digits(input, 2)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn date_day(input: &str) -> IResult<&str, u32> {
|
|
||||||
take_n_digits(input, 2)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use super::*;
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn parse_string() {
|
|
||||||
assert_eq!(quoted_string("\"test\"").unwrap().1, "test");
|
|
||||||
assert_eq!(quoted_string("\"te\\\"st\"").unwrap().1, "te\\\"st");
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
246
src/parser/query.rs
Normal file
246
src/parser/query.rs
Normal file
@ -0,0 +1,246 @@
|
|||||||
|
use nom::{
|
||||||
|
branch::alt,
|
||||||
|
bytes::complete::{tag, tag_no_case},
|
||||||
|
error::{Error, ErrorKind},
|
||||||
|
multi::fold_many0,
|
||||||
|
sequence::{delimited, preceded, tuple},
|
||||||
|
AsChar, Err, IResult, InputTakeAtPosition, Parser,
|
||||||
|
};
|
||||||
|
|
||||||
|
use super::{quoted_string, value, ws, ParseField};
|
||||||
|
use crate::query::{
|
||||||
|
ComparisonFunction, ComparisonOperator, LogicalFunction, LogicalOperator, NotFunction, Query,
|
||||||
|
RegexFunction,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub fn query<'a, Field: ParseField>(input: &'a str) -> IResult<&'a str, Query<Field>> {
|
||||||
|
term_or.parse(input)
|
||||||
|
}
|
||||||
|
|
||||||
|
/////////////
|
||||||
|
// Private //
|
||||||
|
/////////////
|
||||||
|
|
||||||
|
fn expression<'a, Field: ParseField>(input: &'a str) -> IResult<&'a str, Query<Field>> {
|
||||||
|
term_or.parse(input)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn term_or<'a, Field: ParseField>(input: &'a str) -> IResult<&'a str, Query<Field>> {
|
||||||
|
let (input, init) = term_and(input)?;
|
||||||
|
|
||||||
|
fold_many0(
|
||||||
|
tuple((tag_no_case("OR"), term_and)),
|
||||||
|
move || init.clone(),
|
||||||
|
|acc, (_, val)| Query::from_fn(LogicalFunction::new_op(LogicalOperator::OR, acc, val)),
|
||||||
|
)
|
||||||
|
.parse(input)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn term_and<'a, Field: ParseField>(input: &'a str) -> IResult<&'a str, Query<Field>> {
|
||||||
|
let (input, init) = term(input)?;
|
||||||
|
|
||||||
|
fold_many0(
|
||||||
|
tuple((tag_no_case("AND"), term)),
|
||||||
|
move || init.clone(),
|
||||||
|
|acc, (_, val)| Query::from_fn(LogicalFunction::new_op(LogicalOperator::AND, acc, val)),
|
||||||
|
)
|
||||||
|
.parse(input)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn term<'a, Field: ParseField + 'static>(input: &'a str) -> IResult<&'a str, Query<Field>> {
|
||||||
|
alt((function_regex, function_comparison, factor)).parse(input)
|
||||||
|
}
|
||||||
|
|
||||||
|
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?
|
||||||
|
.parse(input)?;
|
||||||
|
match result {
|
||||||
|
Ok(regex_function) => Ok((new_input, Query::from_fn(regex_function))),
|
||||||
|
Err(_) => Err(Err::Error(Error::new(input, ErrorKind::Eof))),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn function_comparison<'a, Field: ParseField + 'static>(
|
||||||
|
input: &'a str,
|
||||||
|
) -> IResult<&'a str, Query<Field>> {
|
||||||
|
let op = alt((
|
||||||
|
tag("=").map(|_| ComparisonOperator::EQ),
|
||||||
|
tag("!=").map(|_| ComparisonOperator::NEQ),
|
||||||
|
tag(">").map(|_| ComparisonOperator::GT),
|
||||||
|
tag("<").map(|_| ComparisonOperator::LT),
|
||||||
|
tag(">=").map(|_| ComparisonOperator::GTE),
|
||||||
|
tag("<=").map(|_| ComparisonOperator::LTE),
|
||||||
|
));
|
||||||
|
|
||||||
|
tuple((factor, op, factor))
|
||||||
|
.map(|(left, op, right)| Query::from_fn(ComparisonFunction::new_op(op, left, right)))
|
||||||
|
.parse(input)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn factor<'a, Field: ParseField + 'static>(input: &'a str) -> IResult<&'a str, Query<Field>> {
|
||||||
|
ws(alt((
|
||||||
|
function_unary,
|
||||||
|
value.map(|v| Query::Value(v)),
|
||||||
|
field.map(|f| Query::Field(f)),
|
||||||
|
parenthesis,
|
||||||
|
)))
|
||||||
|
.parse(input)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn function_unary<'a, Field: ParseField + 'static>(
|
||||||
|
input: &'a str,
|
||||||
|
) -> IResult<&'a str, Query<Field>> {
|
||||||
|
preceded(ws(tag("!")), factor)
|
||||||
|
.map(|f| Query::from_fn(NotFunction::new(f)))
|
||||||
|
.parse(input)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn field<'a, Field: ParseField>(input: &str) -> IResult<&str, Field> {
|
||||||
|
input
|
||||||
|
.split_at_position1_complete(
|
||||||
|
|item| !item.is_alphanum() && item != '.',
|
||||||
|
ErrorKind::AlphaNumeric,
|
||||||
|
)
|
||||||
|
.and_then(|v| {
|
||||||
|
Field::parse(v.1)
|
||||||
|
.map(|f| (v.0, f))
|
||||||
|
.ok_or(nom::Err::Error(Error::new(input, ErrorKind::Eof)))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parenthesis<'a, Field: ParseField + 'static>(input: &'a str) -> IResult<&'a str, Query<Field>> {
|
||||||
|
delimited(ws(tag("(")), expression, ws(tag(")"))).parse(input)
|
||||||
|
}
|
||||||
|
|
||||||
|
///////////
|
||||||
|
// Tests //
|
||||||
|
///////////
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use chrono::NaiveDate;
|
||||||
|
use rust_decimal::Decimal;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
core::{CoreError, DataValue},
|
||||||
|
query::Data,
|
||||||
|
};
|
||||||
|
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, PartialEq)]
|
||||||
|
pub enum Field1 {
|
||||||
|
A,
|
||||||
|
AB,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ParseField for Field1 {
|
||||||
|
fn parse(input: &str) -> Option<Self> {
|
||||||
|
match input {
|
||||||
|
"A" => Some(Field1::A),
|
||||||
|
"A.B" => Some(Field1::AB),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct FieldData {}
|
||||||
|
|
||||||
|
impl Data<Field1> for FieldData {
|
||||||
|
fn get_field(&self, field: &Field1) -> Result<DataValue, CoreError> {
|
||||||
|
if *field == Field1::A {
|
||||||
|
return Ok(Decimal::new(30, 0).into());
|
||||||
|
}
|
||||||
|
return Ok(DataValue::Null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_field() {
|
||||||
|
assert_eq!(field::<Field1>("A").unwrap().1, Field1::A);
|
||||||
|
assert_eq!(field::<Field1>("A.B").unwrap().1, Field1::AB);
|
||||||
|
assert!(field::<Field1>("C").is_err());
|
||||||
|
}
|
||||||
|
|
||||||
|
fn evaluate_query(input: &str) -> Result<DataValue, CoreError> {
|
||||||
|
let parsed = query::<Field1>(input).unwrap();
|
||||||
|
if parsed.0.trim().len() != 0 {
|
||||||
|
panic!("Full string not consumed")
|
||||||
|
}
|
||||||
|
|
||||||
|
print!("{:?}", parsed.1);
|
||||||
|
let context = FieldData {};
|
||||||
|
parsed.1.evaluate(&context)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn query_value() {
|
||||||
|
assert_eq!(evaluate_query("10").unwrap(), Decimal::new(10, 0).into());
|
||||||
|
assert_eq!(evaluate_query("1.2").unwrap(), Decimal::new(12, 1).into());
|
||||||
|
assert_eq!(evaluate_query("null").unwrap(), DataValue::Null);
|
||||||
|
assert_eq!(evaluate_query("true").unwrap(), true.into());
|
||||||
|
assert_eq!(evaluate_query("\"abc\"").unwrap(), "abc".into());
|
||||||
|
assert_eq!(
|
||||||
|
evaluate_query("2000-01-01").unwrap(),
|
||||||
|
NaiveDate::from_ymd_opt(2000, 01, 01).unwrap().into()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn query_field() {
|
||||||
|
assert_eq!(evaluate_query("A").unwrap(), Decimal::new(30, 0).into());
|
||||||
|
assert_eq!(evaluate_query("A.B").unwrap(), DataValue::Null);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn query_comparison() {
|
||||||
|
assert_eq!(evaluate_query("10 > 4").unwrap(), true.into());
|
||||||
|
assert_eq!(evaluate_query("10 < 4").unwrap(), false.into());
|
||||||
|
assert_eq!(evaluate_query("\"ab\" < \"cd\"").unwrap(), true.into());
|
||||||
|
assert_eq!(
|
||||||
|
evaluate_query("2000-02-01 > 2000-01-01").unwrap(),
|
||||||
|
true.into()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn query_regex() {
|
||||||
|
assert_eq!(evaluate_query("'abc' ~ 'abc'").unwrap(), true.into());
|
||||||
|
assert_eq!(evaluate_query("'abcd' ~ 'abc'").unwrap(), true.into());
|
||||||
|
assert_eq!(evaluate_query("'abcd' ~ '^abc$'").unwrap(), false.into());
|
||||||
|
assert_eq!(
|
||||||
|
evaluate_query("'Account:Sub' ~ 'Account'").unwrap(),
|
||||||
|
true.into()
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
evaluate_query("'Account:Sub' ~ 'Account:Sub'").unwrap(),
|
||||||
|
true.into()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn query_logical() {
|
||||||
|
assert_eq!(evaluate_query("true anD false").unwrap(), false.into());
|
||||||
|
assert_eq!(evaluate_query("true Or false").unwrap(), true.into());
|
||||||
|
assert_eq!(evaluate_query(" ! true").unwrap(), false.into());
|
||||||
|
assert_eq!(evaluate_query("!!true").unwrap(), true.into());
|
||||||
|
assert_eq!(
|
||||||
|
evaluate_query("true and true and false").unwrap(),
|
||||||
|
false.into()
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
evaluate_query("true and false or true and true").unwrap(),
|
||||||
|
true.into()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn query_combined() {
|
||||||
|
assert_eq!(evaluate_query("10 > 4 and 4 > 10").unwrap(), false.into());
|
||||||
|
assert_eq!(
|
||||||
|
evaluate_query("true and (false or true)").unwrap(),
|
||||||
|
true.into()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
47
src/parser/value.rs
Normal file
47
src/parser/value.rs
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
use nom::{
|
||||||
|
branch::alt,
|
||||||
|
bytes::complete::tag_no_case,
|
||||||
|
IResult, Parser,
|
||||||
|
};
|
||||||
|
use super::{decimal, parse_iso_date, quoted_string};
|
||||||
|
use crate::core::DataValue;
|
||||||
|
|
||||||
|
pub fn value<'a>(input: &'a str) -> IResult<&'a str, DataValue> {
|
||||||
|
alt((
|
||||||
|
tag_no_case("null").map(|_| DataValue::Null),
|
||||||
|
tag_no_case("true").map(|_| DataValue::Boolean(true)),
|
||||||
|
tag_no_case("false").map(|_| DataValue::Boolean(false)),
|
||||||
|
parse_iso_date.map(|v| DataValue::Date(v)),
|
||||||
|
decimal.map(|v| DataValue::Decimal(v)),
|
||||||
|
quoted_string.map(|v| v.into()),
|
||||||
|
// TODO: list, map
|
||||||
|
))
|
||||||
|
.parse(input)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use chrono::NaiveDate;
|
||||||
|
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_value() {
|
||||||
|
assert_eq!(value("nuLl").unwrap().1, DataValue::Null);
|
||||||
|
assert_eq!(value("TruE").unwrap().1, DataValue::Boolean(true));
|
||||||
|
assert_eq!(value("falSe").unwrap().1, DataValue::Boolean(false));
|
||||||
|
assert_eq!(
|
||||||
|
value("2000-01-01").unwrap().1,
|
||||||
|
DataValue::Date(NaiveDate::from_ymd_opt(2000, 01, 01).unwrap())
|
||||||
|
);
|
||||||
|
assert_eq!(value("10").unwrap().1, DataValue::Decimal(10.into()));
|
||||||
|
assert_eq!(
|
||||||
|
value("20000101").unwrap().1,
|
||||||
|
DataValue::Decimal(20000101.into())
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
value("\"abc\"").unwrap().1,
|
||||||
|
DataValue::String("abc".into())
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,169 +0,0 @@
|
|||||||
use std::{collections::HashMap, time::Instant};
|
|
||||||
|
|
||||||
use crate::core::{Amount, Ledger};
|
|
||||||
use chrono::NaiveDate;
|
|
||||||
use rust_decimal_macros::dec;
|
|
||||||
|
|
||||||
use super::{
|
|
||||||
base::{self, DataValue, Function},
|
|
||||||
functions::ComparisonFunction,
|
|
||||||
transaction::{PostingData, PostingField, TransactionField},
|
|
||||||
};
|
|
||||||
|
|
||||||
pub enum Query {
|
|
||||||
StartDate(NaiveDate),
|
|
||||||
EndDate(NaiveDate),
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn balance2(
|
|
||||||
ledger: &Ledger,
|
|
||||||
end_date: NaiveDate,
|
|
||||||
convert_to_unit: Option<&str>,
|
|
||||||
) -> HashMap<u32, Vec<Amount>> {
|
|
||||||
let q = ComparisonFunction::new(
|
|
||||||
"<=",
|
|
||||||
base::Query::from_field(PostingField::Transaction(TransactionField::Date)),
|
|
||||||
base::Query::from(DataValue::from(end_date)),
|
|
||||||
)
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
let convert_to_unit = convert_to_unit.map(|u| ledger.get_unit_by_symbol(u).unwrap());
|
|
||||||
|
|
||||||
let postings = ledger
|
|
||||||
.get_transactions()
|
|
||||||
.iter()
|
|
||||||
.map(|t| {
|
|
||||||
t.get_postings().iter().map(|p| PostingData {
|
|
||||||
ledger,
|
|
||||||
posting: p,
|
|
||||||
parent_transaction: t,
|
|
||||||
})
|
|
||||||
})
|
|
||||||
.flatten();
|
|
||||||
|
|
||||||
let filtered_postings =
|
|
||||||
postings.filter(|data| q.evaluate(data).map(|v| bool::from(v)).unwrap_or(false));
|
|
||||||
|
|
||||||
let mut accounts = HashMap::new();
|
|
||||||
for posting_data in filtered_postings {
|
|
||||||
let posting = posting_data.posting;
|
|
||||||
let mut amount = *posting.get_amount();
|
|
||||||
if let Some(new_unit) = convert_to_unit {
|
|
||||||
if amount.unit_id != new_unit.get_id() {
|
|
||||||
let price = ledger.get_price_on_date(end_date, amount.unit_id, new_unit.get_id());
|
|
||||||
if let Some(price) = price {
|
|
||||||
amount = Amount {
|
|
||||||
value: amount.value * price,
|
|
||||||
unit_id: new_unit.get_id(),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
let account_vals = accounts
|
|
||||||
.entry(posting.get_account_id())
|
|
||||||
.or_insert(HashMap::new());
|
|
||||||
let a = account_vals.entry(amount.unit_id).or_insert(dec!(0));
|
|
||||||
*a += amount.value;
|
|
||||||
}
|
|
||||||
|
|
||||||
accounts
|
|
||||||
.iter()
|
|
||||||
.map(|(&k, v)| {
|
|
||||||
(
|
|
||||||
k,
|
|
||||||
v.into_iter()
|
|
||||||
.map(|(&unit_id, &value)| Amount { value, unit_id })
|
|
||||||
.collect(),
|
|
||||||
)
|
|
||||||
})
|
|
||||||
.collect()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn balance3(ledger: &Ledger, query: &base::Query<PostingField>) -> HashMap<u32, Vec<Amount>> {
|
|
||||||
// let q = ComparisonFunction::new(
|
|
||||||
// "<=",
|
|
||||||
// base::Query::from_field(PostingField::Transaction(TransactionField::Date)),
|
|
||||||
// base::Query::from(DataValue::from(end_date)),
|
|
||||||
// )
|
|
||||||
// .unwrap();
|
|
||||||
|
|
||||||
let t0 = Instant::now();
|
|
||||||
|
|
||||||
let postings = ledger
|
|
||||||
.get_transactions()
|
|
||||||
.iter()
|
|
||||||
.map(|t| {
|
|
||||||
t.get_postings().iter().map(|p| PostingData {
|
|
||||||
ledger,
|
|
||||||
posting: p,
|
|
||||||
parent_transaction: t,
|
|
||||||
})
|
|
||||||
})
|
|
||||||
.flatten();
|
|
||||||
|
|
||||||
let t1 = Instant::now();
|
|
||||||
|
|
||||||
let filtered_postings =
|
|
||||||
postings.filter(|data| query.evaluate(data).map(|v| bool::from(v)).unwrap_or(false));
|
|
||||||
|
|
||||||
let t2 = Instant::now();
|
|
||||||
|
|
||||||
// println!("{:?} {:?}", t1-t0, t2-t1);
|
|
||||||
|
|
||||||
let mut accounts = HashMap::new();
|
|
||||||
for posting_data in filtered_postings {
|
|
||||||
let posting = posting_data.posting;
|
|
||||||
let amount = posting.get_amount();
|
|
||||||
let account_vals = accounts
|
|
||||||
.entry(posting.get_account_id())
|
|
||||||
.or_insert(HashMap::new());
|
|
||||||
let a = account_vals.entry(amount.unit_id).or_insert(dec!(0));
|
|
||||||
*a += amount.value;
|
|
||||||
}
|
|
||||||
|
|
||||||
accounts
|
|
||||||
.iter()
|
|
||||||
.map(|(&k, v)| {
|
|
||||||
(
|
|
||||||
k,
|
|
||||||
v.into_iter()
|
|
||||||
.map(|(&unit_id, &value)| Amount { value, unit_id })
|
|
||||||
.collect(),
|
|
||||||
)
|
|
||||||
})
|
|
||||||
.collect()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn balance(ledger: &Ledger, query: &[Query]) -> HashMap<u32, Vec<Amount>> {
|
|
||||||
let relevant_transactions = ledger.get_transactions().iter().filter(|txn| {
|
|
||||||
query.iter().all(|q| match q {
|
|
||||||
Query::StartDate(date) => txn.get_date() >= *date,
|
|
||||||
Query::EndDate(date) => txn.get_date() <= *date,
|
|
||||||
})
|
|
||||||
});
|
|
||||||
|
|
||||||
let mut accounts = HashMap::new();
|
|
||||||
|
|
||||||
for txn in relevant_transactions.clone() {
|
|
||||||
for posting in txn.get_postings() {
|
|
||||||
let amount = posting.get_amount();
|
|
||||||
let account_vals = accounts
|
|
||||||
.entry(posting.get_account_id())
|
|
||||||
.or_insert(HashMap::new());
|
|
||||||
let a = account_vals.entry(amount.unit_id).or_insert(dec!(0));
|
|
||||||
*a += amount.value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
accounts
|
|
||||||
.iter()
|
|
||||||
.map(|(&k, v)| {
|
|
||||||
(
|
|
||||||
k,
|
|
||||||
v.into_iter()
|
|
||||||
.map(|(&unit_id, &value)| Amount { value, unit_id })
|
|
||||||
.collect(),
|
|
||||||
)
|
|
||||||
})
|
|
||||||
.collect()
|
|
||||||
}
|
|
||||||
@ -1,294 +0,0 @@
|
|||||||
use std::time::{Duration, Instant};
|
|
||||||
|
|
||||||
use crate::core::CoreError;
|
|
||||||
use regex::Regex;
|
|
||||||
|
|
||||||
use super::base::{Data, DataValue, Function, Query, StringData};
|
|
||||||
|
|
||||||
#[derive(PartialEq, Debug)]
|
|
||||||
pub enum ComparisonOperator {
|
|
||||||
EQ,
|
|
||||||
NEQ,
|
|
||||||
GT,
|
|
||||||
LT,
|
|
||||||
GTE,
|
|
||||||
LTE,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct ComparisonFunction<'a, Field> {
|
|
||||||
op: ComparisonOperator,
|
|
||||||
left: Query<'a, Field>,
|
|
||||||
right: Query<'a, Field>,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub enum StringComparisonOperator<'a> {
|
|
||||||
Func(&'a (dyn Fn(&str) -> bool + 'a)),
|
|
||||||
Regex(Regex),
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct StringComparisonFunction<'a, Field> {
|
|
||||||
op: StringComparisonOperator<'a>,
|
|
||||||
val: Query<'a, Field>,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct SubAccountFunction<'a, Field> {
|
|
||||||
account_name: StringData<'a>,
|
|
||||||
val: Query<'a, Field>,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct RegexFunction<'a, Field> {
|
|
||||||
left: Query<'a, Field>,
|
|
||||||
regex: Regex,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(PartialEq, Debug)]
|
|
||||||
pub enum LogicalOperator {
|
|
||||||
AND,
|
|
||||||
OR,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct LogicalFunction<'a, Field> {
|
|
||||||
op: LogicalOperator,
|
|
||||||
left: Query<'a, Field>,
|
|
||||||
right: Query<'a, Field>,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct NotFunction<'a, Field> {
|
|
||||||
value: Query<'a, Field>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'a, Field> ComparisonFunction<'a, Field> {
|
|
||||||
pub fn new(
|
|
||||||
op: &str,
|
|
||||||
left: Query<'a, Field>,
|
|
||||||
right: Query<'a, Field>,
|
|
||||||
) -> Result<Self, CoreError> {
|
|
||||||
let op = match op {
|
|
||||||
"==" => ComparisonOperator::EQ,
|
|
||||||
"!=" => ComparisonOperator::NEQ,
|
|
||||||
">" => ComparisonOperator::GT,
|
|
||||||
"<" => ComparisonOperator::LT,
|
|
||||||
">=" => ComparisonOperator::GTE,
|
|
||||||
"<=" => ComparisonOperator::LTE,
|
|
||||||
_ => return Err("Invalid Operator".into()),
|
|
||||||
};
|
|
||||||
Ok(ComparisonFunction { op, left, right })
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn new_op(
|
|
||||||
op: ComparisonOperator,
|
|
||||||
left: Query<'a, Field>,
|
|
||||||
right: Query<'a, Field>,
|
|
||||||
) -> Self {
|
|
||||||
ComparisonFunction { op, left, right }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'a, Field> SubAccountFunction<'a, Field> {
|
|
||||||
pub fn new(account: StringData<'a>, val: Query<'a, Field>) -> Self {
|
|
||||||
SubAccountFunction { account_name: account, val }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'a, Field> StringComparisonFunction<'a, Field> {
|
|
||||||
pub fn new_func(
|
|
||||||
val: Query<'a, Field>,
|
|
||||||
func: &'a (impl Fn(&str) -> bool + 'a),
|
|
||||||
) -> Result<Self, CoreError> {
|
|
||||||
Ok(StringComparisonFunction { val, op: StringComparisonOperator::Func(func) })
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn new_regex(val: Query<'a, Field>, regex: &str) -> Result<Self, CoreError> {
|
|
||||||
let regex = Regex::new(regex).map_err(|_| CoreError::from("Unable to parse regex"))?;
|
|
||||||
Ok(StringComparisonFunction { val, op: StringComparisonOperator::Regex(regex) })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'a, Field> RegexFunction<'a, Field> {
|
|
||||||
pub fn new(left: Query<'a, Field>, regex: &str) -> Result<Self, CoreError> {
|
|
||||||
let regex = Regex::new(regex).map_err(|_| CoreError::from("Unable to parse regex"))?;
|
|
||||||
Ok(RegexFunction { left, regex })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'a, Field> Function<Field> for ComparisonFunction<'a, Field> {
|
|
||||||
fn evaluate(&self, context: &dyn Data<Field>) -> Result<DataValue, CoreError> {
|
|
||||||
let left = self.left.evaluate(context)?;
|
|
||||||
let right = self.right.evaluate(context)?;
|
|
||||||
|
|
||||||
match self.op {
|
|
||||||
ComparisonOperator::EQ => Ok(DataValue::Boolean(left == right)),
|
|
||||||
ComparisonOperator::NEQ => Ok(DataValue::Boolean(left != right)),
|
|
||||||
ComparisonOperator::GT => Ok(DataValue::Boolean(left > right)),
|
|
||||||
ComparisonOperator::LT => Ok(DataValue::Boolean(left < right)),
|
|
||||||
ComparisonOperator::GTE => Ok(DataValue::Boolean(left >= right)),
|
|
||||||
ComparisonOperator::LTE => Ok(DataValue::Boolean(left <= right)),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'a, Field> Function<Field> for StringComparisonFunction<'a, Field> {
|
|
||||||
fn evaluate(&self, context: &dyn Data<Field>) -> Result<DataValue, CoreError> {
|
|
||||||
let val = self.val.evaluate(context)?;
|
|
||||||
|
|
||||||
if let DataValue::String(val) = val {
|
|
||||||
match &self.op {
|
|
||||||
StringComparisonOperator::Func(func) => Ok(DataValue::Boolean(func(val.as_ref()))),
|
|
||||||
StringComparisonOperator::Regex(regex) => {
|
|
||||||
Ok(DataValue::Boolean(regex.is_match(val.as_ref())))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Ok(DataValue::Boolean(self.regex.is_match(left.as_ref())))
|
|
||||||
} else {
|
|
||||||
Err("Cannot use REGEX operation on non string types".into())
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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())
|
|
||||||
// }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'a, Field> Function<Field> for SubAccountFunction<'a, Field> {
|
|
||||||
fn evaluate(&self, context: &dyn Data<Field>) -> Result<DataValue, CoreError> {
|
|
||||||
let val = self.val.evaluate(context)?;
|
|
||||||
|
|
||||||
if let DataValue::String(val) = val {
|
|
||||||
Ok(DataValue::Boolean(
|
|
||||||
val.as_ref()
|
|
||||||
.strip_prefix(self.account_name.as_ref())
|
|
||||||
.map(|n| n.is_empty() || n.starts_with(":"))
|
|
||||||
.unwrap_or(false),
|
|
||||||
))
|
|
||||||
} else {
|
|
||||||
Err("Cannot compare account name on non string types".into())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'a, Field> Function<Field> for RegexFunction<'a, 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())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'a, Field> LogicalFunction<'a, Field> {
|
|
||||||
pub fn new(
|
|
||||||
op: &str,
|
|
||||||
left: Query<'a, Field>,
|
|
||||||
right: Query<'a, Field>,
|
|
||||||
) -> Result<Self, CoreError> {
|
|
||||||
if op.eq_ignore_ascii_case("and") {
|
|
||||||
Ok(LogicalFunction { op: LogicalOperator::AND, left, right })
|
|
||||||
} else if op.eq_ignore_ascii_case("or") {
|
|
||||||
Ok(LogicalFunction { op: LogicalOperator::OR, left, right })
|
|
||||||
} else {
|
|
||||||
Err("Invalid logical operator".into())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'a, Field> NotFunction<'a, Field> {
|
|
||||||
pub fn new(value: Query<'a, Field>) -> Self {
|
|
||||||
NotFunction { value }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'a, Field> Function<Field> for LogicalFunction<'a, Field> {
|
|
||||||
fn evaluate(&self, context: &dyn Data<Field>) -> Result<DataValue, CoreError> {
|
|
||||||
// More verbose to try and avoid doing right side computation
|
|
||||||
let value: bool = match self.op {
|
|
||||||
LogicalOperator::AND => {
|
|
||||||
if !bool::from(self.left.evaluate(context)?) {
|
|
||||||
false
|
|
||||||
} else {
|
|
||||||
self.right.evaluate(context)?.into()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
LogicalOperator::OR => {
|
|
||||||
if bool::from(self.left.evaluate(context)?) {
|
|
||||||
true
|
|
||||||
} else {
|
|
||||||
self.right.evaluate(context)?.into()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
Ok(DataValue::Boolean(value))
|
|
||||||
|
|
||||||
// let left = self.left.evaluate(context)?.into();
|
|
||||||
// let right = self.right.evaluate(context)?.into();
|
|
||||||
|
|
||||||
// match self.op {
|
|
||||||
// LogicalOperator::AND => Ok(DataValue::Boolean(left && right)),
|
|
||||||
// LogicalOperator::OR => Ok(DataValue::Boolean(left || right)),
|
|
||||||
// }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'a, Field> Function<Field> for NotFunction<'a, Field> {
|
|
||||||
fn evaluate(&self, context: &dyn Data<Field>) -> Result<DataValue, CoreError> {
|
|
||||||
let value: bool = self.value.evaluate(context)?.into();
|
|
||||||
|
|
||||||
Ok(DataValue::Boolean(!value))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Tests section
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use super::*;
|
|
||||||
|
|
||||||
enum TestField {}
|
|
||||||
|
|
||||||
struct TestData {}
|
|
||||||
|
|
||||||
impl<TestField> Data<TestField> for TestData {
|
|
||||||
fn get_field(&self, _: &TestField) -> Result<DataValue, CoreError> {
|
|
||||||
Err("".into())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn comparison_function_evaluate() {
|
|
||||||
let value1: DataValue = 5.into();
|
|
||||||
let value2: DataValue = 10.into();
|
|
||||||
let context = TestData {};
|
|
||||||
|
|
||||||
let comparison_function =
|
|
||||||
ComparisonFunction::<TestField>::new(">", value1.into(), value2.into()).unwrap();
|
|
||||||
assert_eq!(comparison_function.op, ComparisonOperator::GT);
|
|
||||||
assert_eq!(comparison_function.evaluate(&context), Ok(false.into()));
|
|
||||||
|
|
||||||
let value = Query::from_fn(comparison_function);
|
|
||||||
assert_eq!(value.evaluate(&context), Ok(false.into()));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn logical_function_evaluate() {
|
|
||||||
let value1: DataValue = 1.into();
|
|
||||||
let value2: DataValue = false.into();
|
|
||||||
let context = TestData {};
|
|
||||||
|
|
||||||
let logical_function_and =
|
|
||||||
LogicalFunction::<TestField>::new("and", value1.clone().into(), value2.clone().into())
|
|
||||||
.unwrap();
|
|
||||||
assert_eq!(logical_function_and.op, LogicalOperator::AND);
|
|
||||||
assert_eq!(logical_function_and.evaluate(&context), Ok(false.into()));
|
|
||||||
|
|
||||||
let logical_function_or =
|
|
||||||
LogicalFunction::<TestField>::new("or", value1.into(), value2.into()).unwrap();
|
|
||||||
assert_eq!(logical_function_or.op, LogicalOperator::OR);
|
|
||||||
assert_eq!(logical_function_or.evaluate(&context), Ok(true.into()));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,9 +0,0 @@
|
|||||||
mod balance;
|
|
||||||
mod postings;
|
|
||||||
pub mod base;
|
|
||||||
pub mod functions;
|
|
||||||
pub mod parser;
|
|
||||||
pub mod transaction;
|
|
||||||
|
|
||||||
pub use balance::*;
|
|
||||||
pub use postings::*;
|
|
||||||
@ -1,142 +0,0 @@
|
|||||||
use nom::{
|
|
||||||
branch::alt, bytes::complete::{tag, tag_no_case, take_until}, character::complete::space0, error::{Error, ErrorKind}, multi::fold_many0, sequence::{delimited, preceded, tuple}, AsChar, IResult, InputTakeAtPosition, Parser
|
|
||||||
};
|
|
||||||
|
|
||||||
use crate::{
|
|
||||||
parser::{decimal, parse_iso_date, quoted_string},
|
|
||||||
queries::{
|
|
||||||
base::{DataValue, Query},
|
|
||||||
functions::{ComparisonFunction, ComparisonOperator, LogicalFunction},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
pub trait ParseField: Sized {
|
|
||||||
fn parse(input: &str) -> Option<Self>;
|
|
||||||
}
|
|
||||||
|
|
||||||
// fn query<'a, Field: ParseField + 'static>(
|
|
||||||
// input: &'a str,
|
|
||||||
// ) -> IResult<&'a str, Query<'a, Field>> {
|
|
||||||
// // TODO: uncommented, is this right?
|
|
||||||
// delimited(
|
|
||||||
// space0,
|
|
||||||
// alt((
|
|
||||||
// parenthesis,
|
|
||||||
// value.map(|v| v.into()),
|
|
||||||
// field.map(|v| Query::from_field(v)),
|
|
||||||
// comparison_function::<Field>.map(|v| Query::from_fn(v)),
|
|
||||||
// logical_function::<Field>.map(|v| Query::from_fn(v)),
|
|
||||||
// )),
|
|
||||||
// space0,
|
|
||||||
// )
|
|
||||||
// .parse(input)
|
|
||||||
// }
|
|
||||||
|
|
||||||
fn value<'a>(input: &'a str) -> IResult<&'a str, DataValue<'a>> {
|
|
||||||
alt((
|
|
||||||
tag_no_case("null").map(|_| DataValue::Null),
|
|
||||||
tag_no_case("true").map(|_| DataValue::Boolean(true)),
|
|
||||||
tag_no_case("false").map(|_| DataValue::Boolean(false)),
|
|
||||||
parse_iso_date.map(|v| v.into()),
|
|
||||||
decimal.map(|v| v.into()),
|
|
||||||
quoted_string.map(|v| v.into()),
|
|
||||||
))
|
|
||||||
.parse(input)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn field<'a, Field: ParseField>(input: &str) -> IResult<&str, Field> {
|
|
||||||
input
|
|
||||||
.split_at_position1_complete(
|
|
||||||
|item| !item.is_alphanum() || item != '.',
|
|
||||||
ErrorKind::AlphaNumeric,
|
|
||||||
)
|
|
||||||
.and_then(|v| {
|
|
||||||
Field::parse(v.1)
|
|
||||||
.map(|f| (v.0, f))
|
|
||||||
.ok_or(nom::Err::Error(Error::new(input, ErrorKind::Eof)))
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// fn parenthesis<'a, Field: ParseField + 'static>(
|
|
||||||
// input: &'a str,
|
|
||||||
// ) -> IResult<&'a str, Query<'a, Field>> {
|
|
||||||
// delimited(tag("("), query, tag(")")).parse(input)
|
|
||||||
// }
|
|
||||||
|
|
||||||
// fn query_factor<'a, Field: ParseField + 'static>(input: &'a str) -> IResult<&'a str, Query<'a, Field>> {
|
|
||||||
// delimited(
|
|
||||||
// space0,
|
|
||||||
// alt((
|
|
||||||
// value.map(|v| v.into()),
|
|
||||||
// field.map(|v| Query::from_field(v)),
|
|
||||||
// parenthesis,
|
|
||||||
// )),
|
|
||||||
// space0,
|
|
||||||
// )
|
|
||||||
// .parse(input)
|
|
||||||
// }
|
|
||||||
|
|
||||||
// fn query_comparison<'a, Field: ParseField + 'static>(
|
|
||||||
// input: &'a str,
|
|
||||||
// ) -> IResult<&'a str, Query<'a, Field>> {
|
|
||||||
// let (input, initial) = query_factor(input)?;
|
|
||||||
|
|
||||||
// let op = alt((
|
|
||||||
// tag("=").map(|_| ComparisonOperator::EQ),
|
|
||||||
// tag("!=").map(|_| ComparisonOperator::NEQ),
|
|
||||||
// tag(">").map(|_| ComparisonOperator::GT),
|
|
||||||
// tag("<").map(|_| ComparisonOperator::LT),
|
|
||||||
// tag(">=").map(|_| ComparisonOperator::GTE),
|
|
||||||
// tag("<=").map(|_| ComparisonOperator::LTE),
|
|
||||||
// ));
|
|
||||||
|
|
||||||
// loop {
|
|
||||||
|
|
||||||
// }
|
|
||||||
|
|
||||||
|
|
||||||
// fold_many0(tuple((op, query_factor)), move || initial, |acc, (op, val)| {
|
|
||||||
// Query::from_fn(ComparisonFunction::new_op(op, acc, val))
|
|
||||||
// }).parse(input)
|
|
||||||
// }
|
|
||||||
|
|
||||||
// fn query_logical_and<'a, Field: ParseField>(
|
|
||||||
// input: &str,
|
|
||||||
// ) -> IResult<&'a str, Query<'a, Field>> {
|
|
||||||
// let (input_next, lhs) = query_factor(input)?;
|
|
||||||
|
|
||||||
// fold_many0(preceded(tag_no_case("and"), query_factor), init, g)
|
|
||||||
|
|
||||||
// // loop {
|
|
||||||
// // let rhs_result = tuple((
|
|
||||||
// // alt((tag_no_case("and"), tag_no_case("or"))),
|
|
||||||
// // query_factor,
|
|
||||||
// // )).parse(input_next);
|
|
||||||
// // if let Ok(rhs_result) = rhs_result {
|
|
||||||
// // input_next = rhs_result.0;
|
|
||||||
// // } else {
|
|
||||||
// // break;
|
|
||||||
// // }
|
|
||||||
// // };
|
|
||||||
|
|
||||||
|
|
||||||
// // tuple(take_until(alt((tag("")))))
|
|
||||||
// }
|
|
||||||
|
|
||||||
// fn logical_function<'a, Field: ParseField>(
|
|
||||||
// input: &str,
|
|
||||||
// ) -> IResult<&str, LogicalFunction<'a, Field>> {
|
|
||||||
// let lhs =
|
|
||||||
|
|
||||||
// }
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use super::*;
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn parse_string() {
|
|
||||||
assert_eq!(quoted_string("\"test\"").unwrap().1, "test");
|
|
||||||
assert_eq!(quoted_string("\"te\\\"st\"").unwrap().1, "te\\\"st");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1 +0,0 @@
|
|||||||
mod functions;
|
|
||||||
@ -1,11 +0,0 @@
|
|||||||
use crate::core::Ledger;
|
|
||||||
|
|
||||||
|
|
||||||
pub struct PostingQuery {
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
pub fn query_postings(ledger: &Ledger, query: PostingQuery) {
|
|
||||||
|
|
||||||
}
|
|
||||||
75
src/query/balance.rs
Normal file
75
src/query/balance.rs
Normal file
@ -0,0 +1,75 @@
|
|||||||
|
use chrono::NaiveDate;
|
||||||
|
use rust_decimal_macros::dec;
|
||||||
|
use std::collections::HashMap;
|
||||||
|
|
||||||
|
use super::{
|
||||||
|
transaction::{PostingData, PostingField},
|
||||||
|
Query,
|
||||||
|
};
|
||||||
|
use crate::core::{Amount, Ledger};
|
||||||
|
|
||||||
|
pub fn balance(
|
||||||
|
ledger: &Ledger,
|
||||||
|
filter: Option<&Query<PostingField>>,
|
||||||
|
convert_to_unit: Option<(&str, NaiveDate)>,
|
||||||
|
) -> HashMap<u32, Vec<Amount>> {
|
||||||
|
let convert_to_unit = convert_to_unit.map(|u| (ledger.get_unit_by_symbol(u.0).unwrap(), u.1));
|
||||||
|
|
||||||
|
let postings = ledger
|
||||||
|
.get_transactions()
|
||||||
|
.iter()
|
||||||
|
.map(|t| {
|
||||||
|
t.get_postings().iter().map(|p| PostingData {
|
||||||
|
ledger,
|
||||||
|
posting: p,
|
||||||
|
parent_transaction: t,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.flatten();
|
||||||
|
|
||||||
|
let filter = match filter {
|
||||||
|
Some(filter) => filter,
|
||||||
|
None => &Query::Value(true.into()),
|
||||||
|
};
|
||||||
|
|
||||||
|
let filtered_postings = postings.filter(|data| {
|
||||||
|
filter
|
||||||
|
.evaluate(data)
|
||||||
|
.map(|v| bool::from(v))
|
||||||
|
.unwrap_or(false)
|
||||||
|
});
|
||||||
|
|
||||||
|
let mut accounts = HashMap::new();
|
||||||
|
for posting_data in filtered_postings {
|
||||||
|
let posting = posting_data.posting;
|
||||||
|
let mut amount = *posting.get_amount();
|
||||||
|
if let Some((new_unit, unit_date)) = convert_to_unit {
|
||||||
|
if amount.unit_id != new_unit.get_id() {
|
||||||
|
let price = ledger.get_price_on_date(unit_date, amount.unit_id, new_unit.get_id());
|
||||||
|
if let Some(price) = price {
|
||||||
|
amount = Amount {
|
||||||
|
value: amount.value * price,
|
||||||
|
unit_id: new_unit.get_id(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let account_vals = accounts
|
||||||
|
.entry(posting.get_account_id())
|
||||||
|
.or_insert(HashMap::new());
|
||||||
|
let a = account_vals.entry(amount.unit_id).or_insert(dec!(0));
|
||||||
|
*a += amount.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
accounts
|
||||||
|
.iter()
|
||||||
|
.map(|(&k, v)| {
|
||||||
|
(
|
||||||
|
k,
|
||||||
|
v.into_iter()
|
||||||
|
.map(|(&unit_id, &value)| Amount { value, unit_id })
|
||||||
|
.collect(),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
202
src/query/functions_comparison.rs
Normal file
202
src/query/functions_comparison.rs
Normal file
@ -0,0 +1,202 @@
|
|||||||
|
use regex::{Regex, RegexBuilder};
|
||||||
|
use std::fmt::Debug;
|
||||||
|
|
||||||
|
use super::{Data, Function, Query};
|
||||||
|
use crate::core::{CoreError, DataValue, StringData};
|
||||||
|
|
||||||
|
#[derive(PartialEq, Debug, Clone)]
|
||||||
|
pub enum ComparisonOperator {
|
||||||
|
EQ,
|
||||||
|
NEQ,
|
||||||
|
GT,
|
||||||
|
LT,
|
||||||
|
GTE,
|
||||||
|
LTE,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub struct ComparisonFunction<Field: Clone + Debug + 'static> {
|
||||||
|
op: ComparisonOperator,
|
||||||
|
left: Query<Field>,
|
||||||
|
right: Query<Field>,
|
||||||
|
}
|
||||||
|
|
||||||
|
// #[derive(Clone, Debug)]
|
||||||
|
// pub enum StringComparisonOperator<'a> {
|
||||||
|
// Func(&'a (dyn Fn(&str) -> bool + 'a)),
|
||||||
|
// Regex(Regex),
|
||||||
|
// }
|
||||||
|
|
||||||
|
// #[derive(Clone)]
|
||||||
|
// pub struct StringComparisonFunction<'a, Field: Clone + 'static> {
|
||||||
|
// op: StringComparisonOperator<'a>,
|
||||||
|
// val: Query<Field>,
|
||||||
|
// }
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub struct SubAccountFunction<Field: Clone + Debug + 'static> {
|
||||||
|
account_name: StringData<'static>,
|
||||||
|
val: Query<Field>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub struct RegexFunction<Field: Clone + Debug + 'static> {
|
||||||
|
left: Query<Field>,
|
||||||
|
regex: Regex,
|
||||||
|
}
|
||||||
|
|
||||||
|
/////////////////////
|
||||||
|
// Implementations //
|
||||||
|
/////////////////////
|
||||||
|
|
||||||
|
impl<'a, Field: Clone + Debug> ComparisonFunction<Field> {
|
||||||
|
pub fn new(op: &str, left: Query<Field>, right: Query<Field>) -> Result<Self, CoreError> {
|
||||||
|
let op = match op {
|
||||||
|
"==" => ComparisonOperator::EQ,
|
||||||
|
"!=" => ComparisonOperator::NEQ,
|
||||||
|
">" => ComparisonOperator::GT,
|
||||||
|
"<" => ComparisonOperator::LT,
|
||||||
|
">=" => ComparisonOperator::GTE,
|
||||||
|
"<=" => ComparisonOperator::LTE,
|
||||||
|
_ => return Err("Invalid Operator".into()),
|
||||||
|
};
|
||||||
|
Ok(ComparisonFunction { op, left, right })
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn new_op(op: ComparisonOperator, left: Query<Field>, right: Query<Field>) -> Self {
|
||||||
|
ComparisonFunction { op, left, right }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<Field: Clone + Debug> Function<Field> for ComparisonFunction<Field> {
|
||||||
|
fn evaluate(&self, context: &dyn Data<Field>) -> Result<DataValue, CoreError> {
|
||||||
|
let left = self.left.evaluate(context)?;
|
||||||
|
let right = self.right.evaluate(context)?;
|
||||||
|
|
||||||
|
match self.op {
|
||||||
|
ComparisonOperator::EQ => Ok(DataValue::Boolean(left == right)),
|
||||||
|
ComparisonOperator::NEQ => Ok(DataValue::Boolean(left != right)),
|
||||||
|
ComparisonOperator::GT => Ok(DataValue::Boolean(left > right)),
|
||||||
|
ComparisonOperator::LT => Ok(DataValue::Boolean(left < right)),
|
||||||
|
ComparisonOperator::GTE => Ok(DataValue::Boolean(left >= right)),
|
||||||
|
ComparisonOperator::LTE => Ok(DataValue::Boolean(left <= right)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// impl<'a, Field: Clone> StringComparisonFunction<'a, Field> {
|
||||||
|
// pub fn new_func(
|
||||||
|
// val: Query<Field>,
|
||||||
|
// func: &'a (impl Fn(&str) -> bool + 'a),
|
||||||
|
// ) -> Result<Self, CoreError> {
|
||||||
|
// Ok(StringComparisonFunction { val, op: StringComparisonOperator::Func(func) })
|
||||||
|
// }
|
||||||
|
|
||||||
|
// pub fn new_regex(val: Query<Field>, regex: &str) -> Result<Self, CoreError> {
|
||||||
|
// let regex = Regex::new(regex).map_err(|_| CoreError::from("Unable to parse regex"))?;
|
||||||
|
// Ok(StringComparisonFunction { val, op: StringComparisonOperator::Regex(regex) })
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
// impl<'a, Field: Clone> Function<Field> for StringComparisonFunction<'a, Field> {
|
||||||
|
// fn evaluate(&self, context: &dyn Data<Field>) -> Result<DataValue, CoreError> {
|
||||||
|
// let val = self.val.evaluate(context)?;
|
||||||
|
|
||||||
|
// if let DataValue::String(val) = val {
|
||||||
|
// match &self.op {
|
||||||
|
// StringComparisonOperator::Func(func) => Ok(DataValue::Boolean(func(val.as_ref()))),
|
||||||
|
// StringComparisonOperator::Regex(regex) => {
|
||||||
|
// Ok(DataValue::Boolean(regex.is_match(val.as_ref())))
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// // Ok(DataValue::Boolean(self.regex.is_match(left.as_ref())))
|
||||||
|
// } else {
|
||||||
|
// Err("Cannot use REGEX operation on non string types".into())
|
||||||
|
// }
|
||||||
|
|
||||||
|
// // 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())
|
||||||
|
// // }
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
impl<'a, Field: Clone + Debug> SubAccountFunction<Field> {
|
||||||
|
pub fn new(account: StringData<'a>, val: Query<Field>) -> Self {
|
||||||
|
SubAccountFunction { account_name: account.into_owned(), val }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<Field: Clone + Debug> Function<Field> for SubAccountFunction<Field> {
|
||||||
|
fn evaluate(&self, context: &dyn Data<Field>) -> Result<DataValue, CoreError> {
|
||||||
|
let val = self.val.evaluate(context)?;
|
||||||
|
|
||||||
|
if let DataValue::String(val) = val {
|
||||||
|
Ok(DataValue::Boolean(
|
||||||
|
val.as_ref()
|
||||||
|
.strip_prefix(self.account_name.as_ref())
|
||||||
|
.map(|n| n.is_empty() || n.starts_with(":"))
|
||||||
|
.unwrap_or(false),
|
||||||
|
))
|
||||||
|
} else {
|
||||||
|
Err("Cannot compare account name on non string types".into())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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"))?;
|
||||||
|
Ok(RegexFunction { left, regex })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
///////////
|
||||||
|
// Tests //
|
||||||
|
///////////
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
enum TestField {}
|
||||||
|
|
||||||
|
struct TestData {}
|
||||||
|
|
||||||
|
impl<TestField> Data<TestField> for TestData {
|
||||||
|
fn get_field(&self, _: &TestField) -> Result<DataValue, CoreError> {
|
||||||
|
Err("".into())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn comparison_function_evaluate() {
|
||||||
|
let value1: DataValue = 5.into();
|
||||||
|
let value2: DataValue = 10.into();
|
||||||
|
let context = TestData {};
|
||||||
|
|
||||||
|
let comparison_function =
|
||||||
|
ComparisonFunction::<TestField>::new(">", value1.into(), value2.into()).unwrap();
|
||||||
|
assert_eq!(comparison_function.op, ComparisonOperator::GT);
|
||||||
|
assert_eq!(comparison_function.evaluate(&context), Ok(false.into()));
|
||||||
|
|
||||||
|
let value = Query::from_fn(comparison_function);
|
||||||
|
assert_eq!(value.evaluate(&context), Ok(false.into()));
|
||||||
|
}
|
||||||
|
}
|
||||||
125
src/query/functions_logical.rs
Normal file
125
src/query/functions_logical.rs
Normal file
@ -0,0 +1,125 @@
|
|||||||
|
use super::{Data, Function, Query};
|
||||||
|
use crate::core::{CoreError, DataValue};
|
||||||
|
use std::fmt::Debug;
|
||||||
|
|
||||||
|
#[derive(PartialEq, Debug, Clone)]
|
||||||
|
pub enum LogicalOperator {
|
||||||
|
AND,
|
||||||
|
OR,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct LogicalFunction<Field: Clone + Debug + 'static> {
|
||||||
|
op: LogicalOperator,
|
||||||
|
left: Query<Field>,
|
||||||
|
right: Query<Field>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct NotFunction<Field: Clone + Debug + 'static> {
|
||||||
|
value: Query<Field>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/////////////////////
|
||||||
|
// Implementations //
|
||||||
|
/////////////////////
|
||||||
|
|
||||||
|
impl<Field: Clone + Debug> LogicalFunction<Field> {
|
||||||
|
pub fn new(op: &str, left: Query<Field>, right: Query<Field>) -> Result<Self, CoreError> {
|
||||||
|
if op.eq_ignore_ascii_case("and") {
|
||||||
|
Ok(LogicalFunction { op: LogicalOperator::AND, left, right })
|
||||||
|
} else if op.eq_ignore_ascii_case("or") {
|
||||||
|
Ok(LogicalFunction { op: LogicalOperator::OR, left, right })
|
||||||
|
} else {
|
||||||
|
Err("Invalid logical operator".into())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn new_op(op: LogicalOperator, left: Query<Field>, right: Query<Field>) -> Self {
|
||||||
|
LogicalFunction { op, left, right }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<Field: Clone + Debug> Function<Field> for LogicalFunction<Field> {
|
||||||
|
fn evaluate(&self, context: &dyn Data<Field>) -> Result<DataValue, CoreError> {
|
||||||
|
// More verbose to try and avoid doing right side computation
|
||||||
|
let value: bool = match self.op {
|
||||||
|
LogicalOperator::AND => {
|
||||||
|
if !bool::from(self.left.evaluate(context)?) {
|
||||||
|
false
|
||||||
|
} else {
|
||||||
|
self.right.evaluate(context)?.into()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
LogicalOperator::OR => {
|
||||||
|
if bool::from(self.left.evaluate(context)?) {
|
||||||
|
true
|
||||||
|
} else {
|
||||||
|
self.right.evaluate(context)?.into()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(DataValue::Boolean(value))
|
||||||
|
|
||||||
|
// let left = self.left.evaluate(context)?.into();
|
||||||
|
// let right = self.right.evaluate(context)?.into();
|
||||||
|
|
||||||
|
// match self.op {
|
||||||
|
// LogicalOperator::AND => Ok(DataValue::Boolean(left && right)),
|
||||||
|
// LogicalOperator::OR => Ok(DataValue::Boolean(left || right)),
|
||||||
|
// }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<Field: Clone + Debug> NotFunction<Field> {
|
||||||
|
pub fn new(value: Query<Field>) -> Self {
|
||||||
|
NotFunction { value }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<Field: Clone + Debug> Function<Field> for NotFunction<Field> {
|
||||||
|
fn evaluate(&self, context: &dyn Data<Field>) -> Result<DataValue, CoreError> {
|
||||||
|
let value: bool = self.value.evaluate(context)?.into();
|
||||||
|
|
||||||
|
Ok(DataValue::Boolean(!value))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
///////////
|
||||||
|
// Tests //
|
||||||
|
///////////
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
enum TestField {}
|
||||||
|
|
||||||
|
struct TestData {}
|
||||||
|
|
||||||
|
impl<TestField> Data<TestField> for TestData {
|
||||||
|
fn get_field(&self, _: &TestField) -> Result<DataValue, CoreError> {
|
||||||
|
Err("".into())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn logical_function_evaluate() {
|
||||||
|
let value1: DataValue = 1.into();
|
||||||
|
let value2: DataValue = false.into();
|
||||||
|
let context = TestData {};
|
||||||
|
|
||||||
|
let logical_function_and =
|
||||||
|
LogicalFunction::<TestField>::new("and", value1.clone().into(), value2.clone().into())
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(logical_function_and.op, LogicalOperator::AND);
|
||||||
|
assert_eq!(logical_function_and.evaluate(&context), Ok(false.into()));
|
||||||
|
|
||||||
|
let logical_function_or =
|
||||||
|
LogicalFunction::<TestField>::new("or", value1.into(), value2.into()).unwrap();
|
||||||
|
assert_eq!(logical_function_or.op, LogicalOperator::OR);
|
||||||
|
assert_eq!(logical_function_or.evaluate(&context), Ok(true.into()));
|
||||||
|
}
|
||||||
|
}
|
||||||
11
src/query/mod.rs
Normal file
11
src/query/mod.rs
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
mod query;
|
||||||
|
mod functions_comparison;
|
||||||
|
mod functions_logical;
|
||||||
|
mod transaction;
|
||||||
|
mod balance;
|
||||||
|
|
||||||
|
pub use query::*;
|
||||||
|
pub use functions_comparison::*;
|
||||||
|
pub use functions_logical::*;
|
||||||
|
pub use transaction::*;
|
||||||
|
pub use balance::*;
|
||||||
68
src/query/query.rs
Normal file
68
src/query/query.rs
Normal file
@ -0,0 +1,68 @@
|
|||||||
|
use std::fmt::Debug;
|
||||||
|
|
||||||
|
use crate::core::{CoreError, DataValue};
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub enum Query<T: Clone + Debug + 'static> {
|
||||||
|
Field(T),
|
||||||
|
Value(DataValue),
|
||||||
|
Function(Box<dyn Function<T>>),
|
||||||
|
}
|
||||||
|
|
||||||
|
pub trait Data<T> {
|
||||||
|
fn get_field(&self, field: &T) -> Result<DataValue, CoreError>;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub trait Function<T>: FunctionClone<T> + Debug {
|
||||||
|
fn evaluate(&self, context: &dyn Data<T>) -> Result<DataValue, CoreError>;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub trait FunctionClone<T> {
|
||||||
|
fn clone_box(&self) -> Box<dyn Function<T>>;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T, F> FunctionClone<T> for F
|
||||||
|
where
|
||||||
|
F: Function<T> + Clone + 'static,
|
||||||
|
{
|
||||||
|
fn clone_box(&self) -> Box<dyn Function<T>> {
|
||||||
|
Box::new(self.clone())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> Clone for Query<T>
|
||||||
|
where
|
||||||
|
T: Clone + Debug,
|
||||||
|
{
|
||||||
|
fn clone(&self) -> Self {
|
||||||
|
match self {
|
||||||
|
Query::Field(f) => Query::Field(f.clone()),
|
||||||
|
Query::Value(v) => Query::Value(v.clone()),
|
||||||
|
Query::Function(f) => Query::Function(f.clone_box()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: Clone + Debug> Query<T> {
|
||||||
|
pub fn evaluate(&self, context: &dyn Data<T>) -> Result<DataValue, CoreError> {
|
||||||
|
match self {
|
||||||
|
Query::Field(field) => context.get_field(field),
|
||||||
|
Query::Value(constant) => Ok(constant.clone()),
|
||||||
|
Query::Function(function) => function.evaluate(context),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn from_field(field: T) -> Self {
|
||||||
|
Query::Field(field)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn from_fn<F: Function<T> + Sized + 'static>(function: F) -> Self {
|
||||||
|
Query::Function(Box::new(function))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a, T: Clone + Debug> From<DataValue> for Query<T> {
|
||||||
|
fn from(constant: DataValue) -> Self {
|
||||||
|
Query::Value(constant)
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,13 +1,14 @@
|
|||||||
use crate::core::{CoreError, Ledger, Posting, Transaction};
|
use super::Data;
|
||||||
|
use crate::core::{CoreError, DataValue, Ledger, Posting, Transaction};
|
||||||
use super::base::{Data, DataValue};
|
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
pub enum AccountField {
|
pub enum AccountField {
|
||||||
Name,
|
Name,
|
||||||
OpenDate,
|
OpenDate,
|
||||||
CloseDate,
|
CloseDate,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
pub enum PostingField {
|
pub enum PostingField {
|
||||||
Transaction(TransactionField),
|
Transaction(TransactionField),
|
||||||
Account(AccountField),
|
Account(AccountField),
|
||||||
@ -16,6 +17,7 @@ pub enum PostingField {
|
|||||||
Price,
|
Price,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
pub enum TransactionField {
|
pub enum TransactionField {
|
||||||
Date,
|
Date,
|
||||||
Flag,
|
Flag,
|
||||||
@ -32,7 +34,9 @@ pub struct PostingData<'a> {
|
|||||||
impl<'a> Data<PostingField> for PostingData<'a> {
|
impl<'a> Data<PostingField> for PostingData<'a> {
|
||||||
fn get_field(&self, field: &PostingField) -> Result<DataValue, CoreError> {
|
fn get_field(&self, field: &PostingField) -> Result<DataValue, CoreError> {
|
||||||
match field {
|
match field {
|
||||||
PostingField::Transaction(transaction_field) => get_transaction_value(transaction_field, &self.parent_transaction),
|
PostingField::Transaction(transaction_field) => {
|
||||||
|
get_transaction_value(transaction_field, &self.parent_transaction)
|
||||||
|
}
|
||||||
PostingField::Account(account_field) => {
|
PostingField::Account(account_field) => {
|
||||||
let account = self
|
let account = self
|
||||||
.ledger
|
.ledger
|
||||||
@ -60,7 +64,7 @@ impl<'a> Data<PostingField> for PostingData<'a> {
|
|||||||
fn get_transaction_value<'a>(
|
fn get_transaction_value<'a>(
|
||||||
field: &TransactionField,
|
field: &TransactionField,
|
||||||
transaction: &Transaction,
|
transaction: &Transaction,
|
||||||
) -> Result<DataValue<'a>, CoreError> {
|
) -> Result<DataValue, CoreError> {
|
||||||
match field {
|
match field {
|
||||||
TransactionField::Date => Ok(transaction.get_date().into()),
|
TransactionField::Date => Ok(transaction.get_date().into()),
|
||||||
TransactionField::Flag => Ok(char::from(transaction.get_flag()).to_string().into()),
|
TransactionField::Flag => Ok(char::from(transaction.get_flag()).to_string().into()),
|
||||||
Loading…
Reference in New Issue
Block a user