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