Skip to content

Commit b4445f6

Browse files
Merge pull request #261 from spf13/float-string
Support converting float string numbers to integer types
2 parents d11fd11 + ac48031 commit b4445f6

File tree

4 files changed

+72
-5
lines changed

4 files changed

+72
-5
lines changed

‎number.go‎

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import (
1010
"errors"
1111
"fmt"
1212
"strconv"
13+
"strings"
1314
"time"
1415
)
1516

@@ -391,7 +392,7 @@ func parseNumber[T Number](s string) (T, error) {
391392
}
392393

393394
func parseInt[T integer](s string) (T, error) {
394-
v, err := strconv.ParseInt(trimZeroDecimal(s), 0, 0)
395+
v, err := strconv.ParseInt(trimDecimal(s), 0, 0)
395396
if err != nil {
396397
return 0, err
397398
}
@@ -400,7 +401,7 @@ func parseInt[T integer](s string) (T, error) {
400401
}
401402

402403
func parseUint[T unsigned](s string) (T, error) {
403-
v, err := strconv.ParseUint(trimZeroDecimal(s), 0, 0)
404+
v, err := strconv.ParseUint(trimDecimal(s), 0, 0)
404405
if err != nil {
405406
return 0, err
406407
}
@@ -506,3 +507,15 @@ func trimZeroDecimal(s string) string {
506507
}
507508
return s
508509
}
510+
511+
// trimming decimals seems significantly faster than parsing to float first
512+
//
513+
// see BenchmarkDecimal
514+
func trimDecimal(s string) string {
515+
// trim the decimal part (if any)
516+
if i := strings.Index(s, "."); i >= 0 {
517+
s = s[:i]
518+
}
519+
520+
return s
521+
}

‎number_internal_test.go‎

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
package cast
77

88
import (
9+
"strconv"
910
"testing"
1011

1112
qt "github.com/frankban/quicktest"
@@ -42,3 +43,49 @@ func TestTrimZeroDecimal(t *testing.T) {
4243
c.Assert(trimZeroDecimal("0.0000000000"), qt.Equals, "0")
4344
c.Assert(trimZeroDecimal("0.00000000001"), qt.Equals, "0.00000000001")
4445
}
46+
47+
func TestTrimDecimal(t *testing.T) {
48+
c := qt.New(t)
49+
50+
c.Assert(trimDecimal("10.0"), qt.Equals, "10")
51+
c.Assert(trimDecimal("10.00"), qt.Equals, "10")
52+
c.Assert(trimDecimal("10.010"), qt.Equals, "10")
53+
c.Assert(trimDecimal("0.0000000000"), qt.Equals, "0")
54+
c.Assert(trimDecimal("0.00000000001"), qt.Equals, "0")
55+
}
56+
57+
func BenchmarkDecimal(b *testing.B) {
58+
testCases := []string{"10.0", "10.00", "10.010", "0.0000000000", "0.0000000001", "10.0000000000", "10.0000000001", "10000000000000.0000000000"}
59+
60+
for _, testCase := range testCases {
61+
// TODO: remove after minimum Go version is >=1.22
62+
testCase := testCase
63+
64+
b.Run(testCase, func(b *testing.B) {
65+
b.Run("ParseFloat", func(b *testing.B) {
66+
// TODO: use b.Loop() once updated to Go 1.24
67+
for i := 0; i < b.N; i++ {
68+
v, err := strconv.ParseFloat(testCase, 64)
69+
if err != nil {
70+
b.Fatal(err)
71+
}
72+
73+
n := int64(v)
74+
_ = n
75+
}
76+
})
77+
78+
b.Run("TrimDecimal", func(b *testing.B) {
79+
// TODO: use b.Loop() once updated to Go 1.24
80+
for i := 0; i < b.N; i++ {
81+
v, err := strconv.ParseInt(trimDecimal(testCase), 0, 0)
82+
if err != nil {
83+
b.Fatal(err)
84+
}
85+
86+
_ = v
87+
}
88+
})
89+
})
90+
}
91+
}

‎number_test.go‎

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -204,19 +204,22 @@ func generateNumberTestCases(samples []any) []testCase {
204204
{int64(-8), eightNegative, isUint},
205205
{float32(-8.31), eightPoint31Negative_32, isUint},
206206
{float64(-8.31), eightPoint31Negative, isUint},
207-
{"-8", eightNegative, isUint},
208207

209208
// Other basic types
210209
{true, one, false},
211210
{false, zero, false},
212211
{"8", eight, false},
212+
{"-8", eightNegative, isUint},
213+
{"8.31", eightPoint31, false},
214+
{"-8.31", eightPoint31Negative, isUint},
213215
{"", zero, false},
214216
{nil, zero, false},
215217

216218
// JSON
217219
{json.Number("8"), eight, false},
218220
{json.Number("-8"), eightNegative, isUint},
219-
{json.Number("8.0"), eight, false},
221+
{json.Number("8.31"), eightPoint31, false},
222+
{json.Number("-8.31"), eightPoint31Negative, isUint},
220223
{json.Number(""), zero, false},
221224

222225
// Failure cases

‎time.go‎

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,11 @@ func ToTimeInDefaultLocationE(i interface{}, location *time.Location) (tim time.
2929
case string:
3030
return StringToDateInDefaultLocation(v, location)
3131
case json.Number:
32-
s, err1 := ToInt64E(v)
32+
// Originally this used ToInt64E, but adding string float conversion broke ToTime.
33+
// the behavior of ToTime would have changed if we continued using it.
34+
// For now, using json.Number's own Int64 method should be good enough to preserve backwards compatibility.
35+
v = json.Number(trimZeroDecimal(string(v)))
36+
s, err1 := v.Int64()
3337
if err1 != nil {
3438
return time.Time{}, fmt.Errorf("unable to cast %#v of type %T to Time", i, i)
3539
}

0 commit comments

Comments
 (0)