use std::{ cmp::{max, Ordering}, collections::{HashMap, HashSet}, }; use ratatui::{style::{Color, Style}, text::{Line, Span, Text}}; use crate::{core::{combine_amounts, Account, Amount, Ledger}, output::cli::tui_to_ansi::text_to_ansi}; #[derive(Debug)] struct BalanceTree { name: String, children: Vec, amounts: Option>, } #[derive(Debug)] struct BalanceTreeStr { name: String, children: Vec, amounts: Option>, } struct AccountInfo<'a> { account_path: Vec<&'a str>, amounts: Vec, } fn construct_tree(ledger: &Ledger, account_balances: &HashMap>) -> BalanceTree { let account_info: Vec = account_balances .iter() .map(|(&account_id, amounts)| { let account = ledger.get_account(account_id).unwrap(); AccountInfo { account_path: account.split_name_groups(), amounts: amounts.clone(), } }) .collect(); let root_children = construct_tree_children(account_info.iter().collect(), 0); BalanceTree { name: "(Total)".into(), children: root_children, amounts: None, } } // fn construct_subtree(accounts: Vec, level: usize, parent: &BalanceTree) { // for account in accounts // .iter() // .filter(|v| v.account_path[level] == parent.name) // {} // } fn construct_tree_children(accounts: Vec<&AccountInfo>, level: usize) -> Vec { let mut sub_trees = Vec::new(); let names = HashSet::<&str>::from_iter(accounts.iter().map(|v| v.account_path[level])); for name in names { let leaf_account = accounts .iter() .find(|v| v.account_path.len() - 1 == level && v.account_path[level] == name); let sub_accounts: Vec<&AccountInfo> = accounts .clone() .into_iter() .filter(|v| v.account_path.len() - 1 != level && v.account_path[level] == name) .collect(); let mut sub_tree = BalanceTree { name: name.into(), children: Vec::new(), amounts: None, }; if sub_accounts.len() != 0 { sub_tree.children = construct_tree_children(sub_accounts, level + 1); if let Some(leaf_account) = leaf_account { sub_tree.children.push(BalanceTree { name: "...".into(), children: Vec::new(), amounts: Some(leaf_account.amounts.clone()), }); } } else if let Some(leaf_account) = leaf_account { sub_tree.amounts = Some(leaf_account.amounts.clone()); } else { panic!("No account"); } sub_trees.push(sub_tree) } sub_trees } fn set_tree_totals(tree: &mut BalanceTree) { if tree.amounts.is_some() { return; } for child in &mut tree.children { set_tree_totals(child); } let total_amounts = combine_amounts( tree.children .iter() .map(|v| v.amounts.clone().unwrap().into_iter()) .flatten(), ); tree.amounts = Some(total_amounts); } const STYLE_LINE: Style = Style::new().fg(Color::LightBlue); const STYLE_AMOUNT_LINE: Style = Style::new().fg(Color::DarkGray); const STYLE_ACCOUNT: Style = Style::new().fg(Color::LightBlue); fn tree_to_text(tree: &BalanceTreeStr, ledger: &Ledger, base_amount_pos: usize, max_decimal_pos: usize) -> Text<'static> { let mut text = Text::default(); // let tree_amounts = tree.amounts.as_ref().unwrap().iter().filter(|v| !ledger.round_amount(v).value.is_zero()); let tree_amounts = tree.amounts.as_ref().unwrap().iter(); let tree_amounts_count = tree_amounts.clone().count(); for (i, (amount, decimal_pos)) in tree_amounts.enumerate() { let mut line = Line::default(); let amount_padding_count = max_decimal_pos - decimal_pos; if i == 0 { let amount_pos = base_amount_pos - tree.name.chars().count(); line.push_span(Span::styled(format!("{} ", tree.name), STYLE_ACCOUNT)); let mut line_str = "─".repeat(amount_pos); if tree_amounts_count > 1 { line_str += "┬" } else { line_str += "─" } line_str += &"─".repeat(amount_padding_count); line.push_span(Span::styled(line_str, STYLE_AMOUNT_LINE)); } else { let line_str = if tree.children.len() > 0 { " │" } else { " " }; line.push_span(Span::styled(line_str, STYLE_LINE)); let mut line_str = String::new(); line_str += &" ".repeat(base_amount_pos - 2); if i == tree_amounts_count - 1 { line_str += " └"; line_str += &"─".repeat(amount_padding_count); } else { line_str += " │"; line_str += &" ".repeat(amount_padding_count); } line.push_span(Span::styled(line_str, STYLE_AMOUNT_LINE)); } line.push_span(Span::raw(format!(" {}", amount))); text.push_line(line); } let mut children: Vec<&BalanceTreeStr> = tree.children.iter().collect(); let children_len = children.len(); children.sort_by(|a, b| a.name.cmp(&b.name)); for (i_c, child) in children.into_iter().enumerate() { let mut child_text = tree_to_text(&child, ledger, base_amount_pos - 4, max_decimal_pos); for (i, line) in child_text.lines.into_iter().enumerate() { let mut whole_line = Line::default(); if i_c == children_len - 1 { if i == 0 { whole_line.push_span(Span::styled(" └─ ", STYLE_LINE)); } else { whole_line.push_span(Span::styled(" ", STYLE_LINE)); } } else { if i == 0 { whole_line.push_span(Span::styled(" ├─ ", STYLE_LINE)); } else { whole_line.push_span(Span::styled(" │ ", STYLE_LINE)); } } whole_line.extend(line); text.push_line(whole_line); } } text } // fn print_tree(tree: &BalanceTree, ledger: &Ledger, level: usize, amount_pos: usize) { // let relative_amount_pos = amount_pos - (level*2 + tree.name.len()); // let main_line = format!("{}{} {}", " ".repeat(level), tree.name, "─".repeat(relative_amount_pos)); // let tree_amounts = tree.amounts.as_ref().unwrap().iter().filter(|v| !ledger.round_amount(v).value.is_zero()); // let tree_amounts_count = tree_amounts.clone().count(); // for (i, amount) in tree_amounts.enumerate() { // let mut line = String::new(); // if i == 0 { // line += &main_line; // if tree_amounts_count > 1 { // line += "┬" // } else { // line += "─" // } // } else { // line += &" ".repeat(amount_pos); // if i == tree_amounts_count - 1 { // line += " └" // } else { // line += " │" // } // } // line += &format!(" {}", ledger.format_amount(amount)); // println!("{}", line); // } // // println!("{}{} {} {:?}", " ".repeat(level), tree.name, "-".repeat(relative_amount_pos), tree.amounts); // let mut children: Vec<&BalanceTree> = tree.children.iter().collect(); // children.sort_by(|a, b| a.name.cmp(&b.name)); // for child in children { // print_tree(&child, ledger, level + 1, amount_pos); // } // } fn balance_tree_to_str_tree(tree: BalanceTree, ledger: &Ledger) -> BalanceTreeStr { let amounts = tree.amounts.map(|v| v.iter().map(|a| ledger.format_amount(a).unwrap()).collect()); let children = tree.children.into_iter().map(|c| balance_tree_to_str_tree(c, ledger)).collect(); BalanceTreeStr{amounts, name: tree.name, children} } fn calculate_max_account_len(tree: &BalanceTreeStr, indent_amount: usize, indent_level: usize) -> usize { let current_len = tree.name.len() + indent_amount * indent_level; let mut max_length = current_len; for child in &tree.children { let child_max_len = calculate_max_account_len(child, indent_amount, indent_level+1); max_length = max(max_length, child_max_len); } max_length } fn calculate_max_decimal_pos(tree: &BalanceTreeStr) -> usize { let mut max_decimal_pos = 0; if let Some(amounts) = &tree.amounts { for (_, decimal_pos) in amounts { max_decimal_pos = max(max_decimal_pos, *decimal_pos); } }; for child in &tree.children { let child_max = calculate_max_decimal_pos(child); max_decimal_pos = max(max_decimal_pos, child_max); } max_decimal_pos } pub fn format_balance(ledger: &Ledger, account_balances: &HashMap>) -> String { let mut output = String::new(); let mut tree = construct_tree(ledger, account_balances); set_tree_totals(&mut tree); let str_tree = balance_tree_to_str_tree(tree, &ledger); let max_account_len = calculate_max_account_len(&str_tree, 4, 0); let max_decimal_pos = calculate_max_decimal_pos(&str_tree); let text = tree_to_text(&str_tree, &ledger, max_account_len, max_decimal_pos); println!("{}", text_to_ansi(&text)); // println!("{}", max_account_len); // print_tree(&tree, &ledger, 0, max_account_len + 5); // println!("{:?}", tree); // let base_account_info: Vec = account_balances // .iter() // .map(|(&account_id, amounts)| { // let account = ledger.get_account(account_id).unwrap(); // AccountInfo { // account_path: account.split_name_groups(), // is_group: false, // amounts: amounts.clone(), // } // }) // .collect(); // let mut account_info: Vec = Vec::new(); // for base_account in &base_account_info {} // let mut group_account_info = Vec::new(); // for account in &account_info { // let mut account_path = account.account_path.clone(); // loop { // account_path.pop(); // if account_path.len() == 0 { // break; // } // if !group_account_info // .iter() // .any(|v: &AccountInfo| v.account_path == account_path) // { // let amount_totals = combine_amounts( // account_info // .iter() // .filter(|v| v.account_path.starts_with(&account_path)) // .map(|v| v.amounts.iter()) // .flatten() // .map(|v| *v), // ); // group_account_info.push(AccountInfo { // account_path: account_path.clone(), // is_group: true, // amounts: amount_totals, // }) // } // } // } // account_info.extend(group_account_info.into_iter()); // sort_account_info(&mut account_info); // for i in account_info { // println!("{} {:?}", i.account_path.join(":"), i.amounts); // } output } // fn sort_account_info(account_info: &mut Vec) { // account_info.sort_by(|a, b| { // let ordering = a // .account_path // .iter() // .zip(b.account_path.iter()) // .map(|(&a, &b)| a.cmp(b)) // .find(|val| *val != Ordering::Equal); // ordering.unwrap_or_else(|| a.account_path.len().cmp(&b.account_path.len())) // }); // } // ///////////// // // Tests // // ///////////// // #[cfg(test)] // mod tests { // use super::*; // #[test] // fn test_sort_account_info() { // let mut account_info = vec![ // AccountInfo { // account_path: vec!["a2", "b1"], // is_group: false, // amounts: vec![], // }, // AccountInfo { // account_path: vec!["a1", "b2"], // is_group: false, // amounts: vec![], // }, // AccountInfo { // account_path: vec!["a1", "b1"], // is_group: false, // amounts: vec![], // }, // AccountInfo { // account_path: vec!["a1"], // is_group: false, // amounts: vec![], // }, // ]; // sort_account_info(&mut account_info); // println!("{:?}", account_info[0].account_path); // println!("{:?}", account_info[1].account_path); // println!("{:?}", account_info[2].account_path); // println!("{:?}", account_info[3].account_path); // } // } /* Assets ├─ Bank │ ├─ Checking │ └─ Savings └─ Broker ├─ Brokerage └─ Retirement Liabilities ├─ Credit Card 1 └─ Credit Card 2 Income ├─ Salary ├─ Capital Gains │ ├─ Long │ └─ Short └─ Other Expenses ├─ Taxes ├─ Utilities │ ├─ Power │ └─ Cellular ├─ Rent ├─ Food │ ├─ Fast Food │ ├─ Restaurants │ └─ Groceries └─ Entertainment Equity ├─ Opening Balance └─ Unrealized Gain */ /* Total ($0.00) ├─ Assets ($10,000.00) │ ├─ Bank ($2,000.00) │ │ ├─ Checking $700.00 │ │ └─ Savings $1,300.00 │ └─ Broker ($8,000.00) │ ├─ Brokerage $3,000.00 │ └─ Retirement $5,000.00 ├─ Liabilities (-$1,123.11) │ ├─ Credit Card 1 -$478.20 │ └─ Credit Card 2 -$644.91 ├─ Income (-$10,182.00) │ ├─ Salary │ ├─ Capital Gains │ │ ├─ Long │ │ └─ Short │ └─ Other ├─ Expenses │ ├─ Taxes │ ├─ Utilities │ │ ├─ Power │ │ └─ Cellular │ ├─ Rent │ ├─ Food │ │ ├─ Fast Food │ │ ├─ Restaurants │ │ └─ Groceries │ ├─ Entertainment │ └─ ... └─ Equity ├─ Opening Balance └─ Unrealized Gain */ /* Total ...................... ($0.00) ├─ Assets ............. ($10,000.00) │ ├─ Bank ............. ($2,000.00) │ │ ├─ Checking ........ $700.00 │ │ └─ Savings ....... $1,300.00 │ └─ Broker ........... ($8,000.00) │ ├─ Brokerage ..... $3,000.00 │ └─ Retirement .... $5,000.00 ├─ Liabilities ........ (-$1,123.11) │ ├─ Credit Card 1 ...... -$478.20 │ └─ Credit Card 2 ...... -$644.91 ├─ Income ............ (-$10,182.00) │ ├─ Salary │ ├─ Capital Gains │ │ ├─ Long │ │ └─ Short │ └─ Other ├─ Expenses │ ├─ Taxes │ ├─ Utilities │ │ ├─ Power │ │ └─ Cellular │ ├─ Rent │ ├─ Food │ │ ├─ Fast Food │ │ ├─ Restaurants │ │ └─ Groceries │ └─ Entertainment └─ Equity ├─ Opening Balance └─ Unrealized Gain */ /* Total........................($0.00) ├─ Assets...............($10,000.00) │ ├─ Bank...............($2,000.00) │ │ ├─ Checking..........$700.00 │ │ └─ Savings.........$1,300.00 │ └─ Broker.............($8,000.00) │ ├─ Brokerage.......$3,000.00 │ └─ Retirement......$5,000.00 ├─ Liabilities..........(-$1,123.11) │ ├─ Credit Card 1........-$478.20 │ └─ Credit Card 2........-$644.91 ├─ Income..............(-$10,182.00) │ ├─ Salary │ ├─ Capital Gains │ │ ├─ Long │ │ └─ Short │ └─ Other ├─ Expenses │ ├─ Taxes │ ├─ Utilities │ │ ├─ Power │ │ └─ Cellular │ ├─ Rent │ ├─ Food │ │ ├─ Fast Food │ │ ├─ Restaurants │ │ └─ Groceries │ └─ Entertainment └─ Equity ├─ Opening Balance └─ Unrealized Gain */ /* Total ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌ ($0.00) ├─ Assets ╌╌╌╌╌╌╌╌╌╌╌╌╌ ($10,000.00) │ ├─ Bank ╌╌╌╌╌╌╌╌╌╌╌╌╌ ($2,000.00) │ │ ├─ Checking ╌╌╌╌╌╌╌╌ $700.00 │ │ └─ Savings ╌╌╌╌╌╌╌ $1,300.00 │ └─ Broker ╌╌╌╌╌╌╌╌╌╌╌ ($8,000.00) │ ├─ Brokerage ╌╌╌╌╌ $3,000.00 │ └─ Retirement ╌╌╌╌ $5,000.00 ├─ Liabilities ╌╌╌╌╌╌╌╌ (-$1,123.11) │ ├─ Credit Card 1 ╌╌╌╌╌╌ -$478.20 │ └─ Credit Card 2 ╌╌╌╌╌╌ -$644.91 ├─ Income ╌╌╌╌╌╌╌╌╌╌╌╌ (-$10,182.00) │ ├─ Salary │ ├─ Capital Gains │ │ ├─ Long │ │ └─ Short │ └─ Other ├─ Expenses │ ├─ Taxes │ ├─ Utilities │ │ ├─ Power │ │ └─ Cellular │ ├─ Rent │ ├─ Food │ │ ├─ Fast Food │ │ ├─ Restaurants │ │ └─ Groceries │ └─ Entertainment └─ Equity ├─ Opening Balance └─ Unrealized Gain */ /* Total ├─ Assets │ ├─ Bank │ │ ├─ Checking $700.00 │ │ └─ Savings $1,300.00 │ └─ Broker │ ├─ Brokerage $3,000.00 │ └─ Retirement $5,000.00 ├─ Liabilities │ ├─ Credit Card 1 -$478.20 │ └─ Credit Card 2 -$644.91 ├─ Income │ ├─ Salary │ ├─ Capital Gains │ │ ├─ Long │ │ └─ Short │ └─ Other ├─ Expenses │ ├─ Taxes │ ├─ Utilities │ │ ├─ Power │ │ └─ Cellular │ ├─ Rent │ ├─ Food │ │ ├─ Fast Food │ │ ├─ Restaurants │ │ └─ Groceries │ └─ Entertainment └─ Equity ├─ Opening Balance └─ Unrealized Gain */ /* Assets ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┬ 1.00000000 ETH │ 248.6010 FSCSX ┆ 40.979 SmallCapCore ┆ 159.122 FSMEX ┆ $25063.25 ┆ 47.185 FBGRX ┆ 260.9010 FSCHX ┆ 367.5600 FSRPX ┆ 51.910 FNCMX ┆ 100.151 VanguardTargetSixty └ 2.933 VanguardIndexPlus BofA ----------------- $16922.08 Checking ----------- $5165.22 Savings ------------ $11756.86 Coinbase ------------- 1.00000000 ETH ESPP ----------------- $1071.00 */