ledger_rs_lib/
account.rs

1/*!
2 * Account definition and operations
3 */
4
5use std::{collections::HashMap, ptr::addr_of, vec};
6
7use crate::{balance::Balance, post::Post};
8
9#[derive(Debug, PartialEq)]
10pub struct Account {
11    pub(crate) parent: *const Account,
12    pub name: String,
13    // note
14    // depth
15    pub accounts: HashMap<String, Account>,
16    pub posts: Vec<*const Post>,
17    // deferred posts
18    // value_expr
19    fullname: String,
20}
21
22impl Account {
23    pub fn new(name: &str) -> Self {
24        Self {
25            parent: std::ptr::null(),
26            name: name.to_owned(),
27            // note
28            accounts: HashMap::new(),
29            posts: vec![],
30            fullname: "".to_string(),
31            // post_indices: vec![],
32        }
33    }
34
35    /// called from find_or_create.
36    fn create_account(&self, first: &str) -> &Account {
37        let mut new_account = Account::new(first);
38        new_account.set_parent(self);
39
40        let self_mut = self.get_account_mut(self as *const Account as *mut Account);
41
42        self_mut.accounts.insert(first.into(), new_account);
43
44        let Some(new_ref) = self.accounts.get(first)
45            else {panic!("should not happen")};
46
47        log::debug!("The new account {:?} reference: {:p}", new_ref.name, new_ref);
48        new_ref
49    }
50
51    pub fn fullname(&self) -> &str {
52        // skip the master account.
53        if self.parent.is_null() {
54            return "";
55        }
56
57        if !self.fullname.is_empty() {
58            return &self.fullname;
59        }
60
61        let mut fullname = self.name.to_owned();
62        let mut first = self;
63
64        while !first.parent.is_null() {
65            // If there is a parent account, use it.
66            first = self.get_account_mut(first.parent);
67
68            if !first.name.is_empty() {
69                fullname = format!("{}:{}", &first.name, fullname);
70            }
71        }
72
73        self.set_fullname(fullname);
74
75        &self.fullname
76    }
77
78    fn set_fullname(&self, fullname: String) {
79        // alchemy?
80        // let ptr = self as *const Account;
81        let ptr = addr_of!(*self);
82        let subject = self.get_account_mut(ptr);
83
84        subject.fullname = fullname;
85    }
86
87    /// Finds account by full name.
88    /// i.e. "Assets:Cash"
89    pub fn find_account(&self, name: &str) -> Option<&Account> {
90        if let Some(ptr) = self.find_or_create(name, false) {
91            let acct = Account::from_ptr(ptr);
92            return Some(acct);
93        } else {
94            return None;
95        }
96    }
97
98    /// The variant with all the parameters.
99    /// account_t * find_account(const string& name, bool auto_create = true);
100    pub fn find_or_create(&self, name: &str, auto_create: bool) -> Option<*const Account> {
101        // search for direct hit.
102        if let Some(found) = self.accounts.get(name) {
103            return Some(found);
104        }
105
106        // otherwise search for name parts in between the `:`
107
108        let mut account: *const Account;
109        let first: &str;
110        let rest: &str;
111        if let Some(separator_index) = name.find(':') {
112            // Contains separators
113            first = &name[..separator_index];
114            rest = &name[separator_index + 1..];
115        } else {
116            // take all
117            first = name;
118            rest = "";
119        }
120
121        if let Some(account_opt) = self.accounts.get(first) {
122            // keep this value
123            account = account_opt;
124        } else {
125            if !auto_create {
126                return None;
127            }
128
129            account = self.create_account(first);
130        }
131
132        // Search recursively.
133        if !rest.is_empty() {
134            let acct = self.get_account_mut(account);
135            account = acct.find_or_create(rest, auto_create).unwrap();
136        }
137
138        Some(account)
139    }
140
141    pub fn from_ptr<'a>(acct_ptr: *const Account) -> &'a Account {
142        unsafe { &*acct_ptr }
143    }
144
145    pub fn get_account_mut(&self, acct_ptr: *const Account) -> &mut Account {
146        let mut_ptr = acct_ptr as *mut Account;
147        unsafe { &mut *mut_ptr }
148    }
149
150    pub fn flatten_account_tree(&self) -> Vec<&Account> {
151        let mut list: Vec<&Account> = vec![];
152        self.flatten(&mut list);
153        list
154    }
155
156    /// Returns the amount of this account only.
157    pub fn amount(&self) -> Balance {
158        let mut bal = Balance::new();
159
160        for post_ptr in &self.posts {
161            let post: &Post;
162            unsafe {
163                post = &**post_ptr;
164            }
165            if let Some(amt) = post.amount {
166                bal.add(&amt);
167            }
168        }
169
170        bal
171    }
172
173    fn flatten<'a>(&'a self, nodes: &mut Vec<&'a Account>) {
174        // Push the current node to the Vec
175        nodes.push(self);
176        // 
177        let mut children: Vec<&Account> = self.accounts.values().into_iter().collect();
178        children.sort_unstable_by_key(|acc| &acc.name);
179        // If the node has children, recursively call flatten on them
180        for child in children {
181            child.flatten(nodes);
182        }
183    }
184
185    pub(crate) fn set_parent(&mut self, parent: &Account) {
186        // Confirms the pointers are the same:
187        // assert_eq!(parent as *const Account, addr_of!(*parent));
188        // self.parent = parent as *const Account;
189
190        self.parent = addr_of!(*parent);
191        
192        // log::debug!("Setting the {:?} parent to {:?}, {:p}", self.name, parent.name, self.parent);
193    }
194
195    /// Returns the balance of this account and all sub-accounts.
196    pub fn total(&self) -> Balance {
197        let mut total = Balance::new();
198
199        // Sort the accounts by name
200        let mut acct_names: Vec<_> = self.accounts.keys().collect();
201        acct_names.sort();
202
203        // iterate through children and get their totals
204        for acct_name in acct_names {
205            let subacct = self.accounts.get(acct_name).unwrap();
206            // let subacct = journal.get_account(*index);
207            let subtotal = subacct.total();
208
209            total += subtotal;
210        }
211
212        // Add the balance of this account
213        total += self.amount();
214
215        total
216    }
217}
218
219#[cfg(test)]
220mod tests {
221    use std::{io::Cursor, ptr::addr_of};
222
223    use crate::{amount::Quantity, journal::Journal, parse_file, parse_text, parser};
224
225    use super::Account;
226
227    #[test]
228    fn test_flatten() {
229        let mut j = Journal::new();
230        let _acct = j.register_account("Assets:Cash");
231        let mut nodes: Vec<&Account> = vec![];
232
233        j.master.flatten(&mut nodes);
234
235        assert_eq!(3, nodes.len());
236    }
237
238    #[test]
239    fn test_account_iterator() {
240        let mut j = Journal::new();
241        let mut counter: u8 = 0;
242
243        let _acct = j.register_account("Assets:Cash");
244        for _a in j.master.flatten_account_tree() {
245            //println!("sub-account: {:?}", a);
246            counter += 1;
247        }
248
249        assert_eq!(3, counter);
250    }
251
252    /// Search for an account by the full account name.
253    #[test]
254    fn test_fullname() {
255        let input = r#"2023-05-01 Test
256    Expenses:Food  10 EUR
257    Assets:Cash
258"#;
259        let mut journal = Journal::new();
260        parser::read_into_journal(Cursor::new(input), &mut journal);
261
262        let account = journal.find_account("Expenses:Food").unwrap();
263
264        let actual = account.fullname();
265
266        assert_eq!(5, journal.master.flatten_account_tree().len());
267        assert_eq!("Food", account.name);
268        assert_eq!("Expenses:Food", actual);
269    }
270
271    /// Test parsing of Amount
272    #[test]
273    fn test_amount_parsing() {
274        let mut journal = Journal::new();
275
276        // act
277        parse_file("tests/basic.ledger", &mut journal);
278
279        let ptr = journal.find_account("Assets:Cash").unwrap();
280        let account = journal.get_account(ptr);
281
282        let actual = account.amount();
283
284        // assert
285        assert!(!actual.amounts.is_empty());
286        assert_eq!(Quantity::from(-20), actual.amounts[0].quantity);
287        let commodity = actual.amounts[0].get_commodity().unwrap();
288        assert_eq!("EUR", commodity.symbol);
289    }
290
291    /// Test calculation of the account totals.
292    #[test_log::test]
293    fn test_total() {
294        let mut journal = Journal::new();
295        parse_file("tests/two-xact-sub-acct.ledger", &mut journal);
296        let ptr = journal.find_account("Assets").unwrap();
297        let assets = journal.get_account(ptr);
298
299        // act
300        let actual = assets.total();
301
302        // assert
303        assert_eq!(1, actual.amounts.len());
304        log::debug!(
305            "Amount 1: {:?}, {:?}",
306            actual.amounts[0],
307            actual.amounts[0].get_commodity()
308        );
309
310        assert_eq!(actual.amounts[0].quantity, (-30).into());
311        assert_eq!(actual.amounts[0].get_commodity().unwrap().symbol, "EUR");
312    }
313
314    #[test]
315    fn test_parent_pointer() {
316        let input = r#"2023-05-05 Payee
317    Expenses  20
318    Assets
319"#;
320        let mut journal = Journal::new();
321
322        // act
323        parse_text(input, &mut journal);
324
325        let ptr = journal.master.find_account("Assets").unwrap();
326        let assets = journal.get_account(ptr);
327
328        assert_eq!(&*journal.master as *const Account, assets.parent);
329    }
330
331    #[test]
332    fn test_parent_pointer_after_fullname() {
333        let input = r#"2023-05-05 Payee
334    Expenses  20
335    Assets
336"#;
337        let mut journal = Journal::new();
338        parse_text(input, &mut journal);
339
340        // test parent
341        let ptr = journal.master.find_account("Assets").unwrap();
342        let assets = journal.get_account(ptr);
343
344        assert_eq!(&*journal.master as *const Account, assets.parent);
345
346        // test fullname
347        let assets_fullname = journal.master.accounts.get("Assets").unwrap().fullname();
348        let expenses_fullname = journal.master.accounts.get("Expenses").unwrap().fullname();
349
350        assert_eq!("Assets", assets_fullname);
351        assert_eq!("Expenses", expenses_fullname);
352
353        // test parent
354        let ptr = journal.master.find_account("Assets").unwrap();
355        let assets = journal.get_account(ptr);
356
357        assert_eq!(&*journal.master as *const Account, assets.parent);
358    }
359
360    #[test_log::test]
361    fn test_parent_pointers() {
362        let input = r#"2023-05-05 Payee
363        Expenses:Groceries  20
364        Assets:Cash
365    "#;
366        let mut journal = Journal::new();
367
368        parse_text(input, &mut journal);
369
370        // expenses
371        let expenses = journal.master.find_account("Expenses").unwrap();
372        assert_eq!(&*journal.master as *const Account, expenses.parent);
373
374        // groceries
375        let groceries = expenses.find_account("Groceries").unwrap();
376        assert_eq!(expenses as *const Account, groceries.parent);
377
378        // assets
379        let assets = journal.master.find_account("Assets").unwrap();
380        assert_eq!(&*journal.master as *const Account, assets.parent);
381
382        // confirm that addr_of! and `as *const` are the same.
383        assert_eq!(assets as *const Account, addr_of!(*assets));
384
385        // cash
386        let cash = assets.find_account("Cash").unwrap();
387        assert_eq!(assets as *const Account, cash.parent);
388
389    }
390}