]> Cypherpunks repositories - gostls13.git/commitdiff
math/big: implement precise Float to decimal conversion (core functionality)
authorRobert Griesemer <gri@golang.org>
Tue, 3 Feb 2015 00:31:13 +0000 (16:31 -0800)
committerRobert Griesemer <gri@golang.org>
Tue, 3 Feb 2015 19:04:26 +0000 (19:04 +0000)
Change-Id: Ic0153397922ded28a5cb362e86ecdfec42e92163
Reviewed-on: https://go-review.googlesource.com/3752
Reviewed-by: Alan Donovan <adonovan@google.com>
src/math/big/decimal.go [new file with mode: 0644]
src/math/big/decimal_test.go [new file with mode: 0644]

diff --git a/src/math/big/decimal.go b/src/math/big/decimal.go
new file mode 100644 (file)
index 0000000..f4c535a
--- /dev/null
@@ -0,0 +1,191 @@
+// Copyright 2015 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+// This file implements multi-precision decimal numbers.
+// The implementation is for float to decimal conversion only;
+// not general purpose use.
+// The only operations are precise conversion from binary to
+// decimal and rounding.
+//
+// The key observation and some code (shr) is borrowed from
+// strconv/decimal.go: conversion of binary fractional values can be done
+// precisely in multi-precision decimal because 2 divides 10 (required for
+// >> of mantissa); but conversion of decimal floating-point values cannot
+// be done precisely in binary representation.
+//
+// In contrast to strconv/decimal.go, only right shift is implemented in
+// decimal format - left shift can be done precisely in binary format.
+
+package big
+
+// A decimal represents a floating-point number in decimal representation.
+// The value of a decimal x is x.mant * 10 ** x.exp with 0.5 <= x.mant < 1,
+// with the most-significant mantissa digit at index 0.
+type decimal struct {
+       mant []byte // mantissa ASCII digits, big-endian
+       exp  int    // exponent, valid if len(mant) > 0
+}
+
+// Maximum shift amount that can be done in one pass without overflow.
+// A Word has _W bits and (1<<maxShift - 1)*10 + 9 must fit into Word.
+const maxShift = _W - 4
+
+// TODO(gri) Since we know the desired decimal precision when converting
+// a floating-point number, we may be able to limit the number of decimal
+// digits that need to be computed by init by providing an additional
+// precision argument and keeping track of when a number was truncated early
+// (equivalent of "sticky bit" in binary rounding).
+
+// Init initializes x to the decimal representation of m << shift (for
+// shift >= 0), or m >> -shift (for shift < 0).
+func (x *decimal) init(m nat, shift int) {
+       // special case 0
+       if len(m) == 0 {
+               x.mant = x.mant[:0]
+               return
+       }
+
+       // Optimization: If we need to shift right, first remove any trailing
+       // zero bits from m to reduce shift amount that needs to be done in
+       // decimal format (since that is likely slower).
+       if shift < 0 {
+               ntz := m.trailingZeroBits()
+               s := uint(-shift)
+               if s >= ntz {
+                       s = ntz // shift at most ntz bits
+               }
+               m = nat(nil).shr(m, s)
+               shift += int(s)
+       }
+
+       // Do any shift left in binary representation.
+       if shift > 0 {
+               m = nat(nil).shl(m, uint(shift))
+               shift = 0
+       }
+
+       // Convert mantissa into decimal representation.
+       s := m.decimalString() // TODO(gri) avoid string conversion here
+       n := len(s)
+       x.exp = n
+       // Trim trailing zeros; instead the exponent is tracking
+       // the decimal point independent of the number of digits.
+       for n > 0 && s[n-1] == 0 {
+               n--
+       }
+       x.mant = make([]byte, n)
+       copy(x.mant, s)
+
+       // Do any (remaining) shift right in decimal representation.
+       if shift < 0 {
+               for shift < -maxShift {
+                       x.shr(maxShift)
+                       shift += maxShift
+               }
+               x.shr(uint(-shift))
+       }
+}
+
+// Possibly optimization: The current implementation of nat.string takes
+// a charset argument. When a right shift is needed, we could provide
+// "\x00\x01...\x09" instead of "012..9" (as in nat.decimalString) and
+// avoid the repeated +'0' and -'0' operations in decimal.shr (and do a
+// single +'0' pass at the end).
+
+// shr implements x >> s, for s <= maxShift.
+func (x *decimal) shr(s uint) {
+       // Division by 1<<s using shift-and-subtract algorithm.
+
+       // pick up enough leading digits to cover first shift
+       r := 0 // read index
+       var n Word
+       for n>>s == 0 && r < len(x.mant) {
+               ch := Word(x.mant[r])
+               r++
+               n = n*10 + ch - '0'
+       }
+       if n == 0 {
+               // x == 0; shouldn't get here, but handle anyway
+               x.mant = x.mant[:0]
+               return
+       }
+       for n>>s == 0 {
+               r++
+               n *= 10
+       }
+       x.exp += 1 - r
+
+       // read a digit, write a digit
+       w := 0 // write index
+       for r < len(x.mant) {
+               ch := Word(x.mant[r])
+               r++
+               d := n >> s
+               n -= d << s
+               x.mant[w] = byte(d + '0')
+               w++
+               n = n*10 + ch - '0'
+       }
+
+       // write extra digits that still fit
+       for n > 0 && w < len(x.mant) {
+               d := n >> s
+               n -= d << s
+               x.mant[w] = byte(d + '0')
+               w++
+               n = n * 10
+       }
+       x.mant = x.mant[:w] // the number may be shorter (e.g. 1024 >> 10)
+
+       // append additional digits that didn't fit
+       for n > 0 {
+               d := n >> s
+               n -= d << s
+               x.mant = append(x.mant, byte(d+'0'))
+               n = n * 10
+       }
+
+       // remove trailing zeros
+       w = len(x.mant)
+       for w > 0 && x.mant[w-1] == '0' {
+               w--
+       }
+       x.mant = x.mant[:w]
+}
+
+func (x *decimal) String() string {
+       if len(x.mant) == 0 {
+               return "0"
+       }
+
+       var buf []byte
+       switch {
+       case x.exp <= 0:
+               // 0.00ddd
+               buf = append(buf, "0."...)
+               buf = appendZeros(buf, -x.exp)
+               buf = append(buf, x.mant...)
+
+       case /* 0 < */ x.exp < len(x.mant):
+               // dd.ddd
+               buf = append(buf, x.mant[:x.exp]...)
+               buf = append(buf, '.')
+               buf = append(buf, x.mant[x.exp:]...)
+
+       default: // len(x.mant) <= x.exp
+               // ddd00
+               buf = append(buf, x.mant...)
+               buf = appendZeros(buf, x.exp-len(x.mant))
+       }
+
+       return string(buf)
+}
+
+// appendZeros appends n 0 digits to buf and returns buf.
+func appendZeros(buf []byte, n int) []byte {
+       for ; n > 0; n-- {
+               buf = append(buf, '0')
+       }
+       return buf
+}
diff --git a/src/math/big/decimal_test.go b/src/math/big/decimal_test.go
new file mode 100644 (file)
index 0000000..ce20800
--- /dev/null
@@ -0,0 +1,51 @@
+// Copyright 2015 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package big
+
+import "testing"
+
+func TestDecimalString(t *testing.T) {
+       for _, test := range []struct {
+               x    decimal
+               want string
+       }{
+               {want: "0"},
+               {decimal{nil, 1000}, "0"}, // exponent of 0 is ignored
+               {decimal{[]byte("12345"), 0}, "0.12345"},
+               {decimal{[]byte("12345"), -3}, "0.00012345"},
+               {decimal{[]byte("12345"), +3}, "123.45"},
+               {decimal{[]byte("12345"), +10}, "1234500000"},
+       } {
+               if got := test.x.String(); got != test.want {
+                       t.Errorf("%v == %s; want %s", test.x, got, test.want)
+               }
+       }
+}
+
+func TestDecimalInit(t *testing.T) {
+       for _, test := range []struct {
+               x     Word
+               shift int
+               want  string
+       }{
+               {0, 0, "0"},
+               {0, -100, "0"},
+               {0, 100, "0"},
+               {1, 0, "1"},
+               {1, 10, "1024"},
+               {1, 100, "1267650600228229401496703205376"},
+               {1, -100, "0.0000000000000000000000000000007888609052210118054117285652827862296732064351090230047702789306640625"},
+               {12345678, 8, "3160493568"},
+               {12345678, -8, "48225.3046875"},
+               {195312, 9, "99999744"},
+               {1953125, 9, "1000000000"},
+       } {
+               var d decimal
+               d.init(nat{test.x}.norm(), test.shift)
+               if got := d.String(); got != test.want {
+                       t.Errorf("%d << %d == %s; want %s", test.x, test.shift, got, test.want)
+               }
+       }
+}