fix #218: consider locale while serialization

This commit is contained in:
ToruNiina 2023-05-29 23:18:38 +09:00
commit 1340692442
3 changed files with 111 additions and 2 deletions

View File

@ -20,6 +20,7 @@ jobs:
sudo apt-add-repository ppa:ubuntu-toolchain-r/test sudo apt-add-repository ppa:ubuntu-toolchain-r/test
sudo apt-get update sudo apt-get update
sudo apt-get install libboost-test-dev sudo apt-get install libboost-test-dev
sudo apt-get install language-pack-fr # test serializer w/ locale
sudo apt-get install ${{ matrix.compiler }} sudo apt-get install ${{ matrix.compiler }}
- name: Configure - name: Configure
run: | run: |
@ -48,6 +49,7 @@ jobs:
sudo apt-add-repository ppa:ubuntu-toolchain-r/test sudo apt-add-repository ppa:ubuntu-toolchain-r/test
sudo apt-get update sudo apt-get update
sudo apt-get install libboost-test-dev 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 }} sudo apt-get install clang-${{ matrix.compiler }}
- name: Configure - name: Configure
run: | run: |
@ -80,6 +82,7 @@ jobs:
sudo apt-add-repository ppa:mhier/libboost-latest sudo apt-add-repository ppa:mhier/libboost-latest
sudo apt-get update sudo apt-get update
sudo apt-get install boost1.70 sudo apt-get install boost1.70
sudo apt-get install language-pack-fr # test serializer w/ locale
sudo apt-get install ${{ matrix.compiler }} sudo apt-get install ${{ matrix.compiler }}
- name: Configure - name: Configure
run: | run: |
@ -119,6 +122,7 @@ jobs:
sudo apt-add-repository ppa:mhier/libboost-latest sudo apt-add-repository ppa:mhier/libboost-latest
sudo apt-get update sudo apt-get update
sudo apt-get install boost1.70 sudo apt-get install boost1.70
sudo apt-get install language-pack-fr # test serializer w/ locale
sudo apt-get install clang-${{ matrix.compiler }} sudo apt-get install clang-${{ matrix.compiler }}
- name: Configure - name: Configure
run: | run: |
@ -216,7 +220,6 @@ jobs:
- name: Test - name: Test
working-directory: ./build working-directory: ./build
run: | run: |
./tests/test_literals --log_level=all
file --mime-encoding tests/toml/tests/example.toml file --mime-encoding tests/toml/tests/example.toml
file --mime-encoding tests/toml/tests/fruit.toml file --mime-encoding tests/toml/tests/fruit.toml
file --mime-encoding tests/toml/tests/hard_example.toml file --mime-encoding tests/toml/tests/hard_example.toml

View File

@ -8,6 +8,8 @@
#include <map> #include <map>
#include <sstream> #include <sstream>
#include <clocale>
template<typename Comment, template<typename Comment,
template<typename ...> class Table, template<typename ...> class Table,
template<typename ...> class Array> template<typename ...> 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().size() == 1u);
BOOST_TEST(parsed.at("array_of_table").at(0).comments().front() == " comment about the first element (table)"); 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);
}

View File

@ -7,6 +7,14 @@
#include <limits> #include <limits>
#if defined(_WIN32)
#include <locale.h>
#elif defined(__APPLE__) || defined(__FreeBSD__)
#include <xlocale.h>
#elif defined(__linux__)
#include <locale.h>
#endif
#include "lexer.hpp" #include "lexer.hpp"
#include "value.hpp" #include "value.hpp"
@ -120,7 +128,31 @@ struct serializer
} }
std::string operator()(const integer_type i) const 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 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 fmt = "%.*g";
const auto bsz = std::snprintf(nullptr, 0, fmt, this->float_prec_, f); const auto bsz = std::snprintf(nullptr, 0, fmt, this->float_prec_, f);
// +1 for null character(\0) // +1 for null character(\0)
std::vector<char> buf(static_cast<std::size_t>(bsz + 1), '\0'); std::vector<char> buf(static_cast<std::size_t>(bsz + 1), '\0');
std::snprintf(buf.data(), buf.size(), fmt, this->float_prec_, f); 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())); std::string token(buf.begin(), std::prev(buf.end()));
if(!token.empty() && token.back() == '.') // 1. => 1.0 if(!token.empty() && token.back() == '.') // 1. => 1.0
{ {