Exact representation in NewFromFloat (#78)

* Additional (and some breaking) tests for NewFromFloatWithExponent

* Addressing tests for NewFromFloatWithExponent

* Naming cosmetic correction

* removing unused code

* Improving FromFloatWithExponent

* Tests for exact float representation added

* Exact float representation in FromFloat

* Adding breaking test for NewFromFloat

* Fast path in FromFloat is unreliable, fixing it

* Addressing special meaning of zero exponent in float64

* NewFromFloatWithExponent: subnormals support

* NewFromFloatWithExponent: just a few additional test cases

* NewFromFloat: documentation update

* NewFromFloatWithExponent: optimization and some documentation

* NewFromFloatWithExponent: optimizations

* NewFromFloatWithExponent: optimizations
This commit is contained in:
Igor Mikushkin 2018-03-02 01:29:43 +04:00 committed by Victor Quinn
parent bf9a39e28b
commit 78e9b82f68
2 changed files with 75 additions and 50 deletions

View file

@ -160,24 +160,12 @@ func NewFromString(value string) (Decimal, error) {
// NewFromFloat(123.45678901234567).String() // output: "123.4567890123456" // NewFromFloat(123.45678901234567).String() // output: "123.4567890123456"
// NewFromFloat(.00000000000000001).String() // output: "0.00000000000000001" // NewFromFloat(.00000000000000001).String() // output: "0.00000000000000001"
// //
// NOTE: some float64 numbers can take up about 300 bytes of memory in decimal representation.
// Consider using NewFromFloatWithExponent if space is more important than precision.
//
// NOTE: this will panic on NaN, +/-inf // NOTE: this will panic on NaN, +/-inf
func NewFromFloat(value float64) Decimal { func NewFromFloat(value float64) Decimal {
floor := math.Floor(value) return NewFromFloatWithExponent(value, math.MinInt32)
// fast path, where float is an int
if floor == value && value <= math.MaxInt64 && value >= math.MinInt64 {
return New(int64(value), 0)
}
// slow path: float is a decimal
// HACK(vadim): do this the slow hacky way for now because the logic to
// convert a base-2 float to base-10 properly is not trivial
str := strconv.FormatFloat(value, 'f', -1, 64)
dec, err := NewFromString(str)
if err != nil {
panic(err)
}
return dec
} }
// NewFromFloatWithExponent converts a float64 to Decimal, with an arbitrary // NewFromFloatWithExponent converts a float64 to Decimal, with an arbitrary

View file

@ -14,28 +14,35 @@ import (
"time" "time"
) )
var testTable = map[float64]string{ type testEnt struct {
3.141592653589793: "3.141592653589793", float float64
3: "3", short string
1234567890123456: "1234567890123456", exact string
1234567890123456000: "1234567890123456000", }
1234.567890123456: "1234.567890123456",
.1234567890123456: "0.1234567890123456", var testTable = []*testEnt{
0: "0", {3.141592653589793, "3.141592653589793", ""},
.1111111111111110: "0.111111111111111", {3, "3", ""},
.1111111111111111: "0.1111111111111111", {1234567890123456, "1234567890123456", ""},
.1111111111111119: "0.1111111111111119", {1234567890123456000, "1234567890123456000", ""},
.000000000000000001: "0.000000000000000001", {1234.567890123456, "1234.567890123456", ""},
.000000000000000002: "0.000000000000000002", {.1234567890123456, "0.1234567890123456", ""},
.000000000000000003: "0.000000000000000003", {0, "0", ""},
.000000000000000005: "0.000000000000000005", {.1111111111111110, "0.111111111111111", ""},
.000000000000000008: "0.000000000000000008", {.1111111111111111, "0.1111111111111111", ""},
.1000000000000001: "0.1000000000000001", {.1111111111111119, "0.1111111111111119", ""},
.1000000000000002: "0.1000000000000002", {.000000000000000001, "0.000000000000000001", ""},
.1000000000000003: "0.1000000000000003", {.000000000000000002, "0.000000000000000002", ""},
.1000000000000005: "0.1000000000000005", {.000000000000000003, "0.000000000000000003", ""},
.1000000000000008: "0.1000000000000008", {.000000000000000005, "0.000000000000000005", ""},
1e25: "10000000000000000000000000", {.000000000000000008, "0.000000000000000008", ""},
{.1000000000000001, "0.1000000000000001", ""},
{.1000000000000002, "0.1000000000000002", ""},
{.1000000000000003, "0.1000000000000003", ""},
{.1000000000000005, "0.1000000000000005", ""},
{.1000000000000008, "0.1000000000000008", ""},
{1e25, "10000000000000000000000000", ""},
{math.MaxInt64, strconv.FormatInt(math.MaxInt64, 10), ""},
} }
var testTableScientificNotation = map[string]string{ var testTableScientificNotation = map[string]string{
@ -54,12 +61,23 @@ var testTableScientificNotation = map[string]string{
} }
func init() { func init() {
for _, s := range testTable {
s.exact = strconv.FormatFloat(s.float, 'f', 300, 64)
if strings.ContainsRune(s.exact, '.') {
s.exact = strings.TrimRight(s.exact, "0")
s.exact = strings.TrimRight(s.exact, ".")
}
}
// add negatives // add negatives
for f, s := range testTable { withNeg := testTable[:]
if f > 0 { for _, s := range testTable {
testTable[-f] = "-" + s if s.float > 0 {
withNeg = append(withNeg, &testEnt{-s.float, "-" + s.short, "-" + s.exact})
} }
} }
testTable = withNeg
for e, s := range testTableScientificNotation { for e, s := range testTableScientificNotation {
if string(e[0]) != "-" && s != "0" { if string(e[0]) != "-" && s != "0" {
testTableScientificNotation["-"+e] = "-" + s testTableScientificNotation["-"+e] = "-" + s
@ -68,8 +86,9 @@ func init() {
} }
func TestNewFromFloat(t *testing.T) { func TestNewFromFloat(t *testing.T) {
for f, s := range testTable { for _, x := range testTable {
d := NewFromFloat(f) s := x.exact
d := NewFromFloat(x.float)
if d.String() != s { if d.String() != s {
t.Errorf("expected %s, got %s (%s, %d)", t.Errorf("expected %s, got %s (%s, %d)",
s, d.String(), s, d.String(),
@ -92,7 +111,20 @@ func TestNewFromFloat(t *testing.T) {
} }
func TestNewFromString(t *testing.T) { func TestNewFromString(t *testing.T) {
for _, s := range testTable { for _, x := range testTable {
s := x.short
d, err := NewFromString(s)
if err != nil {
t.Errorf("error while parsing %s", s)
} else if d.String() != s {
t.Errorf("expected %s, got %s (%s, %d)",
s, d.String(),
d.value.String(), d.exp)
}
}
for _, x := range testTable {
s := x.exact
d, err := NewFromString(s) d, err := NewFromString(s)
if err != nil { if err != nil {
t.Errorf("error while parsing %s", s) t.Errorf("error while parsing %s", s)
@ -289,7 +321,8 @@ func TestNewFromBigIntWithExponent(t *testing.T) {
} }
func TestJSON(t *testing.T) { func TestJSON(t *testing.T) {
for _, s := range testTable { for _, x := range testTable {
s := x.short
var doc struct { var doc struct {
Amount Decimal `json:"amount"` Amount Decimal `json:"amount"`
} }
@ -358,7 +391,8 @@ func TestBadJSON(t *testing.T) {
} }
func TestNullDecimalJSON(t *testing.T) { func TestNullDecimalJSON(t *testing.T) {
for _, s := range testTable { for _, x := range testTable {
s := x.short
var doc struct { var doc struct {
Amount NullDecimal `json:"amount"` Amount NullDecimal `json:"amount"`
} }
@ -450,7 +484,8 @@ func TestNullDecimalBadJSON(t *testing.T) {
} }
func TestXML(t *testing.T) { func TestXML(t *testing.T) {
for _, s := range testTable { for _, x := range testTable {
s := x.short
var doc struct { var doc struct {
XMLName xml.Name `xml:"account"` XMLName xml.Name `xml:"account"`
Amount Decimal `xml:"amount"` Amount Decimal `xml:"amount"`
@ -2035,7 +2070,8 @@ func TestNullDecimal_Value(t *testing.T) {
} }
func TestBinary(t *testing.T) { func TestBinary(t *testing.T) {
for x := range testTable { for _, y := range testTable {
x := y.float
// Create the decimal // Create the decimal
d1 := NewFromFloat(x) d1 := NewFromFloat(x)
@ -2070,7 +2106,8 @@ func slicesEqual(a, b []byte) bool {
} }
func TestGobEncode(t *testing.T) { func TestGobEncode(t *testing.T) {
for x := range testTable { for _, y := range testTable {
x := y.float
d1 := NewFromFloat(x) d1 := NewFromFloat(x)
b1, err := d1.GobEncode() b1, err := d1.GobEncode()