[ruby/json] Avoid scientific notation before exponent 15

Fix: https://github.com/ruby/json/issues/861

It's not incorrect to use scientific notation, but it tend
to throw people off a bit, so it's best to keep it for very large
numbers.

https://github.com/ruby/json/commit/1566cd01a6
This commit is contained in:
Jean Boussier 2025-09-18 18:57:26 +02:00 committed by Hiroshi SHIBATA
parent 807faf5445
commit dc406e9b5b
3 changed files with 45 additions and 30 deletions

View File

@ -1345,12 +1345,11 @@ static void generate_json_float(FBuffer *buffer, struct generate_json_data *data
}
/* This implementation writes directly into the buffer. We reserve
* the 28 characters that fpconv_dtoa states as its maximum.
* the 32 characters that fpconv_dtoa states as its maximum.
*/
fbuffer_inc_capa(buffer, 28);
fbuffer_inc_capa(buffer, 32);
char* d = buffer->ptr + buffer->len;
int len = fpconv_dtoa(value, d);
/* fpconv_dtoa converts a float to its shortest string representation,
* but it adds a ".0" if this is a plain integer.
*/

View File

@ -29,6 +29,10 @@
#include <string.h>
#include <stdint.h>
#ifdef JSON_DEBUG
#include <assert.h>
#endif
#define npowers 87
#define steppowers 8
#define firstpower -348 /* 10 ^ -348 */
@ -320,15 +324,7 @@ static int emit_digits(char* digits, int ndigits, char* dest, int K, bool neg)
{
int exp = absv(K + ndigits - 1);
int max_trailing_zeros = 7;
if(neg) {
max_trailing_zeros -= 1;
}
/* write plain integer */
if(K >= 0 && (exp < (ndigits + max_trailing_zeros))) {
if(K >= 0 && exp < 15) {
memcpy(dest, digits, ndigits);
memset(dest + ndigits, '0', K);
@ -432,10 +428,12 @@ static int filter_special(double fp, char* dest)
*
* Input:
* fp -> the double to convert, dest -> destination buffer.
* The generated string will never be longer than 28 characters.
* Make sure to pass a pointer to at least 28 bytes of memory.
* The generated string will never be longer than 32 characters.
* Make sure to pass a pointer to at least 32 bytes of memory.
* The emitted string will not be null terminated.
*
*
*
* Output:
* The number of written characters.
*
@ -474,6 +472,9 @@ static int fpconv_dtoa(double d, char dest[28])
int ndigits = grisu2(d, digits, &K);
str_len += emit_digits(digits, ndigits, dest + str_len, K, neg);
#ifdef JSON_DEBUG
assert(str_len <= 32);
#endif
return str_len;
}

View File

@ -825,26 +825,41 @@ class JSONGeneratorTest < Test::Unit::TestCase
assert_equal object.object_id.to_json, JSON.generate(object, strict: true, as_json: :object_id)
end
def assert_float_roundtrip(expected, actual)
assert_equal(expected, JSON.generate(actual))
assert_equal(actual, JSON.parse(JSON.generate(actual)), "JSON: #{JSON.generate(actual)}")
end
def test_json_generate_float
values = [-1.0, 1.0, 0.0, 12.2, 7.5 / 3.2, 12.0, 100.0, 1000.0]
expecteds = ["-1.0", "1.0", "0.0", "12.2", "2.34375", "12.0", "100.0", "1000.0"]
assert_float_roundtrip "-1.0", -1.0
assert_float_roundtrip "1.0", 1.0
assert_float_roundtrip "0.0", 0.0
assert_float_roundtrip "12.2", 12.2
assert_float_roundtrip "2.34375", 7.5 / 3.2
assert_float_roundtrip "12.0", 12.0
assert_float_roundtrip "100.0", 100.0
assert_float_roundtrip "1000.0", 1000.0
if RUBY_ENGINE == "jruby"
values << 1746861937.7842371
expecteds << "1.7468619377842371E9"
else
values << 1746861937.7842371
expecteds << "1746861937.7842371"
end
if RUBY_ENGINE == "jruby"
assert_float_roundtrip "1.7468619377842371E9", 1746861937.7842371
else
assert_float_roundtrip "1746861937.7842371", 1746861937.7842371
end
if RUBY_ENGINE == "ruby"
values << -2.2471348024634545e-08 << -2.2471348024634545e-09 << -2.2471348024634545e-10
expecteds << "-0.000000022471348024634545" << "-0.0000000022471348024634545" << "-2.2471348024634546e-10"
end
if RUBY_ENGINE == "ruby"
assert_float_roundtrip "100000000000000.0", 100000000000000.0
assert_float_roundtrip "1e+15", 1e+15
assert_float_roundtrip "-100000000000000.0", -100000000000000.0
assert_float_roundtrip "-1e+15", -1e+15
assert_float_roundtrip "1111111111111111.1", 1111111111111111.1
assert_float_roundtrip "1.1111111111111112e+16", 11111111111111111.1
assert_float_roundtrip "-1111111111111111.1", -1111111111111111.1
assert_float_roundtrip "-1.1111111111111112e+16", -11111111111111111.1
values.zip(expecteds).each do |value, expected|
assert_equal expected, value.to_json
end
assert_float_roundtrip "-0.000000022471348024634545", -2.2471348024634545e-08
assert_float_roundtrip "-0.0000000022471348024634545", -2.2471348024634545e-09
assert_float_roundtrip "-2.2471348024634546e-10", -2.2471348024634545e-10
end
end
def test_numbers_of_various_sizes