From acb4d72429c07c64bdaa7b400f767e7e4f502254 Mon Sep 17 00:00:00 2001 From: ccoVeille <3875889+ccoVeille@users.noreply.github.com> Date: Wed, 18 Sep 2024 17:40:05 +0200 Subject: [PATCH 1/3] chore: refactor code - remove assertNotNegative - use checkLowerBoundary --- asserters.go | 11 ----------- conversion.go | 10 +++++----- 2 files changed, 5 insertions(+), 16 deletions(-) diff --git a/asserters.go b/asserters.go index 60aa9dd..57993b5 100644 --- a/asserters.go +++ b/asserters.go @@ -1,16 +1,5 @@ package safecast -func assertNotNegative[T Type, T2 Type](i T, zero T2) error { - if i < 0 { - return Error{ - err: ErrExceedMinimumValue, - value: i, - boundary: zero, - } - } - return nil -} - func checkUpperBoundary[T Type, T2 Type](value T, boundary T2) error { if value <= 0 { return nil diff --git a/conversion.go b/conversion.go index 51c9d70..4b16570 100644 --- a/conversion.go +++ b/conversion.go @@ -26,7 +26,7 @@ func ToInt[T Type](i T) (int, error) { // If the conversion results in a value outside the range of an uint, // an [ErrConversionIssue] error is returned. func ToUint[T Type](i T) (uint, error) { - if err := assertNotNegative(i, uint(0)); err != nil { + if err := checkLowerBoundary(i, uint(0)); err != nil { return 0, err } @@ -56,7 +56,7 @@ func ToInt8[T Type](i T) (int8, error) { // If the conversion results in a value outside the range of an uint8, // an [ErrConversionIssue] error is returned. func ToUint8[T Type](i T) (uint8, error) { - if err := assertNotNegative(i, uint8(0)); err != nil { + if err := checkLowerBoundary(i, uint8(0)); err != nil { return 0, err } @@ -86,7 +86,7 @@ func ToInt16[T Type](i T) (int16, error) { // If the conversion results in a value outside the range of an uint16, // an [ErrConversionIssue] error is returned. func ToUint16[T Type](i T) (uint16, error) { - if err := assertNotNegative(i, uint16(0)); err != nil { + if err := checkLowerBoundary(i, uint16(0)); err != nil { return 0, err } @@ -116,7 +116,7 @@ func ToInt32[T Type](i T) (int32, error) { // If the conversion results in a value outside the range of an uint32, // an [ErrConversionIssue] error is returned. func ToUint32[T Type](i T) (uint32, error) { - if err := assertNotNegative(i, uint32(0)); err != nil { + if err := checkLowerBoundary(i, uint32(0)); err != nil { return 0, err } @@ -142,7 +142,7 @@ func ToInt64[T Type](i T) (int64, error) { // If the conversion results in a value outside the range of an uint64, // an [ErrConversionIssue] error is returned. func ToUint64[T Type](i T) (uint64, error) { - if err := assertNotNegative(i, uint64(0)); err != nil { + if err := checkLowerBoundary(i, uint64(0)); err != nil { return 0, err } From 8762f1cf2f9eb1da07e02583d6dc70a09aa180cf Mon Sep 17 00:00:00 2001 From: ccoVeille <3875889+ccoVeille@users.noreply.github.com> Date: Wed, 18 Sep 2024 17:41:52 +0200 Subject: [PATCH 2/3] fix: missing check on lower boundary for Int64 --- conversion.go | 4 ++++ conversion_test.go | 41 ++++++++++++++++++++++++++++++++--------- 2 files changed, 36 insertions(+), 9 deletions(-) diff --git a/conversion.go b/conversion.go index 4b16570..51e01a8 100644 --- a/conversion.go +++ b/conversion.go @@ -131,6 +131,10 @@ func ToUint32[T Type](i T) (uint32, error) { // If the conversion results in a value outside the range of an int64, // an [ErrConversionIssue] error is returned. func ToInt64[T Type](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 } diff --git a/conversion_test.go b/conversion_test.go index 97e5f0e..1c987ce 100644 --- a/conversion_test.go +++ b/conversion_test.go @@ -1154,11 +1154,18 @@ func TestToInt64(t *testing.T) { {name: "zero", input: 0.0, want: 0}, {name: "rounded value", input: 1.1, want: 1}, {name: "positive within range", input: 10000.9, want: 10000}, - {name: "big value", input: math.MaxInt16, want: math.MaxInt16}, + {name: "max int16", input: math.MaxInt16, want: math.MaxInt16}, + {name: "min int16", input: math.MinInt16, want: math.MinInt16}, + {name: "max int32", input: math.MaxInt32, want: 2147483648}, // number differs due to float imprecision + {name: "min int32", input: math.MinInt32, want: math.MinInt32}, }) assertInt64Error(t, []caseInt64[float32]{ - {name: "positive out of range", input: math.MaxFloat32}, + {name: "out of range math.MaxInt64 + 1", input: math.MaxInt64 + 1}, + {name: "out of range math.MaxUint64", input: math.MaxUint64}, + {name: "out of range math.MinInt64", input: math.MinInt64}, + {name: "out of range math.MaxFloat32", input: math.MaxFloat32}, + {name: "out of range -math.MaxFloat32", input: -math.MaxFloat32}, }) }) @@ -1167,11 +1174,20 @@ func TestToInt64(t *testing.T) { {name: "zero", input: 0.0, want: 0}, {name: "rounded value", input: 1.1, want: 1}, {name: "positive within range", input: 10000.9, want: 10000}, - {name: "big value", input: math.MaxInt32, want: math.MaxInt32}, + {name: "max int16", input: math.MaxInt16, want: math.MaxInt16}, + {name: "min int16", input: math.MinInt16, want: math.MinInt16}, + {name: "max int32", input: math.MaxInt32, want: math.MaxInt32}, + {name: "min int32", input: math.MinInt32, want: math.MinInt32}, }) assertInt64Error(t, []caseInt64[float64]{ - {name: "positive out of range", input: math.MaxInt64}, + {name: "out of range math.MaxInt64 + 1", input: math.MaxInt64 + 1}, + {name: "out of range math.MaxUint64", input: math.MaxUint64}, + {name: "out of range math.MinInt64", input: math.MinInt64}, + {name: "out of range math.MaxFloat32", input: math.MaxFloat32}, + {name: "out of range -math.MaxFloat32", input: -math.MaxFloat32}, + {name: "out of range math.MaxFloat64", input: math.MaxFloat64}, + {name: "out of range -math.MaxFloat64", input: -math.MaxFloat64}, }) }) } @@ -1307,7 +1323,6 @@ 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}, }) }) @@ -1449,8 +1464,11 @@ func TestToInt(t *testing.T) { }) assertIntError(t, []caseInt[float32]{ - {name: "positive out of range", input: math.MaxFloat32}, - {name: "negative out of range", input: -math.MaxFloat32}, + {name: "out of range math.MaxInt64 + 1", input: math.MaxInt64 + 1}, + {name: "out of range math.MaxUint64", input: math.MaxUint64}, + {name: "out of range math.MinInt64", input: math.MinInt64}, + {name: "out of range math.MaxFloat32", input: math.MaxFloat32}, + {name: "out of range -math.MaxFloat32", input: -math.MaxFloat32}, }) }) @@ -1462,8 +1480,13 @@ func TestToInt(t *testing.T) { }) assertIntError(t, []caseInt[float64]{ - {name: "positive out of range", input: math.MaxFloat32}, - {name: "negative out of range", input: -math.MaxFloat32}, + {name: "out of range math.MaxInt64 + 1", input: math.MaxInt64 + 1}, + {name: "out of range math.MaxUint64", input: math.MaxUint64}, + {name: "out of range math.MinInt64", input: math.MinInt64}, + {name: "out of range math.MaxFloat32", input: math.MaxFloat32}, + {name: "out of range -math.MaxFloat32", input: -math.MaxFloat32}, + {name: "out of range math.MaxFloat64", input: math.MaxFloat64}, + {name: "out of range -math.MaxFloat64", input: -math.MaxFloat64}, }) }) } From f8ed6ac96724e43c352c361122ca18e6807c3466 Mon Sep 17 00:00:00 2001 From: ccoVeille <3875889+ccoVeille@users.noreply.github.com> Date: Wed, 18 Sep 2024 18:00:39 +0200 Subject: [PATCH 3/3] fix: underflow with min int64 with floats it doesn't underflow due to float imprecision --- asserters.go | 91 ++++++++++++++++++++++++++++++++++++---------- conversion_test.go | 9 +++-- 2 files changed, 76 insertions(+), 24 deletions(-) diff --git a/asserters.go b/asserters.go index 57993b5..95a09cd 100644 --- a/asserters.go +++ b/asserters.go @@ -5,23 +5,20 @@ func checkUpperBoundary[T Type, T2 Type](value T, boundary T2) error { return nil } - var greater bool + var overflow bool switch f := any(value).(type) { case float64: - // for float64, everything fits in float64 without overflow. - // We are using a greater or equal because float cannot be compared easily because of precision loss. - greater = f >= float64(boundary) + overflow = isFloatOverflow(f, boundary) + case float32: - // everything fits in float32, except float64 greater than math.MaxFloat32. - // So, we must convert to float64 and check. - // We are using a greater or equal because float cannot be compared easily because of precision loss. - greater = float64(f) >= float64(boundary) + overflow = isFloatOverflow(f, boundary) + default: // for all other integer types, it fits in an uint64 without overflow as we know value is positive. - greater = uint64(value) > uint64(boundary) + overflow = uint64(value) > uint64(boundary) } - if greater { + if overflow { return Error{ value: value, boundary: boundary, @@ -37,23 +34,18 @@ func checkLowerBoundary[T Type, T2 Type](value T, boundary T2) error { return nil } - var smaller bool + var underflow bool switch f := any(value).(type) { case float64: - // everything fits in float64 without overflow. - // We are using a lower or equal because float cannot be compared easily because of precision loss. - smaller = f <= float64(boundary) + underflow = isFloatUnderOverflow(f, boundary) case float32: - // everything fits in float32, except float64 smaller than -math.MaxFloat32. - // So, we must convert to float64 and check. - // We are using a lower or equal because float cannot be compared easily because of precision loss. - smaller = float64(f) <= float64(boundary) + underflow = isFloatUnderOverflow(f, boundary) default: // for all other integer types, it fits in an int64 without overflow as we know value is negative. - smaller = int64(value) < int64(boundary) + underflow = int64(value) < int64(boundary) } - if smaller { + if underflow { return Error{ value: value, boundary: boundary, @@ -63,3 +55,62 @@ func checkLowerBoundary[T Type, T2 Type](value T, boundary T2) error { return nil } + +func isFloatOverflow[T Type, T2 Type](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 + } + + 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 +} + +func isFloatUnderOverflow[T Type, T2 Type](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 + } + + 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 +} diff --git a/conversion_test.go b/conversion_test.go index 1c987ce..fc3a4e5 100644 --- a/conversion_test.go +++ b/conversion_test.go @@ -1163,7 +1163,7 @@ func TestToInt64(t *testing.T) { assertInt64Error(t, []caseInt64[float32]{ {name: "out of range math.MaxInt64 + 1", input: math.MaxInt64 + 1}, {name: "out of range math.MaxUint64", input: math.MaxUint64}, - {name: "out of range math.MinInt64", input: math.MinInt64}, + {name: "out of range math.MinInt64", input: math.MinInt64 * 1.02}, // because of float imprecision, we have to exceed the min int64 to trigger the error {name: "out of range math.MaxFloat32", input: math.MaxFloat32}, {name: "out of range -math.MaxFloat32", input: -math.MaxFloat32}, }) @@ -1183,7 +1183,7 @@ func TestToInt64(t *testing.T) { assertInt64Error(t, []caseInt64[float64]{ {name: "out of range math.MaxInt64 + 1", input: math.MaxInt64 + 1}, {name: "out of range math.MaxUint64", input: math.MaxUint64}, - {name: "out of range math.MinInt64", input: math.MinInt64}, + {name: "out of range math.MinInt64", input: math.MinInt64 * 1.02}, // because of float imprecision, we have to exceed the min int64 to trigger the error {name: "out of range math.MaxFloat32", input: math.MaxFloat32}, {name: "out of range -math.MaxFloat32", input: -math.MaxFloat32}, {name: "out of range math.MaxFloat64", input: math.MaxFloat64}, @@ -1466,7 +1466,7 @@ func TestToInt(t *testing.T) { assertIntError(t, []caseInt[float32]{ {name: "out of range math.MaxInt64 + 1", input: math.MaxInt64 + 1}, {name: "out of range math.MaxUint64", input: math.MaxUint64}, - {name: "out of range math.MinInt64", input: math.MinInt64}, + {name: "out of range math.MinInt64", input: math.MinInt64 * 1.02}, // because of float imprecision, we have to exceed the min int64 to trigger the error {name: "out of range math.MaxFloat32", input: math.MaxFloat32}, {name: "out of range -math.MaxFloat32", input: -math.MaxFloat32}, }) @@ -1477,12 +1477,13 @@ func TestToInt(t *testing.T) { {name: "zero", input: 0.0, want: 0}, {name: "rounded value", input: 1.1, want: 1}, {name: "positive within range", input: 10000.9, want: 10000}, + {name: "math.MinInt64", input: math.MinInt64, want: math.MinInt64}, // pass because of float imprecision }) assertIntError(t, []caseInt[float64]{ {name: "out of range math.MaxInt64 + 1", input: math.MaxInt64 + 1}, {name: "out of range math.MaxUint64", input: math.MaxUint64}, - {name: "out of range math.MinInt64", input: math.MinInt64}, + {name: "out of range math.MinInt64", input: math.MinInt64 * 1.02}, // because of float imprecision, we have to exceed the min int64 to trigger the error {name: "out of range math.MaxFloat32", input: math.MaxFloat32}, {name: "out of range -math.MaxFloat32", input: -math.MaxFloat32}, {name: "out of range math.MaxFloat64", input: math.MaxFloat64},