]> Cypherpunks repositories - gostls13.git/commitdiff
encoding/json: use standard ES6 formatting for numbers during marshal
authorRuss Cox <rsc@golang.org>
Wed, 5 Oct 2016 15:26:04 +0000 (11:26 -0400)
committerRuss Cox <rsc@golang.org>
Wed, 5 Oct 2016 19:15:02 +0000 (19:15 +0000)
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 <bradfitz@golang.org>
src/encoding/json/decode_test.go
src/encoding/json/encode.go
src/encoding/json/encode_test.go

index 255ff5c66a75bfde3bf924c9219189a58d62bd98..04fbd7524dee2f04eec770aacb1ad521bb3561d8 100644 (file)
@@ -738,6 +738,18 @@ var unmarshalTests = []unmarshalTest{
                out:    []intWithPtrMarshalText{1, 2, 3},
                golden: true,
        },
+
+       {in: `0.000001`, ptr: new(float64), out: 0.000001, golden: true},
+       {in: `1e-7`, ptr: new(float64), out: 1e-7, golden: true},
+       {in: `100000000000000000000`, ptr: new(float64), out: 100000000000000000000.0, golden: true},
+       {in: `1e+21`, ptr: new(float64), out: 1e21, golden: true},
+       {in: `-0.000001`, ptr: new(float64), out: -0.000001, golden: true},
+       {in: `-1e-7`, ptr: new(float64), out: -1e-7, golden: true},
+       {in: `-100000000000000000000`, ptr: new(float64), out: -100000000000000000000.0, golden: true},
+       {in: `-1e+21`, ptr: new(float64), out: -1e21, golden: true},
+       {in: `999999999999999900000`, ptr: new(float64), out: 999999999999999900000.0, golden: true},
+       {in: `9007199254740992`, ptr: new(float64), out: 9007199254740992.0, golden: true},
+       {in: `9007199254740993`, ptr: new(float64), out: 9007199254740992.0, golden: false},
 }
 
 func TestMarshal(t *testing.T) {
index aab912601a542c12b44ed98a94317ba8896a904e..6e43a9d48b3054c2ea7f9f00a76c55ab57fefa25 100644 (file)
@@ -526,7 +526,31 @@ func (bits floatEncoder) encode(e *encodeState, v reflect.Value, opts encOpts) {
        if math.IsInf(f, 0) || math.IsNaN(f) {
                e.error(&UnsupportedValueError{v, strconv.FormatFloat(f, 'g', -1, int(bits))})
        }
-       b := strconv.AppendFloat(e.scratch[:0], f, 'g', -1, int(bits))
+
+       // Convert as if by ES6 number to string conversion.
+       // This matches most other JSON generators.
+       // See golang.org/issue/6384 and golang.org/issue/14135.
+       // Like fmt %g, but the exponent cutoffs are different
+       // and exponents themselves are not padded to two digits.
+       b := e.scratch[:0]
+       abs := math.Abs(f)
+       fmt := byte('f')
+       // Note: Must use float32 comparisons for underlying float32 value to get precise cutoffs right.
+       if abs != 0 {
+               if bits == 64 && (abs < 1e-6 || abs >= 1e21) || bits == 32 && (float32(abs) < 1e-6 || float32(abs) >= 1e21) {
+                       fmt = 'e'
+               }
+       }
+       b = strconv.AppendFloat(b, f, fmt, -1, int(bits))
+       if fmt == 'e' {
+               // clean up e-09 to e-9
+               n := len(b)
+               if n >= 4 && b[n-4] == 'e' && b[n-3] == '-' && b[n-2] == '0' {
+                       b[n-2] = b[n-1]
+                       b = b[:n-1]
+               }
+       }
+
        if opts.quoted {
                e.WriteByte('"')
        }
index b484022a70e1980956a0f3136722eeb3b1b2d898..13e765afa31d1ae35956fed02b51ebe3ca01258c 100644 (file)
@@ -7,8 +7,11 @@ package json
 import (
        "bytes"
        "fmt"
+       "log"
        "math"
        "reflect"
+       "regexp"
+       "strconv"
        "testing"
        "unicode"
 )
@@ -611,3 +614,106 @@ func TestTextMarshalerMapKeysAreSorted(t *testing.T) {
                t.Errorf("Marshal map with text.Marshaler keys: got %#q, want %#q", b, want)
        }
 }
+
+var re = regexp.MustCompile
+
+// syntactic checks on form of marshalled floating point numbers.
+var badFloatREs = []*regexp.Regexp{
+       re(`p`),                     // no binary exponential notation
+       re(`^\+`),                   // no leading + sign
+       re(`^-?0[^.]`),              // no unnecessary leading zeros
+       re(`^-?\.`),                 // leading zero required before decimal point
+       re(`\.(e|$)`),               // no trailing decimal
+       re(`\.[0-9]+0(e|$)`),        // no trailing zero in fraction
+       re(`^-?(0|[0-9]{2,})\..*e`), // exponential notation must have normalized mantissa
+       re(`e[0-9]`),                // positive exponent must be signed
+       re(`e[+-]0`),                // exponent must not have leading zeros
+       re(`e-[1-6]$`),              // not tiny enough for exponential notation
+       re(`e+(.|1.|20)$`),          // not big enough for exponential notation
+       re(`^-?0\.0000000`),         // too tiny, should use exponential notation
+       re(`^-?[0-9]{22}`),          // too big, should use exponential notation
+       re(`[1-9][0-9]{16}[1-9]`),   // too many significant digits in integer
+       re(`[1-9][0-9.]{17}[1-9]`),  // too many significant digits in decimal
+       // below here for float32 only
+       re(`[1-9][0-9]{8}[1-9]`),  // too many significant digits in integer
+       re(`[1-9][0-9.]{9}[1-9]`), // too many significant digits in decimal
+}
+
+func TestMarshalFloat(t *testing.T) {
+       nfail := 0
+       test := func(f float64, bits int) {
+               vf := interface{}(f)
+               if bits == 32 {
+                       f = float64(float32(f)) // round
+                       vf = float32(f)
+               }
+               bout, err := Marshal(vf)
+               if err != nil {
+                       t.Errorf("Marshal(%T(%g)): %v", vf, vf, err)
+                       nfail++
+                       return
+               }
+               out := string(bout)
+
+               // result must convert back to the same float
+               g, err := strconv.ParseFloat(out, bits)
+               if err != nil {
+                       t.Errorf("Marshal(%T(%g)) = %q, cannot parse back: %v", vf, vf, out, err)
+                       nfail++
+                       return
+               }
+               if f != g || fmt.Sprint(f) != fmt.Sprint(g) { // fmt.Sprint handles ±0
+                       t.Errorf("Marshal(%T(%g)) = %q (is %g, not %g)", vf, vf, out, float32(g), vf)
+                       nfail++
+                       return
+               }
+
+               bad := badFloatREs
+               if bits == 64 {
+                       bad = bad[:len(bad)-2]
+               }
+               for _, re := range bad {
+                       if re.MatchString(out) {
+                               t.Errorf("Marshal(%T(%g)) = %q, must not match /%s/", vf, vf, out, re)
+                               nfail++
+                               return
+                       }
+               }
+       }
+
+       var (
+               bigger  = math.Inf(+1)
+               smaller = math.Inf(-1)
+       )
+
+       var digits = "1.2345678901234567890123"
+       for i := len(digits); i >= 2; i-- {
+               for exp := -30; exp <= 30; exp++ {
+                       for _, sign := range "+-" {
+                               for bits := 32; bits <= 64; bits += 32 {
+                                       s := fmt.Sprintf("%c%se%d", sign, digits[:i], exp)
+                                       f, err := strconv.ParseFloat(s, bits)
+                                       if err != nil {
+                                               log.Fatal(err)
+                                       }
+                                       next := math.Nextafter
+                                       if bits == 32 {
+                                               next = func(g, h float64) float64 {
+                                                       return float64(math.Nextafter32(float32(g), float32(h)))
+                                               }
+                                       }
+                                       test(f, bits)
+                                       test(next(f, bigger), bits)
+                                       test(next(f, smaller), bits)
+                                       if nfail > 50 {
+                                               t.Fatalf("stopping test early")
+                                       }
+                               }
+                       }
+               }
+       }
+       test(0, 64)
+       test(math.Copysign(0, -1), 64)
+       test(0, 32)
+       test(math.Copysign(0, -1), 32)
+}