1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
//! Defines parser for the Ledger format.

mod character;
mod combinator;
mod directive;
mod expr;
mod metadata;
mod posting;
mod primitive;
mod transaction;

#[cfg(test)]
pub mod testing;

use crate::repl;

use nom::{
    branch::alt,
    character::complete::{anychar, line_ending},
    combinator::{eof, fail, map, peek},
    error::{context, convert_error, VerboseError},
    multi::{many0, many_till},
    sequence::{preceded, terminated},
    Finish, IResult,
};

#[derive(thiserror::Error, Debug)]
#[error("failed to parse the input: \n{0}")]
pub struct ParseLedgerError(String);

/// Parses the whole ledger file.
pub fn parse_ledger(input: &str) -> Result<Vec<repl::LedgerEntry>, ParseLedgerError> {
    match preceded(
        many0(line_ending),
        many_till(terminated(parse_ledger_entry, many0(line_ending)), eof),
    )(input)
    .finish()
    {
        Ok((_, (ret, _))) => Ok(ret),
        Err(e) => Err(ParseLedgerError(convert_error(input, e))),
    }
}

fn parse_ledger_entry(input: &str) -> IResult<&str, repl::LedgerEntry, VerboseError<&str>> {
    let (input, c) = peek(anychar)(input)?;
    match c {
        ';' | '#' | '%' | '|' | '*' => {
            map(directive::top_comment, repl::LedgerEntry::Comment)(input)
        }
        'a' => alt((
            map(directive::account_declaration, repl::LedgerEntry::Account),
            map(directive::apply_tag, repl::LedgerEntry::ApplyTag),
        ))(input),
        'c' => map(
            directive::commodity_declaration,
            repl::LedgerEntry::Commodity,
        )(input),
        'e' => map(directive::end_apply_tag, |_| repl::LedgerEntry::EndApplyTag)(input),
        'i' => map(directive::include, repl::LedgerEntry::Include)(input),
        c if c.is_ascii_digit() => map(transaction::transaction, repl::LedgerEntry::Txn)(input),
        _ => context("unexpected character", fail)(input),
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    use chrono::NaiveDate;
    use pretty_assertions::assert_eq;

    #[test]
    fn parse_ledger_skips_empty_lines() {
        let input = "\n\n2022/01/23\n";
        assert_eq!(input.chars().next(), Some('\n'));
        assert_eq!(
            parse_ledger(input).unwrap(),
            vec![repl::LedgerEntry::Txn(repl::Transaction::new(
                NaiveDate::from_ymd_opt(2022, 1, 23).unwrap(),
                "".to_string()
            ))]
        );
    }
}