Merge pull request #458 from zenixls2/fix/fixedpoint

fix: dnum panic, precision loss in parsing string in legacy
This commit is contained in:
Yo-An Lin 2022-03-01 00:43:23 +08:00 committed by GitHub
commit f90070771d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 152 additions and 30 deletions

View File

@ -9,6 +9,7 @@ import (
"fmt"
"math"
"strconv"
"strings"
"sync/atomic"
)
@ -111,7 +112,9 @@ func (v Value) FormatPercentage(prec int) string {
if v == 0 {
return "0"
}
result := strconv.FormatFloat(float64(v)/DefaultPow*100., 'f', prec, 64)
pow := math.Pow10(prec)
result := strconv.FormatFloat(
math.Trunc(float64(v)/DefaultPow * pow * 100.) / pow, 'f', prec, 64)
return result + "%"
}
@ -220,9 +223,7 @@ func (v *Value) UnmarshalYAML(unmarshal func(a interface{}) error) (err error) {
}
func (v Value) MarshalJSON() ([]byte, error) {
f := float64(v) / DefaultPow
o := strconv.FormatFloat(f, 'f', 8, 64)
return []byte(o), nil
return []byte(v.FormatString(DefaultPrecision)), nil
}
func (v *Value) UnmarshalJSON(data []byte) error {
@ -319,17 +320,72 @@ func NewFromString(input string) (Value, error) {
if isPercentage {
input = input[0 : length-1]
}
dotIndex := -1
hasDecimal := false
decimalCount := 0
// if is decimal, we don't need this
hasScientificNotion := false
scIndex := -1
for i, c := range(input) {
if hasDecimal {
if c <= '9' && c >= '0' {
decimalCount++
} else {
break
}
v, err := strconv.ParseFloat(input, 64)
if err != nil {
return 0, err
} else if c == '.' {
dotIndex = i
hasDecimal = true
}
if c == 'e' || c == 'E' {
hasScientificNotion = true
scIndex = i
break
}
}
if hasDecimal {
after := input[dotIndex+1:len(input)]
if decimalCount >= 8 {
after = after[0:8] + "." + after[8:len(after)]
} else {
after = after[0:decimalCount] + strings.Repeat("0", 8-decimalCount) + after[decimalCount:len(after)]
}
input = input[0:dotIndex] + after
v, err := strconv.ParseFloat(input, 64)
if err != nil {
return 0, err
}
if isPercentage {
v = v * 0.01
}
return Value(int64(math.Trunc(v))), nil
} else if hasScientificNotion {
exp, err := strconv.ParseInt(input[scIndex+1:len(input)], 10, 32)
if err != nil {
return 0, err
}
v, err := strconv.ParseFloat(input[0:scIndex+1] + strconv.FormatInt(exp + 8, 10), 64)
if err != nil {
return 0, err
}
return Value(int64(math.Trunc(v))), nil
} else {
v, err := strconv.ParseInt(input, 10, 64)
if err != nil {
return 0, err
}
if isPercentage {
v = v * DefaultPow / 100
} else {
v = v * DefaultPow
}
return Value(v), nil
}
if isPercentage {
v = v * 0.01
}
return NewFromFloat(v), nil
}
func MustNewFromString(input string) Value {
@ -360,7 +416,7 @@ func Must(v Value, err error) Value {
}
func NewFromFloat(val float64) Value {
return Value(int64(math.Round(val * DefaultPow)))
return Value(int64(math.Trunc(val * DefaultPow)))
}
func NewFromInt(val int64) Value {

View File

@ -29,6 +29,8 @@ const (
coefMax = 9999_9999_9999_9999
digitsMax = 16
shiftMax = digitsMax - 1
// to switch between scientific notion and normal presentation format
maxLeadingZeros = 19
)
// common values
@ -251,9 +253,12 @@ func Inf(sign int8) Value {
func (dn Value) FormatString(prec int) string {
if dn.sign == 0 {
return "0"
if prec <= 0 {
return "0"
} else {
return "0." + strings.Repeat("0", prec)
}
}
const maxLeadingZeros = 7
sign := ""
if dn.sign < 0 {
sign = "-"
@ -266,11 +271,18 @@ func (dn Value) FormatString(prec int) string {
e := int(dn.exp) - nd
if -maxLeadingZeros <= dn.exp && dn.exp <= 0 {
// decimal to the left
return sign + "0." + strings.Repeat("0", -e-nd) + digits[:min(prec, nd)] + strings.Repeat("0", max(0, prec-nd+e+nd))
if prec+e+nd > 0 {
return sign + "0." + strings.Repeat("0", -e-nd) + digits[:min(prec+e+nd, nd)] + strings.Repeat("0", max(0, prec-nd+e+nd))
} else if -e-nd > 0 {
return "0." + strings.Repeat("0", -e-nd)
} else {
return "0"
}
} else if -nd < e && e <= -1 {
// decimal within
dec := nd + e
return sign + digits[:dec] + "." + digits[dec:min(dec+prec, nd)] + strings.Repeat("0", max(0, min(dec+prec, nd)-dec-prec))
decimals := digits[dec:min(dec+prec, nd)]
return sign + digits[:dec] + "." + decimals + strings.Repeat("0", max(0, prec - len(decimals)))
} else if 0 < dn.exp && dn.exp <= digitsMax {
// decimal to the right
if prec > 0 {
@ -282,7 +294,7 @@ func (dn Value) FormatString(prec int) string {
// scientific notation
after := ""
if nd > 1 {
after = "." + digits[1:min(1+prec, nd)] + strings.Repeat("0", min(1+prec, nd)-1-prec)
after = "." + digits[1:min(1+prec, nd)] + strings.Repeat("0", max(0, min(1+prec, nd)-1-prec))
}
return sign + digits[:1] + after + "e" + strconv.Itoa(int(dn.exp-1))
}
@ -293,7 +305,6 @@ func (dn Value) String() string {
if dn.sign == 0 {
return "0"
}
const maxLeadingZeros = 7
sign := ""
if dn.sign < 0 {
sign = "-"
@ -328,7 +339,6 @@ func (dn Value) Percentage() string {
if dn.sign == 0 {
return "0%"
}
const maxLeadingZeros = 7
sign := ""
if dn.sign < 0 {
sign = "-"
@ -362,9 +372,12 @@ func (dn Value) Percentage() string {
func (dn Value) FormatPercentage(prec int) string {
if dn.sign == 0 {
return "0"
if prec <= 0 {
return "0"
} else {
return "0." + strings.Repeat("0", prec)
}
}
const maxLeadingZeros = 7
sign := ""
if dn.sign < 0 {
sign = "-"
@ -379,19 +392,30 @@ func (dn Value) FormatPercentage(prec int) string {
if -maxLeadingZeros <= exp && exp <= 0 {
// decimal to the left
return sign + "0." + strings.Repeat("0", -e-nd) + digits[:min(prec, nd)] + strings.Repeat("0", max(0, prec-nd+e+nd)) + "%"
if prec+e+nd > 0 {
return sign + "0." + strings.Repeat("0", -e-nd) + digits[:min(prec+e+nd, nd)] + strings.Repeat("0", max(0, prec-nd+e+nd)) + "%"
} else if -e-nd > 0 {
return "0." + strings.Repeat("0", -e-nd) + "%"
} else {
return "0"
}
} else if -nd < e && e <= -1 {
// decimal within
dec := nd + e
return sign + digits[:dec] + "." + digits[dec:min(dec+prec, nd)] + strings.Repeat("0", max(0, min(dec+prec, nd)-dec-prec)) + "%"
decimals := digits[dec:min(dec+prec, nd)]
return sign + digits[:dec] + "." + decimals + strings.Repeat("0", max(0, prec - len(decimals))) + "%"
} else if 0 < exp && exp <= digitsMax {
// decimal to the right
return sign + digits + strings.Repeat("0", e) + "." + strings.Repeat("0", prec) + "%"
if prec > 0 {
return sign + digits + strings.Repeat("0", e) + "." + strings.Repeat("0", prec) + "%"
} else {
return sign + digits + strings.Repeat("0", e) + "%"
}
} else {
// scientific notation
after := ""
if nd > 1 {
after = "." + digits[1:min(1+prec, nd)] + strings.Repeat("0", min(1+prec, nd)-1-prec)
after = "." + digits[1:min(1+prec, nd)] + strings.Repeat("0", max(0, min(1+prec, nd)-1-prec))
}
return sign + digits[:1] + after + "e" + strconv.Itoa(int(exp-1)) + "%"
}

View File

@ -24,12 +24,14 @@ func TestInternal(t *testing.T) {
f = MustNewFromString("1.00000000000000111")
assert.Equal(t, "1.000000000000001", f.String())
f = MustNewFromString("1.1e-15")
assert.Equal(t, "1.1e-15", f.String())
assert.Equal(t, "0.0000000000000011", f.String())
assert.Equal(t, 16, f.NumFractionalDigits())
f = MustNewFromString("1.00000000000000111")
assert.Equal(t, "1.000000000000001", f.String())
f = MustNewFromString("0.0000000001000111")
assert.Equal(t, "1.000111e-10", f.String())
f = MustNewFromString("0.00000000000000000001000111")
assert.Equal(t, "0.00000000000000000001000111", f.String())
f = MustNewFromString("0.000000000000000000001000111")
assert.Equal(t, "1.000111e-21", f.String())
f = MustNewFromString("1e-100")
assert.Equal(t, 100, f.NumFractionalDigits())
}

View File

@ -3,8 +3,8 @@ package fixedpoint
import (
"math/big"
"testing"
"github.com/stretchr/testify/assert"
"encoding/json"
)
const Delta = 1e-9
@ -128,6 +128,46 @@ func TestFromString(t *testing.T) {
assert.Equal(t, Zero, f)
}
func TestJson(t *testing.T) {
p := MustNewFromString("0")
e, err := json.Marshal(p)
assert.NoError(t, err)
assert.Equal(t, "0.00000000", string(e))
p = MustNewFromString("1.00000003")
e, err = json.Marshal(p)
assert.NoError(t, err)
assert.Equal(t, "1.00000003", string(e))
p = MustNewFromString("1.000000003")
e, err = json.Marshal(p)
assert.NoError(t, err)
assert.Equal(t, "1.00000000", string(e))
p = MustNewFromString("1.000000008")
e, err = json.Marshal(p)
assert.NoError(t, err)
assert.Equal(t, "1.00000000", string(e))
p = MustNewFromString("0.999999999")
e, err = json.Marshal(p)
assert.NoError(t, err)
assert.Equal(t, "0.99999999", string(e))
p = MustNewFromString("1.2e-9")
e, err = json.Marshal(p)
assert.NoError(t, err)
assert.Equal(t, "0.00000000", p.FormatString(8))
assert.Equal(t, "0.00000000", string(e))
_ = json.Unmarshal([]byte("0.00153917575"), &p)
assert.Equal(t, "0.00153917", p.FormatString(8))
var q Value
q = NewFromFloat(0.00153917575)
assert.Equal(t, p, q)
_ = json.Unmarshal([]byte("6e-8"), &p)
_ = json.Unmarshal([]byte("0.000062"), &q)
assert.Equal(t, "0.00006194", q.Sub(p).String())
}
func TestNumFractionalDigits(t *testing.T) {
tests := []struct {
name string