Skip to content

Commit

Permalink
Merge pull request #3 from lunemec/go_generics
Browse files Browse the repository at this point in the history
Add Go generics support.
  • Loading branch information
lunemec authored Nov 3, 2022
2 parents ff579f9 + 0c94cb0 commit 92a7690
Show file tree
Hide file tree
Showing 38 changed files with 428 additions and 1 deletion.
52 changes: 52 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,58 @@ func main() {
}
```

## Example using generics
```go
package main

import (
"fmt"

"github.com/lunemec/as"
)

func main() {
for _, n := range []int{127, 128} {
num, err := as.T[int8](n)
if err != nil {
fmt.Printf("Input invalid: %d, err: %s\n", num, err)
} else {
fmt.Printf("Input valid: %d\n", num)
}
}
// Output: Input valid: 127
// Input invalid: -128, err: 128 (int) overflows int8
}
```

## Example slices type conversion with overflow check
```go
package main

import (
"fmt"

"github.com/lunemec/as"
)

func main() {
out, err := as.SliceT[int, int8]([]int{127, 128})
fmt.Printf("Output: %+v, error: %+v\n", out, err)
// Output: Output: [127 0], error: 1 error occurred:
// * at index [1]: 128 (int) overflows int8
}
```

## Other considerations
Note this checking is not free, check benchmarks for each checked cast.

There are several [Go proposals](https://github.com/golang/go/issues/30613) to have this functionality in the language
but many of them even predate this library.


There are also [libraries](https://github.com/JohnCGriffin/overflow) that
allow for math operations while checking for overflow at the output.

## Architecture support
`As` currently supports these architectures and correctly handles overflows for 64/32 bit numbers:
* 386
Expand Down
65 changes: 65 additions & 0 deletions as.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ package as
import (
"fmt"
"reflect"

"golang.org/x/exp/constraints"
)

// InvalidTypeError is returned when user supplied invalid type to convert.
Expand All @@ -26,6 +28,69 @@ func (e OverflowError) Error() string {
return fmt.Sprintf("%d (%T) overflows %s", e.Value, e.Value, e.ToType)
}

// Number constraint is used for type cast into any number.
// Only Integers for now until float support is added.
type Number interface {
constraints.Integer
}

// T is generic function to allow for easier checked
// type cast from any type to any Number type.
func T[To Number](v any) (To, error) {
var (
err error
out To
)
switch any(out).(type) {
case int:
var n int
n, err = Int(v)
out = To(n)
case int8:
var n int8
n, err = Int8(v)
out = To(n)
case int16:
var n int16
n, err = Int16(v)
out = To(n)
case int32:
var n int32
n, err = Int32(v)
out = To(n)
case int64:
var n int64
n, err = Int64(v)
out = To(n)
case uint:
var n uint
n, err = Uint(v)
out = To(n)
case uint8:
var n uint8
n, err = Uint8(v)
out = To(n)
case uint16:
var n uint16
n, err = Uint16(v)
out = To(n)
case uint32:
var n uint32
n, err = Uint32(v)
out = To(n)
case uint64:
var n uint64
n, err = Uint64(v)
out = To(n)
default:
return out, InvalidTypeError{
ToType: fmt.Sprintf("%T", out),
Value: v,
}
}
return To(out), err
}

// From html/template/content.go
// Copyright 2011 The Go Authors. All rights reserved.
// indirect returns the value, after dereferencing as many times
Expand Down
16 changes: 16 additions & 0 deletions as_32b_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
//go:build 386 || arm
// +build 386 arm

package as_test

import (
"testing"
)

func TestT32b(t *testing.T) {
assertNoError(t, as.T[int], int64(math.MaxInt32))
assertNoError(t, as.T[int], int64(math.MinInt32))
assertError(t, as.T[int], int64(math.MaxInt64))
assertError(t, as.T[int], int64(math.MinInt64))
assertError(t, as.T[int], uint64(math.MaxUint64))
}
33 changes: 33 additions & 0 deletions as_test.go
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
package as_test

import (
"math"
"reflect"
"testing"

"github.com/lunemec/as"
)

// assertError tests if function called with given parameters raises error, if not
Expand Down Expand Up @@ -35,3 +38,33 @@ func callfn(fn interface{}, args interface{}) (reflect.Type, interface{}) {
err := outValue[1].Interface()
return outType, err
}

func TestT(t *testing.T) {
assertNoError(t, as.T[int], int64(math.MaxInt64))
assertNoError(t, as.T[int], int64(math.MinInt64))
assertError(t, as.T[int], uint64(math.MaxUint64))
// uintptr is not supported, will error
assertError(t, as.T[uintptr], uint64(math.MaxUint64))
}

var out int

// BenchmarkT-8 91144112 12.90 ns/op 8 B/op 0 allocs/op
func BenchmarkT(b *testing.B) {
var t int
for n := 0; n < b.N; n++ {
t, _ = as.T[int](n)
}

out = t
}

// BenchmarkInt-8 108703077 10.98 ns/op 8 B/op 0 allocs/op
func BenchmarkInt(b *testing.B) {
var t int
for n := 0; n < b.N; n++ {
t, _ = as.Int(n)
}

out = t
}
26 changes: 26 additions & 0 deletions example_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,3 +18,29 @@ func Example() {
// Output: Input valid: 127
// Input invalid: -128, err: 128 (int) overflows int8
}

func ExampleT() {
for _, n := range []int{127, 128} {
num, err := as.T[int8](n)
if err != nil {
fmt.Printf("Input invalid: %d, err: %s\n", num, err)
} else {
fmt.Printf("Input valid: %d\n", num)
}
}
// Output: Input valid: 127
// Input invalid: -128, err: 128 (int) overflows int8
}

func ExampleSliceT() {
out, err := as.SliceT[int, int8]([]int{127, 128})
fmt.Printf("Output: %+v, error: %+v\n", out, err)
// Output: Output: [127 0], error: 1 error occurred:
// * at index [1]: 128 (int) overflows int8
}

func ExampleSliceTUnchecked() {
out := as.SliceTUnchecked[int, int8]([]int{127, 128})
fmt.Printf("Output: %+v\n", out)
// Output: Output: [127 -128]
}
16 changes: 15 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
@@ -1,3 +1,17 @@
module github.com/lunemec/as

go 1.12
go 1.19

require (
github.com/hashicorp/go-multierror v1.1.1
github.com/pkg/errors v0.9.1
github.com/stretchr/testify v1.8.1
golang.org/x/exp v0.0.0-20221031165847-c99f073a8326
)

require (
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/hashicorp/errwrap v1.0.0 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
25 changes: 25 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/hashicorp/errwrap v1.0.0 h1:hLrqtEDnRye3+sgx6z4qVLNuviH3MR5aQ0ykNJa/UYA=
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo=
github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
golang.org/x/exp v0.0.0-20221031165847-c99f073a8326 h1:QfTh0HpN6hlw6D3vu8DAwC8pBIwikq0AI1evdm+FksE=
golang.org/x/exp v0.0.0-20221031165847-c99f073a8326/go.mod h1:CxIveKay+FTh1D0yPZemJVgC/95VzuuOLq5Qi4xnoYc=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
1 change: 1 addition & 0 deletions int16_32b_test.go
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
//go:build 386 || arm
// +build 386 arm

package as_test
Expand Down
24 changes: 24 additions & 0 deletions int16_64b_test.go
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
//go:build amd64 || arm64
// +build amd64 arm64

package as_test
Expand Down Expand Up @@ -42,3 +43,26 @@ func TestInt16(t *testing.T) {
assertNoError(t, as.Int16, uint(0))
assertError(t, as.Int16, uint(math.MaxUint64))
}

var out16 int16

// BenchmarkAs16-8 36798218 32.61 ns/op 39 B/op 1 allocs/op

func BenchmarkAs16(b *testing.B) {
var t int16
for n := 0; n < b.N; n++ {
t, _ = as.Int16(n)
}

out16 = t
}

// BenchmarkInt16-8 1000000000 0.3211 ns/op 0 B/op 0 allocs/op
func BenchmarkInt16(b *testing.B) {
var t int16
for n := 0; n < b.N; n++ {
t = int16(n)
}

out16 = t
}
1 change: 1 addition & 0 deletions int32_32b_test.go
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
//go:build 386 || arm
// +build 386 arm

package as_test
Expand Down
1 change: 1 addition & 0 deletions int32_64b_test.go
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
//go:build amd64 || arm64
// +build amd64 arm64

package as_test
Expand Down
1 change: 1 addition & 0 deletions int64_32b.go
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
//go:build 386 || arm
// +build 386 arm

package as
Expand Down
1 change: 1 addition & 0 deletions int64_32b_test.go
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
//go:build 386 || arm
// +build 386 arm

package as_test
Expand Down
1 change: 1 addition & 0 deletions int64_64b.go
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
//go:build amd64 || arm64
// +build amd64 arm64

package as
Expand Down
1 change: 1 addition & 0 deletions int64_64b_test.go
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
//go:build amd64 || arm64
// +build amd64 arm64

package as_test
Expand Down
1 change: 1 addition & 0 deletions int8_32b_test.go
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
//go:build 386 || arm
// +build 386 arm

package as_test
Expand Down
1 change: 1 addition & 0 deletions int8_64b_test.go
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
//go:build amd64 || arm64
// +build amd64 arm64

package as_test
Expand Down
1 change: 1 addition & 0 deletions int_32b_test.go
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
//go:build 386 || arm
// +build 386 arm

package as_test
Expand Down
1 change: 1 addition & 0 deletions int_64b.go
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
//go:build amd64 || arm64
// +build amd64 arm64

package as
Expand Down
1 change: 1 addition & 0 deletions int_64b_test.go
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
//go:build amd64 || arm64
// +build amd64 arm64

package as_test
Expand Down
Loading

0 comments on commit 92a7690

Please sign in to comment.