|
@@ -0,0 +1,136 @@
|
|
|
+use std::collections::BTreeMap;
|
|
|
+use std::ops::Range;
|
|
|
+
|
|
|
+use chrono::Datelike;
|
|
|
+
|
|
|
+use itertools::Itertools;
|
|
|
+
|
|
|
+use crate::prelude::*;
|
|
|
+
|
|
|
+#[derive(Clone, Copy, Debug, clap::ValueEnum)]
|
|
|
+pub enum BalancePeriod {
|
|
|
+ Weekly,
|
|
|
+ Monthly,
|
|
|
+ Yearly,
|
|
|
+}
|
|
|
+
|
|
|
+#[derive(Clone)]
|
|
|
+struct PeriodIterator {
|
|
|
+ range: Range<data::Datestamp>,
|
|
|
+ period: BalancePeriod,
|
|
|
+}
|
|
|
+
|
|
|
+impl Iterator for PeriodIterator {
|
|
|
+ type Item = Range<data::Datestamp>;
|
|
|
+
|
|
|
+ fn next(&mut self) -> Option<Self::Item> {
|
|
|
+ if self.range.is_empty() {
|
|
|
+ return None;
|
|
|
+ }
|
|
|
+ match self.period {
|
|
|
+ BalancePeriod::Monthly => {
|
|
|
+ let start = self.range.start.with_day0(0).unwrap();
|
|
|
+
|
|
|
+ let end = if self.range.start.month() == 11 {
|
|
|
+ start
|
|
|
+ .with_month(0)
|
|
|
+ .unwrap()
|
|
|
+ .with_year(start.year() + 1)
|
|
|
+ .unwrap()
|
|
|
+ } else {
|
|
|
+ start.with_month(start.month() + 1).unwrap()
|
|
|
+ };
|
|
|
+
|
|
|
+ self.range.start = end;
|
|
|
+
|
|
|
+ Some(start..end)
|
|
|
+ }
|
|
|
+ _ => todo!(),
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+fn iterate_periods(
|
|
|
+ period: BalancePeriod,
|
|
|
+ since: data::Datestamp,
|
|
|
+ upto: data::Datestamp,
|
|
|
+) -> impl Iterator<Item = std::ops::Range<data::Datestamp>> {
|
|
|
+ PeriodIterator {
|
|
|
+ period,
|
|
|
+ range: since..upto,
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+pub fn show_balance(
|
|
|
+ data: &data::Hoard,
|
|
|
+ period: BalancePeriod,
|
|
|
+ since: data::Datestamp,
|
|
|
+ upto: data::Datestamp,
|
|
|
+) -> anyhow::Result<()> {
|
|
|
+ let periods = iterate_periods(period, since, upto).collect::<Vec<_>>();
|
|
|
+ // let ncols = periods.clone().iter().count();
|
|
|
+
|
|
|
+ let mut cols = vec![
|
|
|
+ show::Column {
|
|
|
+ align: show::Align::Right,
|
|
|
+ left_border: false,
|
|
|
+ right_border: false,
|
|
|
+ };
|
|
|
+ periods.len()
|
|
|
+ ];
|
|
|
+
|
|
|
+ cols.insert(
|
|
|
+ 0,
|
|
|
+ show::Column {
|
|
|
+ align: show::Align::Left,
|
|
|
+ left_border: false,
|
|
|
+ right_border: true,
|
|
|
+ },
|
|
|
+ );
|
|
|
+
|
|
|
+ let mut rows = vec![];
|
|
|
+
|
|
|
+ rows.push(show::Row::Data(
|
|
|
+ [String::new()]
|
|
|
+ .into_iter()
|
|
|
+ .chain(periods.iter().map(|dr| dr.start.to_string()))
|
|
|
+ .collect(),
|
|
|
+ ));
|
|
|
+ rows.push(show::Row::Line);
|
|
|
+
|
|
|
+ for account in data.account_names().sorted_by_key(|v| v.as_str()) {
|
|
|
+ // only show monounit accounts
|
|
|
+ /*let Some(unit) = data.account_spec(account).unwrap().unit else {
|
|
|
+ continue
|
|
|
+ };*/
|
|
|
+
|
|
|
+ let mut row = vec![];
|
|
|
+ row.push(account.to_string());
|
|
|
+
|
|
|
+ let Some(txns) = data.ledger_data_for(account) else {
|
|
|
+ continue;
|
|
|
+ };
|
|
|
+
|
|
|
+ for period in &periods {
|
|
|
+ let mut rbal = BTreeMap::<data::UnitName, data::Decimal>::new();
|
|
|
+ for txnr in txns {
|
|
|
+ let txn = txnr.borrow();
|
|
|
+ if !period.contains(&txn.datestamp_for(account)) {
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+
|
|
|
+ let change = txn.change_for(account).unwrap();
|
|
|
+ let v = rbal.entry(*change.unit).or_default();
|
|
|
+ *v += *change.amount;
|
|
|
+ }
|
|
|
+
|
|
|
+ row.push(rbal.into_iter().map(|(_k, v)| v.to_string()).join(" "));
|
|
|
+ }
|
|
|
+
|
|
|
+ rows.push(show::Row::Data(row));
|
|
|
+ }
|
|
|
+
|
|
|
+ show::show_table(cols, rows.into_iter());
|
|
|
+
|
|
|
+ Ok(())
|
|
|
+}
|