diff --git a/.appveyor.yml b/.appveyor.yml index 1767c353..c18806d8 100644 --- a/.appveyor.yml +++ b/.appveyor.yml @@ -111,11 +111,18 @@ environment: # (Currently) the images up to 2017 use an older Cygwin # This tests that the library works with more recent versions - - FLAVOR: cygwin (64-bit, latest) + - FLAVOR: cygwin (64-bit, latest) C++11 APPVEYOR_BUILD_WORKER_IMAGE: Visual Studio 2022 ADDPATH: C:\cygwin64\bin; B2_ADDRESS_MODEL: 64 - B2_CXXSTD: 11,1z + B2_CXXSTD: 11 + B2_TOOLSET: gcc + # Split to avoid 1h timeout for multi-config runs + - FLAVOR: cygwin (64-bit, latest) C++17 + APPVEYOR_BUILD_WORKER_IMAGE: Visual Studio 2022 + ADDPATH: C:\cygwin64\bin; + B2_ADDRESS_MODEL: 64 + B2_CXXSTD: 1z B2_TOOLSET: gcc - FLAVOR: mingw64 (32-bit) diff --git a/doc/changelog.txt b/doc/changelog.txt index da943877..3e4fe789 100644 --- a/doc/changelog.txt +++ b/doc/changelog.txt @@ -8,6 +8,8 @@ /*! \page changelog Changelog +- 1.88.0 + - Fix parsing of numbers in floating point format to integers - 1.86.0 - Make ICU implementation of `to_title` threadsafe - Add allocator support to `utf_to_utf` diff --git a/src/boost/locale/icu/formatter.cpp b/src/boost/locale/icu/formatter.cpp index 12cb7363..955b3d18 100644 --- a/src/boost/locale/icu/formatter.cpp +++ b/src/boost/locale/icu/formatter.cpp @@ -54,7 +54,9 @@ namespace boost { namespace locale { namespace impl_icu { public: typedef std::basic_string string_type; - number_format(icu::NumberFormat& fmt, std::string codepage) : cvt_(codepage), icu_fmt_(fmt) {} + number_format(icu::NumberFormat& fmt, const std::string& codepage, bool isNumberOnly = false) : + cvt_(codepage), icu_fmt_(fmt), isNumberOnly_(isNumberOnly) + {} string_type format(double value, size_t& code_points) const override { return do_format(value, code_points); } string_type format(int64_t value, size_t& code_points) const override { return do_format(value, code_points); } @@ -107,6 +109,9 @@ namespace boost { namespace locale { namespace impl_icu { icu::ParsePosition pp; icu::UnicodeString tmp = cvt_.icu(str.data(), str.data() + str.size()); + // For the plain number parsing (no currency etc) parse "123.456" as 2 ints + // not a float later converted to int + icu_fmt_.setParseIntegerOnly(std::is_integral::value && isNumberOnly_); icu_fmt_.parse(tmp, val, pp); ValueType tmp_v; @@ -122,6 +127,7 @@ namespace boost { namespace locale { namespace impl_icu { icu_std_converter cvt_; icu::NumberFormat& icu_fmt_; + const bool isNumberOnly_; }; template @@ -355,7 +361,7 @@ namespace boost { namespace locale { namespace impl_icu { icu::NumberFormat& nf = cache.number_format((how == std::ios_base::scientific) ? num_fmt_type::sci : num_fmt_type::number); set_fraction_digits(nf, how, ios.precision()); - return ptr_type(new number_format(nf, encoding)); + return ptr_type(new number_format(nf, encoding, true)); } case currency: { icu::NumberFormat& nf = cache.number_format( diff --git a/test/formatting_common.hpp b/test/formatting_common.hpp new file mode 100644 index 00000000..97ef7919 --- /dev/null +++ b/test/formatting_common.hpp @@ -0,0 +1,90 @@ +// +// Copyright (c) 2024 Alexander Grund +// +// Distributed under the Boost Software License, Version 1.0. +// https://www.boost.org/LICENSE_1_0.txt + +#include +#include +#include +#include +#include + +#include "../src/boost/locale/util/foreach_char.hpp" +#include "boostLocale/test/tools.hpp" +#include "boostLocale/test/unit_test.hpp" + +template +void test_parse_multi_number_by_char(const std::locale& locale) +{ + // thousandsNum will mostly be 12,345 but some systems + // don't have the thousand separator for the POSIX locale. + // So use the formatted output. + const IntType expectedInt = 12345; + std::basic_ostringstream thousandsNum; + thousandsNum.imbue(locale); + thousandsNum << boost::locale::as::number << expectedInt; + + std::basic_istringstream stream; + stream.imbue(locale); + stream.str(ascii_to("42.") + thousandsNum.str()); + stream >> boost::locale::as::number; + + IntType value; + TEST_REQUIRE(stream >> value); + TEST_EQ(value, IntType(42)); + TEST_EQ(static_cast(stream.get()), '.'); + TEST_REQUIRE(stream >> value); + TEST_EQ(value, expectedInt); + TEST_REQUIRE(!(stream >> value)); + TEST(stream.eof()); + + stream.str(ascii_to("42.25,678")); + stream.clear(); + float fValue; + TEST_REQUIRE(stream >> fValue); + TEST_EQ(fValue, 42.25); + TEST_EQ(static_cast(stream.get()), ','); + TEST_REQUIRE(stream >> value); + TEST_EQ(value, IntType(678)); + TEST_REQUIRE(!(stream >> value)); + TEST(stream.eof()); + + // Parsing a floating point currency to integer truncates the floating point value but fully parses it + stream.str(ascii_to("USD1,234.55,67.89")); + stream.clear(); + TEST_REQUIRE(!(stream >> value)); + stream.clear(); + stream >> boost::locale::as::currency >> boost::locale::as::currency_iso; + if(stream >> value) { // Parsing currencies not fully supported by WinAPI backend + TEST_EQ(value, IntType(1234)); + TEST_EQ(static_cast(stream.get()), ','); + TEST_REQUIRE(stream >> boost::locale::as::number >> value); + TEST_EQ(value, IntType(67)); + TEST(!stream.eof()); + } +} + +/// Test that parsing multiple numbers without any spaces works as expected +void test_parse_multi_number() +{ + const auto locale = boost::locale::generator{}("en_US.UTF-8"); + +#define BOOST_LOCALE_CALL_I(T, I) \ + std::cout << "\t" #I << std::endl; \ + test_parse_multi_number_by_char(locale); + +#define BOOST_LOCALE_CALL(T) \ + std::cout << "test_parse_multi_number " #T << std::endl; \ + BOOST_LOCALE_CALL_I(T, int16_t); \ + BOOST_LOCALE_CALL_I(T, uint16_t); \ + BOOST_LOCALE_CALL_I(T, int32_t); \ + BOOST_LOCALE_CALL_I(T, uint32_t); \ + BOOST_LOCALE_CALL_I(T, int64_t); \ + BOOST_LOCALE_CALL_I(T, uint64_t); + + BOOST_LOCALE_CALL(char); + BOOST_LOCALE_CALL(wchar_t); +#undef BOOST_LOCALE_CALL +#undef BOOST_LOCALE_CALL_I +} \ No newline at end of file diff --git a/test/test_formatting.cpp b/test/test_formatting.cpp index a88f06bc..d418c583 100644 --- a/test/test_formatting.cpp +++ b/test/test_formatting.cpp @@ -21,6 +21,7 @@ #include "boostLocale/test/tools.hpp" #include "boostLocale/test/unit_test.hpp" +#include "formatting_common.hpp" const std::string test_locale_name = "en_US"; std::string message_path = "./"; @@ -928,6 +929,8 @@ void test_main(int argc, char** argv) test_manip(); test_format_class(); #endif + + test_parse_multi_number(); } // boostinspect:noascii diff --git a/test/test_posix_formatting.cpp b/test/test_posix_formatting.cpp index c6c46a0f..15c4d6d2 100644 --- a/test/test_posix_formatting.cpp +++ b/test/test_posix_formatting.cpp @@ -19,6 +19,7 @@ #endif #include "boostLocale/test/tools.hpp" #include "boostLocale/test/unit_test.hpp" +#include "formatting_common.hpp" #ifdef BOOST_LOCALE_NO_POSIX_BACKEND // Dummy just to make it compile @@ -185,6 +186,7 @@ void test_main(int /*argc*/, char** /*argv*/) TEST(v == "12345,45" || v == "12 345,45" || v == "12.345,45"); } } + test_parse_multi_number(); } // boostinspect:noascii diff --git a/test/test_std_formatting.cpp b/test/test_std_formatting.cpp index 35085c1b..79156ad0 100644 --- a/test/test_std_formatting.cpp +++ b/test/test_std_formatting.cpp @@ -14,6 +14,7 @@ #include "boostLocale/test/tools.hpp" #include "boostLocale/test/unit_test.hpp" +#include "formatting_common.hpp" template void test_by_char(const std::locale& l, const std::locale& lreal) @@ -230,6 +231,10 @@ void test_main(int /*argc*/, char** /*argv*/) } } } + // Std backend silently falls back to the C locale when the locale is not supported + // which breaks the test assumptions + if(has_std_locale("en_US.UTF-8")) + test_parse_multi_number(); } // boostinspect:noascii diff --git a/test/test_winapi_formatting.cpp b/test/test_winapi_formatting.cpp index 4ccb10b2..499e92a8 100644 --- a/test/test_winapi_formatting.cpp +++ b/test/test_winapi_formatting.cpp @@ -23,6 +23,7 @@ #include "../src/boost/locale/win32/lcid.hpp" #include "boostLocale/test/tools.hpp" #include "boostLocale/test/unit_test.hpp" +#include "formatting_common.hpp" template void test_by_char(const std::locale& l, std::string name, int lcid) @@ -176,6 +177,7 @@ void test_main(int /*argc*/, char** /*argv*/) test_by_char(l, name, name_lcid.second); } } + test_parse_multi_number(); std::cout << "- Testing strftime" << std::endl; test_date_time(gen("en_US.UTF-8")); }