Skip to content

Commit

Permalink
feat: add conversion to float32 and float64
Browse files Browse the repository at this point in the history
The only uses case that can overflow is when a float64 is stored in a float32
  • Loading branch information
ccoVeille committed Dec 14, 2024
1 parent cbcae8f commit ba4dc13
Show file tree
Hide file tree
Showing 5 changed files with 304 additions and 2 deletions.
8 changes: 8 additions & 0 deletions asserters.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,10 @@ func sameSign[T1, T2 Number](a T1, b T2) bool {
func getUpperBoundary(value any) any {
var upper any = math.Inf(1)
switch value.(type) {
case float64:
upper = float64(math.MaxFloat64)
case float32:
upper = float32(math.MaxFloat32)
case int8:
upper = int8(math.MaxInt8)
case int16:
Expand Down Expand Up @@ -41,6 +45,10 @@ func getUpperBoundary(value any) any {
func getLowerBoundary(value any) any {
var lower any = math.Inf(-1)
switch value.(type) {
case float64:
lower = float64(-math.MaxFloat64)
case float32:
lower = float32(-math.MaxFloat32)
case int64:
lower = int64(math.MinInt64)
case int32:
Expand Down
48 changes: 48 additions & 0 deletions asserters_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -378,3 +378,51 @@ func assertUintError[in safecast.Number](t *testing.T, tests []caseUint[in]) {
})
}
}

type caseFloat32[in safecast.Number] struct {
name string
input in
want float32
}

func assertFloat32OK[in safecast.Number](t *testing.T, tests []caseFloat32[in]) {
t.Helper()

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := safecast.ToFloat32(tt.input)
assertNoError(t, err)
assertEqual(t, tt.want, got)
})
}
}

func assertFloat32Error[in safecast.Number](t *testing.T, tests []caseFloat32[in]) {
t.Helper()

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := safecast.ToFloat32(tt.input)
requireErrorIs(t, err, safecast.ErrConversionIssue)
assertEqual(t, tt.want, got)
})
}
}

type caseFloat64[in safecast.Number] struct {
name string
input in
want float64
}

func assertFloat64OK[in safecast.Number](t *testing.T, tests []caseFloat64[in]) {
t.Helper()

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := safecast.ToFloat64(tt.input)
assertNoError(t, err)
assertEqual(t, tt.want, got)
})
}
}
29 changes: 27 additions & 2 deletions conversion.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,19 @@ func convertFromNumber[NumOut Number, NumIn Number](orig NumIn) (converted NumOu
base = NumIn(math.Trunc(float64(f)))
}

if NumIn(converted) == base {
return converted, nil
cast := NumIn(converted)

switch any(converted).(type) {
case float64, float32:
// we have to compare with a tolerance because of floating point inaccuracy
if math.Abs(float64(cast)-float64(orig)) <= 1e-3 {
return converted, nil
}
default:
// exact match
if cast == base {
return converted, nil
}
}

return 0, Error{
Expand Down Expand Up @@ -115,3 +126,17 @@ func ToInt64[T Number](i T) (int64, error) {
func ToUint64[T Number](i T) (uint64, error) {
return convertFromNumber[uint64](i)
}

// ToFloat32 attempts to convert any [Number] value to a float32.
// If the conversion results in a value outside the range of a float32,
// an [ErrConversionIssue] error is returned.
func ToFloat32[T Number](i T) (float32, error) {
return convertFromNumber[float32](i)
}

// ToFloat64 attempts to convert any [Number] value to a float64.
// If the conversion results in a value outside the range of a float64,
// an [ErrConversionIssue] error is returned.
func ToFloat64[T Number](i T) (float64, error) {
return convertFromNumber[float64](i)
}
193 changes: 193 additions & 0 deletions conversion_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1280,3 +1280,196 @@ func TestToUint(t *testing.T) {
})
})
}

func TestToFloat32(t *testing.T) {
t.Run("from int", func(t *testing.T) {
assertFloat32OK(t, []caseFloat32[int]{
{name: "zero", input: 0, want: 0.0},
{name: "negative within range", input: -100, want: -100.0},
{name: "positive within range", input: 100, want: 100.0},
})
})

t.Run("from int8", func(t *testing.T) {
assertFloat32OK(t, []caseFloat32[int8]{
{name: "zero", input: 0, want: 0.0},
{name: "negative within range", input: -100, want: -100.0},
{name: "positive within range", input: 100, want: 100.0},
})
})

t.Run("from int16", func(t *testing.T) {
assertFloat32OK(t, []caseFloat32[int16]{
{name: "zero", input: 0, want: 0.0},
{name: "negative within range", input: -100, want: -100.0},
{name: "positive within range", input: 100, want: 100.0},
})
})

t.Run("from int32", func(t *testing.T) {
assertFloat32OK(t, []caseFloat32[int32]{
{name: "zero", input: 0, want: 0.0},
{name: "negative within range", input: -100, want: -100.0},
{name: "positive within range", input: 100, want: 100.0},
})
})

t.Run("from int64", func(t *testing.T) {
assertFloat32OK(t, []caseFloat32[int64]{
{name: "zero", input: 0, want: 0.0},
{name: "negative within range", input: -100, want: -100.0},
{name: "positive within range", input: 100, want: 100.0},
})
})

t.Run("from uint", func(t *testing.T) {
assertFloat32OK(t, []caseFloat32[uint]{
{name: "zero", input: 0, want: 0.0},
{name: "positive within range", input: 100, want: 100.0},
})
})

t.Run("from uint8", func(t *testing.T) {
assertFloat32OK(t, []caseFloat32[uint8]{
{name: "zero", input: 0, want: 0.0},
{name: "positive within range", input: 100, want: 100.0},
})
})

t.Run("from uint16", func(t *testing.T) {
assertFloat32OK(t, []caseFloat32[uint16]{
{name: "zero", input: 0, want: 0.0},
{name: "positive within range", input: 100, want: 100.0},
})
})

t.Run("from uint32", func(t *testing.T) {
assertFloat32OK(t, []caseFloat32[uint32]{
{name: "zero", input: 0, want: 0.0},
{name: "positive within range", input: 100, want: 100.0},
})
})

t.Run("from uint64", func(t *testing.T) {
assertFloat32OK(t, []caseFloat32[uint64]{
{name: "zero", input: 0, want: 0.0},
{name: "positive within range", input: 100, want: 100.0},
})
})

t.Run("from float32", func(t *testing.T) {
assertFloat32OK(t, []caseFloat32[float32]{
{name: "zero", input: 0.0, want: 0.0},
{name: "rounded value", input: 1.1, want: 1.1},
{name: "negative within range", input: -100.9, want: -100.9},
{name: "positive within range", input: 100.9, want: 100.9},
})
})

t.Run("from float64", func(t *testing.T) {
assertFloat32OK(t, []caseFloat32[float64]{
{name: "zero", input: 0.0, want: 0.0},
{name: "negative within range", input: -100.9, want: -100.9},
{name: "positive within range", input: 100.0, want: 100.0},
})

assertFloat32Error(t, []caseFloat32[float64]{
{name: "out of range max float32", input: math.MaxFloat32 * 1.02}, // because of float imprecision, we have to exceed the min int64 to trigger the error
{name: "out of range min float32", input: -math.MaxFloat32 * 1.02}, // because of float imprecision, we have to exceed the min int64 to trigger the error
})
})
}

func TestToFloat64(t *testing.T) {
t.Run("from int", func(t *testing.T) {
assertFloat64OK(t, []caseFloat64[int]{
{name: "zero", input: 0, want: 0.0},
{name: "negative within range", input: -100, want: -100.0},
{name: "positive within range", input: 100, want: 100.0},
})
})

t.Run("from int8", func(t *testing.T) {
assertFloat64OK(t, []caseFloat64[int8]{
{name: "zero", input: 0, want: 0.0},
{name: "negative within range", input: -100, want: -100.0},
{name: "positive within range", input: 100, want: 100.0},
})
})

t.Run("from int16", func(t *testing.T) {
assertFloat64OK(t, []caseFloat64[int16]{
{name: "zero", input: 0, want: 0.0},
{name: "negative within range", input: -100, want: -100.0},
{name: "positive within range", input: 100, want: 100.0},
})
})

t.Run("from int32", func(t *testing.T) {
assertFloat64OK(t, []caseFloat64[int32]{
{name: "zero", input: 0, want: 0.0},
{name: "negative within range", input: -100, want: -100.0},
{name: "positive within range", input: 100, want: 100.0},
})
})

t.Run("from int64", func(t *testing.T) {
assertFloat64OK(t, []caseFloat64[int64]{
{name: "zero", input: 0, want: 0.0},
{name: "negative within range", input: -100, want: -100.0},
{name: "positive within range", input: 100, want: 100.0},
})
})

t.Run("from uint", func(t *testing.T) {
assertFloat64OK(t, []caseFloat64[uint]{
{name: "zero", input: 0, want: 0.0},
{name: "positive within range", input: 100, want: 100.0},
})
})

t.Run("from uint8", func(t *testing.T) {
assertFloat64OK(t, []caseFloat64[uint8]{
{name: "zero", input: 0, want: 0.0},
{name: "positive within range", input: 100, want: 100.0},
})
})

t.Run("from uint16", func(t *testing.T) {
assertFloat64OK(t, []caseFloat64[uint16]{
{name: "zero", input: 0, want: 0.0},
{name: "positive within range", input: 100, want: 100.0},
})
})

t.Run("from uint32", func(t *testing.T) {
assertFloat64OK(t, []caseFloat64[uint32]{
{name: "zero", input: 0, want: 0.0},
{name: "positive within range", input: 100, want: 100.0},
})
})

t.Run("from uint64", func(t *testing.T) {
assertFloat64OK(t, []caseFloat64[uint64]{
{name: "zero", input: 0, want: 0.0},
{name: "positive within range", input: 100, want: 100.0},
})
})

t.Run("from float32", func(t *testing.T) {
assertFloat64OK(t, []caseFloat64[float64]{
{name: "zero", input: 0.0, want: 0.0},
{name: "rounded value", input: 1.1, want: 1.1},
{name: "negative within range", input: -100.9, want: -100.9},
{name: "positive within range", input: 100.9, want: 100.9},
})
})

t.Run("from float64", func(t *testing.T) {
assertFloat64OK(t, []caseFloat64[float64]{
{name: "zero", input: 0.0, want: 0.0},
{name: "negative within range", input: -100.9, want: -100.9},
{name: "positive within range", input: 100.0, want: 100.0},
})
})
}
28 changes: 28 additions & 0 deletions examples_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -141,3 +141,31 @@ func ExampleToUint() {
// 42 <nil>
// 0 conversion issue: -1 (int8) is less than 0 (uint): minimum value for this type exceeded
}

func ExampleToFloat32() {
a := int8(42)
i, err := safecast.ToFloat32(a)
fmt.Println(i, err)

b := math.MaxFloat64
i, err = safecast.ToFloat32(b)
fmt.Println(i, err)

// Output:
// 42 <nil>
// 0 conversion issue: 1.7976931348623157e+308 (float64) is greater than 3.4028235e+38 (float32): maximum value for this type exceeded
}

func ExampleToFloat64() {
a := int8(42)
i, err := safecast.ToFloat64(a)
fmt.Println(i, err)

b := math.MaxFloat64
i, err = safecast.ToFloat64(b)
fmt.Println(i, err)

// Output:
// 42 <nil>
// 1.7976931348623157e+308 <nil>
}

0 comments on commit ba4dc13

Please sign in to comment.