Browse Source

Implemented transaction merging and killing during match.

Kestrel 2 tuần trước cách đây
mục cha
commit
5fd9eba61b
4 tập tin đã thay đổi với 166 bổ sung80 xóa
  1. 3 25
      src/cmd.rs
  2. 1 1
      src/cmd/infer.rs
  3. 79 6
      src/cmd/txnmatch.rs
  4. 83 48
      src/data.rs

+ 3 - 25
src/cmd.rs

@@ -59,10 +59,6 @@ pub enum Command {
     Match {
         account: data::AccountName,
     },
-    Merge {
-        txn1: String,
-        txn2: String,
-    },
     Comb {
         account: Option<data::AccountName>,
     },
@@ -215,29 +211,11 @@ impl Command {
                 }
             }
             Self::Match { account } => {
-                let data = load_data(&mut fsdata, inv, check::CheckLevel::WellFormed)?;
-
-                /*let Some(acc_data) = data.ledger_data_for(*account) else {
-                    log::error!("No ledger data available for account {account}");
-                    return Ok(());
-                };
-
-                let txns = acc_data
-                    .iter()
-                    .filter(|v| v.borrow().get_annotation("id").is_some())
-                    .sorted()
-                    .collect_vec();
-                */
+                let mut data = load_data(&mut fsdata, inv, check::CheckLevel::WellFormed)?;
 
-                if txnmatch::do_match(&data, *account)? {
-                    
+                if txnmatch::do_match(&mut data, *account)? {
+                    data::format_entire_ledger(&mut fsdata, &data)?;
                 }
-
-                /*show::TransactionTable::default().show_refs(
-                    Some(&data),
-                    *account,
-                    txns.into_iter()
-                );*/
             },
             Self::Comb { account } => {
                 let data = load_data(&mut fsdata, inv, check::CheckLevel::Consistent)?;

+ 1 - 1
src/cmd/infer.rs

@@ -36,7 +36,7 @@ pub fn do_inference(data: &mut Hoard) -> anyhow::Result<bool> {
 
         for other_txn_ref in other_txns {
             let other_txn = other_txn_ref.borrow();
-            if other_txn.title.is_none() || other_txn.title != txn_title || *other_txn == *txn {
+            if other_txn.title.is_none() || other_txn.title != txn_title || other_txn_ref == txn_ref {
                 continue;
             }
 

+ 79 - 6
src/cmd/txnmatch.rs

@@ -1,37 +1,110 @@
+use std::collections::BTreeSet;
+
 use crate::prelude::*;
 
-pub fn do_match(data: &data::Hoard, account: data::AccountName) -> anyhow::Result<bool> {
+fn do_merge(txn1_ref: &data::TransactionRef, txn2_ref: &data::TransactionRef, common: data::AccountName) -> data::TransactionRef {
+    let mut txn1 = txn1_ref.borrow_mut();
+    let mut txn2 = txn2_ref.borrow_mut();
+
+    show::show_transaction(None, &txn1);
+    show::show_transaction(None, &txn2);
+
+    assert_eq!(txn1.changes.len(), 2);
+    assert_eq!(txn2.changes.len(), 2);
+
+    let (change1, mut rest1) = txn1.split_changes_mut(common).unwrap();
+    let (change2, mut rest2) = txn2.split_changes_mut(common).unwrap();
+
+    let other1 = rest1.next().unwrap();
+    let other2 = rest2.next().unwrap();
+
+    drop(rest1);
+    drop(rest2);
+
+    // keep whichever txn has the withdrawal
+    if change1.amount.is_sign_positive() {
+        change1.account = other2.account;
+
+        txn2.annotations.push("killed".into());
+        txn2_ref.clone()
+    } else {
+        change2.account = other1.account;
+
+        txn1.annotations.push("killed".into());
+        txn1_ref.clone()
+    }
+}
+
+#[derive(cliask::ActionEnum)]
+enum Action {
+    Skip,
+    Merge,
+    Ignore,
+    Finish,
+    Abort,
+}
+
+pub fn do_match(data: &mut data::Hoard, account: data::AccountName) -> anyhow::Result<bool> {
     let Some(account_data)  = data.ledger_data_for(account) else { 
         log::warn!("No data for account {account}");
         return Ok(false)
     };
 
+    let mut killed = vec![];
+    let mut skip = BTreeSet::<data::TransactionRef>::new();
+
     let mut sorted_data : Vec<_> = account_data.into();
     sorted_data.sort_by_key(|d| d.borrow().datestamp);
 
     let search_window = chrono::Days::new(7);
 
-    for txn_ref in &sorted_data {
+    'outer_loop: for txn_ref in &sorted_data {
+        if skip.contains(txn_ref) {
+            continue
+        }
         let txn = txn_ref.borrow();
 
         let amount = *txn.change_for(account).unwrap().amount;
 
-
         // go one week prior and after
         let range_start = sorted_data.binary_search_by_key(&txn.datestamp.checked_sub_days(search_window).unwrap(), |d| d.borrow().datestamp).unwrap_or_else(|a| a);
         let range_end = sorted_data.binary_search_by_key(&txn.datestamp.checked_add_days(search_window).unwrap(), |d| d.borrow().datestamp).unwrap_or_else(|a| a);
 
         for otxn_ref in sorted_data.iter().take(range_end).skip(range_start) {
+            if skip.contains(otxn_ref) {
+                continue
+            }
             let otxn = otxn_ref.borrow();
             if *otxn.change_for(account).unwrap().amount == -amount {
-                println!("possible matching transactions:");
+                println!("==== Candidate match ====");
+
                 show::show_transaction(Some(data), &txn);
+                println!();
                 show::show_transaction(Some(data), &otxn);
 
-
+                match cliask::ActionPrompt::new().with_default(Action::Skip).run()? {
+                    Action::Finish => break 'outer_loop,
+                    Action::Skip => continue,
+                    Action::Ignore => break,
+                    Action::Merge => {
+                        drop(txn);
+                        drop(otxn);
+                        killed.push(do_merge(txn_ref, otxn_ref, account));
+                        skip.insert(txn_ref.clone());
+                        skip.insert(otxn_ref.clone());
+                        break
+                    },
+                    Action::Abort => {
+                        return Ok(false)
+                    },
+                }
             }
         }
     }
 
-    Ok(false)
+    let any_merged = !killed.is_empty();
+
+    data.kill_transactions(killed.into_iter());
+
+    Ok(any_merged)
 }

+ 83 - 48
src/data.rs

@@ -30,49 +30,7 @@ pub type AccountName = stringstore::StoredString<AccountTag>;
 
 pub type Datestamp = chrono::NaiveDate;
 
-/*
-#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
-pub struct Datestamp {
-    pub year: u16,
-    pub month: u8,
-    pub day: u8,
-}
-
-impl Datestamp {
-    pub fn distance_from(&self, other: &Self) -> usize {
-        if self > other {
-            return other.distance_from(self)
-        }
-
-        // at target?
-        if self.year == other.year && self.month == other.month && self.day == other.day {
-            0
-        }
-        // advance days?
-        else if self.year == other.year && self.month == other.month {
-            (other.day - self.day) as usize
-        } else if self.year == other.year {
-            todo!()
-        } else {
-            todo!()
-        }
-    }
-}
-*/
-
-/*impl std::fmt::Display for Datestamp {
-    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
-        write!(f, "{:04}-{:02}-{:02}", self.0.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(Clone, Debug, PartialEq, PartialOrd, Ord, Eq)]
+#[derive(Clone, Debug)]
 pub struct Change {
     pub source: Option<io::Span>,
 
@@ -82,7 +40,7 @@ pub struct Change {
     pub unit: Spanned<UnitName>,
 }
 
-#[derive(Clone, Debug, PartialEq, PartialOrd, Eq, Ord)]
+#[derive(Clone, Debug)]
 pub struct Transaction {
     pub source: Option<io::Span>,
 
@@ -101,6 +59,10 @@ impl Transaction {
         self.changes.iter().find(|b| b.account.as_ref() == &account)
     }
 
+    pub fn change_for_mut(&mut self, account: AccountName) -> Option<&mut Change> {
+        self.changes.iter_mut().find(|b| b.account.as_ref() == &account)
+    }
+
     pub fn split_changes(
         &self,
         account: AccountName,
@@ -117,6 +79,31 @@ impl Transaction {
         ))
     }
 
+    pub fn split_changes_mut(
+        &mut self,
+        account: AccountName,
+    ) -> Option<(&mut Change, impl Iterator<Item = &mut Change>)> {
+        let index = self
+            .changes
+            .iter()
+            .position(|b| b.account.as_ref() == &account)?;
+
+        match index {
+            0 => {
+                let (l, r) = self.changes.split_at_mut(1);
+                // chain with the empty list here to make the return types equal
+                Some((&mut l[0], r.into_iter().chain((&mut []).into_iter())))
+            }
+            _ => {
+                let (l, r1) = self.changes.split_at_mut(index);
+
+                let (at, r) = r1.split_at_mut(1);
+
+                Some((&mut at[0], l.into_iter().chain(r.into_iter())))
+            }
+        }
+    }
+
     pub fn is_mono_unit(&self) -> bool {
         self.changes.iter().unique_by(|b| *b.unit).count() == 1
     }
@@ -137,9 +124,43 @@ impl Transaction {
     }
 }
 
-pub type TransactionRef = std::rc::Rc<std::cell::RefCell<Transaction>>;
+#[derive(Clone)]
+pub struct TransactionRef(std::rc::Rc<std::cell::RefCell<Transaction>>);
+
+impl std::ops::Deref for TransactionRef {
+    type Target = std::cell::RefCell<Transaction>;
+    fn deref(&self) -> &Self::Target {
+        &self.0
+    }
+}
+
+impl std::fmt::Debug for TransactionRef {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        self.0.borrow().fmt(f)
+    }
+}
+
+impl PartialEq for TransactionRef {
+    fn eq(&self, other: &Self) -> bool {
+        std::rc::Rc::as_ptr(&self.0) == std::rc::Rc::as_ptr(&other.0)
+    }
+}
+
+impl Eq for TransactionRef { }
+
+impl PartialOrd for TransactionRef {
+    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
+        Some(<Self as Ord>::cmp(self, other))
+    }
+}
+
+impl Ord for TransactionRef {
+    fn cmp(&self, other: &Self) -> std::cmp::Ordering {
+        std::rc::Rc::as_ptr(&self.0).cmp(&std::rc::Rc::as_ptr(&other.0))
+    }
+}
 
-#[derive(Debug, PartialEq, Eq, PartialOrd, Ord)]
+#[derive(Debug)]
 pub enum RawLedgerEntry {
     Transaction(Transaction),
     Comment(Spanned<String>),
@@ -168,7 +189,7 @@ impl RawLedgerEntry {
     }
 }
 
-#[derive(Debug, PartialEq, Eq, PartialOrd, Ord)]
+#[derive(Debug)]
 pub enum LedgerEntry {
     Transaction(TransactionRef),
     Comment(Spanned<String>),
@@ -321,7 +342,7 @@ impl Hoard {
                 }
             };
 
-            let txn_ref = std::rc::Rc::new(std::cell::RefCell::new(tx.clone()));
+            let txn_ref = TransactionRef(std::rc::Rc::new(std::cell::RefCell::new(tx.clone())));
             for bal in &tx.changes {
                 self.account_ledger_data
                     .entry(*bal.account)
@@ -382,4 +403,18 @@ impl Hoard {
 
         Some(running)
     }
+
+    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<_>>() {
+            let Some(account_data) = self.account_ledger_data.get_mut(&account) else { continue };
+            let Some(pos) = account_data.iter().position(|v| *v == txn_ref) else { continue };
+            account_data.remove(pos);
+        }
+        // transaction txn_ref should now be dangling
+    }
+
+    pub fn kill_transactions(&mut self, txns: impl Iterator<Item = TransactionRef>) {
+        txns.for_each(|t| self.kill_transaction(t));
+    }
 }