mirror of
https://github.com/golang/go
synced 2024-10-01 01:18:32 -06:00
math/big: implement precise Float to decimal conversion (core functionality)
Change-Id: Ic0153397922ded28a5cb362e86ecdfec42e92163 Reviewed-on: https://go-review.googlesource.com/3752 Reviewed-by: Alan Donovan <adonovan@google.com>
This commit is contained in:
parent
6966e07ae7
commit
01ba4c000c
191
src/math/big/decimal.go
Normal file
191
src/math/big/decimal.go
Normal file
@ -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
|
||||
}
|
51
src/math/big/decimal_test.go
Normal file
51
src/math/big/decimal_test.go
Normal file
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user