Skip to content

Commit

Permalink
feat: provide a better error message
Browse files Browse the repository at this point in the history
refactor code to use only one detection
  • Loading branch information
ccoVeille committed Sep 7, 2024
1 parent 25078db commit 5b02fc9
Show file tree
Hide file tree
Showing 2 changed files with 162 additions and 43 deletions.
162 changes: 124 additions & 38 deletions conversion.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,153 +36,239 @@ type Number interface {
Integer | Float
}

var ErrOutOfRange = errors.New("out of range")
type Error struct {
value any
boundary any
err error
}

func (e Error) Error() string {
errMessage := ErrIntegerOverflow.Error()
if e.err != nil {
errMessage = fmt.Sprintf("%s: %s", errMessage, e.err.Error())
}
return errMessage
}

func (e Error) Unwrap() []error {
errs := []error{ErrIntegerOverflow}
if e.err != nil {
errs = append(errs, e.err)
}
return errs
}

var (
ErrIntegerOverflow = errors.New("out of range")
ErrNegativeUnsigned = errors.New("unsigned integer cannot accept an negative number")
)

// 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.
// an ErrIntegerOverflow 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)
if err := checkUpperBoundary(i, math.MaxInt); err != nil {
return 0, err
}

if err := checkLowerBoundary(i, math.MinInt); err != nil {
return 0, err
}

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.
// an ErrIntegerOverflow error is returned.
func ToUint[T Number](i T) (uint, error) {
if err := assertNotNegative(i); err != nil {
return 0, err
}

if float64(i) > math.MaxUint64 {
return 0, fmt.Errorf("%w: %v is greater than math.MaxUint64", ErrOutOfRange, i)
if err := checkUpperBoundary(i, math.MaxUint64); err != nil {
return 0, err
}

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.
// an ErrIntegerOverflow 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 err := checkUpperBoundary(i, math.MaxInt8); err != nil {
return 0, err
}

if i < 0 && uint64(-i) > -math.MinInt8 {
return 0, fmt.Errorf("%w: %v is less than math.MinInt8", ErrOutOfRange, i)
if err := checkLowerBoundary(i, math.MinInt8); err != nil {
return 0, err
}

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.
// an ErrIntegerOverflow 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)
if err := checkUpperBoundary(i, math.MaxUint8); err != nil {
return 0, err
}

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.
// an ErrIntegerOverflow 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 err := checkUpperBoundary(i, math.MaxInt16); err != nil {
return 0, err
}

if i < 0 && uint64(-i) > -math.MinInt16 {
return 0, fmt.Errorf("%w: %v is less than math.MinInt16", ErrOutOfRange, i)
if err := checkLowerBoundary(i, math.MinInt16); err != nil {
return 0, err
}

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.
// an ErrIntegerOverflow 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)
if err := checkUpperBoundary(i, math.MaxUint16); err != nil {
return 0, err
}

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.
// an ErrIntegerOverflow 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 err := checkUpperBoundary(i, math.MaxInt32); err != nil {
return 0, err
}

if i < 0 && uint64(-i) > -math.MinInt32 {
return 0, fmt.Errorf("%w: %v is less than math.MinInt32", ErrOutOfRange, i)
if err := checkLowerBoundary(i, math.MinInt32); err != nil {
return 0, err
}

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.
// an ErrIntegerOverflow 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)
if err := checkUpperBoundary(i, math.MaxUint32); err != nil {
return 0, err
}

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.
// an ErrIntegerOverflow 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)
if err := checkUpperBoundary(i, math.MaxInt64); err != nil {
return 0, err
}

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.
// an ErrIntegerOverflow error is returned.
func ToUint64[T Number](i T) (uint64, error) {
if err := assertNotNegative(i); err != nil {
return 0, err
}

if float64(i) > math.MaxUint64 {
return 0, fmt.Errorf("%w: %v is greater than math.MaxUint64", ErrOutOfRange, i)
if err := checkUpperBoundary(i, uint64(math.MaxUint64)); 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 Error{
err: ErrNegativeUnsigned,
value: i,
}
}
return nil
}

func checkUpperBoundary[T Number](value T, boundary uint64) error {
if value <= 0 {
return nil
}

var bigger bool
switch f := any(value).(type) {
case float32:
bigger = float64(f) >= float64(boundary)
case float64:
bigger = float64(f) >= float64(boundary)
default:
bigger = uint64(value) > boundary
}

if bigger {
return Error{
value: value,
boundary: boundary,
err: fmt.Errorf("%v (%T) is greater than %v", value, value, boundary),
}
}

return nil
}

func checkLowerBoundary[T Number](value T, boundary int64) error {
if value >= 0 {
return nil
}



var smaller bool
switch f := any(value).(type) {
case float32:
smaller = float64(f) <= float64(boundary)
case float64:
smaller = float64(f) <= float64(boundary)
default:
smaller = int64(value) < boundary
}

if smaller {
return Error{
value: value,
boundary: boundary,
err: fmt.Errorf("%v (%T) is smaller than %v", value, value, boundary),
}
}

return nil
}
43 changes: 38 additions & 5 deletions conversion_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"errors"
"math"
"testing"
"strings"

"github.com/ccoveille/go-safecast"
)
Expand All @@ -27,11 +28,21 @@ func requireError(t *testing.T, err error) {
t.Fatal("expected error")
}

if !errors.Is(err, safecast.ErrOutOfRange) {
if !errors.Is(err, safecast.ErrIntegerOverflow) {
t.Errorf("expected out of range error, got %v", err)
}
}

func requireErrorContains(t *testing.T, err error, text string) {
t.Helper()
requireError(t, err)

errMessage := err.Error()
if !strings.Contains(errMessage, text) {
t.Fatalf("error message should contain %q: %q", text, errMessage)
}
}

func assertNoError(t *testing.T, err error) {
t.Helper()

Expand Down Expand Up @@ -205,6 +216,18 @@ func TestToInt8(t *testing.T) {
})
}

func TestErrorMessage(t *testing.T) {
_, err := safecast.ToUint8(-1)
requireErrorContains(t, err, "negative")

_, err = safecast.ToUint8(math.MaxInt16)
requireErrorContains(t, err, "greater")

_, err = safecast.ToInt8(-math.MaxInt16)
requireErrorContains(t, err, "smaller")
}


type caseUint8[in safecast.Number] struct {
name string
input in
Expand Down Expand Up @@ -1272,6 +1295,8 @@ func TestToUint64(t *testing.T) {
})

assertUint64Error(t, []caseUint64[float32]{
{name: "negative value", input: -1},
{name: "out of range max uint64", input: math.MaxUint64},
{name: "out of range max float32", input: math.MaxFloat32},
})
})
Expand All @@ -1284,6 +1309,8 @@ func TestToUint64(t *testing.T) {
})

assertUint64Error(t, []caseUint64[float64]{
{name: "negative value", input: -1},
{name: "out of range max uint64", input: math.MaxUint64},
{name: "out of range max float32", input: math.MaxFloat32},
{name: "out of range max float64", input: math.MaxFloat64},
})
Expand Down Expand Up @@ -1412,7 +1439,8 @@ func TestToInt(t *testing.T) {
})

assertIntError(t, []caseInt[float32]{
{name: "positive out of range", input: math.MaxFloat32 + 1},
{name: "positive out of range", input: math.MaxFloat32},
{name: "negative out of range", input: -math.MaxFloat32},
})
})

Expand All @@ -1423,8 +1451,9 @@ func TestToInt(t *testing.T) {
{name: "positive within range", input: 10000.9, want: 10000},
})

assertIntError(t, []caseInt[float32]{
{name: "positive out of range", input: math.MaxFloat32 + 1},
assertIntError(t, []caseInt[float64]{
{name: "positive out of range", input: math.MaxFloat32},
{name: "negative out of range", input: -math.MaxFloat32},
})
})
}
Expand Down Expand Up @@ -1558,7 +1587,9 @@ func TestToUint(t *testing.T) {
})

assertUint64Error(t, []caseUint64[float32]{
{name: "out of range", input: math.MaxFloat32},
{name: "negative value", input: -1},
{name: "out of range max uint64", input: math.MaxUint64},
{name: "out of range max float32", input: math.MaxFloat32},
})
})

Expand All @@ -1570,6 +1601,8 @@ func TestToUint(t *testing.T) {
})

assertUintError(t, []caseUint[float64]{
{name: "negative value", input: -1},
{name: "out of range max uint64", input: math.MaxUint64},
{name: "out of range max float32", input: math.MaxFloat32},
{name: "out of range max float64", input: math.MaxFloat64},
})
Expand Down

0 comments on commit 5b02fc9

Please sign in to comment.