GnuCash  5.6-150-g038405b370+
gnc-autoclear.cpp
1 /********************************************************************
2  * gnc-autoclear.cpp -- Knapsack algorithm functions *
3  * *
4  * Copyright 2020 Cristian Klein <cristian@kleinlabs.eu> *
5  * Copyright 2021 Christopher Lam to clear same-amount splits *
6  * *
7  * This program is free software; you can redistribute it and/or *
8  * modify it under the terms of the GNU General Public License as *
9  * published by the Free Software Foundation; either version 2 of *
10  * the License, or (at your option) any later version. *
11  * *
12  * This program is distributed in the hope that it will be useful, *
13  * but WITHOUT ANY WARRANTY; without even the implied warranty of *
14  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
15  * GNU General Public License for more details. *
16  * *
17  * You should have received a copy of the GNU General Public License*
18  * along with this program; if not, contact: *
19  * *
20  * Free Software Foundation Voice: +1-617-542-5942 *
21  * 51 Franklin Street, Fifth Floor Fax: +1-617-542-2652 *
22  * Boston, MA 02110-1301, USA gnu@gnu.org *
23  *******************************************************************/
24 
25 #include <config.h>
26 
27 #include <glib/gi18n.h>
28 
29 #include "Account.h"
30 #include "Account.hpp"
31 #include "Split.h"
32 #include "Transaction.h"
33 #include "gnc-autoclear.h"
34 
35 #include <optional>
36 #include <vector>
37 #include <numeric>
38 #include <algorithm>
39 #include <cstdint>
40 #include <chrono>
41 #include <stdexcept>
42 #include <cinttypes>
43 #include <charconv>
44 #include <cstring>
45 
46 #define log_module "gnc.autoclear"
47 
49 {
50  uint64_t m_counter = 0;
51  std::optional<double> m_seconds;
52  std::chrono::steady_clock::time_point m_start;
53  RuntimeMonitor (double seconds) : m_start(std::chrono::steady_clock::now())
54  {
55  if (seconds > 0) m_seconds = seconds;
56  };
57  double get_elapsed ()
58  {
59  return std::chrono::duration_cast<std::chrono::duration<double>>(std::chrono::steady_clock::now() - m_start).count();
60  }
61  bool should_abort ()
62  {
63  if (!m_seconds.has_value()) return false;
64  if (++m_counter % 100000 != 0) return false;
65  return get_elapsed() > *m_seconds;
66  }
67 };
68 
69 struct SplitInfo
70 {
71  int64_t m_amount;
72  int64_t m_rem_pos;
73  int64_t m_rem_neg;
74  Split* m_split;
75 };
76 
77 using SplitInfoVec = std::vector<SplitInfo>;
78 using SplitVec = std::vector<Split*>;
79 
80 struct Solution
81 {
82  const char* abort = nullptr;
83  gint abort_id = Autoclear::ABORT_NONE;
84  SplitVec splits;
85 };
86 
87 static const char*
88 path_to_str(const SplitInfoVec& path)
89 {
90  if (path.empty()) return "<empty>";
91 
92  static char buff[1000];
93  char* p = buff;
94  char* end = buff + sizeof(buff);
95  for (const auto& split_info : path)
96  {
97  if (p != buff)
98  {
99  if (p < end) *p++ = '|';
100  else return "<overflow>";
101  }
102  auto res = std::to_chars (p, end, split_info.m_amount);
103  if (res.ec == std::errc()) p = res.ptr;
104  else return "<overflow>";
105  }
106  p = std::min (end - 1, p);
107  *p = '\0';
108  return buff;
109 }
110 
111 static void
112 subset_sum (SplitInfoVec::const_iterator iter,
113  SplitInfoVec::const_iterator end,
114  SplitInfoVec& path, Solution& solution,
115  int64_t target, RuntimeMonitor& monitor)
116 {
117  DEBUG ("this=%" PRId64" target=%" PRId64 " rem_pos=%" PRId64
118  " rem_neg=%" PRId64 " path=%s",
119  iter == end ? 0 : iter->m_amount, target,
120  iter == end ? 0 : iter->m_rem_pos,
121  iter == end ? 0 : iter->m_rem_neg, path_to_str (path));
122 
123  if (target == 0)
124  {
125  DEBUG ("SOLUTION FOUND: %s%s", path_to_str (path),
126  solution.splits.empty() ? "" : " ABORT: AMBIGUOUS");
127  if (!solution.splits.empty())
128  {
129  solution.abort_id = Autoclear::ABORT_MULTI;
130  solution.abort = N_("Cannot uniquely clear splits. Found multiple possibilities.");
131  return;
132  }
133  else
134  {
135  solution.splits.resize (path.size());
136  std::transform (path.begin(), path.end(), solution.splits.begin(),
137  [](SplitInfo& i){ return i.m_split; });
138  return;
139  }
140  }
141 
142  if (solution.abort_id != Autoclear::ABORT_NONE || iter == end)
143  return;
144 
145  if (monitor.should_abort())
146  {
147  DEBUG ("ABORT: timeout");
148  solution.abort_id = Autoclear::ABORT_TIMEOUT;
149  solution.abort = N_("Auto-clear exceeds allocated time");
150  return;
151  }
152 
153  if (target < iter->m_rem_neg || target > iter->m_rem_pos)
154  {
155  DEBUG ("PRUNE target=%" PRId64 " rem_pos=%" PRId64" rem_neg=%" PRId64,
156  target, iter->m_rem_pos, iter->m_rem_neg);
157  return;
158  }
159 
160  auto next = std::next(iter);
161 
162  // 1st path: use current_num
163  path.push_back (*iter);
164  subset_sum (next, end, path, solution, target - iter->m_amount, monitor);
165  path.pop_back ();
166 
167  // 2nd path: skip current_num
168  subset_sum (next, end, path, solution, target, monitor);
169 }
170 
171 GList *
172 gnc_account_get_autoclear_splits (Account *account, gnc_numeric toclear_value,
173  time64 end_date, GError **error,
174  double max_seconds)
175 {
176  g_return_val_if_fail (account && error, nullptr);
177 
178  auto scu{xaccAccountGetCommoditySCU (account)};
179  auto numeric_to_int64 = [scu](gnc_numeric num) -> int64_t
180  {
181  return gnc_numeric_num
183  (num, scu, GNC_HOW_DENOM_EXACT | GNC_HOW_RND_NEVER));
184  };
185 
186  int64_t target{numeric_to_int64 (toclear_value)};
187  SplitInfoVec splits;
188  for (auto split : xaccAccountGetSplits (account))
189  {
190  if (xaccTransGetDate (xaccSplitGetParent (split)) > end_date)
191  break;
192  int64_t amt{numeric_to_int64 (xaccSplitGetAmount (split))};
193  if (xaccSplitGetReconcile (split) != NREC)
194  target -= amt;
195  else if (amt == 0)
196  DEBUG ("skipping zero-amount split %p", split);
197  else
198  splits.push_back ({amt, 0, 0, split});
199  }
200 
201  static GQuark autoclear_quark = g_quark_from_static_string ("autoclear");
202  if (target == 0)
203  {
204  g_set_error (error, autoclear_quark, Autoclear::ABORT_NOP, "%s",
205  N_("Account is already at Auto-Clear Balance."));
206  return nullptr;
207  }
208 
209  // sort splits in descending absolute amount
210  std::sort (splits.begin(), splits.end(),
211  [](const SplitInfo& a, const SplitInfo& b)
212  {
213  int64_t aa = std::llabs(a.m_amount);
214  int64_t bb = std::llabs(b.m_amount);
215  return (aa == bb) ? a.m_amount > b.m_amount : aa > bb;
216  });
217 
218  // for each split, precompute sums of remaining pos or neg amounts
219  int64_t rem_pos{0}, rem_neg{0};
220  std::for_each (splits.rbegin(), splits.rend(),
221  [&rem_pos, &rem_neg](SplitInfo& s)
222  {
223  s.m_rem_pos = rem_pos += std::max<int64_t>(s.m_amount, 0);
224  s.m_rem_neg = rem_neg += std::min<int64_t>(s.m_amount, 0);
225  });
226 
227  RuntimeMonitor monitor{max_seconds};
228  Solution solution;
229  SplitInfoVec path;
230  path.reserve (splits.size());
231 
232  subset_sum (splits.begin(), splits.end(), path, solution, target, monitor);
233 
234  DEBUG ("finished subset_sum in %f seconds", monitor.get_elapsed());
235 
236  if (solution.splits.empty())
237  {
238  g_set_error (error, autoclear_quark, Autoclear::ABORT_UNREACHABLE, "%s",
239  N_("The selected amount cannot be cleared."));
240  return nullptr;
241  }
242  else if (solution.abort_id)
243  {
244  g_set_error (error, autoclear_quark,
245  solution.abort_id, "%s", solution.abort);
246  return nullptr;
247  }
248 
249  return std::accumulate
250  (solution.splits.begin(), solution.splits.end(),
251  static_cast<GList*>(nullptr), g_list_prepend);
252 }
Never round at all, and signal an error if there is a fractional result in a computation.
Definition: gnc-numeric.h:177
time64 xaccTransGetDate(const Transaction *trans)
Retrieve the posted date of the transaction.
int xaccAccountGetCommoditySCU(const Account *acc)
Return the SCU for the account.
Definition: Account.cpp:2719
STRUCTS.
#define DEBUG(format, args...)
Print a debugging message.
Definition: qoflog.h:264
char xaccSplitGetReconcile(const Split *split)
Returns the value of the reconcile flag.
Transaction * xaccSplitGetParent(const Split *split)
Returns the parent transaction of the split.
API for Transactions and Splits (journal entries)
Use any denominator which gives an exactly correct ratio of numerator to denominator.
Definition: gnc-numeric.h:188
Account handling public routines.
gnc_numeric gnc_numeric_convert(gnc_numeric n, gint64 denom, gint how)
Change the denominator of a gnc_numeric value to the specified denominator under standard arguments &#39;...
Account public routines (C++ api)
gint64 time64
Most systems that are currently maintained, including Microsoft Windows, BSD-derived Unixes and Linux...
Definition: gnc-date.h:87
API for Transactions and Splits (journal entries)
#define NREC
not reconciled or cleared
Definition: Split.h:76
gnc_numeric xaccSplitGetAmount(const Split *split)
Returns the amount of the split in the account&#39;s commodity.
Definition: gmock-Split.cpp:69