Skip to content

Commit 92b3e36

Browse files
committed
encoding/json: use standard ES6 formatting for numbers during marshal
Change float32/float64 formatting to use non-exponential form for a slightly wider range, to more closely match ES6 JSON.stringify and other JSON generators. Most notably: 1e20 now formats as 100000000000000000000 (previously 1e+20) 1e-6 now formats as 0.000001 (previously 1e-06) 1e-7 now formats as 1e-7 (previously 1e-07) This also brings the int64 and float64 formatting in line with each other, for all shared representable values. For example both int64(1234567) and float64(1234567) now format as "1234567", where before the float64 formatted as "1.234567e+06". The only variation now compared to ES6 JSON.stringify is that Go continues to encode negative zero as "-0", not "0", so that the value continues to be preserved during JSON round trips. Fixes #6384. Fixes #14135. Change-Id: Ib0e0e009cd9181d75edc0424a28fe776bcc5bbf8 Reviewed-on: https://go-review.googlesource.com/30371 Reviewed-by: Brad Fitzpatrick <[email protected]>
1 parent b662e52 commit 92b3e36

File tree

3 files changed

+143
-1
lines changed

3 files changed

+143
-1
lines changed

src/encoding/json/decode_test.go

+12
Original file line numberDiff line numberDiff line change
@@ -738,6 +738,18 @@ var unmarshalTests = []unmarshalTest{
738738
out: []intWithPtrMarshalText{1, 2, 3},
739739
golden: true,
740740
},
741+
742+
{in: `0.000001`, ptr: new(float64), out: 0.000001, golden: true},
743+
{in: `1e-7`, ptr: new(float64), out: 1e-7, golden: true},
744+
{in: `100000000000000000000`, ptr: new(float64), out: 100000000000000000000.0, golden: true},
745+
{in: `1e+21`, ptr: new(float64), out: 1e21, golden: true},
746+
{in: `-0.000001`, ptr: new(float64), out: -0.000001, golden: true},
747+
{in: `-1e-7`, ptr: new(float64), out: -1e-7, golden: true},
748+
{in: `-100000000000000000000`, ptr: new(float64), out: -100000000000000000000.0, golden: true},
749+
{in: `-1e+21`, ptr: new(float64), out: -1e21, golden: true},
750+
{in: `999999999999999900000`, ptr: new(float64), out: 999999999999999900000.0, golden: true},
751+
{in: `9007199254740992`, ptr: new(float64), out: 9007199254740992.0, golden: true},
752+
{in: `9007199254740993`, ptr: new(float64), out: 9007199254740992.0, golden: false},
741753
}
742754

743755
func TestMarshal(t *testing.T) {

src/encoding/json/encode.go

+25-1
Original file line numberDiff line numberDiff line change
@@ -526,7 +526,31 @@ func (bits floatEncoder) encode(e *encodeState, v reflect.Value, opts encOpts) {
526526
if math.IsInf(f, 0) || math.IsNaN(f) {
527527
e.error(&UnsupportedValueError{v, strconv.FormatFloat(f, 'g', -1, int(bits))})
528528
}
529-
b := strconv.AppendFloat(e.scratch[:0], f, 'g', -1, int(bits))
529+
530+
// Convert as if by ES6 number to string conversion.
531+
// This matches most other JSON generators.
532+
// See golang.org/issue/6384 and golang.org/issue/14135.
533+
// Like fmt %g, but the exponent cutoffs are different
534+
// and exponents themselves are not padded to two digits.
535+
b := e.scratch[:0]
536+
abs := math.Abs(f)
537+
fmt := byte('f')
538+
// Note: Must use float32 comparisons for underlying float32 value to get precise cutoffs right.
539+
if abs != 0 {
540+
if bits == 64 && (abs < 1e-6 || abs >= 1e21) || bits == 32 && (float32(abs) < 1e-6 || float32(abs) >= 1e21) {
541+
fmt = 'e'
542+
}
543+
}
544+
b = strconv.AppendFloat(b, f, fmt, -1, int(bits))
545+
if fmt == 'e' {
546+
// clean up e-09 to e-9
547+
n := len(b)
548+
if n >= 4 && b[n-4] == 'e' && b[n-3] == '-' && b[n-2] == '0' {
549+
b[n-2] = b[n-1]
550+
b = b[:n-1]
551+
}
552+
}
553+
530554
if opts.quoted {
531555
e.WriteByte('"')
532556
}

src/encoding/json/encode_test.go

+106
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,11 @@ package json
77
import (
88
"bytes"
99
"fmt"
10+
"log"
1011
"math"
1112
"reflect"
13+
"regexp"
14+
"strconv"
1215
"testing"
1316
"unicode"
1417
)
@@ -611,3 +614,106 @@ func TestTextMarshalerMapKeysAreSorted(t *testing.T) {
611614
t.Errorf("Marshal map with text.Marshaler keys: got %#q, want %#q", b, want)
612615
}
613616
}
617+
618+
var re = regexp.MustCompile
619+
620+
// syntactic checks on form of marshalled floating point numbers.
621+
var badFloatREs = []*regexp.Regexp{
622+
re(`p`), // no binary exponential notation
623+
re(`^\+`), // no leading + sign
624+
re(`^-?0[^.]`), // no unnecessary leading zeros
625+
re(`^-?\.`), // leading zero required before decimal point
626+
re(`\.(e|$)`), // no trailing decimal
627+
re(`\.[0-9]+0(e|$)`), // no trailing zero in fraction
628+
re(`^-?(0|[0-9]{2,})\..*e`), // exponential notation must have normalized mantissa
629+
re(`e[0-9]`), // positive exponent must be signed
630+
re(`e[+-]0`), // exponent must not have leading zeros
631+
re(`e-[1-6]$`), // not tiny enough for exponential notation
632+
re(`e+(.|1.|20)$`), // not big enough for exponential notation
633+
re(`^-?0\.0000000`), // too tiny, should use exponential notation
634+
re(`^-?[0-9]{22}`), // too big, should use exponential notation
635+
re(`[1-9][0-9]{16}[1-9]`), // too many significant digits in integer
636+
re(`[1-9][0-9.]{17}[1-9]`), // too many significant digits in decimal
637+
// below here for float32 only
638+
re(`[1-9][0-9]{8}[1-9]`), // too many significant digits in integer
639+
re(`[1-9][0-9.]{9}[1-9]`), // too many significant digits in decimal
640+
}
641+
642+
func TestMarshalFloat(t *testing.T) {
643+
nfail := 0
644+
test := func(f float64, bits int) {
645+
vf := interface{}(f)
646+
if bits == 32 {
647+
f = float64(float32(f)) // round
648+
vf = float32(f)
649+
}
650+
bout, err := Marshal(vf)
651+
if err != nil {
652+
t.Errorf("Marshal(%T(%g)): %v", vf, vf, err)
653+
nfail++
654+
return
655+
}
656+
out := string(bout)
657+
658+
// result must convert back to the same float
659+
g, err := strconv.ParseFloat(out, bits)
660+
if err != nil {
661+
t.Errorf("Marshal(%T(%g)) = %q, cannot parse back: %v", vf, vf, out, err)
662+
nfail++
663+
return
664+
}
665+
if f != g || fmt.Sprint(f) != fmt.Sprint(g) { // fmt.Sprint handles ±0
666+
t.Errorf("Marshal(%T(%g)) = %q (is %g, not %g)", vf, vf, out, float32(g), vf)
667+
nfail++
668+
return
669+
}
670+
671+
bad := badFloatREs
672+
if bits == 64 {
673+
bad = bad[:len(bad)-2]
674+
}
675+
for _, re := range bad {
676+
if re.MatchString(out) {
677+
t.Errorf("Marshal(%T(%g)) = %q, must not match /%s/", vf, vf, out, re)
678+
nfail++
679+
return
680+
}
681+
}
682+
}
683+
684+
var (
685+
bigger = math.Inf(+1)
686+
smaller = math.Inf(-1)
687+
)
688+
689+
var digits = "1.2345678901234567890123"
690+
for i := len(digits); i >= 2; i-- {
691+
for exp := -30; exp <= 30; exp++ {
692+
for _, sign := range "+-" {
693+
for bits := 32; bits <= 64; bits += 32 {
694+
s := fmt.Sprintf("%c%se%d", sign, digits[:i], exp)
695+
f, err := strconv.ParseFloat(s, bits)
696+
if err != nil {
697+
log.Fatal(err)
698+
}
699+
next := math.Nextafter
700+
if bits == 32 {
701+
next = func(g, h float64) float64 {
702+
return float64(math.Nextafter32(float32(g), float32(h)))
703+
}
704+
}
705+
test(f, bits)
706+
test(next(f, bigger), bits)
707+
test(next(f, smaller), bits)
708+
if nfail > 50 {
709+
t.Fatalf("stopping test early")
710+
}
711+
}
712+
}
713+
}
714+
}
715+
test(0, 64)
716+
test(math.Copysign(0, -1), 64)
717+
test(0, 32)
718+
test(math.Copysign(0, -1), 32)
719+
}

0 commit comments

Comments
 (0)