123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140 |
- use std::collections::BTreeSet;
- use crate::prelude::*;
- fn do_merge(
- txn1_ref: &data::TransactionRef,
- txn2_ref: &data::TransactionRef,
- common: data::AccountName,
- ) {
- 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);
- change1.account = other2.account;
- change2.account = other1.account;
- // ghost whichever is the deposit
- if change1.amount.is_sign_positive() {
- txn1.annotations
- .insert("ghost".to_string(), txn2.id().unwrap().to_string());
- } else {
- txn2.annotations
- .insert("ghost".to_string(), txn1.id().unwrap().to_string());
- }
- }
- #[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);
- 'outer_loop: for txn_ref in &sorted_data {
- if skip.contains(txn_ref) {
- continue;
- }
- let txn = txn_ref.borrow();
- if txn.id().is_none() {
- continue;
- }
- let (_, mut txn_known_it) = txn.split_changes(account).unwrap();
- let txn_known = txn_known_it.next().unwrap();
- drop(txn_known_it);
- 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.id().is_none() {
- continue;
- }
- let (_, mut otxn_known_it) = otxn.split_changes(account).unwrap();
- let otxn_known = otxn_known_it.next().unwrap();
- drop(otxn_known_it);
- if otxn_known.account == txn_known.account {
- continue;
- }
- if *otxn.change_for(account).unwrap().amount == -amount {
- 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(!skip.is_empty())
- }
|