ledger_rs_lib/
report.rs

1/*!
2 * Reports module containing the report definitions
3 */
4
5use crate::{balance::Balance, journal::Journal, account::Account};
6
7/// Accounts report. Command: `accounts`.
8///
9/// void report_t::posts_report(post_handler_ptr handler)
10/// in output.cc
11/// report_accounts
12pub fn report_accounts(journal: &Journal) -> Vec<String> {
13    let accts = journal.master.flatten_account_tree();
14    accts
15        .iter()
16        .map(|account| account.name.to_string())
17        .collect()
18}
19
20fn report_commodities() {
21    todo!()
22}
23
24fn report_payees() {
25    todo!()
26}
27
28/// Balance report. Invoked with 'b' command.
29/// Or accounts_report in ledger.
30/// Vec<String>
31pub fn balance_report(journal: &Journal) -> Vec<String> {
32    log::debug!("Running the balance report");
33
34    // let balances = get_account_balances(&journal);
35    // Now that the account totals are implemented, simply walk the master account.
36    // Format output
37    // format_balance_report(balances, &journal)
38
39    get_children_lines(&journal.master, journal)
40}
41
42/// Quick test of the account traversal for assembling the totals.
43fn get_children_lines<'a>(account: &'a Account, journal: &'a Journal) -> Vec<String> {
44    let mut result = vec![];
45
46    let mut balance_line = String::new();
47    let total = account.total();
48    for amount in total.amounts {
49        balance_line += amount.quantity.to_string().as_str();
50        if amount.get_commodity().is_some() {
51            if let Some(c) = amount.get_commodity() {
52                balance_line += " ";
53                balance_line += c.symbol.as_str();
54            }
55        }
56    }
57    result.push(format!("Account {} has balance {}", account.fullname(), balance_line));
58
59    // Sort child account names alphabetically. Mainly for consistent output.
60    let mut acct_names: Vec<_> = account.accounts.keys().collect();
61    acct_names.sort();
62
63    // children amounts
64    for acct_name in acct_names {
65        let acct = account.accounts.get(acct_name).unwrap();
66        result.extend(get_children_lines(acct, journal));
67    }
68
69    result
70}
71
72/// To be deprecated, unless significantly faster than the account traversing.
73/// Calculates account balances.
74/// returns (account_name, balance)
75///
76fn get_account_balances(journal: &Journal) -> Vec<(&str, Balance)> {
77    let mut balances = vec![];
78
79    // calculate balances
80    for acc in journal.master.flatten_account_tree() {
81        // get posts for this account.
82        let filtered_posts = journal
83            .xacts.iter().flat_map(|x| x.posts.iter())
84            .filter(|post| post.account == acc);
85
86        // TODO: separate balance per currency
87
88        let mut balance: Balance = Balance::new();
89        for post in filtered_posts {
90            balance.add(&post.amount.as_ref().unwrap());
91        }
92
93        balances.push((acc.fullname(), balance));
94    }
95    balances
96}
97
98/// To be deprecated.
99fn format_balance_report(mut balances: Vec<(String, Balance)>, journal: &Journal) -> Vec<String> {
100    // sort accounts
101    balances.sort_by(|(acc1, _bal1), (acc2, _bal2)| acc1.cmp(&acc2));
102
103    let mut output = vec![];
104    for (account, balance) in balances {
105        let mut bal_text: String = String::new();
106        for amount in &balance.amounts {
107            //
108            let symbol = match amount.get_commodity() {
109                Some(c) => c.symbol.as_str(),
110                None => "",
111            };
112
113            if !bal_text.is_empty() {
114                bal_text += ", ";
115            }
116            
117            bal_text += amount.quantity.to_string().as_str();
118
119            if !symbol.is_empty() {
120                bal_text += " ";
121                bal_text += symbol;
122            }
123        }
124        let line = format!("Account {} has balance {}", account, bal_text);
125        output.push(line);
126    }
127    output
128}
129
130/// Calculates market price, `-X`
131/// 
132/// report.cc
133/// value_t report_t::fn_market(call_scope_t& args)
134/// 
135fn market(target_commodity: &str) {
136
137}
138
139#[cfg(test)]
140mod tests {
141    use std::io::Cursor;
142
143    use super::balance_report;
144    use crate::{journal::Journal, parser};
145
146    #[test]
147    fn test_balance_report_one_xact() {
148        let src = r#";
1492023-05-05 Payee
150    Expenses  25 EUR
151    Assets
152
153"#;
154        let mut journal = Journal::new();
155        parser::read_into_journal(Cursor::new(src), &mut journal);
156
157        let actual: Vec<String> = balance_report(&journal);
158
159        assert!(!actual.is_empty());
160        assert_eq!(3, actual.len());
161        assert_eq!("Account  has balance 0 EUR", actual[0]);
162        assert_eq!("Account Assets has balance -25 EUR", actual[1]);
163        assert_eq!("Account Expenses has balance 25 EUR", actual[2]);
164        // assert_eq!("Account  has balance ", actual[0]);
165        // assert_eq!("Account Assets has balance -25 EUR", actual[1]);
166        // assert_eq!("Account Expenses has balance 25 EUR", actual[2]);
167    }
168
169    #[test]
170    fn test_bal_report_two_commodities() {
171        let src = r#";
1722023-05-05 Payee
173    Expenses  25 EUR
174    Assets
175
1762023-05-05 Payee 2
177    Expenses  13 BAM
178    Assets
179"#;
180        let source = Cursor::new(src);
181        let mut journal = Journal::new();
182        parser::read_into_journal(source, &mut journal);
183
184        // Act
185        let actual: Vec<String> = balance_report(&journal);
186
187        // Assert
188        assert!(!actual.is_empty());
189        assert_eq!(3, actual.len());
190        assert_eq!("Account  has balance 0 EUR0 BAM", actual[0]);
191        assert_eq!("Account Assets has balance -25 EUR-13 BAM", actual[1]);
192        assert_eq!("Account Expenses has balance 25 EUR13 BAM", actual[2]);
193    }
194
195    #[test]
196    fn test_bal_multiple_commodities_in_the_same_xact() {
197        let src = r#";
1982023-05-05 Payee
199    Assets:Cash EUR  -25 EUR
200    Assets:Cash USD   30 USD
201"#;
202        let source = Cursor::new(src);
203        let mut journal = Journal::new();
204        parser::read_into_journal(source, &mut journal);
205
206        // Act
207        let actual: Vec<String> = balance_report(&journal);
208
209        // Assert
210        assert!(!actual.is_empty());
211        assert_eq!(4, actual.len());
212        assert_eq!("Account  has balance -25 EUR30 USD", actual[0]);
213        assert_eq!("Account Assets has balance -25 EUR30 USD", actual[1]);
214        assert_eq!("Account Assets:Cash EUR has balance -25 EUR", actual[2]);
215        assert_eq!("Account Assets:Cash USD has balance 30 USD", actual[3]);
216    }
217
218    // TODO: #[test]
219    fn test_bal_market_prices() {
220        // add a price,
221        // then run the balance report
222        // in one currency (-X EUR)
223        
224    }
225}