Skip to content

Commit ebd4725

Browse files
authored
feat: adds (Must)ConvertAny funcs (#2)
Signed-off-by: Dwi Siswanto <git@dw1.io>
1 parent b3ac9aa commit ebd4725

File tree

8 files changed

+189
-2
lines changed

8 files changed

+189
-2
lines changed

README.md

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ It provides overflow-safe arithmetic (`+`, `-`, `*`, `/`) and safe type conversi
1111

1212
* **Comprehensive generics**: works with all standard integer types.
1313
* **Checked arithmetic**: [`Add`](https://pkg.go.dev/go.dw1.io/safemath#Add), [`Sub`](https://pkg.go.dev/go.dw1.io/safemath#Sub), [`Mul`](https://pkg.go.dev/go.dw1.io/safemath#Mul), [`Div`](https://pkg.go.dev/go.dw1.io/safemath#Div) functions return an error instead of allowing silent, dangerous wrapping.
14-
* **Safe conversions**: [`Convert[To, From](v)`](https://pkg.go.dev/go.dw1.io/safemath#Convert) makes sure no data is lost during type conversion (e.g., checking bounds when casting larger types to smaller ones or signed to unsigned).
14+
* **Safe conversions**: [`Convert[To, From](v)`](https://pkg.go.dev/go.dw1.io/safemath#Convert) makes sure no data is lost during type conversion (e.g., checking bounds when casting larger types to smaller ones or signed to unsigned). [`ConvertAny`](https://pkg.go.dev/go.dw1.io/safemath#ConvertAny) extends the checks to `any` values, rejecting non-integer inputs.
1515
* **Panic APIs**: [`Must*`](https://pkg.go.dev/go.dw1.io/safemath#MustAdd) variants are available for situations where panicking on failure is preferred.
1616
* **Adversarial safety**: robustly handles dangerous edge cases like $$MinInt / -1$$ and avoids hardware exceptions.
1717

@@ -56,6 +56,20 @@ func main() {
5656
}
5757
```
5858

59+
### Converting from interface{}
60+
61+
```go
62+
val := any(int64(42))
63+
out, err := safemath.ConvertAny[uint16](val)
64+
if err != nil {
65+
fmt.Println(err)
66+
}
67+
fmt.Println(out)
68+
69+
// Will panic (ErrInvalidType) because input is not an integer
70+
// _ = safemath.MustConvertAny[uint16]("nope")
71+
```
72+
5973
### Safe Conversion
6074

6175
```go

doc.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,5 +10,7 @@
1010
//
1111
// For type conversions, [Convert] ensures that the value can be represented
1212
// in the target type without data loss, handling both signed-to-unsigned and
13-
// size-based truncation checks.
13+
// size-based truncation checks. When the source value is only available as
14+
// an interface, [ConvertAny] (and [MustConvertAny]) perform the same checks
15+
// while also rejecting non-integer inputs.
1416
package safemath

errors.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,5 +5,6 @@ import "errors"
55
var (
66
ErrOverflow = errors.New("integer overflow/underflow")
77
ErrTruncation = errors.New("integer type truncation")
8+
ErrInvalidType = errors.New("invalid integer type")
89
ErrDivisionByZero = errors.New("division by zero")
910
)

safemath.go

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,38 @@ func Convert[To, From Integer](v From) (To, error) {
123123
return to, nil
124124
}
125125

126+
// ConvertAny attempts to convert v (any integer type) into To.
127+
//
128+
// Returns ErrInvalidType when v is not an integer or cannot fit in To.
129+
func ConvertAny[To Integer](v any) (To, error) {
130+
switch x := v.(type) {
131+
case int:
132+
return Convert[To](x)
133+
case int8:
134+
return Convert[To](x)
135+
case int16:
136+
return Convert[To](x)
137+
case int32:
138+
return Convert[To](x)
139+
case int64:
140+
return Convert[To](x)
141+
case uint:
142+
return Convert[To](x)
143+
case uint8:
144+
return Convert[To](x)
145+
case uint16:
146+
return Convert[To](x)
147+
case uint32:
148+
return Convert[To](x)
149+
case uint64:
150+
return Convert[To](x)
151+
case uintptr:
152+
return Convert[To](x)
153+
default:
154+
return 0, ErrInvalidType
155+
}
156+
}
157+
126158
// MustAdd returns the sum of a and b on success. Panics on error.
127159
func MustAdd[T Integer](a, b T) T {
128160
c, err := Add(a, b)
@@ -172,3 +204,13 @@ func MustConvert[To, From Integer](v From) To {
172204

173205
return c
174206
}
207+
208+
// MustConvertAny converts v (any integer type) into To or panics on error.
209+
func MustConvertAny[To Integer](v any) To {
210+
c, err := ConvertAny[To](v)
211+
if err != nil {
212+
panic(err)
213+
}
214+
215+
return c
216+
}

safemath_bench_test.go

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -163,3 +163,19 @@ func BenchmarkMustConvert(b *testing.B) {
163163
}
164164
_ = res
165165
}
166+
167+
func BenchmarkConvertAny(b *testing.B) {
168+
var res uint8
169+
for i := 0; i < b.N; i++ {
170+
res, _ = safemath.ConvertAny[uint8](int64(i % 127))
171+
}
172+
_ = res
173+
}
174+
175+
func BenchmarkMustConvertAny(b *testing.B) {
176+
var res uint8
177+
for i := 0; i < b.N; i++ {
178+
res = safemath.MustConvertAny[uint8](int64(i % 127))
179+
}
180+
_ = res
181+
}

safemath_example_test.go

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,25 @@ func ExampleConvert() {
104104
// integer type truncation
105105
}
106106

107+
func ExampleConvertAny() {
108+
// Convert from interface{} holding an int64
109+
var v any = int64(42)
110+
res, err := safemath.ConvertAny[uint8](v)
111+
if err != nil {
112+
fmt.Println(err)
113+
}
114+
fmt.Println(res)
115+
116+
// Non-integer input is rejected
117+
_, err = safemath.ConvertAny[int]("nope")
118+
if err != nil {
119+
fmt.Println(err)
120+
}
121+
// Output:
122+
// 42
123+
// invalid integer type
124+
}
125+
107126
func ExampleMustAdd() {
108127
// MustAdd succeeds
109128
fmt.Println(safemath.MustAdd(100, 200))
@@ -139,6 +158,24 @@ func ExampleMustConvert() {
139158
// Recovered from: integer type truncation
140159
}
141160

161+
func ExampleMustConvertAny() {
162+
// MustConvertAny succeeds when the interface holds an integer
163+
var v any = uint32(255)
164+
fmt.Println(safemath.MustConvertAny[uint8](v))
165+
166+
// MustConvertAny panics on invalid type
167+
defer func() {
168+
if r := recover(); r != nil {
169+
fmt.Println("Recovered from:", r)
170+
}
171+
}()
172+
safemath.MustConvertAny[int]("bad")
173+
174+
// Output:
175+
// 255
176+
// Recovered from: invalid integer type
177+
}
178+
142179
func ExampleMustSub() {
143180
// MustSub succeeds
144181
fmt.Println(safemath.MustSub(100, 40))

safemath_fuzz_test.go

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,3 +112,25 @@ func FuzzConvertUnsignedToUnsignedSmall(f *testing.F) {
112112
checkConsistency(t, res, err, func() uint8 { return safemath.MustConvert[uint8](a) })
113113
})
114114
}
115+
116+
// FuzzConvertAny covers the interface-based conversion path.
117+
func FuzzConvertAnyInt64ToUint8(f *testing.F) {
118+
f.Add(int64(100))
119+
f.Add(int64(-1))
120+
f.Add(int64(300))
121+
122+
f.Fuzz(func(t *testing.T, a int64) {
123+
res, err := safemath.ConvertAny[uint8](a)
124+
checkConsistency(t, res, err, func() uint8 { return safemath.MustConvertAny[uint8](a) })
125+
})
126+
}
127+
128+
func FuzzConvertAnyUint64ToInt64(f *testing.F) {
129+
f.Add(uint64(1))
130+
f.Add(uint64(1) << 63)
131+
132+
f.Fuzz(func(t *testing.T, a uint64) {
133+
res, err := safemath.ConvertAny[int64](a)
134+
checkConsistency(t, res, err, func() int64 { return safemath.MustConvertAny[int64](a) })
135+
})
136+
}

safemath_test.go

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -464,3 +464,56 @@ func TestMust(t *testing.T) {
464464
safemath.MustConvert[int8](128)
465465
})
466466
}
467+
468+
func TestConvertAny(t *testing.T) {
469+
tests := []struct {
470+
name string
471+
val any
472+
want uint8
473+
err error
474+
}{
475+
{name: "int to uint8", val: int(10), want: uint8(10), err: nil},
476+
{name: "uint8 to uint8", val: uint8(5), want: uint8(5), err: nil},
477+
{name: "overflow", val: uint16(256), want: 0, err: safemath.ErrTruncation},
478+
{name: "non-integer", val: 1.5, want: 0, err: safemath.ErrInvalidType},
479+
{name: "nil", val: nil, want: 0, err: safemath.ErrInvalidType},
480+
}
481+
482+
for _, tt := range tests {
483+
t.Run(tt.name, func(t *testing.T) {
484+
got, err := safemath.ConvertAny[uint8](tt.val)
485+
if err != tt.err {
486+
t.Fatalf("error = %v, want %v", err, tt.err)
487+
}
488+
if tt.err == nil && got != tt.want {
489+
t.Fatalf("got %v, want %v", got, tt.want)
490+
}
491+
})
492+
}
493+
}
494+
495+
func TestMustConvertAny(t *testing.T) {
496+
t.Run("success", func(t *testing.T) {
497+
if got := safemath.MustConvertAny[uint8](int(7)); got != 7 {
498+
t.Fatalf("MustConvertAny returned %v, want 7", got)
499+
}
500+
})
501+
502+
t.Run("panic on truncation", func(t *testing.T) {
503+
defer func() {
504+
if r := recover(); r == nil {
505+
t.Fatal("MustConvertAny did not panic")
506+
}
507+
}()
508+
safemath.MustConvertAny[uint8](uint16(256))
509+
})
510+
511+
t.Run("panic on non-integer", func(t *testing.T) {
512+
defer func() {
513+
if r := recover(); r == nil {
514+
t.Fatal("MustConvertAny did not panic for non-integer")
515+
}
516+
}()
517+
safemath.MustConvertAny[uint8](1.5)
518+
})
519+
}

0 commit comments

Comments
 (0)