Skip to main content

beancount_parser/
amount.rs

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/// Price directive
21///
22/// # Example
23///
24/// ```
25/// use beancount_parser::{BeancountFile, DirectiveContent};
26/// let input = "2023-05-27 price CHF  4 PLN";
27/// let beancount: BeancountFile<f64> = input.parse().unwrap();
28/// let DirectiveContent::Price(price) = &beancount.directives[0].content else { unreachable!() };
29/// assert_eq!(price.currency.as_str(), "CHF");
30/// assert_eq!(price.amount.value, 4.0);
31/// assert_eq!(price.amount.currency.as_str(), "PLN");
32/// ```
33#[derive(Debug, Clone, PartialEq)]
34pub struct Price<D> {
35    /// Currency
36    pub currency: Currency,
37    /// Price of the currency
38    pub amount: Amount<D>,
39}
40
41/// Amount
42///
43/// Where `D` is the decimal type (like `f64` or `rust_decimal::Decimal`)
44///
45/// For an example, look at the [`Price`] directive
46#[derive(Debug, Clone, PartialEq)]
47pub struct Amount<D> {
48    /// The value (decimal) part
49    pub value: D,
50    /// Currency
51    pub currency: Currency,
52}
53
54/// Currency
55///
56/// One may use [`Currency::as_str`] to get the string representation of the currency
57///
58/// For an example, look at the [`Price`] directive
59#[derive(Debug, Clone, PartialOrd, Ord, PartialEq, Eq, Hash)]
60pub struct Currency(Arc<str>);
61
62impl Currency {
63    /// Returns underlying string representation
64    #[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
199/// Decimal type to which amount values and expressions will be parsed into.
200///
201/// # Notable implementations
202///
203/// * `f64`
204/// * `Decimal` of the crate [rust_decimal]
205///
206/// [rust_decimal]: https://docs.rs/rust_decimal
207///
208pub 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}