diff --git a/include/nlohmann/detail/output/serializer.hpp b/include/nlohmann/detail/output/serializer.hpp index 0c004c491..235e7bafd 100644 --- a/include/nlohmann/detail/output/serializer.hpp +++ b/include/nlohmann/detail/output/serializer.hpp @@ -8,7 +8,7 @@ #pragma once -#include // remove, fill, find, none_of, min +#include // copy, none_of, min, search #include // array #include // localeconv, lconv #include // isfinite @@ -2080,31 +2080,35 @@ class serializer // check if the buffer was large enough JSON_ASSERT(static_cast(len) < number_buffer.size()); - // look up the locale's thousands separator and decimal point now, - // matching what snprintf_float() just used (see lexer::get_decimal_point()) + // look up the locale's decimal point now, matching what + // snprintf_float() just used (see get_decimal_point() in + // number_parse.hpp); it is a string, which can be longer than one + // byte (U+066B in ps_AF, #5993). + // A thousands separator needs no handling: %g never groups digits. const auto* loc = std::localeconv(); JSON_ASSERT(loc != nullptr); - const char thousands_sep = (loc->thousands_sep == nullptr) ? '\0' : *loc->thousands_sep; - const char decimal_point = (loc->decimal_point == nullptr) ? '\0' : *loc->decimal_point; - - // erase thousands separators - if (thousands_sep != '\0') - { - // NOLINTNEXTLINE(readability-qualified-auto,llvm-qualified-auto): std::remove returns an iterator, see https://github.com/nlohmann/json/issues/3081 - const auto end = std::remove(number_buffer.begin(), number_buffer.begin() + len, thousands_sep); - std::fill(end, number_buffer.end(), '\0'); - JSON_ASSERT((end - number_buffer.begin()) <= len); - len = (end - number_buffer.begin()); - } + const char* const decimal_point = (loc->decimal_point == nullptr) ? "" : loc->decimal_point; + const std::size_t decimal_point_len = std::char_traits::length(decimal_point); // convert decimal point to '.' - if (decimal_point != '\0' && decimal_point != '.') + if ((decimal_point_len > 1) || (decimal_point_len == 1 && decimal_point[0] != '.')) { - // NOLINTNEXTLINE(readability-qualified-auto,llvm-qualified-auto): std::find returns an iterator, see https://github.com/nlohmann/json/issues/3081 - const auto dec_pos = std::find(number_buffer.begin(), number_buffer.end(), decimal_point); - if (dec_pos != number_buffer.end()) + const auto first = number_buffer.begin(); + const auto last = first + len; + // NOLINTNEXTLINE(readability-qualified-auto,llvm-qualified-auto): std::search returns an iterator, see https://github.com/nlohmann/json/issues/3081 + const auto dec_pos = std::search(first, last, decimal_point, decimal_point + decimal_point_len); + if (dec_pos != last) { *dec_pos = '.'; + if (decimal_point_len > 1) + { + // move the fraction and exponent behind the '.' (std::copy + // allows the overlap, as the destination is before the source) + // NOLINTNEXTLINE(readability-qualified-auto,llvm-qualified-auto): std::copy returns an iterator, see https://github.com/nlohmann/json/issues/3081 + const auto end = std::copy(dec_pos + static_cast(decimal_point_len), last, dec_pos + 1); + *end = '\0'; + len = end - first; + } } } diff --git a/single_include/nlohmann/json.hpp b/single_include/nlohmann/json.hpp index cf5a4d711..952cea7b6 100644 --- a/single_include/nlohmann/json.hpp +++ b/single_include/nlohmann/json.hpp @@ -33446,7 +33446,7 @@ NLOHMANN_JSON_NAMESPACE_END -#include // remove, fill, find, none_of, min +#include // copy, none_of, min, search #include // array #include // localeconv, lconv #include // isfinite @@ -37300,31 +37300,35 @@ class serializer // check if the buffer was large enough JSON_ASSERT(static_cast(len) < number_buffer.size()); - // look up the locale's thousands separator and decimal point now, - // matching what snprintf_float() just used (see lexer::get_decimal_point()) + // look up the locale's decimal point now, matching what + // snprintf_float() just used (see get_decimal_point() in + // number_parse.hpp); it is a string, which can be longer than one + // byte (U+066B in ps_AF, #5993). + // A thousands separator needs no handling: %g never groups digits. const auto* loc = std::localeconv(); JSON_ASSERT(loc != nullptr); - const char thousands_sep = (loc->thousands_sep == nullptr) ? '\0' : *loc->thousands_sep; - const char decimal_point = (loc->decimal_point == nullptr) ? '\0' : *loc->decimal_point; - - // erase thousands separators - if (thousands_sep != '\0') - { - // NOLINTNEXTLINE(readability-qualified-auto,llvm-qualified-auto): std::remove returns an iterator, see https://github.com/nlohmann/json/issues/3081 - const auto end = std::remove(number_buffer.begin(), number_buffer.begin() + len, thousands_sep); - std::fill(end, number_buffer.end(), '\0'); - JSON_ASSERT((end - number_buffer.begin()) <= len); - len = (end - number_buffer.begin()); - } + const char* const decimal_point = (loc->decimal_point == nullptr) ? "" : loc->decimal_point; + const std::size_t decimal_point_len = std::char_traits::length(decimal_point); // convert decimal point to '.' - if (decimal_point != '\0' && decimal_point != '.') + if ((decimal_point_len > 1) || (decimal_point_len == 1 && decimal_point[0] != '.')) { - // NOLINTNEXTLINE(readability-qualified-auto,llvm-qualified-auto): std::find returns an iterator, see https://github.com/nlohmann/json/issues/3081 - const auto dec_pos = std::find(number_buffer.begin(), number_buffer.end(), decimal_point); - if (dec_pos != number_buffer.end()) + const auto first = number_buffer.begin(); + const auto last = first + len; + // NOLINTNEXTLINE(readability-qualified-auto,llvm-qualified-auto): std::search returns an iterator, see https://github.com/nlohmann/json/issues/3081 + const auto dec_pos = std::search(first, last, decimal_point, decimal_point + decimal_point_len); + if (dec_pos != last) { *dec_pos = '.'; + if (decimal_point_len > 1) + { + // move the fraction and exponent behind the '.' (std::copy + // allows the overlap, as the destination is before the source) + // NOLINTNEXTLINE(readability-qualified-auto,llvm-qualified-auto): std::copy returns an iterator, see https://github.com/nlohmann/json/issues/3081 + const auto end = std::copy(dec_pos + static_cast(decimal_point_len), last, dec_pos + 1); + *end = '\0'; + len = end - first; + } } } diff --git a/tests/src/unit-locale-cpp.cpp b/tests/src/unit-locale-cpp.cpp index d07eabeff..10143326a 100644 --- a/tests/src/unit-locale-cpp.cpp +++ b/tests/src/unit-locale-cpp.cpp @@ -397,6 +397,14 @@ TEST_CASE("locale with a multi-byte decimal point") // a value the locale-independent paths convert is not affected CHECK(json::parse("12.5") == 12.5); + + // dump() of a long double that takes the snprintf path replaces the + // whole decimal point, not only its first byte (#5993) + CHECK(long_double_json(1.5L).dump() == "1.5"); + CHECK(long_double_json(2.0L).dump() == "2.0"); + CHECK(long_double_json(-0.25L).dump() == "-0.25"); + CHECK(long_double_json::parse(long_double_json(1.5e-7L).dump()) == long_double_json(1.5e-7L)); + CHECK(long_double_json::parse(ld.dump()) == expected_long_double); } if (!tested) {