1use winnow::{
4 ascii::space0,
5 combinator::{
6 alt, delimited, dispatch, opt, peek, permutation, preceded, separated_foldl1, terminated,
7 trace,
8 },
9 error::{FromExternalError, ParserError},
10 stream::{AsChar, Stream, StreamIsPartial},
11 token::{any, one_of},
12 Parser,
13};
14
15use crate::syntax::{expr, pretty_decimal};
16
17use super::{adaptor::ParseOptions, character::paren, error, primitive};
18
19pub fn value_expr<'i, I, E>(input: &mut I) -> winnow::Result<expr::ValueExpr<'i>, E>
21where
22 I: Stream<Token = char, Slice = &'i str> + StreamIsPartial + Clone,
23 E: ParserError<I> + FromExternalError<I, pretty_decimal::Error>,
24 <I as Stream>::Token: AsChar + Clone,
25{
26 trace(
27 "expr::value_expr",
28 dispatch! {peek(any);
29 '(' => paren_expr,
30 _ => amount.map(expr::ValueExpr::Amount),
31 },
32 )
33 .parse_next(input)
34}
35
36impl<'i> TryFrom<&'i str> for expr::ValueExpr<'i> {
37 type Error = error::ParseError;
38
39 fn try_from(value: &'i str) -> Result<Self, Self::Error> {
40 ParseOptions::default()
41 .parse_single(value_expr, value)
42 .map(|x| x.1)
43 }
44}
45
46fn paren_expr<'i, I, E>(input: &mut I) -> winnow::Result<expr::ValueExpr<'i>, E>
47where
48 I: Stream<Token = char, Slice = &'i str> + StreamIsPartial + Clone,
49 E: ParserError<I> + FromExternalError<I, pretty_decimal::Error>,
50 <I as Stream>::Token: AsChar + Clone,
51{
52 trace(
53 "expr::paren_expr",
54 paren(delimited(space0, add_expr, space0)).map(expr::ValueExpr::Paren),
55 )
56 .parse_next(input)
57}
58
59fn add_expr<'i, I, E>(input: &mut I) -> winnow::Result<expr::Expr<'i>, E>
60where
61 I: Stream<Token = char, Slice = &'i str> + StreamIsPartial + Clone,
62 E: ParserError<I> + FromExternalError<I, pretty_decimal::Error>,
63 <I as Stream>::Token: AsChar + Clone,
64{
65 trace("expr::add_expr", infixl(add_op, mul_expr)).parse_next(input)
66}
67
68fn add_op<I, E>(input: &mut I) -> winnow::Result<expr::BinaryOp, E>
69where
70 I: Stream + StreamIsPartial + Clone,
71 E: ParserError<I>,
72 <I as Stream>::Token: AsChar + Clone,
73{
74 trace(
75 "expr::add_op",
76 alt((
77 one_of('+').value(expr::BinaryOp::Add),
78 one_of('-').value(expr::BinaryOp::Sub),
79 )),
80 )
81 .parse_next(input)
82}
83
84fn mul_expr<'i, I, E>(input: &mut I) -> winnow::Result<expr::Expr<'i>, E>
85where
86 I: Stream<Token = char, Slice = &'i str> + StreamIsPartial + Clone,
87 E: ParserError<I> + FromExternalError<I, pretty_decimal::Error>,
88 <I as Stream>::Token: AsChar + Clone,
89{
90 trace("expr::mul_expr", infixl(mul_op, unary_expr)).parse_next(input)
91}
92
93fn mul_op<I, E>(input: &mut I) -> winnow::Result<expr::BinaryOp, E>
94where
95 I: Stream + StreamIsPartial + Clone,
96 E: ParserError<I>,
97 <I as Stream>::Token: AsChar + Clone,
98{
99 trace(
100 "expr::mul_op",
101 alt((
102 one_of('*').value(expr::BinaryOp::Mul),
103 one_of('/').value(expr::BinaryOp::Div),
104 )),
105 )
106 .parse_next(input)
107}
108
109fn unary_expr<'i, I, E>(input: &mut I) -> winnow::Result<expr::Expr<'i>, E>
110where
111 I: Stream<Token = char, Slice = &'i str> + StreamIsPartial + Clone,
112 E: ParserError<I> + FromExternalError<I, pretty_decimal::Error>,
113 <I as Stream>::Token: AsChar + Clone,
114{
115 trace(
116 "expr::unary_expr",
117 dispatch! {peek(any);
118 '-' => negate_expr,
119 _ => value_expr.map(|ve| expr::Expr::Value(Box::new(ve))),
120 },
121 )
122 .parse_next(input)
123}
124
125fn unary_amount<'i, I, E>(input: &mut I) -> winnow::Result<expr::Amount<'i>, E>
128where
129 I: Stream<Slice = &'i str> + StreamIsPartial,
130 E: ParserError<I> + FromExternalError<I, pretty_decimal::Error>,
131 <I as Stream>::Token: AsChar + Clone,
132{
133 trace(
135 "expr::unary_amount",
136 (
137 opt(one_of('-')),
138 permutation((
139 terminated(primitive::pretty_decimal, space0),
140 terminated(primitive::commodity, space0),
141 )),
142 )
143 .map(|(negate, (mut value, c)): (_, (_, &str))| {
144 if negate.is_some() {
145 value
146 .value
147 .set_sign_positive(!value.value.is_sign_positive());
148 }
149 expr::Amount {
150 value,
151 commodity: c.into(),
152 }
153 }),
154 )
155 .parse_next(input)
156}
157
158impl<'i> TryFrom<&'i str> for expr::Amount<'i> {
159 type Error = error::ParseError;
160
161 fn try_from(value: &'i str) -> Result<Self, Self::Error> {
162 ParseOptions::default()
163 .parse_single(unary_amount, value)
164 .map(|x| x.1)
165 }
166}
167
168fn negate_expr<'i, I, E>(input: &mut I) -> winnow::Result<expr::Expr<'i>, E>
169where
170 I: Stream<Token = char, Slice = &'i str> + StreamIsPartial + Clone,
171 E: ParserError<I> + FromExternalError<I, pretty_decimal::Error>,
172 <I as Stream>::Token: AsChar + Clone,
173{
174 trace(
175 "expr::negate_expr",
176 preceded(one_of('-'), value_expr).map(|ve| {
177 expr::Expr::Unary(expr::UnaryOpExpr {
178 op: expr::UnaryOp::Negate,
179 expr: Box::new(expr::Expr::Value(Box::new(ve))),
180 })
181 }),
182 )
183 .parse_next(input)
184}
185
186pub fn amount<'i, I, E>(input: &mut I) -> winnow::Result<expr::Amount<'i>, E>
188where
189 I: Stream<Slice = &'i str> + StreamIsPartial,
190 E: ParserError<I> + FromExternalError<I, pretty_decimal::Error>,
191 <I as Stream>::Token: AsChar + Clone,
192{
193 trace(
196 "expr::amount",
197 (
198 terminated(primitive::pretty_decimal, space0),
199 primitive::commodity,
200 )
201 .map(|(value, c): (_, &str)| expr::Amount {
202 value,
203 commodity: c.into(),
204 }),
205 )
206 .parse_next(input)
207}
208
209fn infixl<'i, I, E, F, G>(operator: F, operand: G) -> impl Parser<I, expr::Expr<'i>, E>
213where
214 I: Stream + StreamIsPartial + Clone,
215 E: ParserError<I>,
216 F: Parser<I, expr::BinaryOp, E>,
217 G: Parser<I, expr::Expr<'i>, E>,
218 I: Stream + StreamIsPartial + Clone,
219 <I as Stream>::Token: AsChar,
220{
221 trace(
222 "infixl",
223 separated_foldl1(
224 operand,
225 delimited(space0, operator, space0),
226 |lhs, op, rhs| {
227 expr::Expr::Binary(expr::BinaryOpExpr {
228 lhs: Box::new(lhs),
229 op,
230 rhs: Box::new(rhs),
231 })
232 },
233 ),
234 )
235}
236
237#[cfg(test)]
238mod tests {
239 use super::*;
240
241 use crate::parse::testing::expect_parse_ok;
242 use crate::syntax::pretty_decimal::PrettyDecimal;
243
244 use pretty_assertions::assert_eq;
245 use rust_decimal::Decimal;
246 use rust_decimal_macros::dec;
247
248 #[test]
249 fn value_expr_literal() {
250 assert_eq!(
251 expect_parse_ok(value_expr, "1000 JPY"),
252 (
253 "",
254 expr::ValueExpr::Amount(expr::Amount {
255 value: PrettyDecimal::plain(dec!(1000)),
256 commodity: "JPY".into()
257 }),
258 )
259 );
260
261 assert_eq!(
262 expect_parse_ok(value_expr, "1,234,567.89 USD"),
263 (
264 "",
265 expr::ValueExpr::Amount(expr::Amount {
266 value: PrettyDecimal::comma3dot(dec!(1234567.89)),
267 commodity: "USD".into()
268 })
269 )
270 );
271 }
272
273 fn amount_expr<T: Into<Decimal>>(value: T, commodity: &'static str) -> expr::Expr<'static> {
274 let v: Decimal = value.into();
275 expr::Expr::Value(Box::new(expr::ValueExpr::Amount(expr::Amount {
276 commodity: commodity.into(),
277 value: PrettyDecimal::unformatted(v),
278 })))
279 }
280
281 #[test]
282 fn value_expr_complex() {
283 let input = "(0 - -(1.20 + 2.67) * 3.1 USD + 5 USD)";
284 let want = expr::ValueExpr::Paren(expr::Expr::Binary(expr::BinaryOpExpr {
285 lhs: Box::new(expr::Expr::Binary(expr::BinaryOpExpr {
286 lhs: Box::new(amount_expr(dec!(0), "")),
287 op: expr::BinaryOp::Sub,
288 rhs: Box::new(expr::Expr::Binary(expr::BinaryOpExpr {
289 lhs: Box::new(expr::Expr::Unary(expr::UnaryOpExpr {
290 op: expr::UnaryOp::Negate,
291 expr: Box::new(expr::Expr::Value(Box::new(expr::ValueExpr::Paren(
292 expr::Expr::Binary(expr::BinaryOpExpr {
293 lhs: Box::new(amount_expr(dec!(1.20), "")),
294 op: expr::BinaryOp::Add,
295 rhs: Box::new(amount_expr(dec!(2.67), "")),
296 }),
297 )))),
298 })),
299 op: expr::BinaryOp::Mul,
300 rhs: Box::new(amount_expr(dec!(3.1), "USD")),
301 })),
302 })),
303 op: expr::BinaryOp::Add,
304 rhs: Box::new(amount_expr(5, "USD")),
305 }));
306
307 assert_eq!(expect_parse_ok(value_expr, input), ("", want))
308 }
309}