Эх сурвалжийг харах

Allowed a command to run only a subset of checks.

Kestrel 2 долоо хоног өмнө
parent
commit
3bdefe0217
7 өөрчлөгдсөн 107 нэмэгдсэн , 59 устгасан
  1. 2 0
      hoard.vim
  2. 41 25
      src/check.rs
  3. 23 9
      src/cmd.rs
  4. 10 3
      src/data.rs
  5. 8 9
      src/data/ledger/parse.rs
  6. 21 13
      src/import.rs
  7. 2 0
      testdata/savings-import.csv

+ 2 - 0
hoard.vim

@@ -1,3 +1,5 @@
+" copy to ~/.vim/syntax/hoard.vim
+
 if exists('b:current_syntax') | finish | endif
 
 syntax match hoardTxHeader "\v^\s*\d\d\d\d-\d\d-\d\d\s*:.*$" contains=hoardTxDate,hoardTxTitle

+ 41 - 25
src/check.rs

@@ -1,12 +1,21 @@
 use std::collections::BTreeMap;
 
-use chumsky::span::Span as CSpan;
 use ariadne::Span as ASpan;
+use chumsky::span::Span as CSpan;
 
 use crate::data::{DataError, Decimal, Root, Span, Spanned, UnitName};
 
+#[derive(Default, PartialEq, PartialOrd)]
+pub enum CheckLevel {
+    /// Check individual transactions
+    WellFormed,
+    /// Check relations between transactions
+    #[default]
+    Consistent,
+}
+
 fn check_equal_sum(root: &Root) -> Result<(), DataError> {
-    log::debug!("Checking for equal sums in monounit ledger entries...");
+    log::trace!("Checking for equal sums in monounit ledger entries...");
 
     for le in root.all_ledger_data() {
         let Some(tx) = le.as_transaction() else {
@@ -42,7 +51,7 @@ fn check_equal_sum(root: &Root) -> Result<(), DataError> {
 }
 
 fn check_precision(root: &Root) -> Result<(), DataError> {
-    log::debug!("Checking for precision errors in ledger entries...");
+    log::trace!("Checking for precision errors in ledger entries...");
 
     for le in root.all_ledger_data() {
         let Some(tx) = le.as_transaction() else {
@@ -96,6 +105,8 @@ fn check_precision(root: &Root) -> Result<(), DataError> {
 }
 
 fn check_balances(root: &Root) -> Result<(), DataError> {
+    log::trace!("Checking balance consistency...");
+
     for account in root.account_names() {
         let Some(ledger) = root.ledger_data_for(account) else {
             continue;
@@ -104,31 +115,30 @@ fn check_balances(root: &Root) -> Result<(), DataError> {
         let mut running_balance = BTreeMap::<UnitName, Spanned<Decimal>>::new();
         for txn in ledger {
             let change = txn.change_for(account).unwrap();
-            let bal = running_balance.entry(*change.unit).or_insert_with(|| Spanned::new(Decimal::default(), Span::default()));
+            let bal = running_balance
+                .entry(*change.unit)
+                .or_insert_with(|| Spanned::new(Decimal::default(), Span::default()));
             let last_span = bal.span();
             bal.0 = bal.checked_add(*change.amount).unwrap();
             bal.1 = change.1;
 
             if let Some(sbal) = change.balance.as_ref() {
                 if **sbal != bal.0 {
-                    let report = 
-                        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
-                            )));
-                    let report =
-                        if !last_span.is_empty() {
-                            report.with_label(ariadne::Label::new(last_span).with_message(format!("Last balance is from here")))
-                        } else {
-                            report
-                        };
-
-                    return Err(
+                    let report = 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
+                        )));
+                    let report = if !last_span.is_empty() {
+                        report.with_label(
+                            ariadne::Label::new(last_span)
+                                .with_message(format!("Last balance is from here")),
+                        )
+                    } else {
                         report
-                            .finish()
-                            .into(),
-                    );
+                    };
+
+                    return Err(report.finish().into());
                 }
             }
         }
@@ -136,9 +146,15 @@ fn check_balances(root: &Root) -> Result<(), DataError> {
     Ok(())
 }
 
-pub fn run_checks(root: &mut Root) -> Result<(), DataError> {
-    check_precision(root)?;
-    check_equal_sum(root)?;
-    check_balances(root)?;
+pub fn run_checks(root: &mut Root, level: CheckLevel) -> Result<(), DataError> {
+    if level >= CheckLevel::WellFormed {
+        log::debug!("Running transaction well-formedness checks...");
+        check_precision(root)?;
+        check_equal_sum(root)?;
+    }
+    if level >= CheckLevel::Consistent {
+        log::debug!("Running ledger consistency checks...");
+        check_balances(root)?;
+    }
     Ok(())
 }

+ 23 - 9
src/cmd.rs

@@ -1,6 +1,6 @@
 use itertools::Itertools;
 
-use crate::{data, import::import_from, io, show};
+use crate::{check::CheckLevel, data, import::import_from, io, show};
 
 #[derive(clap::Parser)]
 pub struct Invocation {
@@ -19,9 +19,10 @@ pub struct Invocation {
 fn load_data(
     fsdata: &mut io::FilesystemData,
     invocation: &Invocation,
+    level: CheckLevel,
 ) -> anyhow::Result<data::Root> {
     let path = std::fs::canonicalize(&invocation.file)?;
-    match data::Root::load(fsdata, &path) {
+    match data::Root::load(fsdata, &path, level) {
         Ok(data) => Ok(data),
         Err(data::DataError::IOError(ioerror)) => Err(ioerror.into()),
         Err(data::DataError::Report(report)) => {
@@ -44,6 +45,7 @@ pub enum Command {
         from: std::path::PathBuf,
         target: Option<std::path::PathBuf>,
     },
+    Dedup,
 }
 
 fn summarize(data: &data::Root) {
@@ -91,11 +93,11 @@ impl Command {
 
         match self {
             Self::Summarize => {
-                let data = load_data(&mut fsdata, inv)?;
+                let data = load_data(&mut fsdata, inv, Default::default())?;
                 summarize(&data);
             }
             Self::Ledger { account } => {
-                let data = load_data(&mut fsdata, inv)?;
+                let data = load_data(&mut fsdata, inv, Default::default())?;
 
                 let aname = data::AccountName::new(account.as_str());
 
@@ -107,7 +109,7 @@ impl Command {
                 }
             }
             Self::Reformat => {
-                let data = load_data(&mut fsdata, inv)?;
+                let data = load_data(&mut fsdata, inv, CheckLevel::WellFormed)?;
 
                 data::ledger::print_ledger(&mut fsdata, &data, data.all_ledger_data().iter())?;
             }
@@ -116,18 +118,21 @@ impl Command {
                 from,
                 target,
             } => {
-                let data = load_data(&mut fsdata, inv)?;
+                let data = load_data(&mut fsdata, inv, Default::default())?;
 
                 let aname = account.into();
                 let Some(aspec) = data.account_spec(aname) else {
-                    todo!()
+                    log::error!("Account {aname} does not exist!");
+                    return Ok(());
                 };
 
                 let imported = import_from(aspec, aname, from.as_path()).unwrap();
-                log::info!("Imported {} transactions", imported.len());
+                log::info!("Imported {} transaction(s)", imported.len());
 
                 if let Some(target) = target {
-                    let new_source = std::fs::canonicalize(target.as_os_str())?.into_os_string().into();
+                    let new_source = std::fs::canonicalize(target.as_os_str())?
+                        .into_os_string()
+                        .into();
                     let span = data::Span::null_for_file(new_source);
 
                     let new_data = imported
@@ -146,6 +151,15 @@ impl Command {
                     tt.show(&data, aname, imported.iter());
                 }
             }
+            Self::Dedup => {
+                /*
+                let data = load_data(&mut fsdata, inv, CheckLevel::WellFormed)?;
+
+                let is_dup = |txn1: &data::ledger::Transaction, txn2: &data::ledger::Transaction| {
+
+                };*/
+                todo!()
+            }
         }
         Ok(())
     }

+ 10 - 3
src/data.rs

@@ -5,7 +5,10 @@ use chumsky::span::Span as _;
 
 pub use rust_decimal::Decimal;
 
-use crate::io::{FilesystemData, SourceFile};
+use crate::{
+    check::CheckLevel,
+    io::{FilesystemData, SourceFile},
+};
 
 pub mod ledger;
 pub mod spec;
@@ -211,7 +214,11 @@ pub struct Root {
 }
 
 impl Root {
-    pub fn load(fsdata: &mut FilesystemData, path: &std::path::Path) -> Result<Self, DataError> {
+    pub fn load(
+        fsdata: &mut FilesystemData,
+        path: &std::path::Path,
+        check_level: CheckLevel,
+    ) -> Result<Self, DataError> {
         let sf = SourceFile::new(path.as_os_str());
         let root_data = fsdata.fetch(&sf).unwrap();
 
@@ -227,7 +234,7 @@ impl Root {
                 r.load_ledgers(fsdata)?;
                 r.preprocess_ledger_data();
 
-                crate::check::run_checks(&mut r)?;
+                crate::check::run_checks(&mut r, check_level)?;
 
                 Ok(r)
             }

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

@@ -18,10 +18,18 @@ fn ledger_parser<'a>() -> impl Parser<
         (),
     >,
 > {
+    let mark = |m| just(m).padded_by(inline_whitespace());
+
     let int = chumsky::text::digits(10)
         .to_slice()
         .map(|v: &str| v.parse::<usize>().unwrap());
 
+    let comment = mark('#')
+        .ignore_then(none_of("\n").repeated())
+        .to_slice()
+        .padded()
+        .map_with(|s: &str, e| LedgerEntry::Comment(Spanned::new(s.into(), e.span())));
+
     let datestamp =
         group((int, just('-').ignored(), int, just('-').ignored(), int)).map(|(y, _, m, _, d)| {
             Datestamp {
@@ -31,8 +39,6 @@ fn ledger_parser<'a>() -> impl Parser<
             }
         });
 
-    let mark = |m| just(m).padded_by(inline_whitespace());
-
     let decimal_digit = one_of("0123456789.,");
     let decimal_digits = decimal_digit
         .or(just(' ').repeated().ignore_then(decimal_digit))
@@ -145,12 +151,6 @@ 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())));
-
     (transaction.or(comment)).repeated().collect()
 }
 
@@ -169,7 +169,6 @@ pub fn parse_ledger(
         .into_output_errors();
 
     if let Some(e) = errors.first() {
-        // let span = e.span().start()..e.span().end();
         let span = *e.span();
 
         let report = ariadne::Report::build(ariadne::ReportKind::Error, span)

+ 21 - 13
src/import.rs

@@ -180,47 +180,52 @@ fn postprocess(account: AccountName, transactions: &mut Vec<Transaction>) {
         match running_balances.entry(*change.unit) {
             std::collections::hash_map::Entry::Vacant(entry) => {
                 entry.insert(bal);
-                return true
-            },
+                return true;
+            }
             std::collections::hash_map::Entry::Occupied(mut entry) => {
                 let rbal = entry.get_mut();
                 let new_rbal = rbal.checked_add(*change.amount).unwrap();
                 if new_rbal != bal {
-                    return false
+                    return false;
                 } else {
                     *rbal = new_rbal;
-                    return true
+                    return true;
                 }
-            },
+            }
         }
     };
 
-
-    let mut removed : Vec<Transaction> = vec![];
+    let mut removed: Vec<Transaction> = vec![];
 
     'outer: loop {
         for ridx in 0..removed.len() {
-            if check_for_match(&mut running_balances, removed[ridx].change_for(account).unwrap()) {
+            if check_for_match(
+                &mut running_balances,
+                removed[ridx].change_for(account).unwrap(),
+            ) {
                 transactions.insert(idx, removed.remove(ridx));
                 log::trace!("pulling transaction out of removed");
                 idx += 1;
-                continue 'outer
+                continue 'outer;
             }
         }
 
         if idx >= transactions.len() {
-            break
+            break;
         }
 
         let tx = &transactions[idx];
         let change = tx.change_for(account).unwrap();
         if change.balance.is_none() {
             idx += 1;
-            continue
+            continue;
         };
 
         if check_for_match(&mut running_balances, change) {
-            log::trace!("transaction is good! balance is now: {}", running_balances[&*change.unit]);
+            log::trace!(
+                "transaction is good! balance is now: {}",
+                running_balances[&*change.unit]
+            );
             idx += 1;
         } else {
             log::trace!("shifting transaction to removed");
@@ -229,7 +234,10 @@ fn postprocess(account: AccountName, transactions: &mut Vec<Transaction>) {
     }
 
     if removed.len() > 0 {
-        log::error!("Not all transactions are consistent!");
+        log::error!(
+            "Not all transactions are consistent! Inconsistent transactions below will be discarded:"
+        );
+        // crate::show::TransactionTable::default().show(
     }
 }
 

+ 2 - 0
testdata/savings-import.csv

@@ -0,0 +1,2 @@
+Date,Description,Transfer,Balance
+01 SEP 2023,Interest,$1.56,$201.56