hledger_parser/component/
amount.rs1use 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}