mirror of
https://github.com/nlohmann/json.git
synced 2026-11-03 03:15:34 +00:00
* CI: measure line coverage without lcov's unexecuted-block adjustment Drop geninfo_unexecuted_blocks=1 from the lcov capture of ci_test_coverage: it reset every line of an inlined or templated function to 0 as soon as one copy had an unexecuted block, which understated the line coverage by about three points. Add -fvar-tracking-assignments to both coverage builds. At -O0, GCC inlines always_inline functions before it places the coverage counters, so the call sites of such helpers (put_literal() and others) got no line record and showed as 0 in an -O3 translation unit that never reaches them. With the flag, the call sites carry counts in every translation unit. The generated code is identical. Signed-off-by: Niels Lohmann <mail@nlohmann.me> * Remove unreachable code found by the coverage analysis Remove code that no input can reach, and mark the lines that cannot be covered with LCOV_EXCL_LINE: - BJData writer: the dry_run parameter of the ND-array element writers was always true; replace them by range checks. - BON8 writer: call throw_integer_out_of_range() directly instead of write_bon8_value() for values that always throw. - binary_writer: integer_bits() is only called for integers of up to 64 bits. - serializer, view serializer, lexer: replace guards that the callers make unreachable by assertions or by the simpler code (surrogate pairs under ensure_ascii, next_unget, '-' at the end of the buffer). - json_view: drop unreachable branches in scalar(), set(), check_utf8_scan(), append_input_impl() and unescape(). - Mark exception-cleanup braces, 2 GiB limits, big-endian and 32-bit code, and fall-through case labels with LCOV_EXCL_LINE. No behavior changes for any input that reaches these functions. Signed-off-by: Niels Lohmann <mail@nlohmann.me> * Tests: cover the remaining lines of the library headers Add tests for the lines of include/nlohmann that were not covered: the binary readers and writers, the lexer and locale paths, the json_view modules, the custom number, key and array types, json_pointer, json_patch, diff and the iterators. Signed-off-by: Niels Lohmann <mail@nlohmann.me> * Fix test failures in the coverage tests - unit-custom-object-key-type: remove the unused make_strict_key(strict_key) overload, the unused range constructor and default constructor, and guard data() with JSON_DIAGNOSTICS (unused-function/-member-function errors with GCC and clang -Weverything). - unit-json_view_compare: move the two new sections to the new unit-json_view_compare_values.cpp, as the object file got too large for the MinGW assembler again, and use a std::map with std::greater instead of std::unordered_map, which does not accept the incomplete basic_json (clang 3.9, icpc). - unit-json_view_dump: rename the variables that hid outer ones (MSVC C4456). - unit-stream-bulk-scan: restore the real size of the oversized get area before the adapter is destroyed (JSON_PRECISE_STREAM_POSITION reads the character at the read position), and skip the test where the streambuf cannot hold a get area larger than an int (MSVC). - unit-regression1: with JSON_NO_THREAD_LOCAL, copies are built member by member and keep correct timestamps, so check the corresponding outcome. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01N5NasAgbDGG28MqXjQvLqp Signed-off-by: Claude <noreply@anthropic.com> * Return std::size_t from the hash helper in the ordered_json test MSVC (Win32) rejected the implicit conversion of the unsigned long long product to the 32-bit std::size_t (C4244 as an error). Also note when JSON_NO_THREAD_LOCAL is set in the regression test. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01N5NasAgbDGG28MqXjQvLqp Signed-off-by: Claude <noreply@anthropic.com> --------- Signed-off-by: Niels Lohmann <mail@nlohmann.me> Signed-off-by: Claude <noreply@anthropic.com> Co-authored-by: Claude <noreply@anthropic.com>
701 lines
22 KiB
C++
701 lines
22 KiB
C++
// __ _____ _____ _____
|
|
// __| | __| | | | JSON for Modern C++ (supporting code)
|
|
// | | |__ | | | | | | version 3.12.0
|
|
// |_____|_____|_____|_|___| https://github.com/nlohmann/json
|
|
//
|
|
// SPDX-FileCopyrightText: 2013-2026 Niels Lohmann <https://nlohmann.me>
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
#include "doctest_compatibility.h"
|
|
|
|
#define JSON_TESTS_PRIVATE
|
|
#include <nlohmann/json.hpp>
|
|
using nlohmann::json;
|
|
|
|
#include <array>
|
|
#include <clocale>
|
|
#include <limits>
|
|
#include <map>
|
|
#include <ostream>
|
|
#include <streambuf>
|
|
#include <string>
|
|
#include <utility>
|
|
#include <vector>
|
|
|
|
struct ParserImpl final: public nlohmann::json_sax<json>
|
|
{
|
|
bool null() override
|
|
{
|
|
return true;
|
|
}
|
|
bool boolean(bool /*val*/) override
|
|
{
|
|
return true;
|
|
}
|
|
bool number_integer(json::number_integer_t /*val*/) override
|
|
{
|
|
return true;
|
|
}
|
|
bool number_unsigned(json::number_unsigned_t /*val*/) override
|
|
{
|
|
return true;
|
|
}
|
|
bool number_float(json::number_float_t /*val*/, const json::string_t& s) override
|
|
{
|
|
float_string_copy = s;
|
|
return true;
|
|
}
|
|
bool string(json::string_t& /*val*/) override
|
|
{
|
|
return true;
|
|
}
|
|
bool binary(json::binary_t& /*val*/) override
|
|
{
|
|
return true;
|
|
}
|
|
bool start_object(std::size_t /*val*/) override
|
|
{
|
|
return true;
|
|
}
|
|
bool key(json::string_t& /*val*/) override
|
|
{
|
|
return true;
|
|
}
|
|
bool end_object() override
|
|
{
|
|
return true;
|
|
}
|
|
bool start_array(std::size_t /*val*/) override
|
|
{
|
|
return true;
|
|
}
|
|
bool end_array() override
|
|
{
|
|
return true;
|
|
}
|
|
bool parse_error(std::size_t /*val*/, const std::string& /*val*/, const nlohmann::detail::exception& /*val*/) override
|
|
{
|
|
return false;
|
|
}
|
|
|
|
~ParserImpl() override;
|
|
|
|
ParserImpl()
|
|
: float_string_copy("not set")
|
|
{}
|
|
|
|
ParserImpl(const ParserImpl& other)
|
|
: float_string_copy(other.float_string_copy)
|
|
{}
|
|
|
|
ParserImpl(ParserImpl&& other) noexcept
|
|
: float_string_copy(std::move(other.float_string_copy))
|
|
{}
|
|
|
|
ParserImpl& operator=(const ParserImpl& other)
|
|
{
|
|
if (this != &other)
|
|
{
|
|
float_string_copy = other.float_string_copy;
|
|
}
|
|
return *this;
|
|
}
|
|
|
|
ParserImpl& operator=(ParserImpl&& other) noexcept
|
|
{
|
|
if (this != &other)
|
|
{
|
|
float_string_copy = std::move(other.float_string_copy);
|
|
}
|
|
return *this;
|
|
}
|
|
|
|
json::string_t float_string_copy;
|
|
};
|
|
|
|
ParserImpl::~ParserImpl() = default;
|
|
|
|
TEST_CASE("locale-dependent test (LC_NUMERIC=C)")
|
|
{
|
|
WARN_MESSAGE(std::setlocale(LC_NUMERIC, "C") != nullptr, "could not set locale");
|
|
|
|
SECTION("check if locale is properly set")
|
|
{
|
|
std::array<char, 6> buffer = {};
|
|
CHECK(std::snprintf(buffer.data(), buffer.size(), "%.2f", 12.34) == 5); // NOLINT(cppcoreguidelines-pro-type-vararg,hicpp-vararg)
|
|
CHECK(std::string(buffer.data()) == "12.34");
|
|
}
|
|
|
|
SECTION("parsing")
|
|
{
|
|
CHECK(json::parse("12.34").dump() == "12.34");
|
|
}
|
|
|
|
SECTION("SAX parsing")
|
|
{
|
|
ParserImpl sax {};
|
|
json::sax_parse( "12.34", &sax );
|
|
CHECK(sax.float_string_copy == "12.34");
|
|
}
|
|
}
|
|
|
|
TEST_CASE("locale-dependent test (LC_NUMERIC=de_DE)")
|
|
{
|
|
if (std::setlocale(LC_NUMERIC, "de_DE") != nullptr)
|
|
{
|
|
SECTION("check if locale is properly set")
|
|
{
|
|
std::array<char, 6> buffer = {};
|
|
CHECK(std::snprintf(buffer.data(), buffer.size(), "%.2f", 12.34) == 5); // NOLINT(cppcoreguidelines-pro-type-vararg,hicpp-vararg)
|
|
const auto snprintf_result = std::string(buffer.data());
|
|
if (snprintf_result != "12,34")
|
|
{
|
|
CAPTURE(snprintf_result)
|
|
MESSAGE("To test if number parsing is locale-independent, we set the locale to de_DE. However, on this system, the decimal separator doesn't change to `,` potentially due to a known musl issue (https://github.com/nlohmann/json/issues/4767).");
|
|
}
|
|
}
|
|
|
|
SECTION("parsing")
|
|
{
|
|
CHECK(json::parse("12.34").dump() == "12.34");
|
|
}
|
|
|
|
SECTION("SAX parsing")
|
|
{
|
|
ParserImpl sax{};
|
|
json::sax_parse("12.34", &sax);
|
|
CHECK(sax.float_string_copy == "12.34");
|
|
}
|
|
|
|
SECTION("serializing a long double")
|
|
{
|
|
// a floating-point type that is not a float or a double is written
|
|
// with snprintf, whose locale-specific decimal point and thousands
|
|
// separator are undone afterwards
|
|
using long_double_json = nlohmann::json::with_float_t<long double>;
|
|
CHECK(long_double_json(12345.5L).dump() == "12345.5");
|
|
CHECK(long_double_json(1.0L).dump() == "1.0");
|
|
CHECK(long_double_json(-0.25L).dump() == "-0.25");
|
|
}
|
|
}
|
|
else
|
|
{
|
|
MESSAGE("locale de_DE is not usable");
|
|
}
|
|
}
|
|
|
|
namespace
|
|
{
|
|
// records the numbers of a flat array and switches LC_NUMERIC to the given
|
|
// locale once the array opens - after the lexer was constructed, but before
|
|
// any number in the array is lexed
|
|
struct LocaleSwitchingSax final: public nlohmann::json_sax<json>
|
|
{
|
|
explicit LocaleSwitchingSax(const char* switch_to)
|
|
: locale_after_open(switch_to)
|
|
{}
|
|
|
|
bool null() override
|
|
{
|
|
return true;
|
|
}
|
|
bool boolean(bool /*val*/) override
|
|
{
|
|
return true;
|
|
}
|
|
bool number_integer(json::number_integer_t /*val*/) override
|
|
{
|
|
return true;
|
|
}
|
|
bool number_unsigned(json::number_unsigned_t /*val*/) override
|
|
{
|
|
return true;
|
|
}
|
|
bool number_float(json::number_float_t val, const json::string_t& s) override
|
|
{
|
|
values.push_back(val);
|
|
strings.push_back(s);
|
|
return true;
|
|
}
|
|
bool string(json::string_t& /*val*/) override
|
|
{
|
|
return true;
|
|
}
|
|
bool binary(json::binary_t& /*val*/) override
|
|
{
|
|
return true;
|
|
}
|
|
bool start_object(std::size_t /*val*/) override
|
|
{
|
|
return true;
|
|
}
|
|
bool key(json::string_t& /*val*/) override
|
|
{
|
|
return true;
|
|
}
|
|
bool end_object() override
|
|
{
|
|
return true;
|
|
}
|
|
bool start_array(std::size_t /*val*/) override
|
|
{
|
|
switched = std::setlocale(LC_NUMERIC, locale_after_open.c_str()) != nullptr;
|
|
return true;
|
|
}
|
|
bool end_array() override
|
|
{
|
|
return true;
|
|
}
|
|
bool parse_error(std::size_t /*val*/, const std::string& /*val*/, const nlohmann::detail::exception& /*val*/) override
|
|
{
|
|
return false;
|
|
}
|
|
|
|
std::string locale_after_open;
|
|
bool switched = false;
|
|
std::vector<json::number_float_t> values {}; // NOLINT(readability-redundant-member-init)
|
|
std::vector<json::string_t> strings {}; // NOLINT(readability-redundant-member-init)
|
|
};
|
|
} // namespace
|
|
|
|
TEST_CASE("locale changes between lexer construction and number conversion (#5198)")
|
|
{
|
|
// float and double are converted without the locale. A long double that
|
|
// is not binary64 can take the strtold fallback, which honors the locale
|
|
// that is current at conversion time. The numbers are chosen so that it
|
|
// does: too many significant digits for Clinger's fast path, an underflow
|
|
// that std::from_chars rejects, and a plain value.
|
|
const std::vector<std::string> numbers = {"3.14159265358979323846", "1.5e-400", "12.34", "-0.000123456789012345678"};
|
|
std::string text = "[";
|
|
for (const auto& n : numbers)
|
|
{
|
|
text += (text.size() == 1 ? "" : ",") + n;
|
|
}
|
|
text += "]";
|
|
|
|
using long_double_json = nlohmann::json::with_float_t<long double>;
|
|
|
|
// reference values, parsed without a locale switch
|
|
REQUIRE(std::setlocale(LC_NUMERIC, "C") != nullptr);
|
|
const json expected = json::parse(text);
|
|
const long_double_json expected_ld = long_double_json::parse(text);
|
|
|
|
const std::array<std::pair<const char*, const char*>, 2> transitions =
|
|
{
|
|
{
|
|
{"C", "de_DE"},
|
|
{"de_DE", "C"}
|
|
}
|
|
};
|
|
|
|
for (const auto& transition : transitions)
|
|
{
|
|
CAPTURE(transition.first)
|
|
CAPTURE(transition.second)
|
|
|
|
if (std::setlocale(LC_NUMERIC, transition.first) == nullptr)
|
|
{
|
|
MESSAGE("locale is not usable");
|
|
continue;
|
|
}
|
|
|
|
// SAX parsing
|
|
{
|
|
LocaleSwitchingSax sax(transition.second);
|
|
CHECK(json::sax_parse(text, &sax));
|
|
if (sax.switched)
|
|
{
|
|
CHECK(sax.values == expected.get<std::vector<json::number_float_t>>());
|
|
CHECK(sax.strings == numbers);
|
|
}
|
|
}
|
|
|
|
// DOM parsing with a callback
|
|
{
|
|
bool switched = false;
|
|
const auto cb = [&](int /*depth*/, json::parse_event_t event, json& /*parsed*/) noexcept
|
|
{
|
|
if (event == json::parse_event_t::array_start)
|
|
{
|
|
switched = std::setlocale(LC_NUMERIC, transition.second) != nullptr;
|
|
}
|
|
return true;
|
|
};
|
|
const json j = json::parse(text, cb);
|
|
if (switched)
|
|
{
|
|
CHECK(j == expected);
|
|
}
|
|
}
|
|
|
|
// a long double goes through std::strtold unless it is binary64 or
|
|
// std::from_chars supports it
|
|
{
|
|
bool switched = false;
|
|
const auto cb = [&](int /*depth*/, long_double_json::parse_event_t event, long_double_json& /*parsed*/) noexcept
|
|
{
|
|
if (event == long_double_json::parse_event_t::array_start)
|
|
{
|
|
switched = std::setlocale(LC_NUMERIC, transition.second) != nullptr;
|
|
}
|
|
return true;
|
|
};
|
|
const long_double_json j = long_double_json::parse(text, cb);
|
|
if (switched)
|
|
{
|
|
CHECK(j == expected_ld);
|
|
}
|
|
}
|
|
}
|
|
|
|
CHECK(std::setlocale(LC_NUMERIC, "C") != nullptr);
|
|
}
|
|
|
|
TEST_CASE("locale with a multi-byte decimal point")
|
|
{
|
|
// Some locales use a decimal point that is not a single character, e.g.
|
|
// U+066B ARABIC DECIMAL SEPARATOR (two bytes in UTF-8). It cannot be
|
|
// substituted in place for '.', so the strtold fallback (only for long
|
|
// double formats other than binary64) converts a copy of the token with
|
|
// the whole decimal point instead (#5660). The values must be those of the
|
|
// "C" locale.
|
|
using long_double_json = nlohmann::basic_json<std::map, std::vector, std::string, bool, std::int64_t, std::uint64_t, long double>;
|
|
const char* const long_double_numbers = "[3.14159265358979323846, 1.5e-400, -0.000123456789012345678]";
|
|
REQUIRE(std::setlocale(LC_NUMERIC, "C") != nullptr);
|
|
const long_double_json expected_long_double = long_double_json::parse(long_double_numbers);
|
|
|
|
const std::array<const char*, 6> names = {{"ar_EG.UTF-8", "ar_SA.UTF-8", "fa_IR.UTF-8", "ps_AF.UTF-8", "ar_EG", "fa_IR"}};
|
|
bool tested = false;
|
|
for (const char* name : names)
|
|
{
|
|
if (std::setlocale(LC_NUMERIC, name) == nullptr)
|
|
{
|
|
continue;
|
|
}
|
|
const std::string decimal_point = std::localeconv()->decimal_point;
|
|
if (decimal_point.size() < 2)
|
|
{
|
|
continue;
|
|
}
|
|
CAPTURE(name)
|
|
tested = true;
|
|
|
|
// too many significant digits for Clinger's fast path, and an underflow
|
|
// that std::from_chars rejects: double does not depend on the locale
|
|
json j;
|
|
CHECK_NOTHROW(j = json::parse("[3.14159265358979323846, 1.5e-400, -0.000123456789012345678]"));
|
|
CHECK(j.is_array());
|
|
CHECK(j[0] == 3.14159265358979323846);
|
|
CHECK(j[1] == 0.0);
|
|
CHECK(j[2] == -0.000123456789012345678);
|
|
CHECK(json::accept("3.14159265358979323846"));
|
|
|
|
// a long double that reaches the strtold fallback is not truncated
|
|
long_double_json ld;
|
|
CHECK_NOTHROW(ld = long_double_json::parse(long_double_numbers));
|
|
CHECK(ld == expected_long_double);
|
|
|
|
// a value the locale-independent paths convert is not affected
|
|
CHECK(json::parse("12.5") == 12.5);
|
|
}
|
|
if (!tested)
|
|
{
|
|
MESSAGE("no locale with a multi-byte decimal point is usable");
|
|
}
|
|
|
|
CHECK(std::setlocale(LC_NUMERIC, "C") != nullptr);
|
|
}
|
|
|
|
namespace
|
|
{
|
|
// a streambuf that switches LC_NUMERIC the first time anything is written to
|
|
// it, so a dump() in progress can be made to change locale mid-flight: after
|
|
// the serializer was constructed (and, before #5709 item 3, after it had
|
|
// cached std::localeconv() for the whole call) but before a later float is
|
|
// converted
|
|
struct LocaleSwitchingStreambuf final : std::streambuf
|
|
{
|
|
explicit LocaleSwitchingStreambuf(const char* switch_to)
|
|
: locale_after_first_write(switch_to)
|
|
{}
|
|
|
|
std::string data {}; // NOLINT(readability-redundant-member-init)
|
|
std::string locale_after_first_write;
|
|
bool switched = false;
|
|
|
|
protected:
|
|
std::streamsize xsputn(const char* s, std::streamsize n) override
|
|
{
|
|
if (!switched)
|
|
{
|
|
switched = std::setlocale(LC_NUMERIC, locale_after_first_write.c_str()) != nullptr;
|
|
}
|
|
data.append(s, static_cast<std::size_t>(n));
|
|
return n;
|
|
}
|
|
};
|
|
} // namespace
|
|
|
|
TEST_CASE("locale changes during a single dump() (#5709 item 3)")
|
|
{
|
|
// dump_float() only reads the locale on the snprintf path, taken for a
|
|
// number_float_t that is not an IEEE-754 single or double, i.e. not
|
|
// (is_iec559 && digits == 24 && max_exponent == 128) and not (is_iec559
|
|
// && digits == 53 && max_exponent == 1024) - see dump_float(). Checking
|
|
// is_iec559 alone is not enough: on x86_64, long double is a 64-bit
|
|
// (80-bit extended) format for which is_iec559 is also true, so it still
|
|
// takes the snprintf path this test means to exercise. Only a
|
|
// number_float_t whose digits/max_exponent match float or double (e.g.
|
|
// long double on 64-bit Arm, where it is IEEE-754 double) takes the
|
|
// locale-independent to_chars() path instead, and this test is a no-op
|
|
// there.
|
|
using long_double_json = nlohmann::json::with_float_t<long double>;
|
|
using ld_limits = std::numeric_limits<long_double_json::number_float_t>;
|
|
const bool is_ieee_single_or_double =
|
|
(ld_limits::is_iec559 && ld_limits::digits == 24 && ld_limits::max_exponent == 128) ||
|
|
(ld_limits::is_iec559 && ld_limits::digits == 53 && ld_limits::max_exponent == 1024);
|
|
if (is_ieee_single_or_double)
|
|
{
|
|
MESSAGE("long double is IEEE-754 single or double on this platform; dump_float()'s snprintf/locale path is not exercised here");
|
|
}
|
|
|
|
const char* de_DE_name = "de_DE.UTF-8";
|
|
if (std::setlocale(LC_NUMERIC, de_DE_name) == nullptr)
|
|
{
|
|
de_DE_name = "de_DE";
|
|
if (std::setlocale(LC_NUMERIC, de_DE_name) == nullptr)
|
|
{
|
|
MESSAGE("locale de_DE is not usable");
|
|
return;
|
|
}
|
|
}
|
|
const std::string decimal_point = std::localeconv()->decimal_point;
|
|
REQUIRE(std::setlocale(LC_NUMERIC, "C") != nullptr);
|
|
if (decimal_point != ",")
|
|
{
|
|
MESSAGE("de_DE's decimal point is not ',' on this platform, skipping");
|
|
return;
|
|
}
|
|
|
|
// a string long enough to overflow the serializer's internal write
|
|
// buffer, so that it is flushed to the output adapter - and the locale
|
|
// switched - before the number after it is converted
|
|
const std::string padding(5000, 'a');
|
|
const long_double_json j = { padding, 1234.5L };
|
|
|
|
LocaleSwitchingStreambuf buf(de_DE_name);
|
|
std::ostream os(&buf);
|
|
os << j;
|
|
CHECK(std::setlocale(LC_NUMERIC, "C") != nullptr);
|
|
|
|
REQUIRE(buf.switched);
|
|
// whatever locale was in effect when the float was actually converted,
|
|
// the output is normalized to use '.' as the decimal point: it must be
|
|
// looked up at conversion time, not once for the whole dump() - the same
|
|
// fix #5597 made on the parser side
|
|
CHECK(buf.data == "[\"" + padding + "\",1234.5]");
|
|
}
|
|
|
|
namespace
|
|
{
|
|
// a token whose first data() call switches LC_NUMERIC, so that the locale
|
|
// changes between convert_float_locale_aware() looking up the decimal point
|
|
// and strtod reading the token (the race of #5198, made deterministic)
|
|
struct locale_switching_token
|
|
{
|
|
using value_type = char;
|
|
|
|
locale_switching_token(std::string text_, const char* switch_to_)
|
|
: text(std::move(text_)), switch_to(switch_to_)
|
|
{}
|
|
|
|
std::size_t size() const
|
|
{
|
|
return text.size();
|
|
}
|
|
|
|
char& operator[](std::size_t i)
|
|
{
|
|
return text[i];
|
|
}
|
|
|
|
char* data()
|
|
{
|
|
if (!switched)
|
|
{
|
|
switched = std::setlocale(LC_NUMERIC, switch_to) != nullptr;
|
|
}
|
|
return &text[0];
|
|
}
|
|
|
|
std::string text;
|
|
const char* switch_to;
|
|
bool switched = false;
|
|
};
|
|
|
|
// set LC_NUMERIC to the first of @a names that is usable and has a decimal
|
|
// point of @a min_size bytes at least (and at most @a max_size)
|
|
bool use_decimal_point_locale(const std::vector<const char*>& names, std::size_t min_size, std::size_t max_size, const char*& used)
|
|
{
|
|
for (const char* name : names)
|
|
{
|
|
if (std::setlocale(LC_NUMERIC, name) == nullptr)
|
|
{
|
|
continue;
|
|
}
|
|
const std::size_t size = std::string(std::localeconv()->decimal_point).size();
|
|
if (size >= min_size && size <= max_size)
|
|
{
|
|
used = name;
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
// convert a token with convert_float_locale_aware(); the token must be given
|
|
// back as it was
|
|
template<typename FloatType>
|
|
FloatType convert_locale_aware(const std::string& text)
|
|
{
|
|
std::string token = text;
|
|
const std::size_t dot = token.find('.');
|
|
FloatType value{};
|
|
nlohmann::detail::convert_float_locale_aware(token, dot, value);
|
|
CHECK(token == text);
|
|
return value;
|
|
}
|
|
|
|
template<typename FloatType>
|
|
void check_locale_aware_conversions()
|
|
{
|
|
CHECK(convert_locale_aware<FloatType>("3.5") == FloatType(3.5));
|
|
CHECK(convert_locale_aware<FloatType>("-0.25") == FloatType(-0.25));
|
|
CHECK(convert_locale_aware<FloatType>("1.25e2") == FloatType(125));
|
|
// no decimal point at all
|
|
CHECK(convert_locale_aware<FloatType>("12e1") == FloatType(120));
|
|
CHECK(convert_locale_aware<FloatType>("7") == FloatType(7));
|
|
}
|
|
} // namespace
|
|
|
|
TEST_CASE("convert_float_locale_aware")
|
|
{
|
|
const std::vector<const char*> single_byte_names = {"de_DE.UTF-8", "de_DE", "fr_FR.UTF-8", "fr_FR", "ru_RU.UTF-8"};
|
|
const std::vector<const char*> multi_byte_names = {"ar_EG.UTF-8", "ar_SA.UTF-8", "fa_IR.UTF-8", "ps_AF.UTF-8", "ar_EG", "fa_IR"};
|
|
const char* used = nullptr;
|
|
|
|
SECTION("decimal point '.'")
|
|
{
|
|
REQUIRE(std::setlocale(LC_NUMERIC, "C") != nullptr);
|
|
check_locale_aware_conversions<float>();
|
|
check_locale_aware_conversions<double>();
|
|
check_locale_aware_conversions<long double>();
|
|
}
|
|
|
|
SECTION("single-byte decimal point other than '.'")
|
|
{
|
|
if (use_decimal_point_locale(single_byte_names, 1, 1, used) && std::localeconv()->decimal_point[0] != '.')
|
|
{
|
|
CAPTURE(used)
|
|
check_locale_aware_conversions<float>();
|
|
check_locale_aware_conversions<double>();
|
|
check_locale_aware_conversions<long double>();
|
|
}
|
|
else
|
|
{
|
|
MESSAGE("no locale with a single-byte decimal point other than '.' is usable");
|
|
}
|
|
}
|
|
|
|
SECTION("multi-byte decimal point")
|
|
{
|
|
if (use_decimal_point_locale(multi_byte_names, 2, 8, used))
|
|
{
|
|
CAPTURE(used)
|
|
check_locale_aware_conversions<float>();
|
|
check_locale_aware_conversions<double>();
|
|
check_locale_aware_conversions<long double>();
|
|
}
|
|
else
|
|
{
|
|
MESSAGE("no locale with a multi-byte decimal point is usable");
|
|
}
|
|
}
|
|
|
|
SECTION("a token that is not converted completely")
|
|
{
|
|
// the locale does not change, so there is nothing to retry: the
|
|
// value up to the garbage is kept
|
|
REQUIRE(std::setlocale(LC_NUMERIC, "C") != nullptr);
|
|
std::string token = "1.5x";
|
|
double value = 0;
|
|
nlohmann::detail::convert_float_locale_aware(token, token.find('.'), value);
|
|
CHECK(value == 1.5);
|
|
CHECK(token == "1.5x");
|
|
}
|
|
|
|
SECTION("the locale changes before the conversion")
|
|
{
|
|
if (!use_decimal_point_locale(single_byte_names, 1, 1, used) || std::localeconv()->decimal_point[0] == '.')
|
|
{
|
|
MESSAGE("no locale with a single-byte decimal point other than '.' is usable");
|
|
}
|
|
else
|
|
{
|
|
const std::string comma_locale = used;
|
|
REQUIRE(std::setlocale(LC_NUMERIC, "C") != nullptr);
|
|
|
|
// "C" -> comma locale: strtod stops at the '.', so the conversion is repeated
|
|
{
|
|
locale_switching_token token("1.5", comma_locale.c_str());
|
|
double value = 0;
|
|
nlohmann::detail::convert_float_locale_aware(token, 1, value);
|
|
if (token.switched)
|
|
{
|
|
CHECK(value == 1.5);
|
|
CHECK(token.text == "1.5");
|
|
}
|
|
}
|
|
|
|
// the same for the other floating-point types
|
|
REQUIRE(std::setlocale(LC_NUMERIC, "C") != nullptr);
|
|
{
|
|
locale_switching_token token("2.25", comma_locale.c_str());
|
|
float value = 0;
|
|
nlohmann::detail::convert_float_locale_aware(token, 1, value);
|
|
if (token.switched)
|
|
{
|
|
CHECK(value == 2.25f);
|
|
}
|
|
}
|
|
REQUIRE(std::setlocale(LC_NUMERIC, "C") != nullptr);
|
|
{
|
|
locale_switching_token token("2.25", comma_locale.c_str());
|
|
long double value = 0;
|
|
nlohmann::detail::convert_float_locale_aware(token, 1, value);
|
|
if (token.switched)
|
|
{
|
|
CHECK(value == 2.25L);
|
|
}
|
|
}
|
|
|
|
// comma locale -> multi-byte decimal point
|
|
const char* multi_byte = nullptr;
|
|
if (use_decimal_point_locale(multi_byte_names, 2, 8, multi_byte))
|
|
{
|
|
REQUIRE(std::setlocale(LC_NUMERIC, comma_locale.c_str()) != nullptr);
|
|
locale_switching_token token("1.5", multi_byte);
|
|
double value = 0;
|
|
nlohmann::detail::convert_float_locale_aware(token, 1, value);
|
|
if (token.switched)
|
|
{
|
|
CHECK(value == 1.5);
|
|
CHECK(token.text == "1.5");
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
CHECK(std::setlocale(LC_NUMERIC, "C") != nullptr);
|
|
}
|