gnucash stable: Multiple changes pushed
Christopher Lam
clam at code.gnucash.org
Fri Nov 14 23:03:47 EST 2025
Updated via https://github.com/Gnucash/gnucash/commit/43561ded (commit)
via https://github.com/Gnucash/gnucash/commit/03af6204 (commit)
from https://github.com/Gnucash/gnucash/commit/60410172 (commit)
commit 43561deda4a29f9fceea92f5e261614157b85008
Author: Christopher Lam <christopher.lck at gmail.com>
Date: Sun Nov 9 14:50:28 2025 +0800
[window-reconcile.cpp] offer auto-clear if combination found
diff --git a/gnucash/gnome/window-reconcile.cpp b/gnucash/gnome/window-reconcile.cpp
index aabb067a04..19c0947750 100644
--- a/gnucash/gnome/window-reconcile.cpp
+++ b/gnucash/gnome/window-reconcile.cpp
@@ -44,6 +44,7 @@
#include "dialog-transfer.h"
#include "dialog-utils.h"
#include "gnc-amount-edit.h"
+#include "gnc-autoclear.h"
#include "gnc-component-manager.h"
#include "gnc-date.h"
#include "gnc-date-edit.h"
@@ -1719,6 +1720,39 @@ close_handler (gpointer user_data)
gtk_widget_destroy (recnData->window);
}
+static void
+offer_autoclear (GtkWindow* window, Account* account, gnc_numeric new_ending,
+ time64 statement_date)
+{
+ g_return_if_fail (window && account);
+
+#define MAX_AUTOCLEAR_SECONDS 1
+
+ GError* error = nullptr;
+ GList* splits_to_clear = gnc_account_get_autoclear_splits
+ (account, new_ending, statement_date, &error, MAX_AUTOCLEAR_SECONDS);
+
+ if (error)
+ {
+ PWARN ("cannot autoclear: %s", error->message);
+ g_error_free (error);
+ return;
+ }
+
+ const char* autoclear = N_("Found %u splits up to %s which will clear "
+ "to %s. Do you want to auto-clear?");
+ guint num_splits = g_list_length (splits_to_clear);
+ GNCPrintAmountInfo pinfo = gnc_account_print_info (account, true);
+ const char *balance_str = xaccPrintAmount (new_ending, pinfo);
+ char *date_str = qof_print_date (statement_date);
+
+ if (gnc_action_dialog (window, _("Auto-clear"), false, _(autoclear),
+ num_splits, date_str, balance_str))
+ g_list_foreach (splits_to_clear, (GFunc)xaccSplitSetReconcile,
+ (gpointer)CREC);
+ g_free (date_str);
+ g_list_free (splits_to_clear);
+}
/********************************************************************\
* recnWindow *
@@ -1759,6 +1793,8 @@ recnWindow (GtkWidget *parent, Account *account)
enable_subaccounts))
return NULL;
+ offer_autoclear (GTK_WINDOW (parent), account, new_ending, statement_date);
+
return recnWindowWithBalance (parent, account, new_ending, statement_date);
}
commit 03af6204428d62b3bead7500eca480b77caf523b
Author: Christopher Lam <christopher.lck at gmail.com>
Date: Thu Nov 6 18:03:16 2025 +0800
[gnc-autoclear] upgrade - improve Subset Sum Problem
- still O(2^n) runtime but uses O(n) stack size, and *much* less memory
- a runtime monitor, limiting time (default 1s)
diff --git a/gnucash/gnome-utils/CMakeLists.txt b/gnucash/gnome-utils/CMakeLists.txt
index a1eba00534..5131bf4a3a 100644
--- a/gnucash/gnome-utils/CMakeLists.txt
+++ b/gnucash/gnome-utils/CMakeLists.txt
@@ -48,7 +48,6 @@ set (gnome_utils_SOURCES
dialog-utils.c
gnc-account-sel.c
gnc-amount-edit.c
- gnc-autoclear.c
gnc-autosave.c
gnc-cell-renderer-text-flag.c
gnc-cell-renderer-text-view.c
@@ -135,7 +134,6 @@ set (gnome_utils_HEADERS
dialog-utils.h
gnc-account-sel.h
gnc-amount-edit.h
- gnc-autoclear.h
gnc-cell-renderer-text-flag.h
gnc-cell-renderer-text-view.h
gnc-cell-view.h
diff --git a/gnucash/gnome-utils/gnc-autoclear.c b/gnucash/gnome-utils/gnc-autoclear.c
deleted file mode 100644
index b6e4120bad..0000000000
--- a/gnucash/gnome-utils/gnc-autoclear.c
+++ /dev/null
@@ -1,226 +0,0 @@
-/********************************************************************
- * gnc-autoclear.c -- Knapsack algorithm functions *
- * *
- * Copyright 2020 Cristian Klein <cristian at kleinlabs.eu> *
- * Modified 2021 Christopher Lam to clear same-amount splits *
- * *
- * This program is free software; you can redistribute it and/or *
- * modify it under the terms of the GNU General Public License as *
- * published by the Free Software Foundation; either version 2 of *
- * the License, or (at your option) any later version. *
- * *
- * This program is distributed in the hope that it will be useful, *
- * but WITHOUT ANY WARRANTY; without even the implied warranty of *
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
- * GNU General Public License for more details. *
- * *
- * You should have received a copy of the GNU General Public License*
- * along with this program; if not, contact: *
- * *
- * Free Software Foundation Voice: +1-617-542-5942 *
- * 51 Franklin Street, Fifth Floor Fax: +1-617-542-2652 *
- * Boston, MA 02110-1301, USA gnu at gnu.org *
- *******************************************************************/
-#include <config.h>
-
-#include <gtk/gtk.h>
-#include <glib/gi18n.h>
-
-#include "Account.h"
-#include "Split.h"
-#include "Transaction.h"
-#include "gncOwner.h"
-#include "qof.h"
-#include "gnc-autoclear.h"
-
-/* the following functions are used in window-autoclear: */
-
-typedef enum
-{
- AUTOCLEAR_OVERLOAD = 1,
- AUTOCLEAR_UNABLE,
- AUTOCLEAR_MULTIPLE,
- AUTOCLEAR_NOP,
-} autoclear_error_type;
-
-#define MAXIMUM_SACK_SIZE 1000000
-
-#define log_module "autoclear"
-
-typedef struct
-{
- GList *worklist;
- GHashTable *sack;
- Split *split;
-} sack_data;
-
-typedef struct
-{
- gnc_numeric reachable_amount;
- GList *list_of_splits;
-} WorkItem;
-
-static GList *DUP_LIST;
-
-static WorkItem *
-make_workitem (GHashTable *hash, gnc_numeric amount,
- Split *split, GList *splits)
-{
- WorkItem *item = g_new0 (WorkItem, 1);
- item->reachable_amount = amount;
- if (g_hash_table_lookup (hash, GINT_TO_POINTER (amount.num)) || splits == DUP_LIST)
- item->list_of_splits = DUP_LIST;
- else
- item->list_of_splits = g_list_prepend (g_list_copy (splits), split);
- return item;
-}
-
-static void
-sack_foreach_func (gint thisvalue_num, GList *splits, sack_data *data)
-{
- gnc_numeric itemval = xaccSplitGetAmount (data->split);
- gnc_numeric this_value = gnc_numeric_create (thisvalue_num, itemval.denom);
- gnc_numeric new_value = gnc_numeric_add_fixed (this_value, itemval);
- WorkItem *item = make_workitem (data->sack, new_value, data->split, splits);
-
- data->worklist = g_list_prepend (data->worklist, item);
-}
-
-static void
-sack_free (gpointer thisvalue_num, GList *splits, sack_data *data)
-{
- if (splits != DUP_LIST)
- g_list_free (splits);
-}
-
-static void
-process_work (WorkItem *item, GHashTable *sack)
-{
- GList *existing = g_hash_table_lookup (sack, GINT_TO_POINTER(item->reachable_amount.num));
- if (existing && existing != DUP_LIST)
- {
- DEBUG ("removing existing for %6.2f\n",
- gnc_numeric_to_double (item->reachable_amount));
- g_list_free (existing);
- }
- g_hash_table_insert (sack, GINT_TO_POINTER(item->reachable_amount.num), item->list_of_splits);
-}
-
-gboolean
-gnc_autoclear_get_splits (Account *account, gnc_numeric toclear_value,
- time64 end_date,
- GList **splits, GError **error, GtkLabel *label)
-{
- GList *nc_list = NULL, *toclear_list = NULL;
- GHashTable *sack;
- GQuark autoclear_quark = g_quark_from_static_string ("autoclear");
-
- g_return_val_if_fail (GNC_IS_ACCOUNT (account), FALSE);
- g_return_val_if_fail (splits != NULL, FALSE);
-
- sack = g_hash_table_new (NULL, NULL);
- DUP_LIST = g_list_prepend (NULL, NULL);
-
- /* Extract which splits are not cleared and compute the amount we have to clear */
- GList *acc_splits = xaccAccountGetSplitList (account);
- for (GList *node = acc_splits; node; node = node->next)
- {
- Split *split = (Split *)node->data;
- gnc_numeric amount = xaccSplitGetAmount (split);
-
- if (amount.denom != toclear_value.denom)
- {
- g_set_error (error, autoclear_quark, AUTOCLEAR_NOP, "Split amount and toclear amount have different denoms");
- goto skip_knapsack;
- }
- if (xaccSplitGetReconcile (split) != NREC)
- toclear_value = gnc_numeric_sub_fixed (toclear_value, amount);
- else if (gnc_numeric_zero_p (amount))
- DEBUG ("skipping zero-amount split %p", split);
- else if (end_date != INT64_MAX &&
- xaccTransGetDate (xaccSplitGetParent (split)) > end_date)
- DEBUG ("skipping split after statement_date %p", split);
- else
- nc_list = g_list_prepend (nc_list, split);
- }
- g_list_free (acc_splits);
-
- if (gnc_numeric_zero_p (toclear_value))
- {
- g_set_error (error, autoclear_quark, AUTOCLEAR_NOP,
- _("Account is already at Auto-Clear Balance."));
- goto skip_knapsack;
- }
- else if (!nc_list)
- {
- g_set_error (error, autoclear_quark, AUTOCLEAR_NOP,
- _("No uncleared splits found."));
- goto skip_knapsack;
- }
-
- for (GList *node = nc_list; node; node = node->next)
- {
- Split *split = (Split *)node->data;
- WorkItem *item = make_workitem (sack, xaccSplitGetAmount (split), split, NULL);
- sack_data s_data = { g_list_prepend (NULL, item), sack, split };
-
- g_hash_table_foreach (sack, (GHFunc) sack_foreach_func, &s_data);
- g_list_foreach (s_data.worklist, (GFunc) process_work, sack);
- g_list_free_full (s_data.worklist, g_free);
- if (g_hash_table_size (sack) > MAXIMUM_SACK_SIZE)
- {
- g_set_error (error, autoclear_quark, AUTOCLEAR_OVERLOAD,
- _("Too many uncleared splits"));
- goto skip_knapsack;
- }
- else if (g_hash_table_lookup (sack, GINT_TO_POINTER(toclear_value.num)) == DUP_LIST)
- {
- g_set_error (error, autoclear_quark, AUTOCLEAR_MULTIPLE,
- _("Cannot uniquely clear splits. Found multiple possibilities."));
- goto skip_knapsack;
- }
- }
-
- toclear_list = g_hash_table_lookup (sack, GINT_TO_POINTER(toclear_value.num));
-
- /* Check solution */
- if (!toclear_list)
- {
- g_set_error (error, autoclear_quark, AUTOCLEAR_UNABLE,
- _("The selected amount cannot be cleared."));
- goto skip_knapsack;
- }
- else
- /* copy GList because GHashTable value will be freed */
- *splits = g_list_copy (toclear_list);
-
- skip_knapsack:
- g_hash_table_foreach (sack, (GHFunc) sack_free, NULL);
- g_hash_table_destroy (sack);
- g_list_free (nc_list);
- g_list_free (DUP_LIST);
-
- return (toclear_list != NULL);
-}
-
-
-GList *
-gnc_account_get_autoclear_splits (Account *account, gnc_numeric toclear_value,
- gchar **errmsg)
-{
- GError *error = NULL;
- GList *splits = NULL;
-
- gnc_autoclear_get_splits (account, toclear_value, INT64_MAX,
- &splits, &error, NULL);
-
- if (error)
- {
- *errmsg = g_strdup (error->message);
- g_error_free (error);
- return NULL;
- }
-
- *errmsg = NULL;
- return splits;
-}
diff --git a/gnucash/gnome-utils/test/CMakeLists.txt b/gnucash/gnome-utils/test/CMakeLists.txt
index ea25c04468..1a86fd0d6a 100644
--- a/gnucash/gnome-utils/test/CMakeLists.txt
+++ b/gnucash/gnome-utils/test/CMakeLists.txt
@@ -24,28 +24,7 @@ gnc_add_scheme_test_targets(scm-test-load-gnome-utils-module
OUTPUT_DIR "tests"
DEPENDS "${GUILE_DEPENDS}")
-set(test_autoclear_SOURCES
- test-autoclear.cpp
-)
-
-set(test_autoclear_INCLUDE_DIRS
- ${CMAKE_BINARY_DIR}/common
- ${CMAKE_SOURCE_DIR}/libgnucash/engine
-)
-
-set(test_autoclear_LIBS
- gnc-engine
- gnc-gnome-utils
- gtest
-)
-
-gnc_add_test(test-autoclear "${test_autoclear_SOURCES}"
- test_autoclear_INCLUDE_DIRS
- test_autoclear_LIBS
-)
-
gnc_add_scheme_tests(test-load-gnome-utils-module.scm)
-set_dist_list(test_gnome_utils_DIST CMakeLists.txt test-load-gnome-utils-module.scm
- ${test_autoclear_SOURCES})
+set_dist_list(test_gnome_utils_DIST CMakeLists.txt test-load-gnome-utils-module.scm)
diff --git a/gnucash/gnome/window-autoclear.c b/gnucash/gnome/window-autoclear.c
index c9039f5aa3..5cd4c50ed1 100644
--- a/gnucash/gnome/window-autoclear.c
+++ b/gnucash/gnome/window-autoclear.c
@@ -139,8 +139,11 @@ gnc_autoclear_window_ok_cb (GtkWidget *widget,
toclear_value = gnc_numeric_convert
(toclear_value, xaccAccountGetCommoditySCU(data->account), GNC_HOW_RND_ROUND);
- gnc_autoclear_get_splits (data->account, toclear_value, INT64_MAX,
- &toclear_list, &error, data->status_label);
+#define MAX_AUTOCLEAR_SECONDS 5
+
+ toclear_list = gnc_account_get_autoclear_splits
+ (data->account, toclear_value, INT64_MAX, &error,
+ MAX_AUTOCLEAR_SECONDS);
}
if (error && error->message)
diff --git a/libgnucash/app-utils/CMakeLists.txt b/libgnucash/app-utils/CMakeLists.txt
index 45b2bf5ea3..26476c3c76 100644
--- a/libgnucash/app-utils/CMakeLists.txt
+++ b/libgnucash/app-utils/CMakeLists.txt
@@ -10,6 +10,7 @@ set (app_utils_HEADERS
QuickFill.h
file-utils.h
gnc-account-merge.h
+ gnc-autoclear.h
gnc-addr-quickfill.h
gnc-entry-quickfill.h
gnc-gsettings.h
@@ -25,6 +26,7 @@ set (app_utils_SOURCES
QuickFill.c
file-utils.c
gnc-account-merge.c
+ gnc-autoclear.cpp
gnc-addr-quickfill.c
gnc-entry-quickfill.c
gnc-gsettings.cpp
diff --git a/libgnucash/app-utils/gnc-autoclear.cpp b/libgnucash/app-utils/gnc-autoclear.cpp
new file mode 100644
index 0000000000..2a31f5f01b
--- /dev/null
+++ b/libgnucash/app-utils/gnc-autoclear.cpp
@@ -0,0 +1,210 @@
+/********************************************************************
+ * gnc-autoclear.cpp -- Knapsack algorithm functions *
+ * *
+ * Copyright 2020 Cristian Klein <cristian at kleinlabs.eu> *
+ * Copyright 2021 Christopher Lam to clear same-amount splits *
+ * *
+ * This program is free software; you can redistribute it and/or *
+ * modify it under the terms of the GNU General Public License as *
+ * published by the Free Software Foundation; either version 2 of *
+ * the License, or (at your option) any later version. *
+ * *
+ * This program is distributed in the hope that it will be useful, *
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of *
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
+ * GNU General Public License for more details. *
+ * *
+ * You should have received a copy of the GNU General Public License*
+ * along with this program; if not, contact: *
+ * *
+ * Free Software Foundation Voice: +1-617-542-5942 *
+ * 51 Franklin Street, Fifth Floor Fax: +1-617-542-2652 *
+ * Boston, MA 02110-1301, USA gnu at gnu.org *
+ *******************************************************************/
+
+#include <config.h>
+
+#include <glib/gi18n.h>
+
+#include "Account.h"
+#include "Account.hpp"
+#include "Split.h"
+#include "Transaction.h"
+#include "gnc-autoclear.h"
+
+#include <optional>
+#include <vector>
+#include <numeric>
+#include <algorithm>
+#include <cstdint>
+#include <chrono>
+#include <stdexcept>
+#include <cinttypes>
+#include <charconv>
+#include <cstring>
+
+#define log_module "gnc.autoclear"
+
+struct RuntimeMonitor
+{
+ uint64_t m_counter = 0;
+ std::optional<double> m_seconds;
+ std::chrono::steady_clock::time_point m_start;
+ RuntimeMonitor (double seconds) : m_start(std::chrono::steady_clock::now())
+ {
+ if (seconds > 0) m_seconds = seconds;
+ };
+ double get_elapsed ()
+ {
+ return std::chrono::duration_cast<std::chrono::duration<double>>(std::chrono::steady_clock::now() - m_start).count();
+ }
+ bool should_abort ()
+ {
+ if (!m_seconds.has_value()) return false;
+ if (++m_counter % 100000 != 0) return false;
+ return get_elapsed() > *m_seconds;
+ }
+};
+
+struct SplitInfo
+{
+ int64_t m_amount;
+ Split* m_split;
+};
+
+using SplitInfoVec = std::vector<SplitInfo>;
+using SplitVec = std::vector<Split*>;
+
+struct Solution
+{
+ bool abort = false;
+ SplitVec splits;
+};
+
+static const char*
+path_to_str(const SplitInfoVec& path)
+{
+ if (path.empty()) return "<empty>";
+
+ static char buff[1000];
+ char* p = buff;
+ char* end = buff + sizeof(buff);
+ for (const auto& split_info : path)
+ {
+ if (p != buff)
+ {
+ if (p < end) *p++ = '|';
+ else return "<overflow>";
+ }
+ auto res = std::to_chars (p, end, split_info.m_amount);
+ if (res.ec == std::errc()) p = res.ptr;
+ else return "<overflow>";
+ }
+ p = std::min (end - 1, p);
+ *p = '\0';
+ return buff;
+}
+
+static void
+subset_sum (SplitInfoVec::const_iterator iter,
+ SplitInfoVec::const_iterator end,
+ SplitInfoVec& path, Solution& solution,
+ int64_t target, RuntimeMonitor& monitor)
+{
+ DEBUG ("this=%" PRId64" target=%" PRId64 " path=%s",
+ iter == end ? 0 : iter->m_amount, target, path_to_str (path));
+
+ if (target == 0)
+ {
+ DEBUG ("SOLUTION FOUND: %s%s", path_to_str (path),
+ solution.splits.empty() ? "" : " ABORT: AMBIGUOUS");
+ if (!solution.splits.empty())
+ solution.abort = true;
+ else
+ {
+ solution.splits.resize (path.size());
+ std::transform (path.begin(), path.end(), solution.splits.begin(),
+ [](SplitInfo& i){ return i.m_split; });
+ return;
+ }
+ }
+
+ if (solution.abort || iter == end)
+ return;
+
+ if (monitor.should_abort())
+ {
+ DEBUG ("ABORT: timeout");
+ solution.abort = true;
+ return;
+ }
+
+ auto next = std::next(iter);
+
+ // 1st path: use current_num
+ path.push_back (*iter);
+ subset_sum (next, end, path, solution, target - iter->m_amount, monitor);
+ path.pop_back ();
+
+ // 2nd path: skip current_num
+ subset_sum (next, end, path, solution, target, monitor);
+}
+
+GList *
+gnc_account_get_autoclear_splits (Account *account, gnc_numeric toclear_value,
+ time64 end_date, GError **error,
+ double max_seconds)
+{
+ g_return_val_if_fail (account && error, nullptr);
+
+ auto scu{xaccAccountGetCommoditySCU (account)};
+ auto numeric_to_int64 = [scu](gnc_numeric num) -> int64_t
+ {
+ return gnc_numeric_num
+ (gnc_numeric_convert
+ (num, scu, GNC_HOW_DENOM_EXACT | GNC_HOW_RND_NEVER));
+ };
+
+ int64_t target{numeric_to_int64 (toclear_value)};
+ SplitInfoVec splits;
+ for (auto split : xaccAccountGetSplits (account))
+ {
+ if (xaccTransGetDate (xaccSplitGetParent (split)) > end_date)
+ break;
+ int64_t amt{numeric_to_int64 (xaccSplitGetAmount (split))};
+ if (xaccSplitGetReconcile (split) != NREC)
+ target -= amt;
+ else if (amt == 0)
+ DEBUG ("skipping zero-amount split %p", split);
+ else
+ splits.push_back ({amt, split});
+ }
+
+ static GQuark autoclear_quark = g_quark_from_static_string ("autoclear");
+ if (target == 0)
+ {
+ g_set_error (error, autoclear_quark, 1, "%s",
+ "Account is already at Auto-Clear Balance.");
+ return nullptr;
+ }
+
+ RuntimeMonitor monitor{max_seconds};
+ Solution solution;
+ SplitInfoVec path;
+ path.reserve (splits.size());
+
+ subset_sum (splits.begin(), splits.end(), path, solution, target, monitor);
+
+ DEBUG ("finished subset_sum in %f seconds", monitor.get_elapsed());
+
+ if (solution.splits.empty() || solution.abort)
+ {
+ g_set_error (error, autoclear_quark, 1, "%s",
+ "The selected amount cannot be cleared.");
+ return nullptr;
+ }
+
+ return std::accumulate
+ (solution.splits.begin(), solution.splits.end(),
+ static_cast<GList*>(nullptr), g_list_prepend);
+}
diff --git a/gnucash/gnome-utils/gnc-autoclear.h b/libgnucash/app-utils/gnc-autoclear.h
similarity index 67%
rename from gnucash/gnome-utils/gnc-autoclear.h
rename to libgnucash/app-utils/gnc-autoclear.h
index 1f53629748..dcdc42b185 100644
--- a/gnucash/gnome-utils/gnc-autoclear.h
+++ b/libgnucash/app-utils/gnc-autoclear.h
@@ -26,7 +26,6 @@
#include <glib.h>
#include <stdint.h>
-#include <gtk/gtk.h>
#include <Account.h>
#ifdef __cplusplus
@@ -35,18 +34,18 @@ extern "C" {
/** Account splits are analysed; attempts to find a unique combination
* of uncleared splits which would set cleared balance to
- * toclear_value. If this is not possible, *errmsg will be error
- * message. errmsg must be a pointer to a gchar. If it is set, it
- * must be freed by the caller.
+ * toclear_value. If this is not possible, GError will be error
+ * message.
+ * @param account: account whose unreconciled splits must be assessed.
+ * @param toclear_value: gnc_numeric target cleared balance
+ * @param end_date: latest date for splits to be assesed
+ * @param error: a GError* to collect error conditions
+ * @param max_seconds: timeout limit. 0 or less will disable timeout monitor.
*/
-GList * gnc_account_get_autoclear_splits (Account *account, gnc_numeric toclear_value,
- gchar **errmsg);
-
-/* same as above, but returns TRUE if successful, and FALSE if
- unsuccessful and sets GError appropriately */
-gboolean gnc_autoclear_get_splits (Account *account, gnc_numeric toclear_value,
- time64 end_date,
- GList **splits, GError **error, GtkLabel *label);
+GList * gnc_account_get_autoclear_splits (Account *account,
+ gnc_numeric toclear_value,
+ time64 end_date, GError **error,
+ double max_seconds);
#ifdef __cplusplus
}
diff --git a/libgnucash/app-utils/test/CMakeLists.txt b/libgnucash/app-utils/test/CMakeLists.txt
index c8143a333e..08c6a80a02 100644
--- a/libgnucash/app-utils/test/CMakeLists.txt
+++ b/libgnucash/app-utils/test/CMakeLists.txt
@@ -54,6 +54,27 @@ set(test_gnc_quotes_LIBS
gnc_add_test(test-gnc-quotes "${test_gnc_quotes_SOURCES}" test_gnc_quotes_INCLUDES test_gnc_quotes_LIBS
"GTEST_FILTER=-GncQuotesTest.online_wiggle")
+
+set(test_autoclear_SOURCES
+ test-autoclear.cpp
+)
+
+set(test_autoclear_INCLUDE_DIRS
+ ${CMAKE_BINARY_DIR}/common
+ ${CMAKE_SOURCE_DIR}/libgnucash/engine
+)
+
+set(test_autoclear_LIBS
+ gnc-engine
+ gnc-gnome-utils
+ gtest
+)
+
+gnc_add_test(test-autoclear "${test_autoclear_SOURCES}"
+ test_autoclear_INCLUDE_DIRS
+ test_autoclear_LIBS
+)
+
set(GUILE_DEPENDS
scm-test-engine
scm-app-utils
@@ -71,4 +92,5 @@ set_dist_list(test_app_utils_DIST
test-sx.cpp
${test_app_utils_scheme_SOURCES}
${test_app_utils_SOURCES}
+ ${test_autoclear_SOURCES}
)
diff --git a/gnucash/gnome-utils/test/test-autoclear.cpp b/libgnucash/app-utils/test/test-autoclear.cpp
similarity index 71%
rename from gnucash/gnome-utils/test/test-autoclear.cpp
rename to libgnucash/app-utils/test/test-autoclear.cpp
index bd42895f2e..5ba76ccd25 100644
--- a/gnucash/gnome-utils/test/test-autoclear.cpp
+++ b/libgnucash/app-utils/test/test-autoclear.cpp
@@ -102,14 +102,84 @@ TestCase ambiguousTestCase = {
{ "Memo 03", -10, false },
},
.tests = {
- { -10, "Cannot uniquely clear splits. Found multiple possibilities." },
- { -20, "Cannot uniquely clear splits. Found multiple possibilities." },
+ { -10, "The selected amount cannot be cleared." },
+ { -20, "The selected amount cannot be cleared." },
// -30 can be cleared by returning all three -10 splits
{ -30, nullptr },
},
};
+
+TestCase sequentialTestCase1 =
+{
+ .splits = {
+ { "Memo 01", 2, false },
+ { "Memo 02", 3, false },
+ { "Memo 03", 5, false },
+ },
+ .tests = {
+ { 1, "The selected amount cannot be cleared." },
+ { 4, "The selected amount cannot be cleared." },
+ { 5, "The selected amount cannot be cleared." },
+ { 6, "The selected amount cannot be cleared." },
+ { 9, "The selected amount cannot be cleared." },
+ { 11, "The selected amount cannot be cleared." },
+ { 12, "The selected amount cannot be cleared." },
+ { 2, nullptr },
+ { 7, nullptr },
+ { 10, nullptr },
+ },
+};
+
+TestCase sequentialTestCase2 =
+{
+ .splits = {
+ { "Memo 01", 2, false },
+ { "Memo 02", 3, false },
+ { "Memo 03", 5, false },
+ },
+ .tests = {
+ { 3, nullptr },
+ { 8, nullptr },
+ { 10, nullptr },
+ },
+};
+
+
+TestCase sequentialTestCase3 =
+{
+ .splits = {
+ { "Memo 01", 2, false },
+ { "Memo 02", 3, false },
+ { "Memo 03", 5, false },
+ { "Memo 04", 5, false },
+ },
+ .tests = {
+ { 5, "The selected amount cannot be cleared." },
+ { 7, "The selected amount cannot be cleared." },
+ { 10, "The selected amount cannot be cleared." },
+ { 2, nullptr },
+ { 12, nullptr },
+ { 15, nullptr },
+ },
+};
+
+TestCase sequentialTestCase4 =
+{
+ .splits = {
+ { "Memo 01", 2, false },
+ { "Memo 02", 3, false },
+ { "Memo 03", 5, false },
+ { "Memo 04", 5, false },
+ },
+ .tests = {
+ { 3, nullptr },
+ { 13, nullptr },
+ { 15, nullptr },
+ },
+};
+
class AutoClearTest : public ::testing::TestWithParam<TestCase *> {
protected:
std::shared_ptr<QofBook> m_book;
@@ -122,6 +192,7 @@ public:
m_account(xaccMallocAccount(m_book.get())),
m_testCase(*GetParam())
{
+ std::cout << "\n\ncreating new account with splits";
xaccAccountSetName(m_account, "Test Account");
xaccAccountBeginEdit(m_account);
for (auto &d : m_testCase.splits) {
@@ -130,19 +201,28 @@ public:
xaccSplitSetAmount(split, gnc_numeric_create(d.amount, DENOM));
xaccSplitSetReconcile(split, d.cleared ? CREC : NREC);
xaccSplitSetAccount(split, m_account);
+ std::cout << ' ' << d.amount;
gnc_account_insert_split(m_account, split);
}
xaccAccountCommitEdit(m_account);
+ std::cout << std::endl;
}
};
TEST_P(AutoClearTest, DoesAutoClear) {
for (auto &t : m_testCase.tests) {
gnc_numeric amount_to_clear = gnc_numeric_create(t.amount, DENOM);
- char *err;
+ GError *error = nullptr;
+
+ GList *splits_to_clear = gnc_account_get_autoclear_splits
+ (m_account, amount_to_clear, INT64_MAX, &error, 0);
+
+ auto err = error ? error->message : nullptr;
- GList *splits_to_clear = gnc_account_get_autoclear_splits(m_account, amount_to_clear, &err);
+ std::cout << "testing clearing " << t.amount
+ << " should be [" << std::string (err ? err : "ok") << ']'
+ << std::endl;
// Actually clear splits
for (GList *node = splits_to_clear; node; node = node->next) {
@@ -152,13 +232,14 @@ TEST_P(AutoClearTest, DoesAutoClear) {
g_list_free (splits_to_clear);
- ASSERT_STREQ(err, t.expectedErr);
+ EXPECT_STREQ(err, t.expectedErr);
if (t.expectedErr == NULL) {
gnc_numeric c = xaccAccountGetClearedBalance(m_account);
- ASSERT_EQ(c.num, t.amount);
- ASSERT_EQ(c.denom, DENOM);
+ EXPECT_EQ(c.num, t.amount);
+ EXPECT_EQ(c.denom, DENOM);
}
- g_free (err);
+ if (error)
+ g_error_free (error);
}
}
@@ -175,6 +256,10 @@ INSTANTIATE_TEST_SUITE_P(
AutoClearTest,
::testing::Values(
&easyTestCase,
+ &sequentialTestCase1,
+ &sequentialTestCase2,
+ &sequentialTestCase3,
+ &sequentialTestCase4,
&ambiguousTestCase
)
);
diff --git a/po/POTFILES.in b/po/POTFILES.in
index 0296ee112f..e521993ae9 100644
--- a/po/POTFILES.in
+++ b/po/POTFILES.in
@@ -147,7 +147,6 @@ gnucash/gnome-utils/dialog-userpass.c
gnucash/gnome-utils/dialog-utils.c
gnucash/gnome-utils/gnc-account-sel.c
gnucash/gnome-utils/gnc-amount-edit.c
-gnucash/gnome-utils/gnc-autoclear.c
gnucash/gnome-utils/gnc-autosave.c
gnucash/gnome-utils/gnc-cell-renderer-text-flag.c
gnucash/gnome-utils/gnc-cell-renderer-text-view.c
@@ -536,6 +535,7 @@ libgnucash/app-utils/file-utils.c
libgnucash/app-utils/gfec.c
libgnucash/app-utils/gnc-account-merge.c
libgnucash/app-utils/gnc-addr-quickfill.c
+libgnucash/app-utils/gnc-autoclear.cpp
libgnucash/app-utils/gnc-entry-quickfill.c
libgnucash/app-utils/gnc-exp-parser.c
libgnucash/app-utils/gnc-gsettings.cpp
Summary of changes:
gnucash/gnome-utils/CMakeLists.txt | 2 -
gnucash/gnome-utils/gnc-autoclear.c | 226 ---------------------
gnucash/gnome-utils/test/CMakeLists.txt | 23 +--
gnucash/gnome/window-autoclear.c | 7 +-
gnucash/gnome/window-reconcile.cpp | 36 ++++
libgnucash/app-utils/CMakeLists.txt | 2 +
libgnucash/app-utils/gnc-autoclear.cpp | 210 +++++++++++++++++++
.../app-utils}/gnc-autoclear.h | 23 +--
libgnucash/app-utils/test/CMakeLists.txt | 22 ++
.../app-utils}/test/test-autoclear.cpp | 101 ++++++++-
po/POTFILES.in | 2 +-
11 files changed, 381 insertions(+), 273 deletions(-)
delete mode 100644 gnucash/gnome-utils/gnc-autoclear.c
create mode 100644 libgnucash/app-utils/gnc-autoclear.cpp
rename {gnucash/gnome-utils => libgnucash/app-utils}/gnc-autoclear.h (67%)
rename {gnucash/gnome-utils => libgnucash/app-utils}/test/test-autoclear.cpp (71%)
More information about the gnucash-changes
mailing list