diff --git a/gnucash/gnome-utils/gnc-autoclear.c b/gnucash/gnome-utils/gnc-autoclear.c index fbd5a2dffa..05a370e525 100644 --- a/gnucash/gnome-utils/gnc-autoclear.c +++ b/gnucash/gnome-utils/gnc-autoclear.c @@ -2,6 +2,7 @@ * gnc-autoclear.c -- Knapsack algorithm functions * * * * Copyright 2020 Cristian Klein * + * 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 * @@ -27,6 +28,7 @@ #include "Account.h" #include "Split.h" +#include "Transaction.h" #include "gncOwner.h" #include "qof.h" #include "gnc-autoclear.h" @@ -43,36 +45,78 @@ typedef enum #define MAXIMUM_SACK_SIZE 1000000 +#define log_module "autoclear" + static gboolean -ght_gnc_numeric_equal(gconstpointer v1, gconstpointer v2) +numeric_equal (gnc_numeric *n1, gnc_numeric *n2) { - gnc_numeric n1 = *(gnc_numeric *)v1, n2 = *(gnc_numeric *)v2; - return gnc_numeric_equal(n1, n2); + return gnc_numeric_equal (*n1, *n2); } static guint -ght_gnc_numeric_hash(gconstpointer v1) +numeric_hash (gnc_numeric *n1) { - gnc_numeric n1 = *(gnc_numeric *)v1; - gdouble d1 = gnc_numeric_to_double(n1); + gdouble d1 = gnc_numeric_to_double (*n1); return g_double_hash (&d1); } -typedef struct _sack_foreach_data_t +typedef struct { - gnc_numeric split_value; - GList *reachable_list; -} *sack_foreach_data_t; + GList *worklist; + GHashTable *sack; + Split *split; +} sack_data; -static void sack_foreach_func(gpointer key, gpointer value, gpointer user_data) +typedef struct { - sack_foreach_data_t data = (sack_foreach_data_t) user_data; - gnc_numeric thisvalue = *(gnc_numeric *) key; - gnc_numeric reachable_value = gnc_numeric_add_fixed (thisvalue, data->split_value); - gnc_numeric* new_value = g_new(gnc_numeric, 1); + gnc_numeric reachable_amount; + GList *list_of_splits; +} WorkItem; - *new_value = reachable_value; - data->reachable_list = g_list_prepend(data->reachable_list, new_value); +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, &amount) || 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 (gnc_numeric *thisvalue, GList *splits, sack_data *data) +{ + gnc_numeric itemval = xaccSplitGetAmount (data->split); + gnc_numeric new_value = gnc_numeric_add_fixed (*thisvalue, itemval); + WorkItem *item = make_workitem (data->sack, new_value, data->split, splits); + + data->worklist = g_list_prepend (data->worklist, item); +} + +static void +sack_free (gnc_numeric *thisvalue, GList *splits, sack_data *data) +{ + g_free (thisvalue); + if (splits != DUP_LIST) + g_list_free (splits); +} + +static void +process_work (WorkItem *item, GHashTable *sack) +{ + GList *existing = g_hash_table_lookup (sack, &item->reachable_amount); + 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, &item->reachable_amount, item->list_of_splits); } gboolean @@ -82,25 +126,29 @@ gnc_autoclear_get_splits (Account *account, gnc_numeric toclear_value, { GList *nc_list = NULL, *toclear_list = NULL; GHashTable *sack; - gboolean success = FALSE; GQuark autoclear_quark = g_quark_from_static_string ("autoclear"); - guint sack_size = 0; g_return_val_if_fail (GNC_IS_ACCOUNT (account), FALSE); + g_return_val_if_fail (splits != NULL, FALSE); - sack = g_hash_table_new_full (ght_gnc_numeric_hash, ght_gnc_numeric_equal, - g_free, NULL); + sack = g_hash_table_new ((GHashFunc) numeric_hash, (GEqualFunc) numeric_equal); + DUP_LIST = g_list_prepend (NULL, NULL); /* Extract which splits are not cleared and compute the amount we have to clear */ for (GList *node = xaccAccountGetSplitList (account); node; node = node->next) { Split *split = (Split *)node->data; - if (xaccSplitGetReconcile (split) == NREC) - nc_list = g_list_prepend (nc_list, split); - else + if (xaccSplitGetReconcile (split) != NREC) toclear_value = gnc_numeric_sub_fixed (toclear_value, xaccSplitGetAmount (split)); + else if (gnc_numeric_zero_p (xaccSplitGetAmount (split))) + 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); } if (gnc_numeric_zero_p (toclear_value)) @@ -109,95 +157,55 @@ gnc_autoclear_get_splits (Account *account, gnc_numeric toclear_value, _("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; + } - /* Run knapsack */ - /* Entries in the hash table are: - * - key = amount to which we know how to clear (freed by GHashTable) - * - value = last split we used to clear this amount (not managed by GHashTable) - */ for (GList *node = nc_list; node; node = node->next) { Split *split = (Split *)node->data; - gnc_numeric split_value = xaccSplitGetAmount (split); - gnc_numeric *new_value = g_new(gnc_numeric, 1); + WorkItem *item = make_workitem (sack, xaccSplitGetAmount (split), split, NULL); + sack_data s_data = { g_list_prepend (NULL, item), sack, split }; - struct _sack_foreach_data_t s_data[1]; - s_data->split_value = split_value; - s_data->reachable_list = NULL; - - /* For each value in the sack, compute a new reachable value */ - g_hash_table_foreach (sack, sack_foreach_func, s_data); - - /* Add the value of the split itself to the reachable_list */ - *new_value = split_value; - s_data->reachable_list = g_list_prepend - (s_data->reachable_list, new_value); - - /* Add everything to the sack, looking out for duplicates */ - for (GList *s_node = s_data->reachable_list; s_node; s_node = s_node->next) + 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) { - gnc_numeric *reachable_value = s_node->data; - - /* Check if it already exists */ - if (g_hash_table_lookup_extended (sack, reachable_value, NULL, NULL)) - { - /* If yes, we are in trouble, we reached an amount - using two solutions */ - g_hash_table_insert (sack, reachable_value, NULL); - } - else - { - g_hash_table_insert (sack, reachable_value, split); - sack_size++; - - if (sack_size > MAXIMUM_SACK_SIZE) - { - g_set_error (error, autoclear_quark, AUTOCLEAR_OVERLOAD, - _("Too many uncleared splits")); - goto skip_knapsack; - } - } - } - g_list_free (s_data->reachable_list); - } - - /* Check solution */ - while (!gnc_numeric_zero_p (toclear_value)) - { - Split *split = NULL; - - if (!g_hash_table_lookup_extended (sack, &toclear_value, - NULL, (gpointer) &split)) - { - g_set_error (error, autoclear_quark, AUTOCLEAR_UNABLE, - _("The selected amount cannot be cleared.")); + g_set_error (error, autoclear_quark, AUTOCLEAR_OVERLOAD, + _("Too many uncleared splits")); goto skip_knapsack; } - - if (!split) + else if (g_hash_table_lookup (sack, &toclear_value) == DUP_LIST) { g_set_error (error, autoclear_quark, AUTOCLEAR_MULTIPLE, _("Cannot uniquely clear splits. Found multiple possibilities.")); goto skip_knapsack; } - - toclear_list = g_list_prepend (toclear_list, split); - toclear_value = gnc_numeric_sub_fixed (toclear_value, - xaccSplitGetAmount (split)); } - success = TRUE; + + toclear_list = g_hash_table_lookup (sack, &toclear_value); + + /* 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); - if (!success) - { - g_list_free (toclear_list); - toclear_list = NULL; - } - - *splits = toclear_list; return (toclear_list != NULL); } @@ -209,8 +217,8 @@ gnc_account_get_autoclear_splits (Account *account, gnc_numeric toclear_value, GError *error = NULL; GList *splits = NULL; - gnc_autoclear_get_splits (account, toclear_value, - &splits, &error); + gnc_autoclear_get_splits (account, toclear_value, INT64_MAX, + &splits, &error, NULL); if (error) { diff --git a/gnucash/gnome-utils/test/test-autoclear.cpp b/gnucash/gnome-utils/test/test-autoclear.cpp index 288074e900..6a41b267ca 100644 --- a/gnucash/gnome-utils/test/test-autoclear.cpp +++ b/gnucash/gnome-utils/test/test-autoclear.cpp @@ -100,9 +100,8 @@ TestCase ambiguousTestCase = { { -10, "Cannot uniquely clear splits. Found multiple possibilities." }, { -20, "Cannot uniquely clear splits. Found multiple possibilities." }, - // Forbid auto-clear to be too smart. We expect the user to manually deal - // with such situations. - { -30, "Cannot uniquely clear splits. Found multiple possibilities." }, + // -30 can be cleared by returning all three -10 splits + { -30, nullptr }, }, };