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::{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
19/// Parses value expression.
20pub 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
123/// Parses amount expression, possibly with negative op.
124fn 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    // This supports prefix commodity.
131    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
181/// Parses amount expression.
182pub 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    // Currently it only supports suffix commodity,
189    // and there is no plan to support prefix commodities.
190    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
204/// Parses `x (op x)*` format, and feed the list into the given function.
205/// This is similar to foldl, so it'll be evaluated as `f(f(...f(x, x), x), ... x)))`.
206/// operand parser needs to be Copy so that it can be used twice.
207fn 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}