Skip to main content

beancount_parser/
account.rs

1use std::{
2    borrow::Borrow,
3    collections::HashSet,
4    fmt::{Display, Formatter},
5    str::FromStr,
6    sync::Arc,
7};
8
9use nom::{
10    bytes::complete::take_while,
11    character::complete::{char, satisfy, space0, space1},
12    combinator::{all_consuming, cut, iterator, opt, recognize},
13    multi::many1_count,
14    sequence::{delimited, preceded},
15    Finish, Parser,
16};
17
18use crate::{
19    amount::{self, Amount, Currency},
20    Decimal, Span,
21};
22
23use super::IResult;
24
25/// Account
26///
27/// You may convert it into a string slice with [`Account::as_str`]
28///
29/// # Example
30/// ```
31/// use beancount_parser::{BeancountFile, DirectiveContent};
32/// let input = "2022-05-24 open Assets:Bank:Checking";
33/// let beancount: BeancountFile<f64> = input.parse().unwrap();
34/// let DirectiveContent::Open(open) = &beancount.directives[0].content else { unreachable!() };
35/// assert_eq!(open.account.as_str(), "Assets:Bank:Checking");
36/// ```
37#[derive(Debug, Clone, PartialOrd, Ord, PartialEq, Eq, Hash)]
38pub struct Account(Arc<str>);
39
40impl Account {
41    /// Returns underlying string representation
42    #[must_use]
43    pub fn as_str(&self) -> &str {
44        &self.0
45    }
46}
47
48impl Display for Account {
49    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
50        Display::fmt(&self.0, f)
51    }
52}
53
54impl AsRef<str> for Account {
55    fn as_ref(&self) -> &str {
56        self.0.as_ref()
57    }
58}
59
60impl Borrow<str> for Account {
61    fn borrow(&self) -> &str {
62        self.0.borrow()
63    }
64}
65
66impl FromStr for Account {
67    type Err = crate::Error;
68
69    fn from_str(input: &str) -> Result<Self, Self::Err> {
70        let spanned = Span::new(input);
71        match all_consuming(parse).parse(spanned).finish() {
72            Ok((_, account)) => Ok(account),
73            Err(_) => Err(Self::Err::new(input, spanned)),
74        }
75    }
76}
77
78/// Open account directive
79///
80/// # Example
81/// ```
82/// use beancount_parser::{BeancountFile, DirectiveContent};
83/// let input = "2022-05-24 open Assets:Bank:Checking    CHF";
84/// let beancount: BeancountFile<f64> = input.parse().unwrap();
85/// let DirectiveContent::Open(open) = &beancount.directives[0].content else { unreachable!() };
86/// assert_eq!(open.account.as_str(), "Assets:Bank:Checking");
87/// assert_eq!(open.currencies.iter().next().unwrap().as_str(), "CHF");
88/// ```
89#[derive(Debug, Clone, PartialEq)]
90#[non_exhaustive]
91pub struct Open {
92    /// Account being open
93    pub account: Account,
94    /// Currency constraints
95    pub currencies: HashSet<Currency>,
96    /// Booking method
97    pub booking_method: Option<BookingMethod>,
98}
99
100impl Open {
101    /// Create an open directive from an account
102    #[must_use]
103    pub fn from_account(account: Account) -> Self {
104        Open {
105            account,
106            currencies: HashSet::new(),
107            booking_method: None,
108        }
109    }
110}
111
112#[derive(Debug, Clone, Eq, PartialEq, Ord, PartialOrd)]
113pub struct BookingMethod(Arc<str>);
114
115impl AsRef<str> for BookingMethod {
116    fn as_ref(&self) -> &str {
117        &self.0
118    }
119}
120
121impl Borrow<str> for BookingMethod {
122    fn borrow(&self) -> &str {
123        self.0.borrow()
124    }
125}
126
127impl Display for BookingMethod {
128    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
129        Display::fmt(&self.0, f)
130    }
131}
132
133impl From<&str> for BookingMethod {
134    fn from(value: &str) -> Self {
135        Self(Arc::from(value))
136    }
137}
138
139/// Close account directive
140///
141/// # Example
142/// ```
143/// use beancount_parser::{BeancountFile, DirectiveContent};
144/// let input = "2022-05-24 close Assets:Bank:Checking";
145/// let beancount: BeancountFile<f64> = input.parse().unwrap();
146/// let DirectiveContent::Close(close) = &beancount.directives[0].content else { unreachable!() };
147/// assert_eq!(close.account.as_str(), "Assets:Bank:Checking");
148/// ```
149#[derive(Debug, Clone, PartialEq)]
150#[non_exhaustive]
151pub struct Close {
152    /// Account being closed
153    pub account: Account,
154}
155
156impl Close {
157    /// Create a close directive from an account
158    #[must_use]
159    pub fn from_account(account: Account) -> Self {
160        Close { account }
161    }
162}
163
164/// Balance assertion
165///
166/// # Example
167/// ```
168/// use beancount_parser::{BeancountFile, DirectiveContent};
169/// let input = "2022-05-24 balance Assets:Bank:Checking 10 CHF";
170/// let beancount: BeancountFile<f64> = input.parse().unwrap();
171/// let DirectiveContent::Balance(balance) = &beancount.directives[0].content else { unreachable!() };
172/// assert_eq!(balance.account.as_str(), "Assets:Bank:Checking");
173/// assert_eq!(balance.amount.value, 10.0);
174/// assert_eq!(balance.amount.currency.as_str(), "CHF");
175/// ```
176#[derive(Debug, Clone, PartialEq)]
177#[non_exhaustive]
178pub struct Balance<D> {
179    /// Account being asserted
180    pub account: Account,
181    /// Amount the amount should have on the date
182    pub amount: Amount<D>,
183    /// Explicit precision tolerance
184    ///
185    /// See: <https://beancount.github.io/docs/precision_tolerances.html#explicit-tolerances-on-balance-assertions>
186    pub tolerance: Option<D>,
187}
188
189impl<D> Balance<D> {
190    /// Create a new from account and amount, with unspecified tolerance
191    pub fn new(account: Account, amount: Amount<D>) -> Self {
192        Balance {
193            account,
194            amount,
195            tolerance: None,
196        }
197    }
198}
199
200/// Pad directive
201///
202/// # Example
203/// ```
204/// # use beancount_parser::{BeancountFile, DirectiveContent};
205/// let raw = "2014-06-01 pad Assets:BofA:Checking Equity:Opening-Balances";
206/// let file: BeancountFile<f64> = raw.parse().unwrap();
207/// let DirectiveContent::Pad(pad) = &file.directives[0].content else { unreachable!() };
208/// assert_eq!(pad.account.as_str(), "Assets:BofA:Checking");
209/// assert_eq!(pad.source_account.as_str(), "Equity:Opening-Balances");
210/// ```
211#[derive(Debug, Clone, PartialEq)]
212#[non_exhaustive]
213pub struct Pad {
214    /// Account being padded
215    pub account: Account,
216    /// Source account from which take the money
217    pub source_account: Account,
218}
219
220impl Pad {
221    /// Create a new pad directive
222    #[must_use]
223    pub fn new(account: Account, source_account: Account) -> Self {
224        Pad {
225            account,
226            source_account,
227        }
228    }
229}
230
231pub(super) fn parse(input: Span<'_>) -> IResult<'_, Account> {
232    let (input, name) = recognize(preceded(
233        preceded(
234            satisfy(|c: char| c.is_uppercase() || c.is_ascii_digit()),
235            take_while(|c: char| c.is_alphanumeric() || c == '-'),
236        ),
237        cut(many1_count(preceded(
238            char(':'),
239            preceded(
240                satisfy(|c: char| c.is_uppercase() || c.is_ascii_digit()),
241                take_while(|c: char| c.is_alphanumeric() || c == '-'),
242            ),
243        ))),
244    ))
245    .parse(input)?;
246    Ok((input, Account(Arc::from(*name.fragment()))))
247}
248
249pub(super) fn open(input: Span<'_>) -> IResult<'_, Open> {
250    let (input, account) = parse(input)?;
251    let (input, currencies) = opt(preceded(space1, currencies)).parse(input)?;
252    let (input, booking_method) = opt(preceded(space1, crate::string)).parse(input)?;
253    Ok((
254        input,
255        Open {
256            account,
257            currencies: currencies.unwrap_or_default(),
258            booking_method: booking_method.map(|s| s.as_str().into()),
259        },
260    ))
261}
262
263fn currencies(input: Span<'_>) -> IResult<'_, HashSet<Currency>> {
264    let (input, first) = amount::currency(input)?;
265    let sep = delimited(space0, char(','), space0);
266    let mut iter = iterator(input, preceded(sep, amount::currency));
267    let mut currencies = HashSet::new();
268    currencies.insert(first);
269    currencies.extend(&mut iter);
270    let (input, ()) = iter.finish()?;
271    Ok((input, currencies))
272}
273
274pub(super) fn close(input: Span<'_>) -> IResult<'_, Close> {
275    let (input, account) = parse(input)?;
276    Ok((input, Close { account }))
277}
278
279pub(super) fn balance<D: Decimal>(input: Span<'_>) -> IResult<'_, Balance<D>> {
280    let (input, account) = parse(input)?;
281    let (input, _) = space1(input)?;
282    let (input, value) = amount::expression(input)?;
283    let (input, tolerance) = opt(preceded(space0, tolerance)).parse(input)?;
284    let (input, _) = space1(input)?;
285    let (input, currency) = amount::currency(input)?;
286    Ok((
287        input,
288        Balance {
289            account,
290            amount: Amount { value, currency },
291            tolerance,
292        },
293    ))
294}
295
296fn tolerance<D: Decimal>(input: Span<'_>) -> IResult<'_, D> {
297    let (input, _) = char('~')(input)?;
298    let (input, _) = space0(input)?;
299    let (input, tolerance) = amount::expression(input)?;
300    Ok((input, tolerance))
301}
302
303pub(super) fn pad(input: Span<'_>) -> IResult<'_, Pad> {
304    let (input, account) = parse(input)?;
305    let (input, _) = space1(input)?;
306    let (input, source_account) = parse(input)?;
307    Ok((
308        input,
309        Pad {
310            account,
311            source_account,
312        },
313    ))
314}