Selaa lähdekoodia

Renamed 'balance' to 'change', minor cleanups, started table infra.

Kestrel 9 tuntia sitten
vanhempi
commit
5d81a901db
9 muutettua tiedostoa jossa 205 lisäystä ja 66 poistoa
  1. 30 0
      hoard.vim
  2. 3 3
      src/check.rs
  3. 25 10
      src/data.rs
  4. 16 15
      src/data/ledger.rs
  5. 14 9
      src/data/ledger/parse.rs
  6. 17 11
      src/data/ledger/print.rs
  7. 6 17
      src/main.rs
  8. 94 0
      src/show.rs
  9. 0 1
      testdata/ledger

+ 30 - 0
hoard.vim

@@ -0,0 +1,30 @@
+if exists('b:current_syntax') | finish | endif
+
+syntax match hoardTxHeader "\v^\s*\d\d\d\d-\d\d-\d\d\s*:.*$" contains=hoardTxDate,hoardTxTitle
+syntax match hoardTxDate "\v^\s*[^:]+(:.*$)@=" contained
+syntax match hoardTxTitle "\v(^\s*[^:]+:\s+)@<=.{-}$" contained
+
+syntax match hoardAnnotation "\v\[.{-}\]"
+
+syntax match hoardChange "\v^\s*-\s*[^:]*\s*:\s*[-+]?\d+(\.\d*)(\s+\S+)?$" contains=hoardChangeAccount,hoardChangeDeposit,hoardChangeWithdrawal,hoardChangeUnit
+syntax match hoardChangeAccount "\v(^\s*-\s*)@<=.*(\s*:)@=" contained
+syntax match hoardChangeDeposit "\v\+?\s*\d+(\.?\d*)?" contained
+syntax match hoardChangeWithdrawal "\v-\s*\d+(\.?\d*)?" contained
+syntax match hoardChangeUnit "\v\s+\S+$" contained
+
+syntax match hoardComment "^\s*#.*$"
+
+highlight def link hoardTxDate Keyword
+highlight def link hoardTxTitle Underlined
+highlight def link hoardAnnotation String
+highlight def link hoardChangeAccount Function
+highlight def link hoardChangeDeposit Added
+highlight def link hoardChangeWithdrawal Removed
+highlight def link hoardChangeUnit Type
+highlight def link hoardComment Comment
+
+" autocmd BufEnter,BufCreate * if &filetype ==# 'hoard' | syntax off | syntax on | echom "turned syntax on" | endif
+" autocmd BufCreate *.hoard setf hoard
+" autocmd FileType hoard setf hoard | echom "hoard filetype detected"
+
+let b:current_syntax = 'hoard'

+ 3 - 3
src/check.rs

@@ -10,14 +10,14 @@ fn check_equal_sum(root: &Root) -> Result<(), DataError> {
         let Some(mono) = tx.mono_unit() else { continue };
 
         let net = tx
-            .balances
+            .changes
             .iter()
             .try_fold(Decimal::ZERO, |acc, b| acc.checked_add(*b.amount));
         if net != Some(Decimal::ZERO) {
             let report = ariadne::Report::build(ariadne::ReportKind::Error, tx.span()).with_labels(
-                tx.balances.iter().map(|v| {
+                tx.changes.iter().map(|v| {
                     let span = v.amount.span().union(v.unit.span());
-                    ariadne::Label::new(span).with_message("balance here")
+                    ariadne::Label::new(span).with_message("change here")
                 }),
             );
 

+ 25 - 10
src/data.rs

@@ -1,5 +1,3 @@
-// #![allow(unused)]
-
 use std::collections::HashMap;
 
 use ariadne::Cache;
@@ -89,12 +87,6 @@ impl From<std::io::Error> for DataError {
     }
 }
 
-/*impl From<ariadne::Report<'static, (SourceFile, std::ops::Range<usize>)>> for DataError {
-    fn from(value: ariadne::Report<'static, (SourceFile, std::ops::Range<usize>)>) -> Self {
-        Self::Report(value.into())
-    }
-}*/
-
 impl From<ariadne::Report<'static, Span>> for DataError {
     fn from(value: ariadne::Report<'static, Span>) -> Self {
         Self::Report(value.into())
@@ -211,6 +203,25 @@ impl<T: std::fmt::Display> std::fmt::Display for Spanned<T> {
     }
 }
 
+#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
+pub struct Datestamp {
+    pub year: u16,
+    pub month: u8,
+    pub day: u8,
+}
+
+impl std::fmt::Display for Datestamp {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        write!(f, "{:04}-{:02}-{:02}", self.year, self.month, self.day)
+    }
+}
+
+impl std::fmt::Debug for Datestamp {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        write!(f, "Datestamp ({self})")
+    }
+}
+
 #[derive(Debug)]
 pub struct Root {
     path: std::path::PathBuf,
@@ -320,13 +331,17 @@ impl Root {
     fn preprocess_ledger_data(&mut self) {
         for entry in &self.ledger_data {
             let ledger::LedgerEntry::Transaction(tx) = &entry else { continue };
-            for bal in &tx.balances {
+            for bal in &tx.changes {
                 self.account_ledger_data
                     .entry(*bal.account)
                     .or_default()
                     .push(tx.clone());
             }
         }
+
+        for txns in self.account_ledger_data.values_mut() {
+            txns.sort_by_key(|txn| txn.datestamp);
+        }
     }
 
     pub fn all_ledger_data(&self) -> &[ledger::LedgerEntry] {
@@ -353,7 +368,7 @@ impl Root {
         let mut running = HashMap::<UnitName, Decimal>::new();
 
         for le in self.ledger_data_for(aname)? {
-            for b in &le.balances {
+            for b in &le.changes {
                 if *b.account != aname {
                     continue;
                 }

+ 16 - 15
src/data/ledger.rs

@@ -1,6 +1,6 @@
 use itertools::Itertools;
 
-use super::{AccountName, Decimal, Spanned, UnitName};
+use super::{AccountName, Datestamp, Decimal, Spanned, UnitName};
 
 mod parse;
 pub use parse::parse_ledger;
@@ -9,53 +9,54 @@ mod print;
 pub use print::print_ledger;
 
 #[derive(Clone, Debug, PartialEq, PartialOrd, Ord, Eq)]
-pub struct Balance {
+pub struct Change {
     pub account: Spanned<AccountName>,
     pub amount: Spanned<Decimal>,
+    pub balance: Option<Spanned<Decimal>>,
     pub unit: Spanned<UnitName>,
 }
 
 #[derive(Clone, Debug, PartialEq, PartialOrd, Eq, Ord)]
 pub struct Transaction {
-    pub datestamp: (u16, u8, u8),
+    pub datestamp: Datestamp,
     pub title: Option<String>,
     pub annotations: Vec<String>,
-    pub balances: Vec<Spanned<Balance>>,
+    pub changes: Vec<Spanned<Change>>,
 }
 
 impl Transaction {
     pub fn modifies(&self, account: AccountName) -> bool {
-        self.balances.iter().any(|b| b.account.as_ref() == &account)
+        self.changes.iter().any(|b| b.account.as_ref() == &account)
     }
 
-    pub fn balance_for(&self, account: AccountName) -> Option<&Spanned<Balance>> {
-        self.balances
+    pub fn change_for(&self, account: AccountName) -> Option<&Spanned<Change>> {
+        self.changes
             .iter()
             .find(|b| b.account.as_ref() == &account)
     }
 
-    pub fn split_balances(
+    pub fn split_changes(
         &self,
         account: AccountName,
-    ) -> Option<(&Spanned<Balance>, impl Iterator<Item = &Spanned<Balance>>)> {
+    ) -> Option<(&Spanned<Change>, impl Iterator<Item = &Spanned<Change>>)> {
         let index = self
-            .balances
+            .changes
             .iter()
             .position(|b| b.account.as_ref() == &account)?;
         Some((
-            &self.balances[index],
-            self.balances[0..index]
+            &self.changes[index],
+            self.changes[0..index]
                 .iter()
-                .chain(self.balances[index + 1..].iter()),
+                .chain(self.changes[index + 1..].iter()),
         ))
     }
 
     pub fn is_mono_unit(&self) -> bool {
-        self.balances.iter().unique_by(|b| *b.unit).count() == 1
+        self.changes.iter().unique_by(|b| *b.unit).count() == 1
     }
 
     pub fn mono_unit(&self) -> Option<UnitName> {
-        let mut it = self.balances.iter().unique_by(|b| *b.unit);
+        let mut it = self.changes.iter().unique_by(|b| *b.unit);
         let uniq = it.next()?;
         it.next().is_none().then_some(*uniq.unit)
     }

+ 14 - 9
src/data/ledger/parse.rs

@@ -1,8 +1,8 @@
 use crate::data::{
-    AccountName, DataError, Decimal, SourceFile, Span, Spanned, UnitName, spec::SpecRoot,
+    AccountName, DataError, Datestamp, Decimal, SourceFile, Span, Spanned, UnitName, spec::SpecRoot,
 };
 
-use super::{Balance, Transaction, LedgerEntry};
+use super::{Change, Transaction, LedgerEntry};
 
 use chumsky::{prelude::*, text::inline_whitespace};
 
@@ -23,7 +23,7 @@ fn ledger_parser<'a>() -> impl Parser<
         .map(|v: &str| v.parse::<usize>().unwrap());
 
     let datestamp = group((int, just('-').ignored(), int, just('-').ignored(), int))
-        .map(|(y, _, m, _, d)| (y as u16, m as u8, d as u8));
+        .map(|(y, _, m, _, d)| Datestamp { year: y as u16, month: m as u8, day: d as u8 });
 
     let mark = |m| just(m).padded_by(inline_whitespace());
 
@@ -47,11 +47,15 @@ fn ledger_parser<'a>() -> impl Parser<
             ))
         });
 
-    let balance = group((
+    let change = group((
         mark('-'),
         none_of(": \n\t").repeated().to_slice().map_with(|v, e| Spanned(AccountName::new(v), e.span())),
         mark(':'),
         decimal,
+        choice((
+            mark('=').ignore_then(decimal).map(Some),
+            empty().map(|_| None)
+        )),
         choice((
             inline_whitespace()
                 .at_least(1)
@@ -62,7 +66,7 @@ fn ledger_parser<'a>() -> impl Parser<
         ))
         .then_ignore(chumsky::text::newline()),
     ))
-    .try_map_with(|(_, acc, _, amount, unit, ), e| {
+    .try_map_with(|(_, acc, _, amount, balance, unit, ), e| {
         let span = e.span();
         let spec: &mut chumsky::extra::SimpleState<&SpecRoot> = e.state();
 
@@ -89,9 +93,10 @@ fn ledger_parser<'a>() -> impl Parser<
             return Err(chumsky::error::Rich::custom(unit.span(), format!("no such unit '{unit}' found")))
         }
 
-        Ok(Spanned::new(Balance {
+        Ok(Spanned::new(Change {
             account: acc,
             amount,
+            balance,
             unit,
         }, span))
     });
@@ -117,17 +122,17 @@ fn ledger_parser<'a>() -> impl Parser<
                 .then_ignore(chumsky::text::newline()),
             empty().map(|_| vec![]),
         )),
-        balance.repeated().at_least(1).collect(),
+        change.repeated().at_least(1).collect(),
         chumsky::text::whitespace(),
     ))
     .map_with(
-        |(_, datestamp, _, _, title, _, annotations, balances, _), e| {
+        |(_, datestamp, _, _, title, _, annotations, changes, _), e| {
             LedgerEntry::Transaction(Spanned::new(
                 Transaction {
                     datestamp,
                     title: (!title.is_empty()).then_some(title),
                     annotations,
-                    balances,
+                    changes,
                 },
                 e.span(),
             ))

+ 17 - 11
src/data/ledger/print.rs

@@ -8,10 +8,8 @@ use super::{LedgerEntry, Transaction};
 
 fn print_transaction(root: &Root, tx: &Transaction, padding: usize) {
     println!(
-        "{}-{:02}-{:02}: {}",
-        tx.datestamp.0,
-        tx.datestamp.1,
-        tx.datestamp.2,
+        "{}: {}",
+        tx.datestamp,
         tx.title.as_deref().unwrap_or("")
     );
 
@@ -22,22 +20,30 @@ fn print_transaction(root: &Root, tx: &Transaction, padding: usize) {
         );
     }
 
-    let force_show = tx.balances.iter().unique_by(|b| *b.account).count() > 1;
+    let force_show = tx.changes.iter().unique_by(|b| *b.account).count() > 1;
 
-    for bal in &tx.balances {
-        let spacer = if bal.amount.is_sign_positive() {
+    for change in &tx.changes {
+        let spacer = if change.amount.is_sign_positive() {
             " "
         } else {
             ""
         };
-        if Some(bal.unit.as_ref()) == root.account_spec(*bal.account).unwrap().unit.as_ref()
+
+        let balance = if let Some(bal) = change.balance {
+            format!(" = {bal}")
+        } else {
+            String::new()
+        };
+
+
+        if Some(change.unit.as_ref()) == root.account_spec(*change.account).unwrap().unit.as_ref()
             && !force_show
         {
-            println!(" - {:padding$}: {spacer}{}", bal.account, bal.amount,);
+            println!(" - {:padding$}: {spacer}{}{balance}", change.account, change.amount,);
         } else {
             println!(
-                " - {:padding$}: {spacer}{} {}",
-                bal.account, bal.amount, bal.unit,
+                " - {:padding$}: {spacer}{}{balance} {}",
+                change.account, change.amount, change.unit,
             );
         }
     }

+ 6 - 17
src/main.rs

@@ -2,6 +2,7 @@ use itertools::Itertools;
 
 mod check;
 mod data;
+mod show;
 
 #[derive(clap::Parser)]
 struct Invocation {
@@ -34,7 +35,7 @@ fn load_data(
         Err(data::DataError::Report(report)) => {
             report.eprint(fsdata)?;
             Err(anyhow::anyhow!("Error reported"))
-        }
+        },
         Err(data::DataError::Validation(verr)) => Err(anyhow::anyhow!("Validation error: {verr}")),
     }
 }
@@ -88,25 +89,13 @@ impl Command {
                 let data = load_data(&mut fsdata, inv)?;
 
                 let aname = data::AccountName::new(account.as_str());
+
+                let tt = show::TransactionTable::default();
                 if let Some(ld) = data.ledger_data_for(aname) {
-                    for le in ld {
-                        let Some((acc_bal, other_bal)) = le.split_balances(aname) else {
-                            continue;
-                        };
-                        log::info!(
-                            "{:?}: {} {:?}",
-                            le.datestamp,
-                            acc_bal.amount,
-                            other_bal.map(|b| b.account).collect::<Vec<_>>()
-                        );
-                    }
+                    tt.show(aname, ld.iter().map(data::Spanned::as_ref));
                 } else {
-                    log::info!("account not found. data: {data:?}");
+                    log::error!("account not found!");
                 }
-
-                // let data = data::Root::load(&mut fsdata, &inv.file);
-
-                // data.account(account)?;
             }
             Self::Reformat => {
                 let data = load_data(&mut fsdata, inv)?;

+ 94 - 0
src/show.rs

@@ -0,0 +1,94 @@
+use std::fmt::Display;
+
+use console::Style;
+
+use crate::data::{AccountName, ledger::{Transaction,Change}};
+
+#[derive(Clone, Copy, Default)]
+enum Align {
+    #[default]
+    Left,
+    Centre,
+    Right,
+}
+
+#[derive(Default)]
+struct Column {
+    pub align: Align,
+    pub left_border: bool,
+    pub right_border: bool,
+    // pub contents: Box<dyn Iterator<Item = dyn Display>>,
+}
+
+enum Row {
+    Line,
+    Data(Vec<String>),
+}
+
+fn show_table<'d>(cols: Vec<Column>, rows: impl Iterator<Item = Row>) {
+    let mut min_widths = vec![0usize; cols.len()];
+
+    let table_data = rows.collect::<Vec<_>>();
+    for row in &table_data {
+        let Row::Data(row) = &row else { continue };
+        for (idx,rc) in row.iter().enumerate() {
+            min_widths[idx] = min_widths[idx].max(console::measure_text_width(rc.as_str()));
+        }
+    }
+
+    for row in table_data {
+        match row {
+            Row::Line => for (idx, col) in min_widths.iter().enumerate() {
+                if cols[idx].left_border {
+                    print!("+");
+                }
+                print!("{:-width$}", "", width = col + 1);
+                if cols[idx].right_border {
+                    print!("+");
+                }
+            },
+            Row::Data(row) => for (idx, col) in row.into_iter().enumerate() {
+                if cols[idx].left_border {
+                    print!("|");
+                }
+                match cols[idx].align {
+                    Align::Left => print!("{col:<width$}", width = min_widths[idx] + 1),
+                    Align::Centre => print!("{col:^width$}", width = min_widths[idx] + 1),
+                    Align::Right => print!("{col:>width$}", width = min_widths[idx] + 1),
+                }
+                if cols[idx].right_border {
+                    print!("|");
+                }
+            },
+        }
+        println!();
+    }
+}
+
+#[derive(Clone, Copy, Default)]
+pub struct TransactionTable {
+}
+
+impl TransactionTable {
+    pub fn show<'d>(self, account: AccountName, txns: impl Iterator<Item = &'d Transaction>) {
+        show_table(
+            vec![
+                // datestamp
+                Column { align: Align::Left, left_border: false, right_border: true },
+                // Memo
+                Column { align: Align::Left, ..Default::default() },
+                // Amount
+                Column { align: Align::Right, ..Default::default() },
+            ],
+            txns
+                .filter_map(|txn| txn.change_for(account).map(|chg| (txn, chg)))
+                .map(|(txn, chg)| {
+                    Row::Data(vec![
+                        txn.datestamp.to_string(),
+                        txn.title.clone().unwrap_or_else(String::new),
+                        format!("{}", chg.amount)
+                    ])
+                })
+        )
+    }
+}

+ 0 - 1
testdata/ledger

@@ -1,4 +1,3 @@
-==== file testdata/./ledger ====
 2001-01-05: initial balance
  - initial    : -400.00 CAD
  - chequing   :  400.00 CAD