okane_core/parse/
expr.rs

1//! Defines parsers related to value expression.
2
3use 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
19/// Parses value expression.
20pub 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
125/// Parses amount expression, possibly with negative op.
126// Only used for [`Amount`] `TryFrom<&'str>` implementation.
127fn 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    // This supports prefix commodity.
134    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
186/// Parses amount expression.
187pub 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    // Currently it only supports suffix commodity,
194    // and there is no plan to support prefix commodities.
195    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
209/// Parses `x (op x)*` format, and feed the list into the given function.
210/// This is similar to foldl, so it'll be evaluated as `f(f(...f(x, x), x), ... x)))`.
211/// operand parser needs to be Copy so that it can be used twice.
212fn 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}