1use std::io::Read;
8
9use crate::{
10 account::Account,
11 commodity::Commodity,
12 parser,
13 pool::{CommodityIndex, CommodityPool},
14 post::Post,
15 xact::Xact,
16};
17
18pub struct Journal {
21 pub master: Box<Account>,
22
23 pub commodity_pool: CommodityPool,
24 pub xacts: Vec<Xact>,
25}
26
27impl Journal {
28 pub fn new() -> Self {
29 Self {
30 master: Box::new(Account::new("")),
31
32 commodity_pool: CommodityPool::new(),
33 xacts: vec![],
34 }
36 }
37
38 pub fn add_xact(&mut self, xact: Xact) -> &Xact {
39 self.xacts.push(xact);
40 self.xacts.last().unwrap()
42 }
43
44 pub fn all_posts(&self) -> Vec<&Post> {
45 self.xacts.iter().flat_map(|x| x.posts.iter()).collect()
46 }
47
48 pub fn get_account(&self, acct_ptr: *const Account) -> &Account {
49 unsafe { &*acct_ptr }
50 }
51
52 pub fn get_account_mut(&self, acct_ptr: *const Account) -> &mut Account {
53 unsafe {
54 let mut_ptr = acct_ptr as *mut Account;
55 &mut *mut_ptr
56 }
57 }
58
59 pub fn get_commodity(&self, index: CommodityIndex) -> &Commodity {
60 self.commodity_pool.get_by_index(index)
61 }
62
63 pub fn register_account(&mut self, name: &str) -> Option<*const Account> {
69 if name.is_empty() {
70 panic!("Invalid account name {:?}", name);
71 }
72
73 let master_account: &mut Account = &mut self.master;
77
78 let Some(account_ptr) = master_account.find_or_create(name, true)
82 else { return None };
83
84 let account = self.get_account(account_ptr);
87 Some(account)
88 }
89
90 pub fn find_account(&self, name: &str) -> Option<&Account> {
91 self.master.find_account(name)
92 }
93
94 pub fn read<T: Read>(&mut self, source: T) -> usize {
100 parser::read_into_journal(source, self);
102
103 self.xacts.len()
104 }
105}
106
107#[cfg(test)]
108mod tests {
109 use core::panic;
110 use std::{io::Cursor, ptr::addr_of};
111
112 use super::Journal;
113 use crate::{account::Account, parse_file};
114
115 #[test]
116 fn test_add_account() {
117 const ACCT_NAME: &str = "Assets";
118 let mut journal = Journal::new();
119 let ptr = journal.register_account(ACCT_NAME).unwrap();
120 let actual = journal.get_account(ptr);
121
122 assert_eq!(ACCT_NAME, actual.name);
125 }
126
127 #[test]
128 fn test_add_account_to_master() {
129 let mut journal = Journal::new();
130 const NAME: &str = "Assets";
131
132 let Some(ptr) = journal.register_account(NAME) else {panic!("unexpected")};
133 let actual = journal.get_account(ptr);
134
135 assert_eq!(&*journal.master as *const Account, actual.parent);
136 }
137
138 #[test]
139 fn test_find_account() {
140 let mut journal = Journal::new();
141 parse_file("tests/basic.ledger", &mut journal);
142
143 let actual = journal.find_account("Assets:Cash");
144
145 assert!(actual.is_some());
146 }
147
148 #[test]
149 fn test_register_account() {
150 const NAME: &str = "Assets:Investments:Broker";
151 let mut j = Journal::new();
152
153 let new_acct = j.register_account(NAME).unwrap();
155 let actual = j.get_account_mut(new_acct);
156
157 let journal = &j;
158
159 assert_eq!(4, journal.master.flatten_account_tree().len());
161 assert_eq!(NAME, actual.fullname());
162
163 let master = &journal.master;
165 assert_eq!("", master.name);
166
167 let assets = master.find_account("Assets").unwrap();
168 assert_eq!("Assets", assets.name);
170 assert_eq!(&*journal.master as *const Account, assets.parent);
171
172 let inv = assets.find_account("Investments").unwrap();
173 assert_eq!("Investments", inv.name);
175 assert_eq!(addr_of!(*assets), inv.parent);
176
177 let broker = inv.find_account("Broker").unwrap();
178 assert_eq!("Broker", broker.name);
180 assert_eq!(inv as *const Account, broker.parent);
181 }
182
183 #[test]
185 fn test_master_gets_created() {
186 let j = Journal::new();
187
188 let actual = j.master;
189
190 assert_eq!("", actual.name);
191 }
192
193 #[test]
194 fn test_read() {
195 let src = r#"2023-05-01 Test
196 Expenses:Food 20 EUR
197 Assets:Cash
198"#;
199 let mut j = Journal::new();
200
201 let num_xact = j.read(Cursor::new(src));
203
204 assert_eq!(1, num_xact);
206 }
207}