hledger_parser/component/
amount.rs

1use chumsky::prelude::*;
2use rust_decimal::Decimal;
3
4use crate::component::commodity::commodity;
5use crate::component::quantity::quantity;
6use crate::component::whitespace::whitespace;
7use crate::state::State;
8
9#[derive(Debug, Default, Clone, PartialEq)]
10pub struct Amount {
11    pub quantity: Decimal,
12    pub commodity: String,
13}
14
15pub fn amount<'a>() -> impl Parser<'a, &'a str, Amount, extra::Full<Rich<'a, char>, State, ()>> {
16    let sign_quantity_commodity = one_of("-+")
17        .then_ignore(whitespace().repeated())
18        .then(quantity())
19        .then_ignore(whitespace().repeated())
20        .then(commodity())
21        .map(|((sign, mut quantity), commodity)| {
22            if sign == '-' {
23                quantity.set_sign_negative(true);
24            }
25            Amount {
26                quantity,
27                commodity,
28            }
29        });
30    let quantity_sign_commodity = quantity()
31        .then_ignore(whitespace().repeated())
32        .then(one_of("-+"))
33        .then_ignore(whitespace().repeated())
34        .then(commodity())
35        .map(|((mut quantity, sign), commodity)| {
36            if sign == '-' {
37                quantity.set_sign_negative(true);
38            }
39            Amount {
40                quantity,
41                commodity,
42            }
43        });
44    let sign_commodity_quantity = one_of("-+")
45        .then_ignore(whitespace().repeated())
46        .then(commodity())
47        .then_ignore(whitespace().repeated())
48        .then(quantity())
49        .map(|((sign, commodity), mut quantity)| {
50            if sign == '-' {
51                quantity.set_sign_negative(true);
52            }
53            Amount {
54                quantity,
55                commodity,
56            }
57        });
58    let commodity_sign_quantity = commodity()
59        .then_ignore(whitespace().repeated())
60        .then(one_of("-+"))
61        .then_ignore(whitespace().repeated())
62        .then(quantity())
63        .map(|((commodity, sign), mut quantity)| {
64            if sign == '-' {
65                quantity.set_sign_negative(true);
66            }
67            Amount {
68                quantity,
69                commodity,
70            }
71        });
72    let quantity_commodity = quantity()
73        .then_ignore(whitespace().repeated())
74        .then(commodity())
75        .map(|(quantity, commodity)| Amount {
76            quantity,
77            commodity,
78        });
79    let commodity_quantity = commodity()
80        .then_ignore(whitespace().repeated())
81        .then(quantity())
82        .map(|(commodity, quantity)| Amount {
83            quantity,
84            commodity,
85        });
86    let just_quantity = quantity().map(|quantity| Amount {
87        quantity,
88        ..Amount::default()
89    });
90    sign_quantity_commodity
91        .or(quantity_sign_commodity)
92        .or(sign_commodity_quantity)
93        .or(commodity_sign_quantity)
94        .or(quantity_commodity)
95        .or(commodity_quantity)
96        .or(just_quantity)
97}
98
99#[cfg(test)]
100mod tests {
101    use super::*;
102
103    #[test]
104    fn quantity_no_commodity() {
105        let result = amount().then_ignore(end()).parse("1").into_result();
106        assert_eq!(
107            result,
108            Ok(Amount {
109                quantity: Decimal::new(1, 0),
110                ..Amount::default()
111            })
112        );
113    }
114
115    #[test]
116    fn quantity_with_commodity() {
117        for (input, expected) in [
118            (
119                "$1",
120                Amount {
121                    quantity: Decimal::new(1, 0),
122                    commodity: String::from("$"),
123                },
124            ),
125            (
126                "4000 AAPL",
127                Amount {
128                    quantity: Decimal::new(4000, 0),
129                    commodity: String::from("AAPL"),
130                },
131            ),
132            (
133                "3 \"green apples\"",
134                Amount {
135                    quantity: Decimal::new(3, 0),
136                    commodity: String::from("green apples"),
137                },
138            ),
139        ] {
140            let result = amount().then_ignore(end()).parse(input).into_result();
141            assert_eq!(result, Ok(expected), "{input}");
142        }
143    }
144
145    #[test]
146    fn signed_quantity_with_commodity() {
147        for (input, expected) in [
148            (
149                "-$1",
150                Amount {
151                    quantity: Decimal::new(-1, 0),
152                    commodity: String::from("$"),
153                },
154            ),
155            (
156                "$-1",
157                Amount {
158                    quantity: Decimal::new(-1, 0),
159                    commodity: String::from("$"),
160                },
161            ),
162            (
163                "+ $1",
164                Amount {
165                    quantity: Decimal::new(1, 0),
166                    commodity: String::from("$"),
167                },
168            ),
169            (
170                "$-      1",
171                Amount {
172                    quantity: Decimal::new(-1, 0),
173                    commodity: String::from("$"),
174                },
175            ),
176            (
177                "-1 USD",
178                Amount {
179                    quantity: Decimal::new(-1, 0),
180                    commodity: String::from("USD"),
181                },
182            ),
183        ] {
184            let result = amount().then_ignore(end()).parse(input).into_result();
185            assert_eq!(result, Ok(expected), "{input}");
186        }
187    }
188}