txnmatch.rs 4.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140
  1. use std::collections::BTreeSet;
  2. use crate::prelude::*;
  3. fn do_merge(
  4. txn1_ref: &data::TransactionRef,
  5. txn2_ref: &data::TransactionRef,
  6. common: data::AccountName,
  7. ) {
  8. let mut txn1 = txn1_ref.borrow_mut();
  9. let mut txn2 = txn2_ref.borrow_mut();
  10. show::show_transaction(None, &txn1);
  11. show::show_transaction(None, &txn2);
  12. assert_eq!(txn1.changes.len(), 2);
  13. assert_eq!(txn2.changes.len(), 2);
  14. let (change1, mut rest1) = txn1.split_changes_mut(common).unwrap();
  15. let (change2, mut rest2) = txn2.split_changes_mut(common).unwrap();
  16. let other1 = rest1.next().unwrap();
  17. let other2 = rest2.next().unwrap();
  18. drop(rest1);
  19. drop(rest2);
  20. change1.account = other2.account;
  21. change2.account = other1.account;
  22. // ghost whichever is the deposit
  23. if change1.amount.is_sign_positive() {
  24. txn1.annotations
  25. .insert("ghost".to_string(), txn2.id().unwrap().to_string());
  26. } else {
  27. txn2.annotations
  28. .insert("ghost".to_string(), txn1.id().unwrap().to_string());
  29. }
  30. }
  31. #[derive(cliask::ActionEnum)]
  32. enum Action {
  33. Skip,
  34. Merge,
  35. Ignore,
  36. Finish,
  37. Abort,
  38. }
  39. pub fn do_match(data: &mut data::Hoard, account: data::AccountName) -> anyhow::Result<bool> {
  40. let Some(account_data) = data.ledger_data_for(account) else {
  41. log::warn!("No data for account {account}");
  42. return Ok(false);
  43. };
  44. let mut killed = vec![];
  45. let mut skip = BTreeSet::<data::TransactionRef>::new();
  46. let mut sorted_data: Vec<_> = account_data.into();
  47. sorted_data.sort_by_key(|d| d.borrow().datestamp);
  48. let search_window = chrono::Days::new(7);
  49. 'outer_loop: for txn_ref in &sorted_data {
  50. if skip.contains(txn_ref) {
  51. continue;
  52. }
  53. let txn = txn_ref.borrow();
  54. if txn.id().is_none() {
  55. continue;
  56. }
  57. let (_, mut txn_known_it) = txn.split_changes(account).unwrap();
  58. let txn_known = txn_known_it.next().unwrap();
  59. drop(txn_known_it);
  60. let amount = *txn.change_for(account).unwrap().amount;
  61. // go one week prior and after
  62. let range_start = sorted_data
  63. .binary_search_by_key(
  64. &txn.datestamp.checked_sub_days(search_window).unwrap(),
  65. |d| d.borrow().datestamp,
  66. )
  67. .unwrap_or_else(|a| a);
  68. let range_end = sorted_data
  69. .binary_search_by_key(
  70. &txn.datestamp.checked_add_days(search_window).unwrap(),
  71. |d| d.borrow().datestamp,
  72. )
  73. .unwrap_or_else(|a| a);
  74. for otxn_ref in sorted_data.iter().take(range_end).skip(range_start) {
  75. if skip.contains(otxn_ref) {
  76. continue;
  77. }
  78. let otxn = otxn_ref.borrow();
  79. if otxn.id().is_none() {
  80. continue;
  81. }
  82. let (_, mut otxn_known_it) = otxn.split_changes(account).unwrap();
  83. let otxn_known = otxn_known_it.next().unwrap();
  84. drop(otxn_known_it);
  85. if otxn_known.account == txn_known.account {
  86. continue;
  87. }
  88. if *otxn.change_for(account).unwrap().amount == -amount {
  89. println!("==== Candidate match ====");
  90. show::show_transaction(Some(data), &txn);
  91. println!();
  92. show::show_transaction(Some(data), &otxn);
  93. match cliask::ActionPrompt::new()
  94. .with_default(Action::Skip)
  95. .run()?
  96. {
  97. Action::Finish => break 'outer_loop,
  98. Action::Skip => continue,
  99. Action::Ignore => break,
  100. Action::Merge => {
  101. drop(txn);
  102. drop(otxn);
  103. killed.push(do_merge(txn_ref, otxn_ref, account));
  104. skip.insert(txn_ref.clone());
  105. skip.insert(otxn_ref.clone());
  106. break;
  107. }
  108. Action::Abort => return Ok(false),
  109. }
  110. }
  111. }
  112. }
  113. Ok(!skip.is_empty())
  114. }