gnucash future: Multiple changes pushed

John Ralls jralls at code.gnucash.org
Sat Jul 18 16:53:10 EDT 2026


Updated	 via  https://github.com/Gnucash/gnucash/commit/0e53aff8 (commit)
	 via  https://github.com/Gnucash/gnucash/commit/85646ea0 (commit)
	 via  https://github.com/Gnucash/gnucash/commit/770fd5af (commit)
	from  https://github.com/Gnucash/gnucash/commit/458b2ea4 (commit)



commit 0e53aff804ada073bfaae88f46cb7e737d52e345
Merge: 458b2ea4c4 85646ea094
Author: John Ralls <jralls at ceridwen.us>
Date:   Sat Jul 18 13:49:37 2026 -0700

    Merge Brent McBride's 'gnc-glib-utils-cpp' into future.


commit 85646ea0946fefd451bd4890f6b761990bcbb23d
Author: Brent McBride <mcbridebt at hotmail.com>
Date:   Tue Jul 14 22:49:26 2026 -0700

    Modernize gnc-string-utils to C++ and convert its test to Googletest
    
    Convert the string utilities from GLib to boost::locale: safe_utf8_collate
    compares with a boost::locale::collator, gnc_locale_from_utf8/to_utf8 use the
    boost::locale info facet for the charset (short-circuiting UTF-8 locales), and
    utf8_strstr normalizes with boost::locale::normalize.
    
    These functions are reached from low-level engine code such as
    xaccAccountOrder -- including from the Python bindings and unit tests -- that
    runs before gnucash-core-app calls gnc_init_boost_locale.  gnc_get_boost_locale
    therefore now lazily creates a facet-equipped locale on first use, while still
    letting gnc_init_boost_locale install message catalogs afterwards.
    
    The test is converted to Googletest and pins LC_ALL=C so the conversion path
    and its failure handling are exercised deterministically.

diff --git a/libgnucash/core-utils/gnc-locale-utils.cpp b/libgnucash/core-utils/gnc-locale-utils.cpp
index fe99ffaf37..9d57203cb6 100644
--- a/libgnucash/core-utils/gnc-locale-utils.cpp
+++ b/libgnucash/core-utils/gnc-locale-utils.cpp
@@ -1,6 +1,7 @@
 /********************************************************************\
  * gnc-locale-utils.cpp -- provide a default locale for C++         *
  * Copyright (C) 2019 John Ralls <jralls at ceridwen.us                *
+ * Copyright (C) 2026 Brent McBride <mcbridebt at hotmail.com>         *
  *                                                                  *
  * This program is free software; you can redistribute it and/or    *
  * modify it under the terms of the GNU General Public License as   *
@@ -71,39 +72,54 @@ gnc_get_locale()
 
 
 static std::locale boost_cached;
-static bool tried_boost_already = false;
+static bool boost_locale_created = false;
+static bool boost_messages_loaded = false;
+
+/* Build a boost::locale from the environment.  Message catalogs are loaded
+ * only when a path is supplied; the collation, character-set and other
+ * facets are always present so that gnc_get_boost_locale() is usable even
+ * before translations have been set up. */
+static std::locale
+create_boost_locale (const std::string& messages_path)
+{
+    try
+    {
+        boost::locale::generator gen;
+        if (!messages_path.empty())
+            gen.add_messages_path(messages_path);
+        gen.add_messages_domain(PROJECT_NAME);
+        return gen ("");
+    }
+    catch (const std::runtime_error& err)
+    {
+        const char* locale = setlocale(LC_ALL, "");
+
+        g_log(G_LOG_DOMAIN, G_LOG_LEVEL_WARNING,
+              "Failed to create C++ default locale from "
+              "%s because %s. Using the 'C' locale for C++.",
+              locale, err.what());
+        return std::locale::classic();
+    }
+}
 
 void
 gnc_init_boost_locale (const std::string& messages_path)
 {
-    if (!tried_boost_already)
-    {
-        tried_boost_already = true;
+    /* Once message catalogs are loaded there is nothing more to do; a locale
+     * that was created lazily (without catalogs) is still replaced here. */
+    if (boost_messages_loaded)
+        return;
 
-        try
-        {
-            boost::locale::generator gen;
-            if (!messages_path.empty())
-                gen.add_messages_path(messages_path);
-            else
-                g_log(G_LOG_DOMAIN, G_LOG_LEVEL_WARNING,
-                      "Attempt to initialize boost_locale without a message_path. "
-                      "If message catalogs are not installed in the system's default locations "
-                      "user interface strings will not be translated.");
-            gen.add_messages_domain(PROJECT_NAME);
-            boost_cached = gen ("");
-        }
-        catch (const std::runtime_error& err)
-        {
-            const char* locale = setlocale(LC_ALL, "");
+    if (messages_path.empty())
+        g_log(G_LOG_DOMAIN, G_LOG_LEVEL_WARNING,
+              "Attempt to initialize boost_locale without a message_path. "
+              "If message catalogs are not installed in the system's default locations "
+              "user interface strings will not be translated.");
 
-            g_log(G_LOG_DOMAIN, G_LOG_LEVEL_WARNING,
-                  "Failed to create C++ default locale from"
-                  "%s because %s. Using the 'C' locale for C++.",
-                  locale, err.what());
-            boost_cached = std::locale::classic();
-        }
-    }
+    boost_cached = create_boost_locale (messages_path);
+    boost_locale_created = true;
+    if (!messages_path.empty())
+        boost_messages_loaded = true;
 }
 
 
@@ -111,6 +127,16 @@ gnc_init_boost_locale (const std::string& messages_path)
 const std::locale&
 gnc_get_boost_locale()
 {
+    if (!boost_locale_created)
+    {
+        /* A consumer needs boost::locale facets before the application
+         * initialized them -- e.g. the Python bindings, a unit test, or
+         * engine code such as xaccAccountOrder().  Create a usable locale
+         * now; message catalogs, which require a path, are installed later
+         * by gnc_init_boost_locale(). */
+        boost_cached = create_boost_locale ("");
+        boost_locale_created = true;
+    }
     return boost_cached;
 }
 
diff --git a/libgnucash/core-utils/gnc-string-utils.cpp b/libgnucash/core-utils/gnc-string-utils.cpp
index d287485a77..7be5850caa 100644
--- a/libgnucash/core-utils/gnc-string-utils.cpp
+++ b/libgnucash/core-utils/gnc-string-utils.cpp
@@ -1,6 +1,7 @@
 /********************************************************************\
- * gnc-glib-utils.c -- utility functions based on glib functions    *
+ * gnc-string-utils.cpp -- string and list utility functions        *
  * Copyright (C) 2006 David Hampton <hampton at employees.org>         *
+ * Copyright (C) 2026 Brent McBride <mcbridebt at hotmail.com>         *
  *                                                                  *
  * This program is free software; you can redistribute it and/or    *
  * modify it under the terms of the GNU General Public License as   *
@@ -22,12 +23,16 @@
 \********************************************************************/
 
 #include <config.h>
-#include <errno.h>
-#include <stdio.h>
-#include <signal.h>
-#include <string.h>
-#include <stdbool.h>
 
+#include <cstring>
+#include <string_view>
+
+#include <boost/locale/collator.hpp>
+#include <boost/locale/conversion.hpp>
+#include <boost/locale/encoding.hpp>
+#include <boost/locale/info.hpp>
+
+#include "gnc-locale-utils.hpp"
 #include "gnc-string-utils.h"
 
 #ifdef G_OS_WIN32
@@ -38,12 +43,16 @@ int
 safe_utf8_collate (const char * da, const char * db)
 {
     if (da && !(*da))
-        da = NULL;
+        da = nullptr;
     if (db && !(*db))
-        db = NULL;
+        db = nullptr;
 
     if (da && db)
-        return g_utf8_collate(da, db);
+    {
+        auto const& coll{std::use_facet<boost::locale::collator<char>>(
+            gnc_get_boost_locale())};
+        return coll.compare(boost::locale::collate_level::quaternary, da, db);
+    }
     if (da)
         return 1;
     if (db)
@@ -126,20 +135,18 @@ gnc_utf8_validate(const gchar  *str,
                   const gchar **end)
 {
 
-    const gchar *p;
-
-    g_return_val_if_fail (str != NULL, FALSE);
+    g_return_val_if_fail (str != nullptr, false);
 
     if (end)
         *end = str;
 
-    p = str;
+    const gchar *p = str;
 
     while ((max_len < 0 || (p - str) < max_len) && *p)
     {
         int i, mask = 0, len;
         gunichar result;
-        unsigned char c = (unsigned char) * p;
+        unsigned char c = static_cast<unsigned char>(*p);
 
         UTF8_COMPUTE (c, mask, len);
 
@@ -156,7 +163,7 @@ gnc_utf8_validate(const gchar  *str,
         if (UTF8_LENGTH (result) != len) /* Check for overlong UTF-8 */
             break;
 
-        if (result == (gunichar) - 1)
+        if (result == static_cast<gunichar>(-1))
             break;
 
         if (!UNICODE_VALID (result))
@@ -173,32 +180,31 @@ gnc_utf8_validate(const gchar  *str,
      */
     if (max_len >= 0 &&
             p != (str + max_len))
-        return FALSE;
+        return false;
     else if (max_len < 0 &&
              *p != '\0')
-        return FALSE;
+        return false;
     else
-        return TRUE;
+        return true;
 }
 
 void
 gnc_utf8_strip_invalid (gchar *str)
 {
     gchar *end;
-    gint len;
 
     g_return_if_fail(str);
 
-    if (gnc_utf8_validate(str, -1, (const gchar **)&end))
+    if (gnc_utf8_validate(str, -1, const_cast<const gchar **>(&end)))
         return;
 
     g_warning("Invalid utf8 string: %s", str);
     do
     {
-        len = strlen(end);
+        int len = strlen(end);
         memmove(end, end + 1, len);	/* shuffle the remainder one byte */
     }
-    while (!gnc_utf8_validate(str, -1, (const gchar **)&end));
+    while (!gnc_utf8_validate(str, -1, const_cast<const gchar **>(&end)));
 }
 
 gchar *
@@ -212,14 +218,13 @@ gnc_utf8_strip_invalid_strdup(const gchar* str)
 void
 gnc_utf8_strip_invalid_and_controls (gchar *str)
 {
-    gchar *c = NULL;
-    const gchar *controls = "\b\f\n\r\t\v";
-    g_return_if_fail (str != NULL && strlen (str) > 0);
+    const char *controls = "\b\f\n\r\t\v";
+    g_return_if_fail (str != nullptr && strlen (str) > 0);
     gnc_utf8_strip_invalid (str); /* First fix the UTF-8 */
-    for(c = str + strlen (str) - 1; c != str; --c)
+    for (gchar *c = str + strlen (str) - 1; c != str; --c)
     {
-        gboolean line_control = ((unsigned char)(*c) < 0x20);
-        if (line_control || strchr(controls, *c) != NULL)
+        bool line_control = (static_cast<unsigned char>(*c) < 0x20);
+        if (line_control || strchr(controls, *c) != nullptr)
             *c = ' '; /*replace controls with a single space. */
     }
 }
@@ -227,44 +232,54 @@ gnc_utf8_strip_invalid_and_controls (gchar *str)
 gchar *
 gnc_locale_from_utf8(const gchar* str)
 {
-    gchar *   locale_str;
-    gsize     bytes_written = 0;
-    GError *  err = NULL;
-
-    /* Convert from UTF-8 to the encoding used in the current locale. */
-    locale_str = g_locale_from_utf8(str, -1, NULL, &bytes_written, &err);
-    if (err)
+    g_return_val_if_fail (str != nullptr, nullptr);
+
+    // Convert from UTF-8 to the encoding used in the current locale.
+    auto const& info{std::use_facet<boost::locale::info> (
+        gnc_get_boost_locale ())};
+    if (info.utf8 ())
+        return g_strdup (str);
+    try
     {
-        g_warning("g_locale_from_utf8 failed: %s", err->message);
-        g_error_free(err);
+        auto locale_str = boost::locale::conv::from_utf<char> (
+            str, info.encoding (), boost::locale::conv::stop);
+        return g_strdup (locale_str.c_str ());
+    }
+    catch (const std::exception& err)
+    {
+        g_warning ("gnc_locale_from_utf8 failed: %s", err.what ());
+        return nullptr;
     }
-
-    return locale_str;
 }
 
 gchar *
 gnc_locale_to_utf8(const gchar* str)
 {
-    gchar *   utf8_str;
-    gsize     bytes_written = 0;
-    GError *  err = NULL;
-
-    /* Convert to UTF-8 from the encoding used in the current locale. */
-    utf8_str = g_locale_to_utf8(str, -1, NULL, &bytes_written, &err);
-    if (err)
+    g_return_val_if_fail (str != nullptr, nullptr);
+
+    // Convert to UTF-8 from the encoding used in the current locale.
+    auto const& info{std::use_facet<boost::locale::info> (
+        gnc_get_boost_locale ())};
+    if (info.utf8 ())
+        return g_strdup (str);
+    try
     {
-        g_warning("g_locale_to_utf8 failed: %s", err->message);
-        g_error_free(err);
+        auto utf8_str = boost::locale::conv::to_utf<char> (
+            str, info.encoding (), boost::locale::conv::stop);
+        return g_strdup (utf8_str.c_str ());
+    }
+    catch (const std::exception& err)
+    {
+        g_warning ("gnc_locale_to_utf8 failed: %s", err.what ());
+        return nullptr;
     }
-
-    return utf8_str;
 }
 
 GList*
 gnc_g_list_map(GList* list, GncGMapFunc fn, gpointer user_data)
 {
-    GList *rtn = NULL;
-    for (; list != NULL; list = list->next)
+    GList *rtn = nullptr;
+    for (; list != nullptr; list = list->next)
     {
         rtn = g_list_prepend (rtn, (*fn)(list->data, user_data));
     }
@@ -274,58 +289,54 @@ gnc_g_list_map(GList* list, GncGMapFunc fn, gpointer user_data)
 void
 gnc_g_list_cut(GList **list, GList *cut_point)
 {
-    if (list == NULL || *list == NULL)
+    if (list == nullptr || *list == nullptr)
         return;
 
     // if it's the first element.
-    if (cut_point->prev == NULL)
+    if (cut_point->prev == nullptr)
     {
-        *list = NULL;
+        *list = nullptr;
         return;
     }
 
-    cut_point->prev->next = NULL;
-    cut_point->prev = NULL;
+    cut_point->prev->next = nullptr;
+    cut_point->prev = nullptr;
 }
 
 static bool
 utf8_strstr(char **needle, char *haystack)
 {
-    char *tmp = g_utf8_normalize (*needle, -1, G_NORMALIZE_NFC);
-    if (haystack && *haystack)
-    {
-        char *place = strstr(haystack, tmp);
-        if (place)
-        {
-            g_free (tmp);
-            return false;
-        }
-    }
-    *needle = tmp; //so that haystack is already normalized
+    auto tmp{boost::locale::normalize (*needle, boost::locale::norm_nfc,
+                                       gnc_get_boost_locale ())};
+    if (haystack && *haystack &&
+        std::string_view{haystack}.find (tmp) != std::string_view::npos)
+        return false;
+
+    *needle = g_strdup (tmp.c_str ()); //so that haystack is already normalized
     return true;
 }
 
 static gchar *
-gnc_g_list_stringjoin_internal (GList *list_of_strings, const gchar *sep, bool testdups)
+stringjoin_internal (GList *list_of_strings, const gchar *sep, bool testdups)
 {
-    gint seplen = sep ? strlen(sep) : 0;
+    gint seplen = sep ? strlen (sep) : 0;
     gint length = -seplen;
     gchar *retval, *p;
 
     for (GList *n = list_of_strings; n; n = n->next)
     {
-        gchar *str = n->data;
+        gchar *str = static_cast<gchar*>(n->data);
         if (str && *str)
             length += strlen (str) + seplen;
     }
 
     if (length <= 0)
-        return NULL;
+        return nullptr;
 
-    p = retval = (gchar*) g_malloc0 (length * sizeof (gchar) + 1);
+    p = retval = static_cast<gchar*>(g_malloc0 (length * sizeof (gchar) + 1));
     for (GList *n = list_of_strings; n; n = n->next)
     {
-        gchar *str = n->data;
+        gchar *str = static_cast<gchar*>(n->data);
         if (!str || !str[0])
             continue;
         if (!testdups || utf8_strstr (&str, retval))
@@ -344,19 +355,20 @@ gnc_g_list_stringjoin_internal (GList *list_of_strings, const gchar *sep, bool t
 gchar *
 gnc_g_list_stringjoin (GList *list_of_strings, const gchar *sep)
 {
-    return gnc_g_list_stringjoin_internal (list_of_strings, sep, false);
+    return stringjoin_internal (list_of_strings, sep, false);
 }
 
 gchar *
 gnc_g_list_stringjoin_nodups (GList *list_of_strings, const gchar *sep)
 {
-    return gnc_g_list_stringjoin_internal (list_of_strings, sep, true);
+    return stringjoin_internal (list_of_strings, sep, true);
 }
 
 gint
 gnc_list_length_cmp (const GList *list, size_t len)
 {
-    for (GList *lst = (GList*) list;; lst = g_list_next (lst), len--)
+    for (GList *lst = const_cast<GList*>(list);;
+         lst = g_list_next (lst), len--)
     {
         if (!lst) return (len ? -1 : 0);
         if (!len) return 1;
diff --git a/libgnucash/core-utils/gnc-string-utils.h b/libgnucash/core-utils/gnc-string-utils.h
index b0281bdbaf..6b9cc2c323 100644
--- a/libgnucash/core-utils/gnc-string-utils.h
+++ b/libgnucash/core-utils/gnc-string-utils.h
@@ -24,10 +24,9 @@
 
 /** @addtogroup GLib
     @{ */
-/** @addtogroup Helpers GLib Helpers
+/** @addtogroup Helpers String Helpers
 
-    The API in this file is designed to provide support functions that
-    wrap the base glib functions and make them easier to use.
+    The API in this file provides string and list utility functions.
 
     @{ */
 /** @file gnc-string-utils.h
diff --git a/libgnucash/core-utils/test/CMakeLists.txt b/libgnucash/core-utils/test/CMakeLists.txt
index 0416017fba..b5c92ffdf2 100644
--- a/libgnucash/core-utils/test/CMakeLists.txt
+++ b/libgnucash/core-utils/test/CMakeLists.txt
@@ -15,7 +15,15 @@ macro(add_core_utils_test _TARGET _SOURCE_FILES)
   gnc_add_test(${_TARGET} "${_SOURCE_FILES}" CORE_UTILS_TEST_INCLUDE_DIRS CORE_UTILS_TEST_LIBS)
 endmacro()
 
-add_core_utils_test(test-gnc-string-utils gtest-gnc-string-utils.cpp)
+set(gtest_string_utils_INCLUDES
+  ${CMAKE_BINARY_DIR}/common # for config.h
+  ${MODULEPATH}
+  ${CMAKE_SOURCE_DIR}/common/test-core)
+
+set(gtest_string_utils_LIBS gnc-core-utils test-core gtest)
+
+gnc_add_test(test-gnc-string-utils gtest-gnc-string-utils.cpp
+  gtest_string_utils_INCLUDES gtest_string_utils_LIBS)
 add_core_utils_test(test-resolve-file-path test-resolve-file-path.c)
 add_core_utils_test(test-userdata-dir test-userdata-dir.c)
 if (NOT MAC_INTEGRATION AND NOT WIN32)
diff --git a/libgnucash/core-utils/test/gtest-gnc-string-utils.cpp b/libgnucash/core-utils/test/gtest-gnc-string-utils.cpp
index d0eb1815ee..d74a4e3f75 100644
--- a/libgnucash/core-utils/test/gtest-gnc-string-utils.cpp
+++ b/libgnucash/core-utils/test/gtest-gnc-string-utils.cpp
@@ -1,6 +1,7 @@
 /********************************************************************
- * testmain.c: GLib g_test test execution file.			    *
- * Copyright 2011 John Ralls <jralls at ceridwen.us>		    *
+ * gtest-gnc-string-utils.cpp: Unit tests for gnc-string-utils.     *
+ * Copyright 2011 John Ralls <jralls at ceridwen.us>                   *
+ * Copyright 2026 Brent McBride <mcbridebt at hotmail.com>             *
  *                                                                  *
  * This program is free software; you can redistribute it and/or    *
  * modify it under the terms of the GNU General Public License as   *
@@ -22,150 +23,312 @@
 
 
 #include <config.h>
-#include <string.h>
+#include <cstring>
 #include <glib.h>
 #include <gnc-string-utils.h>
+#include <gnc-locale-utils.hpp>
 #include <unittest-support.h>
+#include <gtest/gtest.h>
+
+/* safe_utf8_collate compares with boost::locale::collator via
+ * gnc_get_boost_locale(), which returns a usable locale only after
+ * gnc_init_boost_locale() has run (done in main() for the application).
+ * Initialize it once for the whole test run.
+ *
+ * Pin the encoding to US-ASCII (LC_ALL=C) so that gnc_locale_from_utf8 and
+ * gnc_locale_to_utf8 exercise their conversion (and failure) path rather
+ * than the UTF-8 no-op short-circuit, deterministically on any host. */
+class BoostLocaleEnvironment : public ::testing::Environment
+{
+public:
+    void SetUp () override
+    {
+        g_setenv ("LC_ALL", "C", TRUE);
+        gnc_init_boost_locale ("");
+    }
+};
+
+static auto* const boost_locale_env =
+    ::testing::AddGlobalTestEnvironment (new BoostLocaleEnvironment);
+
+TEST(GncGlibUtils, safe_utf8_collate)
+{
+    EXPECT_EQ (0, safe_utf8_collate ("abc", "abc"));
+    EXPECT_LT (safe_utf8_collate ("abc", "abd"), 0);
+    EXPECT_GT (safe_utf8_collate ("abd", "abc"), 0);
+
+    /* Empty strings are treated the same as nullptr. */
+    EXPECT_EQ (0, safe_utf8_collate ("", ""));
+    EXPECT_EQ (0, safe_utf8_collate (nullptr, nullptr));
+    EXPECT_EQ (0, safe_utf8_collate ("", nullptr));
+    EXPECT_EQ (0, safe_utf8_collate (nullptr, ""));
+
+    /* When only one side is empty/nullptr the non-empty side sorts later. */
+    EXPECT_EQ (1, safe_utf8_collate ("abc", nullptr));
+    EXPECT_EQ (1, safe_utf8_collate ("abc", ""));
+    EXPECT_EQ (-1, safe_utf8_collate (nullptr, "abc"));
+    EXPECT_EQ (-1, safe_utf8_collate ("", "abc"));
+}
+
+TEST(GncGlibUtils, gnc_utf8_validate)
+{
+    const gchar *valid = "Hello, world";
+    const gchar *end = nullptr;
+
+    EXPECT_TRUE (gnc_utf8_validate (valid, -1, &end));
+    EXPECT_EQ (valid + strlen (valid), end);
+
+    /* max_len path: validate only a prefix of the string. */
+    EXPECT_TRUE (gnc_utf8_validate (valid, 5, &end));
+    EXPECT_EQ (valid + 5, end);
+
+    /* Invalid input: end is left pointing at the first bad byte. */
+    const gchar *invalid = "abc\xb2\xf3xyz";
+    EXPECT_FALSE (gnc_utf8_validate (invalid, -1, &end));
+    EXPECT_EQ (invalid + 3, end);
+
+    /* A multi-byte character (here "é" == 0xc3 0xa9) validates when whole,
+     * but fails when max_len splits it, with end left at the split point. */
+    const gchar *multibyte = "a\xc3\xa9";
+    EXPECT_TRUE (gnc_utf8_validate (multibyte, 3, &end));
+    EXPECT_EQ (multibyte + 3, end);
+    EXPECT_FALSE (gnc_utf8_validate (multibyte, 2, &end));
+    EXPECT_EQ (multibyte + 1, end);
+
+    /* A well-formed 6-byte lead byte (0xFC) followed by an invalid
+     * continuation byte decodes to (gunichar)-1 while UTF8_LENGTH still
+     * matches, exercising the result == -1 rejection rather than the
+     * overlong-length check. */
+    const gchar *bad_seq = "\xFC\x20";
+    EXPECT_FALSE (gnc_utf8_validate (bad_seq, -1, &end));
+    EXPECT_EQ (bad_seq, end);
+}
+
+TEST(GncGlibUtils, gnc_utf8_strip_invalid_strdup)
+{
+    /* Valid input yields an equal, independently-allocated copy. */
+    gchar *result = gnc_utf8_strip_invalid_strdup ("valid string");
+    EXPECT_STREQ ("valid string", result);
+    g_free (result);
+}
 
 static void
-test_gnc_utf8_strip_invalid_and_controls (gconstpointer data)
+check_strip_invalid_and_controls (const gchar *input)
 {
-    gchar *str = g_strdup (data);
+    gchar *str = g_strdup (input);
     const gchar *controls = "\b\f\n\r\t\v\x01\x02\x03\x04\x05\x06\x07"
         "\x08\x09\xa\xb\xc\xd\xe\xf\x10\x11\x12\x13\x14\x15\x16"
         "\x17\x18\x19\x1a\x1b\x1c\x1d\x1e\x1f";
-    char *msg1 = g_strdup_printf ("Invalid utf8 string: %s",
-                                  (const gchar*)data);
-    const GLogLevelFlags level = G_LOG_LEVEL_WARNING | G_LOG_FLAG_FATAL;
-    TestErrorStruct check = {level, NULL, msg1, 0};
-
-    guint handler = g_log_set_handler (NULL, level,
+    gchar *msg = g_strdup_printf ("Invalid utf8 string: %s", input);
+    const GLogLevelFlags level = static_cast<GLogLevelFlags>
+        (G_LOG_LEVEL_WARNING | G_LOG_FLAG_FATAL);
+    TestErrorStruct check = {level, nullptr, msg, 0};
+    guint handler = g_log_set_handler (nullptr, level,
                                        (GLogFunc)test_null_handler, &check);
-    g_test_log_set_fatal_handler((GTestLogFatalFunc)test_checked_handler,
-                                 &check);
 
     gnc_utf8_strip_invalid_and_controls (str);
-    g_assert_true (g_utf8_validate(str, -1, NULL) == TRUE);
-    g_assert_true (strpbrk(str, controls) == NULL);
-    g_assert_true (g_utf8_strlen(str, -1) > 0);
-    g_log_remove_handler (NULL, handler);
+    EXPECT_TRUE (g_utf8_validate (str, -1, nullptr));
+    EXPECT_EQ (nullptr, strpbrk (str, controls));
+    EXPECT_GT (g_utf8_strlen (str, -1), 0);
+
+    g_log_remove_handler (nullptr, handler);
     g_free (str);
-    g_free (msg1);
+    g_free (msg);
 }
 
-static void
-test_g_list_stringjoin (gconstpointer data)
+TEST(GncGlibUtils, gnc_utf8_strip_invalid_and_controls)
 {
-    GList *test = NULL;
-    gchar *ret;
+    check_strip_invalid_and_controls
+        ("Η γρήγορη καφέ αλεπού πήδηξε πάνω από την \xb2\xf3ργή σκύλο.");
+    check_strip_invalid_and_controls
+        ("Η γρήγορη καφέ αλεπού\bπήδηξε\nπάνω από\tτην αργή σκύλο.");
+}
+
+TEST(GncGlibUtils, gnc_locale_utf8_roundtrip)
+{
+    /* Plain ASCII round-trips through any locale encoding. */
+    const gchar *ascii = "Plain ASCII text 12345";
+    gchar *locale_str = gnc_locale_from_utf8 (ascii);
+    ASSERT_NE (nullptr, locale_str);
+    gchar *utf8_str = gnc_locale_to_utf8 (locale_str);
+    ASSERT_NE (nullptr, utf8_str);
+    EXPECT_STREQ (ascii, utf8_str);
+    g_free (locale_str);
+    g_free (utf8_str);
+}
 
-    ret = gnc_g_list_stringjoin (NULL, NULL);
-    g_assert_true (ret == NULL);
+TEST(GncGlibUtils, gnc_locale_conversion_failure)
+{
+    /* Input that cannot be converted makes boost::locale::conv throw with
+     * the stop method; the functions catch it, warn, and return nullptr.
+     * 0xff is invalid UTF-8 and non-ASCII, so both directions fail in any
+     * locale encoding. */
+    const gchar *invalid = "\xff\xfe";
+    const GLogLevelFlags level = static_cast<GLogLevelFlags>
+        (G_LOG_LEVEL_WARNING | G_LOG_FLAG_FATAL);
+    TestErrorStruct check = {level, nullptr, nullptr, 0};
+    guint handler = g_log_set_handler (nullptr, level,
+                                       (GLogFunc)test_null_handler, &check);
 
-    ret = gnc_g_list_stringjoin (NULL, ":");
-    g_assert_true (ret == NULL);
+    EXPECT_EQ (nullptr, gnc_locale_from_utf8 (invalid));
+    EXPECT_EQ (nullptr, gnc_locale_to_utf8 (invalid));
 
-    test = g_list_prepend (test, "one");
+    g_log_remove_handler (nullptr, handler);
+}
 
-    ret = gnc_g_list_stringjoin (test, NULL);
-    g_assert_cmpstr (ret, ==, "one");
+static gpointer
+add_offset_map_fn (gpointer data, gpointer user_data)
+{
+    return GINT_TO_POINTER (GPOINTER_TO_INT (data) +
+                            GPOINTER_TO_INT (user_data));
+}
+
+TEST(GncGlibUtils, gnc_g_list_map)
+{
+    GList *list = nullptr;
+    list = g_list_append (list, GINT_TO_POINTER (1));
+    list = g_list_append (list, GINT_TO_POINTER (2));
+    list = g_list_append (list, GINT_TO_POINTER (3));
+
+    GList *mapped = gnc_g_list_map (list, add_offset_map_fn,
+                                    GINT_TO_POINTER (10));
+    ASSERT_EQ (3u, g_list_length (mapped));
+    EXPECT_EQ (11, GPOINTER_TO_INT (g_list_nth_data (mapped, 0)));
+    EXPECT_EQ (12, GPOINTER_TO_INT (g_list_nth_data (mapped, 1)));
+    EXPECT_EQ (13, GPOINTER_TO_INT (g_list_nth_data (mapped, 2)));
+
+    g_list_free (list);
+    g_list_free (mapped);
+}
+
+TEST(GncGlibUtils, gnc_g_list_cut)
+{
+    /* A null or empty list is left untouched. */
+    GList *empty = nullptr;
+    gnc_g_list_cut (&empty, nullptr);
+    EXPECT_EQ (nullptr, empty);
+
+    GList *list = nullptr;
+    list = g_list_append (list, GINT_TO_POINTER (1));
+    list = g_list_append (list, GINT_TO_POINTER (2));
+    list = g_list_append (list, GINT_TO_POINTER (3));
+    list = g_list_append (list, GINT_TO_POINTER (4));
+
+    /* Cut at the third element: head keeps [1, 2], tail becomes [3, 4]. */
+    GList *tail = g_list_nth (list, 2);
+    gnc_g_list_cut (&list, tail);
+
+    ASSERT_EQ (2u, g_list_length (list));
+    EXPECT_EQ (1, GPOINTER_TO_INT (g_list_nth_data (list, 0)));
+    EXPECT_EQ (2, GPOINTER_TO_INT (g_list_nth_data (list, 1)));
+
+    ASSERT_EQ (2u, g_list_length (tail));
+    EXPECT_EQ (3, GPOINTER_TO_INT (g_list_nth_data (tail, 0)));
+    EXPECT_EQ (4, GPOINTER_TO_INT (g_list_nth_data (tail, 1)));
+
+    g_list_free (list);
+    g_list_free (tail);
+
+    /* Cutting at the first element clears the caller's list pointer. */
+    GList *head = g_list_append (nullptr, GINT_TO_POINTER (1));
+    GList *single = head;
+    gnc_g_list_cut (&single, head);
+    EXPECT_EQ (nullptr, single);
+    g_list_free (head);
+}
+
+TEST(GncGlibUtils, gnc_g_list_stringjoin)
+{
+    GList *test = nullptr;
+    gchar *ret;
+
+    EXPECT_EQ (nullptr, gnc_g_list_stringjoin (nullptr, nullptr));
+    EXPECT_EQ (nullptr, gnc_g_list_stringjoin (nullptr, ":"));
+
+    test = g_list_prepend (test, (gpointer)"one");
+
+    ret = gnc_g_list_stringjoin (test, nullptr);
+    EXPECT_STREQ ("one", ret);
     g_free (ret);
 
     ret = gnc_g_list_stringjoin (test, "");
-    g_assert_cmpstr (ret, ==, "one");
+    EXPECT_STREQ ("one", ret);
     g_free (ret);
 
     ret = gnc_g_list_stringjoin (test, ":");
-    g_assert_cmpstr (ret, ==, "one");
+    EXPECT_STREQ ("one", ret);
     g_free (ret);
 
-   /* The following inserts a NULL between "two" and "one". As a
+    /* The following inserts a nullptr between "two" and "one". As a
        result, the stringjoin effectively skips a step, i.e. it does
        not insert separator repeatedly between NULL strings */
-    test = g_list_prepend (test, NULL);
+    test = g_list_prepend (test, nullptr);
 
-    test = g_list_prepend (test, "two");
+    test = g_list_prepend (test, (gpointer)"two");
 
-    ret = gnc_g_list_stringjoin (test, NULL);
-    g_assert_cmpstr (ret, ==, "twoone");
+    ret = gnc_g_list_stringjoin (test, nullptr);
+    EXPECT_STREQ ("twoone", ret);
     g_free (ret);
 
     ret = gnc_g_list_stringjoin (test, "");
-    g_assert_cmpstr (ret, ==, "twoone");
+    EXPECT_STREQ ("twoone", ret);
     g_free (ret);
 
     ret = gnc_g_list_stringjoin (test, ":");
-    g_assert_cmpstr (ret, ==, "two:one");
+    EXPECT_STREQ ("two:one", ret);
     g_free (ret);
 
-    test = g_list_prepend (test, "three");
+    test = g_list_prepend (test, (gpointer)"three");
 
-    ret = gnc_g_list_stringjoin (test, NULL);
-    g_assert_cmpstr (ret, ==, "threetwoone");
+    ret = gnc_g_list_stringjoin (test, nullptr);
+    EXPECT_STREQ ("threetwoone", ret);
     g_free (ret);
 
     ret = gnc_g_list_stringjoin (test, "");
-    g_assert_cmpstr (ret, ==, "threetwoone");
+    EXPECT_STREQ ("threetwoone", ret);
     g_free (ret);
 
     ret = gnc_g_list_stringjoin (test, ":");
-    g_assert_cmpstr (ret, ==, "three:two:one");
+    EXPECT_STREQ ("three:two:one", ret);
     g_free (ret);
 
     g_list_free (test);
 }
 
-static void
-test_g_list_stringjoin_nodups (gconstpointer data)
+TEST(GncGlibUtils, gnc_g_list_stringjoin_nodups)
 {
-    GList *test = NULL;
+    GList *test = nullptr;
     gchar *ret;
 
-    test = g_list_prepend (test, "one");
-    test = g_list_prepend (test, "two");
-    test = g_list_prepend (test, "two");
-    test = g_list_prepend (test, "three");
-    test = g_list_prepend (test, "one:two");
-    test = g_list_prepend (test, "four");
+    test = g_list_prepend (test, (gpointer)"one");
+    test = g_list_prepend (test, (gpointer)"two");
+    test = g_list_prepend (test, (gpointer)"two");
+    test = g_list_prepend (test, (gpointer)"three");
+    test = g_list_prepend (test, (gpointer)"one:two");
+    test = g_list_prepend (test, (gpointer)"four");
     test = g_list_reverse (test);
     ret = gnc_g_list_stringjoin_nodups (test, ":");
-    g_assert_cmpstr (ret, ==, "one:two:three:four");
+    EXPECT_STREQ ("one:two:three:four", ret);
     g_free (ret);
+    g_list_free (test);
 }
 
-static void
-test_gnc_list_length (gconstpointer data)
+TEST(GncGlibUtils, gnc_list_length_cmp)
 {
-    GList *lst = NULL;
+    GList *lst = nullptr;
 
-    g_assert_true (gnc_list_length_cmp (lst, 0) == 0);
-    g_assert_true (gnc_list_length_cmp (lst, 1) == -1);
+    EXPECT_EQ (0, gnc_list_length_cmp (lst, 0));
+    EXPECT_EQ (-1, gnc_list_length_cmp (lst, 1));
 
-    lst = g_list_prepend (lst, (gpointer)1);
-    g_assert_true (gnc_list_length_cmp (lst, 0) == 1);
-    g_assert_true (gnc_list_length_cmp (lst, 1) == 0);
-    g_assert_true (gnc_list_length_cmp (lst, 2) == -1);
+    lst = g_list_prepend (lst, GINT_TO_POINTER (1));
+    EXPECT_EQ (1, gnc_list_length_cmp (lst, 0));
+    EXPECT_EQ (0, gnc_list_length_cmp (lst, 1));
+    EXPECT_EQ (-1, gnc_list_length_cmp (lst, 2));
 
-    lst = g_list_prepend (lst, (gpointer)2);
-    g_assert_true (gnc_list_length_cmp (lst, 1) == 1);
-    g_assert_true (gnc_list_length_cmp (lst, 2) == 0);
-    g_assert_true (gnc_list_length_cmp (lst, 3) == -1);
+    lst = g_list_prepend (lst, GINT_TO_POINTER (2));
+    EXPECT_EQ (1, gnc_list_length_cmp (lst, 1));
+    EXPECT_EQ (0, gnc_list_length_cmp (lst, 2));
+    EXPECT_EQ (-1, gnc_list_length_cmp (lst, 3));
 
     g_list_free (lst);
 }
-
-
-int
-main (int argc, char *argv[])
-{
-    const gchar *invalid_utf8 = "Η γρήγορη καφέ αλεπού πήδηξε πάνω από την \xb2\xf3ργή σκύλο.";
-    const gchar *controls = "Η γρήγορη καφέ αλεπού\bπήδηξε\nπάνω από\tτην αργή σκύλο.";
-    g_test_init (&argc, &argv, NULL); // initialize test program
-    g_test_add_data_func ("/core-utils/gnc_utf8_strip_invalid_and_controls invalid utf8", (gconstpointer)invalid_utf8, test_gnc_utf8_strip_invalid_and_controls);
-    g_test_add_data_func ("/core-utils/gnc_utf8_strip_invalid_and_controls control chars", (gconstpointer)controls, test_gnc_utf8_strip_invalid_and_controls);
-    g_test_add_data_func ("/core-utils/gnc_g_list_stringjoin", NULL, test_g_list_stringjoin);
-    g_test_add_data_func ("/core-utils/gnc_g_list_stringjoin_nodups", NULL, test_g_list_stringjoin_nodups);
-    g_test_add_data_func ("/core-utils/gnc_list_length", NULL, test_gnc_list_length);
-
-    return g_test_run();
-}

commit 770fd5af633e1fd15684cf33ab0beaebfe7355b4
Author: Brent McBride <mcbridebt at hotmail.com>
Date:   Tue Jul 14 21:37:34 2026 -0700

    Rename gnc-glib-utils.{c,h} and its test to gnc-string-utils for C++ migration

diff --git a/bindings/core-utils.i b/bindings/core-utils.i
index 064d60707a..893ea651f5 100644
--- a/bindings/core-utils.i
+++ b/bindings/core-utils.i
@@ -22,7 +22,7 @@
 %{
 #include <config.h>
 #include <gnc-environment.h>
-#include <gnc-glib-utils.h>
+#include <gnc-string-utils.h>
 #include <gnc-prefs.h>
 #include <gnc-path.h>
 #include <gnc-filepath-utils.h>
diff --git a/bindings/guile/glib-guile.c b/bindings/guile/glib-guile.c
index 849c069ab3..53df5d393a 100644
--- a/bindings/guile/glib-guile.c
+++ b/bindings/guile/glib-guile.c
@@ -45,7 +45,7 @@
 #include <libguile.h>
 #include "swig-runtime.h"
 #include "guile-mappings.h"
-#include "gnc-glib-utils.h"
+#include "gnc-string-utils.h"
 #include "gnc-guile-utils.h"
 #include "glib-guile.h"
 
diff --git a/gnucash/gnome-utils/gnc-file.c b/gnucash/gnome-utils/gnc-file.c
index 10094d2376..0cd2c26f2a 100644
--- a/gnucash/gnome-utils/gnc-file.c
+++ b/gnucash/gnome-utils/gnc-file.c
@@ -36,7 +36,7 @@
 #include "gnc-file.h"
 #include "gnc-features.h"
 #include "gnc-filepath-utils.h"
-#include "gnc-glib-utils.h"
+#include "gnc-string-utils.h"
 #include "gnc-gui-query.h"
 #include "gnc-hooks.h"
 #include "gnc-keyring.h"
diff --git a/gnucash/gnome-utils/gnc-main-window.cpp b/gnucash/gnome-utils/gnc-main-window.cpp
index 1f321b6c5a..72e928471b 100644
--- a/gnucash/gnome-utils/gnc-main-window.cpp
+++ b/gnucash/gnome-utils/gnc-main-window.cpp
@@ -69,7 +69,7 @@
 #include "gnc-state.h"
 #include "gnc-ui.h"
 #include "gnc-ui-util.h"
-#include <gnc-glib-utils.h>
+#include <gnc-string-utils.h>
 #include "gnc-uri.hpp"
 #include "gnc-version.h"
 #include "gnc-warnings.h"
diff --git a/gnucash/gnome-utils/gnc-tree-view-account.c b/gnucash/gnome-utils/gnc-tree-view-account.c
index 586472a68e..a43ea1d1fa 100644
--- a/gnucash/gnome-utils/gnc-tree-view-account.c
+++ b/gnucash/gnome-utils/gnc-tree-view-account.c
@@ -39,7 +39,7 @@
 #include "gnc-commodity.h"
 #include "gnc-component-manager.h"
 #include "gnc-engine.h"
-#include "gnc-glib-utils.h"
+#include "gnc-string-utils.h"
 #include "gnc-gobject-utils.h"
 #include "gnc-prefs.h"
 #include "gnc-hooks.h"
diff --git a/gnucash/gnome-utils/gnc-tree-view-commodity.c b/gnucash/gnome-utils/gnc-tree-view-commodity.c
index 9fd3bb4a93..bf242cef29 100644
--- a/gnucash/gnome-utils/gnc-tree-view-commodity.c
+++ b/gnucash/gnome-utils/gnc-tree-view-commodity.c
@@ -35,7 +35,7 @@
 #include "gnc-commodity.h"
 #include "gnc-component-manager.h"
 #include "gnc-engine.h"
-#include "gnc-glib-utils.h"
+#include "gnc-string-utils.h"
 #include "gnc-gnome-utils.h"
 #include "gnc-icons.h"
 #include "gnc-ui-util.h"
diff --git a/gnucash/gnome-utils/gnc-tree-view-owner.c b/gnucash/gnome-utils/gnc-tree-view-owner.c
index d1b1762845..16d6f2e122 100644
--- a/gnucash/gnome-utils/gnc-tree-view-owner.c
+++ b/gnucash/gnome-utils/gnc-tree-view-owner.c
@@ -37,7 +37,7 @@
 #include "gnc-commodity.h"
 #include "gnc-component-manager.h"
 #include "gnc-engine.h"
-#include "gnc-glib-utils.h"
+#include "gnc-string-utils.h"
 #include "gnc-gobject-utils.h"
 #include "gnc-hooks.h"
 #include "gnc-session.h"
diff --git a/gnucash/gnome-utils/gnc-tree-view-price.c b/gnucash/gnome-utils/gnc-tree-view-price.c
index 96e22ea77f..f51c10dc4a 100644
--- a/gnucash/gnome-utils/gnc-tree-view-price.c
+++ b/gnucash/gnome-utils/gnc-tree-view-price.c
@@ -35,7 +35,7 @@
 #include "gnc-pricedb.h"
 #include "gnc-component-manager.h"
 #include "gnc-engine.h"
-#include "gnc-glib-utils.h"
+#include "gnc-string-utils.h"
 #include "gnc-gnome-utils.h"
 #include "gnc-icons.h"
 #include "gnc-ui-util.h"
diff --git a/gnucash/gnome-utils/gnc-tree-view.c b/gnucash/gnome-utils/gnc-tree-view.c
index 38af5ec221..b71742affc 100644
--- a/gnucash/gnome-utils/gnc-tree-view.c
+++ b/gnucash/gnome-utils/gnc-tree-view.c
@@ -40,7 +40,7 @@
 
 #include "gnc-tree-view.h"
 #include "gnc-engine.h"
-#include "gnc-glib-utils.h"
+#include "gnc-string-utils.h"
 #include "gnc-gnome-utils.h"
 #include "gnc-gobject-utils.h"
 #include "gnc-cell-renderer-label.h"
diff --git a/gnucash/gnome/dialog-imap-editor.c b/gnucash/gnome/dialog-imap-editor.c
index c20d5a7b4d..669d6c822e 100644
--- a/gnucash/gnome/dialog-imap-editor.c
+++ b/gnucash/gnome/dialog-imap-editor.c
@@ -33,7 +33,7 @@
 
 #include "gnc-ui.h"
 #include "gnc-ui-util.h"
-#include <gnc-glib-utils.h>
+#include <gnc-string-utils.h>
 #include "Account.h"
 
 #define DIALOG_IMAP_CM_CLASS    "dialog-imap-edit"
diff --git a/gnucash/gnome/dialog-invoice.c b/gnucash/gnome/dialog-invoice.c
index 67f9279f64..d62d4aee16 100644
--- a/gnucash/gnome/dialog-invoice.c
+++ b/gnucash/gnome/dialog-invoice.c
@@ -50,7 +50,7 @@
 #include "gncOwner.h"
 #include "gncInvoice.h"
 #include "gncInvoiceP.h"
-#include <gnc-glib-utils.h>
+#include <gnc-string-utils.h>
 
 #include "gncEntryLedger.h"
 
diff --git a/gnucash/gnome/dialog-payment.c b/gnucash/gnome/dialog-payment.c
index 51dcf9e1f0..e622ab9719 100644
--- a/gnucash/gnome/dialog-payment.c
+++ b/gnucash/gnome/dialog-payment.c
@@ -32,7 +32,7 @@
 #include "gnc-ui.h"
 #include "gnc-gui-query.h"
 #include "gnc-ui-util.h"
-#include <gnc-glib-utils.h>
+#include <gnc-string-utils.h>
 #include "qof.h"
 #include "gnc-date.h"
 #include "gnc-date-edit.h"
diff --git a/gnucash/gnome/dialog-price-edit-db.cpp b/gnucash/gnome/dialog-price-edit-db.cpp
index 2308e36e30..8885aa488b 100644
--- a/gnucash/gnome/dialog-price-edit-db.cpp
+++ b/gnucash/gnome/dialog-price-edit-db.cpp
@@ -48,7 +48,7 @@
 #include "gnc-ui.h"
 #include "gnc-ui-util.h"
 #include "gnc-warnings.h"
-#include <gnc-glib-utils.h>
+#include <gnc-string-utils.h>
 
 
 #define DIALOG_PRICE_DB_CM_CLASS "dialog-price-edit-db"
diff --git a/gnucash/gnome/dialog-sx-editor.c b/gnucash/gnome/dialog-sx-editor.c
index 1cae867e51..b5c8932883 100644
--- a/gnucash/gnome/dialog-sx-editor.c
+++ b/gnucash/gnome/dialog-sx-editor.c
@@ -64,7 +64,7 @@
 #include "gnc-ui-util.h"
 #include "gnucash-sheet.h"
 #include "gnc-session.h"
-#include <gnc-glib-utils.h>
+#include <gnc-string-utils.h>
 
 #include "gnc-split-reg.h"
 
diff --git a/gnucash/gnome/dialog-sx-since-last-run.c b/gnucash/gnome/dialog-sx-since-last-run.c
index fc1972553b..6c07774556 100644
--- a/gnucash/gnome/dialog-sx-since-last-run.c
+++ b/gnucash/gnome/dialog-sx-since-last-run.c
@@ -43,7 +43,7 @@
 #include "gnc-prefs.h"
 #include "gnc-ui.h"
 #include "gnc-ui-util.h"
-#include "gnc-glib-utils.h"
+#include "gnc-string-utils.h"
 #include "Query.h"
 #include "qof.h"
 #include "gnc-ledger-display.h"
diff --git a/gnucash/gnome/gnc-plugin-page-account-tree.cpp b/gnucash/gnome/gnc-plugin-page-account-tree.cpp
index 4a7fb6a468..5f2ca6b696 100644
--- a/gnucash/gnome/gnc-plugin-page-account-tree.cpp
+++ b/gnucash/gnome/gnc-plugin-page-account-tree.cpp
@@ -73,7 +73,7 @@
 #include "window-main-summarybar.h"
 #include "dialog-object-references.h"
 #include "dialog-find-account.h"
-#include <gnc-glib-utils.h>
+#include <gnc-string-utils.h>
 
 /* This static indicates the debugging module that this .o belongs to.  */
 static QofLogModule log_module = GNC_MOD_GUI;
diff --git a/gnucash/gnome/gnc-plugin-page-register-filter.cpp b/gnucash/gnome/gnc-plugin-page-register-filter.cpp
index 1a1dea4161..50df6ab09e 100644
--- a/gnucash/gnome/gnc-plugin-page-register-filter.cpp
+++ b/gnucash/gnome/gnc-plugin-page-register-filter.cpp
@@ -37,7 +37,7 @@
 #include "dialog-utils.h"
 #include "gnc-date.h"
 #include "gnc-date-edit.h"
-#include "gnc-glib-utils.h"
+#include "gnc-string-utils.h"
 #include "gnc-ui.h"
 #include "gnc-state.h"
 #include "gnc-period-select.h"
diff --git a/gnucash/gnome/gnc-plugin-page-register.cpp b/gnucash/gnome/gnc-plugin-page-register.cpp
index 89b52dcc49..3c459ad359 100644
--- a/gnucash/gnome/gnc-plugin-page-register.cpp
+++ b/gnucash/gnome/gnc-plugin-page-register.cpp
@@ -65,7 +65,7 @@
 #include "gnc-engine.h"
 #include "gnc-event.h"
 #include "gnc-features.h"
-#include "gnc-glib-utils.h"
+#include "gnc-string-utils.h"
 #include "gnc-gnome-utils.h"
 #include "gnc-gobject-utils.h"
 #include "gnc-gui-query.h"
diff --git a/gnucash/gnome/gnc-plugin-page-report.cpp b/gnucash/gnome/gnc-plugin-page-report.cpp
index 68c2ace85c..71dc73c92f 100644
--- a/gnucash/gnome/gnc-plugin-page-report.cpp
+++ b/gnucash/gnome/gnc-plugin-page-report.cpp
@@ -47,7 +47,7 @@
 #include <sys/stat.h>
 #include <errno.h>
 
-#include <gnc-glib-utils.h>
+#include <gnc-string-utils.h>
 #include "gfec.h"
 #include "dialog-custom-report.h"
 #include "dialog-utils.h"
diff --git a/gnucash/gnome/gnc-plugin-page-sx-list.cpp b/gnucash/gnome/gnc-plugin-page-sx-list.cpp
index 2a0dd17fd3..ad28429cfc 100644
--- a/gnucash/gnome/gnc-plugin-page-sx-list.cpp
+++ b/gnucash/gnome/gnc-plugin-page-sx-list.cpp
@@ -60,7 +60,7 @@
 #include "gnc-dense-cal.h"
 #include "gnc-engine.h"
 #include "gnc-event.h"
-#include "gnc-glib-utils.h"
+#include "gnc-string-utils.h"
 #include "gnc-icons.h"
 #include "gnc-main-window.h"
 #include "gnc-plugin.h"
diff --git a/gnucash/import-export/aqb/assistant-ab-initial.c b/gnucash/import-export/aqb/assistant-ab-initial.c
index 5a2dba7f30..effb1aae47 100644
--- a/gnucash/import-export/aqb/assistant-ab-initial.c
+++ b/gnucash/import-export/aqb/assistant-ab-initial.c
@@ -58,7 +58,7 @@
 #include "gnc-ab-kvp.h"
 #include "gnc-ab-utils.h"
 #include "gnc-component-manager.h"
-#include "gnc-glib-utils.h"
+#include "gnc-string-utils.h"
 #include "gnc-ui.h"
 #include "gnc-ui-util.h"
 #include "gnc-session.h"
diff --git a/gnucash/import-export/aqb/gnc-ab-utils.c b/gnucash/import-export/aqb/gnc-ab-utils.c
index 2c4defcc92..de616c747a 100644
--- a/gnucash/import-export/aqb/gnc-ab-utils.c
+++ b/gnucash/import-export/aqb/gnc-ab-utils.c
@@ -43,7 +43,7 @@
 #include "Transaction.h"
 #include "dialog-ab-trans.h"
 #include "gnc-ab-kvp.h"
-#include "gnc-glib-utils.h"
+#include "gnc-string-utils.h"
 #include "gnc-gwen-gui.h"
 #include "gnc-prefs.h"
 #include "gnc-ui.h"
diff --git a/gnucash/import-export/bi-import/dialog-bi-import.c b/gnucash/import-export/bi-import/dialog-bi-import.c
index f29743b933..f17df19205 100644
--- a/gnucash/import-export/bi-import/dialog-bi-import.c
+++ b/gnucash/import-export/bi-import/dialog-bi-import.c
@@ -36,7 +36,7 @@
 #include <glib.h>
 #include <glib/gstdio.h>
 
-#include "gnc-glib-utils.h"
+#include "gnc-string-utils.h"
 #include "gnc-date.h"
 #include "gnc-ui.h"
 #include "gnc-ui-util.h"
diff --git a/gnucash/import-export/csv-imp/csv-account-import.c b/gnucash/import-export/csv-imp/csv-account-import.c
index f575fee7dd..6f9fbc6680 100644
--- a/gnucash/import-export/csv-imp/csv-account-import.c
+++ b/gnucash/import-export/csv-imp/csv-account-import.c
@@ -30,7 +30,7 @@
 #include <glib/gi18n.h>
 #include <glib/gstdio.h>
 
-#include "gnc-glib-utils.h"
+#include "gnc-string-utils.h"
 #include "gnc-ui-util.h"
 #include <regex.h>
 #include "Account.h"
diff --git a/gnucash/import-export/customer-import/dialog-customer-import.c b/gnucash/import-export/customer-import/dialog-customer-import.c
index 2991be1756..3f61c0966b 100644
--- a/gnucash/import-export/customer-import/dialog-customer-import.c
+++ b/gnucash/import-export/customer-import/dialog-customer-import.c
@@ -34,7 +34,7 @@
 #include <glib.h>
 #include <glib/gstdio.h>
 
-#include "gnc-glib-utils.h"
+#include "gnc-string-utils.h"
 #include "gnc-ui.h"
 #include "gnc-ui-util.h"
 #include "gnc-gui-query.h"
diff --git a/gnucash/import-export/import-main-matcher.cpp b/gnucash/import-export/import-main-matcher.cpp
index 3a5e7c2bcd..8331b7fda1 100644
--- a/gnucash/import-export/import-main-matcher.cpp
+++ b/gnucash/import-export/import-main-matcher.cpp
@@ -49,7 +49,7 @@
 #include "Account.hpp"
 #include "dialog-transfer.h"
 #include "dialog-utils.h"
-#include "gnc-glib-utils.h"
+#include "gnc-string-utils.h"
 #include "gnc-ui.h"
 #include "gnc-ui-util.h"
 #include "gnc-engine.h"
diff --git a/gnucash/import-export/ofx/gnc-ofx-import.cpp b/gnucash/import-export/ofx/gnc-ofx-import.cpp
index 8e4d218917..d4cf3e9846 100644
--- a/gnucash/import-export/ofx/gnc-ofx-import.cpp
+++ b/gnucash/import-export/ofx/gnc-ofx-import.cpp
@@ -46,7 +46,7 @@
 #include "gnc-file.h"
 #include "gnc-engine.h"
 #include "gnc-ui-util.h"
-#include "gnc-glib-utils.h"
+#include "gnc-string-utils.h"
 #include "gnc-prefs.h"
 #include "gnc-ui.h"
 #include "gnc-window.h"
diff --git a/gnucash/register/ledger-core/gnc-ledger-display.c b/gnucash/register/ledger-core/gnc-ledger-display.c
index ae632ef782..8b8554c216 100644
--- a/gnucash/register/ledger-core/gnc-ledger-display.c
+++ b/gnucash/register/ledger-core/gnc-ledger-display.c
@@ -37,7 +37,7 @@
 #include "gnc-ledger-display.h"
 #include "gnc-prefs.h"
 #include "gnc-ui-util.h"
-#include <gnc-glib-utils.h>
+#include <gnc-string-utils.h>
 #include "split-register-control.h"
 #include "split-register-model.h"
 
diff --git a/gnucash/register/ledger-core/split-register-model.c b/gnucash/register/ledger-core/split-register-model.c
index 34ed7d5a1f..203d46772e 100644
--- a/gnucash/register/ledger-core/split-register-model.c
+++ b/gnucash/register/ledger-core/split-register-model.c
@@ -32,7 +32,7 @@
 #include "gnc-prefs.h"
 #include "gnc-ui.h"
 #include "gnc-uri-utils.h"
-#include "gnc-glib-utils.h"
+#include "gnc-string-utils.h"
 #include "gnc-filepath-utils.h"
 #include "gnc-warnings.h"
 #include "doclinkcell.h"
diff --git a/gnucash/register/register-gnome/combocell-gnome.c b/gnucash/register/register-gnome/combocell-gnome.c
index 05b942c069..525a857c15 100644
--- a/gnucash/register/register-gnome/combocell-gnome.c
+++ b/gnucash/register/register-gnome/combocell-gnome.c
@@ -47,7 +47,7 @@
 #include "gnucash-sheetP.h"
 #include "table-allgui.h"
 #include "Account.h"
-#include "gnc-glib-utils.h"
+#include "gnc-string-utils.h"
 
 #define GNC_PREF_AUTO_RAISE_LISTS "auto-raise-lists"
 
diff --git a/gnucash/register/register-gnome/completioncell-gnome.c b/gnucash/register/register-gnome/completioncell-gnome.c
index 76e47ad6e7..74022a3726 100644
--- a/gnucash/register/register-gnome/completioncell-gnome.c
+++ b/gnucash/register/register-gnome/completioncell-gnome.c
@@ -43,7 +43,7 @@
 #include "gnucash-sheet.h"
 #include "gnucash-sheetP.h"
 #include "table-allgui.h"
-#include "gnc-glib-utils.h"
+#include "gnc-string-utils.h"
 #include <gnc-unicode.h>
 
 typedef struct _PopBox
diff --git a/libgnucash/app-utils/gnc-sx-instance-model.c b/libgnucash/app-utils/gnc-sx-instance-model.c
index b742c8dc31..dbccc92d1a 100644
--- a/libgnucash/app-utils/gnc-sx-instance-model.c
+++ b/libgnucash/app-utils/gnc-sx-instance-model.c
@@ -46,7 +46,7 @@
 #include "gnc-date.h"
 #include "gnc-event.h"
 #include "gnc-exp-parser.h"
-#include "gnc-glib-utils.h"
+#include "gnc-string-utils.h"
 #include "gnc-sx-instance-model.h"
 #include "gnc-ui-util.h"
 #include "qof.h"
diff --git a/libgnucash/core-utils/CMakeLists.txt b/libgnucash/core-utils/CMakeLists.txt
index 21ce5acc08..6bbf5de4c1 100644
--- a/libgnucash/core-utils/CMakeLists.txt
+++ b/libgnucash/core-utils/CMakeLists.txt
@@ -10,7 +10,7 @@ set (core_utils_SOURCES
   gnc-environment.c
   gnc-filepath-utils.cpp
   gnc-gkeyfile-utils.c
-  gnc-glib-utils.c
+  gnc-string-utils.cpp
   gnc-unicode.cpp
   gnc-locale-utils.c
   gnc-locale-utils.cpp
@@ -28,7 +28,7 @@ set(core_utils_noinst_HEADERS
   gnc-environment.h
   gnc-filepath-utils.h
   gnc-gkeyfile-utils.h
-  gnc-glib-utils.h
+  gnc-string-utils.h
   gnc-unicode.h
   gnc-locale-utils.h
   gnc-locale-utils.hpp
diff --git a/libgnucash/core-utils/gnc-glib-utils.c b/libgnucash/core-utils/gnc-string-utils.cpp
similarity index 99%
rename from libgnucash/core-utils/gnc-glib-utils.c
rename to libgnucash/core-utils/gnc-string-utils.cpp
index 0b55982625..d287485a77 100644
--- a/libgnucash/core-utils/gnc-glib-utils.c
+++ b/libgnucash/core-utils/gnc-string-utils.cpp
@@ -28,7 +28,7 @@
 #include <string.h>
 #include <stdbool.h>
 
-#include "gnc-glib-utils.h"
+#include "gnc-string-utils.h"
 
 #ifdef G_OS_WIN32
 #include <windows.h>
diff --git a/libgnucash/core-utils/gnc-glib-utils.h b/libgnucash/core-utils/gnc-string-utils.h
similarity index 96%
rename from libgnucash/core-utils/gnc-glib-utils.h
rename to libgnucash/core-utils/gnc-string-utils.h
index a2770aaaf8..b0281bdbaf 100644
--- a/libgnucash/core-utils/gnc-glib-utils.h
+++ b/libgnucash/core-utils/gnc-string-utils.h
@@ -1,6 +1,7 @@
 /********************************************************************\
- * gnc-glib-utils.c -- utility functions based on glib functions    *
+ * gnc-string-utils.h -- string and list utility functions         *
  * Copyright (C) 2006 David Hampton <hampton at employees.org>         *
+ * Copyright (C) 2026 Brent McBride <mcbridebt at hotmail.com>         *
  *                                                                  *
  * This program is free software; you can redistribute it and/or    *
  * modify it under the terms of the GNU General Public License as   *
@@ -29,13 +30,13 @@
     wrap the base glib functions and make them easier to use.
 
     @{ */
-/** @file gnc-glib-utils.h
- *  @brief GLib helper routines
+/** @file gnc-string-utils.h
+ *  @brief String and list utility routines
  *  @author Copyright (C) 2006 David Hampton <hampton at employees.org>
  */
 
-#ifndef GNC_GLIB_UTILS_H
-#define GNC_GLIB_UTILS_H
+#ifndef GNC_STRING_UTILS_H
+#define GNC_STRING_UTILS_H
 
 #include <glib.h>
 
@@ -216,6 +217,6 @@ gint gnc_list_length_cmp (const GList *list, size_t len);
 } /* extern "C" */
 #endif
 
-#endif /* GNC_GLIB_UTILS_H */
+#endif /* GNC_STRING_UTILS_H */
 /** @} */
 /** @} */
diff --git a/libgnucash/core-utils/test/CMakeLists.txt b/libgnucash/core-utils/test/CMakeLists.txt
index 1e5ad913a1..0416017fba 100644
--- a/libgnucash/core-utils/test/CMakeLists.txt
+++ b/libgnucash/core-utils/test/CMakeLists.txt
@@ -15,7 +15,7 @@ macro(add_core_utils_test _TARGET _SOURCE_FILES)
   gnc_add_test(${_TARGET} "${_SOURCE_FILES}" CORE_UTILS_TEST_INCLUDE_DIRS CORE_UTILS_TEST_LIBS)
 endmacro()
 
-add_core_utils_test(test-gnc-glib-utils test-gnc-glib-utils.c)
+add_core_utils_test(test-gnc-string-utils gtest-gnc-string-utils.cpp)
 add_core_utils_test(test-resolve-file-path test-resolve-file-path.c)
 add_core_utils_test(test-userdata-dir test-userdata-dir.c)
 if (NOT MAC_INTEGRATION AND NOT WIN32)
@@ -67,5 +67,5 @@ gnc_add_test(test-gnc-unicode
   gtest_icu_locale_LIBS)
 
 set_dist_list(test_core_utils_DIST CMakeLists.txt
-  test-gnc-glib-utils.c test-resolve-file-path.c test-userdata-dir.c
+  gtest-gnc-string-utils.cpp test-resolve-file-path.c test-userdata-dir.c
   test-userdata-dir-invalid-home.c gtest-gnc-unicode.cpp gtest-path-utilities.cpp)
diff --git a/libgnucash/core-utils/test/test-gnc-glib-utils.c b/libgnucash/core-utils/test/gtest-gnc-string-utils.cpp
similarity index 99%
rename from libgnucash/core-utils/test/test-gnc-glib-utils.c
rename to libgnucash/core-utils/test/gtest-gnc-string-utils.cpp
index 2f87d80f96..d0eb1815ee 100644
--- a/libgnucash/core-utils/test/test-gnc-glib-utils.c
+++ b/libgnucash/core-utils/test/gtest-gnc-string-utils.cpp
@@ -24,7 +24,7 @@
 #include <config.h>
 #include <string.h>
 #include <glib.h>
-#include <gnc-glib-utils.h>
+#include <gnc-string-utils.h>
 #include <unittest-support.h>
 
 static void
diff --git a/libgnucash/engine/Account.cpp b/libgnucash/engine/Account.cpp
index 6bb094a49f..df2f8c794c 100644
--- a/libgnucash/engine/Account.cpp
+++ b/libgnucash/engine/Account.cpp
@@ -39,7 +39,7 @@
 #include "Transaction.h"
 #include "TransactionP.hpp"
 #include "gnc-event.h"
-#include "gnc-glib-utils.h"
+#include "gnc-string-utils.h"
 #include "gnc-lot.h"
 #include "gnc-pricedb.h"
 #include "qofevent.h"
diff --git a/libgnucash/engine/Recurrence.cpp b/libgnucash/engine/Recurrence.cpp
index 47f5439cf8..c063500a32 100644
--- a/libgnucash/engine/Recurrence.cpp
+++ b/libgnucash/engine/Recurrence.cpp
@@ -31,7 +31,7 @@
 #include "gnc-date.h"
 #include "Account.h"
 #include <stdint.h>
-#include <gnc-glib-utils.h>
+#include <gnc-string-utils.h>
 
 #define LOG_MOD "gnc.engine.recurrence"
 static QofLogModule log_module = LOG_MOD;
diff --git a/libgnucash/engine/gncIDSearch.c b/libgnucash/engine/gncIDSearch.c
index 921708d1d4..0146ebb758 100644
--- a/libgnucash/engine/gncIDSearch.c
+++ b/libgnucash/engine/gncIDSearch.c
@@ -22,7 +22,7 @@
 **********************************************************************/
 
 #include "gncIDSearch.h"
-#include <gnc-glib-utils.h>
+#include <gnc-string-utils.h>
 
 typedef enum
 {   UNDEFINED,
diff --git a/libgnucash/engine/test/utest-Account.cpp b/libgnucash/engine/test/utest-Account.cpp
index fc52032ba1..c8f2a35442 100644
--- a/libgnucash/engine/test/utest-Account.cpp
+++ b/libgnucash/engine/test/utest-Account.cpp
@@ -28,7 +28,7 @@
 #include <gnc-event.h>
 #include <gnc-date.h>
 /* Add specific headers for this class */
-#include "gnc-glib-utils.h"
+#include "gnc-string-utils.h"
 #include "../Account.h"
 #include "../AccountP.hpp"
 #include "../Split.h"
diff --git a/po/POTFILES.in b/po/POTFILES.in
index d2acbd170f..a0e5d7f20f 100644
--- a/po/POTFILES.in
+++ b/po/POTFILES.in
@@ -617,11 +617,11 @@ libgnucash/core-utils/binreloc.c
 libgnucash/core-utils/gnc-environment.c
 libgnucash/core-utils/gnc-filepath-utils.cpp
 libgnucash/core-utils/gnc-gkeyfile-utils.c
-libgnucash/core-utils/gnc-glib-utils.c
 libgnucash/core-utils/gnc-locale-utils.c
 libgnucash/core-utils/gnc-locale-utils.cpp
 libgnucash/core-utils/gnc-path.c
 libgnucash/core-utils/gnc-prefs.cpp
+libgnucash/core-utils/gnc-string-utils.cpp
 libgnucash/core-utils/gnc-unicode.cpp
 libgnucash/core-utils/gnc-version.c
 libgnucash/engine/Account.cpp



Summary of changes:
 bindings/core-utils.i                              |   2 +-
 bindings/guile/glib-guile.c                        |   2 +-
 gnucash/gnome-utils/gnc-file.c                     |   2 +-
 gnucash/gnome-utils/gnc-main-window.cpp            |   2 +-
 gnucash/gnome-utils/gnc-tree-view-account.c        |   2 +-
 gnucash/gnome-utils/gnc-tree-view-commodity.c      |   2 +-
 gnucash/gnome-utils/gnc-tree-view-owner.c          |   2 +-
 gnucash/gnome-utils/gnc-tree-view-price.c          |   2 +-
 gnucash/gnome-utils/gnc-tree-view.c                |   2 +-
 gnucash/gnome/dialog-imap-editor.c                 |   2 +-
 gnucash/gnome/dialog-invoice.c                     |   2 +-
 gnucash/gnome/dialog-payment.c                     |   2 +-
 gnucash/gnome/dialog-price-edit-db.cpp             |   2 +-
 gnucash/gnome/dialog-sx-editor.c                   |   2 +-
 gnucash/gnome/dialog-sx-since-last-run.c           |   2 +-
 gnucash/gnome/gnc-plugin-page-account-tree.cpp     |   2 +-
 gnucash/gnome/gnc-plugin-page-register-filter.cpp  |   2 +-
 gnucash/gnome/gnc-plugin-page-register.cpp         |   2 +-
 gnucash/gnome/gnc-plugin-page-report.cpp           |   2 +-
 gnucash/gnome/gnc-plugin-page-sx-list.cpp          |   2 +-
 gnucash/import-export/aqb/assistant-ab-initial.c   |   2 +-
 gnucash/import-export/aqb/gnc-ab-utils.c           |   2 +-
 gnucash/import-export/bi-import/dialog-bi-import.c |   2 +-
 gnucash/import-export/csv-imp/csv-account-import.c |   2 +-
 .../customer-import/dialog-customer-import.c       |   2 +-
 gnucash/import-export/import-main-matcher.cpp      |   2 +-
 gnucash/import-export/ofx/gnc-ofx-import.cpp       |   2 +-
 gnucash/register/ledger-core/gnc-ledger-display.c  |   2 +-
 .../register/ledger-core/split-register-model.c    |   2 +-
 gnucash/register/register-gnome/combocell-gnome.c  |   2 +-
 .../register/register-gnome/completioncell-gnome.c |   2 +-
 libgnucash/app-utils/gnc-sx-instance-model.c       |   2 +-
 libgnucash/core-utils/CMakeLists.txt               |   4 +-
 libgnucash/core-utils/gnc-locale-utils.cpp         |  80 +++--
 .../{gnc-glib-utils.c => gnc-string-utils.cpp}     | 168 ++++++-----
 .../{gnc-glib-utils.h => gnc-string-utils.h}       |  18 +-
 libgnucash/core-utils/test/CMakeLists.txt          |  12 +-
 .../core-utils/test/gtest-gnc-string-utils.cpp     | 334 +++++++++++++++++++++
 libgnucash/core-utils/test/test-gnc-glib-utils.c   | 171 -----------
 libgnucash/engine/Account.cpp                      |   2 +-
 libgnucash/engine/Recurrence.cpp                   |   2 +-
 libgnucash/engine/gncIDSearch.c                    |   2 +-
 libgnucash/engine/test/utest-Account.cpp           |   2 +-
 po/POTFILES.in                                     |   2 +-
 44 files changed, 535 insertions(+), 326 deletions(-)
 rename libgnucash/core-utils/{gnc-glib-utils.c => gnc-string-utils.cpp} (66%)
 rename libgnucash/core-utils/{gnc-glib-utils.h => gnc-string-utils.h} (94%)
 create mode 100644 libgnucash/core-utils/test/gtest-gnc-string-utils.cpp
 delete mode 100644 libgnucash/core-utils/test/test-gnc-glib-utils.c



More information about the gnucash-changes mailing list