diff --git a/ryu.go b/ryu.go index f149243..833fe7b 100644 --- a/ryu.go +++ b/ryu.go @@ -78,17 +78,17 @@ func AppendFloat32(b []byte, f float32) []byte { func FormatFloat64(f float64) string { b := make([]byte, 0, 24) b = AppendFloat64(b, f) + return byteSliceToString(b) +} - // Convert the output to a string without copying. - var s string - sh := (*reflect.StringHeader)(unsafe.Pointer(&s)) - sh.Data = uintptr(unsafe.Pointer(&b[0])) - sh.Len = len(b) - return s +func byteSliceToString(b []byte) string { + // Zero alloc conversion following pattern found in stdlib strings.Builder. + return *(*string)(unsafe.Pointer(&b)) } // AppendFloat64 appends the string form of the 64-bit floating point number f, // as generated by FormatFloat64, to b and returns the extended buffer. +// It behaves like strconv.AppendFloat(b, f, 'e', -1, 64). func AppendFloat64(b []byte, f float64) []byte { // Step 1: Decode the floating-point number. // Unify normalized and subnormal cases. @@ -126,6 +126,54 @@ func appendSpecial(b []byte, neg, expZero, mantZero bool) []byte { return append(b, "0e+00"...) } +// FormatFloat64 converts a 64-bit floating point number f to a string. +// It behaves like strconv.FormatFloat(f, 'f', -1, 64). +func FormatFloat64f(f float64) string { + b := make([]byte, 0, 24) + b = AppendFloat64f(b, f) + return byteSliceToString(b) +} + +// AppendFloat64 appends the string form of the 64-bit floating point number f, +// as generated by FormatFloat64, to b and returns the extended buffer. +// It behaves like strconv.AppendFloat(b, f, 'f', -1, 64). +func AppendFloat64f(b []byte, f float64) []byte { + // Step 1: Decode the floating-point number. + // Unify normalized and subnormal cases. + u := math.Float64bits(f) + neg := u>>(mantBits64+expBits64) != 0 + mant := u & (uint64(1)<> mantBits64) & (uint64(1)<= bufLen { + // Avoid function call in the common case. + return b[:len(b)+bufLen] + } + + return append(b, make([]byte, bufLen)...) +} + +func (d dec64) appendF(b []byte, neg bool) []byte { + // Step 5: Print the decimal representation. + if neg { + b = append(b, '-') + } + + out := d.m + outLen := decimalLen64(out) + + dE := int(d.e) + if dE >= 0 { + // XYZ + n := len(b) + b = sizeSlice(b, dE+outLen) + for i := n; i < dE+n; i++ { + b[outLen+i] = '0' + } + + for i := n + outLen - 1; i >= n; i-- { + b[i] = '0' + byte(out%10) + out /= 10 + } + + return b + } + + ePos := -dE + if ePos >= outLen { + // 0.XYZ + b := append(b, "0."...) + n := len(b) + b = sizeSlice(b, ePos) + for i := n + ePos - 1; i >= n; i-- { + b[i] = '0' + byte(out%10) + out /= 10 + } + + return b + } + + // Y.XZ + b = sizeSlice(b, outLen+1) // + "." + n := len(b) + i := n - 1 + end := i - outLen + for ; ePos > 0; i-- { + b[i] = '0' + byte(out%10) + out /= 10 + ePos-- + } + + b[i] = '.' + i-- + + for ; i >= end; i-- { + b[i] = '0' + byte(out%10) + out /= 10 + } + + return b +} + func float64ToDecimalExactInt(mant, exp uint64) (d dec64, ok bool) { e := exp - bias64 if e > mantBits64 { diff --git a/ryu_test.go b/ryu_test.go index e897c56..a85999b 100644 --- a/ryu_test.go +++ b/ryu_test.go @@ -44,6 +44,7 @@ var genericTestCases = []float64{ -0.3, 1000000, 123456.7, + 10.01, 123e45, -123.45, 1e23, @@ -191,8 +192,10 @@ var benchCases = []float64{ 0, 1, 0.3, + 0.0000000003, 1000000, -123.45, + -123456789.123456789, } func BenchmarkAppendFloat32(b *testing.B) { @@ -319,3 +322,49 @@ func measureCall(b []byte, f float64, format func([]byte, float64) []byte, times elapsed := time.Since(start) return elapsed / time.Duration(times) } + +func TestFormatFloat64f(t *testing.T) { + for _, f := range append(genericTestCases, float64TestCases...) { + got := FormatFloat64f(f) + want := strconv.FormatFloat(f, 'f', -1, 64) + if got != want { + t.Errorf("FormatFloat64f(%g): got %q; want %q", f, got, want) + } + } +} + +func TestAppendFloat64f(t *testing.T) { + for _, f := range append(genericTestCases, float64TestCases...) { + // Test with under sized and over sized buffer since the implementation + // differs a bit between the two cases. + bigBufResult := string(AppendFloat64f(make([]byte, 0, 1000), f)) + smallBufResult := string(AppendFloat64f(make([]byte, 0), f)) + if bigBufResult != smallBufResult { + t.Errorf("AppendFloat64f(%g): big %q; small %q", f, bigBufResult, smallBufResult) + } + } +} + +func BenchmarkStrconvAppendFloat64f(b *testing.B) { + for _, f := range append(benchCases, benchCases64...) { + b.Run(FormatFloat64(f), func(b *testing.B) { + var buf []byte + for i := 0; i < b.N; i++ { + buf = strconv.AppendFloat(buf[:0], f, 'f', -1, 64) + } + sinkb = buf + }) + } +} + +func BenchmarkAppendFloat64f(b *testing.B) { + for _, f := range append(benchCases, benchCases64...) { + b.Run(FormatFloat64(f), func(b *testing.B) { + var buf []byte + for i := 0; i < b.N; i++ { + buf = AppendFloat64f(buf[:0], f) + } + sinkb = buf + }) + } +}