ledger_rs_lib/
journal.rs

1/*!
2 * Journal
3 * The main model object. The journal files are parsed into the Journal structure.
4 * Provides methods for fetching and iterating over the contained elements
5 * (transactions, posts, accounts...).
6 */
7use 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
18// pub type XactIndex = usize;
19
20pub 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            // sources: Vec<fileinfo?>
35        }
36    }
37
38    pub fn add_xact(&mut self, xact: Xact) -> &Xact {
39        self.xacts.push(xact);
40        //self.xacts.len() - 1
41        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    /// Called to create an account during Post parsing.
64    ///
65    /// account_t * journal_t::register_account(const string& name, post_t * post,
66    ///                                         account_t * master_account)
67    ///
68    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        // todo: expand_aliases
74        // account_t * result = expand_aliases(name);
75
76        let master_account: &mut Account = &mut self.master;
77
78        // Create the account object and associate it with the journal; this
79        // is registering the account.
80
81        let Some(account_ptr) = master_account.find_or_create(name, true)
82        else { return None };
83
84        // todo: add any validity checks here.
85
86        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    /// Read journal source (file or string).
95    ///
96    /// std::size_t journal_t::read(parse_context_stack_t& context)
97    ///
98    /// returns number of transactions parsed
99    pub fn read<T: Read>(&mut self, source: T) -> usize {
100        // read_textual
101        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        // There is master account
123        // assert_eq!(1, i);
124        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        // act
154        let new_acct = j.register_account(NAME).unwrap();
155        let actual = j.get_account_mut(new_acct);
156        
157        let journal = &j;
158
159        // Asserts
160        assert_eq!(4, journal.master.flatten_account_tree().len());
161        assert_eq!(NAME, actual.fullname());
162
163        // tree structure
164        let master = &journal.master;
165        assert_eq!("", master.name);
166
167        let assets = master.find_account("Assets").unwrap();
168        // let assets = journal.get_account(assets_ptr);
169        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        // let inv = journal.get_account_mut(inv_ix);
174        assert_eq!("Investments", inv.name);
175        assert_eq!(addr_of!(*assets), inv.parent);
176
177        let broker = inv.find_account("Broker").unwrap();
178        // let broker = journal.get_account(broker_ix);
179        assert_eq!("Broker", broker.name);
180        assert_eq!(inv as *const Account, broker.parent);
181    }
182
183    /// The master account needs to be created in the Journal automatically.
184    #[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        // Act
202        let num_xact = j.read(Cursor::new(src));
203
204        // Assert
205        assert_eq!(1, num_xact);
206    }
207}