From bf9a39e28bc9aea136acaf2c3628e68f65e3c3df Mon Sep 17 00:00:00 2001 From: Igor Mikushkin Date: Fri, 2 Mar 2018 01:29:13 +0400 Subject: [PATCH] Addressing rounding and overflow issues in NewFromFloatWithExponent (#77) * Additional (and some breaking) tests for NewFromFloatWithExponent * Addressing tests for NewFromFloatWithExponent * Naming cosmetic correction * removing unused code * Improving FromFloatWithExponent * Addressing special meaning of zero exponent in float64 * NewFromFloatWithExponent: subnormals support * NewFromFloatWithExponent: just a few additional test cases * NewFromFloatWithExponent: optimization and some documentation * NewFromFloatWithExponent: optimizations * NewFromFloatWithExponent: optimizations --- decimal.go | 86 +++++++++++++++++++++++++++++++++++++++++-------- decimal_test.go | 41 ++++++++++++++++++----- 2 files changed, 106 insertions(+), 21 deletions(-) diff --git a/decimal.go b/decimal.go index 44bc2fb..cd15c30 100644 --- a/decimal.go +++ b/decimal.go @@ -188,15 +188,82 @@ func NewFromFloat(value float64) Decimal { // NewFromFloatWithExponent(123.456, -2).String() // output: "123.46" // func NewFromFloatWithExponent(value float64, exp int32) Decimal { - mul := math.Pow(10, -float64(exp)) - floatValue := value * mul - if math.IsNaN(floatValue) || math.IsInf(floatValue, 0) { - panic(fmt.Sprintf("Cannot create a Decimal from %v", floatValue)) + if math.IsNaN(value) || math.IsInf(value, 0) { + panic(fmt.Sprintf("Cannot create a Decimal from %v", value)) + } + + bits := math.Float64bits(value) + mant := bits & (1<<52 - 1) + exp2 := int32((bits >> 52) & (1<<11 - 1)) + sign := bits >> 63 + + if exp2 == 0 { + // specials + if mant == 0 { + return Decimal{} + } else { + // subnormal + exp2++ + } + } else { + // normal + mant |= 1 << 52 + } + + exp2 -= 1023 + 52 + + // normalizing base-2 values + for mant&1 == 0 { + mant = mant >> 1 + exp2++ + } + + // maximum number of fractional base-10 digits to represent 2^N exactly cannot be more than -N if N<0 + if exp < 0 && exp < exp2 { + if exp2 < 0 { + exp = exp2 + } else { + exp = 0 + } + } + + // representing 10^M * 2^N as 5^M * 2^(M+N) + exp2 -= exp + + temp := big.NewInt(1) + dMant := big.NewInt(int64(mant)) + + // applying 5^M + if exp > 0 { + temp = temp.SetInt64(int64(exp)) + temp = temp.Exp(fiveInt, temp, nil) + } else if exp < 0 { + temp = temp.SetInt64(-int64(exp)) + temp = temp.Exp(fiveInt, temp, nil) + dMant = dMant.Mul(dMant, temp) + temp = temp.SetUint64(1) + } + + // applying 2^(M+N) + if exp2 > 0 { + dMant = dMant.Lsh(dMant, uint(exp2)) + } else if exp2 < 0 { + temp = temp.Lsh(temp, uint(-exp2)) + } + + // rounding and downscaling + if exp > 0 || exp2 < 0 { + halfDown := new(big.Int).Rsh(temp, 1) + dMant = dMant.Add(dMant, halfDown) + dMant = dMant.Quo(dMant, temp) + } + + if sign == 1 { + dMant = dMant.Neg(dMant) } - dValue := big.NewInt(round(floatValue)) return Decimal{ - value: dValue, + value: dMant, exp: exp, } } @@ -972,13 +1039,6 @@ func min(x, y int32) int32 { return x } -func round(n float64) int64 { - if n < 0 { - return int64(n - 0.5) - } - return int64(n + 0.5) -} - func unquoteIfQuoted(value interface{}) (string, error) { var bytes []byte diff --git a/decimal_test.go b/decimal_test.go index 384f8a5..fa57083 100644 --- a/decimal_test.go +++ b/decimal_test.go @@ -191,20 +191,45 @@ func TestNewFromFloatWithExponent(t *testing.T) { float float64 exp int32 } + // some tests are taken from here https://www.cockroachlabs.com/blog/rounding-implementations-in-go/ tests := map[Inp]string{ - Inp{123.4, -3}: "123.4", - Inp{123.4, -1}: "123.4", - Inp{123.412345, 1}: "120", - Inp{123.412345, 0}: "123", - Inp{123.412345, -5}: "123.41235", - Inp{123.412345, -6}: "123.412345", - Inp{123.412345, -7}: "123.412345", + Inp{123.4, -3}: "123.4", + Inp{123.4, -1}: "123.4", + Inp{123.412345, 1}: "120", + Inp{123.412345, 0}: "123", + Inp{123.412345, -5}: "123.41235", + Inp{123.412345, -6}: "123.412345", + Inp{123.412345, -7}: "123.412345", + Inp{123.412345, -28}: "123.4123450000000019599610823207", + Inp{1230000000, 3}: "1230000000", + Inp{123.9999999999999999, -7}: "124", + Inp{123.8989898999999999, -7}: "123.8989899", + Inp{0.49999999999999994, 0}: "0", + Inp{0.5, 0}: "1", + Inp{0., -1000}: "0", + Inp{0.5000000000000001, 0}: "1", + Inp{1.390671161567e-309, 0}: "0", + Inp{4.503599627370497e+15, 0}: "4503599627370497", + Inp{4.503599627370497e+60, 0}: "4503599627370497110902645731364739935039854989106233267453952", + Inp{4.503599627370497e+60, 1}: "4503599627370497110902645731364739935039854989106233267453950", + Inp{4.503599627370497e+60, -1}: "4503599627370497110902645731364739935039854989106233267453952", + Inp{50, 2}: "100", + Inp{49, 2}: "0", + Inp{50, 3}: "0", + // subnormals + Inp{1.390671161567e-309, -2000}: "0.000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001390671161567000864431395448332752540137009987788957394095829635554502771758698872408926974382819387852542087331897381878220271350970912568035007740861074263206736245957501456549756342151614772544950978154339064833880234531754156635411349342950306987480369774780312897442981323940546749863054846093718407237782253156822124910364044261653195961209878120072488178603782495270845071470243842997312255994555557251870400944414666445871039673491570643357351279578519863428540219295076767898526278029257129758694673164251056158277568765100904638511604478844087596428177947970563689475826736810456067108202083804368114484417399279328807983736233036662284338182105684628835292230438999173947056675615385756827890872955322265625", + Inp{1.390671161567e-309, -862}: "0.0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000013906711615670008644313954483327525401370099877889573940958296355545027717586988724089269743828193878525420873318973818782202713509709125680350077408610742632067362459575014565497563421516147725449509781543390648338802345317541566354113493429503069874803697747803128974429813239405467498630548460937184072377822531568221249103640442616531959612098781200724881786037824952708450714702438429973122559945555572518704009444146664458710396734915706433573512795785198634285402192950767678985262780292571297586946731642510561582775687651009046385116044788440876", + Inp{1.390671161567e-309, -863}: "0.0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000013906711615670008644313954483327525401370099877889573940958296355545027717586988724089269743828193878525420873318973818782202713509709125680350077408610742632067362459575014565497563421516147725449509781543390648338802345317541566354113493429503069874803697747803128974429813239405467498630548460937184072377822531568221249103640442616531959612098781200724881786037824952708450714702438429973122559945555572518704009444146664458710396734915706433573512795785198634285402192950767678985262780292571297586946731642510561582775687651009046385116044788440876", } // add negatives for p, s := range tests { if p.float > 0 { - tests[Inp{-p.float, p.exp}] = "-" + s + if s != "0" { + tests[Inp{-p.float, p.exp}] = "-" + s + } else { + tests[Inp{-p.float, p.exp}] = "0" + } } }