Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Convert and support conversion from string and boolean #55

Merged
merged 2 commits into from
Dec 17, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
128 changes: 119 additions & 9 deletions conversion.go
Original file line number Diff line number Diff line change
@@ -1,14 +1,66 @@
// Package go-safecast solves the type conversion issues in Go
//
// 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.

package safecast

import (
"errors"
"fmt"
"math"
"strconv"
"strings"
)

// Convert attempts to convert any value to the desired type
// - If the conversion is possible, the converted value is returned.
// - If the conversion results in a value outside the range of the desired type, an [ErrRangeOverflow] error is wrapped in the returned error.
// - If the conversion exceeds the maximum value of the desired type, an [ErrExceedMaximumValue] error is wrapped in the returned error.
// - If the conversion exceeds the minimum value of the desired type, an [ErrExceedMinimumValue] error is wrapped in the returned error.
// - 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) {
ccoVeille marked this conversation as resolved.
Show resolved Hide resolved
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, errorHelper{
err: fmt.Errorf("%w from %T", ErrUnsupportedConversion, orig),
}
}

func convertFromNumber[NumOut Number, NumIn Number](orig NumIn) (converted NumOut, err error) {
converted = NumOut(orig)

Expand Down Expand Up @@ -40,7 +92,7 @@ func convertFromNumber[NumOut Number, NumIn Number](orig NumIn) (converted NumOu
errBoundary = ErrExceedMinimumValue
}

return 0, Error{
return 0, errorHelper{
value: orig,
err: errBoundary,
boundary: boundary,
Expand All @@ -55,7 +107,7 @@ func convertFromNumber[NumOut Number, NumIn Number](orig NumIn) (converted NumOu
}

if !sameSign(orig, converted) {
return 0, Error{
return 0, errorHelper{
value: orig,
err: errBoundary,
boundary: boundary,
Expand All @@ -78,14 +130,72 @@ func convertFromNumber[NumOut Number, NumIn Number](orig NumIn) (converted NumOu
return converted, nil
}

return 0, Error{
return 0, errorHelper{
value: orig,
err: errBoundary,
boundary: boundary,
}
}

// 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
}
ccoVeille marked this conversation as resolved.
Show resolved Hide resolved

if strings.Contains(s, ".") {
o, err := strconv.ParseFloat(s, 64)
if err != nil {
return 0, errorHelper{
value: s,
err: fmt.Errorf("%w %v to %T", ErrStringConversion, s, converted),
}
}
return convertFromNumber[NumOut](o)
}

if strings.HasPrefix(s, "-") {
o, err := strconv.ParseInt(s, 0, 64)
if err != nil {
ccoVeille marked this conversation as resolved.
Show resolved Hide resolved
if errors.Is(err, strconv.ErrRange) {
return 0, errorHelper{
value: s,
err: ErrExceedMinimumValue,
boundary: math.MinInt,
}
}
return 0, errorHelper{
value: s,
err: fmt.Errorf("%w %v to %T", ErrStringConversion, s, converted),
}
}

return convertFromNumber[NumOut](o)
}

o, err := strconv.ParseUint(s, 0, 64)
if err != nil {
if errors.Is(err, strconv.ErrRange) {
return 0, errorHelper{
value: s,
err: ErrExceedMaximumValue,
boundary: uint(math.MaxUint),
}
}

return 0, errorHelper{
value: s,
err: fmt.Errorf("%w %v to %T", ErrStringConversion, 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) {
Expand Down
21 changes: 21 additions & 0 deletions conversion_64bit_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ package safecast_test
import (
"math"
"testing"

"github.com/ccoveille/go-safecast"
)

func TestToInt32_64bit(t *testing.T) {
Expand Down Expand Up @@ -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)
})
}
})
}
Loading
Loading