Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions decimal-go.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,13 @@
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

//go:build !go1.13
// +build !go1.13

// This file is only built for Go versions older than 1.13. From Go 1.13 on,
// strconv's own shortest-float conversion is correct (golang/go#29491) and
// newFromFloat delegates to it instead. See newfromfloat_go113.go.

// Multiprecision decimal numbers.
// For floating-point formatting only; not general purpose.
// Only operations are assign and (binary) left/right shift.
Expand Down
52 changes: 2 additions & 50 deletions decimal.go
Original file line number Diff line number Diff line change
Expand Up @@ -329,7 +329,7 @@ func NewFromFloat(value float64) Decimal {
if value == 0 {
return New(0, 0)
}
return newFromFloat(value, math.Float64bits(value), &float64info)
return newFromFloat(value, 64)
}

// NewFromFloat32 converts a float32 to Decimal.
Expand All @@ -346,55 +346,7 @@ func NewFromFloat32(value float32) Decimal {
if value == 0 {
return New(0, 0)
}
// XOR is workaround for https://github.com/golang/go/issues/26285
a := math.Float32bits(value) ^ 0x80808080
return newFromFloat(float64(value), uint64(a)^0x80808080, &float32info)
}

func newFromFloat(val float64, bits uint64, flt *floatInfo) Decimal {
if math.IsNaN(val) || math.IsInf(val, 0) {
panic(fmt.Sprintf("Cannot create a Decimal from %v", val))
}
exp := int(bits>>flt.mantbits) & (1<<flt.expbits - 1)
mant := bits & (uint64(1)<<flt.mantbits - 1)

switch exp {
case 0:
// denormalized
exp++

default:
// add implicit top bit
mant |= uint64(1) << flt.mantbits
}
exp += flt.bias

var d decimal
d.Assign(mant)
d.Shift(exp - int(flt.mantbits))
d.neg = bits>>(flt.expbits+flt.mantbits) != 0

roundShortest(&d, mant, exp, flt)
// If less than 19 digits, we can do calculation in an int64.
if d.nd < 19 {
tmp := int64(0)
m := int64(1)
for i := d.nd - 1; i >= 0; i-- {
tmp += m * int64(d.d[i]-'0')
m *= 10
}
if d.neg {
tmp *= -1
}
return Decimal{value: big.NewInt(tmp), exp: int32(d.dp) - int32(d.nd)}
}
dValue := new(big.Int)
dValue, ok := dValue.SetString(string(d.d[:d.nd]), 10)
if ok {
return Decimal{value: dValue, exp: int32(d.dp) - int32(d.nd)}
}

return NewFromFloatWithExponent(val, int32(d.dp)-int32(d.nd))
return newFromFloat(float64(value), 32)
}

// NewFromFloatWithExponent converts a float64 to Decimal, with an arbitrary
Expand Down
70 changes: 70 additions & 0 deletions newfromfloat_go113.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
//go:build go1.13
// +build go1.13

package decimal

import (
"fmt"
"math"
"math/big"
"strconv"
)

// newFromFloat converts val to a Decimal holding the shortest decimal digit
// string that round-trips back to val when parsed at the given bit size.
//
// strconv already computes exactly that string, so we ask it for the value in
// 'e' format with precision -1 and read the digits back out. The 'e' layout is
// [-]d[.ddd]e±dd, which is bounded (at most 24 bytes) and trivial to scan,
// unlike 'f' which can run to over 750 bytes for extreme exponents.
func newFromFloat(val float64, bitSize int) Decimal {
if math.IsNaN(val) || math.IsInf(val, 0) {
panic(fmt.Sprintf("Cannot create a Decimal from %v", val))
}

var buf [32]byte
b := strconv.AppendFloat(buf[:0], val, 'e', -1, bitSize)

neg := b[0] == '-'
if neg {
b = b[1:]
}

// Split the significand from the exponent. b always contains an 'e'.
var i int
for b[i] != 'e' {
i++
}
digits, expDigits := b[:i], b[i+1:]

// The shortest round-trip form has at most 17 significant digits for a
// float64 and 9 for a float32, so the significand always fits in an int64.
var mant int64
nd := 0
for _, c := range digits {
if c == '.' {
continue
}
mant = mant*10 + int64(c-'0')
nd++
}
if neg {
mant = -mant
}

esign := 1
switch expDigits[0] {
case '-':
esign = -1
expDigits = expDigits[1:]
case '+':
expDigits = expDigits[1:]
}
e := 0
for _, c := range expDigits {
e = e*10 + int(c-'0')
}

// digits is d.ddd, i.e. the significand scaled by 10^(nd-1).
return Decimal{value: big.NewInt(mant), exp: int32(esign*e - nd + 1)}
}
114 changes: 114 additions & 0 deletions newfromfloat_go113_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
//go:build go1.13
// +build go1.13

package decimal

import (
"math"
"math/rand"
"strconv"
"testing"
)

// TestNewFromFloatShortestRoundTrip pins the contract of NewFromFloat and
// NewFromFloat32: the result is the shortest decimal that parses back to the
// original float. It covers the exponent and significand shapes a
// shortest-representation conversion has to get right, namely subnormals,
// three-digit exponents, single-digit significands and the range extremes.
//
// It is gated on Go 1.13 because before that strconv itself could return a
// shortest form that was not the nearest one (golang/go#29491), which would make
// the reference value below wrong rather than the conversion under test.
func TestNewFromFloatShortestRoundTrip(t *testing.T) {
f64 := []float64{
1, -1, 0.1, -0.1, 0.5, 2.5, 100, 123.456,
1e15, 1e16, 1e17, 1e21, 1e22, 1e23, 9007199254740992,
1e-100, 1e100, 1e-300, 1e300,
math.MaxFloat64, -math.MaxFloat64,
math.SmallestNonzeroFloat64, -math.SmallestNonzeroFloat64,
math.Pi, math.E,
}
// Every power of two, which sweeps the whole exponent range including the
// subnormals, plus both neighbours of every value collected so far.
for e := -1074; e <= 1023; e++ {
f64 = append(f64, math.Ldexp(1, e))
}
for _, f := range append([]float64{}, f64...) {
f64 = append(f64, math.Nextafter(f, math.Inf(1)), math.Nextafter(f, math.Inf(-1)))
}

for _, f := range f64 {
if f == 0 || math.IsInf(f, 0) {
continue
}
want, err := NewFromString(strconv.FormatFloat(f, 'f', -1, 64))
if err != nil {
t.Fatalf("NewFromString(%v): %v", f, err)
}
got := NewFromFloat(f)
if !got.Equal(want) {
t.Errorf("NewFromFloat(%v) = %s (%s, %d), want %s (%s, %d)",
f, got, got.value, got.exp, want, want.value, want.exp)
}
if back, _ := got.Float64(); back != f {
t.Errorf("NewFromFloat(%v).Float64() = %v, does not round-trip", f, back)
}
}

f32 := []float32{
1, -1, 0.1, -0.1, 100, 123.456,
math.MaxFloat32, -math.MaxFloat32,
math.SmallestNonzeroFloat32, -math.SmallestNonzeroFloat32,
}
for e := -149; e <= 127; e++ {
f32 = append(f32, float32(math.Ldexp(1, e)))
}
for _, f := range f32 {
if f == 0 {
continue
}
want, err := NewFromString(strconv.FormatFloat(float64(f), 'f', -1, 32))
if err != nil {
t.Fatalf("NewFromString(%v): %v", f, err)
}
got := NewFromFloat32(f)
if !got.Equal(want) {
t.Errorf("NewFromFloat32(%v) = %s (%s, %d), want %s (%s, %d)",
f, got, got.value, got.exp, want, want.value, want.exp)
}
}
}

// TestNewFromFloatRandomRoundTrip sweeps random bit patterns, which puts a
// meaningful share of subnormals and extreme exponents through the conversion.
func TestNewFromFloatRandomRoundTrip(t *testing.T) {
rng := rand.New(rand.NewSource(0xdead1337))
for i := 0; i < 200000; i++ {
f := math.Float64frombits(rng.Uint64())
if f == 0 || math.IsNaN(f) || math.IsInf(f, 0) {
continue
}
want, err := NewFromString(strconv.FormatFloat(f, 'f', -1, 64))
if err != nil {
t.Fatalf("NewFromString(%v): %v", f, err)
}
if got := NewFromFloat(f); !got.Equal(want) {
t.Fatalf("NewFromFloat(%v) = %s (%s, %d), want %s (%s, %d)",
f, got, got.value, got.exp, want, want.value, want.exp)
}
}
for i := 0; i < 200000; i++ {
f := math.Float32frombits(rng.Uint32())
if f == 0 || math.IsNaN(float64(f)) || math.IsInf(float64(f), 0) {
continue
}
want, err := NewFromString(strconv.FormatFloat(float64(f), 'f', -1, 32))
if err != nil {
t.Fatalf("NewFromString(%v): %v", f, err)
}
if got := NewFromFloat32(f); !got.Equal(want) {
t.Fatalf("NewFromFloat32(%v) = %s (%s, %d), want %s (%s, %d)",
f, got, got.value, got.exp, want, want.value, want.exp)
}
}
}
76 changes: 76 additions & 0 deletions newfromfloat_legacy.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
//go:build !go1.13
// +build !go1.13

package decimal

import (
"fmt"
"math"
"math/big"
)

// newFromFloat converts val to a Decimal holding the shortest decimal digit
// string that round-trips back to val when parsed at the given bit size.
//
// This is the pre-Go 1.13 implementation: it computes the shortest form itself,
// using the vendored copy of strconv's multiprecision decimal (decimal-go.go and
// rounding.go), because strconv.FormatFloat could return a shortest form that is
// not the nearest one before Go 1.13 (golang/go#29491).
func newFromFloat(val float64, bitSize int) Decimal {
if math.IsNaN(val) || math.IsInf(val, 0) {
panic(fmt.Sprintf("Cannot create a Decimal from %v", val))
}

var bits uint64
var flt *floatInfo
if bitSize == 32 {
// XOR is workaround for https://github.com/golang/go/issues/26285
a := math.Float32bits(float32(val)) ^ 0x80808080
bits = uint64(a) ^ 0x80808080
flt = &float32info
} else {
bits = math.Float64bits(val)
flt = &float64info
}

exp := int(bits>>flt.mantbits) & (1<<flt.expbits - 1)
mant := bits & (uint64(1)<<flt.mantbits - 1)

switch exp {
case 0:
// denormalized
exp++

default:
// add implicit top bit
mant |= uint64(1) << flt.mantbits
}
exp += flt.bias

var d decimal
d.Assign(mant)
d.Shift(exp - int(flt.mantbits))
d.neg = bits>>(flt.expbits+flt.mantbits) != 0

roundShortest(&d, mant, exp, flt)
// If less than 19 digits, we can do calculation in an int64.
if d.nd < 19 {
tmp := int64(0)
m := int64(1)
for i := d.nd - 1; i >= 0; i-- {
tmp += m * int64(d.d[i]-'0')
m *= 10
}
if d.neg {
tmp *= -1
}
return Decimal{value: big.NewInt(tmp), exp: int32(d.dp) - int32(d.nd)}
}
dValue := new(big.Int)
dValue, ok := dValue.SetString(string(d.d[:d.nd]), 10)
if ok {
return Decimal{value: dValue, exp: int32(d.dp) - int32(d.nd)}
}

return NewFromFloatWithExponent(val, int32(d.dp)-int32(d.nd))
}
7 changes: 7 additions & 0 deletions rounding.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,13 @@
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

//go:build !go1.13
// +build !go1.13

// This file is only built for Go versions older than 1.13. From Go 1.13 on,
// strconv's own shortest-float conversion is correct (golang/go#29491) and
// newFromFloat delegates to it instead. See newfromfloat_go113.go.

// Multiprecision decimal numbers.
// For floating-point formatting only; not general purpose.
// Only operations are assign and (binary) left/right shift.
Expand Down