diff --git a/asserters.go b/asserters.go index 7df788e..678f54e 100644 --- a/asserters.go +++ b/asserters.go @@ -1,116 +1,67 @@ package safecast -func checkUpperBoundary[T Number, T2 Number](value T, boundary T2) error { - if value <= 0 { - return nil - } - - var overflow bool - switch f := any(value).(type) { - case float64: - overflow = isFloatOverflow(f, boundary) - - case float32: - overflow = isFloatOverflow(f, boundary) - - default: - // for all other integer types, it fits in an uint64 without overflow as we know value is positive. - overflow = uint64(value) > uint64(boundary) - } - - if overflow { - return Error{ - value: value, - boundary: boundary, - err: ErrExceedMaximumValue, - } - } +import "math" - return nil +func negative[T Number](t T) bool { + return t < 0 } -func checkLowerBoundary[T Number, T2 Number](value T, boundary T2) error { - if value >= 0 { - return nil - } - - var underflow bool - switch f := any(value).(type) { - case float64: - underflow = isFloatUnderOverflow(f, boundary) - case float32: - underflow = isFloatUnderOverflow(f, boundary) - default: - // for all other integer types, it fits in an int64 without overflow as we know value is negative. - underflow = int64(value) < int64(boundary) - } - - if underflow { - return Error{ - value: value, - boundary: boundary, - err: ErrExceedMinimumValue, - } - } - - return nil +func sameSign[T1, T2 Number](a T1, b T2) bool { + return negative(a) == negative(b) } -func isFloatOverflow[T Number, T2 Number](value T, boundary T2) bool { - // boundary is positive when checking for an overflow - - // everything fits in float64 without overflow. - v := float64(value) - b := float64(boundary) - - if v > b*1.01 { - // way greater than the maximum value - return true +func getUpperBoundary(value any) any { + var upper any = math.Inf(1) + switch value.(type) { + case int8: + upper = int8(math.MaxInt8) + case int16: + upper = int16(math.MaxInt16) + case int32: + upper = int32(math.MaxInt32) + case int64: + upper = int64(math.MaxInt64) + case int: + upper = int(math.MaxInt) + case uint8: + upper = uint8(math.MaxUint8) + case uint32: + upper = uint32(math.MaxUint32) + case uint16: + upper = uint16(math.MaxUint16) + case uint64: + upper = uint64(math.MaxUint64) + case uint: + upper = uint(math.MaxUint) } - if v < b*0.99 { - // we are way below the maximum value - return false - } - // we are close to the maximum value - - // let's try to create the overflow - // by converting back and forth with type juggling - conv := float64(T(T2(v))) - - // the number was between 0.99 and 1.01 of the maximum value - // once converted back and forth, we need to check if the value is in the same range - // if not, so it's an overflow - return conv <= b*0.99 + return upper } -func isFloatUnderOverflow[T Number, T2 Number](value T, boundary T2) bool { - // everything fits in float64 without overflow. - v := float64(value) - b := float64(boundary) - - if b == 0 { - // boundary is 0 - // we can check easily - return value < 0 - } - - if v < b*1.01 { // please note value and boundary are negative here - // way below than the minimum value, it would underflow - return true +func getLowerBoundary(value any) any { + var lower any = math.Inf(-1) + switch value.(type) { + case int64: + lower = int64(math.MinInt64) + case int32: + lower = int32(math.MinInt32) + case int16: + lower = int16(math.MinInt16) + case int8: + lower = int8(math.MinInt8) + case int: + lower = int(math.MinInt) + case uint: + lower = uint(0) + case uint8: + lower = uint8(0) + case uint16: + lower = uint16(0) + case uint32: + lower = uint32(0) + case uint64: + lower = uint64(0) } - if v > b*0.99 { // please note value and boundary are negative here - // way greater than the minimum value - return false - } - - // we are just above to the minimum value - // let's try to create the underflow - conv := float64(T(T2(v))) - - // the number was between 0.99 and 1.01 of the minimum value - // once converted back and forth, we need to check if the value is in the same range - // if not, so it's an underflow - return conv >= b*0.99 + return lower } diff --git a/conversion.go b/conversion.go index 52fd476..910c6fc 100644 --- a/conversion.go +++ b/conversion.go @@ -5,154 +5,113 @@ package safecast -import "math" +import ( + "math" +) -// ToInt attempts to convert any [Number] 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) { - if err := checkUpperBoundary(i, int(math.MaxInt)); err != nil { - return 0, err +func convertFromNumber[NumOut Number, NumIn Number](orig NumIn) (converted NumOut, err error) { + converted = NumOut(orig) + + errBoundary := ErrExceedMaximumValue + boundary := getUpperBoundary(converted) + if negative(orig) { + errBoundary = ErrExceedMinimumValue + boundary = getLowerBoundary(converted) + } + + if !sameSign(orig, converted) { + return 0, Error{ + value: orig, + err: errBoundary, + boundary: boundary, + } } - if err := checkLowerBoundary(i, int(math.MinInt)); err != nil { - return 0, err + base := orig + switch f := any(orig).(type) { + case float64: + base = NumIn(math.Trunc(f)) + case float32: + base = NumIn(math.Trunc(float64(f))) } - return int(i), nil + if NumIn(converted) == base { + return converted, nil + } + + return 0, Error{ + value: orig, + err: errBoundary, + boundary: boundary, + } +} + +// ToInt attempts to convert any [Number] 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) { + return convertFromNumber[int](i) } // ToUint attempts to convert any [Number] value to an uint. // If the conversion results in a value outside the range of an uint, // an [ErrConversionIssue] error is returned. func ToUint[T Number](i T) (uint, error) { - if err := checkLowerBoundary(i, uint(0)); err != nil { - return 0, err - } - - if err := checkUpperBoundary(i, uint(math.MaxUint)); err != nil { - return 0, err - } - - return uint(i), nil + return convertFromNumber[uint](i) } // ToInt8 attempts to convert any [Number] value to an int8. // If the conversion results in a value outside the range of an int8, // an [ErrConversionIssue] error is returned. func ToInt8[T Number](i T) (int8, error) { - if err := checkUpperBoundary(i, int8(math.MaxInt8)); err != nil { - return 0, err - } - - if err := checkLowerBoundary(i, int8(math.MinInt8)); err != nil { - return 0, err - } - - return int8(i), nil + return convertFromNumber[int8](i) } // ToUint8 attempts to convert any [Number] value to an uint8. // If the conversion results in a value outside the range of an uint8, // an [ErrConversionIssue] error is returned. func ToUint8[T Number](i T) (uint8, error) { - if err := checkLowerBoundary(i, uint8(0)); err != nil { - return 0, err - } - - if err := checkUpperBoundary(i, uint8(math.MaxUint8)); err != nil { - return 0, err - } - - return uint8(i), nil + return convertFromNumber[uint8](i) } // ToInt16 attempts to convert any [Number] value to an int16. // If the conversion results in a value outside the range of an int16, // an [ErrConversionIssue] error is returned. func ToInt16[T Number](i T) (int16, error) { - if err := checkUpperBoundary(i, int16(math.MaxInt16)); err != nil { - return 0, err - } - - if err := checkLowerBoundary(i, int16(math.MinInt16)); err != nil { - return 0, err - } - - return int16(i), nil + return convertFromNumber[int16](i) } // ToUint16 attempts to convert any [Number] value to an uint16. // If the conversion results in a value outside the range of an uint16, // an [ErrConversionIssue] error is returned. func ToUint16[T Number](i T) (uint16, error) { - if err := checkLowerBoundary(i, uint16(0)); err != nil { - return 0, err - } - - if err := checkUpperBoundary(i, uint16(math.MaxUint16)); err != nil { - return 0, err - } - - return uint16(i), nil + return convertFromNumber[uint16](i) } // ToInt32 attempts to convert any [Number] value to an int32. // If the conversion results in a value outside the range of an int32, // an [ErrConversionIssue] error is returned. func ToInt32[T Number](i T) (int32, error) { - if err := checkUpperBoundary(i, int32(math.MaxInt32)); err != nil { - return 0, err - } - - if err := checkLowerBoundary(i, int32(math.MinInt32)); err != nil { - return 0, err - } - - return int32(i), nil + return convertFromNumber[int32](i) } // ToUint32 attempts to convert any [Number] value to an uint32. // If the conversion results in a value outside the range of an uint32, // an [ErrConversionIssue] error is returned. func ToUint32[T Number](i T) (uint32, error) { - if err := checkLowerBoundary(i, uint32(0)); err != nil { - return 0, err - } - - if err := checkUpperBoundary(i, uint32(math.MaxUint32)); err != nil { - return 0, err - } - - return uint32(i), nil + return convertFromNumber[uint32](i) } // ToInt64 attempts to convert any [Number] value to an int64. // If the conversion results in a value outside the range of an int64, // an [ErrConversionIssue] error is returned. func ToInt64[T Number](i T) (int64, error) { - if err := checkLowerBoundary(i, int64(math.MinInt64)); err != nil { - return 0, err - } - - if err := checkUpperBoundary(i, int64(math.MaxInt64)); err != nil { - return 0, err - } - - return int64(i), nil + return convertFromNumber[int64](i) } // ToUint64 attempts to convert any [Number] value to an uint64. // If the conversion results in a value outside the range of an uint64, // an [ErrConversionIssue] error is returned. func ToUint64[T Number](i T) (uint64, error) { - if err := checkLowerBoundary(i, uint64(0)); err != nil { - return 0, err - } - - if err := checkUpperBoundary(i, uint64(math.MaxUint64)); err != nil { - return 0, err - } - - return uint64(i), nil + return convertFromNumber[uint64](i) }