Skip to content

Commit bda298d

Browse files
authored
Fix JSON encoding of float exponents to be like json.Marshal / ES6 (#537)
* Fix JSON encoding of float exponents to be like json.Marshal / ES6 * Add test cases for the *e-9 JSON number encoding edge case
1 parent 0d16f63 commit bda298d

File tree

2 files changed

+360
-3
lines changed

2 files changed

+360
-3
lines changed

internal/json/types.go

+19-1
Original file line numberDiff line numberDiff line change
@@ -311,7 +311,25 @@ func appendFloat(dst []byte, val float64, bitSize int) []byte {
311311
case math.IsInf(val, -1):
312312
return append(dst, `"-Inf"`...)
313313
}
314-
return strconv.AppendFloat(dst, val, 'f', -1, bitSize)
314+
// convert as if by es6 number to string conversion
315+
// see also https://cs.opensource.google/go/go/+/refs/tags/go1.20.3:src/encoding/json/encode.go;l=573
316+
strFmt := byte('f')
317+
// Use float32 comparisons for underlying float32 value to get precise cutoffs right.
318+
if abs := math.Abs(val); abs != 0 {
319+
if bitSize == 64 && (abs < 1e-6 || abs >= 1e21) || bitSize == 32 && (float32(abs) < 1e-6 || float32(abs) >= 1e21) {
320+
strFmt = 'e'
321+
}
322+
}
323+
dst = strconv.AppendFloat(dst, val, strFmt, -1, bitSize)
324+
if strFmt == 'e' {
325+
// Clean up e-09 to e-9
326+
n := len(dst)
327+
if n >= 4 && dst[n-4] == 'e' && dst[n-3] == '-' && dst[n-2] == '0' {
328+
dst[n-2] = dst[n-1]
329+
dst = dst[:n-1]
330+
}
331+
}
332+
return dst
315333
}
316334

317335
// AppendFloat32 converts the input float32 to a string and

internal/json/types_test.go

+341-2
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
package json
22

33
import (
4+
"encoding/json"
45
"math"
6+
"math/rand"
57
"net"
68
"reflect"
79
"testing"
@@ -44,15 +46,15 @@ func TestAppendType(t *testing.T) {
4446
{"AppendFloat32(0)", "AppendFloat32", float32(0), []byte(`0`)},
4547
{"AppendFloat32(-1.1)", "AppendFloat32", float32(-1.1), []byte(`-1.1`)},
4648
{"AppendFloat32(1e20)", "AppendFloat32", float32(1e20), []byte(`100000000000000000000`)},
47-
{"AppendFloat32(1e21)", "AppendFloat32", float32(1e21), []byte(`1000000000000000000000`)},
49+
{"AppendFloat32(1e21)", "AppendFloat32", float32(1e21), []byte(`1e+21`)},
4850

4951
{"AppendFloat64(-Inf)", "AppendFloat64", float64(math.Inf(-1)), []byte(`"-Inf"`)},
5052
{"AppendFloat64(+Inf)", "AppendFloat64", float64(math.Inf(1)), []byte(`"+Inf"`)},
5153
{"AppendFloat64(NaN)", "AppendFloat64", float64(math.NaN()), []byte(`"NaN"`)},
5254
{"AppendFloat64(0)", "AppendFloat64", float64(0), []byte(`0`)},
5355
{"AppendFloat64(-1.1)", "AppendFloat64", float64(-1.1), []byte(`-1.1`)},
5456
{"AppendFloat64(1e20)", "AppendFloat64", float64(1e20), []byte(`100000000000000000000`)},
55-
{"AppendFloat64(1e21)", "AppendFloat64", float64(1e21), []byte(`1000000000000000000000`)},
57+
{"AppendFloat64(1e21)", "AppendFloat64", float64(1e21), []byte(`1e+21`)},
5658
}
5759
for _, tt := range tests {
5860
t.Run(tt.name, func(t *testing.T) {
@@ -207,3 +209,340 @@ func Test_appendObjectData(t *testing.T) {
207209
})
208210
}
209211
}
212+
213+
var float64Tests = []struct {
214+
Name string
215+
Val float64
216+
Want string
217+
}{
218+
{
219+
Name: "Positive integer",
220+
Val: 1234.0,
221+
Want: "1234",
222+
},
223+
{
224+
Name: "Negative integer",
225+
Val: -5678.0,
226+
Want: "-5678",
227+
},
228+
{
229+
Name: "Positive decimal",
230+
Val: 12.3456,
231+
Want: "12.3456",
232+
},
233+
{
234+
Name: "Negative decimal",
235+
Val: -78.9012,
236+
Want: "-78.9012",
237+
},
238+
{
239+
Name: "Large positive number",
240+
Val: 123456789.0,
241+
Want: "123456789",
242+
},
243+
{
244+
Name: "Large negative number",
245+
Val: -987654321.0,
246+
Want: "-987654321",
247+
},
248+
{
249+
Name: "Zero",
250+
Val: 0.0,
251+
Want: "0",
252+
},
253+
{
254+
Name: "Smallest positive value",
255+
Val: math.SmallestNonzeroFloat64,
256+
Want: "5e-324",
257+
},
258+
{
259+
Name: "Largest positive value",
260+
Val: math.MaxFloat64,
261+
Want: "1.7976931348623157e+308",
262+
},
263+
{
264+
Name: "Smallest negative value",
265+
Val: -math.SmallestNonzeroFloat64,
266+
Want: "-5e-324",
267+
},
268+
{
269+
Name: "Largest negative value",
270+
Val: -math.MaxFloat64,
271+
Want: "-1.7976931348623157e+308",
272+
},
273+
{
274+
Name: "NaN",
275+
Val: math.NaN(),
276+
Want: `"NaN"`,
277+
},
278+
{
279+
Name: "+Inf",
280+
Val: math.Inf(1),
281+
Want: `"+Inf"`,
282+
},
283+
{
284+
Name: "-Inf",
285+
Val: math.Inf(-1),
286+
Want: `"-Inf"`,
287+
},
288+
{
289+
Name: "Clean up e-09 to e-9 case 1",
290+
Val: 1e-9,
291+
Want: "1e-9",
292+
},
293+
{
294+
Name: "Clean up e-09 to e-9 case 2",
295+
Val: -2.236734e-9,
296+
Want: "-2.236734e-9",
297+
},
298+
}
299+
300+
func TestEncoder_AppendFloat64(t *testing.T) {
301+
for _, tc := range float64Tests {
302+
t.Run(tc.Name, func(t *testing.T) {
303+
var b []byte
304+
b = (Encoder{}).AppendFloat64(b, tc.Val)
305+
if s := string(b); tc.Want != s {
306+
t.Errorf("%q", s)
307+
}
308+
})
309+
}
310+
}
311+
312+
func FuzzEncoder_AppendFloat64(f *testing.F) {
313+
for _, tc := range float64Tests {
314+
f.Add(tc.Val)
315+
}
316+
f.Fuzz(func(t *testing.T, val float64) {
317+
actual := (Encoder{}).AppendFloat64(nil, val)
318+
if len(actual) == 0 {
319+
t.Fatal("empty buffer")
320+
}
321+
322+
if actual[0] == '"' {
323+
switch string(actual) {
324+
case `"NaN"`:
325+
if !math.IsNaN(val) {
326+
t.Fatalf("expected %v got NaN", val)
327+
}
328+
case `"+Inf"`:
329+
if !math.IsInf(val, 1) {
330+
t.Fatalf("expected %v got +Inf", val)
331+
}
332+
case `"-Inf"`:
333+
if !math.IsInf(val, -1) {
334+
t.Fatalf("expected %v got -Inf", val)
335+
}
336+
default:
337+
t.Fatalf("unexpected string: %s", actual)
338+
}
339+
return
340+
}
341+
342+
if expected, err := json.Marshal(val); err != nil {
343+
t.Error(err)
344+
} else if string(actual) != string(expected) {
345+
t.Errorf("expected %s, got %s", expected, actual)
346+
}
347+
348+
var parsed float64
349+
if err := json.Unmarshal(actual, &parsed); err != nil {
350+
t.Fatal(err)
351+
}
352+
353+
if parsed != val {
354+
t.Fatalf("expected %v, got %v", val, parsed)
355+
}
356+
})
357+
}
358+
359+
var float32Tests = []struct {
360+
Name string
361+
Val float32
362+
Want string
363+
}{
364+
{
365+
Name: "Positive integer",
366+
Val: 1234.0,
367+
Want: "1234",
368+
},
369+
{
370+
Name: "Negative integer",
371+
Val: -5678.0,
372+
Want: "-5678",
373+
},
374+
{
375+
Name: "Positive decimal",
376+
Val: 12.3456,
377+
Want: "12.3456",
378+
},
379+
{
380+
Name: "Negative decimal",
381+
Val: -78.9012,
382+
Want: "-78.9012",
383+
},
384+
{
385+
Name: "Large positive number",
386+
Val: 123456789.0,
387+
Want: "123456790",
388+
},
389+
{
390+
Name: "Large negative number",
391+
Val: -987654321.0,
392+
Want: "-987654340",
393+
},
394+
{
395+
Name: "Zero",
396+
Val: 0.0,
397+
Want: "0",
398+
},
399+
{
400+
Name: "Smallest positive value",
401+
Val: math.SmallestNonzeroFloat32,
402+
Want: "1e-45",
403+
},
404+
{
405+
Name: "Largest positive value",
406+
Val: math.MaxFloat32,
407+
Want: "3.4028235e+38",
408+
},
409+
{
410+
Name: "Smallest negative value",
411+
Val: -math.SmallestNonzeroFloat32,
412+
Want: "-1e-45",
413+
},
414+
{
415+
Name: "Largest negative value",
416+
Val: -math.MaxFloat32,
417+
Want: "-3.4028235e+38",
418+
},
419+
{
420+
Name: "NaN",
421+
Val: float32(math.NaN()),
422+
Want: `"NaN"`,
423+
},
424+
{
425+
Name: "+Inf",
426+
Val: float32(math.Inf(1)),
427+
Want: `"+Inf"`,
428+
},
429+
{
430+
Name: "-Inf",
431+
Val: float32(math.Inf(-1)),
432+
Want: `"-Inf"`,
433+
},
434+
{
435+
Name: "Clean up e-09 to e-9 case 1",
436+
Val: 1e-9,
437+
Want: "1e-9",
438+
},
439+
{
440+
Name: "Clean up e-09 to e-9 case 2",
441+
Val: -2.236734e-9,
442+
Want: "-2.236734e-9",
443+
},
444+
}
445+
446+
func TestEncoder_AppendFloat32(t *testing.T) {
447+
for _, tc := range float32Tests {
448+
t.Run(tc.Name, func(t *testing.T) {
449+
var b []byte
450+
b = (Encoder{}).AppendFloat32(b, tc.Val)
451+
if s := string(b); tc.Want != s {
452+
t.Errorf("%q", s)
453+
}
454+
})
455+
}
456+
}
457+
458+
func FuzzEncoder_AppendFloat32(f *testing.F) {
459+
for _, tc := range float32Tests {
460+
f.Add(tc.Val)
461+
}
462+
f.Fuzz(func(t *testing.T, val float32) {
463+
actual := (Encoder{}).AppendFloat32(nil, val)
464+
if len(actual) == 0 {
465+
t.Fatal("empty buffer")
466+
}
467+
468+
if actual[0] == '"' {
469+
val := float64(val)
470+
switch string(actual) {
471+
case `"NaN"`:
472+
if !math.IsNaN(val) {
473+
t.Fatalf("expected %v got NaN", val)
474+
}
475+
case `"+Inf"`:
476+
if !math.IsInf(val, 1) {
477+
t.Fatalf("expected %v got +Inf", val)
478+
}
479+
case `"-Inf"`:
480+
if !math.IsInf(val, -1) {
481+
t.Fatalf("expected %v got -Inf", val)
482+
}
483+
default:
484+
t.Fatalf("unexpected string: %s", actual)
485+
}
486+
return
487+
}
488+
489+
if expected, err := json.Marshal(val); err != nil {
490+
t.Error(err)
491+
} else if string(actual) != string(expected) {
492+
t.Errorf("expected %s, got %s", expected, actual)
493+
}
494+
495+
var parsed float32
496+
if err := json.Unmarshal(actual, &parsed); err != nil {
497+
t.Fatal(err)
498+
}
499+
500+
if parsed != val {
501+
t.Fatalf("expected %v, got %v", val, parsed)
502+
}
503+
})
504+
}
505+
506+
func generateFloat32s(n int) []float32 {
507+
floats := make([]float32, n)
508+
for i := 0; i < n; i++ {
509+
floats[i] = rand.Float32()
510+
}
511+
return floats
512+
}
513+
514+
func generateFloat64s(n int) []float64 {
515+
floats := make([]float64, n)
516+
for i := 0; i < n; i++ {
517+
floats[i] = rand.Float64()
518+
}
519+
return floats
520+
}
521+
522+
// this is really just for the memory allocation characteristics
523+
func BenchmarkEncoder_AppendFloat32(b *testing.B) {
524+
floats := append(generateFloat32s(5000), float32(math.NaN()), float32(math.Inf(1)), float32(math.Inf(-1)))
525+
dst := make([]byte, 0, 128)
526+
527+
b.ResetTimer()
528+
529+
for i := 0; i < b.N; i++ {
530+
for _, f := range floats {
531+
dst = (Encoder{}).AppendFloat32(dst[:0], f)
532+
}
533+
}
534+
}
535+
536+
// this is really just for the memory allocation characteristics
537+
func BenchmarkEncoder_AppendFloat64(b *testing.B) {
538+
floats := append(generateFloat64s(5000), math.NaN(), math.Inf(1), math.Inf(-1))
539+
dst := make([]byte, 0, 128)
540+
541+
b.ResetTimer()
542+
543+
for i := 0; i < b.N; i++ {
544+
for _, f := range floats {
545+
dst = (Encoder{}).AppendFloat64(dst[:0], f)
546+
}
547+
}
548+
}

0 commit comments

Comments
 (0)