Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

locale-independent num-to-str #378

192 changes: 156 additions & 36 deletions src/json.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -2203,14 +2203,6 @@ class basic_json
string_t dump(const int indent = -1) const
{
std::stringstream ss;
// fix locale problems
ss.imbue(std::locale::classic());

// 6, 15 or 16 digits of precision allows round-trip IEEE 754
// string->float->string, string->double->string or string->long
// double->string; to be safe, we read this value from
// std::numeric_limits<number_float_t>::digits10
ss.precision(std::numeric_limits<double>::digits10);

if (indent >= 0)
{
Expand Down Expand Up @@ -5867,10 +5859,6 @@ class basic_json
`std::setw(4)` on @a o sets the indentation level to `4` and the
serialization result is the same as calling `dump(4)`.

@note During serializaion, the locale and the precision of the output
stream @a o are changed. The original values are restored when the
function returns.

@param[in,out] o stream to serialize to
@param[in] j JSON value to serialize

Expand All @@ -5892,22 +5880,9 @@ class basic_json
// reset width to 0 for subsequent calls to this stream
o.width(0);

// fix locale problems
const auto old_locale = o.imbue(std::locale::classic());
// set precision

// 6, 15 or 16 digits of precision allows round-trip IEEE 754
// string->float->string, string->double->string or string->long
// double->string; to be safe, we read this value from
// std::numeric_limits<number_float_t>::digits10
const auto old_precision = o.precision(std::numeric_limits<double>::digits10);

// do the actual serialization
j.dump(o, pretty_print, static_cast<unsigned int>(indentation));

// reset locale and precision
o.imbue(old_locale);
o.precision(old_precision);
return o;
}

Expand Down Expand Up @@ -7892,6 +7867,159 @@ class basic_json
return result;
}


/*!
@brief locale-independent serialization for built-in arithmetic types
*/
struct numtostr
{
public:
template<typename T>
numtostr(T value)
{
x_write(value, std::is_integral<T>());
}

operator const char*() const
{
return m_buf.data();
}

const char* c_str() const
{
return m_buf.data();
}

private:
static constexpr size_t s_capacity = 30;
std::array<char, s_capacity + 2> m_buf{{}}; // +2 for leading '-'
// and trailing '\0'
template<typename T>
void x_write(T x, std::true_type)
{
static_assert(std::numeric_limits<T>::digits10 <= s_capacity, "");

const bool is_neg = x < 0;
size_t i = 0;

while(x and i < s_capacity)
{
const auto digit = std::labs(static_cast<long>(x % 10));
m_buf[i++] = static_cast<char>('0' + digit);
x /= 10;
}

assert(i < s_capacity);

if(i == 0)
{
m_buf[i++] = '0';
}

if(is_neg)
{
m_buf[i++] = '-';
}

std::reverse(m_buf.begin(), m_buf.begin() + i);
}

template<typename T>
void x_write(T x, std::false_type)
{
if (x == 0)
{
std::strcpy(m_buf.data(),
std::signbit(x) ? "-0.0" : "0.0");
return;
}

static constexpr auto d =
std::numeric_limits<number_float_t>::digits10;
static_assert(d == 6 or d == 15 or d == 16 or d == 17, "");

static constexpr auto fmt = d == 6 ? "%.7g"
: d == 15 ? "%.16g"
: d == 16 ? "%.17g"
: d == 17 ? "%.18g"
: "%.19g";
// I'm not sure why we need to +1 the precision,
// but without it there's a unit-test that fails
// that asserts precision of the output

snprintf(m_buf.data(), m_buf.size(), fmt, x);

#if 0
// C locales and C++ locales are similar but
// different.
//
// If working with C++ streams we'd've used
// these, but for C formatting functions we
// have to use C locales (setlocale / localeconv),
// rather than C++ locales (std::locale installed
// by std::locale::global()).
const std::locale loc;

const char thousands_sep =
std::use_facet< std::numpunct<char> >(
loc).thousands_sep();

const char decimal_point =
std::use_facet< std::numpunct<char> >(
loc).decimal_point();
#else
const auto loc = localeconv();
assert(loc != nullptr);
const char thousands_sep = !loc->thousands_sep ? '\0'
: loc->thousands_sep[0];

const char decimal_point = !loc->decimal_point ? '\0'
: loc->decimal_point[0];
#endif

// erase thousands separator
if (thousands_sep) {
auto end = std::remove(m_buf.begin(),
m_buf.end(),
thousands_sep);

std::fill(end, m_buf.end(), '\0');
}

// convert decimal point to '.'
if (decimal_point and decimal_point != '.')
{
for (auto& c : m_buf)
{
if (c == decimal_point)
{
c = '.';
break;
}
}
}

// determine if need to apperd ".0"
auto data_end = m_buf.begin() + strlen(m_buf.data());

const bool value_is_int_like =
std::find_if(m_buf.begin(), data_end,
[](const char c)
{ return c == '.'
or c == 'e'
or c == 'E'; })
== data_end;

assert(data_end + 2 < m_buf.end());
if(value_is_int_like)
{
strcat(m_buf.data(), ".0");
}
}
};



/*!
@brief internal implementation of the serialization function

Expand Down Expand Up @@ -8011,27 +8139,19 @@ class basic_json

case value_t::number_integer:
{
o << m_value.number_integer;
o << numtostr(m_value.number_integer).c_str();
return;
}

case value_t::number_unsigned:
{
o << m_value.number_unsigned;
o << numtostr(m_value.number_unsigned).c_str();
return;
}

case value_t::number_float:
{
if (m_value.number_float == 0)
{
// special case for zero to get "0.0"/"-0.0"
o << (std::signbit(m_value.number_float) ? "-0.0" : "0.0");
}
else
{
o << m_value.number_float;
}
o << numtostr(m_value.number_float).c_str();
return;
}

Expand Down
Loading