621 lines
19 KiB
Rust
621 lines
19 KiB
Rust
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<BalanceTree>,
|
|
amounts: Option<Vec<Amount>>,
|
|
}
|
|
|
|
#[derive(Debug)]
|
|
struct BalanceTreeStr {
|
|
name: String,
|
|
children: Vec<BalanceTreeStr>,
|
|
amounts: Option<Vec<(String, usize)>>,
|
|
}
|
|
|
|
struct AccountInfo<'a> {
|
|
account_path: Vec<&'a str>,
|
|
amounts: Vec<Amount>,
|
|
}
|
|
|
|
fn construct_tree(ledger: &Ledger, account_balances: &HashMap<u32, Vec<Amount>>) -> BalanceTree {
|
|
let account_info: Vec<AccountInfo> = 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<AccountInfo>, 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<BalanceTree> {
|
|
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<u32, Vec<Amount>>) -> 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<AccountInfo> = 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<AccountInfo> = 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<AccountInfo>) {
|
|
// 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
|
|
|
|
*/ |