Skip to content

Commit

Permalink
encoding/json: use standard ES6 formatting for numbers during marshal
Browse files Browse the repository at this point in the history
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>
  • Loading branch information
rsc committed Oct 5, 2016
1 parent b662e52 commit 92b3e36
Show file tree
Hide file tree
Showing 3 changed files with 143 additions and 1 deletion.
12 changes: 12 additions & 0 deletions src/encoding/json/decode_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
26 changes: 25 additions & 1 deletion src/encoding/json/encode.go
Original file line number Diff line number Diff line change
Expand Up @@ -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('"')
}
Expand Down
106 changes: 106 additions & 0 deletions src/encoding/json/encode_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,11 @@ package json
import (
"bytes"
"fmt"
"log"
"math"
"reflect"
"regexp"
"strconv"
"testing"
"unicode"
)
Expand Down Expand Up @@ -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)
}

0 comments on commit 92b3e36

Please sign in to comment.