Skip to content

Commit 08afb35

Browse files
authored
UnmarshalJSON, Scan and NewFromString performance improvements (#403)
* `UnmarshalJSON` and `NewFromString` performance improvements This PR improves `UnmarshalJSON` performance by reducing unnecessary allocation caused by `unquoteIfQuoted` function. This also touches on `Scan` method to split `default` case into `string` and `[]byte` cases. This PR also slightly touches `NewFromString` function by making so scientific notation and dots are checked in a single loop. * Add malformed scientific notation check and tests
1 parent 01bb203 commit 08afb35

File tree

3 files changed

+65
-43
lines changed

3 files changed

+65
-43
lines changed

decimal.go

Lines changed: 39 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -182,8 +182,26 @@ func NewFromString(value string) (Decimal, error) {
182182
var intString string
183183
var exp int64
184184

185-
// Check if number is using scientific notation
186-
eIndex := strings.IndexAny(value, "Ee")
185+
// Check if number is using scientific notation and find dots
186+
eIndex := -1
187+
pIndex := -1
188+
for i, r := range value {
189+
if r == 'E' || r == 'e' {
190+
if eIndex > -1 {
191+
return Decimal{}, fmt.Errorf("can't convert %s to decimal: multiple 'E' characters found", value)
192+
}
193+
eIndex = i
194+
continue
195+
}
196+
197+
if r == '.' {
198+
if pIndex > -1 {
199+
return Decimal{}, fmt.Errorf("can't convert %s to decimal: too many .s", value)
200+
}
201+
pIndex = i
202+
}
203+
}
204+
187205
if eIndex != -1 {
188206
expInt, err := strconv.ParseInt(value[eIndex+1:], 10, 32)
189207
if err != nil {
@@ -196,23 +214,12 @@ func NewFromString(value string) (Decimal, error) {
196214
exp = expInt
197215
}
198216

199-
pIndex := -1
200-
vLen := len(value)
201-
for i := 0; i < vLen; i++ {
202-
if value[i] == '.' {
203-
if pIndex > -1 {
204-
return Decimal{}, fmt.Errorf("can't convert %s to decimal: too many .s", value)
205-
}
206-
pIndex = i
207-
}
208-
}
209-
210217
if pIndex == -1 {
211218
// There is no decimal point, we can just parse the original string as
212219
// an int
213220
intString = value
214221
} else {
215-
if pIndex+1 < vLen {
222+
if pIndex+1 < len(value) {
216223
intString = value[:pIndex] + value[pIndex+1:]
217224
} else {
218225
intString = value[:pIndex]
@@ -1766,15 +1773,10 @@ func (d *Decimal) UnmarshalJSON(decimalBytes []byte) error {
17661773
return nil
17671774
}
17681775

1769-
str, err := unquoteIfQuoted(decimalBytes)
1770-
if err != nil {
1771-
return fmt.Errorf("error decoding string '%s': %s", decimalBytes, err)
1772-
}
1773-
1774-
decimal, err := NewFromString(str)
1776+
decimal, err := NewFromString(unquoteIfQuoted(string(decimalBytes)))
17751777
*d = decimal
17761778
if err != nil {
1777-
return fmt.Errorf("error decoding string '%s': %s", str, err)
1779+
return fmt.Errorf("error decoding string '%s': %s", string(decimalBytes), err)
17781780
}
17791781
return nil
17801782
}
@@ -1852,14 +1854,18 @@ func (d *Decimal) Scan(value interface{}) error {
18521854
*d = NewFromUint64(v)
18531855
return nil
18541856

1855-
default:
1856-
// default is trying to interpret value stored as string
1857-
str, err := unquoteIfQuoted(v)
1858-
if err != nil {
1859-
return err
1860-
}
1861-
*d, err = NewFromString(str)
1857+
case string:
1858+
var err error
1859+
*d, err = NewFromString(unquoteIfQuoted(v))
1860+
return err
1861+
1862+
case []byte:
1863+
var err error
1864+
*d, err = NewFromString(unquoteIfQuoted(string(v)))
18621865
return err
1866+
1867+
default:
1868+
return fmt.Errorf("could not convert value '%+v' to any known type", value)
18631869
}
18641870
}
18651871

@@ -2021,23 +2027,13 @@ func RescalePair(d1 Decimal, d2 Decimal) (Decimal, Decimal) {
20212027
return d1, d2
20222028
}
20232029

2024-
func unquoteIfQuoted(value interface{}) (string, error) {
2025-
var bytes []byte
2026-
2027-
switch v := value.(type) {
2028-
case string:
2029-
bytes = []byte(v)
2030-
case []byte:
2031-
bytes = v
2032-
default:
2033-
return "", fmt.Errorf("could not convert value '%+v' to byte array of type '%T'", value, value)
2034-
}
2035-
2030+
func unquoteIfQuoted(value string) string {
20362031
// If the amount is quoted, strip the quotes
2037-
if len(bytes) > 2 && bytes[0] == '"' && bytes[len(bytes)-1] == '"' {
2038-
bytes = bytes[1 : len(bytes)-1]
2032+
if len(value) > 2 && value[0] == '"' && value[len(value)-1] == '"' {
2033+
return value[1 : len(value)-1]
20392034
}
2040-
return string(bytes), nil
2035+
2036+
return value
20412037
}
20422038

20432039
// NullDecimal represents a nullable decimal with compatibility for

decimal_bench_test.go

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -312,3 +312,15 @@ func BenchmarkDecimal_ExpTaylor(b *testing.B) {
312312
_, _ = d.ExpTaylor(10)
313313
}
314314
}
315+
316+
func BenchmarkDecimal_UnmarshalJSON(b *testing.B) {
317+
b.ResetTimer()
318+
319+
bstr := []byte("1234.56789")
320+
321+
b.ReportAllocs()
322+
b.ResetTimer()
323+
for i := 0; i < b.N; i++ {
324+
_ = (&Decimal{}).UnmarshalJSON(bstr)
325+
}
326+
}

decimal_test.go

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,11 @@ var testTableScientificNotation = map[string]string{
8080
"123.456e10": "1234560000000",
8181
}
8282

83+
var testMalformedDecimalStrings = map[string]error{
84+
"1ee10": fmt.Errorf("can't convert %s to decimal: multiple 'E' characters found", "1ee10"),
85+
"123.45.66": fmt.Errorf("can't convert %s to decimal: too many .s", "123.45.66"),
86+
}
87+
8388
func init() {
8489
for _, s := range testTable {
8590
s.exact = strconv.FormatFloat(s.float, 'f', 1500, 64)
@@ -239,6 +244,15 @@ func TestNewFromString(t *testing.T) {
239244
d.value.String(), d.exp)
240245
}
241246
}
247+
248+
for s, e := range testMalformedDecimalStrings {
249+
_, err := NewFromString(s)
250+
if err == nil {
251+
t.Errorf("expected an error, got nil %s", s)
252+
} else if err.Error() != e.Error() {
253+
t.Errorf("expected %v error, got %v", e, err)
254+
}
255+
}
242256
}
243257

244258
func TestNewFromFormattedString(t *testing.T) {

0 commit comments

Comments
 (0)