mirror of
https://github.com/golang/go
synced 2024-11-18 13:44:48 -07:00
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 <bradfitz@golang.org>
This commit is contained in:
parent
b662e524e4
commit
92b3e3651d
@ -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) {
|
||||
|
@ -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('"')
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user