okane_core/report/
context.rs

1use bumpalo::Bump;
2
3use super::{
4    commodity::{Commodity, CommodityStore},
5    intern::{FromInterned, InternStore, InternedStr, StoredValue},
6};
7
8/// `&str` for accounts, interned within the `'arena` bounded allocator lifetime.
9#[derive(Debug, PartialEq, Eq, Hash, Clone, Copy)]
10pub struct Account<'arena>(InternedStr<'arena>);
11
12impl<'arena> FromInterned<'arena> for Account<'arena> {
13    fn from_interned(v: InternedStr<'arena>) -> Self {
14        Self(v)
15    }
16
17    fn as_interned(&self) -> InternedStr<'arena> {
18        self.0
19    }
20}
21
22impl<'arena> Account<'arena> {
23    /// Returns the `&str`.
24    pub fn as_str(&self) -> &'arena str {
25        self.0.as_str()
26    }
27}
28
29/// `Interner` for `Account`.
30pub(super) type AccountStore<'arena> = InternStore<'arena, Account<'arena>>;
31
32/// Context object extensively used across Ledger file evaluation.
33pub struct ReportContext<'ctx> {
34    pub(super) arena: &'ctx Bump,
35    pub(super) accounts: AccountStore<'ctx>,
36    pub(super) commodities: CommodityStore<'ctx>,
37}
38
39impl<'ctx> ReportContext<'ctx> {
40    /// Create a new instance of `ReportContext`.
41    pub fn new(arena: &'ctx Bump) -> Self {
42        let accounts = AccountStore::new(arena);
43        let commodities = CommodityStore::new(arena);
44        Self {
45            arena,
46            accounts,
47            commodities,
48        }
49    }
50
51    /// Returns all accounts, sorted as string order.
52    pub(super) fn all_accounts_unsorted(&self) -> impl Iterator<Item = Account<'ctx>> + '_ {
53        self.accounts.iter().filter_map(|x| match x {
54            StoredValue::Canonical(x) => Some(x),
55            StoredValue::Alias { .. } => None,
56        })
57    }
58    /// Returns all accounts, sorted as string order.
59    pub(super) fn all_accounts(&self) -> Vec<Account<'ctx>> {
60        let mut r: Vec<Account<'ctx>> = self.all_accounts_unsorted().collect();
61        r.sort_unstable_by_key(|x| x.as_str());
62        r
63    }
64
65    /// Returns the given account, or `None` if not found.
66    pub fn account(&self, value: &str) -> Option<Account<'ctx>> {
67        self.accounts.resolve(value)
68    }
69
70    /// Returns the given commmodity, or `None` if not found.
71    pub fn commodity(&self, value: &str) -> Option<Commodity<'ctx>> {
72        self.commodities.resolve(value)
73    }
74}
75
76#[cfg(test)]
77mod tests {
78    use pretty_assertions::assert_eq;
79
80    use super::*;
81
82    #[test]
83    fn context_all_accounts() {
84        let arena = Bump::new();
85        let mut ctx = ReportContext::new(&arena);
86        let want = vec![
87            ctx.accounts.ensure("Account 1"),
88            ctx.accounts.ensure("Account 2"),
89            ctx.accounts.ensure("Account 2:Sub account"),
90            ctx.accounts.ensure("Account 3"),
91            // I don't think ordering in Japanese doesn't make sense a lot without 'yomi' information.
92            // OTOH, I don't have plan to use Japanese label, thus no urgent priorities.
93            ctx.accounts.ensure("資産:りんご"),
94            ctx.accounts.ensure("資産:バナナ"),
95            ctx.accounts.ensure("資産:吉祥寺"),
96        ];
97
98        let got = ctx.all_accounts();
99
100        assert_eq!(want, got);
101    }
102
103    #[test]
104    fn context_sccount() {
105        let arena = Bump::new();
106        let mut ctx = ReportContext::new(&arena);
107        let a1 = ctx.accounts.ensure("Account 1");
108        let a3 = ctx.accounts.ensure("Account 3");
109
110        assert_eq!(Some(a1), ctx.account("Account 1"));
111        assert_eq!(None, ctx.account("Account 2"));
112        assert_eq!(Some(a3), ctx.account("Account 3"));
113    }
114}