From c4b5af094a73f8d8c4b35e3a905bf9df33d2e4ba Mon Sep 17 00:00:00 2001 From: Florent AIDE Date: Mon, 23 Nov 2015 16:43:59 +0100 Subject: [PATCH 01/12] Added support for Numeric type in db storage --- decimal.go | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/decimal.go b/decimal.go index e8d99b4..42acf30 100644 --- a/decimal.go +++ b/decimal.go @@ -469,6 +469,14 @@ func (d Decimal) MarshalJSON() ([]byte, error) { // Scan implements the sql.Scanner interface for database deserialization. func (d *Decimal) Scan(value interface{}) error { + var err error + // first try to see if the data is stored in database as a Numeric datatype + if val, ok := value.(float64); ok { + *d = NewFromFloat(val) + return err + } + + // then try to see if it is stored as a string str, err := unquoteIfQuoted(value) if err != nil { return err From 9e9cba8a855f8289b77e6e4b13443166aa29e8e3 Mon Sep 17 00:00:00 2001 From: Florent AIDE Date: Mon, 23 Nov 2015 18:45:31 +0100 Subject: [PATCH 02/12] Better implementatin with switch and support for 0 --- decimal.go | 31 ++++++++++++++++++++----------- 1 file changed, 20 insertions(+), 11 deletions(-) diff --git a/decimal.go b/decimal.go index 42acf30..1a53440 100644 --- a/decimal.go +++ b/decimal.go @@ -471,19 +471,28 @@ func (d Decimal) MarshalJSON() ([]byte, error) { func (d *Decimal) Scan(value interface{}) error { var err error // first try to see if the data is stored in database as a Numeric datatype - if val, ok := value.(float64); ok { - *d = NewFromFloat(val) + switch value.(type) { + + case float64: + // numeric in sqlite3 sends us float64 + *d = NewFromFloat(value.(float64)) + return err + + case int64: + // at least in sqlite3 when the value is 0 in db, the data is sent + // to us as an int64 instead of a float64 ... + *d = NewFromFloat(float64(value.(int64))) + return err + + default: + // default is trying to interpret value stored as string + str, err := unquoteIfQuoted(value) + if err != nil { + return err + } + *d, err = NewFromString(str) return err } - - // then try to see if it is stored as a string - str, err := unquoteIfQuoted(value) - if err != nil { - return err - } - *d, err = NewFromString(str) - - return err } // Value implements the driver.Valuer interface for database serialization. From 28bb8ff9f579e0cfba5d94161d1d9b6ae7875fce Mon Sep 17 00:00:00 2001 From: Florent AIDE Date: Thu, 3 Dec 2015 11:06:49 +0100 Subject: [PATCH 03/12] use directly the int64 instead of casting to float --- decimal.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/decimal.go b/decimal.go index 1a53440..8dbc062 100644 --- a/decimal.go +++ b/decimal.go @@ -481,7 +481,7 @@ func (d *Decimal) Scan(value interface{}) error { case int64: // at least in sqlite3 when the value is 0 in db, the data is sent // to us as an int64 instead of a float64 ... - *d = NewFromFloat(float64(value.(int64))) + *d = New(value.(int64), 0) return err default: From c30673fb5f7b29a4cfab0f1e27098e0860bcec64 Mon Sep 17 00:00:00 2001 From: Florent AIDE Date: Fri, 11 Dec 2015 06:34:52 +0100 Subject: [PATCH 04/12] Use an idiomatic type switch for Scan type comparison --- decimal.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/decimal.go b/decimal.go index 8dbc062..fd2f4c5 100644 --- a/decimal.go +++ b/decimal.go @@ -471,22 +471,22 @@ func (d Decimal) MarshalJSON() ([]byte, error) { func (d *Decimal) Scan(value interface{}) error { var err error // first try to see if the data is stored in database as a Numeric datatype - switch value.(type) { + switch v := value.(type) { case float64: // numeric in sqlite3 sends us float64 - *d = NewFromFloat(value.(float64)) + *d = NewFromFloat(v) return err case int64: // at least in sqlite3 when the value is 0 in db, the data is sent // to us as an int64 instead of a float64 ... - *d = New(value.(int64), 0) + *d = New(v, 0) return err default: // default is trying to interpret value stored as string - str, err := unquoteIfQuoted(value) + str, err := unquoteIfQuoted(v) if err != nil { return err } From b873aa2b431b3ff78acd919b777b0cfe49664ce6 Mon Sep 17 00:00:00 2001 From: Florent AIDE Date: Fri, 11 Dec 2015 06:37:27 +0100 Subject: [PATCH 05/12] Return nil instead of empty err in Scan --- decimal.go | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/decimal.go b/decimal.go index fd2f4c5..cbbce7b 100644 --- a/decimal.go +++ b/decimal.go @@ -469,20 +469,19 @@ func (d Decimal) MarshalJSON() ([]byte, error) { // Scan implements the sql.Scanner interface for database deserialization. func (d *Decimal) Scan(value interface{}) error { - var err error // first try to see if the data is stored in database as a Numeric datatype switch v := value.(type) { case float64: // numeric in sqlite3 sends us float64 *d = NewFromFloat(v) - return err + return nil case int64: // at least in sqlite3 when the value is 0 in db, the data is sent // to us as an int64 instead of a float64 ... *d = New(v, 0) - return err + return nil default: // default is trying to interpret value stored as string From faca378c8a8c3b9e4b1668db106c4cc69099d6aa Mon Sep 17 00:00:00 2001 From: Florent AIDE Date: Fri, 11 Dec 2015 19:46:00 +0100 Subject: [PATCH 06/12] Adding tests from the Scan method --- decimal_test.go | 61 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 61 insertions(+) diff --git a/decimal_test.go b/decimal_test.go index 5202234..e560ffb 100644 --- a/decimal_test.go +++ b/decimal_test.go @@ -813,6 +813,67 @@ func TestDecimal_Max(t *testing.T) { } } +func TestDecimal_Scan(t *testing.T) { + // test the Scan method the implements the + // sql.Scanner interface + // check for the for different type of values + // that are possible to be received from the database + // drivers + + // in normal operations the db driver (sqlite at least) + // will return an int64 if you specified a numeric format + a := Decimal{} + dbvalue := float64(54.33) + expected := NewFromFloat(dbvalue) + + err := a.Scan(dbvalue) + if err != nil { + // Scan failed... no need to test result value + t.Errorf("a.Scan(54.33) failed with message: %s", err) + + } else { + // Scan suceeded... test resulting values + if !a.Equals(expected) { + t.Errorf("%s does not equal to %s", a, expected) + } + } + + // at least SQLite returns an int64 when 0 is stored in the db + // and you specified a numeric format on the schema + dbvalue_int := int64(0) + expected = New(dbvalue_int, 0) + + err = a.Scan(dbvalue_int) + if err != nil { + // Scan failed... no need to test result value + t.Errorf("a.Scan(0) failed with message: %s", err) + + } else { + // Scan suceeded... test resulting values + if !a.Equals(expected) { + t.Errorf("%s does not equal to %s", a, expected) + } + } + + // in case you specified a varchar in your SQL schema, + // the database driver will return byte slice []byte + value_str := "535.666" + dbvalue_str := []byte(value_str) + expected, err = NewFromString(value_str) + + err = a.Scan(dbvalue_str) + if err != nil { + // Scan failed... no need to test result value + t.Errorf("a.Scan('535.666') failed with message: %s", err) + + } else { + // Scan suceeded... test resulting values + if !a.Equals(expected) { + t.Errorf("%s does not equal to %s", a, expected) + } + } +} + // old tests after this line func TestDecimal_Scale(t *testing.T) { From cb949cead428aa2affdfd1d318f0a60f9cfbbfb1 Mon Sep 17 00:00:00 2001 From: Piotr Date: Fri, 11 Dec 2015 23:54:57 +0100 Subject: [PATCH 07/12] Add a fast track to d.Cmp(d2) when d.exp == d2.exp This makes a noticeable difference when one is sorting a large slice of decimals when most of them have the same precision, as it is saving a lot of calls to rescale(). --- decimal.go | 7 +++++++ decimal_test.go | 17 +++++++++++++++++ 2 files changed, 24 insertions(+) diff --git a/decimal.go b/decimal.go index e8d99b4..2055fb1 100644 --- a/decimal.go +++ b/decimal.go @@ -294,6 +294,13 @@ func (d Decimal) Div(d2 Decimal) Decimal { // +1 if d > d2 // func (d Decimal) Cmp(d2 Decimal) int { + d.ensureInitialized() + d2.ensureInitialized() + + if d.exp == d2.exp { + return d.value.Cmp(d2.value) + } + baseExp := min(d.exp, d2.exp) rd := d.rescale(baseExp) rd2 := d2.rescale(baseExp) diff --git a/decimal_test.go b/decimal_test.go index 5202234..6af9d1c 100644 --- a/decimal_test.go +++ b/decimal_test.go @@ -4,6 +4,7 @@ import ( "encoding/json" "encoding/xml" "math" + "sort" "strconv" "strings" "testing" @@ -895,3 +896,19 @@ func didPanic(f func()) bool { return ret } + +type DecimalSlice []Decimal + +func (p DecimalSlice) Len() int { return len(p) } +func (p DecimalSlice) Swap(i, j int) { p[i], p[j] = p[j], p[i] } +func (p DecimalSlice) Less(i, j int) bool { return p[i].Cmp(p[j]) < 0 } +func Benchmark_Cmp(b *testing.B) { + decimals := DecimalSlice([]Decimal{}) + for i := 0; i < 1000000; i++ { + decimals = append(decimals, New(int64(i), 0)) + } + b.ResetTimer() + for i := 0; i < b.N; i++ { + sort.Sort(decimals) + } +} From eae6d13435116dfc37038a23537ecb24c1a474b8 Mon Sep 17 00:00:00 2001 From: Nathan VanBenschoten Date: Wed, 13 Jan 2016 13:27:32 -0500 Subject: [PATCH 08/12] Prevent unnecessary int64 overflow in NewFromFloat --- decimal.go | 2 +- decimal_test.go | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/decimal.go b/decimal.go index cbbce7b..9f62fea 100644 --- a/decimal.go +++ b/decimal.go @@ -140,7 +140,7 @@ func NewFromFloat(value float64) Decimal { floor := math.Floor(value) // fast path, where float is an int - if floor == value && !math.IsInf(value, 0) { + if floor == value && value <= math.MaxInt64 && value >= math.MinInt64 { return New(int64(value), 0) } diff --git a/decimal_test.go b/decimal_test.go index e560ffb..f3169f3 100644 --- a/decimal_test.go +++ b/decimal_test.go @@ -31,6 +31,7 @@ var testTable = map[float64]string{ .1000000000000003: "0.1000000000000003", .1000000000000005: "0.1000000000000005", .1000000000000008: "0.1000000000000008", + 1e25: "10000000000000000000000000", } var testTableScientificNotation = map[string]string{ From ecd4a8353a9cab722c72466fe5ecaa7ef7e353bd Mon Sep 17 00:00:00 2001 From: Nathan VanBenschoten Date: Fri, 15 Jan 2016 16:32:48 -0500 Subject: [PATCH 09/12] Implement Modulus arithmetic function --- decimal.go | 6 ++++++ decimal_test.go | 33 +++++++++++++++++++++++++++++++++ 2 files changed, 39 insertions(+) diff --git a/decimal.go b/decimal.go index 0d2425f..287f2c5 100644 --- a/decimal.go +++ b/decimal.go @@ -287,6 +287,12 @@ func (d Decimal) Div(d2 Decimal) Decimal { return ret } +// Mod returns d % d2. +func (d Decimal) Mod(d2 Decimal) Decimal { + quo := d.Div(d2).Truncate(0) + return d.Sub(d2.Mul(quo)) +} + // Cmp compares the numbers represented by d and d2 and returns: // // -1 if d < d2 diff --git a/decimal_test.go b/decimal_test.go index ecfb5ad..9175622 100644 --- a/decimal_test.go +++ b/decimal_test.go @@ -685,6 +685,39 @@ func TestDecimal_Div(t *testing.T) { } } +func TestDecimal_Mod(t *testing.T) { + type Inp struct { + a string + b string + } + + inputs := map[Inp]string{ + Inp{"3", "2"}: "1", + Inp{"3451204593", "2454495034"}: "996709559", + Inp{"24544.95034", ".3451204593"}: "0.3283950433", + Inp{".1", ".1"}: "0", + Inp{"0", "1.001"}: "0", + Inp{"-7.5", "2"}: "-1.5", + Inp{"7.5", "-2"}: "1.5", + Inp{"-7.5", "-2"}: "-1.5", + } + + for inp, res := range inputs { + a, err := NewFromString(inp.a) + if err != nil { + t.FailNow() + } + b, err := NewFromString(inp.b) + if err != nil { + t.FailNow() + } + c := a.Mod(b) + if c.String() != res { + t.Errorf("expected %s, got %s", res, c.String()) + } + } +} + func TestDecimal_Overflow(t *testing.T) { if !didPanic(func() { New(1, math.MinInt32).Mul(New(1, math.MinInt32)) }) { t.Fatalf("should have gotten an overflow panic") From 95627a6ad0f3ec63704b8d1f8e503f486ab2362d Mon Sep 17 00:00:00 2001 From: Vadim Graboys Date: Tue, 2 Feb 2016 21:30:32 -0500 Subject: [PATCH 10/12] Add badginator badge --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index dfa6498..a1d15bc 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# decimal [![Build Status](https://travis-ci.org/shopspring/decimal.png?branch=master)](https://travis-ci.org/shopspring/decimal) +# decimal [![Build Status](https://travis-ci.org/shopspring/decimal.png?branch=master)](https://travis-ci.org/shopspring/decimal) [![BADGINATOR](https://badginator.herokuapp.com//.svg)](https://github.com/defunctzombie/badginator) Arbitrary-precision fixed-point decimal numbers in go. From d3c5a19729ca9f4962b1f4701df3f595397998f9 Mon Sep 17 00:00:00 2001 From: Vadim Graboys Date: Tue, 2 Feb 2016 21:32:16 -0500 Subject: [PATCH 11/12] Add badginator badge, 4real --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index a1d15bc..7c08804 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# decimal [![Build Status](https://travis-ci.org/shopspring/decimal.png?branch=master)](https://travis-ci.org/shopspring/decimal) [![BADGINATOR](https://badginator.herokuapp.com//.svg)](https://github.com/defunctzombie/badginator) +# decimal [![Build Status](https://travis-ci.org/shopspring/decimal.png?branch=master)](https://travis-ci.org/shopspring/decimal) [![BADGINATOR](https://badginator.herokuapp.com/shopspring/decimal.svg)](https://github.com/defunctzombie/badginator) Arbitrary-precision fixed-point decimal numbers in go. From d282b0416666d39ec41fc37a31123241ffe6d935 Mon Sep 17 00:00:00 2001 From: Vadim Graboys Date: Tue, 2 Feb 2016 21:55:03 -0500 Subject: [PATCH 12/12] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 7c08804..7aaadae 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# decimal [![Build Status](https://travis-ci.org/shopspring/decimal.png?branch=master)](https://travis-ci.org/shopspring/decimal) [![BADGINATOR](https://badginator.herokuapp.com/shopspring/decimal.svg)](https://github.com/defunctzombie/badginator) +# decimal [![Build Status](https://travis-ci.org/shopspring/decimal.png?branch=master)](https://travis-ci.org/shopspring/decimal) [![BADGINATOR](https://badginator.herokuapp.com/shopspring/decimal.svg?image_analysis=1)](https://github.com/defunctzombie/badginator) Arbitrary-precision fixed-point decimal numbers in go.