Skip to content

Commit

Permalink
chore: simplify code
Browse files Browse the repository at this point in the history
One code to rule them all.

This code is pretty similar to the one @ldemailly provided for
https://github.com/fortio/safecast

We worked at the same time on the same code, we came to distinct solutions.
His one was better and simpler than mine, so now I'm simply switching to
something highly inspired from his code.

Co-Authored-By: Laurent Demailly <ldemailly@gmail.com>
  • Loading branch information
ccoVeille and ldemailly committed Dec 14, 2024
1 parent 81da728 commit cbcae8f
Show file tree
Hide file tree
Showing 2 changed files with 103 additions and 193 deletions.
155 changes: 53 additions & 102 deletions asserters.go
Original file line number Diff line number Diff line change
@@ -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
}
141 changes: 50 additions & 91 deletions conversion.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}

0 comments on commit cbcae8f

Please sign in to comment.