From fd760b41a11f8bbe3f5c9abbbcc01460fef55071 Mon Sep 17 00:00:00 2001 From: ccoVeille <3875889+ccoVeille@users.noreply.github.com> Date: Fri, 6 Sep 2024 21:35:09 +0200 Subject: [PATCH] feat: add initial code --- conversion.go | 155 ++++++ conversion_test.go | 1269 ++++++++++++++++++++++++++++++++++++++++++++ go.mod | 3 + go.sum | 0 4 files changed, 1427 insertions(+) create mode 100644 conversion.go create mode 100644 conversion_test.go create mode 100644 go.mod create mode 100644 go.sum diff --git a/conversion.go b/conversion.go new file mode 100644 index 0000000..bdf8fec --- /dev/null +++ b/conversion.go @@ -0,0 +1,155 @@ +// In Go, integer type conversion can lead to unexpected behavior and errors if not handled carefully. +// Issues can happen when converting between signed and unsigned integers, or when converting to a smaller integer type. +// This package aims to solve this issue + +package safecast + +import ( + "errors" + "fmt" + "math" +) + +// Number is an alias for the int, uint, int8, uint8, int16, uint16, int32, uint32, int64, and uint64 types. +type Number interface { + ~int | ~uint | ~int8 | ~uint8 | ~int16 | ~uint16 | ~int32 | ~uint32 | ~int64 | ~uint64 +} + +var ErrOutOfRange = errors.New("out of range") + +// ToInt attempts to convert any [Number] value to an int. +// If the conversion results in a value outside the range of an int, +// an ErrOutOfRange error is returned. +func ToInt[T Number](i T) (int, error) { + if i > 0 && uint64(i) > math.MaxInt { + return 0, fmt.Errorf("%w: %v is greater than math.MaxInt", ErrOutOfRange, i) + } + + return int(i), nil +} + +// ToUint attempts to convert any [Number] value to an uint. +// If the conversion results in a value outside the range of an uint, +// an ErrOutOfRange error is returned. +func ToUint[T Number](i T) (uint, error) { + return uint(i), nil +} + +// ToInt8 attempts to convert any [Number] value to an int8. +// If the conversion results in a value outside the range of an int8, +// an ErrOutOfRange error is returned. +func ToInt8[T Number](i T) (int8, error) { + if i > math.MaxInt8 { + return 0, fmt.Errorf("%w: %v is greater than math.MaxInt8", ErrOutOfRange, i) + } + + if i < 0 && uint64(-i) > -math.MinInt8 { + return 0, fmt.Errorf("%w: %v is less than math.MinInt8", ErrOutOfRange, i) + } + + return int8(i), nil +} + +// ToUint8 attempts to convert any [Number] value to an uint8. +// If the conversion results in a value outside the range of an uint8, +// an ErrOutOfRange error is returned. +func ToUint8[T Number](i T) (uint8, error) { + if err := assertNotNegative(i); err != nil { + return 0, err + } + + if uint64(i) > math.MaxUint8 { + return 0, fmt.Errorf("%w: %v is greater than math.MaxUint8", ErrOutOfRange, i) + } + + return uint8(i), nil +} + +// ToInt16 attempts to convert any [Number] value to an int16. +// If the conversion results in a value outside the range of an int16, +// an ErrOutOfRange error is returned. +func ToInt16[T Number](i T) (int16, error) { + if i > 0 && uint64(i) > math.MaxInt16 { + return 0, fmt.Errorf("%w: %v is greater than math.MaxInt16", ErrOutOfRange, i) + } + + if i < 0 && uint64(-i) > -math.MinInt16 { + return 0, fmt.Errorf("%w: %v is less than math.MinInt16", ErrOutOfRange, i) + } + + return int16(i), nil +} + +// ToUint16 attempts to convert any [Number] value to an uint16. +// If the conversion results in a value outside the range of an uint16, +// an ErrOutOfRange error is returned. +func ToUint16[T Number](i T) (uint16, error) { + if err := assertNotNegative(i); err != nil { + return 0, err + } + + if uint64(i) > math.MaxUint16 { + return 0, fmt.Errorf("%w: %v is greater than math.MaxUint16", ErrOutOfRange, i) + } + + return uint16(i), nil +} + +// ToInt32 attempts to convert any [Number] value to an int32. +// If the conversion results in a value outside the range of an int32, +// an ErrOutOfRange error is returned. +func ToInt32[T Number](i T) (int32, error) { + if i > 0 && uint64(i) > math.MaxInt32 { + return 0, fmt.Errorf("%w: %v is greater than math.MaxInt32", ErrOutOfRange, i) + } + + if i < 0 && uint64(-i) > -math.MinInt32 { + return 0, fmt.Errorf("%w: %v is less than math.MinInt32", ErrOutOfRange, i) + } + + return int32(i), nil +} + +// ToUint32 attempts to convert any [Number] value to an uint32. +// If the conversion results in a value outside the range of an uint32, +// an ErrOutOfRange error is returned. +func ToUint32[T Number](i T) (uint32, error) { + if err := assertNotNegative(i); err != nil { + return 0, err + } + + if uint64(i) > math.MaxUint32 { + return 0, fmt.Errorf("%w: %v is greater than math.MaxUint32", ErrOutOfRange, i) + } + + return uint32(i), nil +} + +// ToInt64 attempts to convert any [Number] value to an int64. +// If the conversion results in a value outside the range of an int64, +// an ErrOutOfRange error is returned. +func ToInt64[T Number](i T) (int64, error) { + if i > 0 && uint64(i) > math.MaxInt64 { + return 0, fmt.Errorf("%w: %v is greater than math.MaxInt64", ErrOutOfRange, i) + } + + return int64(i), nil +} + +// ToUint64 attempts to convert any [Number] value to an uint64. +// If the conversion results in a value outside the range of an uint64, +// an ErrOutOfRange error is returned. +func ToUint64[T Number](i T) (uint64, error) { + if err := assertNotNegative(i); err != nil { + return 0, err + } + + return uint64(i), nil +} + +func assertNotNegative[T Number](i T) error { + if i < 0 { + return fmt.Errorf("%w: %v is negative", ErrOutOfRange, i) + } + return nil +} diff --git a/conversion_test.go b/conversion_test.go new file mode 100644 index 0000000..d6deb66 --- /dev/null +++ b/conversion_test.go @@ -0,0 +1,1269 @@ +package safecast_test + +import ( + "errors" + "math" + "testing" + + "github.com/ccoveille/go-safecast" +) + +func assertEqual[V comparable](t *testing.T, expected, got V) { + t.Helper() + + if expected == got { + return + } + + t.Errorf("Not equal: \n"+ + "expected: %v (%T)\n"+ + "actual : %v (%T)", expected, expected, got, got) +} + +func requireError(t *testing.T, err error) { + t.Helper() + + if err == nil { + t.Fatal("expected error") + } + + if !errors.Is(err, safecast.ErrOutOfRange) { + t.Errorf("expected out of range error, got %v", err) + } +} + +func assertNoError(t *testing.T, err error) { + t.Helper() + + if err != nil { + t.Errorf("expected no error, got %v", err) + } +} + +type caseInt8[in safecast.Number] struct { + name string + input in + want int8 +} + +func assertInt8OK[in safecast.Number](t *testing.T, tests []caseInt8[in]) { + t.Helper() + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := safecast.ToInt8(tt.input) + assertNoError(t, err) + assertEqual(t, tt.want, got) + }) + } +} + +func assertInt8Error[in safecast.Number](t *testing.T, tests []caseInt8[in]) { + t.Helper() + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := safecast.ToInt8(tt.input) + requireError(t, err) + assertEqual(t, tt.want, got) + }) + } +} + +func TestToInt8(t *testing.T) { + t.Run("from int", func(t *testing.T) { + assertInt8OK(t, []caseInt8[int]{ + {name: "zero", input: 0, want: 0}, + {name: "positive within range", input: 100, want: 100}, + {name: "negative within range", input: -100, want: -100}, + }) + + assertInt8Error(t, []caseInt8[int]{ + {name: "positive out of range", input: math.MaxInt8 + 1}, + {name: "negative out of range", input: math.MinInt8 - 1}, + }) + }) + + t.Run("from int8", func(t *testing.T) { + assertInt8OK(t, []caseInt8[int8]{ + {name: "zero", input: 0, want: 0}, + {name: "positive within range", input: 100, want: 100}, + {name: "negative within range", input: -100, want: -100}, + }) + }) + + t.Run("from int16", func(t *testing.T) { + assertInt8OK(t, []caseInt8[int16]{ + {name: "zero", input: 0, want: 0}, + {name: "positive within range", input: 100, want: 100}, + {name: "negative within range", input: -100, want: -100}, + }) + + assertInt8Error(t, []caseInt8[int16]{ + {name: "positive out of range", input: math.MaxInt8 + 1}, + {name: "negative out of range", input: math.MinInt8 - 1}, + }) + }) + + t.Run("from int32", func(t *testing.T) { + assertInt8OK(t, []caseInt8[int32]{ + {name: "zero", input: 0, want: 0}, + {name: "positive within range", input: 100, want: 100}, + {name: "negative within range", input: -100, want: -100}, + }) + + assertInt8Error(t, []caseInt8[int32]{ + {name: "positive out of range", input: math.MaxInt8 + 1}, + {name: "negative out of range", input: math.MinInt8 - 1}, + }) + }) + + t.Run("from int64", func(t *testing.T) { + assertInt8OK(t, []caseInt8[int64]{ + {name: "zero", input: 0, want: 0}, + {name: "positive within range", input: 100, want: 100}, + {name: "negative within range", input: -100, want: -100}, + }) + + assertInt8Error(t, []caseInt8[int64]{ + {name: "positive out of range", input: math.MaxInt8 + 1}, + {name: "negative out of range", input: math.MinInt8 - 1}, + }) + }) + + t.Run("from uint", func(t *testing.T) { + assertInt8OK(t, []caseInt8[uint]{ + {name: "zero", input: 0, want: 0}, + {name: "positive within range", input: 100, want: 100}, + }) + + assertInt8Error(t, []caseInt8[uint]{ + {name: "positive out of range", input: math.MaxInt8 + 1}, + }) + }) + + t.Run("from uint8", func(t *testing.T) { + assertInt8OK(t, []caseInt8[uint8]{ + {name: "zero", input: 0, want: 0}, + {name: "positive within range", input: 100, want: 100}, + }) + }) + + t.Run("from uint16", func(t *testing.T) { + assertInt8OK(t, []caseInt8[uint16]{ + {name: "zero", input: 0, want: 0}, + {name: "positive within range", input: 100, want: 100}, + }) + + assertInt8Error(t, []caseInt8[uint16]{ + {name: "positive out of range", input: math.MaxInt8 + 1}, + }) + }) + + t.Run("from uint32", func(t *testing.T) { + assertInt8OK(t, []caseInt8[uint32]{ + {name: "zero", input: 0, want: 0}, + {name: "positive within range", input: 100, want: 100}, + }) + + assertInt8Error(t, []caseInt8[uint32]{ + {name: "positive out of range", input: math.MaxInt8 + 1}, + }) + }) + + t.Run("from uint64", func(t *testing.T) { + assertInt8OK(t, []caseInt8[uint64]{ + {name: "zero", input: 0, want: 0}, + {name: "positive within range", input: 100, want: 100}, + }) + + assertInt8Error(t, []caseInt8[uint64]{ + {name: "positive out of range", input: math.MaxInt8 + 1}, + }) + }) +} + +type caseUint8[in safecast.Number] struct { + name string + input in + want uint8 +} + +func assertUint8OK[in safecast.Number](t *testing.T, tests []caseUint8[in]) { + t.Helper() + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := safecast.ToUint8(tt.input) + assertNoError(t, err) + assertEqual(t, tt.want, got) + }) + } +} + +func assertUint8Error[in safecast.Number](t *testing.T, tests []caseUint8[in]) { + t.Helper() + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Helper() + + got, err := safecast.ToUint8(tt.input) + requireError(t, err) + assertEqual(t, tt.want, got) + }) + } +} + +func TestToUint8(t *testing.T) { + t.Run("from int", func(t *testing.T) { + assertUint8OK(t, []caseUint8[int]{ + {name: "zero", input: 0, want: 0}, + {name: "positive within range", input: 100, want: 100}, + }) + + assertUint8Error(t, []caseUint8[int]{ + {name: "positive out of range", input: math.MaxUint8 + 1}, + {name: "negative value", input: -1}, + }) + }) + + t.Run("from int8", func(t *testing.T) { + assertUint8OK(t, []caseUint8[int8]{ + {name: "zero", input: 0, want: 0}, + {name: "positive within range", input: 100, want: 100}, + }) + }) + + t.Run("from int16", func(t *testing.T) { + assertUint8OK(t, []caseUint8[int16]{ + {name: "zero", input: 0, want: 0}, + {name: "positive within range", input: 100, want: 100}, + }) + }) + + t.Run("from int32", func(t *testing.T) { + assertUint8OK(t, []caseUint8[int32]{ + {name: "zero", input: 0, want: 0}, + {name: "positive within range", input: 100, want: 100}, + }) + + assertUint8Error(t, []caseUint8[int32]{ + {name: "positive out of range", input: 100000}, + {name: "negative value", input: -1}, + }) + }) + + t.Run("from int64", func(t *testing.T) { + assertUint8OK(t, []caseUint8[int64]{ + {name: "zero", input: 0, want: 0}, + {name: "positive within range", input: 100, want: 100}, + }) + + assertUint8Error(t, []caseUint8[int64]{ + {name: "positive out of range", input: 100000}, + {name: "negative value", input: -1}, + }) + }) + + t.Run("from uint", func(t *testing.T) { + assertUint8OK(t, []caseUint8[uint]{ + {name: "zero", input: 0, want: 0}, + {name: "positive within range", input: 100, want: 100}, + }) + + assertUint8Error(t, []caseUint8[uint]{ + {name: "positive out of range", input: math.MaxUint8 + 1}, + }) + }) + + t.Run("from uint8", func(t *testing.T) { + assertUint8OK(t, []caseUint8[uint8]{ + {name: "zero", input: 0, want: 0}, + {name: "positive within range", input: 100, want: 100}, + }) + }) + + t.Run("from uint16", func(t *testing.T) { + assertUint8OK(t, []caseUint8[uint16]{ + {name: "zero", input: 0, want: 0}, + {name: "positive within range", input: 100, want: 100}, + }) + + assertUint8Error(t, []caseUint8[uint]{ + {name: "positive out of range", input: math.MaxUint8 + 1}, + }) + }) + + t.Run("from uint32", func(t *testing.T) { + assertUint8OK(t, []caseUint8[uint32]{ + {name: "zero", input: 0, want: 0}, + {name: "positive within range", input: 100, want: 100}, + }) + + assertUint8Error(t, []caseUint8[uint]{ + {name: "positive out of range", input: math.MaxUint8 + 1}, + }) + }) + + t.Run("from uint64", func(t *testing.T) { + assertUint8OK(t, []caseUint8[uint64]{ + {name: "zero", input: 0, want: 0}, + {name: "positive within range", input: 100, want: 100}, + }) + + assertUint8Error(t, []caseUint8[uint]{ + {name: "positive out of range", input: math.MaxUint8 + 1}, + }) + }) +} + +type caseInt16[in safecast.Number] struct { + name string + input in + want int16 +} + +func assertInt16OK[in safecast.Number](t *testing.T, tests []caseInt16[in]) { + t.Helper() + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := safecast.ToInt16(tt.input) + assertNoError(t, err) + assertEqual(t, tt.want, got) + }) + } +} + +func assertInt16Error[in safecast.Number](t *testing.T, tests []caseInt16[in]) { + t.Helper() + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := safecast.ToInt16(tt.input) + requireError(t, err) + assertEqual(t, tt.want, got) + }) + } +} + +func TestToInt16(t *testing.T) { + t.Run("from int", func(t *testing.T) { + assertInt16OK(t, []caseInt16[int]{ + {name: "zero", input: 0, want: 0}, + {name: "positive within range", input: 10000, want: 10000}, + {name: "negative within range", input: -10000, want: -10000}, + }) + + assertInt16Error(t, []caseInt16[int]{ + {name: "positive out of range", input: math.MaxInt16 + 1}, + {name: "negative out of range", input: math.MinInt16 - 1}, + }) + }) + + t.Run("from int8", func(t *testing.T) { + assertInt16OK(t, []caseInt16[int8]{ + {name: "zero", input: 0, want: 0}, + {name: "positive within range", input: 100, want: 100}, + {name: "negative within range", input: -100, want: -100}, + }) + }) + + t.Run("from int16", func(t *testing.T) { + assertInt16OK(t, []caseInt16[int16]{ + {name: "zero", input: 0, want: 0}, + {name: "positive within range", input: 10000, want: 10000}, + {name: "negative within range", input: -10000, want: -10000}, + }) + }) + + t.Run("from int32", func(t *testing.T) { + assertInt16OK(t, []caseInt16[int32]{ + {name: "zero", input: 0, want: 0}, + {name: "positive within range", input: 10000, want: 10000}, + {name: "negative within range", input: -10000, want: -10000}, + }) + + assertInt16Error(t, []caseInt16[int32]{ + {name: "positive out of range", input: 100000}, + {name: "negative out of range", input: -100000}, + }) + }) + + t.Run("from int64", func(t *testing.T) { + assertInt16OK(t, []caseInt16[int64]{ + {name: "zero", input: 0, want: 0}, + {name: "positive within range", input: 10000, want: 10000}, + {name: "negative within range", input: -10000, want: -10000}, + }) + + assertInt16Error(t, []caseInt16[int64]{ + {name: "positive out of range", input: 100000}, + {name: "negative out of range", input: -100000}, + }) + }) + + t.Run("from uint", func(t *testing.T) { + assertInt16OK(t, []caseInt16[uint]{ + {name: "zero", input: 0, want: 0}, + {name: "positive within range", input: 100, want: 100}, + }) + + assertInt16Error(t, []caseInt16[uint]{ + {name: "positive out of range", input: math.MaxInt16 + 1}, + }) + }) + + t.Run("from uint8", func(t *testing.T) { + assertInt16OK(t, []caseInt16[uint8]{ + {name: "zero", input: 0, want: 0}, + {name: "positive within range", input: 100, want: 100}, + }) + }) + + t.Run("from uint16", func(t *testing.T) { + assertInt16OK(t, []caseInt16[uint16]{ + {name: "zero", input: 0, want: 0}, + {name: "positive within range", input: 100, want: 100}, + }) + + assertInt16Error(t, []caseInt16[uint16]{ + {name: "positive out of range", input: math.MaxInt16 + 1}, + }) + }) + + t.Run("from uint32", func(t *testing.T) { + assertInt16OK(t, []caseInt16[uint32]{ + {name: "zero", input: 0, want: 0}, + {name: "positive within range", input: 100, want: 100}, + }) + + assertInt16Error(t, []caseInt16[uint32]{ + {name: "positive out of range", input: math.MaxInt16 + 1}, + }) + }) + + t.Run("from uint64", func(t *testing.T) { + assertInt16OK(t, []caseInt16[uint64]{ + {name: "zero", input: 0, want: 0}, + {name: "positive within range", input: 100, want: 100}, + }) + + assertInt16Error(t, []caseInt16[uint64]{ + {name: "positive out of range", input: math.MaxInt16 + 1}, + }) + }) +} + +type caseUint16[in safecast.Number] struct { + name string + input in + want uint16 +} + +func assertUint16OK[in safecast.Number](t *testing.T, tests []caseUint16[in]) { + t.Helper() + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := safecast.ToUint16(tt.input) + assertNoError(t, err) + assertEqual(t, tt.want, got) + }) + } +} + +func assertUint16Error[in safecast.Number](t *testing.T, tests []caseUint16[in]) { + t.Helper() + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Helper() + + got, err := safecast.ToUint16(tt.input) + requireError(t, err) + assertEqual(t, tt.want, got) + }) + } +} + +func TestToUint16(t *testing.T) { + t.Run("from int", func(t *testing.T) { + assertUint16OK(t, []caseUint16[int]{ + {name: "zero", input: 0, want: 0}, + {name: "positive within range", input: 10000, want: 10000}, + }) + + assertUint16Error(t, []caseUint16[int]{ + {name: "positive out of range", input: math.MaxUint16 + 1}, + {name: "negative value", input: -1}, + }) + }) + + t.Run("from int8", func(t *testing.T) { + assertUint16OK(t, []caseUint16[int8]{ + {name: "zero", input: 0, want: 0}, + {name: "positive within range", input: 100, want: 100}, + }) + }) + + t.Run("from int16", func(t *testing.T) { + assertUint16OK(t, []caseUint16[int16]{ + {name: "zero", input: 0, want: 0}, + {name: "positive within range", input: 10000, want: 10000}, + }) + }) + + t.Run("from int32", func(t *testing.T) { + assertUint16OK(t, []caseUint16[int32]{ + {name: "zero", input: 0, want: 0}, + {name: "positive within range", input: 10000, want: 10000}, + }) + + assertUint16Error(t, []caseUint16[int32]{ + {name: "positive out of range", input: 100000}, + {name: "negative value", input: -1}, + }) + }) + + t.Run("from int64", func(t *testing.T) { + assertUint16OK(t, []caseUint16[int64]{ + {name: "zero", input: 0, want: 0}, + {name: "positive within range", input: 10000, want: 10000}, + }) + + assertUint16Error(t, []caseUint16[int64]{ + {name: "positive out of range", input: 100000}, + {name: "negative value", input: -1}, + }) + }) + + t.Run("from uint", func(t *testing.T) { + assertUint16OK(t, []caseUint16[uint]{ + {name: "zero", input: 0, want: 0}, + {name: "positive within range", input: 100, want: 100}, + }) + + assertUint16Error(t, []caseUint16[uint]{ + {name: "positive out of range", input: math.MaxUint16 + 1}, + }) + }) + + t.Run("from uint8", func(t *testing.T) { + assertUint16OK(t, []caseUint16[uint8]{ + {name: "zero", input: 0, want: 0}, + {name: "positive within range", input: 100, want: 100}, + }) + }) + + t.Run("from uint16", func(t *testing.T) { + assertUint16OK(t, []caseUint16[uint16]{ + {name: "zero", input: 0, want: 0}, + {name: "positive within range", input: 100, want: 100}, + }) + }) + + t.Run("from uint32", func(t *testing.T) { + assertUint16OK(t, []caseUint16[uint32]{ + {name: "zero", input: 0, want: 0}, + {name: "positive within range", input: 100, want: 100}, + }) + + assertUint16Error(t, []caseUint16[uint32]{ + {name: "positive out of range", input: math.MaxUint16 + 1}, + }) + }) + + t.Run("from uint64", func(t *testing.T) { + assertUint16OK(t, []caseUint16[uint64]{ + {name: "zero", input: 0, want: 0}, + {name: "positive within range", input: 100, want: 100}, + }) + + assertUint16Error(t, []caseUint16[uint64]{ + {name: "positive out of range", input: math.MaxUint16 + 1}, + }) + }) +} + +type caseInt32[in safecast.Number] struct { + name string + input in + want int32 +} + +func assertInt32OK[in safecast.Number](t *testing.T, tests []caseInt32[in]) { + t.Helper() + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := safecast.ToInt32(tt.input) + assertNoError(t, err) + assertEqual(t, tt.want, got) + }) + } +} + +func assertInt32Error[in safecast.Number](t *testing.T, tests []caseInt32[in]) { + t.Helper() + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := safecast.ToInt32(tt.input) + requireError(t, err) + assertEqual(t, tt.want, got) + }) + } +} + +func TestToInt32(t *testing.T) { + t.Run("from int", func(t *testing.T) { + assertInt32OK(t, []caseInt32[int]{ + {name: "zero", input: 0, want: 0}, + {name: "positive within range", input: 10000, want: 10000}, + {name: "negative within range", input: -10000, want: -10000}, + }) + + assertInt32Error(t, []caseInt32[int]{ + {name: "positive out of range", input: math.MaxInt32 + 1}, + {name: "negative out of range", input: math.MinInt32 - 1}, + }) + }) + + t.Run("from int8", func(t *testing.T) { + assertInt32OK(t, []caseInt32[int8]{ + {name: "zero", input: 0, want: 0}, + {name: "positive within range", input: 100, want: 100}, + {name: "negative within range", input: -100, want: -100}, + }) + }) + + t.Run("from int16", func(t *testing.T) { + assertInt32OK(t, []caseInt32[int16]{ + {name: "zero", input: 0, want: 0}, + {name: "positive within range", input: 10000, want: 10000}, + {name: "negative within range", input: -10000, want: -10000}, + }) + }) + + t.Run("from int32", func(t *testing.T) { + assertInt32OK(t, []caseInt32[int32]{ + {name: "zero", input: 0, want: 0}, + {name: "positive within range", input: 10000, want: 10000}, + {name: "negative within range", input: -10000, want: -10000}, + }) + }) + + t.Run("from int64", func(t *testing.T) { + assertInt32OK(t, []caseInt32[int64]{ + {name: "zero", input: 0, want: 0}, + {name: "positive within range", input: 10000, want: 10000}, + {name: "negative within range", input: -10000, want: -10000}, + }) + + assertInt32Error(t, []caseInt32[int64]{ + {name: "positive out of range", input: math.MaxInt32 + 1}, + {name: "negative out of range", input: math.MinInt32 - 1}, + }) + }) + + t.Run("from uint", func(t *testing.T) { + assertInt32OK(t, []caseInt32[uint]{ + {name: "zero", input: 0, want: 0}, + {name: "positive within range", input: 100, want: 100}, + }) + + assertInt32Error(t, []caseInt32[uint]{ + {name: "positive out of range", input: math.MaxInt32 + 1}, + }) + }) + + t.Run("from uint8", func(t *testing.T) { + assertInt32OK(t, []caseInt32[uint8]{ + {name: "zero", input: 0, want: 0}, + {name: "positive within range", input: 100, want: 100}, + }) + }) + + t.Run("from uint16", func(t *testing.T) { + assertInt32OK(t, []caseInt32[uint16]{ + {name: "zero", input: 0, want: 0}, + {name: "positive within range", input: math.MaxUint16, want: math.MaxUint16}, + }) + }) + + t.Run("from uint32", func(t *testing.T) { + assertInt32OK(t, []caseInt32[uint32]{ + {name: "zero", input: 0, want: 0}, + {name: "positive within range", input: 100, want: 100}, + }) + + assertInt32Error(t, []caseInt32[uint32]{ + {name: "positive out of range", input: math.MaxInt32 + 1}, + }) + }) + + t.Run("from uint64", func(t *testing.T) { + assertInt32OK(t, []caseInt32[uint64]{ + {name: "zero", input: 0, want: 0}, + {name: "positive within range", input: 100, want: 100}, + }) + + assertInt32Error(t, []caseInt32[uint64]{ + {name: "positive out of range", input: math.MaxInt32 + 1}, + }) + }) +} + +type caseUint32[in safecast.Number] struct { + name string + input in + want uint32 +} + +func assertUint32OK[in safecast.Number](t *testing.T, tests []caseUint32[in]) { + t.Helper() + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := safecast.ToUint32(tt.input) + assertNoError(t, err) + assertEqual(t, tt.want, got) + }) + } +} + +func assertUint32Error[in safecast.Number](t *testing.T, tests []caseUint32[in]) { + t.Helper() + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Helper() + + got, err := safecast.ToUint32(tt.input) + requireError(t, err) + assertEqual(t, tt.want, got) + }) + } +} + +func TestToUint32(t *testing.T) { + t.Run("from int", func(t *testing.T) { + assertUint32OK(t, []caseUint32[int]{ + {name: "zero", input: 0, want: 0}, + {name: "positive within range", input: 10000, want: 10000}, + }) + + assertUint32Error(t, []caseUint32[int]{ + {name: "positive out of range", input: math.MaxUint32 + 1}, + {name: "negative value", input: -1}, + }) + }) + + t.Run("from int8", func(t *testing.T) { + assertUint32OK(t, []caseUint32[int8]{ + {name: "zero", input: 0, want: 0}, + {name: "positive within range", input: 100, want: 100}, + }) + + assertUint32Error(t, []caseUint32[int8]{ + {name: "negative value", input: -1}, + }) + }) + + t.Run("from int16", func(t *testing.T) { + assertUint32OK(t, []caseUint32[int16]{ + {name: "zero", input: 0, want: 0}, + {name: "positive within range", input: 10000, want: 10000}, + }) + + assertUint32Error(t, []caseUint32[int16]{ + {name: "negative value", input: -1}, + }) + }) + + t.Run("from int32", func(t *testing.T) { + assertUint32OK(t, []caseUint32[int32]{ + {name: "zero", input: 0, want: 0}, + {name: "positive within range", input: 10000, want: 10000}, + }) + + assertUint32Error(t, []caseUint32[int32]{ + {name: "negative value", input: -1}, + }) + }) + + t.Run("from int64", func(t *testing.T) { + assertUint32OK(t, []caseUint32[int64]{ + {name: "zero", input: 0, want: 0}, + {name: "positive within range", input: 10000, want: 10000}, + }) + + assertUint32Error(t, []caseUint32[int64]{ + {name: "positive out of range", input: math.MaxUint32 + 1}, + {name: "negative value", input: -1}, + }) + }) + + t.Run("from uint", func(t *testing.T) { + assertUint32OK(t, []caseUint32[uint]{ + {name: "zero", input: 0, want: 0}, + {name: "positive within range", input: 100, want: 100}, + }) + + assertUint32Error(t, []caseUint32[uint]{ + {name: "positive out of range", input: math.MaxUint32 + 1}, + }) + }) + + t.Run("from uint8", func(t *testing.T) { + assertUint32OK(t, []caseUint32[uint8]{ + {name: "zero", input: 0, want: 0}, + {name: "positive within range", input: 100, want: 100}, + }) + }) + + t.Run("from uint16", func(t *testing.T) { + assertUint32OK(t, []caseUint32[uint16]{ + {name: "zero", input: 0, want: 0}, + {name: "positive within range", input: 100, want: 100}, + }) + }) + + t.Run("from uint32", func(t *testing.T) { + assertUint32OK(t, []caseUint32[uint32]{ + {name: "zero", input: 0, want: 0}, + {name: "positive within range", input: 100, want: 100}, + }) + }) + + t.Run("from uint64", func(t *testing.T) { + assertUint32OK(t, []caseUint32[uint64]{ + {name: "zero", input: 0, want: 0}, + {name: "positive within range", input: 100, want: 100}, + }) + + assertUint32Error(t, []caseUint32[uint64]{ + {name: "positive out of range", input: math.MaxUint32 + 1}, + }) + }) +} + +type caseInt64[in safecast.Number] struct { + name string + input in + want int64 +} + +func assertInt64OK[in safecast.Number](t *testing.T, tests []caseInt64[in]) { + t.Helper() + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := safecast.ToInt64(tt.input) + assertNoError(t, err) + assertEqual(t, tt.want, got) + }) + } +} + +func assertInt64Error[in safecast.Number](t *testing.T, tests []caseInt64[in]) { + t.Helper() + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := safecast.ToInt64(tt.input) + requireError(t, err) + assertEqual(t, tt.want, got) + }) + } +} + +func TestToInt64(t *testing.T) { + t.Run("from int", func(t *testing.T) { + assertInt64OK(t, []caseInt64[int]{ + {name: "zero", input: 0, want: 0}, + {name: "positive within range", input: 10000, want: 10000}, + {name: "negative within range", input: -10000, want: -10000}, + }) + }) + + t.Run("from int8", func(t *testing.T) { + assertInt64OK(t, []caseInt64[int8]{ + {name: "zero", input: 0, want: 0}, + {name: "positive within range", input: 100, want: 100}, + {name: "negative within range", input: -100, want: -100}, + }) + }) + + t.Run("from int16", func(t *testing.T) { + assertInt64OK(t, []caseInt64[int16]{ + {name: "zero", input: 0, want: 0}, + {name: "positive within range", input: 10000, want: 10000}, + {name: "negative within range", input: -10000, want: -10000}, + }) + }) + + t.Run("from int32", func(t *testing.T) { + assertInt64OK(t, []caseInt64[int32]{ + {name: "zero", input: 0, want: 0}, + {name: "positive within range", input: 10000, want: 10000}, + {name: "negative within range", input: -10000, want: -10000}, + }) + }) + + t.Run("from int64", func(t *testing.T) { + assertInt64OK(t, []caseInt64[int64]{ + {name: "zero", input: 0, want: 0}, + {name: "positive within range", input: 10000, want: 10000}, + {name: "negative within range", input: -10000, want: -10000}, + }) + }) + + t.Run("from uint", func(t *testing.T) { + assertInt64OK(t, []caseInt64[uint]{ + {name: "zero", input: 0, want: 0}, + {name: "positive within range", input: 100, want: 100}, + }) + + assertInt64Error(t, []caseInt64[uint]{ + {name: "positive out of range", input: math.MaxInt64 + 1}, + }) + }) + + t.Run("from uint8", func(t *testing.T) { + assertInt64OK(t, []caseInt64[uint8]{ + {name: "zero", input: 0, want: 0}, + {name: "positive within range", input: 100, want: 100}, + }) + }) + + t.Run("from uint16", func(t *testing.T) { + assertInt64OK(t, []caseInt64[uint16]{ + {name: "zero", input: 0, want: 0}, + {name: "positive within range", input: 100, want: 100}, + }) + }) + + t.Run("from uint32", func(t *testing.T) { + assertInt64OK(t, []caseInt64[uint32]{ + {name: "zero", input: 0, want: 0}, + {name: "positive within range", input: 100, want: 100}, + }) + }) + + t.Run("from uint64", func(t *testing.T) { + assertInt64OK(t, []caseInt64[uint64]{ + {name: "zero", input: 0, want: 0}, + {name: "positive within range", input: 100, want: 100}, + }) + + assertInt64Error(t, []caseInt64[uint64]{ + {name: "positive out of range", input: math.MaxInt64 + 1}, + }) + }) +} + +type caseUint64[in safecast.Number] struct { + name string + input in + want uint64 +} + +func assertUint64OK[in safecast.Number](t *testing.T, tests []caseUint64[in]) { + t.Helper() + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := safecast.ToUint64(tt.input) + assertNoError(t, err) + assertEqual(t, tt.want, got) + }) + } +} + +func TestToUint64(t *testing.T) { + t.Run("from int", func(t *testing.T) { + assertUint64OK(t, []caseUint64[int]{ + {name: "zero", input: 0, want: 0}, + {name: "positive within range", input: 10000, want: 10000}, + }) + }) + + t.Run("from int8", func(t *testing.T) { + assertUint64OK(t, []caseUint64[int8]{ + {name: "zero", input: 0, want: 0}, + {name: "positive within range", input: 100, want: 100}, + }) + }) + + t.Run("from int16", func(t *testing.T) { + assertUint64OK(t, []caseUint64[int16]{ + {name: "zero", input: 0, want: 0}, + {name: "positive within range", input: 10000, want: 10000}, + }) + }) + + t.Run("from int32", func(t *testing.T) { + assertUint64OK(t, []caseUint64[int32]{ + {name: "zero", input: 0, want: 0}, + {name: "positive within range", input: 10000, want: 10000}, + }) + }) + + t.Run("from int64", func(t *testing.T) { + assertUint64OK(t, []caseUint64[int64]{ + {name: "zero", input: 0, want: 0}, + {name: "positive within range", input: 10000, want: 10000}, + }) + }) + + t.Run("from uint", func(t *testing.T) { + assertUint64OK(t, []caseUint64[uint]{ + {name: "zero", input: 0, want: 0}, + {name: "positive within range", input: 100, want: 100}, + }) + }) + + t.Run("from uint8", func(t *testing.T) { + assertUint64OK(t, []caseUint64[uint8]{ + {name: "zero", input: 0, want: 0}, + {name: "positive within range", input: 100, want: 100}, + }) + }) + + t.Run("from uint16", func(t *testing.T) { + assertUint64OK(t, []caseUint64[uint16]{ + {name: "zero", input: 0, want: 0}, + {name: "positive within range", input: 100, want: 100}, + }) + }) + + t.Run("from uint32", func(t *testing.T) { + assertUint64OK(t, []caseUint64[uint32]{ + {name: "zero", input: 0, want: 0}, + {name: "positive within range", input: 100, want: 100}, + }) + }) + + t.Run("from uint64", func(t *testing.T) { + assertUint64OK(t, []caseUint64[uint64]{ + {name: "zero", input: 0, want: 0}, + {name: "positive within range", input: 100, want: 100}, + }) + }) +} + +type caseInt[in safecast.Number] struct { + name string + input in + want int +} + +func assertIntOK[in safecast.Number](t *testing.T, tests []caseInt[in]) { + t.Helper() + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := safecast.ToInt(tt.input) + assertNoError(t, err) + assertEqual(t, tt.want, got) + }) + } +} + +func assertIntError[in safecast.Number](t *testing.T, tests []caseInt[in]) { + t.Helper() + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := safecast.ToInt(tt.input) + requireError(t, err) + assertEqual(t, tt.want, got) + }) + } +} + +func TestToInt(t *testing.T) { + t.Run("from int", func(t *testing.T) { + assertIntOK(t, []caseInt[int]{ + {name: "zero", input: 0, want: 0}, + {name: "positive within range", input: 10000, want: 10000}, + {name: "negative within range", input: -10000, want: -10000}, + }) + }) + + t.Run("from int8", func(t *testing.T) { + assertIntOK(t, []caseInt[int8]{ + {name: "zero", input: 0, want: 0}, + {name: "positive within range", input: 100, want: 100}, + {name: "negative within range", input: -100, want: -100}, + }) + }) + + t.Run("from int16", func(t *testing.T) { + assertIntOK(t, []caseInt[int16]{ + {name: "zero", input: 0, want: 0}, + {name: "positive within range", input: 10000, want: 10000}, + {name: "negative within range", input: -10000, want: -10000}, + }) + }) + + t.Run("from int32", func(t *testing.T) { + assertIntOK(t, []caseInt[int32]{ + {name: "zero", input: 0, want: 0}, + {name: "positive within range", input: 10000, want: 10000}, + {name: "negative within range", input: -10000, want: -10000}, + }) + }) + + t.Run("from int64", func(t *testing.T) { + assertIntOK(t, []caseInt[int64]{ + {name: "zero", input: 0, want: 0}, + {name: "positive within range", input: 10000, want: 10000}, + {name: "negative within range", input: -10000, want: -10000}, + }) + }) + + t.Run("from uint", func(t *testing.T) { + assertIntOK(t, []caseInt[uint]{ + {name: "zero", input: 0, want: 0}, + {name: "positive within range", input: 100, want: 100}, + }) + + assertIntError(t, []caseInt[uint]{ + {name: "positive out of range", input: math.MaxInt64 + 1}, + }) + }) + + t.Run("from uint8", func(t *testing.T) { + assertIntOK(t, []caseInt[uint8]{ + {name: "zero", input: 0, want: 0}, + {name: "positive within range", input: 100, want: 100}, + }) + }) + + t.Run("from uint16", func(t *testing.T) { + assertIntOK(t, []caseInt[uint16]{ + {name: "zero", input: 0, want: 0}, + {name: "positive within range", input: 100, want: 100}, + }) + }) + + t.Run("from uint32", func(t *testing.T) { + assertIntOK(t, []caseInt[uint32]{ + {name: "zero", input: 0, want: 0}, + {name: "positive within range", input: 100, want: 100}, + }) + }) + + t.Run("from uint64", func(t *testing.T) { + assertIntOK(t, []caseInt[uint64]{ + {name: "zero", input: 0, want: 0}, + {name: "positive within range", input: 100, want: 100}, + }) + + assertIntError(t, []caseInt[uint64]{ + {name: "positive out of range", input: math.MaxInt64 + 1}, + }) + }) +} + +type caseUint[in safecast.Number] struct { + name string + input in + want uint +} + +func assertUintOK[in safecast.Number](t *testing.T, tests []caseUint[in]) { + t.Helper() + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := safecast.ToUint(tt.input) + assertNoError(t, err) + assertEqual(t, tt.want, got) + }) + } +} + +func TestToUint(t *testing.T) { + t.Run("from int", func(t *testing.T) { + assertUintOK(t, []caseUint[int]{ + {name: "zero", input: 0, want: 0}, + {name: "positive within range", input: 10000, want: 10000}, + }) + }) + + t.Run("from int8", func(t *testing.T) { + assertUintOK(t, []caseUint[int8]{ + {name: "zero", input: 0, want: 0}, + {name: "positive within range", input: 100, want: 100}, + }) + }) + + t.Run("from int16", func(t *testing.T) { + assertUintOK(t, []caseUint[int16]{ + {name: "zero", input: 0, want: 0}, + {name: "positive within range", input: 10000, want: 10000}, + }) + }) + + t.Run("from int32", func(t *testing.T) { + assertUintOK(t, []caseUint[int32]{ + {name: "zero", input: 0, want: 0}, + {name: "positive within range", input: 10000, want: 10000}, + }) + }) + + t.Run("from int64", func(t *testing.T) { + assertUintOK(t, []caseUint[int64]{ + {name: "zero", input: 0, want: 0}, + {name: "positive within range", input: 10000, want: 10000}, + }) + }) + + t.Run("from uint", func(t *testing.T) { + assertUintOK(t, []caseUint[uint]{ + {name: "zero", input: 0, want: 0}, + {name: "positive within range", input: 100, want: 100}, + }) + }) + + t.Run("from uint8", func(t *testing.T) { + assertUintOK(t, []caseUint[uint8]{ + {name: "zero", input: 0, want: 0}, + {name: "positive within range", input: 100, want: 100}, + }) + }) + + t.Run("from uint16", func(t *testing.T) { + assertUintOK(t, []caseUint[uint16]{ + {name: "zero", input: 0, want: 0}, + {name: "positive within range", input: 100, want: 100}, + }) + }) + + t.Run("from uint32", func(t *testing.T) { + assertUintOK(t, []caseUint[uint32]{ + {name: "zero", input: 0, want: 0}, + {name: "positive within range", input: 100, want: 100}, + }) + }) + + t.Run("from uint64", func(t *testing.T) { + assertUintOK(t, []caseUint[uint64]{ + {name: "zero", input: 0, want: 0}, + {name: "positive within range", input: 100, want: 100}, + }) + }) + + type UintAlias uint + t.Run("from type alias", func(t *testing.T) { + assertUintOK(t, []caseUint[UintAlias]{ + {name: "zero", input: 0, want: 0}, + {name: "positive within range", input: 100, want: 100}, + }) + }) +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..9fc9821 --- /dev/null +++ b/go.mod @@ -0,0 +1,3 @@ +module github.com/ccoveille/go-safecast + +go 1.23.0 diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..e69de29