浏览代码

Update vim syntax highlighting, minor tweaks.

Kestrel 2 周之前
父节点
当前提交
ff5605172c
共有 11 个文件被更改,包括 184 次插入70 次删除
  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" }
 pretty_env_logger = { version = "0.5.0" }
 clap = { version = "4.5", features = ["derive", "env"] }
 clap = { version = "4.5", features = ["derive", "env"] }
 console = { version = "0.15" }
 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 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 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 hoardChangeUnit "\v\s+\S+$" contained
 
 
 syntax match hoardComment "^\s*#.*$"
 syntax match hoardComment "^\s*#.*$"
@@ -20,11 +21,8 @@ highlight def link hoardAnnotation String
 highlight def link hoardChangeAccount Function
 highlight def link hoardChangeAccount Function
 highlight def link hoardChangeDeposit Added
 highlight def link hoardChangeDeposit Added
 highlight def link hoardChangeWithdrawal Removed
 highlight def link hoardChangeWithdrawal Removed
+highlight def link hoardChangeBalance Comment
 highlight def link hoardChangeUnit Type
 highlight def link hoardChangeUnit Type
 highlight def link hoardComment Comment
 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'
 let b:current_syntax = 'hoard'

+ 92 - 2
src/check.rs

@@ -1,12 +1,16 @@
+use std::collections::BTreeMap;
+
 use chumsky::span::Span;
 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> {
 fn check_equal_sum(root: &Root) -> Result<(), DataError> {
     log::debug!("Checking for equal sums in monounit ledger entries...");
     log::debug!("Checking for equal sums in monounit ledger entries...");
 
 
     for le in root.all_ledger_data() {
     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 Some(mono) = tx.mono_unit() else { continue };
 
 
         let net = tx
         let net = tx
@@ -36,7 +40,93 @@ fn check_equal_sum(root: &Root) -> Result<(), DataError> {
     Ok(())
     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> {
 pub fn run_checks(root: &mut Root) -> Result<(), DataError> {
+    check_precision(root)?;
     check_equal_sum(root)?;
     check_equal_sum(root)?;
+    check_balances(root)?;
     Ok(())
     Ok(())
 }
 }

+ 3 - 1
src/data.rs

@@ -330,7 +330,9 @@ impl Root {
 
 
     fn preprocess_ledger_data(&mut self) {
     fn preprocess_ledger_data(&mut self) {
         for entry in &self.ledger_data {
         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 {
             for bal in &tx.changes {
                 self.account_ledger_data
                 self.account_ledger_data
                     .entry(*bal.account)
                     .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>> {
     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(
     pub fn split_changes(
@@ -72,7 +70,7 @@ impl LedgerEntry {
     pub fn as_transaction(&self) -> Option<&Spanned<Transaction>> {
     pub fn as_transaction(&self) -> Option<&Spanned<Transaction>> {
         match self {
         match self {
             Self::Transaction(tx) => Some(tx),
             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,
     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};
 use chumsky::{prelude::*, text::inline_whitespace};
 
 
@@ -22,8 +22,14 @@ fn ledger_parser<'a>() -> impl Parser<
         .to_slice()
         .to_slice()
         .map(|v: &str| v.parse::<usize>().unwrap());
         .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());
     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()
     (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};
 use super::{LedgerEntry, Transaction};
 
 
 fn print_transaction(root: &Root, tx: &Transaction, padding: usize) {
 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() {
     if !tx.annotations.is_empty() {
         println!(
         println!(
@@ -35,11 +31,13 @@ fn print_transaction(root: &Root, tx: &Transaction, padding: usize) {
             String::new()
             String::new()
         };
         };
 
 
-
         if Some(change.unit.as_ref()) == root.account_spec(*change.account).unwrap().unit.as_ref()
         if Some(change.unit.as_ref()) == root.account_spec(*change.account).unwrap().unit.as_ref()
             && !force_show
             && !force_show
         {
         {
-            println!(" - {:padding$}: {spacer}{}{balance}", change.account, change.amount,);
+            println!(
+                " - {:padding$}: {spacer}{}{balance}",
+                change.account, change.amount,
+            );
         } else {
         } else {
             println!(
             println!(
                 " - {:padding$}: {spacer}{}{balance} {}",
                 " - {:padding$}: {spacer}{}{balance} {}",

+ 1 - 1
src/data/spec.rs

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

+ 1 - 1
src/main.rs

@@ -35,7 +35,7 @@ fn load_data(
         Err(data::DataError::Report(report)) => {
         Err(data::DataError::Report(report)) => {
             report.eprint(fsdata)?;
             report.eprint(fsdata)?;
             Err(anyhow::anyhow!("Error reported"))
             Err(anyhow::anyhow!("Error reported"))
-        },
+        }
         Err(data::DataError::Validation(verr)) => Err(anyhow::anyhow!("Validation error: {verr}")),
         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 console::Style;
 
 
-use crate::data::{AccountName, ledger::{Transaction,Change}};
+use crate::data::{
+    AccountName, Decimal,
+    ledger::{Change, Transaction},
+};
 
 
 #[derive(Clone, Copy, Default)]
 #[derive(Clone, Copy, Default)]
 enum Align {
 enum Align {
@@ -31,64 +34,89 @@ fn show_table<'d>(cols: Vec<Column>, rows: impl Iterator<Item = Row>) {
     let table_data = rows.collect::<Vec<_>>();
     let table_data = rows.collect::<Vec<_>>();
     for row in &table_data {
     for row in &table_data {
         let Row::Data(row) = &row else { continue };
         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()));
             min_widths[idx] = min_widths[idx].max(console::measure_text_width(rc.as_str()));
         }
         }
     }
     }
 
 
     for row in table_data {
     for row in table_data {
         match row {
         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!();
         println!();
     }
     }
 }
 }
 
 
 #[derive(Clone, Copy, Default)]
 #[derive(Clone, Copy, Default)]
-pub struct TransactionTable {
-}
+pub struct TransactionTable {}
 
 
 impl TransactionTable {
 impl TransactionTable {
     pub fn show<'d>(self, account: AccountName, txns: impl Iterator<Item = &'d Transaction>) {
     pub fn show<'d>(self, account: AccountName, txns: impl Iterator<Item = &'d Transaction>) {
         show_table(
         show_table(
             vec![
             vec![
                 // datestamp
                 // datestamp
-                Column { align: Align::Left, left_border: false, right_border: true },
+                Column {
+                    align: Align::Left,
+                    right_border: true,
+                    ..Default::default()
+                },
                 // Memo
                 // Memo
-                Column { align: Align::Left, ..Default::default() },
+                Column {
+                    align: Align::Left,
+                    ..Default::default()
+                },
                 // Amount
                 // 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)| {
                 .map(|(txn, chg)| {
                     Row::Data(vec![
                     Row::Data(vec![
                         txn.datestamp.to_string(),
                         txn.datestamp.to_string(),
                         txn.title.clone().unwrap_or_else(String::new),
                         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
  - chequing   :  400.00 CAD
 
 
 2001-01-07: transfer to savings
 2001-01-07: transfer to savings
- - chequing   : -300.00 CAD
+ - chequing   : -300.00 = 100.00 CAD
  - savings    :  300.00 CAD
  - savings    :  300.00 CAD
 
 
 2001-02-08: test for unusual account name
 2001-02-08: test for unusual account name
@@ -14,16 +14,5 @@
  - savings    : -100.00 CAD
  - savings    : -100.00 CAD
  - savings_usd:  80.00 USD
  - 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 :
 # vim: set filetype=hoard :