diff options
author | Igor Mikushkin <igor.mikushkin@gmail.com> | 2018-03-02 05:29:43 +0800 |
---|---|---|
committer | Victor Quinn <mail@victorquinn.com> | 2018-03-02 05:29:43 +0800 |
commit | 78e9b82f68c54fcfaf34d71d751e594d20d2e242 (patch) | |
tree | 03609a1984d7cc5c503f3fcc69dd725d5d3218de | |
parent | bf9a39e28bc9aea136acaf2c3628e68f65e3c3df (diff) | |
download | dexon-decimal-78e9b82f68c54fcfaf34d71d751e594d20d2e242.tar.gz dexon-decimal-78e9b82f68c54fcfaf34d71d751e594d20d2e242.tar.zst dexon-decimal-78e9b82f68c54fcfaf34d71d751e594d20d2e242.zip |
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
-rw-r--r-- | decimal.go | 20 | ||||
-rw-r--r-- | decimal_test.go | 103 |
2 files changed, 74 insertions, 49 deletions
@@ -160,24 +160,12 @@ func NewFromString(value string) (Decimal, error) { // NewFromFloat(123.45678901234567).String() // output: "123.4567890123456" // 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 func NewFromFloat(value float64) Decimal { - floor := math.Floor(value) - - // 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 + return NewFromFloatWithExponent(value, math.MinInt32) } // NewFromFloatWithExponent converts a float64 to Decimal, with an arbitrary diff --git a/decimal_test.go b/decimal_test.go index fa57083..33e344c 100644 --- a/decimal_test.go +++ b/decimal_test.go @@ -14,28 +14,35 @@ import ( "time" ) -var testTable = map[float64]string{ - 3.141592653589793: "3.141592653589793", - 3: "3", - 1234567890123456: "1234567890123456", - 1234567890123456000: "1234567890123456000", - 1234.567890123456: "1234.567890123456", - .1234567890123456: "0.1234567890123456", - 0: "0", - .1111111111111110: "0.111111111111111", - .1111111111111111: "0.1111111111111111", - .1111111111111119: "0.1111111111111119", - .000000000000000001: "0.000000000000000001", - .000000000000000002: "0.000000000000000002", - .000000000000000003: "0.000000000000000003", - .000000000000000005: "0.000000000000000005", - .000000000000000008: "0.000000000000000008", - .1000000000000001: "0.1000000000000001", - .1000000000000002: "0.1000000000000002", - .1000000000000003: "0.1000000000000003", - .1000000000000005: "0.1000000000000005", - .1000000000000008: "0.1000000000000008", - 1e25: "10000000000000000000000000", +type testEnt struct { + float float64 + short string + exact string +} + +var testTable = []*testEnt{ + {3.141592653589793, "3.141592653589793", ""}, + {3, "3", ""}, + {1234567890123456, "1234567890123456", ""}, + {1234567890123456000, "1234567890123456000", ""}, + {1234.567890123456, "1234.567890123456", ""}, + {.1234567890123456, "0.1234567890123456", ""}, + {0, "0", ""}, + {.1111111111111110, "0.111111111111111", ""}, + {.1111111111111111, "0.1111111111111111", ""}, + {.1111111111111119, "0.1111111111111119", ""}, + {.000000000000000001, "0.000000000000000001", ""}, + {.000000000000000002, "0.000000000000000002", ""}, + {.000000000000000003, "0.000000000000000003", ""}, + {.000000000000000005, "0.000000000000000005", ""}, + {.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{ @@ -54,12 +61,23 @@ var testTableScientificNotation = map[string]string{ } 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 - for f, s := range testTable { - if f > 0 { - testTable[-f] = "-" + s + withNeg := testTable[:] + for _, s := range testTable { + if s.float > 0 { + withNeg = append(withNeg, &testEnt{-s.float, "-" + s.short, "-" + s.exact}) } } + testTable = withNeg + for e, s := range testTableScientificNotation { if string(e[0]) != "-" && s != "0" { testTableScientificNotation["-"+e] = "-" + s @@ -68,8 +86,9 @@ func init() { } func TestNewFromFloat(t *testing.T) { - for f, s := range testTable { - d := NewFromFloat(f) + for _, x := range testTable { + s := x.exact + d := NewFromFloat(x.float) if d.String() != s { t.Errorf("expected %s, got %s (%s, %d)", s, d.String(), @@ -92,7 +111,20 @@ func TestNewFromFloat(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) if err != nil { t.Errorf("error while parsing %s", s) @@ -289,7 +321,8 @@ func TestNewFromBigIntWithExponent(t *testing.T) { } func TestJSON(t *testing.T) { - for _, s := range testTable { + for _, x := range testTable { + s := x.short var doc struct { Amount Decimal `json:"amount"` } @@ -358,7 +391,8 @@ func TestBadJSON(t *testing.T) { } func TestNullDecimalJSON(t *testing.T) { - for _, s := range testTable { + for _, x := range testTable { + s := x.short var doc struct { Amount NullDecimal `json:"amount"` } @@ -450,7 +484,8 @@ func TestNullDecimalBadJSON(t *testing.T) { } func TestXML(t *testing.T) { - for _, s := range testTable { + for _, x := range testTable { + s := x.short var doc struct { XMLName xml.Name `xml:"account"` Amount Decimal `xml:"amount"` @@ -2035,7 +2070,8 @@ func TestNullDecimal_Value(t *testing.T) { } func TestBinary(t *testing.T) { - for x := range testTable { + for _, y := range testTable { + x := y.float // Create the decimal d1 := NewFromFloat(x) @@ -2070,7 +2106,8 @@ func slicesEqual(a, b []byte) bool { } func TestGobEncode(t *testing.T) { - for x := range testTable { + for _, y := range testTable { + x := y.float d1 := NewFromFloat(x) b1, err := d1.GobEncode() |