Skip to main content

okane_core/report/
account.rs

1//! Defines account and its related types.
2
3use std::collections::{HashMap, hash_map};
4
5use bumpalo::Bump;
6use bumpalo_intern::direct::{
7    DirectInternStore, FromInterned, InternedStr, OccupiedError, StoredValue,
8};
9
10/// Account which used in the Ledger format to store the balance with,
11/// such as `Assets:Bank:Foo` or `Income:Salary`.
12#[derive(Debug, PartialEq, Eq, Hash, Clone, Copy)]
13pub struct Account<'arena>(InternedStr<'arena>);
14
15impl<'arena> FromInterned<'arena> for Account<'arena> {
16    fn from_interned(v: InternedStr<'arena>) -> Self {
17        Self(v)
18    }
19
20    fn as_interned(&self) -> InternedStr<'arena> {
21        self.0
22    }
23}
24
25impl<'arena> Account<'arena> {
26    /// Returns the `&str`.
27    #[inline]
28    pub fn as_str(&self) -> &'arena str {
29        self.0.as_str()
30    }
31}
32
33/// Manages [`Account`] instances.
34pub struct AccountStore<'arena> {
35    /// Interned Account store.
36    intern: DirectInternStore<'arena, Account<'arena>>,
37}
38
39impl<'arena> AccountStore<'arena> {
40    /// Creates a new instance.
41    pub fn new(arena: &'arena Bump) -> Self {
42        Self {
43            intern: DirectInternStore::new(arena),
44        }
45    }
46
47    /// Returns the [`Account`] with the given `value`,
48    /// potentially resolving the alias.
49    /// If not available, registers the given `value` as the canonical.
50    pub fn ensure(&mut self, value: &str) -> Account<'arena> {
51        self.intern.ensure(value)
52    }
53
54    /// Returns the Account with the given `value` if and only if it's already registered.
55    pub fn resolve(&self, value: &str) -> Option<Account<'arena>> {
56        self.intern.resolve(value)
57    }
58
59    /// Registers given `value` as always alias of `canonical`.
60    /// Returns error if given `value` is already registered as canonical.
61    pub fn register_alias(
62        &mut self,
63        value: &str,
64        canonical: Account<'arena>,
65    ) -> Result<(), OccupiedError<'arena, Account<'arena>>> {
66        self.intern.register_alias(value, canonical)
67    }
68
69    /// Returns the [`Iterator`] for just `Account`.
70    /// Order is unspecified.
71    pub fn iter(&self) -> impl Iterator<Item = Account<'arena>> {
72        self.intern.iter().filter_map(|v| match v {
73            StoredValue::Alias { .. } => None,
74            StoredValue::Canonical(a) => Some(a),
75        })
76    }
77}
78
79/// Represents not the [`Account`] itself, but rather a ancestor group which
80/// itself is not an acount.
81///
82/// For example, if the Ledger has `Assets:Bank:Foo`, `Assets:Bank` or `Assets`
83/// might be a [`AccountGroup`] if those don't appear as accounts.
84#[derive(Debug, PartialEq, Eq, Hash, Clone, Copy)]
85pub struct AccountAncestor<'arena>(InternedStr<'arena>);
86
87impl<'arena> FromInterned<'arena> for AccountAncestor<'arena> {
88    fn from_interned(v: InternedStr<'arena>) -> Self {
89        Self(v)
90    }
91
92    fn as_interned(&self) -> InternedStr<'arena> {
93        self.0
94    }
95}
96
97/// Represents either an [`AccountAncestor`] or an [`Account`].
98/// Useful for tree representation.
99#[derive(Debug, PartialEq, Eq, Clone, Copy, Hash)]
100pub enum AccountAggregate<'arena> {
101    /// Concrete account which appeared in the Ledger file.
102    Account(Account<'arena>),
103    /// Ancestor of the tree acounts, which doesn't appear in the Ledger.
104    Ancestor(AccountAncestor<'arena>),
105}
106
107impl<'arena> From<Account<'arena>> for AccountAggregate<'arena> {
108    fn from(value: Account<'arena>) -> Self {
109        Self::Account(value)
110    }
111}
112
113impl<'arena> From<AccountAncestor<'arena>> for AccountAggregate<'arena> {
114    fn from(value: AccountAncestor<'arena>) -> Self {
115        Self::Ancestor(value)
116    }
117}
118
119impl<'arena> AccountAggregate<'arena> {
120    /// Returns the `&str` representation.
121    #[inline]
122    pub fn as_str(&self) -> &'arena str {
123        match self {
124            AccountAggregate::Account(Account(v)) => v.as_str(),
125            AccountAggregate::Ancestor(AccountAncestor(v)) => v.as_str(),
126        }
127    }
128
129    /// Returns the [`Account`] if this is an account, otherwise `None`.
130    #[inline]
131    pub fn as_account(&self) -> Option<Account<'arena>> {
132        match self {
133            AccountAggregate::Account(a) => Some(*a),
134            AccountAggregate::Ancestor(_) => None,
135        }
136    }
137
138    /// Returns the last colon-separated segment of the account name, i.e. the
139    /// leaf label in a tree view (the whole name when there is no `:`).
140    #[inline]
141    pub fn last_segment(&self) -> &'arena str {
142        let full = self.as_str();
143        full.rsplit_once(':').map_or(full, |(_, leaf)| leaf)
144    }
145}
146
147/// Represents the tree structure of the accounts, stored in the [`AccountStore`].
148pub struct AccountTree<'arena> {
149    prefixes: DirectInternStore<'arena, AccountAncestor<'arena>>,
150    root_children: Vec<AccountAggregate<'arena>>,
151    nodes: HashMap<AccountAggregate<'arena>, Node<'arena>>,
152}
153
154impl<'arena> AccountTree<'arena> {
155    /// Creates a new instance.
156    ///
157    /// This constructs only a plain empty tree, and the actual work is done
158    /// in [`Self::construct()`] because we want to retain the `prefixes`
159    /// lifetime aligned with [`AccountStore`].
160    pub fn new(arena: &'arena Bump) -> Self {
161        Self {
162            prefixes: DirectInternStore::new(arena),
163            root_children: Vec::new(),
164            nodes: HashMap::new(),
165        }
166    }
167
168    /// Constructs the tree state from the given [`AccountStore`].
169    pub fn construct(&mut self, accounts: &AccountStore<'arena>) {
170        self.root_children.clear();
171        self.nodes.clear();
172        for account in accounts.iter() {
173            if self.nodes.contains_key(&account.into()) {
174                continue;
175            }
176            let mut current = AccountAggregate::Account(account);
177            loop {
178                let Some(parent) = self.ensure_parent(accounts, current) else {
179                    // parent is root.
180                    self.nodes.entry(current).or_insert_with(Node::placeholder);
181                    self.root_children.push(current);
182                    break;
183                };
184                let child_node = self.nodes.entry(current).or_insert_with(Node::placeholder);
185                child_node.parent = AccountTreeKey::Descendant(parent);
186                let (children, already_exists) = match self.nodes.entry(parent) {
187                    hash_map::Entry::Occupied(existing) => {
188                        (&mut existing.into_mut().children, true)
189                    }
190                    hash_map::Entry::Vacant(new) => {
191                        (&mut new.insert(Node::placeholder()).children, false)
192                    }
193                };
194                children.push(current);
195                if already_exists {
196                    break;
197                };
198                current = parent;
199            }
200        }
201        self.root_children.sort_by_key(AccountAggregate::as_str);
202        for node in self.nodes.values_mut() {
203            node.children.sort_by_key(AccountAggregate::as_str);
204        }
205    }
206
207    /// Ensures the parent of the given `child` account.
208    /// Returns `None` if the parent is Root element of the tree.
209    fn ensure_parent(
210        &mut self,
211        accounts: &AccountStore<'arena>,
212        child: AccountAggregate<'arena>,
213    ) -> Option<AccountAggregate<'arena>> {
214        // if this is `None`, this means the given child is simple "Foo", and the parent is the root.
215        let (parent, _) = child.as_str().rsplit_once(':')?;
216        Some(match accounts.resolve(parent) {
217            Some(v) => AccountAggregate::Account(v),
218            None => AccountAggregate::Ancestor(self.prefixes.ensure(parent)),
219        })
220    }
221
222    /// Resolves the [`AccountAncestor`] corresponding to the given `label`.
223    /// Returns `None` if not found.
224    /// Note that this would also return `None` if the given label is [`Account`].
225    /// See [`super::ReportContext::account_aggregate()`], that is more convenient.
226    pub fn resolve_ancestor(&self, label: &str) -> Option<AccountAncestor<'arena>> {
227        self.prefixes.resolve(label)
228    }
229
230    /// Returns the parent of the given node.
231    /// `None` if the given `account` is not found in the tree.
232    pub fn parent<T: Into<AccountAggregate<'arena>>>(
233        &self,
234        account: T,
235    ) -> Option<AccountTreeKey<'arena>> {
236        self.nodes.get(&account.into()).map(|node| node.parent)
237    }
238
239    /// Returns the children of the given node.
240    /// `None` if the given `key` is not found,
241    /// and empty slice for the leaf account.
242    pub fn children<T: Into<AccountTreeKey<'arena>>>(
243        &self,
244        key: T,
245    ) -> Option<&[AccountAggregate<'arena>]> {
246        match key.into() {
247            AccountTreeKey::Root => Some(&self.root_children),
248            AccountTreeKey::Descendant(account) => {
249                self.nodes.get(&account).map(|n| n.children.as_slice())
250            }
251        }
252    }
253}
254
255/// Key of the [`AccountTree`] which can be treated as a tree node.
256#[derive(Debug, PartialEq, Eq, Clone, Copy, Hash)]
257pub enum AccountTreeKey<'arena> {
258    /// Root of the tree.
259    Root,
260    /// Non-root element of the tree.
261    Descendant(AccountAggregate<'arena>),
262}
263
264impl<'arena> AccountTreeKey<'arena> {
265    /// Returns as [`Option<AccountAggregate>`].
266    /// It will be `Some` unless the self is root.
267    #[inline]
268    pub fn as_aggregate(self) -> Option<AccountAggregate<'arena>> {
269        match self {
270            Self::Root => None,
271            Self::Descendant(a) => Some(a),
272        }
273    }
274}
275
276impl<'arena> From<Account<'arena>> for AccountTreeKey<'arena> {
277    fn from(value: Account<'arena>) -> Self {
278        Self::Descendant(value.into())
279    }
280}
281
282impl<'arena> From<AccountAncestor<'arena>> for AccountTreeKey<'arena> {
283    fn from(value: AccountAncestor<'arena>) -> Self {
284        Self::Descendant(value.into())
285    }
286}
287
288impl<'arena> From<AccountAggregate<'arena>> for AccountTreeKey<'arena> {
289    fn from(value: AccountAggregate<'arena>) -> Self {
290        Self::Descendant(value)
291    }
292}
293
294/// Node of the [`AccountTree`].
295#[derive(Debug, Clone)]
296struct Node<'arena> {
297    parent: AccountTreeKey<'arena>,
298    children: Vec<AccountAggregate<'arena>>,
299}
300
301impl<'arena> Node<'arena> {
302    /// Creates a place holder.
303    fn placeholder() -> Self {
304        Self {
305            parent: AccountTreeKey::Root,
306            children: Vec::new(),
307        }
308    }
309}
310
311#[cfg(test)]
312mod tests {
313    use super::*;
314
315    use pretty_assertions::assert_eq;
316
317    #[test]
318    fn account_aggregate_last_segment() {
319        let arena = Bump::new();
320        let mut accounts = AccountStore::new(&arena);
321        let mut tree = AccountTree::new(&arena);
322        let grocery = accounts.ensure("Expenses:Grocery");
323        tree.construct(&accounts);
324        let expenses = tree.resolve_ancestor("Expenses").unwrap();
325
326        assert_eq!("Grocery", AccountAggregate::from(grocery).last_segment());
327        assert_eq!("Expenses", AccountAggregate::from(expenses).last_segment());
328    }
329
330    mod account_tree {
331        use super::assert_eq;
332        use super::*;
333
334        #[test]
335        fn init_reuses_previous_value() {
336            let arena = Bump::new();
337            let mut accounts = AccountStore::new(&arena);
338            let mut tree = AccountTree::new(&arena);
339
340            let foo = accounts.ensure("Assets:Foo");
341            tree.construct(&accounts);
342            let assets = tree.parent(AccountAggregate::Account(foo)).unwrap();
343
344            tree.construct(&accounts);
345            let assets2 = tree.parent(AccountAggregate::Account(foo)).unwrap();
346
347            assert_eq!(assets, assets2);
348        }
349
350        #[test]
351        fn empty_tree() {
352            let arena = Bump::new();
353            let accounts = AccountStore::new(&arena);
354            let mut tree = AccountTree::new(&arena);
355
356            tree.construct(&accounts);
357
358            assert!(tree.children(AccountTreeKey::Root).unwrap().is_empty());
359        }
360
361        #[test]
362        fn tree_structure() {
363            let arena = Bump::new();
364            let mut accounts = AccountStore::new(&arena);
365
366            let foo = accounts.ensure("Assets:Banks:Foo");
367            let bar = accounts.ensure("Assets:Banks:Bar");
368            let food = accounts.ensure("Expenses:Food");
369            let food_special = accounts.ensure("Expenses:Food:Special");
370
371            let mut tree = AccountTree::new(&arena);
372            tree.construct(&accounts);
373
374            let assets = tree.resolve_ancestor("Assets").unwrap();
375            let banks = tree.resolve_ancestor("Assets:Banks").unwrap();
376            let expenses = tree.resolve_ancestor("Expenses").unwrap();
377            let parent_of: [(AccountTreeKey, AccountAggregate); _] = [
378                (AccountTreeKey::Root, assets.into()),
379                (AccountTreeKey::Root, expenses.into()),
380                (assets.into(), banks.into()),
381                (banks.into(), foo.into()),
382                (banks.into(), bar.into()),
383                (expenses.into(), food.into()),
384                (food.into(), food_special.into()),
385            ];
386            for (expected, child) in parent_of {
387                assert_eq!(expected, tree.parent(child).unwrap());
388            }
389            let children_of: [(&[AccountAggregate], AccountTreeKey); _] = [
390                (
391                    &[
392                        AccountAggregate::Ancestor(assets),
393                        AccountAggregate::Ancestor(expenses),
394                    ],
395                    AccountTreeKey::Root,
396                ),
397                (&[AccountAggregate::Ancestor(banks)], assets.into()),
398                (
399                    // alphabetically sorted.
400                    &[
401                        AccountAggregate::Account(bar),
402                        AccountAggregate::Account(foo),
403                    ],
404                    banks.into(),
405                ),
406                (&[AccountAggregate::Account(food)], expenses.into()),
407                (&[AccountAggregate::Account(food_special)], food.into()),
408            ];
409            for (expected, parent) in children_of {
410                assert_eq!(expected, tree.children(parent).unwrap());
411            }
412        }
413    }
414}