1use std::{
2 borrow::Borrow,
3 fmt::{Debug, Display, Formatter},
4 ops::{Add, Div, Mul, Neg, Sub},
5 str::FromStr,
6 sync::Arc,
7};
8
9use nom::{
10 branch::alt,
11 bytes::complete::{take_while, take_while1},
12 character::complete::{char, one_of, satisfy, space0, space1},
13 combinator::{all_consuming, iterator, map_res, opt, recognize, verify},
14 sequence::{delimited, preceded, terminated},
15 Finish, Parser,
16};
17
18use crate::{IResult, Span};
19
20#[derive(Debug, Clone, PartialEq)]
34pub struct Price<D> {
35 pub currency: Currency,
37 pub amount: Amount<D>,
39}
40
41#[derive(Debug, Clone, PartialEq)]
47pub struct Amount<D> {
48 pub value: D,
50 pub currency: Currency,
52}
53
54#[derive(Debug, Clone, PartialOrd, Ord, PartialEq, Eq, Hash)]
60pub struct Currency(Arc<str>);
61
62impl Currency {
63 #[must_use]
65 pub fn as_str(&self) -> &str {
66 &self.0
67 }
68}
69
70impl Display for Currency {
71 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
72 Display::fmt(&self.0, f)
73 }
74}
75
76impl AsRef<str> for Currency {
77 fn as_ref(&self) -> &str {
78 self.0.as_ref()
79 }
80}
81
82impl Borrow<str> for Currency {
83 fn borrow(&self) -> &str {
84 self.0.borrow()
85 }
86}
87
88impl<'a> TryFrom<&'a str> for Currency {
89 type Error = crate::ConversionError;
90 fn try_from(value: &'a str) -> Result<Self, Self::Error> {
91 value.parse().map_err(|_| crate::ConversionError)
92 }
93}
94
95impl FromStr for Currency {
96 type Err = crate::Error;
97 fn from_str(s: &str) -> Result<Self, Self::Err> {
98 let span = Span::new(s);
99 match all_consuming(currency).parse(span).finish() {
100 Ok((_, currency)) => Ok(currency),
101 Err(_) => Err(crate::Error::new(s, span)),
102 }
103 }
104}
105
106pub(crate) fn parse<D: Decimal>(input: Span<'_>) -> IResult<'_, Amount<D>> {
107 let (input, value) = expression(input)?;
108 let (input, _) = space1(input)?;
109 let (input, currency) = currency(input)?;
110 Ok((input, Amount { value, currency }))
111}
112
113pub(crate) fn expression<D: Decimal>(input: Span<'_>) -> IResult<'_, D> {
114 alt((negation, sum)).parse(input)
115}
116
117fn sum<D: Decimal>(input: Span<'_>) -> IResult<'_, D> {
118 let (input, value) = product(input)?;
119 let mut iter = iterator(input, (delimited(space0, one_of("+-"), space0), product));
120 let value = iter.by_ref().fold(value, |a, (op, b)| match op {
121 '+' => a + b,
122 '-' => a - b,
123 op => unreachable!("unsupported operator: {}", op),
124 });
125 let (input, ()) = iter.finish()?;
126 Ok((input, value))
127}
128
129fn product<D: Decimal>(input: Span<'_>) -> IResult<'_, D> {
130 let (input, value) = atom(input)?;
131 let mut iter = iterator(input, (delimited(space0, one_of("*/"), space0), atom));
132 let value = iter.by_ref().fold(value, |a, (op, b)| match op {
133 '*' => a * b,
134 '/' => a / b,
135 op => unreachable!("unsupported operator: {}", op),
136 });
137 let (input, ()) = iter.finish()?;
138 Ok((input, value))
139}
140
141fn atom<D: Decimal>(input: Span<'_>) -> IResult<'_, D> {
142 alt((literal, group)).parse(input)
143}
144
145fn group<D: Decimal>(input: Span<'_>) -> IResult<'_, D> {
146 delimited(
147 terminated(char('('), space0),
148 expression,
149 preceded(space0, char(')')),
150 )
151 .parse(input)
152}
153
154fn negation<D: Decimal>(input: Span<'_>) -> IResult<'_, D> {
155 let (input, _) = char('-')(input)?;
156 let (input, _) = space0(input)?;
157 let (input, expr) = group::<D>(input)?;
158 Ok((input, -expr))
159}
160
161fn literal<D: Decimal>(input: Span<'_>) -> IResult<'_, D> {
162 map_res(
163 recognize((
164 opt(char('-')),
165 space0,
166 take_while1(|c: char| c.is_numeric() || c == '.' || c == ','),
167 )),
168 |s: Span<'_>| s.fragment().replace([',', ' '], "").parse(),
169 )
170 .parse(input)
171}
172
173pub(crate) fn price<D: Decimal>(input: Span<'_>) -> IResult<'_, Price<D>> {
174 let (input, currency) = currency(input)?;
175 let (input, _) = space1(input)?;
176 let (input, amount) = parse(input)?;
177 Ok((input, Price { currency, amount }))
178}
179
180pub(crate) fn currency(input: Span<'_>) -> IResult<'_, Currency> {
181 let (input, currency) = recognize((
182 satisfy(char::is_uppercase),
183 verify(
184 take_while(|c: char| {
185 c.is_uppercase() || c.is_numeric() || c == '-' || c == '_' || c == '.' || c == '\''
186 }),
187 |s: &Span<'_>| {
188 s.fragment()
189 .chars()
190 .last()
191 .map_or(true, |c| c.is_uppercase() || c.is_numeric())
192 },
193 ),
194 ))
195 .parse(input)?;
196 Ok((input, Currency(Arc::from(*currency.fragment()))))
197}
198
199pub trait Decimal:
209 FromStr
210 + Default
211 + Clone
212 + Debug
213 + Add<Output = Self>
214 + Sub<Output = Self>
215 + Mul<Output = Self>
216 + Div<Output = Self>
217 + Neg<Output = Self>
218 + PartialEq
219 + PartialOrd
220{
221}
222
223impl<D> Decimal for D where
224 D: FromStr
225 + Default
226 + Clone
227 + Debug
228 + Add<Output = Self>
229 + Sub<Output = Self>
230 + Mul<Output = Self>
231 + Div<Output = Self>
232 + Neg<Output = Self>
233 + PartialEq
234 + PartialOrd
235{
236}
237
238#[cfg(test)]
239mod tests {
240 use super::*;
241 use rstest::rstest;
242
243 #[rstest]
244 #[case("CHF")]
245 fn currency_from_str_should_parse_valid_currency(#[case] input: &str) {
246 let currency: Currency = input.parse().unwrap();
247 assert_eq!(currency.as_str(), input);
248 }
249
250 #[rstest]
251 #[case("")]
252 #[case(" ")]
253 #[case("oops")]
254 fn currency_from_str_should_not_parse_invalid_currency(#[case] input: &str) {
255 let currency: Result<Currency, _> = input.parse();
256 assert!(currency.is_err(), "{currency:?}");
257 }
258}