1use 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 pub accounts: HashMap<String, Account>,
16 pub posts: Vec<*const Post>,
17 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 accounts: HashMap::new(),
29 posts: vec![],
30 fullname: "".to_string(),
31 }
33 }
34
35 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 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 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 let ptr = addr_of!(*self);
82 let subject = self.get_account_mut(ptr);
83
84 subject.fullname = fullname;
85 }
86
87 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 pub fn find_or_create(&self, name: &str, auto_create: bool) -> Option<*const Account> {
101 if let Some(found) = self.accounts.get(name) {
103 return Some(found);
104 }
105
106 let mut account: *const Account;
109 let first: &str;
110 let rest: &str;
111 if let Some(separator_index) = name.find(':') {
112 first = &name[..separator_index];
114 rest = &name[separator_index + 1..];
115 } else {
116 first = name;
118 rest = "";
119 }
120
121 if let Some(account_opt) = self.accounts.get(first) {
122 account = account_opt;
124 } else {
125 if !auto_create {
126 return None;
127 }
128
129 account = self.create_account(first);
130 }
131
132 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 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 nodes.push(self);
176 let mut children: Vec<&Account> = self.accounts.values().into_iter().collect();
178 children.sort_unstable_by_key(|acc| &acc.name);
179 for child in children {
181 child.flatten(nodes);
182 }
183 }
184
185 pub(crate) fn set_parent(&mut self, parent: &Account) {
186 self.parent = addr_of!(*parent);
191
192 }
194
195 pub fn total(&self) -> Balance {
197 let mut total = Balance::new();
198
199 let mut acct_names: Vec<_> = self.accounts.keys().collect();
201 acct_names.sort();
202
203 for acct_name in acct_names {
205 let subacct = self.accounts.get(acct_name).unwrap();
206 let subtotal = subacct.total();
208
209 total += subtotal;
210 }
211
212 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 counter += 1;
247 }
248
249 assert_eq!(3, counter);
250 }
251
252 #[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]
273 fn test_amount_parsing() {
274 let mut journal = Journal::new();
275
276 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!(!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_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 let actual = assets.total();
301
302 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 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 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 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 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 let expenses = journal.master.find_account("Expenses").unwrap();
372 assert_eq!(&*journal.master as *const Account, expenses.parent);
373
374 let groceries = expenses.find_account("Groceries").unwrap();
376 assert_eq!(expenses as *const Account, groceries.parent);
377
378 let assets = journal.master.find_account("Assets").unwrap();
380 assert_eq!(&*journal.master as *const Account, assets.parent);
381
382 assert_eq!(assets as *const Account, addr_of!(*assets));
384
385 let cash = assets.find_account("Cash").unwrap();
387 assert_eq!(assets as *const Account, cash.parent);
388
389 }
390}