Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
231 changes: 192 additions & 39 deletions number.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (
"encoding/json"
"errors"
"fmt"
"math"
"regexp"
"strconv"
"strings"
Expand Down Expand Up @@ -95,29 +96,29 @@ func toNumber[T Number](i any) (T, bool) {
case T:
return s, true
case int:
return T(s), true
return narrowSigned[T](int64(s))
case int8:
return T(s), true
return narrowSigned[T](int64(s))
case int16:
return T(s), true
return narrowSigned[T](int64(s))
case int32:
return T(s), true
return narrowSigned[T](int64(s))
case int64:
return T(s), true
return narrowSigned[T](s)
case uint:
return T(s), true
return narrowUnsigned[T](uint64(s))
case uint8:
return T(s), true
return narrowUnsigned[T](uint64(s))
case uint16:
return T(s), true
return narrowUnsigned[T](uint64(s))
case uint32:
return T(s), true
return narrowUnsigned[T](uint64(s))
case uint64:
return T(s), true
return narrowUnsigned[T](s)
case float32:
return T(s), true
return narrowFloat[T](float64(s))
case float64:
return T(s), true
return narrowFloat[T](s)
case bool:
if s {
return 1, true
Expand All @@ -127,9 +128,9 @@ func toNumber[T Number](i any) (T, bool) {
case nil:
return 0, true
case time.Weekday:
return T(s), true
return narrowSigned[T](int64(s))
case time.Month:
return T(s), true
return narrowSigned[T](int64(s))
}

return 0, false
Expand Down Expand Up @@ -202,54 +203,59 @@ func toUnsignedNumber[T Number](i any) (T, bool, bool) {
if s < 0 {
return 0, false, false
}

return T(s), true, true
n, ok := narrowUnsigned[T](uint64(s))
return n, true, ok
case int8:
if s < 0 {
return 0, false, false
}

return T(s), true, true
n, ok := narrowUnsigned[T](uint64(s))
return n, true, ok
case int16:
if s < 0 {
return 0, false, false
}

return T(s), true, true
n, ok := narrowUnsigned[T](uint64(s))
return n, true, ok
case int32:
if s < 0 {
return 0, false, false
}

return T(s), true, true
n, ok := narrowUnsigned[T](uint64(s))
return n, true, ok
case int64:
if s < 0 {
return 0, false, false
}

return T(s), true, true
n, ok := narrowUnsigned[T](uint64(s))
return n, true, ok
case uint:
return T(s), true, true
n, ok := narrowUnsigned[T](uint64(s))
return n, true, ok
case uint8:
return T(s), true, true
n, ok := narrowUnsigned[T](uint64(s))
return n, true, ok
case uint16:
return T(s), true, true
n, ok := narrowUnsigned[T](uint64(s))
return n, true, ok
case uint32:
return T(s), true, true
n, ok := narrowUnsigned[T](uint64(s))
return n, true, ok
case uint64:
return T(s), true, true
n, ok := narrowUnsigned[T](s)
return n, true, ok
case float32:
if s < 0 {
return 0, false, false
}

return T(s), true, true
n, ok := narrowFloat[T](float64(s))
return n, true, ok
case float64:
if s < 0 {
return 0, false, false
}

return T(s), true, true
n, ok := narrowFloat[T](s)
return n, true, ok
case bool:
if s {
return 1, true, true
Expand All @@ -262,14 +268,14 @@ func toUnsignedNumber[T Number](i any) (T, bool, bool) {
if s < 0 {
return 0, false, false
}

return T(s), true, true
n, ok := narrowUnsigned[T](uint64(s))
return n, true, ok
case time.Month:
if s < 0 {
return 0, false, false
}

return T(s), true, true
n, ok := narrowUnsigned[T](uint64(s))
return n, true, ok
}

return 0, true, false
Expand Down Expand Up @@ -405,7 +411,7 @@ func parseNumber[T Number](s string) (T, error) {
}

func parseInt[T integer](s string) (T, error) {
v, err := strconv.ParseInt(trimDecimal(s), 0, 0)
v, err := strconv.ParseInt(trimDecimal(s), 0, intBitSize[T]())
if err != nil {
return 0, err
}
Expand All @@ -414,14 +420,161 @@ func parseInt[T integer](s string) (T, error) {
}

func parseUint[T unsigned](s string) (T, error) {
v, err := strconv.ParseUint(strings.TrimLeft(trimDecimal(s), "+"), 0, 0)
v, err := strconv.ParseUint(strings.TrimLeft(trimDecimal(s), "+"), 0, uintBitSize[T]())
if err != nil {
return 0, err
}

return T(v), nil
}

func intBitSize[T integer]() int {
var zero T
switch any(zero).(type) {
case int:
return strconv.IntSize
case int8:
return 8
case int16:
return 16
case int32:
return 32
case int64:
return 64
default:
return 0
}
}

func uintBitSize[T unsigned]() int {
var zero T
switch any(zero).(type) {
case uint:
return strconv.IntSize
case uint8:
return 8
case uint16:
return 16
case uint32:
return 32
case uint64:
return 64
default:
return 0
}
}

// narrowSigned converts v to T. An integer target that cannot hold v fails.
// Float targets keep the language conversion.
func narrowSigned[T Number](v int64) (T, bool) {
var zero T
switch any(zero).(type) {
case float32:
return any(float32(v)).(T), true
case float64:
return any(float64(v)).(T), true
case int:
if v < int64(math.MinInt) || v > int64(math.MaxInt) {
return 0, false
}
return any(int(v)).(T), true
case int8:
if v < math.MinInt8 || v > math.MaxInt8 {
return 0, false
}
return any(int8(v)).(T), true
case int16:
if v < math.MinInt16 || v > math.MaxInt16 {
return 0, false
}
return any(int16(v)).(T), true
case int32:
if v < math.MinInt32 || v > math.MaxInt32 {
return 0, false
}
return any(int32(v)).(T), true
case int64:
return any(v).(T), true
case uint:
if v < 0 || uint64(v) > uint64(^uint(0)) {
return 0, false
}
return any(uint(v)).(T), true
case uint8:
if v < 0 || v > math.MaxUint8 {
return 0, false
}
return any(uint8(v)).(T), true
case uint16:
if v < 0 || v > math.MaxUint16 {
return 0, false
}
return any(uint16(v)).(T), true
case uint32:
if v < 0 || v > math.MaxUint32 {
return 0, false
}
return any(uint32(v)).(T), true
case uint64:
if v < 0 {
return 0, false
}
return any(uint64(v)).(T), true
default:
return 0, false
}
}

func narrowUnsigned[T Number](v uint64) (T, bool) {
if v > math.MaxInt64 {
var zero T
switch any(zero).(type) {
case uint64:
return any(v).(T), true
case uint:
if v <= uint64(^uint(0)) {
return any(uint(v)).(T), true
}
return 0, false
default:
return 0, false
}
}
return narrowSigned[T](int64(v))
}

func narrowFloat[T Number](v float64) (T, bool) {
if math.IsNaN(v) || math.IsInf(v, 0) {
return 0, false
}

var zero T
switch any(zero).(type) {
case float32:
return any(float32(v)).(T), true
case float64:
return any(v).(T), true
case uint64:
if v < 0 || v >= float64(math.MaxUint64) {
return 0, false
}
return any(uint64(v)).(T), true
case uint:
if strconv.IntSize == 64 && v >= 0 && v < float64(math.MaxUint64) {
return any(uint(v)).(T), true
}
if v < 0 || v >= float64(math.MaxInt64) {
return 0, false
}
return narrowSigned[T](int64(v))
default:
if v < float64(math.MinInt64) || v >= float64(math.MaxInt64) {
return 0, false
}
return narrowSigned[T](int64(v))
}
}

func parseFloat[T float](s string) (T, error) {
var t T

Expand Down
43 changes: 42 additions & 1 deletion number_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -262,7 +262,11 @@ func generateNumberTestCases(samples []any) []testCase {
testCases = append(testCases, testCase{underflowString, zero, true})
}

if kind == reflect.Uint64 && isUint && overflowString != nil {
if isSint && underflowString != nil {
testCases = append(testCases, testCase{underflowString, zero, true})
}

if isInt && overflowString != nil {
testCases = append(testCases, testCase{overflowString, zero, true})
}

Expand Down Expand Up @@ -464,3 +468,40 @@ func BenchmarkNumber(b *testing.B) {
})
}
}

func TestNarrowIntegerOverflow(t *testing.T) {
c := qt.New(t)

v8, err := cast.ToInt8E("128")
c.Assert(err, qt.IsNotNil)
c.Assert(v8, qt.Equals, int8(0))
c.Assert(cast.ToInt8("128"), qt.Equals, int8(0))

v8, err = cast.ToInt8E(int64(128))
c.Assert(err, qt.IsNotNil)
c.Assert(v8, qt.Equals, int8(0))

v8, err = cast.ToInt8E("127")
c.Assert(err, qt.IsNil)
c.Assert(v8, qt.Equals, int8(127))

v8, err = cast.ToInt8E("-128")
c.Assert(err, qt.IsNil)
c.Assert(v8, qt.Equals, int8(-128))

v8, err = cast.ToInt8E("8.9")
c.Assert(err, qt.IsNil)
c.Assert(v8, qt.Equals, int8(8))

u8, err := cast.ToUint8E("256")
c.Assert(err, qt.IsNotNil)
c.Assert(u8, qt.Equals, uint8(0))

u8, err = cast.ToUint8E(int(256))
c.Assert(err, qt.IsNotNil)
c.Assert(u8, qt.Equals, uint8(0))

u8, err = cast.ToUint8E("255")
c.Assert(err, qt.IsNil)
c.Assert(u8, qt.Equals, uint8(255))
}