Selaa lähdekoodia

Implement merge groups.

Kestrel 1 viikko sitten
vanhempi
commit
d892bf2500
4 muutettua tiedostoa jossa 80 lisäystä ja 28 poistoa
  1. 51 26
      src/check.rs
  2. 4 2
      src/cmd.rs
  3. 2 0
      src/cmd/txnmatch.rs
  4. 23 0
      src/data.rs

+ 51 - 26
src/check.rs

@@ -176,43 +176,67 @@ fn check_balances(root: &data::Hoard) -> Result<(), DataError> {
         for group in date_groups.into_iter() {
             check_balance_group(account, &mut running_balance, group.1)?;
         }
+    }
+    Ok(())
+}
+
+fn check_merge_groups(root: &data::Hoard) -> Result<(), DataError> {
+    log::trace!("Checking merge groups...");
+
+    for account in root.account_names() {
+        let Some(ledger) = root.ledger_data_for(account) else {
+            continue;
+        };
 
-        /*
         for txn_ref in ledger {
             let txn = txn_ref.borrow();
-            let change = txn.change_for(account).unwrap();
-            let bal = running_balance
-                .entry(*change.unit)
-                .or_insert_with(|| Spanned::new(data::Decimal::default(), io::Span::default()));
-            let last_span = bal.span();
-            bal.0 = bal.checked_add(*change.amount).unwrap();
-            bal.1 = change.source.unwrap();
+            let Some(group_id) = txn.merge_group() else { continue };
+            let Some(group) = root.merge_group(group_id) else {
+                todo!()
+            };
+
+            let txn_changes = txn.changes.iter().map(|c| (c.account, c.amount, c.unit)).sorted().collect_vec();
 
-            if let Some(sbal) = change.balance.as_ref() {
-                if **sbal != bal.0 {
+            for placeholder_ref in group {
+                let placeholder = placeholder_ref.borrow();
+
+                if txn.datestamp != placeholder.datestamp {
                     let report = ariadne::Report::build(
                         ariadne::ReportKind::Error,
-                        txn.source.unwrap_or_default(),
+                        txn.source.unwrap()
                     )
-                    .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("Last balance is from here"),
-                        )
-                    } else {
-                        report
-                    };
-
-                    return Err(report.finish().into());
+                    .with_label(ariadne::Label::new(txn.source.unwrap()).with_message("this transaction"))
+                    .with_label(ariadne::Label::new(placeholder.source.unwrap()).with_message("and this transaction"))
+                    .with_message("different datestamps despite being in one merge group")
+                    .finish();
+
+                    return Err(report.into())
+                }
+
+                let placeholder_changes = placeholder.changes.iter().map(|c| (c.account, c.amount, c.unit)).sorted().collect_vec();
+
+                let mut different_changes = txn_changes.iter().zip(placeholder_changes.iter()).filter(|(t,p)| t != p);
+
+                if let Some((t,p)) = different_changes.next() {
+                    let tspan = t.0.span().union(t.1.span()).union(t.2.span());
+                    let pspan = p.0.span().union(p.1.span()).union(p.2.span());
+
+                    let report = ariadne::Report::build(
+                        ariadne::ReportKind::Error,
+                        txn.source.unwrap()
+                    )
+                    .with_label(ariadne::Label::new(txn.source.unwrap()).with_message("in this transaction"))
+                    .with_label(ariadne::Label::new(tspan).with_message("this change differs from"))
+                    .with_label(ariadne::Label::new(placeholder.source.unwrap()).with_message("in this transaction"))
+                    .with_label(ariadne::Label::new(pspan).with_message("this change"))
+                    .with_message("merged transactions differ in at least one change")
+                    .finish();
+                    return Err(report.into())
                 }
             }
         }
-        */
     }
+
     Ok(())
 }
 
@@ -221,6 +245,7 @@ pub fn run_checks(root: &mut data::Hoard, level: CheckLevel) -> Result<(), DataE
         log::debug!("Running transaction well-formedness checks...");
         check_precision(root)?;
         check_equal_sum(root)?;
+        check_merge_groups(root)?;
     }
     if level >= CheckLevel::Consistent {
         log::debug!("Running ledger consistency checks...");

+ 4 - 2
src/cmd.rs

@@ -57,7 +57,7 @@ pub enum Command {
     },
     Infer,
     Match {
-        account: data::AccountName,
+        account: Option<data::AccountName>,
     },
     Comb {
         account: Option<data::AccountName>,
@@ -217,7 +217,9 @@ impl Command {
             Self::Match { account } => {
                 let mut data = load_data(&mut fsdata, inv, check::CheckLevel::WellFormed)?;
 
-                if txnmatch::do_match(&mut data, *account)? {
+                let account = account.unwrap_or(data.spec_root().placeholder_account);
+
+                if txnmatch::do_match(&mut data, account)? {
                     data::format_entire_ledger(&mut fsdata, &data)?;
                 }
             }

+ 2 - 0
src/cmd/txnmatch.rs

@@ -27,7 +27,9 @@ fn do_merge(
     drop(rest2);
 
     change1.account = other2.account;
+    change1.balance = other2.balance;
     change2.account = other1.account;
+    change2.balance = other1.balance;
 
     // generate unused ID
     let merge_id = loop {

+ 23 - 0
src/data.rs

@@ -219,6 +219,19 @@ impl LedgerEntry {
     }
 }
 
+fn merge_balances(into_ref: &TransactionRef, from_ref: &TransactionRef) {
+    // merge balance assertions
+    let mut into = into_ref.borrow_mut();
+    for change in from_ref.borrow().changes.iter() {
+        let Some(into_change) = into.change_for_mut(*change.account) else {
+            log::error!("Merged transactions have different accounts. Skipping ...");
+            continue
+        };
+        let Some(bal) = change.balance else { continue };
+        into_change.balance.get_or_insert(bal);
+    }
+}
+
 #[derive(Debug)]
 pub struct Hoard {
     path: std::path::PathBuf,
@@ -336,6 +349,7 @@ impl Hoard {
     }
 
     fn preprocess_ledger_data(&mut self) {
+        let mut group_leads = BTreeMap::<String, TransactionRef>::new();
         for entry in &self.raw_ledger_data {
             let tx = match entry {
                 RawLedgerEntry::Transaction(tx) => tx,
@@ -351,10 +365,15 @@ impl Hoard {
                 use std::collections::btree_map::Entry;
                 match self.groups.entry(merge_group.to_string()) {
                     Entry::Vacant(e) => {
+                        group_leads.insert(merge_group.to_string(), txn_ref.clone());
                         e.insert(vec![]);
                     },
                     Entry::Occupied(mut e) => {
                         e.get_mut().push(txn_ref.clone());
+                        drop(e);
+                        log::info!("merging balances into representative txn");
+                        let lead = group_leads.get(merge_group).unwrap();
+                        merge_balances(lead, &txn_ref);
                         continue
                     },
                 }
@@ -430,6 +449,10 @@ impl Hoard {
         self.groups.contains_key(name)
     }
 
+    pub fn merge_group(&self, name: &str) -> Option<&Vec<TransactionRef>> {
+        self.groups.get(name)
+    }
+
     pub fn kill_transaction(&mut self, txn_ref: TransactionRef) {
         let txn = txn_ref.borrow();
         for account in txn.changes.iter().map(|v| *v.account).collect::<Vec<_>>() {