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
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
//! Types for representing an [`Account`]

use std::fmt::Display;

#[cfg(feature = "unstable")]
use crate::pest_parser::Pair;
use crate::{IResult, Span};
use nom::{
    branch::alt,
    bytes::complete::{tag, take_while1},
    character::complete::char,
    combinator::{iterator, map},
    sequence::preceded,
};

/// Account
///
/// An account has a type (`Assets`, `Liabilities`, `Equity`, `Income` or `Expenses`)
/// and components.
///
/// # Examples
///
/// * `Assets:Liquidity:Cash` (type: `Assets`, components: ["Liquidity", "Cash"]
/// * `Expenses:Groceries` (type: `Assets`, components: ["Groceries"]
#[derive(Debug, Clone, Eq, PartialEq, Hash)]
pub struct Account<'a> {
    type_: Type,
    components: Vec<&'a str>,
}

impl Display for Account<'_> {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "{}", self.type_)?;
        for c in &self.components {
            write!(f, ":{c}")?;
        }
        Ok(())
    }
}

/// Type of account
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
pub enum Type {
    /// The assets
    Assets,
    /// The liabilities
    Liabilities,
    /// The equity
    Equity,
    /// Income
    Income,
    /// Expenses
    Expenses,
}

impl Display for Type {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        std::fmt::Debug::fmt(self, f)
    }
}

impl<'a> Account<'a> {
    #[cfg(test)]
    pub(crate) fn new(type_: Type, path: impl IntoIterator<Item = &'a str>) -> Self {
        Self {
            type_,
            components: path.into_iter().collect(),
        }
    }

    /// Returns the type of account
    #[must_use]
    pub fn type_(&self) -> Type {
        self.type_
    }

    /// Returns the components
    #[must_use]
    pub fn components(&self) -> &[&'a str] {
        self.components.as_ref()
    }

    #[cfg(feature = "unstable")]
    pub(crate) fn from_pair(pair: Pair<'a>) -> Self {
        let mut inner = pair.into_inner();
        let type_ = match inner.next().expect("no account type in account").as_str() {
            "Assets" => Type::Assets,
            "Liabilities" => Type::Liabilities,
            "Expenses" => Type::Expenses,
            "Income" => Type::Income,
            "Equity" => Type::Equity,
            _ => unreachable!("invalid account type"),
        };
        let components = inner.map(|c| c.as_str()).collect();
        Account { type_, components }
    }
}

pub(crate) fn account(input: Span<'_>) -> IResult<'_, Account<'_>> {
    let (input, type_) = type_(input)?;
    let mut iter = iterator(
        input,
        preceded(
            char(':'),
            take_while1(|c: char| c.is_alphanumeric() || c == '-'),
        ),
    );
    let components = iter.map(|s| *s.fragment()).collect();
    let (input, _) = iter.finish()?;
    Ok((input, Account { type_, components }))
}

fn type_(input: Span<'_>) -> IResult<'_, Type> {
    alt((
        map(tag("Assets"), |_| Type::Assets),
        map(tag("Liabilities"), |_| Type::Liabilities),
        map(tag("Income"), |_| Type::Income),
        map(tag("Expenses"), |_| Type::Expenses),
        map(tag("Equity"), |_| Type::Equity),
    ))(input)
}

#[cfg(test)]
mod tests {
    use nom::combinator::all_consuming;

    use super::*;

    #[rstest]
    #[case("Assets", Account::new(Type::Assets, []))]
    #[case("Assets:Hello", Account::new(Type::Assets, ["Hello"]))]
    #[case("Assets:MyAccount", Account::new(Type::Assets, ["MyAccount"]))]
    #[case("Liabilities:A:B:C", Account::new(Type::Liabilities, ["A", "B", "C"]))]
    #[case("Income:Foo:Bar12", Account::new(Type::Income, ["Foo", "Bar12"]))]
    #[case("Expenses:3Foo", Account::new(Type::Expenses, ["3Foo"]))]
    #[case("Equity:Foo-Bar", Account::new(Type::Equity, ["Foo-Bar"]))]
    fn valid_account(#[case] input: &str, #[case] expected: Account<'_>) {
        let (_, actual) = all_consuming(account)(Span::new(input)).unwrap();
        assert_eq!(actual, expected);
        let formatted = format!("{actual}");
        assert_eq!(&formatted, input);
    }

    #[rstest]
    #[case(Type::Assets, "Assets")]
    #[case(Type::Liabilities, "Liabilities")]
    #[case(Type::Income, "Income")]
    #[case(Type::Expenses, "Expenses")]
    #[case(Type::Equity, "Equity")]
    fn display_type(#[case] type_: Type, #[case] expected: &str) {
        let actual = format!("{type_}");
        assert_eq!(actual, expected);
    }
}