okane-core 0.19.0

Library to support parsing, emitting and processing Ledger (https://www.ledger-cli.org/) format files.
Documentation
//! Evaluation logics of expressions.
//!
//! This module provides several types.
//! * [`Amount`] to represent general amount, which is commodity associted decimal amount.
//! * [`SingleAmount`] to represent an amount with only one commodity.
//! * [`Evaluated`] which is the result of syntax expression eval.

mod amount;
mod error;
mod evaluated;
mod posting_amount;
mod single_amount;

pub use amount::Amount;
pub use error::{EvalError, OwnedEvalError};
pub use evaluated::Evaluated;
pub(super) use posting_amount::PostingAmount;
pub use single_amount::SingleAmount;

use super::context::ReportContext;
use crate::syntax::expr;

/// Provides syntax tree evaluation.
pub(crate) trait Evaluable {
    /// Visitor for AST.
    /// Argument `evaluator` is a call operator to evaluate the given syntax amount to [`Evaluated`].
    fn eval_visit<'ctx, F: FnMut(&expr::Amount) -> Result<Evaluated<'ctx>, EvalError<'ctx>>>(
        &self,
        evaluator: &mut F,
    ) -> Result<Evaluated<'ctx>, EvalError<'ctx>>;

    /// Evaluate the self with mutable `ctx`, which allows unknown commodities in the expressions to be registered.
    fn eval_mut<'ctx>(
        &self,
        ctx: &mut ReportContext<'ctx>,
    ) -> Result<Evaluated<'ctx>, EvalError<'ctx>> {
        self.eval_visit(&mut |amount| Ok(Evaluated::from_expr_amount_mut(ctx, amount)))
    }

    /// Evaluate the self with immutable `ctx`, which raises error on unknown commodities.
    fn eval<'ctx>(&self, ctx: &ReportContext<'ctx>) -> Result<Evaluated<'ctx>, EvalError<'ctx>> {
        self.eval_visit(&mut |amount| Evaluated::from_expr_amount(ctx, amount))
    }
}

impl Evaluable for expr::ValueExpr<'_> {
    fn eval_visit<'ctx, F: FnMut(&expr::Amount) -> Result<Evaluated<'ctx>, EvalError<'ctx>>>(
        &self,
        evaluator: &mut F,
    ) -> Result<Evaluated<'ctx>, EvalError<'ctx>> {
        match self {
            expr::ValueExpr::Paren(x) => x.eval_visit(evaluator),
            expr::ValueExpr::Amount(x) => evaluator(x),
        }
    }
}

impl Evaluable for expr::Expr<'_> {
    fn eval_visit<'ctx, F: FnMut(&expr::Amount) -> Result<Evaluated<'ctx>, EvalError<'ctx>>>(
        &self,
        evaluator: &mut F,
    ) -> Result<Evaluated<'ctx>, EvalError<'ctx>> {
        match self {
            expr::Expr::Unary(e) => e.eval_visit(evaluator),
            expr::Expr::Binary(e) => e.eval_visit(evaluator),
            expr::Expr::Value(e) => e.eval_visit(evaluator),
        }
    }
}

impl Evaluable for expr::UnaryOpExpr<'_> {
    fn eval_visit<'ctx, F: FnMut(&expr::Amount) -> Result<Evaluated<'ctx>, EvalError<'ctx>>>(
        &self,
        evaluator: &mut F,
    ) -> Result<Evaluated<'ctx>, EvalError<'ctx>> {
        match self.op {
            expr::UnaryOp::Negate => {
                let val = self.expr.eval_visit(evaluator)?;
                Ok(val.negate())
            }
        }
    }
}

impl Evaluable for expr::BinaryOpExpr<'_> {
    fn eval_visit<'ctx, F: FnMut(&expr::Amount) -> Result<Evaluated<'ctx>, EvalError<'ctx>>>(
        &self,
        evaluator: &mut F,
    ) -> Result<Evaluated<'ctx>, EvalError<'ctx>> {
        let lhs = self.lhs.eval_visit(evaluator)?;
        let rhs = self.rhs.eval_visit(evaluator)?;
        match self.op {
            expr::BinaryOp::Add => lhs.check_add(rhs),
            expr::BinaryOp::Sub => lhs.check_sub(rhs),
            expr::BinaryOp::Mul => lhs.check_mul(rhs),
            expr::BinaryOp::Div => lhs.check_div(rhs),
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    use bumpalo::Bump;
    use maplit::btreemap;
    use pretty_assertions::assert_eq;
    use pretty_decimal::PrettyDecimal;
    use rust_decimal_macros::dec;

    #[test]
    fn eval_expr_simple() {
        let input = expr::ValueExpr::Amount(expr::Amount {
            value: PrettyDecimal::plain(dec!(100.12345)),
            commodity: "USD".into(),
        });
        let arena = Bump::new();
        let mut ctx = ReportContext::new(&arena);
        let got = input.eval_mut(&mut ctx).unwrap();
        let got: Amount<'_> = got.try_into().expect("not an amount");
        assert_eq!(
            btreemap! {
                ctx.commodities.ensure("USD") => dec!(100.12345),
            },
            got.into_values()
        );
    }

    #[test]
    fn eval_expr_add_negate() {
        let input = "(100 USD + 300 EUR + (-100 USD + 20,000 JPY))";
        let input: expr::ValueExpr<'static> = input.try_into().expect("must succeed to parse");
        let arena = Bump::new();
        let mut ctx = ReportContext::new(&arena);
        let got = input.eval_mut(&mut ctx).unwrap();
        let got: Amount<'_> = got.try_into().expect("not an amount");
        assert_eq!(
            btreemap! {
                ctx.commodities.ensure("USD") => dec!(0),
                ctx.commodities.ensure("EUR") => dec!(300),
                ctx.commodities.ensure("JPY") => dec!(20000),
            },
            got.into_values()
        );
    }

    #[test]
    fn eval_expr_complex() {
        let input = "((100 USD + 200 EUR) * 2 - 100 USD / 5)";
        let input: expr::ValueExpr = input.try_into().expect("must not fail to parse");
        let arena = Bump::new();
        let mut ctx = ReportContext::new(&arena);
        let got = input.eval_mut(&mut ctx).unwrap();
        let got: Amount<'_> = got.try_into().expect("not an amount");
        assert_eq!(
            btreemap! {
                ctx.commodities.ensure("USD") => dec!(180),
                ctx.commodities.ensure("EUR") => dec!(400),
            },
            got.into_values()
        );
    }
}