GnuCash c935c2f+
Loading...
Searching...
No Matches
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
70{
71 int64_t m_amount;
72 int64_t m_rem_pos;
73 int64_t m_rem_neg;
74 Split* m_split;
75};
76
77using SplitInfoVec = std::vector<SplitInfo>;
78using SplitVec = std::vector<Split*>;
79
81{
82 const char* abort = nullptr;
83 gint abort_id = Autoclear::ABORT_NONE;
84 SplitVec splits;
85};
86
87static const char*
88path_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
111static void
112subset_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
171GList *
172gnc_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
182 (gnc_numeric_convert
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}
Account handling public routines.
Account public routines (C++ api)
API for Transactions and Splits (journal entries)
API for Transactions and Splits (journal entries)
int xaccAccountGetCommoditySCU(const Account *acc)
Return the SCU for the account.
Definition Account.cpp:2745
gint64 time64
Most systems that are currently maintained, including Microsoft Windows, BSD-derived Unixes and Linux...
Definition gnc-date.h:87
time64 xaccTransGetDate(const Transaction *trans)
Retrieve the posted date of the transaction.
#define DEBUG(format, args...)
Print a debugging message.
Definition qoflog.h:264
Transaction * xaccSplitGetParent(const Split *split)
Returns the parent transaction of the split.
char xaccSplitGetReconcile(const Split *split)
Returns the value of the reconcile flag.
#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's commodity.
STRUCTS.