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 { 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::::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()) }