Sfoglia il codice sorgente

Simple monthly balance sheet implemented.

Kestrel 1 settimana fa
parent
commit
4388050015
7 ha cambiato i file con 188 aggiunte e 12 eliminazioni
  1. 1 0
      .gitignore
  2. 1 1
      Cargo.toml
  3. 15 4
      src/check.rs
  4. 30 1
      src/cmd.rs
  5. 136 0
      src/cmd/balance.rs
  6. 0 1
      src/cmd/txnmatch.rs
  7. 5 5
      src/show.rs

+ 1 - 0
.gitignore

@@ -1,2 +1,3 @@
 /target
 .*.sw?
+/Cargo.lock

+ 1 - 1
Cargo.toml

@@ -11,7 +11,7 @@ stringstore = { version = "0.1.5", features = ["serde"] }
 rust_decimal = { version = "1.37" }
 itertools = { version = "0.14" }
 nanoid = { version = "0.4.0" }
-chrono = { version = "0.4.4", default-features = false, features = ["alloc"] }
+chrono = { version = "0.4.4", default-features = false, features = ["alloc", "now"] }
 
 # i/o dependencies
 serde = { version = "1.0", features = ["derive"] }

+ 15 - 4
src/check.rs

@@ -153,7 +153,9 @@ fn check_balance_group<'a>(
             .unwrap()
             .union(first.change_for(account).unwrap().source.unwrap()),
     )
-    .with_message(format!("No valid ordering of transactions to satisfy balance assertions in {account}"))
+    .with_message(format!(
+        "No valid ordering of transactions to satisfy balance assertions in {account}"
+    ))
     .with_labels(group.iter().map(|txn| {
         let txn = txn.borrow();
         let span = txn
@@ -162,7 +164,9 @@ fn check_balance_group<'a>(
             .union(txn.change_for(account).unwrap().source.unwrap());
         ariadne::Label::new(span).with_message("transaction in group")
     }))
-    .with_labels(last_bal.iter().map(|bal| ariadne::Label::new(bal.span()).with_message(format!("last calculated balance was {bal}"))))
+    .with_labels(last_bal.iter().map(|bal| {
+        ariadne::Label::new(bal.span()).with_message(format!("last calculated balance was {bal}"))
+    }))
     .finish();
 
     Err(DataError::Report(Box::new(report)))
@@ -177,7 +181,9 @@ fn check_balances(root: &data::Hoard) -> Result<(), DataError> {
         };
 
         let mut running_balance = BTreeMap::<data::UnitName, Spanned<data::Decimal>>::new();
-        let date_groups = ledger.iter().chunk_by(|tx| tx.borrow().datestamp_for(account));
+        let date_groups = ledger
+            .iter()
+            .chunk_by(|tx| tx.borrow().datestamp_for(account));
 
         for group in date_groups.into_iter() {
             check_balance_group(account, &mut running_balance, group.1)?;
@@ -195,7 +201,12 @@ fn check_merge_groups(root: &data::Hoard) -> Result<(), DataError> {
             span = span.union(ds.span());
         }
 
-        (Spanned::new((), span), c.account.0, c.amount.0, c.datestamp.map(|d| d.0).unwrap_or(txn.datestamp))
+        (
+            Spanned::new((), span),
+            c.account.0,
+            c.amount.0,
+            c.datestamp.map(|d| d.0).unwrap_or(txn.datestamp),
+        )
     };
 
     for account in root.account_names() {

+ 30 - 1
src/cmd.rs

@@ -5,6 +5,7 @@ use itertools::Itertools;
 // use crate::{check::CheckLevel, data, import::import_from, io, show};
 use crate::prelude::*;
 
+mod balance;
 mod comb;
 mod infer;
 mod txnmatch;
@@ -45,7 +46,12 @@ pub enum Command {
     Ledger {
         account: String,
         #[clap(long, short)]
-        raw: bool
+        raw: bool,
+    },
+    Balance {
+        period: balance::BalancePeriod,
+        since: Option<String>,
+        upto: Option<String>,
     },
     Reformat {
         #[clap(long)]
@@ -136,6 +142,29 @@ impl Command {
                     log::error!("account not found!");
                 }
             }
+            Self::Balance {
+                period,
+                since,
+                upto,
+            } => {
+                let since = match since {
+                    Some(v) => data::Datestamp::parse_from_str(&v, "%Y-%m-%d").map_err(|e| {
+                        anyhow::anyhow!(format!("could not parse date from '{v}': {e}"))
+                    })?,
+
+                    None => todo!(),
+                };
+
+                let upto = match upto {
+                    Some(v) => data::Datestamp::parse_from_str(&v, "%Y-%m-%d").map_err(|e| {
+                        anyhow::anyhow!(format!("could not parse date from '{v}': {e}"))
+                    })?,
+                    None => chrono::prelude::Utc::now().date_naive(),
+                };
+                let data = load_data(&mut fsdata, inv, check::CheckLevel::WellFormed)?;
+
+                balance::show_balance(&data, *period, since, upto)?;
+            }
             Self::Reformat { skip_ids } => {
                 let mut data = load_data(&mut fsdata, inv, check::CheckLevel::WellFormed)?;
 

+ 136 - 0
src/cmd/balance.rs

@@ -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(())
+}

+ 0 - 1
src/cmd/txnmatch.rs

@@ -23,7 +23,6 @@ fn do_merge(
     let (change1, mut rest1) = txn1.split_changes_mut(common).unwrap();
     let (change2, mut rest2) = txn2.split_changes_mut(common).unwrap();
 
-
     let other1 = rest1.next().unwrap();
     let other2 = rest2.next().unwrap();
 

+ 5 - 5
src/show.rs

@@ -2,26 +2,26 @@ use crate::data::{AccountName, Hoard, Transaction, TransactionRef};
 
 #[derive(Clone, Copy, Default)]
 #[allow(unused)]
-enum Align {
+pub enum Align {
     #[default]
     Left,
     Centre,
     Right,
 }
 
-#[derive(Default)]
-struct Column {
+#[derive(Clone, Default)]
+pub struct Column {
     pub align: Align,
     pub left_border: bool,
     pub right_border: bool,
 }
 
-enum Row {
+pub enum Row {
     Line,
     Data(Vec<String>),
 }
 
-fn show_table(cols: Vec<Column>, rows: impl Iterator<Item = Row>) {
+pub fn show_table(cols: Vec<Column>, rows: impl Iterator<Item = Row>) {
     let mut min_widths = vec![0usize; cols.len()];
 
     let table_data = rows.collect::<Vec<_>>();