Skip to content

Commit 1566cd0

Browse files
committed
Avoid scientific notation before exponent 15
Fix: #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.
1 parent 51ce76e commit 1566cd0

File tree

4 files changed

+50
-33
lines changed

4 files changed

+50
-33
lines changed

CHANGES.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22

33
### Unreleased
44

5+
* Tuned the floating point number generator to not use scientific notation as agressively.
6+
57
### 2025-09-18 (2.14.1)
68

79
* Fix `IndexOutOfBoundsException` in the JRuby extension when encoding shared strings.

ext/json/ext/generator/generator.c

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1345,12 +1345,11 @@ static void generate_json_float(FBuffer *buffer, struct generate_json_data *data
13451345
}
13461346

13471347
/* This implementation writes directly into the buffer. We reserve
1348-
* the 28 characters that fpconv_dtoa states as its maximum.
1348+
* the 32 characters that fpconv_dtoa states as its maximum.
13491349
*/
1350-
fbuffer_inc_capa(buffer, 28);
1350+
fbuffer_inc_capa(buffer, 32);
13511351
char* d = buffer->ptr + buffer->len;
13521352
int len = fpconv_dtoa(value, d);
1353-
13541353
/* fpconv_dtoa converts a float to its shortest string representation,
13551354
* but it adds a ".0" if this is a plain integer.
13561355
*/

ext/json/ext/vendor/fpconv.c

Lines changed: 12 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,10 @@
2929
#include <string.h>
3030
#include <stdint.h>
3131

32+
#ifdef JSON_DEBUG
33+
#include <assert.h>
34+
#endif
35+
3236
#define npowers 87
3337
#define steppowers 8
3438
#define firstpower -348 /* 10 ^ -348 */
@@ -320,15 +324,7 @@ static int emit_digits(char* digits, int ndigits, char* dest, int K, bool neg)
320324
{
321325
int exp = absv(K + ndigits - 1);
322326

323-
int max_trailing_zeros = 7;
324-
325-
if(neg) {
326-
max_trailing_zeros -= 1;
327-
}
328-
329-
/* write plain integer */
330-
if(K >= 0 && (exp < (ndigits + max_trailing_zeros))) {
331-
327+
if(K >= 0 && exp < 15) {
332328
memcpy(dest, digits, ndigits);
333329
memset(dest + ndigits, '0', K);
334330

@@ -432,10 +428,12 @@ static int filter_special(double fp, char* dest)
432428
*
433429
* Input:
434430
* fp -> the double to convert, dest -> destination buffer.
435-
* The generated string will never be longer than 28 characters.
436-
* Make sure to pass a pointer to at least 28 bytes of memory.
431+
* The generated string will never be longer than 32 characters.
432+
* Make sure to pass a pointer to at least 32 bytes of memory.
437433
* The emitted string will not be null terminated.
438434
*
435+
*
436+
*
439437
* Output:
440438
* The number of written characters.
441439
*
@@ -474,6 +472,9 @@ static int fpconv_dtoa(double d, char dest[28])
474472
int ndigits = grisu2(d, digits, &K);
475473

476474
str_len += emit_digits(digits, ndigits, dest + str_len, K, neg);
475+
#ifdef JSON_DEBUG
476+
assert(str_len <= 32);
477+
#endif
477478

478479
return str_len;
479480
}

test/json/json_generator_test.rb

Lines changed: 34 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -825,26 +825,41 @@ def test_json_generate_as_json_convert_to_proc
825825
assert_equal object.object_id.to_json, JSON.generate(object, strict: true, as_json: :object_id)
826826
end
827827

828-
def test_json_generate_float
829-
values = [-1.0, 1.0, 0.0, 12.2, 7.5 / 3.2, 12.0, 100.0, 1000.0]
830-
expecteds = ["-1.0", "1.0", "0.0", "12.2", "2.34375", "12.0", "100.0", "1000.0"]
831-
832-
if RUBY_ENGINE == "jruby"
833-
values << 1746861937.7842371
834-
expecteds << "1.7468619377842371E9"
835-
else
836-
values << 1746861937.7842371
837-
expecteds << "1746861937.7842371"
838-
end
839-
840-
if RUBY_ENGINE == "ruby"
841-
values << -2.2471348024634545e-08 << -2.2471348024634545e-09 << -2.2471348024634545e-10
842-
expecteds << "-0.000000022471348024634545" << "-0.0000000022471348024634545" << "-2.2471348024634546e-10"
843-
end
828+
def assert_float_roundtrip(expected, actual)
829+
assert_equal(expected, JSON.generate(actual))
830+
assert_equal(actual, JSON.parse(JSON.generate(actual)), "JSON: #{JSON.generate(actual)}")
831+
end
844832

845-
values.zip(expecteds).each do |value, expected|
846-
assert_equal expected, value.to_json
847-
end
833+
def test_json_generate_float
834+
assert_float_roundtrip "-1.0", -1.0
835+
assert_float_roundtrip "1.0", 1.0
836+
assert_float_roundtrip "0.0", 0.0
837+
assert_float_roundtrip "12.2", 12.2
838+
assert_float_roundtrip "2.34375", 7.5 / 3.2
839+
assert_float_roundtrip "12.0", 12.0
840+
assert_float_roundtrip "100.0", 100.0
841+
assert_float_roundtrip "1000.0", 1000.0
842+
843+
if RUBY_ENGINE == "jruby"
844+
assert_float_roundtrip "1.7468619377842371E9", 1746861937.7842371
845+
else
846+
assert_float_roundtrip "1746861937.7842371", 1746861937.7842371
847+
end
848+
849+
if RUBY_ENGINE == "ruby"
850+
assert_float_roundtrip "100000000000000.0", 100000000000000.0
851+
assert_float_roundtrip "1e+15", 1e+15
852+
assert_float_roundtrip "-100000000000000.0", -100000000000000.0
853+
assert_float_roundtrip "-1e+15", -1e+15
854+
assert_float_roundtrip "1111111111111111.1", 1111111111111111.1
855+
assert_float_roundtrip "1.1111111111111112e+16", 11111111111111111.1
856+
assert_float_roundtrip "-1111111111111111.1", -1111111111111111.1
857+
assert_float_roundtrip "-1.1111111111111112e+16", -11111111111111111.1
858+
859+
assert_float_roundtrip "-0.000000022471348024634545", -2.2471348024634545e-08
860+
assert_float_roundtrip "-0.0000000022471348024634545", -2.2471348024634545e-09
861+
assert_float_roundtrip "-2.2471348024634546e-10", -2.2471348024634545e-10
862+
end
848863
end
849864

850865
def test_numbers_of_various_sizes

0 commit comments

Comments
 (0)