use rust_decimal::Decimal;
use rustledger_core::{Amount, MetaValue};
use crate::ast::FunctionCall;
use crate::error::QueryError;
use super::super::Executor;
use super::super::types::{PostingContext, Value};
impl Executor<'_> {
pub(crate) fn eval_meta_function(
&self,
name: &str,
func: &FunctionCall,
ctx: &PostingContext,
) -> Result<Value, QueryError> {
Self::require_args(name, func, 1)?;
let key = match self.evaluate_expr(&func.args[0], ctx)? {
Value::String(s) => s,
_ => {
return Err(QueryError::Type(format!(
"{name}: argument must be a string key"
)));
}
};
let posting = &ctx.transaction.postings[ctx.posting_index];
let meta_value = match name {
"META" | "POSTING_META" => posting.meta.get(&key),
"ENTRY_META" => ctx.transaction.meta.get(&key),
"ANY_META" => posting
.meta
.get(&key)
.or_else(|| ctx.transaction.meta.get(&key)),
_ => unreachable!(),
};
Ok(Self::meta_value_to_value(meta_value))
}
pub(crate) fn meta_value_to_value(mv: Option<&MetaValue>) -> Value {
match mv {
None => Value::Null,
Some(MetaValue::String(s)) => Value::String(s.clone()),
Some(MetaValue::Number(n)) => Value::Number(*n),
Some(MetaValue::Date(d)) => Value::Date(*d),
Some(MetaValue::Bool(b)) => Value::Boolean(*b),
Some(MetaValue::Amount(a)) => Value::Amount(a.clone()),
Some(MetaValue::Account(s)) => Value::String(s.clone()),
Some(MetaValue::Currency(s)) => Value::String(s.clone()),
Some(MetaValue::Tag(s)) => Value::String(s.clone()),
Some(MetaValue::Link(s)) => Value::String(s.clone()),
Some(MetaValue::None) => Value::Null,
}
}
pub(crate) fn eval_convert(
&self,
func: &FunctionCall,
ctx: &PostingContext,
) -> Result<Value, QueryError> {
if func.args.len() < 2 || func.args.len() > 3 {
return Err(QueryError::InvalidArguments(
"CONVERT".to_string(),
"expected 2 or 3 arguments: (value, currency[, date])".to_string(),
));
}
let val = self.evaluate_expr(&func.args[0], ctx)?;
let target_currency = match self.evaluate_expr(&func.args[1], ctx)? {
Value::String(s) => s,
_ => {
return Err(QueryError::Type(
"CONVERT: second argument must be a currency string".to_string(),
));
}
};
let date: Option<chrono::NaiveDate> = if func.args.len() == 3 {
match self.evaluate_expr(&func.args[2], ctx)? {
Value::Date(d) => Some(d),
_ => {
return Err(QueryError::Type(
"CONVERT: third argument must be a date".to_string(),
));
}
}
} else {
None };
let convert_amount = |amt: &Amount| -> Option<Amount> {
if let Some(d) = date {
self.price_db.convert(amt, &target_currency, d)
} else {
self.price_db.convert_latest(amt, &target_currency)
}
};
match val {
Value::Position(p) => {
if p.units.currency == target_currency {
Ok(Value::Amount(p.units))
} else if let Some(converted) = convert_amount(&p.units) {
Ok(Value::Amount(converted))
} else {
Ok(Value::Amount(p.units))
}
}
Value::Amount(a) => {
if a.currency == target_currency {
Ok(Value::Amount(a))
} else if let Some(converted) = convert_amount(&a) {
Ok(Value::Amount(converted))
} else {
Ok(Value::Amount(a))
}
}
Value::Inventory(inv) => {
let mut total = Decimal::ZERO;
for pos in inv.positions() {
if pos.units.currency == target_currency {
total += pos.units.number;
} else if let Some(converted) = convert_amount(&pos.units) {
total += converted.number;
}
}
Ok(Value::Amount(Amount::new(total, &target_currency)))
}
Value::Number(n) => {
Ok(Value::Amount(Amount::new(n, &target_currency)))
}
_ => Err(QueryError::Type(
"CONVERT expects a position, amount, inventory, or number".to_string(),
)),
}
}
pub(crate) fn eval_int(
&self,
func: &FunctionCall,
ctx: &PostingContext,
) -> Result<Value, QueryError> {
use rust_decimal::prelude::ToPrimitive;
Self::require_args("INT", func, 1)?;
let val = self.evaluate_expr(&func.args[0], ctx)?;
match val {
Value::Integer(i) => Ok(Value::Integer(i)),
Value::Number(n) => {
n.trunc()
.to_i64()
.map(Value::Integer)
.ok_or_else(|| QueryError::Type("INT: number too large for integer".into()))
}
Value::Boolean(b) => Ok(Value::Integer(i64::from(b))),
Value::String(s) => s
.parse::<i64>()
.map(Value::Integer)
.map_err(|_| QueryError::Type(format!("INT: cannot parse '{s}' as integer"))),
Value::Null => Ok(Value::Null),
_ => Err(QueryError::Type(
"INT expects a number, integer, boolean, or string".to_string(),
)),
}
}
pub(crate) fn eval_decimal(
&self,
func: &FunctionCall,
ctx: &PostingContext,
) -> Result<Value, QueryError> {
Self::require_args("DECIMAL", func, 1)?;
let val = self.evaluate_expr(&func.args[0], ctx)?;
match val {
Value::Number(n) => Ok(Value::Number(n)),
Value::Integer(i) => Ok(Value::Number(Decimal::from(i))),
Value::Boolean(b) => Ok(Value::Number(if b { Decimal::ONE } else { Decimal::ZERO })),
Value::String(s) => s
.parse::<Decimal>()
.map(Value::Number)
.map_err(|_| QueryError::Type(format!("DECIMAL: cannot parse '{s}' as decimal"))),
Value::Null => Ok(Value::Null),
_ => Err(QueryError::Type(
"DECIMAL expects a number, integer, boolean, or string".to_string(),
)),
}
}
pub(crate) fn eval_str(
&self,
func: &FunctionCall,
ctx: &PostingContext,
) -> Result<Value, QueryError> {
Self::require_args("STR", func, 1)?;
let val = self.evaluate_expr(&func.args[0], ctx)?;
match val {
Value::String(s) => Ok(Value::String(s)),
Value::Integer(i) => Ok(Value::String(i.to_string())),
Value::Number(n) => Ok(Value::String(n.to_string())),
Value::Boolean(b) => Ok(Value::String(if b { "TRUE" } else { "FALSE" }.to_string())),
Value::Date(d) => Ok(Value::String(d.to_string())),
Value::Amount(a) => Ok(Value::String(format!("{} {}", a.number, a.currency))),
Value::Null => Ok(Value::Null),
_ => Err(QueryError::Type("STR expects a scalar value".to_string())),
}
}
pub(crate) fn eval_bool(
&self,
func: &FunctionCall,
ctx: &PostingContext,
) -> Result<Value, QueryError> {
Self::require_args("BOOL", func, 1)?;
let val = self.evaluate_expr(&func.args[0], ctx)?;
match val {
Value::Boolean(b) => Ok(Value::Boolean(b)),
Value::Integer(i) => Ok(Value::Boolean(i != 0)),
Value::Number(n) => Ok(Value::Boolean(!n.is_zero())),
Value::String(s) => {
let s_upper = s.to_uppercase();
match s_upper.as_str() {
"TRUE" | "YES" | "1" | "T" | "Y" => Ok(Value::Boolean(true)),
"FALSE" | "NO" | "0" | "F" | "N" | "" => Ok(Value::Boolean(false)),
_ => Err(QueryError::Type(format!(
"BOOL: cannot parse '{s}' as boolean"
))),
}
}
Value::Null => Ok(Value::Null),
_ => Err(QueryError::Type(
"BOOL expects an integer, number, boolean, or string".to_string(),
)),
}
}
pub(crate) fn eval_coalesce(
&self,
func: &FunctionCall,
ctx: &PostingContext,
) -> Result<Value, QueryError> {
for arg in &func.args {
let val = self.evaluate_expr(arg, ctx)?;
if !matches!(val, Value::Null) {
return Ok(val);
}
}
Ok(Value::Null)
}
pub(crate) fn eval_only(
&self,
func: &FunctionCall,
ctx: &PostingContext,
) -> Result<Value, QueryError> {
Self::require_args("ONLY", func, 2)?;
let key = match self.evaluate_expr(&func.args[0], ctx)? {
Value::String(s) => s,
_ => {
return Err(QueryError::Type(
"ONLY: first argument must be a currency string".to_string(),
));
}
};
let inv = match self.evaluate_expr(&func.args[1], ctx)? {
Value::Inventory(inv) => inv,
Value::Position(pos) => {
if pos.units.currency == key {
return Ok(Value::Amount(pos.units));
}
return Ok(Value::Null);
}
Value::Amount(amt) => {
if amt.currency == key {
return Ok(Value::Amount(amt));
}
return Ok(Value::Null);
}
Value::Null => return Ok(Value::Null),
_ => {
return Err(QueryError::Type(
"ONLY: second argument must be an inventory, position, or amount".to_string(),
));
}
};
let matching: Vec<_> = inv
.positions()
.iter()
.filter(|p| p.units.currency == key)
.collect();
match matching.len() {
0 => Ok(Value::Null),
1 => Ok(Value::Amount(matching[0].units.clone())),
_ => Ok(Value::Null), }
}
}