diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 59c1c72..29d484b 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -20,6 +20,7 @@ jobs: sudo apt-add-repository ppa:ubuntu-toolchain-r/test sudo apt-get update sudo apt-get install libboost-test-dev + sudo apt-get install language-pack-fr # test serializer w/ locale sudo apt-get install ${{ matrix.compiler }} - name: Configure run: | @@ -48,6 +49,7 @@ jobs: sudo apt-add-repository ppa:ubuntu-toolchain-r/test sudo apt-get update sudo apt-get install libboost-test-dev + sudo apt-get install language-pack-fr # test serializer w/ locale sudo apt-get install clang-${{ matrix.compiler }} - name: Configure run: | @@ -80,6 +82,7 @@ jobs: sudo apt-add-repository ppa:mhier/libboost-latest sudo apt-get update sudo apt-get install boost1.70 + sudo apt-get install language-pack-fr # test serializer w/ locale sudo apt-get install ${{ matrix.compiler }} - name: Configure run: | @@ -119,6 +122,7 @@ jobs: sudo apt-add-repository ppa:mhier/libboost-latest sudo apt-get update sudo apt-get install boost1.70 + sudo apt-get install language-pack-fr # test serializer w/ locale sudo apt-get install clang-${{ matrix.compiler }} - name: Configure run: | @@ -216,7 +220,6 @@ jobs: - name: Test working-directory: ./build run: | - ./tests/test_literals --log_level=all file --mime-encoding tests/toml/tests/example.toml file --mime-encoding tests/toml/tests/fruit.toml file --mime-encoding tests/toml/tests/hard_example.toml diff --git a/tests/test_serialize_file.cpp b/tests/test_serialize_file.cpp index b6b8acb..40cb484 100644 --- a/tests/test_serialize_file.cpp +++ b/tests/test_serialize_file.cpp @@ -8,6 +8,8 @@ #include #include +#include + template class Table, template class Array> @@ -357,3 +359,47 @@ array_of_table = [ BOOST_TEST(parsed.at("array_of_table").at(0).comments().size() == 1u); BOOST_TEST(parsed.at("array_of_table").at(0).comments().front() == " comment about the first element (table)"); } + + +BOOST_AUTO_TEST_CASE(test_serialize_under_locale) +{ + // avoid null init (setlocale returns null when it failed) + std::string setloc(std::setlocale(LC_ALL, nullptr)); + + // fr_FR is a one of locales that uses `,` as a decimal separator. + if(const char* try_hyphen = std::setlocale(LC_ALL, "fr_FR.UTF-8")) + { + setloc = std::string(try_hyphen); + } + else if(const char* try_nohyphen = std::setlocale(LC_ALL, "fr_FR.utf8")) + { + setloc = std::string(try_nohyphen); + } + // In some envs, fr_FR locale has not been installed. Tests must work even in such a case. +// else +// { +// BOOST_TEST(false); +// } + BOOST_TEST_MESSAGE("current locale at the beginning of the test = " << setloc); + + const std::string str = R"( +pi = 3.14159 +large_int = 1234567890 +)"; + std::istringstream iss(str); + const auto ref = toml::parse(iss); + const auto serialized_str = toml::format(ref, /*width = */ 80); + + BOOST_TEST_MESSAGE("serialized = " << serialized_str); + + std::istringstream serialized_iss(serialized_str); + const auto serialized_ref = toml::parse(serialized_iss); + + BOOST_TEST(serialized_ref.at("pi").as_floating() == ref.at("pi").as_floating()); + BOOST_TEST(serialized_ref.at("large_int").as_integer() == ref.at("large_int").as_integer()); + + const std::string endloc(std::setlocale(LC_ALL, nullptr)); + BOOST_TEST_MESSAGE("current locale at the end of the test = " << endloc); + // check if serializer change global locale + BOOST_TEST(setloc == endloc); +} diff --git a/toml/serializer.hpp b/toml/serializer.hpp index 88ae775..592c33f 100644 --- a/toml/serializer.hpp +++ b/toml/serializer.hpp @@ -7,6 +7,14 @@ #include +#if defined(_WIN32) +#include +#elif defined(__APPLE__) || defined(__FreeBSD__) +#include +#elif defined(__linux__) +#include +#endif + #include "lexer.hpp" #include "value.hpp" @@ -120,7 +128,31 @@ struct serializer } std::string operator()(const integer_type i) const { - return std::to_string(i); +#if defined(_WIN32) + _configthreadlocale(_ENABLE_PER_THREAD_LOCALE); + const std::string original_locale(setlocale(LC_NUMERIC, nullptr)); + setlocale(LC_NUMERIC, "C"); +#elif defined(__APPLE__) || defined(__FreeBSD__) || defined(__linux__) + const auto c_locale = newlocale(LC_NUMERIC_MASK, "C", locale_t(0)); + locale_t original_locale(0); + if(c_locale != locale_t(0)) + { + original_locale = uselocale(c_locale); + } +#endif + + const auto str = std::to_string(i); + +#if defined(_WIN32) + setlocale(LC_NUMERIC, original_locale.c_str()); + _configthreadlocale(_DISABLE_PER_THREAD_LOCALE); +#elif defined(__APPLE__) || defined(__FreeBSD__) || defined(__linux__) + if(original_locale != locale_t(0)) + { + uselocale(original_locale); + } +#endif + return str; } std::string operator()(const floating_type f) const { @@ -147,12 +179,40 @@ struct serializer } } + // set locale to "C". + // To make it thread-local, we use OS-specific features. + // If we set process-global locale, it can break other thread that also + // outputs something simultaneously. +#if defined(_WIN32) + _configthreadlocale(_ENABLE_PER_THREAD_LOCALE); + const std::string original_locale(setlocale(LC_NUMERIC, nullptr)); + setlocale(LC_NUMERIC, "C"); +#elif defined(__APPLE__) || defined(__FreeBSD__) || defined(__linux__) + const auto c_locale = newlocale(LC_NUMERIC_MASK, "C", locale_t(0)); + locale_t original_locale(0); + if(c_locale != locale_t(0)) + { + original_locale = uselocale(c_locale); + } +#endif + const auto fmt = "%.*g"; const auto bsz = std::snprintf(nullptr, 0, fmt, this->float_prec_, f); // +1 for null character(\0) std::vector buf(static_cast(bsz + 1), '\0'); std::snprintf(buf.data(), buf.size(), fmt, this->float_prec_, f); + // restore the original locale +#if defined(_WIN32) + setlocale(LC_NUMERIC, original_locale.c_str()); + _configthreadlocale(_DISABLE_PER_THREAD_LOCALE); +#elif defined(__APPLE__) || defined(__FreeBSD__) || defined(__linux__) + if(original_locale != locale_t(0)) + { + uselocale(original_locale); + } +#endif + std::string token(buf.begin(), std::prev(buf.end())); if(!token.empty() && token.back() == '.') // 1. => 1.0 {