diff --git a/decimal/decimal.go b/decimal/decimal.go index a57f1d05..335866e5 100644 --- a/decimal/decimal.go +++ b/decimal/decimal.go @@ -49,18 +49,26 @@ func NewFromInt(i int64) Decimal { return Decimal(i) * scaleFactor } +var errEmpty = errors.New("empty string") +var errTooBig = errors.New("number too big") +var errInvalid = errors.New("invalid syntax") + // atoi64 is equivalent to strconv.Atoi func atoi64(s string) (bool, int64, error) { sLen := len(s) - if sLen < 1 || sLen > 18 { - return false, 0, errors.New("atoi failed") + if sLen < 1 { + return false, 0, errEmpty + } + if sLen > 18 { + return false, 0, errTooBig } + neg := false if s[0] == '-' { neg = true s = s[1:] if len(s) < 1 { - return false, 0, errors.New("atoi failed") + return neg, 0, errEmpty } } @@ -68,7 +76,7 @@ func atoi64(s string) (bool, int64, error) { for _, ch := range []byte(s) { ch -= '0' if ch > 9 { - return false, 0, errors.New("atoi failed") + return neg, 0, errInvalid } n = n*10 + int64(ch) } @@ -82,21 +90,15 @@ func atoi64(s string) (bool, int64, error) { // error if integer parsing fails. func NewFromString(s string) (Decimal, error) { if whole, frac, split := strings.Cut(s, "."); split { - var neg bool - var w int64 - if whole == "-" { - neg = true - } else if whole != "" { - var err error - neg, w, err = atoi64(whole) - if err != nil { - return Zero, err - } + neg, w, err := atoi64(whole) + // if fractional portion exists, whole part can be empty + if err != nil && err != errEmpty { + return Zero, err } // overflow if w > parseMax || w < parseMin { - return Zero, errors.New("number too big") + return Zero, errTooBig } w = w * int64(scaleFactor) @@ -106,7 +108,7 @@ func NewFromString(s string) (Decimal, error) { for _, b := range frac { f *= 10 if b < '0' || b > '9' { - return Zero, errors.New("invalid syntax") + return Zero, errInvalid } f += int64(b - '0') seen++ @@ -126,7 +128,7 @@ func NewFromString(s string) (Decimal, error) { } else { _, i, err := atoi64(s) if i > parseMax || i < parseMin { - return Zero, errors.New("number too big") + return Zero, errTooBig } i = i * int64(scaleFactor) return Decimal(i), err diff --git a/decimal/decimal_test.go b/decimal/decimal_test.go index 5320bfaf..7fe624ec 100644 --- a/decimal/decimal_test.go +++ b/decimal/decimal_test.go @@ -298,47 +298,47 @@ var testParseCases = []testCase{ }, { "error-1", - "number too big", + errTooBig.Error(), "100000000000000000", }, { "error-2", - "number too big", + errTooBig.Error(), "10000000000000000", }, { "error-3", - "number too big", + errTooBig.Error(), "10000000000000000.56", }, { "error-4", - "invalid syntax", + errInvalid.Error(), "0.e0", }, { "error-5", - "atoi failed", + errTooBig.Error(), "5555555555555555555555555550000000000000000", }, { "error-6", - "atoi failed", + errEmpty.Error(), "-", }, { "error-7", - "atoi failed", + errEmpty.Error(), "", }, { "error-badint-1", - `atoi failed`, + errInvalid.Error(), "1QZ.56", }, { "error-expr-1", - `atoi failed`, + errInvalid.Error(), "(123 * 6)", }, { @@ -351,6 +351,21 @@ var testParseCases = []testCase{ "-0.50", "-.50", }, + { + "missingfrac", + "5.00", + "5.", + }, + { + "neg-missingfrac", + "-5.00", + "-5.", + }, + { + "just-a-decimal", + "0.00", + ".", + }, } func TestStringParse(t *testing.T) { @@ -364,6 +379,9 @@ func TestStringParse(t *testing.T) { t.Fatalf("Error(%s): expected `%s`, got `%s`", tc.name, tc.Result, err) } } + if !strings.HasPrefix(tc.name, "error") && err != nil { + t.Fatalf("Error(%s): unexpected error `%s`", tc.name, err) + } if !strings.HasPrefix(tc.name, "error") && tc.Result != d.StringFixedBank() { t.Errorf("Error(%s): expected \n`%s`, \ngot \n`%s`", tc.name, tc.Result, d.StringFixedBank()) } @@ -395,7 +413,7 @@ func FuzzStringParse(f *testing.F) { } func BenchmarkNewFromString(b *testing.B) { - numbers := []string{"10.0", "245.6", "354", "2.456"} + numbers := []string{"10.0", "245.6", "354", "2.456", "-31.2"} for n := 0; n < b.N; n++ { for _, numStr := range numbers { NewFromString(numStr)