diff --git a/conversion.go b/conversion.go index 2e18861..6365d8c 100644 --- a/conversion.go +++ b/conversion.go @@ -16,8 +16,8 @@ import ( // - If the conversion is not possible for the desired type, an [ErrUnsupportedConversion] error is wrapped in the returned error. // - If the conversion fails from string, an [ErrStringConversion] error is wrapped in the returned error. // - If the conversion results in an error, an [ErrConversionIssue] error is wrapped in the returned error. -func Convert[NumOut Number](orig any) (converted NumOut, err error) { - switch v := orig.(type) { +func Convert[NumOut Number, NumIn Input](orig NumIn) (converted NumOut, err error) { + switch v := any(orig).(type) { case int: return convertFromNumber[NumOut](v) case uint: @@ -58,7 +58,7 @@ func Convert[NumOut Number](orig any) (converted NumOut, err error) { } // MustConvert calls [Convert] to convert the value to the desired type, and panics if the conversion fails. -func MustConvert[NumOut Number](orig any) NumOut { +func MustConvert[NumOut Number, NumIn Input](orig NumIn) NumOut { converted, err := Convert[NumOut](orig) if err != nil { panic(err) diff --git a/conversion_64bit_test.go b/conversion_64bit_test.go index 53892aa..a0d4674 100644 --- a/conversion_64bit_test.go +++ b/conversion_64bit_test.go @@ -61,7 +61,7 @@ func TestToInt_64bit(t *testing.T) { func TestConvert_64bit(t *testing.T) { t.Run("to uint32", func(t *testing.T) { for name, tt := range map[string]struct { - input any + input uint64 want uint32 }{ "positive out of range": {input: uint64(math.MaxUint32 + 1), want: 0}, diff --git a/conversion_test.go b/conversion_test.go index d80886e..1fb5fe8 100644 --- a/conversion_test.go +++ b/conversion_test.go @@ -9,7 +9,6 @@ package safecast_test import ( "errors" - "fmt" "math" "testing" @@ -1482,434 +1481,487 @@ func TestToFloat64(t *testing.T) { }) } -func TestConvert(t *testing.T) { - negativeZero := math.Copysign(0, -1) - - type helper func(input any) (any, error) - - convertUint := func(input any) (any, error) { - return safecast.Convert[uint](input) - } - - convertUint8 := func(input any) (any, error) { - return safecast.Convert[uint8](input) - } - - convertUint16 := func(input any) (any, error) { - return safecast.Convert[uint16](input) - } - - convertUint32 := func(input any) (any, error) { - return safecast.Convert[uint32](input) - } - - convertUint64 := func(input any) (any, error) { - return safecast.Convert[uint64](input) - } - - convertInt := func(input any) (any, error) { - return safecast.Convert[int](input) - } - - convertInt8 := func(input any) (any, error) { - return safecast.Convert[int8](input) +func Map[T any, U any](fn func(v T) U, input []T) []U { + var output []U + for _, v := range input { + output = append(output, fn(v)) } + return output +} - convertInt16 := func(input any) (any, error) { - return safecast.Convert[int16](input) - } +type MapTest[TypeInput safecast.Input, TypeOutput safecast.Number] struct { + Input TypeInput + ExpectedOutput TypeOutput + ExpectedError error +} - convertInt32 := func(input any) (any, error) { - return safecast.Convert[int32](input) - } +func (mt MapTest[I, O]) TestConvert(t *testing.T) { + // configure a helper to validate there is no panic + defer func(t *testing.T) { + t.Helper() - convertInt64 := func(input any) (any, error) { - return safecast.Convert[int64](input) - } + err := recover() + if err != nil { + t.Fatalf("panic with %v", err) + } + }(t) - convertFloat32 := func(input any) (any, error) { - return safecast.Convert[float32](input) + out, err := safecast.Convert[O](mt.Input) + if mt.ExpectedError != nil { + requireErrorIs(t, err, safecast.ErrConversionIssue) + requireErrorIs(t, err, mt.ExpectedError) + return } - convertFloat64 := func(input any) (any, error) { - return safecast.Convert[float64](input) - } + assertNoError(t, err) + assertEqual(t, mt.ExpectedOutput, out) +} - unsignedConverters := map[string]helper{ - "uint": convertUint, - "uint8": convertUint8, - "uint16": convertUint16, - "uint32": convertUint32, - "uint64": convertUint64, - } +type TestableConvert interface { + TestConvert(t *testing.T) +} - allConverters := map[string]helper{ - "uint": convertUint, - "uint8": convertUint8, - "uint16": convertUint16, - "uint32": convertUint32, - "uint64": convertUint64, - "int": convertInt, - "int8": convertInt8, - "int16": convertInt16, - "int32": convertInt32, - "int64": convertInt64, - "float32": convertFloat32, - "float64": convertFloat64, +func TestConvert(t *testing.T) { + t.Run("untyped integer", func(t *testing.T) { + out, err := safecast.Convert[uint](42) + assertNoError(t, err) + assertEqual(t, uint(42), out) + }) + + for name, c := range map[string]TestableConvert{ + "int to float32": MapTest[int, float32]{Input: 42, ExpectedOutput: 42}, + "int to float64": MapTest[int, float64]{Input: 42, ExpectedOutput: 42}, + "int to int": MapTest[int, int]{Input: 42, ExpectedOutput: 42}, + "int to int16": MapTest[int, int16]{Input: 42, ExpectedOutput: 42}, + "int to int32": MapTest[int, int32]{Input: 42, ExpectedOutput: 42}, + "int to int64": MapTest[int, int64]{Input: 42, ExpectedOutput: 42}, + "int to int8": MapTest[int, int8]{Input: 42, ExpectedOutput: 42}, + "int to uint": MapTest[int, uint]{Input: 42, ExpectedOutput: 42}, + "int to uint16": MapTest[int, uint16]{Input: 42, ExpectedOutput: 42}, + "int to uint32": MapTest[int, uint32]{Input: 42, ExpectedOutput: 42}, + "int to uint64": MapTest[int, uint64]{Input: 42, ExpectedOutput: 42}, + "int to uint8": MapTest[int, uint8]{Input: 42, ExpectedOutput: 42}, + "int8 to float32": MapTest[int8, float32]{Input: 42, ExpectedOutput: 42}, + "int8 to float64": MapTest[int8, float64]{Input: 42, ExpectedOutput: 42}, + "int8 to int": MapTest[int8, int]{Input: 42, ExpectedOutput: 42}, + "int8 to int16": MapTest[int8, int16]{Input: 42, ExpectedOutput: 42}, + "int8 to int32": MapTest[int8, int32]{Input: 42, ExpectedOutput: 42}, + "int8 to int64": MapTest[int8, int64]{Input: 42, ExpectedOutput: 42}, + "int8 to int8": MapTest[int8, int8]{Input: 42, ExpectedOutput: 42}, + "int8 to uint": MapTest[int8, uint]{Input: 42, ExpectedOutput: 42}, + "int8 to uint16": MapTest[int8, uint16]{Input: 42, ExpectedOutput: 42}, + "int8 to uint32": MapTest[int8, uint32]{Input: 42, ExpectedOutput: 42}, + "int8 to uint64": MapTest[int8, uint64]{Input: 42, ExpectedOutput: 42}, + "int8 to uint8": MapTest[int8, uint8]{Input: 42, ExpectedOutput: 42}, + "int16 to float32": MapTest[int16, float32]{Input: 42, ExpectedOutput: 42}, + "int16 to float64": MapTest[int16, float64]{Input: 42, ExpectedOutput: 42}, + "int16 to int": MapTest[int16, int]{Input: 42, ExpectedOutput: 42}, + "int16 to int16": MapTest[int16, int16]{Input: 42, ExpectedOutput: 42}, + "int16 to int32": MapTest[int16, int32]{Input: 42, ExpectedOutput: 42}, + "int16 to int64": MapTest[int16, int64]{Input: 42, ExpectedOutput: 42}, + "int16 to int8": MapTest[int16, int8]{Input: 42, ExpectedOutput: 42}, + "int16 to uint": MapTest[int16, uint]{Input: 42, ExpectedOutput: 42}, + "int16 to uint16": MapTest[int16, uint16]{Input: 42, ExpectedOutput: 42}, + "int16 to uint32": MapTest[int16, uint32]{Input: 42, ExpectedOutput: 42}, + "int16 to uint64": MapTest[int16, uint64]{Input: 42, ExpectedOutput: 42}, + "int16 to uint8": MapTest[int16, uint8]{Input: 42, ExpectedOutput: 42}, + "int32 to float32": MapTest[int32, float32]{Input: 42, ExpectedOutput: 42}, + "int32 to float64": MapTest[int32, float64]{Input: 42, ExpectedOutput: 42}, + "int32 to int": MapTest[int32, int]{Input: 42, ExpectedOutput: 42}, + "int32 to int16": MapTest[int32, int16]{Input: 42, ExpectedOutput: 42}, + "int32 to int32": MapTest[int32, int32]{Input: 42, ExpectedOutput: 42}, + "int32 to int64": MapTest[int32, int64]{Input: 42, ExpectedOutput: 42}, + "int32 to int8": MapTest[int32, int8]{Input: 42, ExpectedOutput: 42}, + "int32 to uint": MapTest[int32, uint]{Input: 42, ExpectedOutput: 42}, + "int32 to uint16": MapTest[int32, uint16]{Input: 42, ExpectedOutput: 42}, + "int32 to uint32": MapTest[int32, uint32]{Input: 42, ExpectedOutput: 42}, + "int32 to uint64": MapTest[int32, uint64]{Input: 42, ExpectedOutput: 42}, + "int32 to uint8": MapTest[int32, uint8]{Input: 42, ExpectedOutput: 42}, + "int64 to float32": MapTest[int64, float32]{Input: 42, ExpectedOutput: 42}, + "int64 to float64": MapTest[int64, float64]{Input: 42, ExpectedOutput: 42}, + "int64 to int": MapTest[int64, int]{Input: 42, ExpectedOutput: 42}, + "int64 to int16": MapTest[int64, int16]{Input: 42, ExpectedOutput: 42}, + "int64 to int32": MapTest[int64, int32]{Input: 42, ExpectedOutput: 42}, + "int64 to int64": MapTest[int64, int64]{Input: 42, ExpectedOutput: 42}, + "int64 to int8": MapTest[int64, int8]{Input: 42, ExpectedOutput: 42}, + "int64 to uint": MapTest[int64, uint]{Input: 42, ExpectedOutput: 42}, + "int64 to uint16": MapTest[int64, uint16]{Input: 42, ExpectedOutput: 42}, + "int64 to uint32": MapTest[int64, uint32]{Input: 42, ExpectedOutput: 42}, + "int64 to uint64": MapTest[int64, uint64]{Input: 42, ExpectedOutput: 42}, + "int64 to uint8": MapTest[int64, uint8]{Input: 42, ExpectedOutput: 42}, + "uint to float32": MapTest[uint, float32]{Input: 42, ExpectedOutput: 42}, + "uint to float64": MapTest[uint, float64]{Input: 42, ExpectedOutput: 42}, + "uint to int": MapTest[uint, int]{Input: 42, ExpectedOutput: 42}, + "uint to int16": MapTest[uint, int16]{Input: 42, ExpectedOutput: 42}, + "uint to int32": MapTest[uint, int32]{Input: 42, ExpectedOutput: 42}, + "uint to int64": MapTest[uint, int64]{Input: 42, ExpectedOutput: 42}, + "uint to int8": MapTest[uint, int8]{Input: 42, ExpectedOutput: 42}, + "uint to uint": MapTest[uint, uint]{Input: 42, ExpectedOutput: 42}, + "uint to uint16": MapTest[uint, uint16]{Input: 42, ExpectedOutput: 42}, + "uint to uint32": MapTest[uint, uint32]{Input: 42, ExpectedOutput: 42}, + "uint to uint64": MapTest[uint, uint64]{Input: 42, ExpectedOutput: 42}, + "uint to uint8": MapTest[uint, uint8]{Input: 42, ExpectedOutput: 42}, + "uint8 to float32": MapTest[uint8, float32]{Input: 42, ExpectedOutput: 42}, + "uint8 to float64": MapTest[uint8, float64]{Input: 42, ExpectedOutput: 42}, + "uint8 to int": MapTest[uint8, int]{Input: 42, ExpectedOutput: 42}, + "uint8 to int16": MapTest[uint8, int16]{Input: 42, ExpectedOutput: 42}, + "uint8 to int32": MapTest[uint8, int32]{Input: 42, ExpectedOutput: 42}, + "uint8 to int64": MapTest[uint8, int64]{Input: 42, ExpectedOutput: 42}, + "uint8 to int8": MapTest[uint8, int8]{Input: 42, ExpectedOutput: 42}, + "uint8 to uint": MapTest[uint8, uint]{Input: 42, ExpectedOutput: 42}, + "uint8 to uint16": MapTest[uint8, uint16]{Input: 42, ExpectedOutput: 42}, + "uint8 to uint32": MapTest[uint8, uint32]{Input: 42, ExpectedOutput: 42}, + "uint8 to uint64": MapTest[uint8, uint64]{Input: 42, ExpectedOutput: 42}, + "uint8 to uint8": MapTest[uint8, uint8]{Input: 42, ExpectedOutput: 42}, + "uint16 to float32": MapTest[uint16, float32]{Input: 42, ExpectedOutput: 42}, + "uint16 to float64": MapTest[uint16, float64]{Input: 42, ExpectedOutput: 42}, + "uint16 to int": MapTest[uint16, int]{Input: 42, ExpectedOutput: 42}, + "uint16 to int16": MapTest[uint16, int16]{Input: 42, ExpectedOutput: 42}, + "uint16 to int32": MapTest[uint16, int32]{Input: 42, ExpectedOutput: 42}, + "uint16 to int64": MapTest[uint16, int64]{Input: 42, ExpectedOutput: 42}, + "uint16 to int8": MapTest[uint16, int8]{Input: 42, ExpectedOutput: 42}, + "uint16 to uint": MapTest[uint16, uint]{Input: 42, ExpectedOutput: 42}, + "uint16 to uint16": MapTest[uint16, uint16]{Input: 42, ExpectedOutput: 42}, + "uint16 to uint32": MapTest[uint16, uint32]{Input: 42, ExpectedOutput: 42}, + "uint16 to uint64": MapTest[uint16, uint64]{Input: 42, ExpectedOutput: 42}, + "uint16 to uint8": MapTest[uint16, uint8]{Input: 42, ExpectedOutput: 42}, + "uint32 to float32": MapTest[uint32, float32]{Input: 42, ExpectedOutput: 42}, + "uint32 to float64": MapTest[uint32, float64]{Input: 42, ExpectedOutput: 42}, + "uint32 to int": MapTest[uint32, int]{Input: 42, ExpectedOutput: 42}, + "uint32 to int16": MapTest[uint32, int16]{Input: 42, ExpectedOutput: 42}, + "uint32 to int32": MapTest[uint32, int32]{Input: 42, ExpectedOutput: 42}, + "uint32 to int64": MapTest[uint32, int64]{Input: 42, ExpectedOutput: 42}, + "uint32 to int8": MapTest[uint32, int8]{Input: 42, ExpectedOutput: 42}, + "uint32 to uint": MapTest[uint32, uint]{Input: 42, ExpectedOutput: 42}, + "uint32 to uint16": MapTest[uint32, uint16]{Input: 42, ExpectedOutput: 42}, + "uint32 to uint32": MapTest[uint32, uint32]{Input: 42, ExpectedOutput: 42}, + "uint32 to uint64": MapTest[uint32, uint64]{Input: 42, ExpectedOutput: 42}, + "uint32 to uint8": MapTest[uint32, uint8]{Input: 42, ExpectedOutput: 42}, + "uint64 to float32": MapTest[uint64, float32]{Input: 42, ExpectedOutput: 42}, + "uint64 to float64": MapTest[uint64, float64]{Input: 42, ExpectedOutput: 42}, + "uint64 to int": MapTest[uint64, int]{Input: 42, ExpectedOutput: 42}, + "uint64 to int16": MapTest[uint64, int16]{Input: 42, ExpectedOutput: 42}, + "uint64 to int32": MapTest[uint64, int32]{Input: 42, ExpectedOutput: 42}, + "uint64 to int64": MapTest[uint64, int64]{Input: 42, ExpectedOutput: 42}, + "uint64 to int8": MapTest[uint64, int8]{Input: 42, ExpectedOutput: 42}, + "uint64 to uint": MapTest[uint64, uint]{Input: 42, ExpectedOutput: 42}, + "uint64 to uint16": MapTest[uint64, uint16]{Input: 42, ExpectedOutput: 42}, + "uint64 to uint32": MapTest[uint64, uint32]{Input: 42, ExpectedOutput: 42}, + "uint64 to uint64": MapTest[uint64, uint64]{Input: 42, ExpectedOutput: 42}, + "uint64 to uint8": MapTest[uint64, uint8]{Input: 42, ExpectedOutput: 42}, + "float32 to int": MapTest[float32, int]{Input: 42, ExpectedOutput: 42}, + "float32 to int8": MapTest[float32, int8]{Input: 42, ExpectedOutput: 42}, + "float32 to int16": MapTest[float32, int16]{Input: 42, ExpectedOutput: 42}, + "float32 to int32": MapTest[float32, int32]{Input: 42, ExpectedOutput: 42}, + "float32 to int64": MapTest[float32, int64]{Input: 42, ExpectedOutput: 42}, + "float32 to uint": MapTest[float32, uint]{Input: 42, ExpectedOutput: 42}, + "float32 to uint8": MapTest[float32, uint8]{Input: 42, ExpectedOutput: 42}, + "float32 to uint16": MapTest[float32, uint16]{Input: 42, ExpectedOutput: 42}, + "float32 to uint32": MapTest[float32, uint32]{Input: 42, ExpectedOutput: 42}, + "float32 to uint64": MapTest[float32, uint64]{Input: 42, ExpectedOutput: 42}, + "float32 to float32": MapTest[float32, float32]{Input: 42, ExpectedOutput: 42}, + "float32 to float64": MapTest[float32, float64]{Input: 42, ExpectedOutput: 42}, + "float64 to int": MapTest[float64, int]{Input: 42, ExpectedOutput: 42}, + "float64 to int8": MapTest[float64, int8]{Input: 42, ExpectedOutput: 42}, + "float64 to int16": MapTest[float64, int16]{Input: 42, ExpectedOutput: 42}, + "float64 to int32": MapTest[float64, int32]{Input: 42, ExpectedOutput: 42}, + "float64 to int64": MapTest[float64, int64]{Input: 42, ExpectedOutput: 42}, + "float64 to uint": MapTest[float64, uint]{Input: 42, ExpectedOutput: 42}, + "float64 to uint8": MapTest[float64, uint8]{Input: 42, ExpectedOutput: 42}, + "float64 to uint16": MapTest[float64, uint16]{Input: 42, ExpectedOutput: 42}, + "float64 to uint32": MapTest[float64, uint32]{Input: 42, ExpectedOutput: 42}, + "float64 to uint64": MapTest[float64, uint64]{Input: 42, ExpectedOutput: 42}, + "float64 to float32": MapTest[float64, float32]{Input: 42, ExpectedOutput: 42}, + "float64 to float64": MapTest[float64, float64]{Input: 42, ExpectedOutput: 42}, + } { + t.Run(name, func(t *testing.T) { + c.TestConvert(t) + }) } - for name, converter := range allConverters { - t.Run(fmt.Sprintf("convert to %s", name), func(t *testing.T) { - for name, tt := range map[string]struct { - input any - want any - }{ - "untyped int zero": {input: 0, want: 0}, - - "positive untyped int within range": {input: 42, want: 42}, - - "int zero": {input: int(0), want: 0}, - "positive int within range": {input: int(42), want: 42}, - - "int8 zero": {input: int8(0), want: 0}, - "positive int8 within range": {input: int8(42), want: 42}, - - "int16 zero": {input: int16(0), want: 0}, - "positive int16 within range": {input: int16(42), want: 42}, - - "int32 zero": {input: int32(0), want: 0}, - "positive int32 within range": {input: int32(42), want: 42}, - - "int64 zero": {input: int64(0), want: 0}, - "positive int64 within range": {input: int64(42), want: 42}, - - "uint zero": {input: uint(0), want: 0}, - "uint within range": {input: uint(42), want: 42}, - - "uint8 zero": {input: uint8(0), want: 0}, - "uint8 within range": {input: uint8(42), want: 42}, - - "uint16 zero": {input: uint16(0), want: 0}, - "uint16 within range": {input: uint16(42), want: 42}, - - "uint32 zero": {input: uint32(0), want: 0}, - "uint32 within range": {input: uint32(42), want: 42}, - - "uint64 zero": {input: uint64(0), want: 0}, - "uint64 within range": {input: uint64(42), want: 42}, - - "float32 zero": {input: float32(0), want: 0}, - "positive float32 within range": {input: float32(42.0), want: 42.0}, - - "float64 zero": {input: float64(0), want: 0}, - "positive float64 within range": {input: float64(42.0), want: 42.0}, - - "string integer": {input: "42", want: 42}, - "string with spaces": {input: "42 ", want: 42}, - "string float": {input: "42.0", want: 42}, - "string true": {input: "true", want: 1}, - "string false": {input: "false", want: 0}, - "string 10_0": {input: "10_0", want: 100}, - "string binary": {input: "0b101010", want: 42}, - "string short octal notation": {input: "042", want: 34}, - "string octal": {input: "0o42", want: 34}, - "string hexadecimal": {input: "0x42", want: 66}, - - "boolean true": {input: true, want: 1}, - "boolean false": {input: false, want: 0}, - } { - t.Run(fmt.Sprintf("from %s", name), func(t *testing.T) { - got, err := converter(tt.input) - assertNoError(t, err) - - if fmt.Sprint(got) != fmt.Sprint(tt.want) { - t.Fatalf("unexpected result %+v != %+v", tt.want, got) - } - }) - } - - for name, tt := range map[string]struct { - input any - errExpected error - }{ - "nil": { - input: nil, - errExpected: safecast.ErrUnsupportedConversion, - }, - "unexpected type": { - input: struct{}{}, - errExpected: safecast.ErrUnsupportedConversion, - }, - "empty string": { - input: "", - errExpected: safecast.ErrStringConversion, - }, - "simple space": { - input: " ", - errExpected: safecast.ErrStringConversion, - }, - "simple dot": { - input: ".", - errExpected: safecast.ErrStringConversion, - }, - "simple dash": { - input: "-", - errExpected: safecast.ErrStringConversion, - }, - "invalid string": { - input: "abc", - errExpected: safecast.ErrStringConversion, - }, - "invalid string with dot": { - input: "ab.c", - errExpected: safecast.ErrStringConversion, - }, - "string with leading +": { - input: "+42", - errExpected: safecast.ErrStringConversion, - }, - "invalid string multiple leading dashes": { - input: "--42", - errExpected: safecast.ErrStringConversion, - }, - "invalid string with leading dash": { - input: "-abc", - errExpected: safecast.ErrStringConversion, - }, - "invalid string with leading dash and dot": { - input: "-ab.c", - errExpected: safecast.ErrStringConversion, - }, - } { - t.Run(name, func(t *testing.T) { - _, err := converter(tt.input) - requireErrorIs(t, err, safecast.ErrConversionIssue) - requireErrorIs(t, err, tt.errExpected) - }) - } + for name, c := range map[string]TestableConvert{ + "string integer": MapTest[string, uint]{Input: "42", ExpectedOutput: 42}, + "string with spaces": MapTest[string, uint]{Input: "42 ", ExpectedOutput: 42}, + "string float": MapTest[string, uint]{Input: "42.0", ExpectedOutput: 42}, + "string true": MapTest[string, uint]{Input: "true", ExpectedOutput: 1}, + "string false": MapTest[string, uint]{Input: "false", ExpectedOutput: 0}, + "string 10_0": MapTest[string, uint]{Input: "10_0", ExpectedOutput: 100}, + "string binary": MapTest[string, uint]{Input: "0b101010", ExpectedOutput: 42}, + "string short octal notation": MapTest[string, uint]{Input: "042", ExpectedOutput: 34}, + "string octal": MapTest[string, uint]{Input: "0o42", ExpectedOutput: 34}, + "string hexadecimal": MapTest[string, uint]{Input: "0x42", ExpectedOutput: 66}, + "boolean true": MapTest[bool, uint]{Input: true, ExpectedOutput: 1}, + "boolean false": MapTest[bool, uint]{Input: false, ExpectedOutput: 0}, + + "empty string": MapTest[string, uint]{Input: "", ExpectedError: safecast.ErrStringConversion}, + "simple space": MapTest[string, uint]{Input: " ", ExpectedError: safecast.ErrStringConversion}, + "simple dot": MapTest[string, uint]{Input: ".", ExpectedError: safecast.ErrStringConversion}, + "simple dash": MapTest[string, uint]{Input: "-", ExpectedError: safecast.ErrStringConversion}, + "invalid string": MapTest[string, uint]{Input: "abc", ExpectedError: safecast.ErrStringConversion}, + "invalid string with dot": MapTest[string, uint]{Input: "ab.c", ExpectedError: safecast.ErrStringConversion}, + "strings with leading +": MapTest[string, uint]{Input: "+42", ExpectedError: safecast.ErrStringConversion}, + "invalid string multiple leading dashes": MapTest[string, uint]{Input: "--42", ExpectedError: safecast.ErrStringConversion}, + "invalid string with dash": MapTest[string, uint]{Input: "-abc", ExpectedError: safecast.ErrStringConversion}, + "invalid string with dash and dot": MapTest[string, uint]{Input: "-ab.c", ExpectedError: safecast.ErrStringConversion}, + } { + t.Run(name, func(t *testing.T) { + c.TestConvert(t) }) } + negativeZero := math.Copysign(0, -1) t.Run("convert to float32 near zero", func(t *testing.T) { - for name, tt := range map[string]struct { - input any - want float32 - }{ - "negative untyped zero": {input: negativeZero, want: float32(negativeZero)}, - "smallest positive non-zero float32": {input: math.SmallestNonzeroFloat32, want: 1e-45}, - "smallest negative non-zero float32": {input: -math.SmallestNonzeroFloat32, want: -1e-45}, - "smallest positive non-zero float64": {input: math.SmallestNonzeroFloat64, want: 4.9e-324}, - "smallest negative non-zero float64": {input: -math.SmallestNonzeroFloat64, want: -4.9e-324}, + for name, tt := range map[string]TestableConvert{ + "negative untyped zero": MapTest[float64, float32]{Input: negativeZero, ExpectedOutput: float32(negativeZero)}, + "smallest positive non-zero float32": MapTest[float64, float32]{Input: math.SmallestNonzeroFloat32, ExpectedOutput: 1e-45}, + "smallest negative non-zero float32": MapTest[float64, float32]{Input: -math.SmallestNonzeroFloat32, ExpectedOutput: -1e-45}, + "smallest positive non-zero float64": MapTest[float64, float32]{Input: math.SmallestNonzeroFloat64, ExpectedOutput: 4.9e-324}, + "smallest negative non-zero float64": MapTest[float64, float32]{Input: -math.SmallestNonzeroFloat64, ExpectedOutput: -4.9e-324}, } { - t.Run(fmt.Sprintf("from %s", name), func(t *testing.T) { - got, err := convertFloat32(tt.input) - assertNoError(t, err) - - if got != tt.want { - t.Fatalf("unexpected result want:%+v got:%+v", tt.want, got) - } + t.Run(name, func(t *testing.T) { + tt.TestConvert(t) }) } }) t.Run("convert to float64 near zero", func(t *testing.T) { - for name, tt := range map[string]struct { - input any - want float64 - }{ - "negative untyped zero": {input: negativeZero, want: negativeZero}, - "smallest positive non-zero float32": {input: math.SmallestNonzeroFloat32, want: 1.401298464324817e-45}, - "smallest negative non-zero float32": {input: -math.SmallestNonzeroFloat32, want: -1.401298464324817e-45}, - "smallest positive non-zero float64": {input: math.SmallestNonzeroFloat64, want: 4.9e-324}, - "smallest negative non-zero float64": {input: -math.SmallestNonzeroFloat64, want: -4.9e-324}, + for name, tt := range map[string]TestableConvert{ + "negative untyped zero": MapTest[float64, float64]{Input: negativeZero, ExpectedOutput: negativeZero}, + "smallest positive non-zero float32": MapTest[float64, float64]{Input: math.SmallestNonzeroFloat32, ExpectedOutput: 1.401298464324817e-45}, + "smallest negative non-zero float32": MapTest[float64, float64]{Input: -math.SmallestNonzeroFloat32, ExpectedOutput: -1.401298464324817e-45}, + "smallest positive non-zero float64": MapTest[float64, float64]{Input: math.SmallestNonzeroFloat64, ExpectedOutput: 4.9e-324}, + "smallest negative non-zero float64": MapTest[float64, float64]{Input: -math.SmallestNonzeroFloat64, ExpectedOutput: -4.9e-324}, } { - t.Run(fmt.Sprintf("from %s", name), func(t *testing.T) { - got, err := convertFloat64(tt.input) - assertNoError(t, err) - - if got != tt.want { - t.Fatalf("unexpected result want:%+v got:%+v", tt.want, got) - } + t.Run(name, func(t *testing.T) { + tt.TestConvert(t) }) } }) - t.Run("upper bound overflows", func(t *testing.T) { - for name, tt := range map[string]struct { - converter helper - value any - }{ - "int": { - converter: convertInt, - value: uint(math.MaxInt + 1), - }, - "int8": { - converter: convertInt8, - value: math.MaxInt8 + 1, - }, - "int16": { - converter: convertInt16, - value: math.MaxInt16 + 1, - }, - "int32": { - converter: convertInt32, - value: uint(math.MaxInt32 + 1), - }, - "int64": { - converter: convertInt64, - value: float64(math.MaxInt64 + 1), // the float64 conversion is used to avoid overflow on 32-bit - }, - "uint": { - converter: convertUint, - value: float64(math.MaxUint * 1.01), - }, - "uint8": { - converter: convertUint8, - value: math.MaxUint8 + 1, - }, - "uint16": { - converter: convertUint16, - value: math.MaxUint16 + 1, - }, - "uint32": { - converter: convertUint32, - value: float64(math.MaxUint32 * 1.01), // the float64 conversion is used to avoid overflow on 32-bit - }, - "uint64": { - converter: convertUint64, - value: float64(math.MaxUint64 * 1.01), - }, - "float32": { - converter: convertFloat32, - value: math.MaxFloat32 * 1.01, - }, - "int string": { - converter: convertInt, - value: "9223372036854775808", // math.MaxInt64 + 1 - }, - "int8 string": { - converter: convertInt8, - value: "129", // math.MaxInt8 + 1 - }, - "int16 string": { - converter: convertInt16, - value: "32769", // math.MaxInt16 + 1 - }, - "int32 string": { - converter: convertInt32, - value: "2147483648", // math.MaxInt32 + 1 - }, - "int64 string": { - converter: convertInt64, - value: "9223372036854775808", // math.MaxInt64 + 1 - }, - - "int64 string overflow": { - converter: convertInt64, - value: "123456789012345678901234567890", // more string than math.MaxInt64 represented as string - }, - } { - t.Run("for "+name, func(t *testing.T) { - _, err := tt.converter(tt.value) - requireErrorIs(t, err, safecast.ErrConversionIssue) - requireErrorIs(t, err, safecast.ErrExceedMaximumValue) - }) + for name, c := range map[string]TestableConvert{ + "upper bound overflows for int": MapTest[uint, int]{ + Input: uint(math.MaxInt + 1), + ExpectedError: safecast.ErrExceedMaximumValue, + }, + "upper bound overflows for int8": MapTest[uint, int8]{ + Input: uint(math.MaxInt8 + 1), + ExpectedError: safecast.ErrExceedMaximumValue, + }, + "upper bound overflows for int16": MapTest[uint, int16]{ + Input: uint(math.MaxInt16 + 1), + ExpectedError: safecast.ErrExceedMaximumValue, + }, + "upper bound overflows for int32": MapTest[uint, int32]{ + Input: uint(math.MaxInt32 + 1), + ExpectedError: safecast.ErrExceedMaximumValue, + }, + "upper bound overflows for int64": MapTest[float64, int64]{ + Input: float64(math.MaxInt64 * 1.01), // using float64 here avoid issue when testing on 32-bit systems + ExpectedError: safecast.ErrExceedMaximumValue, + }, + "upper bound overflows for uint": MapTest[float64, uint]{ + Input: float64(math.MaxUint * 1.01), // using float64 here avoid issue when testing on 32-bit systems + ExpectedError: safecast.ErrExceedMaximumValue, + }, + "upper bound overflows for uint8": MapTest[uint, uint8]{ + Input: uint(math.MaxUint8 + 1), + ExpectedError: safecast.ErrExceedMaximumValue, + }, + "upper bound overflows for uint16": MapTest[uint, uint16]{ + Input: uint(math.MaxUint16 + 1), + ExpectedError: safecast.ErrExceedMaximumValue, + }, + "upper bound overflows for uint32": MapTest[float64, uint32]{ + Input: float64(math.MaxUint32 * 1.01), + ExpectedError: safecast.ErrExceedMaximumValue, + }, + "upper bound overflows for uint64": MapTest[float64, uint64]{ + Input: float64(math.MaxUint64 * 1.01), + ExpectedError: safecast.ErrExceedMaximumValue, + }, + + "upper bound overflows for int string": MapTest[string, int]{ + Input: "9223372036854775808", // math.MaxInt64 + 1 + ExpectedError: safecast.ErrExceedMaximumValue, + }, + "upper bound overflows for int8 string": MapTest[string, int8]{ + Input: "129", // math.MaxInt8 + 1 + ExpectedError: safecast.ErrExceedMaximumValue, + }, + "upper bound overflows for int16 string": MapTest[string, int16]{ + Input: "32769", // math.MaxInt16 + 1 + ExpectedError: safecast.ErrExceedMaximumValue, + }, + "upper bound overflows for int32 string": MapTest[string, int32]{ + Input: "2147483648", // math.MaxInt32 + 1 + ExpectedError: safecast.ErrExceedMaximumValue, + }, + "upper bound overflows for int64 string": MapTest[string, int64]{ + Input: "9223372036854775808", // math.MaxInt64 + 1 + ExpectedError: safecast.ErrExceedMaximumValue, + }, + + "upper bound overflows for int64 string overflow": MapTest[string, int64]{ + Input: "123456789012345678901234567890", // more characters than math.MaxInt64 represented as string + ExpectedError: safecast.ErrExceedMaximumValue, + }, + } { + t.Run(name, func(t *testing.T) { + c.TestConvert(t) + }) + } + + for name, c := range map[string]TestableConvert{ + "lower bound overflows for int": MapTest[float64, int]{ + Input: float64(math.MinInt * 1.01), // the float64 conversion is used to avoid overflow on 32-bit + ExpectedError: safecast.ErrExceedMinimumValue, + }, + "lower bound overflows for int8": MapTest[int, int8]{ + Input: math.MinInt8 - 1, + ExpectedError: safecast.ErrExceedMinimumValue, + }, + "lower bound overflows for int16": MapTest[int, int16]{ + Input: math.MinInt16 - 1, + ExpectedError: safecast.ErrExceedMinimumValue, + }, + "lower bound overflows for int32": MapTest[float64, int32]{ + Input: float64(math.MinInt32 - 1), // the float64 conversion is used to avoid overflow on 32-bit, + ExpectedError: safecast.ErrExceedMinimumValue, + }, + "lower bound overflows for int64": MapTest[float64, int64]{ + Input: float64(math.MinInt64 * 1.01), // the float64 conversion is used to avoid overflow on 32-bit + ExpectedError: safecast.ErrExceedMinimumValue, + }, + + "lower bound overflows for float32": MapTest[float64, float32]{ + Input: -float64(math.MaxFloat32 * 1.01), + ExpectedError: safecast.ErrExceedMinimumValue, + }, + + // Note: float64 cannot overflow + + "negative overflows uint": MapTest[int, uint]{ + Input: -42, + ExpectedError: safecast.ErrExceedMinimumValue, + }, + + "negative overflows uint8": MapTest[int, uint8]{ + Input: -42, + ExpectedError: safecast.ErrExceedMinimumValue, + }, + + "negative overflows uint16": MapTest[int, uint16]{ + Input: -42, + ExpectedError: safecast.ErrExceedMinimumValue, + }, + + "negative overflows uint32": MapTest[int, uint32]{ + Input: -42, + ExpectedError: safecast.ErrExceedMinimumValue, + }, + + "negative overflows uint64": MapTest[int, uint64]{ + Input: -42, + ExpectedError: safecast.ErrExceedMinimumValue, + }, + + "lower bound overflows int from string": MapTest[string, int]{ + Input: "-9223372036854775809", // math.MinInt64 - 1 + ExpectedError: safecast.ErrExceedMinimumValue, + }, + + "lower bound overflows int8 from string": MapTest[string, int8]{ + Input: "-129", // math.MinInt8 - 1 + ExpectedError: safecast.ErrExceedMinimumValue, + }, + + "lower bound overflows int16 from string": MapTest[string, int16]{ + Input: "-32769", // math.MinInt16 - 1 + ExpectedError: safecast.ErrExceedMinimumValue, + }, + + "lower bound overflows int32 from string": MapTest[string, int32]{ + Input: "-2147483649", // math.MinInt32 - 1 + ExpectedError: safecast.ErrExceedMinimumValue, + }, + + "lower bound overflows int64 from string": MapTest[string, int64]{ + Input: "-9223372036854775809", // math.MinInt64 - 1 + ExpectedError: safecast.ErrExceedMinimumValue, + }, + + "lower bound overflows int64 from string overflow": MapTest[string, int64]{ + Input: "-123456789012345678901234567890", // more characters than math.MinInt64 represented as string + ExpectedError: safecast.ErrExceedMinimumValue, + }, + + "negative string overflows uint": MapTest[string, uint]{ + Input: "-1", + ExpectedError: safecast.ErrExceedMinimumValue, + }, + + "negative string overflows uint8": MapTest[string, uint8]{ + Input: "-1", + ExpectedError: safecast.ErrExceedMinimumValue, + }, + + "negative string overflows uint16": MapTest[string, uint16]{ + Input: "-1", + ExpectedError: safecast.ErrExceedMinimumValue, + }, + + "negative string overflows uint32": MapTest[string, uint32]{ + Input: "-1", + ExpectedError: safecast.ErrExceedMinimumValue, + }, + + "negative string overflows uint64": MapTest[string, uint64]{ + Input: "-1", + ExpectedError: safecast.ErrExceedMinimumValue, + }, + } { + t.Run(name, func(t *testing.T) { + c.TestConvert(t) + }) + } +} + +type MapMustConvertTest[TypeInput safecast.Input, TypeOutput safecast.Number] struct { + Input TypeInput + ExpectedOutput TypeOutput + ExpectedError error +} + +func (mt MapMustConvertTest[I, O]) TestConvert(t *testing.T) { + defer func(t *testing.T) { + t.Helper() + + r := recover() + + if mt.ExpectedError == nil && r == nil { + return } - }) - t.Run("lower bound overflows", func(t *testing.T) { - for name, tt := range map[string]struct { - converter helper - value any - }{ - "int": { - converter: convertInt, - value: float32(math.MinInt * 1.01), - }, - "int8": { - converter: convertInt8, - value: math.MinInt8 - 1, - }, - "int16": { - converter: convertInt16, - value: math.MinInt16 - 1, - }, - "int32": { - converter: convertInt32, - value: float64(math.MinInt32 - 1), // the float64 conversion is used to avoid overflow on 32-bit - }, - "int64": { - converter: convertInt64, - value: float32(math.MinInt64 * 1.01), - }, - "float32": { - converter: convertFloat32, - value: -float64(math.MaxFloat32 * 1.01), - }, - - // Note: float64 cannot overflow - - "int from string": { - converter: convertInt, - value: "-9223372036854775809", // math.MinInt64 - 1 - }, - - "int8 from string": { - converter: convertInt8, - value: "-129", // math.MinInt8 - 1 - }, - - "int16 from string": { - converter: convertInt16, - value: "-32769", // math.MinInt16 - 1 - }, - - "int32 from string": { - converter: convertInt32, - value: "-2147483649", // math.MinInt32 - 1 - }, - - "int64 from string": { - converter: convertInt64, - value: "-9223372036854775809", // math.MinInt64 - 1 - }, - - "int64 string overflow": { - converter: convertInt64, - value: "-123456789012345678901234567890", // more characters than math.MinInt64 represented as string - }, - } { - t.Run("for "+name, func(t *testing.T) { - _, err := tt.converter(tt.value) - requireErrorIs(t, err, safecast.ErrConversionIssue) - requireErrorIs(t, err, safecast.ErrExceedMinimumValue) - }) + if r == nil { + t.Fatal("did not panic") } - for name, converter := range unsignedConverters { - t.Run(fmt.Sprintf("for %s", name), func(t *testing.T) { - for name, input := range map[string]any{ - "untyped int within range": -42, - "int": int(-42), - "int8": int8(-42), - "int16": int16(-42), - "int32": int32(-42), - "int64": int64(-42), - "float32": float32(-42), - "float64": float64(-42), - - "negative string": "-42", - "negative string with dot": "-42.0", - } { - t.Run(fmt.Sprintf("from %s", name), func(t *testing.T) { - _, err := converter(input) - requireErrorIs(t, err, safecast.ErrConversionIssue) - requireErrorIs(t, err, safecast.ErrExceedMinimumValue) - }) - } - }) + err, ok := r.(error) + if !ok { + t.Fatalf("panic value is not an error: %v", r) } - }) + + if !errors.Is(err, safecast.ErrConversionIssue) { + t.Fatalf("panic with unexpected error: %v", err) + } + + if !errors.Is(err, mt.ExpectedError) { + t.Fatalf("panic with unexpected error: %v", err) + } + }(t) + + out := safecast.MustConvert[O](mt.Input) + assertEqual(t, mt.ExpectedOutput, out) } func TestMustConvert(t *testing.T) { @@ -1917,57 +1969,26 @@ func TestMustConvert(t *testing.T) { // here we are simply checking that the function panic on errors t.Run("panic on error", func(t *testing.T) { - for name, input := range map[string]any{ - "nil": nil, - "negative": -1, - "overflow": math.MaxInt, - "string": "cats", + for name, tt := range map[string]TestableConvert{ + "negative": MapMustConvertTest[int, uint8]{Input: -1, ExpectedError: safecast.ErrExceedMinimumValue}, + "overflow": MapMustConvertTest[int, uint8]{Input: math.MaxInt, ExpectedError: safecast.ErrExceedMaximumValue}, + "string": MapMustConvertTest[string, uint8]{Input: "cats", ExpectedError: safecast.ErrStringConversion}, } { t.Run(name, func(t *testing.T) { - // configure validate there is no panic - defer func(t *testing.T) { - t.Helper() - - r := recover() - if r == nil { - t.Fatal("did not panic") - } - - err, ok := r.(error) - if !ok { - t.Fatalf("panic value is not an error: %v", r) - } - - if !errors.Is(err, safecast.ErrConversionIssue) { - t.Fatalf("panic with unexpected error: %v", err) - } - }(t) - - safecast.MustConvert[uint8](input) + tt.TestConvert(t) }) } }) t.Run("no panic", func(t *testing.T) { - for name, input := range map[string]any{ - "number": 42, - "string": "42", - "octal": "0o52", - "float": 42.0, + for name, tt := range map[string]TestableConvert{ + "number": MapMustConvertTest[int, uint8]{Input: 42, ExpectedOutput: 42}, + "string": MapMustConvertTest[string, uint8]{Input: "42", ExpectedOutput: 42}, + "float": MapMustConvertTest[float64, uint8]{Input: 42.0, ExpectedOutput: 42}, + "octal": MapMustConvertTest[string, uint8]{Input: "0o52", ExpectedOutput: 42}, } { t.Run(name, func(t *testing.T) { - // configure a helper to validate there is no panic - defer func(t *testing.T) { - t.Helper() - - err := recover() - if err != nil { - t.Fatalf("panic with %v", err) - } - }(t) - - converted := safecast.MustConvert[int](input) - assertEqual(t, 42, converted) + tt.TestConvert(t) }) } }) diff --git a/types.go b/types.go index 3ede9fe..7d992c8 100644 --- a/types.go +++ b/types.go @@ -3,29 +3,34 @@ package safecast // This files is highly inspired from https://pkg.go.dev/golang.org/x/exp/constraints // I didn't import it as it would have added an unnecessary dependency -// Signed is an alias for all signed integers: int, int8, int16, int32, and int64 types. +// Signed is a constraint for all signed integers: int, int8, int16, int32, and int64 types. type Signed interface { ~int | ~int8 | ~int16 | ~int32 | ~int64 } -// Unsigned is an alias for all unsigned integers: uint, uint8, uint16, uint32, and uint64 types. +// Unsigned is a constraint for all unsigned integers: uint, uint8, uint16, uint32, and uint64 types. // TODO: support uintpr type Unsigned interface { ~uint | ~uint8 | ~uint16 | ~uint32 | ~uint64 } -// Integer is an alias for the all unsigned and signed integers +// Integer is a constraint for the all unsigned and signed integers type Integer interface { Signed | Unsigned } -// Float is an alias for the float32 and float64 types. +// Float is a constraint for the float32 and float64 types. type Float interface { ~float32 | ~float64 } -// Number is an alias for all integers and floats +// Number is a constraint for all integers and floats // TODO: consider using complex, but not sure there is a need type Number interface { Integer | Float } + +// Input is a constraint for all types that can be used as input for [Convert], and [MustConvert] +type Input interface { + Number | ~string | ~bool +}