1use core::fmt;
5use std::borrow::Cow;
6
7use bounded_static::ToStatic;
8use pretty_decimal::PrettyDecimal;
9
10#[derive(Debug, PartialEq, Eq, Clone, ToStatic)]
12pub struct Amount<'i> {
13 pub value: PrettyDecimal,
14 pub commodity: Cow<'i, str>,
15}
16
17#[derive(Debug, PartialEq, Eq, Clone, ToStatic)]
21pub enum ValueExpr<'i> {
22 Paren(Expr<'i>),
23 Amount(Amount<'i>),
24}
25
26impl<'i> From<Amount<'i>> for ValueExpr<'i> {
27 fn from(v: Amount<'i>) -> Self {
28 ValueExpr::Amount(v)
29 }
30}
31
32#[derive(Debug, PartialEq, Eq, Clone, ToStatic)]
34pub enum Expr<'i> {
35 Unary(UnaryOpExpr<'i>),
36 Binary(BinaryOpExpr<'i>),
37 Value(Box<ValueExpr<'i>>),
38}
39
40#[derive(Debug, PartialEq, Eq, Clone, Copy, ToStatic)]
42pub enum UnaryOp {
43 Negate,
45}
46
47impl fmt::Display for UnaryOp {
48 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
49 let op = match self {
50 UnaryOp::Negate => "-",
51 };
52 write!(f, "{}", op)
53 }
54}
55
56#[derive(Debug, PartialEq, Eq, Clone, ToStatic)]
58pub struct UnaryOpExpr<'i> {
59 pub op: UnaryOp,
60 pub expr: Box<Expr<'i>>,
61}
62
63#[derive(Debug, PartialEq, Eq, Clone, Copy, strum::EnumIter, ToStatic)]
65pub enum BinaryOp {
66 Add,
68 Sub,
70 Mul,
72 Div,
74}
75
76impl fmt::Display for BinaryOp {
77 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
78 let op = match self {
79 BinaryOp::Add => "+",
80 BinaryOp::Sub => "-",
81 BinaryOp::Mul => "*",
82 BinaryOp::Div => "/",
83 };
84 write!(f, "{}", op)
85 }
86}
87
88#[derive(Debug, PartialEq, Eq, Clone, ToStatic)]
90pub struct BinaryOpExpr<'i> {
91 pub op: BinaryOp,
92 pub lhs: Box<Expr<'i>>,
93 pub rhs: Box<Expr<'i>>,
94}
95
96pub(crate) trait ExprVisitor<'i> {
107 type Output;
109
110 type Error;
112
113 fn visit_amount(&mut self, amount: &Amount<'i>) -> Result<Self::Output, Self::Error>;
115
116 fn visit_unary(&mut self, expr: &UnaryOpExpr<'i>) -> Result<Self::Output, Self::Error>;
118
119 fn visit_binary(&mut self, expr: &BinaryOpExpr<'i>) -> Result<Self::Output, Self::Error>;
121
122 #[inline]
124 fn visit_value_expr(&mut self, expr: &ValueExpr<'i>) -> Result<Self::Output, Self::Error> {
125 match expr {
126 ValueExpr::Paren(x) => self.visit_expr(x),
127 ValueExpr::Amount(x) => self.visit_amount(x),
128 }
129 }
130
131 #[inline]
133 fn visit_expr(&mut self, expr: &Expr<'i>) -> Result<Self::Output, Self::Error> {
134 match expr {
135 Expr::Unary(e) => self.visit_unary(e),
136 Expr::Binary(e) => self.visit_binary(e),
137 Expr::Value(e) => self.visit_value_expr(e),
138 }
139 }
140}
141
142pub(crate) trait Visitable<'i> {
144 fn accept<V: ExprVisitor<'i> + ?Sized>(&self, visitor: &mut V) -> Result<V::Output, V::Error>;
146}
147
148impl<'i> Visitable<'i> for Amount<'i> {
149 #[inline]
150 fn accept<V: ExprVisitor<'i> + ?Sized>(&self, visitor: &mut V) -> Result<V::Output, V::Error> {
151 visitor.visit_amount(self)
152 }
153}
154
155impl<'i> Visitable<'i> for ValueExpr<'i> {
156 #[inline]
157 fn accept<V: ExprVisitor<'i> + ?Sized>(&self, visitor: &mut V) -> Result<V::Output, V::Error> {
158 visitor.visit_value_expr(self)
159 }
160}
161
162impl<'i> Visitable<'i> for Expr<'i> {
163 #[inline]
164 fn accept<V: ExprVisitor<'i> + ?Sized>(&self, visitor: &mut V) -> Result<V::Output, V::Error> {
165 visitor.visit_expr(self)
166 }
167}
168
169impl<'i> Visitable<'i> for UnaryOpExpr<'i> {
170 #[inline]
171 fn accept<V: ExprVisitor<'i> + ?Sized>(&self, visitor: &mut V) -> Result<V::Output, V::Error> {
172 visitor.visit_unary(self)
173 }
174}
175
176impl<'i> Visitable<'i> for BinaryOpExpr<'i> {
177 #[inline]
178 fn accept<V: ExprVisitor<'i> + ?Sized>(&self, visitor: &mut V) -> Result<V::Output, V::Error> {
179 visitor.visit_binary(self)
180 }
181}
182
183#[cfg(test)]
184mod tests {
185 use super::*;
186
187 use std::convert::Infallible;
188
189 use pretty_assertions::assert_eq;
190
191 #[derive(Default)]
193 struct TraceVisitor(Vec<String>);
194
195 impl<'i> ExprVisitor<'i> for TraceVisitor {
196 type Output = ();
197 type Error = Infallible;
198
199 fn visit_amount(&mut self, amount: &Amount<'i>) -> Result<(), Infallible> {
200 self.0.push(format!("amount({})", amount.commodity));
201 Ok(())
202 }
203
204 fn visit_unary(&mut self, expr: &UnaryOpExpr<'i>) -> Result<(), Infallible> {
205 self.0.push(format!("unary({})", expr.op));
206 self.visit_expr(&expr.expr)
207 }
208
209 fn visit_binary(&mut self, expr: &BinaryOpExpr<'i>) -> Result<(), Infallible> {
210 self.0.push(format!("binary({})", expr.op));
211 self.visit_expr(&expr.lhs)?;
212 self.visit_expr(&expr.rhs)
213 }
214 }
215
216 #[test]
217 fn visitor_reaches_all_nodes_in_pre_order() {
218 let input: ValueExpr = "(1 USD * (-2 + 3 EUR))".try_into().expect("must parse");
219 let mut visitor = TraceVisitor::default();
220 input.accept(&mut visitor).expect("must not fail");
221 assert_eq!(
222 vec![
223 "binary(*)",
224 "amount(USD)",
225 "binary(+)",
226 "unary(-)",
227 "amount()",
228 "amount(EUR)",
229 ],
230 visitor.0
231 );
232 }
233
234 #[test]
235 fn visitor_aborts_on_error() {
236 struct FailingVisitor(usize);
238
239 impl<'i> ExprVisitor<'i> for FailingVisitor {
240 type Output = ();
241 type Error = ();
242
243 fn visit_amount(&mut self, amount: &Amount<'i>) -> Result<(), ()> {
244 self.0 += 1;
245 if amount.commodity.is_empty() {
246 return Err(());
247 }
248 Ok(())
249 }
250
251 fn visit_unary(&mut self, expr: &UnaryOpExpr<'i>) -> Result<(), ()> {
252 self.visit_expr(&expr.expr)
253 }
254
255 fn visit_binary(&mut self, expr: &BinaryOpExpr<'i>) -> Result<(), ()> {
256 self.visit_expr(&expr.lhs)?;
257 self.visit_expr(&expr.rhs)
258 }
259 }
260
261 let input: ValueExpr = "(1 USD + 2 + 3 EUR)".try_into().expect("must parse");
262 let mut visitor = FailingVisitor(0);
263 assert_eq!(Err(()), input.accept(&mut visitor));
264 assert_eq!(2, visitor.0);
266 }
267}