use core::fmt;
use std::borrow::Cow;
use bounded_static::ToStatic;
use pretty_decimal::PrettyDecimal;
#[derive(Debug, PartialEq, Eq, Clone, ToStatic)]
pub struct Amount<'i> {
pub value: PrettyDecimal,
pub commodity: Cow<'i, str>,
}
#[derive(Debug, PartialEq, Eq, Clone, ToStatic)]
pub enum ValueExpr<'i> {
Paren(Expr<'i>),
Amount(Amount<'i>),
}
impl<'i> From<Amount<'i>> for ValueExpr<'i> {
fn from(v: Amount<'i>) -> Self {
ValueExpr::Amount(v)
}
}
#[derive(Debug, PartialEq, Eq, Clone, ToStatic)]
pub enum Expr<'i> {
Unary(UnaryOpExpr<'i>),
Binary(BinaryOpExpr<'i>),
Value(Box<ValueExpr<'i>>),
}
#[derive(Debug, PartialEq, Eq, Clone, Copy, ToStatic)]
pub enum UnaryOp {
Negate,
}
impl fmt::Display for UnaryOp {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let op = match self {
UnaryOp::Negate => "-",
};
write!(f, "{}", op)
}
}
#[derive(Debug, PartialEq, Eq, Clone, ToStatic)]
pub struct UnaryOpExpr<'i> {
pub op: UnaryOp,
pub expr: Box<Expr<'i>>,
}
#[derive(Debug, PartialEq, Eq, Clone, Copy, strum::EnumIter, ToStatic)]
pub enum BinaryOp {
Add,
Sub,
Mul,
Div,
}
impl fmt::Display for BinaryOp {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let op = match self {
BinaryOp::Add => "+",
BinaryOp::Sub => "-",
BinaryOp::Mul => "*",
BinaryOp::Div => "/",
};
write!(f, "{}", op)
}
}
#[derive(Debug, PartialEq, Eq, Clone, ToStatic)]
pub struct BinaryOpExpr<'i> {
pub op: BinaryOp,
pub lhs: Box<Expr<'i>>,
pub rhs: Box<Expr<'i>>,
}
pub(crate) trait ExprVisitor<'i> {
type Output;
type Error;
fn visit_amount(&mut self, amount: &Amount<'i>) -> Result<Self::Output, Self::Error>;
fn visit_unary(&mut self, expr: &UnaryOpExpr<'i>) -> Result<Self::Output, Self::Error>;
fn visit_binary(&mut self, expr: &BinaryOpExpr<'i>) -> Result<Self::Output, Self::Error>;
#[inline]
fn visit_value_expr(&mut self, expr: &ValueExpr<'i>) -> Result<Self::Output, Self::Error> {
match expr {
ValueExpr::Paren(x) => self.visit_expr(x),
ValueExpr::Amount(x) => self.visit_amount(x),
}
}
#[inline]
fn visit_expr(&mut self, expr: &Expr<'i>) -> Result<Self::Output, Self::Error> {
match expr {
Expr::Unary(e) => self.visit_unary(e),
Expr::Binary(e) => self.visit_binary(e),
Expr::Value(e) => self.visit_value_expr(e),
}
}
}
pub(crate) trait Visitable<'i> {
fn accept<V: ExprVisitor<'i> + ?Sized>(&self, visitor: &mut V) -> Result<V::Output, V::Error>;
}
impl<'i> Visitable<'i> for Amount<'i> {
#[inline]
fn accept<V: ExprVisitor<'i> + ?Sized>(&self, visitor: &mut V) -> Result<V::Output, V::Error> {
visitor.visit_amount(self)
}
}
impl<'i> Visitable<'i> for ValueExpr<'i> {
#[inline]
fn accept<V: ExprVisitor<'i> + ?Sized>(&self, visitor: &mut V) -> Result<V::Output, V::Error> {
visitor.visit_value_expr(self)
}
}
impl<'i> Visitable<'i> for Expr<'i> {
#[inline]
fn accept<V: ExprVisitor<'i> + ?Sized>(&self, visitor: &mut V) -> Result<V::Output, V::Error> {
visitor.visit_expr(self)
}
}
impl<'i> Visitable<'i> for UnaryOpExpr<'i> {
#[inline]
fn accept<V: ExprVisitor<'i> + ?Sized>(&self, visitor: &mut V) -> Result<V::Output, V::Error> {
visitor.visit_unary(self)
}
}
impl<'i> Visitable<'i> for BinaryOpExpr<'i> {
#[inline]
fn accept<V: ExprVisitor<'i> + ?Sized>(&self, visitor: &mut V) -> Result<V::Output, V::Error> {
visitor.visit_binary(self)
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::convert::Infallible;
use pretty_assertions::assert_eq;
#[derive(Default)]
struct TraceVisitor(Vec<String>);
impl<'i> ExprVisitor<'i> for TraceVisitor {
type Output = ();
type Error = Infallible;
fn visit_amount(&mut self, amount: &Amount<'i>) -> Result<(), Infallible> {
self.0.push(format!("amount({})", amount.commodity));
Ok(())
}
fn visit_unary(&mut self, expr: &UnaryOpExpr<'i>) -> Result<(), Infallible> {
self.0.push(format!("unary({})", expr.op));
self.visit_expr(&expr.expr)
}
fn visit_binary(&mut self, expr: &BinaryOpExpr<'i>) -> Result<(), Infallible> {
self.0.push(format!("binary({})", expr.op));
self.visit_expr(&expr.lhs)?;
self.visit_expr(&expr.rhs)
}
}
#[test]
fn visitor_reaches_all_nodes_in_pre_order() {
let input: ValueExpr = "(1 USD * (-2 + 3 EUR))".try_into().expect("must parse");
let mut visitor = TraceVisitor::default();
input.accept(&mut visitor).expect("must not fail");
assert_eq!(
vec![
"binary(*)",
"amount(USD)",
"binary(+)",
"unary(-)",
"amount()",
"amount(EUR)",
],
visitor.0
);
}
#[test]
fn visitor_aborts_on_error() {
struct FailingVisitor(usize);
impl<'i> ExprVisitor<'i> for FailingVisitor {
type Output = ();
type Error = ();
fn visit_amount(&mut self, amount: &Amount<'i>) -> Result<(), ()> {
self.0 += 1;
if amount.commodity.is_empty() {
return Err(());
}
Ok(())
}
fn visit_unary(&mut self, expr: &UnaryOpExpr<'i>) -> Result<(), ()> {
self.visit_expr(&expr.expr)
}
fn visit_binary(&mut self, expr: &BinaryOpExpr<'i>) -> Result<(), ()> {
self.visit_expr(&expr.lhs)?;
self.visit_expr(&expr.rhs)
}
}
let input: ValueExpr = "(1 USD + 2 + 3 EUR)".try_into().expect("must parse");
let mut visitor = FailingVisitor(0);
assert_eq!(Err(()), input.accept(&mut visitor));
assert_eq!(2, visitor.0);
}
}