From 982eb80ce20d8739c4285cde2fd4bfd845a6421c Mon Sep 17 00:00:00 2001 From: ccoVeille <3875889+ccoVeille@users.noreply.github.com> Date: Sat, 14 Dec 2024 17:50:20 +0100 Subject: [PATCH] feat: add Convert and allow to convert from string and bool --- conversion.go | 82 ++++- conversion_64bit_test.go | 21 ++ conversion_test.go | 639 +++++++++++++++++++++++++++++++++++++++ examples_test.go | 57 ++++ 4 files changed, 798 insertions(+), 1 deletion(-) diff --git a/conversion.go b/conversion.go index abd776e..ab692a1 100644 --- a/conversion.go +++ b/conversion.go @@ -6,9 +6,55 @@ package safecast import ( + "fmt" "math" + "strconv" + "strings" ) +func Convert[NumOut Number](orig any) (converted NumOut, err error) { + switch v := orig.(type) { + case int: + return convertFromNumber[NumOut](v) + case uint: + return convertFromNumber[NumOut](v) + case int8: + return convertFromNumber[NumOut](v) + case uint8: + return convertFromNumber[NumOut](v) + case int16: + return convertFromNumber[NumOut](v) + case uint16: + return convertFromNumber[NumOut](v) + case int32: + return convertFromNumber[NumOut](v) + case uint32: + return convertFromNumber[NumOut](v) + case int64: + return convertFromNumber[NumOut](v) + case uint64: + return convertFromNumber[NumOut](v) + case float32: + return convertFromNumber[NumOut](v) + case float64: + return convertFromNumber[NumOut](v) + case bool: + o := 0 + if v { + o = 1 + } + return NumOut(o), nil + case fmt.Stringer: + return convertFromString[NumOut](v.String()) + case error: + return convertFromString[NumOut](v.Error()) + case string: + return convertFromString[NumOut](v) + } + + return 0, ErrConversionIssue +} + func convertFromNumber[NumOut Number, NumIn Number](orig NumIn) (converted NumOut, err error) { converted = NumOut(orig) @@ -85,7 +131,41 @@ func convertFromNumber[NumOut Number, NumIn Number](orig NumIn) (converted NumOu } } -// ToInt attempts to convert any [Number] value to an int. +func convertFromString[NumOut Number](s string) (converted NumOut, err error) { + s = strings.TrimSpace(s) + + if b, err := strconv.ParseBool(s); err == nil { + if b { + return NumOut(1), nil + } + return NumOut(0), nil + } + + if strings.Contains(s, ".") { + o, err := strconv.ParseFloat(s, 64) + if err != nil { + return 0, fmt.Errorf("%w: cannot convert %v to %T", ErrConversionIssue, s, converted) + } + return convertFromNumber[NumOut](o) + } + + if s, found := strings.CutPrefix(s, "-"); found { + o, err := strconv.ParseInt(s, 0, 64) + if err != nil || o < 0 { + return 0, fmt.Errorf("%w: cannot convert %v to %T", ErrConversionIssue, s, converted) + } + + return convertFromNumber[NumOut](-o) + } + + o, err := strconv.ParseUint(s, 0, 64) + if err != nil { + return 0, fmt.Errorf("%w: cannot convert %v to %T", ErrConversionIssue, s, converted) + } + return convertFromNumber[NumOut](o) +} + +// ToInt attempts to convert any [Type] value to an int. // If the conversion results in a value outside the range of an int, // an [ErrConversionIssue] error is returned. func ToInt[T Number](i T) (int, error) { diff --git a/conversion_64bit_test.go b/conversion_64bit_test.go index 6f0e1fd..53892aa 100644 --- a/conversion_64bit_test.go +++ b/conversion_64bit_test.go @@ -12,6 +12,8 @@ package safecast_test import ( "math" "testing" + + "github.com/ccoveille/go-safecast" ) func TestToInt32_64bit(t *testing.T) { @@ -53,3 +55,22 @@ func TestToInt_64bit(t *testing.T) { }) }) } + +// TestConvert_64bit completes the [TestConvert] tests in conversion_test.go +// it contains the tests that can only works on 64-bit systems +func TestConvert_64bit(t *testing.T) { + t.Run("to uint32", func(t *testing.T) { + for name, tt := range map[string]struct { + input any + want uint32 + }{ + "positive out of range": {input: uint64(math.MaxUint32 + 1), want: 0}, + } { + t.Run(name, func(t *testing.T) { + got, err := safecast.Convert[uint32](tt.input) + assertEqual(t, tt.want, got) + requireErrorIs(t, err, safecast.ErrConversionIssue) + }) + } + }) +} diff --git a/conversion_test.go b/conversion_test.go index 9460488..b410e48 100644 --- a/conversion_test.go +++ b/conversion_test.go @@ -10,8 +10,22 @@ package safecast_test import ( "math" "testing" + + "github.com/ccoveille/go-safecast" ) +type anyStringer struct{} + +func (anyStringer) String() string { + return "42" +} + +type anyError struct{} + +func (anyError) Error() string { + return "42" +} + func TestToInt8(t *testing.T) { t.Run("from int", func(t *testing.T) { assertInt8OK(t, []caseInt8[int]{ @@ -1477,3 +1491,628 @@ func TestToFloat64(t *testing.T) { }) }) } + +func TestConvert(t *testing.T) { + t.Run("to int", func(t *testing.T) { + for name, tt := range map[string]struct { + input any + want int + }{ + "negative within range": {input: -100, want: -100}, + "zero": {input: 0, want: 0}, + "int8 within range": {input: int8(100), want: 100}, + "int16 within range": {input: int16(100), want: 100}, + "int32 within range": {input: int32(100), want: 100}, + "int64 within range": {input: int64(100), want: 100}, + "uint within range": {input: uint(100), want: 100}, + "uint8 within range": {input: uint8(100), want: 100}, + "uint16 within range": {input: uint16(100), want: 100}, + "uint32 within range": {input: uint32(100), want: 100}, + "uint64 within range": {input: uint64(100), want: 100}, + "float32 within range": {input: float32(100), want: 100}, + "float64 within range": {input: float64(100), want: 100}, + "boolean true": {input: true, want: 1}, + "boolean false": {input: false, want: 0}, + "string true": {input: "true", want: 1}, + "string false": {input: "false", want: 0}, + "positive string within range": {input: "42", want: 42}, + "negative string within range": {input: "-42", want: -42}, + "float string within range": {input: "100.0", want: 100}, + } { + t.Run(name, func(t *testing.T) { + got, err := safecast.Convert[int](tt.input) + assertNoError(t, err) + assertEqual(t, tt.want, got) + }) + } + + for name, tt := range map[string]struct { + input any + want int + }{ + "invalid string": {input: "abc", want: 0}, + "invalid string with dot": {input: "ab.c", want: 0}, + "invalid string with leading dash": {input: "-abc", want: 0}, + } { + t.Run(name, func(t *testing.T) { + got, err := safecast.Convert[int](tt.input) + assertEqual(t, tt.want, got) + requireErrorIs(t, err, safecast.ErrConversionIssue) + }) + } + }) + + t.Run("to int8", func(t *testing.T) { + for name, tt := range map[string]struct { + input any + want int8 + }{ + "negative within range": {input: -100, want: -100}, + "zero": {input: 0, want: 0}, + "int within range": {input: int(100), want: 100}, + "int8 within range": {input: int8(100), want: 100}, + "int16 within range": {input: int16(100), want: 100}, + "int32 within range": {input: int32(100), want: 100}, + "int64 within range": {input: int64(100), want: 100}, + "uint within range": {input: uint(100), want: 100}, + "uint8 within range": {input: uint8(100), want: 100}, + "uint16 within range": {input: uint16(100), want: 100}, + "uint32 within range": {input: uint32(100), want: 100}, + "uint64 within range": {input: uint64(100), want: 100}, + "float32 within range": {input: float32(100), want: 100}, + "float64 within range": {input: float64(100), want: 100}, + "boolean true": {input: true, want: 1}, + "boolean false": {input: false, want: 0}, + "string true": {input: "true", want: 1}, + "string false": {input: "false", want: 0}, + "positive string within range": {input: "42", want: 42}, + "negative string within range": {input: "-42", want: -42}, + "float string within range": {input: "100.0", want: 100}, + "stringer": {input: anyStringer{}, want: 42}, + "error": {input: anyError{}, want: 42}, + } { + t.Run(name, func(t *testing.T) { + got, err := safecast.Convert[int8](tt.input) + assertNoError(t, err) + assertEqual(t, tt.want, got) + }) + } + + for name, tt := range map[string]struct { + input any + want int8 + }{ + "invalid string": {input: "abc", want: 0}, + "invalid string with dot": {input: "ab.c", want: 0}, + "invalid string with leading dash": {input: "-abc", want: 0}, + } { + t.Run(name, func(t *testing.T) { + got, err := safecast.Convert[int8](tt.input) + assertEqual(t, tt.want, got) + requireErrorIs(t, err, safecast.ErrConversionIssue) + }) + } + }) + + t.Run("to int16", func(t *testing.T) { + for name, tt := range map[string]struct { + input any + want int16 + }{ + "negative within range": {input: -100, want: -100}, + "zero": {input: 0, want: 0}, + "int within range": {input: int(100), want: 100}, + "int8 within range": {input: int8(100), want: 100}, + "int16 within range": {input: int16(100), want: 100}, + "int32 within range": {input: int32(100), want: 100}, + "int64 within range": {input: int64(100), want: 100}, + "uint within range": {input: uint(100), want: 100}, + "uint8 within range": {input: uint8(100), want: 100}, + "uint16 within range": {input: uint16(100), want: 100}, + "uint32 within range": {input: uint32(100), want: 100}, + "uint64 within range": {input: uint64(100), want: 100}, + "float32 within range": {input: float32(100), want: 100}, + "float64 within range": {input: float64(100), want: 100}, + "boolean true": {input: true, want: 1}, + "boolean false": {input: false, want: 0}, + "string true": {input: "true", want: 1}, + "string false": {input: "false", want: 0}, + "positive string within range": {input: "42", want: 42}, + "negative string within range": {input: "-42", want: -42}, + "float string within range": {input: "100.0", want: 100}, + } { + t.Run(name, func(t *testing.T) { + got, err := safecast.Convert[int16](tt.input) + assertNoError(t, err) + assertEqual(t, tt.want, got) + }) + } + + for name, tt := range map[string]struct { + input any + want int16 + }{ + "invalid string": {input: "abc", want: 0}, + "invalid string with dot": {input: "ab.c", want: 0}, + "invalid string with leading dash": {input: "-abc", want: 0}, + } { + t.Run(name, func(t *testing.T) { + got, err := safecast.Convert[int16](tt.input) + assertEqual(t, tt.want, got) + requireErrorIs(t, err, safecast.ErrConversionIssue) + }) + } + }) + + t.Run("to int32", func(t *testing.T) { + for name, tt := range map[string]struct { + input any + want int32 + }{ + "negative within range": {input: -100, want: -100}, + "zero": {input: 0, want: 0}, + "int within range": {input: int(100), want: 100}, + "int8 within range": {input: int8(100), want: 100}, + "int16 within range": {input: int16(100), want: 100}, + "int32 within range": {input: int32(100), want: 100}, + "int64 within range": {input: int64(100), want: 100}, + "uint within range": {input: uint(100), want: 100}, + "uint8 within range": {input: uint8(100), want: 100}, + "uint16 within range": {input: uint16(100), want: 100}, + "uint32 within range": {input: uint32(100), want: 100}, + "uint64 within range": {input: uint64(100), want: 100}, + "float32 within range": {input: float32(100), want: 100}, + "float64 within range": {input: float64(100), want: 100}, + "boolean true": {input: true, want: 1}, + "boolean false": {input: false, want: 0}, + "string true": {input: "true", want: 1}, + "string false": {input: "false", want: 0}, + "positive string within range": {input: "42", want: 42}, + "negative string within range": {input: "-42", want: -42}, + "float string within range": {input: "100.0", want: 100}, + } { + t.Run(name, func(t *testing.T) { + got, err := safecast.Convert[int32](tt.input) + assertNoError(t, err) + assertEqual(t, tt.want, got) + }) + } + + for name, tt := range map[string]struct { + input any + want int32 + }{ + "invalid string": {input: "abc", want: 0}, + "invalid string with dot": {input: "ab.c", want: 0}, + "invalid string with leading dash": {input: "-abc", want: 0}, + } { + t.Run(name, func(t *testing.T) { + got, err := safecast.Convert[int32](tt.input) + assertEqual(t, tt.want, got) + requireErrorIs(t, err, safecast.ErrConversionIssue) + }) + } + }) + + t.Run("to int64", func(t *testing.T) { + for name, tt := range map[string]struct { + input any + want int64 + }{ + "negative within range": {input: -100, want: -100}, + "zero": {input: 0, want: 0}, + "int within range": {input: int(100), want: 100}, + "int8 within range": {input: int8(100), want: 100}, + "int16 within range": {input: int16(100), want: 100}, + "int32 within range": {input: int32(100), want: 100}, + "int64 within range": {input: int64(100), want: 100}, + "uint within range": {input: uint(100), want: 100}, + "uint8 within range": {input: uint8(100), want: 100}, + "uint16 within range": {input: uint16(100), want: 100}, + "uint32 within range": {input: uint32(100), want: 100}, + "uint64 within range": {input: uint64(100), want: 100}, + "float32 within range": {input: float32(100), want: 100}, + "float64 within range": {input: float64(100), want: 100}, + "boolean true": {input: true, want: 1}, + "boolean false": {input: false, want: 0}, + "string true": {input: "true", want: 1}, + "string false": {input: "false", want: 0}, + "positive string within range": {input: "42", want: 42}, + "negative string within range": {input: "-42", want: -42}, + "float string within range": {input: "100.0", want: 100}, + "string -10_000": {input: "-10_000", want: -10000}, + "string negative binary": {input: "-0b101010", want: -42}, + "string negative octal": {input: "-0o42", want: -34}, + "string negative hexadecimal": {input: "-0x42", want: -66}, + } { + t.Run(name, func(t *testing.T) { + got, err := safecast.Convert[int64](tt.input) + assertNoError(t, err) + assertEqual(t, tt.want, got) + }) + } + + for name, tt := range map[string]struct { + input any + want int64 + }{ + "invalid string": {input: "abc", want: 0}, + "invalid string with dot": {input: "ab.c", want: 0}, + "invalid string with leading dash": {input: "-abc", want: 0}, + } { + t.Run(name, func(t *testing.T) { + got, err := safecast.Convert[int64](tt.input) + assertEqual(t, tt.want, got) + requireErrorIs(t, err, safecast.ErrConversionIssue) + }) + } + }) + + t.Run("to uint", func(t *testing.T) { + for name, tt := range map[string]struct { + input any + want uint + }{ + "zero": {input: 0, want: 0}, + "int within range": {input: int(100), want: 100}, + "int8 within range": {input: int8(100), want: 100}, + "int16 within range": {input: int16(100), want: 100}, + "int32 within range": {input: int32(100), want: 100}, + "int64 within range": {input: int64(100), want: 100}, + "uint within range": {input: uint(100), want: 100}, + "uint8 within range": {input: uint8(100), want: 100}, + "uint16 within range": {input: uint16(100), want: 100}, + "uint32 within range": {input: uint32(100), want: 100}, + "uint64 within range": {input: uint64(100), want: 100}, + "float32 within range": {input: float32(100), want: 100}, + "float64 within range": {input: float64(100), want: 100}, + "boolean true": {input: true, want: 1}, + "boolean false": {input: false, want: 0}, + "string true": {input: "true", want: 1}, + "string false": {input: "false", want: 0}, + "positive string within range": {input: "42", want: 42}, + "float string within range": {input: "100.0", want: 100}, + "string 10_000": {input: "10_000", want: 10000}, + "string binary": {input: "0b101010", want: 42}, + "string octal": {input: "0o42", want: 34}, + "string hexadecimal": {input: "0x42", want: 66}, + } { + t.Run(name, func(t *testing.T) { + got, err := safecast.Convert[uint](tt.input) + assertNoError(t, err) + assertEqual(t, tt.want, got) + }) + } + + for name, tt := range map[string]struct { + input any + want uint + }{ + "negative": {input: -1, want: 0}, + "string negative": {input: "-42", want: 0}, + "invalid string": {input: "abc", want: 0}, + "invalid string with dot": {input: "ab.c", want: 0}, + "invalid string with leading dash": {input: "-abc", want: 0}, + } { + t.Run(name, func(t *testing.T) { + got, err := safecast.Convert[uint](tt.input) + assertEqual(t, tt.want, got) + requireErrorIs(t, err, safecast.ErrConversionIssue) + }) + } + }) + + t.Run("to uint8", func(t *testing.T) { + for name, tt := range map[string]struct { + input any + want uint8 + }{ + "zero": {input: 0, want: 0}, + "int within range": {input: int(100), want: 100}, + "int8 within range": {input: int8(100), want: 100}, + "int16 within range": {input: int16(100), want: 100}, + "int32 within range": {input: int32(100), want: 100}, + "int64 within range": {input: int64(100), want: 100}, + "uint within range": {input: uint(100), want: 100}, + "uint8 within range": {input: uint8(100), want: 100}, + "uint16 within range": {input: uint16(100), want: 100}, + "uint32 within range": {input: uint32(100), want: 100}, + "uint64 within range": {input: uint64(100), want: 100}, + "float32 within range": {input: float32(100), want: 100}, + "float64 within range": {input: float64(100), want: 100}, + "range boundary": {input: math.MaxUint8, want: math.MaxUint8}, + "boolean true": {input: true, want: 1}, + "boolean false": {input: false, want: 0}, + "string true": {input: "true", want: 1}, + "string false": {input: "false", want: 0}, + "positive string within range": {input: "42", want: 42}, + "float string within range": {input: "100.0", want: 100}, + } { + t.Run(name, func(t *testing.T) { + got, err := safecast.Convert[uint8](tt.input) + assertNoError(t, err) + assertEqual(t, tt.want, got) + }) + } + + for name, tt := range map[string]struct { + input any + want uint8 + }{ + "negative": {input: -1, want: 0}, + "positive out of range": {input: math.MaxUint8 + 1, want: 0}, + "string negative": {input: "-42", want: 0}, + "invalid string": {input: "abc", want: 0}, + "invalid string with dot": {input: "ab.c", want: 0}, + "invalid string with leading dash": {input: "-abc", want: 0}, + } { + t.Run(name, func(t *testing.T) { + got, err := safecast.Convert[uint8](tt.input) + assertEqual(t, tt.want, got) + requireErrorIs(t, err, safecast.ErrConversionIssue) + }) + } + }) + + t.Run("to uint16", func(t *testing.T) { + for name, tt := range map[string]struct { + input any + want uint16 + }{ + "zero": {input: 0, want: 0}, + "int within range": {input: int(100), want: 100}, + "int8 within range": {input: int8(100), want: 100}, + "int16 within range": {input: int16(100), want: 100}, + "int32 within range": {input: int32(100), want: 100}, + "int64 within range": {input: int64(100), want: 100}, + "uint within range": {input: uint(100), want: 100}, + "uint8 within range": {input: uint8(100), want: 100}, + "uint16 within range": {input: uint16(100), want: 100}, + "uint32 within range": {input: uint32(100), want: 100}, + "uint64 within range": {input: uint64(100), want: 100}, + "float32 within range": {input: float32(100), want: 100}, + "float64 within range": {input: float64(100), want: 100}, + "range boundary": {input: math.MaxUint16, want: math.MaxUint16}, + "boolean true": {input: true, want: 1}, + "boolean false": {input: false, want: 0}, + "string true": {input: "true", want: 1}, + "string 10_000": {input: "10_000", want: 10000}, + "string false": {input: "false", want: 0}, + "string hexadecimal": {input: "0x42", want: 66}, + "string binary": {input: "0b101010", want: 42}, + "string octal": {input: "0o42", want: 34}, + "positive string within range": {input: "42", want: 42}, + "float string within range": {input: "100.0", want: 100}, + } { + t.Run(name, func(t *testing.T) { + got, err := safecast.Convert[uint16](tt.input) + assertNoError(t, err) + assertEqual(t, tt.want, got) + }) + } + + for name, tt := range map[string]struct { + input any + want uint16 + }{ + "negative": {input: -1, want: 0}, + "positive out of range": {input: math.MaxUint16 + 1, want: 0}, + "string negative": {input: "-42", want: 0}, + "invalid string": {input: "abc", want: 0}, + "invalid string with dot": {input: "ab.c", want: 0}, + "invalid string with leading dash": {input: "-abc", want: 0}, + "string -10_000": {input: "-10_000", want: 0}, + "string negative binary": {input: "-0b101010", want: 0}, + "string negative octal": {input: "-0o42", want: 0}, + "string negative hexadecimal": {input: "-0x42", want: 0}, + } { + t.Run(name, func(t *testing.T) { + got, err := safecast.Convert[uint16](tt.input) + assertEqual(t, tt.want, got) + requireErrorIs(t, err, safecast.ErrConversionIssue) + }) + } + }) + + t.Run("to uint32", func(t *testing.T) { + for name, tt := range map[string]struct { + input any + want uint32 + }{ + "zero": {input: 0, want: 0}, + "int within range": {input: int(100), want: 100}, + "int8 within range": {input: int8(100), want: 100}, + "int16 within range": {input: int16(100), want: 100}, + "int32 within range": {input: int32(100), want: 100}, + "int64 within range": {input: int64(100), want: 100}, + "uint within range": {input: uint(100), want: 100}, + "uint8 within range": {input: uint8(100), want: 100}, + "uint16 within range": {input: uint16(100), want: 100}, + "uint32 within range": {input: uint32(100), want: 100}, + "uint64 within range": {input: uint64(100), want: 100}, + "float32 within range": {input: float32(100), want: 100}, + "float64 within range": {input: float64(100), want: 100}, + "range boundary": {input: uint32(math.MaxUint32), want: uint32(math.MaxUint32)}, + "boolean true": {input: true, want: 1}, + "boolean false": {input: false, want: 0}, + "string true": {input: "true", want: 1}, + "string false": {input: "false", want: 0}, + "positive string within range": {input: "42", want: 42}, + "float string within range": {input: "100.0", want: 100}, + } { + t.Run(name, func(t *testing.T) { + got, err := safecast.Convert[uint32](tt.input) + assertNoError(t, err) + assertEqual(t, tt.want, got) + }) + } + + for name, tt := range map[string]struct { + input any + want uint32 + }{ + "negative": {input: -1, want: 0}, + "string negative": {input: "-42", want: 0}, + "invalid string": {input: "abc", want: 0}, + "invalid string with dot": {input: "ab.c", want: 0}, + "invalid string with leading dash": {input: "-abc", want: 0}, + } { + t.Run(name, func(t *testing.T) { + got, err := safecast.Convert[uint32](tt.input) + assertEqual(t, tt.want, got) + requireErrorIs(t, err, safecast.ErrConversionIssue) + }) + } + }) + + t.Run("to uint64", func(t *testing.T) { + for name, tt := range map[string]struct { + input any + want uint64 + }{ + "zero": {input: 0, want: 0}, + "int within range": {input: int(100), want: 100}, + "int8 within range": {input: int8(100), want: 100}, + "int16 within range": {input: int16(100), want: 100}, + "int32 within range": {input: int32(100), want: 100}, + "int64 within range": {input: int64(100), want: 100}, + "uint within range": {input: uint(100), want: 100}, + "uint8 within range": {input: uint8(100), want: 100}, + "uint16 within range": {input: uint16(100), want: 100}, + "uint32 within range": {input: uint32(100), want: 100}, + "uint64 within range": {input: uint64(100), want: 100}, + "float32 within range": {input: float32(100), want: 100}, + "float64 within range": {input: float64(100), want: 100}, + "range boundary": {input: uint64(math.MaxUint64), want: math.MaxUint64}, + "boolean true": {input: true, want: 1}, + "boolean false": {input: false, want: 0}, + "string true": {input: "true", want: 1}, + "string false": {input: "false", want: 0}, + "positive string within range": {input: "42", want: 42}, + "float string within range": {input: "100.0", want: 100}, + } { + t.Run(name, func(t *testing.T) { + got, err := safecast.Convert[uint64](tt.input) + assertNoError(t, err) + assertEqual(t, tt.want, got) + }) + } + + for name, tt := range map[string]struct { + input any + want uint64 + }{ + "negative": {input: -1, want: 0}, + "string negative": {input: "-42", want: 0}, + "invalid string": {input: "abc", want: 0}, + "invalid string with dot": {input: "ab.c", want: 0}, + "invalid string with leading dash": {input: "-abc", want: 0}, + "unsupported type": {input: struct{}{}, want: 0}, + } { + t.Run(name, func(t *testing.T) { + got, err := safecast.Convert[uint64](tt.input) + assertEqual(t, tt.want, got) + requireErrorIs(t, err, safecast.ErrConversionIssue) + }) + } + }) + + t.Run("to float32", func(t *testing.T) { + for name, tt := range map[string]struct { + input any + want float32 + }{ + "zero": {input: 0, want: 0.0}, + "int within range": {input: int(100), want: 100.0}, + "int8 within range": {input: int8(100), want: 100.0}, + "int16 within range": {input: int16(100), want: 100.0}, + "int32 within range": {input: int32(100), want: 100.0}, + "int64 within range": {input: int64(100), want: 100.0}, + "uint within range": {input: uint(100), want: 100.0}, + "uint8 within range": {input: uint8(100), want: 100.0}, + "uint16 within range": {input: uint16(100), want: 100.0}, + "uint32 within range": {input: uint32(100), want: 100.0}, + "uint64 within range": {input: uint64(100), want: 100.0}, + "float32 within range": {input: float32(100), want: 100.0}, + "float64 within range": {input: float64(100), want: 100.0}, + "boolean true": {input: true, want: 1}, + "boolean false": {input: false, want: 0}, + "string true": {input: "true", want: 1}, + "string false": {input: "false", want: 0}, + "positive string within range": {input: "42", want: 42}, + "float string within range": {input: "100.0", want: 100}, + } { + t.Run(name, func(t *testing.T) { + got, err := safecast.Convert[float32](tt.input) + assertNoError(t, err) + assertEqual(t, tt.want, got) + }) + } + + for name, tt := range map[string]struct { + input any + want float32 + }{ + "invalid string": {input: "abc", want: 0}, + "invalid string with dot": {input: "ab.c", want: 0}, + "invalid string with leading dash": {input: "-abc", want: 0}, + "unsupported type": {input: struct{}{}, want: 0}, + } { + t.Run(name, func(t *testing.T) { + got, err := safecast.Convert[float32](tt.input) + assertEqual(t, tt.want, got) + requireErrorIs(t, err, safecast.ErrConversionIssue) + }) + } + }) + + t.Run("to float64", func(t *testing.T) { + for name, tt := range map[string]struct { + input any + want float64 + }{ + "zero": {input: 0, want: 0.0}, + "int within range": {input: int(100), want: 100.0}, + "int8 within range": {input: int8(100), want: 100.0}, + "int16 within range": {input: int16(100), want: 100.0}, + "int32 within range": {input: int32(100), want: 100.0}, + "int64 within range": {input: int64(100), want: 100.0}, + "uint within range": {input: uint(100), want: 100.0}, + "uint8 within range": {input: uint8(100), want: 100.0}, + "uint16 within range": {input: uint16(100), want: 100.0}, + "uint32 within range": {input: uint32(100), want: 100.0}, + "uint64 within range": {input: uint64(100), want: 100.0}, + "float32 within range": {input: float32(100), want: 100.0}, + "float64 within range": {input: float64(100), want: 100.0}, + "boolean true": {input: true, want: 1}, + "boolean false": {input: false, want: 0}, + "string true": {input: "true", want: 1}, + "string false": {input: "false", want: 0}, + "positive string within range": {input: "42", want: 42}, + "float string within range": {input: "100.0", want: 100}, + } { + t.Run(name, func(t *testing.T) { + got, err := safecast.Convert[float64](tt.input) + assertNoError(t, err) + assertEqual(t, tt.want, got) + }) + } + + for name, tt := range map[string]struct { + input any + want float64 + }{ + "invalid string": {input: "abc", want: 0}, + "invalid string with dot": {input: "ab.c", want: 0}, + "invalid string with leading dash": {input: "-abc", want: 0}, + "invalid string with multiple leading dash": {input: "--123", want: 0}, + "unsupported type": {input: struct{}{}, want: 0}, + } { + t.Run(name, func(t *testing.T) { + got, err := safecast.Convert[float64](tt.input) + assertEqual(t, tt.want, got) + requireErrorIs(t, err, safecast.ErrConversionIssue) + }) + } + }) +} diff --git a/examples_test.go b/examples_test.go index b13b088..dee9a63 100644 --- a/examples_test.go +++ b/examples_test.go @@ -169,3 +169,60 @@ func ExampleToFloat64() { // 42 // 1.7976931348623157e+308 } + +func ExampleConvert() { + b, err := safecast.Convert[int8](true) + fmt.Println(b, err) + + b, err = safecast.Convert[int8]("true") + fmt.Println(b, err) + + c, err := safecast.Convert[int16](17.1) + fmt.Println(c, err) + + c, err = safecast.Convert[int16](int64(17)) + fmt.Println(c, err) + + d, err := safecast.Convert[int32]("17.1") + fmt.Println(d, err) + + i, err := safecast.Convert[uint]("100_000") + fmt.Println(i, err) + + i, err = safecast.Convert[uint]("0b11") + fmt.Println(i, err) + + i, err = safecast.Convert[uint]("0x11") + fmt.Println(i, err) + + a := int8(-1) + i, err = safecast.Convert[uint](a) + fmt.Println(i, err) + + i, err = safecast.Convert[uint]("-1") + fmt.Println(i, err) + + i, err = safecast.Convert[uint]("abc") + fmt.Println(i, err) + + i, err = safecast.Convert[uint]("-1.1") + fmt.Println(i, err) + + i, err = safecast.Convert[uint](".12345E+5") + fmt.Println(i, err) + + // Output: + // 1 + // 1 + // 17 + // 17 + // 17 + // 100000 + // 3 + // 17 + // 0 conversion issue: -1 (int8) is less than 0 (uint): minimum value for this type exceeded + // 0 conversion issue: -1 (int64) is less than 0 (uint): minimum value for this type exceeded + // 0 conversion issue: cannot convert abc to uint + // 0 conversion issue: -1.1 (float64) is less than 0 (uint): minimum value for this type exceeded + // 12345 +}