gnucash stable: Multiple changes pushed

John Ralls jralls at code.gnucash.org
Fri Mar 7 15:57:36 EST 2025


Updated	 via  https://github.com/Gnucash/gnucash/commit/1fa4f71f (commit)
	 via  https://github.com/Gnucash/gnucash/commit/058e2a19 (commit)
	from  https://github.com/Gnucash/gnucash/commit/9172d1dd (commit)



commit 1fa4f71f59585665202a206b4175a5e0866beaf8
Author: John Ralls <jralls at ceridwen.us>
Date:   Fri Mar 7 12:54:36 2025 -0800

    Fix gnc-numeric test failure on macOS 15.4 beta.
    
    Caused by Apple suddenly fixing ostreams to put the thousands
    separator in numbers when it's set in the locale.

diff --git a/libgnucash/engine/test/gtest-gnc-numeric.cpp b/libgnucash/engine/test/gtest-gnc-numeric.cpp
index 12f301463d..58bd1162ca 100644
--- a/libgnucash/engine/test/gtest-gnc-numeric.cpp
+++ b/libgnucash/engine/test/gtest-gnc-numeric.cpp
@@ -24,6 +24,9 @@
 #include <cstdint>
 #include "../gnc-numeric.hpp"
 #include "../gnc-rational.hpp"
+#ifdef __APPLE__
+#include <sys/sysctl.h>
+#endif
 
 TEST(gncnumeric_constructors, test_default_constructor)
 {
@@ -255,6 +258,23 @@ TEST(gncnumeric_output, string_output)
     EXPECT_EQ("-123/456", neg_rational_string.to_string());
 }
 
+#ifdef __APPLE__
+static float
+get_macos_version()
+{
+    int major, minor, micro;
+    float rv{};
+    char vstr[64];
+    size_t len = sizeof(vstr);
+    auto err = sysctlbyname("kern.osrelease", vstr, &len, nullptr, 0);
+    if (err)
+        return rv;
+    sscanf(vstr, "%d.%d.%d", &major, &minor, &micro);
+    rv = major + minor / 100.0 + micro / 10000.0;
+    return rv;
+}
+#endif
+
 TEST(gncnumeric_stream, output_stream)
 {
     std::ostringstream output;
@@ -286,6 +306,12 @@ TEST(gncnumeric_stream, output_stream)
         output.imbue(std::locale("fr_FR"));
         output.str("");
         output << simple_int;
+// Apple fixed this in macOS 15.4 to include the separator.
+#ifdef __APPLE__
+        if (get_macos_version() >= 24.04)
+            EXPECT_EQ("123\xe2\x80\xaf""456", output.str());
+        else
+#endif
         EXPECT_EQ("123456", output.str());
     }
     output.str("");

commit 058e2a196b39814e343dd606a9f9f0d3d8e20704
Author: John Ralls <jralls at ceridwen.us>
Date:   Fri Mar 7 11:50:22 2025 -0800

    Bug 799564 - Decimal point confusions when getting stock quotes from aex
    
    Add parsing thousands-grouped numbers to GncNumeric(const std::string&).

diff --git a/libgnucash/engine/gnc-numeric.cpp b/libgnucash/engine/gnc-numeric.cpp
index 9a0ea98c92..62aacfaa60 100644
--- a/libgnucash/engine/gnc-numeric.cpp
+++ b/libgnucash/engine/gnc-numeric.cpp
@@ -242,24 +242,37 @@ fast_numeral_rational (const char* str)
 }
 
 GncNumeric::GncNumeric(const std::string &str, bool autoround) {
+    static const std::string begin("^[^-.0-9]*");
+    static const std::string end("[^0-9]*$");
+    static const std::string begin_group("(?:");
+    static const std::string end_group(")");
+    static const std::string or_op("|");
     static const std::string maybe_sign ("(-?)");
     static const std::string opt_signed_int("(-?[0-9]*)");
+    static const std::string opt_signed_separated_int("(-?[0-9]{1,3})");
     static const std::string unsigned_int("([0-9]+)");
+    static const std::string eu_separated_int("(?:[\\s'.]([0-9]{3}))?");
+    static const std::string en_separated_int("(?:\\,([0-9]{3}))?");
+    static const std::string eu_decimal_part("(?:\\,([0-9]+))?");
+    static const std::string en_decimal_part("(?:\\.([0-9]+))?");
     static const std::string hex_frag("(0[xX][A-Fa-f0-9]+)");
     static const std::string slash("[ \\t]*/[ \\t]*");
     static const std::string whitespace("[ \\t]+");
+    static const std::string eu_sep_decimal(begin_group + opt_signed_separated_int + eu_separated_int + eu_separated_int + eu_separated_int + eu_separated_int + eu_decimal_part + end_group);
+    static const std::string en_sep_decimal(begin_group + opt_signed_separated_int + en_separated_int + en_separated_int + en_separated_int + en_separated_int + en_decimal_part + end_group);
     /* The llvm standard C++ library refused to recognize the - in the
      * opt_signed_int pattern with the default ECMAScript syntax so we use the
      * awk syntax.
      */
-    static const regex numeral(opt_signed_int);
-    static const regex hex(hex_frag);
-    static const regex numeral_rational(opt_signed_int + slash + unsigned_int);
-    static const regex integer_and_fraction(maybe_sign + unsigned_int + whitespace + unsigned_int + slash + unsigned_int);
-    static const regex hex_rational(hex_frag + slash + hex_frag);
-    static const regex hex_over_num(hex_frag + slash + unsigned_int);
-    static const regex num_over_hex(opt_signed_int + slash + hex_frag);
-    static const regex decimal(opt_signed_int + "[.,]" + unsigned_int);
+    static const regex numeral(begin + opt_signed_int + end);
+    static const regex hex(begin + hex_frag + end);
+    static const regex numeral_rational(begin + opt_signed_int + slash + unsigned_int + end);
+    static const regex integer_and_fraction(begin + maybe_sign + unsigned_int + whitespace + unsigned_int + slash + unsigned_int + end);
+    static const regex hex_rational(begin + hex_frag + slash + hex_frag + end);
+    static const regex hex_over_num(begin + hex_frag + slash + unsigned_int + end);
+    static const regex num_over_hex(begin + opt_signed_int + slash + hex_frag + end);
+    static const regex decimal(begin + opt_signed_int + "[.,]" + unsigned_int + end);
+    static const regex sep_decimal(begin + begin_group + eu_sep_decimal + or_op + en_sep_decimal + end_group + end);
     static const regex scientific("(?:(-?[0-9]+[.,]?)|(-?[0-9]*)[.,]([0-9]+))[Ee](-?[0-9]+)");
     static const regex has_hex_prefix(".*0[xX]$");
     smatch m, x;
@@ -327,6 +340,37 @@ GncNumeric::GncNumeric(const std::string &str, bool autoround) {
     {
         std::string integer{m[1].matched ? m[1].str() : ""};
         std::string decimal{m[2].matched ? m[2].str() : ""};
+        auto [num, denom] = reduce_number_pair(numeric_from_decimal_match(integer, decimal), str, autoround);
+        m_num = num;
+        m_den = denom;
+        return;
+    }
+    if (regex_search(str, m, sep_decimal))
+    {
+        /* There's a bit of magic here because of the complexity of
+         * the regex. It supports two formats, one for locales that
+         * use space, apostrophe, or dot for thousands separator and
+         * comma for decimal separator and the other for locales that
+         * use comma for thousands and dot for decimal. For each
+         * format there are 5 captures for thousands-groups (allowing
+         * up to 10^16 - 1) and one for decimal, hence the loops from
+         * 1 - 5 and 7 - 11 with the decimal being either capture 6 or
+         * capture 12.
+         */
+        std::string integer(""), decimal("");
+        for (auto i{1}; i < 6; ++i)
+            if (m[i].matched)
+                integer += m[i].str();
+        if (m[6].matched)
+            decimal += m[6].str();
+        if (integer.empty() && decimal.empty())
+        {
+            for (auto i{7}; i <12; ++i)
+                if (m[i].matched)
+                integer += m[i].str();
+        if (m[12].matched)
+            decimal += m[12].str();
+        }
         auto [num, denom] =
             reduce_number_pair(numeric_from_decimal_match(integer, decimal),
                                str, autoround);
diff --git a/libgnucash/engine/test/gtest-gnc-numeric.cpp b/libgnucash/engine/test/gtest-gnc-numeric.cpp
index a38d4b4f25..12f301463d 100644
--- a/libgnucash/engine/test/gtest-gnc-numeric.cpp
+++ b/libgnucash/engine/test/gtest-gnc-numeric.cpp
@@ -141,6 +141,10 @@ TEST(gncnumeric_constructors, test_string_constructor)
     GncNumeric neg_simple_decimal("-123.456");
     EXPECT_EQ(-123456, neg_simple_decimal.num());
     EXPECT_EQ(1000, neg_simple_decimal.denom());
+    ASSERT_NO_THROW(GncNumeric thousep_decimal("123,456,789.123"));
+    GncNumeric thousep_decimal("123,456,789.123");
+    EXPECT_EQ(123456789123, thousep_decimal.num());
+    EXPECT_EQ(1000, thousep_decimal.denom());
     ASSERT_NO_THROW(GncNumeric continental_decimal("123,456"));
     GncNumeric continental_decimal("123,456");
     EXPECT_EQ(123456, continental_decimal.num());
@@ -149,6 +153,10 @@ TEST(gncnumeric_constructors, test_string_constructor)
     GncNumeric neg_continental_decimal("-123,456");
     EXPECT_EQ(-123456, neg_continental_decimal.num());
     EXPECT_EQ(1000, neg_continental_decimal.denom());
+    ASSERT_NO_THROW(GncNumeric swiss_thousep_decimal("123 456 789,123"));
+    GncNumeric swiss_thousep_decimal("123 456 789,123");
+    EXPECT_EQ(123456789123, swiss_thousep_decimal.num());
+    EXPECT_EQ(1000, swiss_thousep_decimal.denom());
     GncNumeric from_scientific("1.234e4");
     EXPECT_EQ(12340, from_scientific.num());
     EXPECT_EQ(1, from_scientific.denom());



Summary of changes:
 libgnucash/engine/gnc-numeric.cpp            | 60 ++++++++++++++++++++++++----
 libgnucash/engine/test/gtest-gnc-numeric.cpp | 34 ++++++++++++++++
 2 files changed, 86 insertions(+), 8 deletions(-)



More information about the gnucash-changes mailing list