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