Browse Source

Update vim syntax highlighting, minor tweaks.

Kestrel 2 weeks ago
parent
commit
ff5605172c
11 changed files with 184 additions and 70 deletions
  1. 1 0
      Cargo.toml
  2. 5 7
      hoard.vim
  3. 92 2
      src/check.rs
  4. 3 1
      src/data.rs
  5. 2 4
      src/data/ledger.rs
  6. 14 4
      src/data/ledger/parse.rs
  7. 5 7
      src/data/ledger/print.rs
  8. 1 1
      src/data/spec.rs
  9. 1 1
      src/main.rs
  10. 59 31
      src/show.rs
  11. 1 12
      testdata/ledger

+ 1 - 0
Cargo.toml

@@ -21,3 +21,4 @@ ariadne = { version = "0.5" }
 pretty_env_logger = { version = "0.5.0" }
 clap = { version = "4.5", features = ["derive", "env"] }
 console = { version = "0.15" }
+boxy = { version = "0.1.0" }

+ 5 - 7
hoard.vim

@@ -6,10 +6,11 @@ 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 hoardChange "\v^\s*-\s*[^:]*\s*:\s*[-+]?\d+(\.\d*)?(\s*\=\s*[-+]?\d+(\.\d*)?)?(\s+\S+)?$" contains=hoardChangeAccount,hoardChangeDeposit,hoardChangeWithdrawal,hoardChangeBalance,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 hoardChangeDeposit "\v(:[^=]+)@<=\+?\s*\d+(\.\d*)?" contained
+syntax match hoardChangeWithdrawal "\v(:[^=]+)@<=-\s*\d+(\.\d*)?" contained
+syntax match hoardChangeBalance "\v(\S+\s*:\s*[+-]?\d+(\.\d*)?\s*\=\s*)@<=([-+]?\d+(\.\d*)?)" contained
 syntax match hoardChangeUnit "\v\s+\S+$" contained
 
 syntax match hoardComment "^\s*#.*$"
@@ -20,11 +21,8 @@ highlight def link hoardAnnotation String
 highlight def link hoardChangeAccount Function
 highlight def link hoardChangeDeposit Added
 highlight def link hoardChangeWithdrawal Removed
+highlight def link hoardChangeBalance Comment
 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'

+ 92 - 2
src/check.rs

@@ -1,12 +1,16 @@
+use std::collections::BTreeMap;
+
 use chumsky::span::Span;
 
-use crate::data::{DataError, Decimal, Root};
+use crate::data::{DataError, Decimal, Root, UnitName};
 
 fn check_equal_sum(root: &Root) -> Result<(), DataError> {
     log::debug!("Checking for equal sums in monounit ledger entries...");
 
     for le in root.all_ledger_data() {
-        let Some(tx) = le.as_transaction() else { continue };
+        let Some(tx) = le.as_transaction() else {
+            continue;
+        };
         let Some(mono) = tx.mono_unit() else { continue };
 
         let net = tx
@@ -36,7 +40,93 @@ fn check_equal_sum(root: &Root) -> Result<(), DataError> {
     Ok(())
 }
 
+fn check_precision(root: &Root) -> Result<(), DataError> {
+    log::debug!("Checking for precision errors in ledger entries...");
+
+    for le in root.all_ledger_data() {
+        let Some(tx) = le.as_transaction() else {
+            continue;
+        };
+
+        for change in &tx.changes {
+            let unit_spec = root.unit_spec(*change.unit).unwrap();
+            let Some(precision) = unit_spec.precision else {
+                continue;
+            };
+            let amount_scale = change.amount.scale();
+            if amount_scale > precision {
+                return Err(ariadne::Report::build(
+                    ariadne::ReportKind::Error,
+                    change.amount.span(),
+                )
+                .with_label(
+                    ariadne::Label::new(change.amount.span()).with_message(format!(
+                        "{} has {} digits of precision, not {}",
+                        change.unit,
+                        precision,
+                        change.amount.0.scale()
+                    )),
+                )
+                .finish()
+                .into());
+            }
+
+            let Some(balance) = change.balance.as_ref() else {
+                continue;
+            };
+            let balance_scale = balance.scale();
+            if balance_scale > precision {
+                return Err(
+                    ariadne::Report::build(ariadne::ReportKind::Error, balance.span())
+                        .with_label(ariadne::Label::new(balance.span()).with_message(format!(
+                            "{} has {} digits of precision, not {}",
+                            change.unit,
+                            precision,
+                            change.amount.0.scale()
+                        )))
+                        .finish()
+                        .into(),
+                );
+            }
+        }
+    }
+
+    Ok(())
+}
+
+fn check_balances(root: &Root) -> Result<(), DataError> {
+    for account in root.account_names() {
+        let Some(ledger) = root.ledger_data_for(account) else {
+            continue;
+        };
+
+        let mut running_balance = BTreeMap::<UnitName, Decimal>::new();
+        for txn in ledger {
+            let change = txn.change_for(account).unwrap();
+            let bal = running_balance.entry(*change.unit).or_default();
+            *bal = bal.checked_add(*change.amount).unwrap();
+
+            if let Some(sbal) = change.balance.as_ref() {
+                if **sbal != *bal {
+                    return Err(
+                        ariadne::Report::build(ariadne::ReportKind::Error, txn.span())
+                            .with_label(ariadne::Label::new(sbal.span()).with_message(format!(
+                                "Calculated balance is {} {}, specified balance is {} {}",
+                                bal, change.unit, sbal, change.unit
+                            )))
+                            .finish()
+                            .into(),
+                    );
+                }
+            }
+        }
+    }
+    Ok(())
+}
+
 pub fn run_checks(root: &mut Root) -> Result<(), DataError> {
+    check_precision(root)?;
     check_equal_sum(root)?;
+    check_balances(root)?;
     Ok(())
 }

+ 3 - 1
src/data.rs

@@ -330,7 +330,9 @@ impl Root {
 
     fn preprocess_ledger_data(&mut self) {
         for entry in &self.ledger_data {
-            let ledger::LedgerEntry::Transaction(tx) = &entry else { continue };
+            let ledger::LedgerEntry::Transaction(tx) = &entry else {
+                continue;
+            };
             for bal in &tx.changes {
                 self.account_ledger_data
                     .entry(*bal.account)

+ 2 - 4
src/data/ledger.rs

@@ -30,9 +30,7 @@ impl Transaction {
     }
 
     pub fn change_for(&self, account: AccountName) -> Option<&Spanned<Change>> {
-        self.changes
-            .iter()
-            .find(|b| b.account.as_ref() == &account)
+        self.changes.iter().find(|b| b.account.as_ref() == &account)
     }
 
     pub fn split_changes(
@@ -72,7 +70,7 @@ impl LedgerEntry {
     pub fn as_transaction(&self) -> Option<&Spanned<Transaction>> {
         match self {
             Self::Transaction(tx) => Some(tx),
-            _ => None
+            _ => None,
         }
     }
 

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

@@ -2,7 +2,7 @@ use crate::data::{
     AccountName, DataError, Datestamp, Decimal, SourceFile, Span, Spanned, UnitName, spec::SpecRoot,
 };
 
-use super::{Change, Transaction, LedgerEntry};
+use super::{Change, LedgerEntry, Transaction};
 
 use chumsky::{prelude::*, text::inline_whitespace};
 
@@ -22,8 +22,14 @@ fn ledger_parser<'a>() -> impl Parser<
         .to_slice()
         .map(|v: &str| v.parse::<usize>().unwrap());
 
-    let datestamp = group((int, just('-').ignored(), int, just('-').ignored(), int))
-        .map(|(y, _, m, _, d)| Datestamp { year: y as u16, month: m as u8, day: d as u8 });
+    let datestamp =
+        group((int, just('-').ignored(), int, just('-').ignored(), int)).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());
 
@@ -139,7 +145,11 @@ fn ledger_parser<'a>() -> impl Parser<
         },
     );
 
-    let comment = mark('#').ignore_then(none_of("\n").repeated()).padded().to_slice().map_with(|s: &str, e| LedgerEntry::Comment(Spanned::new(s.into(), e.span())));
+    let comment = mark('#')
+        .ignore_then(none_of("\n").repeated())
+        .padded()
+        .to_slice()
+        .map_with(|s: &str, e| LedgerEntry::Comment(Spanned::new(s.into(), e.span())));
 
     (transaction.or(comment)).repeated().collect()
 }

+ 5 - 7
src/data/ledger/print.rs

@@ -7,11 +7,7 @@ use crate::data::{Root, SourceFile, Span, Spanned};
 use super::{LedgerEntry, Transaction};
 
 fn print_transaction(root: &Root, tx: &Transaction, padding: usize) {
-    println!(
-        "{}: {}",
-        tx.datestamp,
-        tx.title.as_deref().unwrap_or("")
-    );
+    println!("{}: {}", tx.datestamp, tx.title.as_deref().unwrap_or(""));
 
     if !tx.annotations.is_empty() {
         println!(
@@ -35,11 +31,13 @@ fn print_transaction(root: &Root, tx: &Transaction, padding: usize) {
             String::new()
         };
 
-
         if Some(change.unit.as_ref()) == root.account_spec(*change.account).unwrap().unit.as_ref()
             && !force_show
         {
-            println!(" - {:padding$}: {spacer}{}{balance}", change.account, change.amount,);
+            println!(
+                " - {:padding$}: {spacer}{}{balance}",
+                change.account, change.amount,
+            );
         } else {
             println!(
                 " - {:padding$}: {spacer}{}{balance} {}",

+ 1 - 1
src/data/spec.rs

@@ -17,7 +17,7 @@ pub struct AccountSpec {
 #[serde(deny_unknown_fields)]
 pub struct UnitSpec {
     pub name: UnitName,
-    pub precision: Option<u8>,
+    pub precision: Option<u32>,
 }
 
 #[derive(Debug, serde::Deserialize)]

+ 1 - 1
src/main.rs

@@ -35,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}")),
     }
 }

+ 59 - 31
src/show.rs

@@ -2,7 +2,10 @@ use std::fmt::Display;
 
 use console::Style;
 
-use crate::data::{AccountName, ledger::{Transaction,Change}};
+use crate::data::{
+    AccountName, Decimal,
+    ledger::{Change, Transaction},
+};
 
 #[derive(Clone, Copy, Default)]
 enum Align {
@@ -31,64 +34,89 @@ fn show_table<'d>(cols: Vec<Column>, rows: impl Iterator<Item = Row>) {
     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() {
+        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!("+");
+            Row::Line => {
+                for (idx, col) in min_widths.iter().enumerate() {
+                    if cols[idx].left_border {
+                        print!("{}", boxy::Char::cross(boxy::Weight::Normal));
+                    }
+                    for _ in 0..=*col {
+                        print!("{}", boxy::Char::horizontal(boxy::Weight::Normal));
+                    }
+                    // print!("{:-width$}", "", width = col + 1);
+                    if cols[idx].right_border {
+                        print!("{}", boxy::Char::cross(boxy::Weight::Normal));
+                    }
                 }
-                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!("{}", boxy::Char::vertical(boxy::Weight::Normal));
+                    }
+                    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!("{}", boxy::Char::vertical(boxy::Weight::Normal));
+                    }
                 }
-            },
-            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 {
-}
+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 },
+                Column {
+                    align: Align::Left,
+                    right_border: true,
+                    ..Default::default()
+                },
                 // Memo
-                Column { align: Align::Left, ..Default::default() },
+                Column {
+                    align: Align::Left,
+                    ..Default::default()
+                },
                 // Amount
-                Column { align: Align::Right, ..Default::default() },
+                Column {
+                    align: Align::Right,
+                    ..Default::default()
+                },
+                // Balance
+                Column {
+                    align: Align::Right,
+                    left_border: true,
+                    ..Default::default()
+                },
             ],
-            txns
-                .filter_map(|txn| txn.change_for(account).map(|chg| (txn, chg)))
+            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)
+                        format!("{}", chg.amount),
+                        chg.balance
+                            .as_deref()
+                            .map(Decimal::to_string)
+                            .unwrap_or(String::new()),
                     ])
-                })
+                }),
         )
     }
 }

+ 1 - 12
testdata/ledger

@@ -3,7 +3,7 @@
  - chequing   :  400.00 CAD
 
 2001-01-07: transfer to savings
- - chequing   : -300.00 CAD
+ - chequing   : -300.00 = 100.00 CAD
  - savings    :  300.00 CAD
 
 2001-02-08: test for unusual account name
@@ -14,16 +14,5 @@
  - savings    : -100.00 CAD
  - savings_usd:  80.00 USD
 
-2001-02-10: check for floating-point error
- - initial    : -0.0001 CAD
- - chequing   : -0.0002 CAD
- - savings    :  0.0003 CAD
-
-2001-02-10: clean up
-    [tag] [anothertag]
- - initial    :  0.0001 CAD
- - chequing   :  0.0002 CAD
- - savings    : -0.0003 CAD
-
 # vim: set filetype=hoard :