File: safecast.go

package info (click to toggle)
golang-fortio-safecast 1.2.0-2
  • links: PTS, VCS
  • area: main
  • in suites: experimental
  • size: 148 kB
  • sloc: makefile: 2
file content (145 lines) | stat: -rw-r--r-- 4,886 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
// Package safecast allows you to safely cast between numeric types in Go and return errors (or panic when using the
// Must* variants) when the cast would result in a loss of precision, range or sign.
package safecast

// Implementation: me (@ldemailly), idea: @ccoVeille - https://github.com/ccoVeille/go-safecast
// Started https://github.com/ldemailly/go-scratch/commits/main/safecast/safecast.go
// then moved here to fortio.org/safecast

import (
	"errors"
	"fmt"
	"math"
	"unsafe"
)

type Integer interface {
	// Same as golang.org/x/contraints.Integer but without importing the whole thing for 1 line.
	~int | ~uint | ~int8 | ~uint8 | ~int16 | ~uint16 | ~int32 | ~uint32 | ~int64 | ~uint64 | ~uintptr
}

type Float interface {
	~float32 | ~float64 // Consider removing the ~ because of +Inf issue.
}

type Number interface {
	Integer | Float
}

var ErrOutOfRange = errors.New("out of range")

const (
	all63bits = uint64(math.MaxInt64)
	all31bits = uint64(math.MaxInt32)
)

func isFloat[Num Number](f Num) (isFloat bool) {
	switch any(f).(type) { //nolint:exhaustive // only 2 we want to check
	case float32, float64:
		isFloat = true
	}
	return
}

// Convert converts a number from one type to another,
// returning an error if the conversion would result in a loss of precision,
// range or sign (overflow). In other words if the converted number is not
// equal to the original number.
// Use [Conv] instead if both of your types are Integer.
// Do not use for identity (same type in and out) but in particular this
// will error for Convert[uint64](uint64(math.MaxUint64)) or
// Convert[int64](int64(math.MaxInt64)) because it needs to
// when converting to any float. Note that +Inf will convert correctly (as in error
// only if going to an integer type) from a float64/float32 and not a ~float (it will
// error from say ~float32 to float64 while it shouldn't).
func Convert[NumOut Number, NumIn Number](orig NumIn) (converted NumOut, err error) {
	origPositive := (orig >= 0)
	// All bits set on uint64 or positive int63 are two of 4 special cases not detected by roundtrip (afaik).
	if origPositive && (uint64(orig)&all63bits == all63bits) && !isFloat(orig) {
		err = ErrOutOfRange
		return
	}
	converted = NumOut(orig)
	if origPositive != (converted >= 0) {
		err = ErrOutOfRange
		return
	}
	if NumIn(converted) != orig && ((converted == converted) || (orig == orig)) { //nolint:gocritic // NaN check
		err = ErrOutOfRange
		return
	}
	// And this are the other 2 weird case, maxint32 and maxuint32 (on armhf) conversion to float32.
	if origPositive && (uint64(orig)&all31bits == all31bits) && unsafe.Sizeof(converted) == 4 && !isFloat(orig) {
		err = ErrOutOfRange
	}
	return
}

// Conv is an integer only and simpler version of [Convert] without the
// weird issue with round trip to floating point.
// Unlike [Convert] it can be used for identity (same type in and out) even if that's
// of dubious value.
func Conv[NumOut Integer, NumIn Integer](orig NumIn) (converted NumOut, err error) {
	origPositive := (orig >= 0)
	converted = NumOut(orig)
	if origPositive != (converted >= 0) {
		err = ErrOutOfRange
		return
	}
	if NumIn(converted) != orig {
		err = ErrOutOfRange
	}
	return
}

// Same as Convert but panics if there is an error.
func MustConvert[NumOut Number, NumIn Number](orig NumIn) NumOut {
	converted, err := Convert[NumOut](orig)
	if err != nil {
		doPanic(err, orig, converted)
	}
	return converted
}

// MustConv is as [Conv] but panics if there is an error.
func MustConv[NumOut Integer, NumIn Integer](orig NumIn) NumOut {
	converted, err := Conv[NumOut](orig)
	if err != nil {
		doPanic(err, orig, converted)
	}
	return converted
}

// Converts a float to an integer by truncating the fractional part.
// Returns an error if the conversion would result in a loss of precision.
func Truncate[NumOut Number, NumIn Float](orig NumIn) (converted NumOut, err error) {
	return Convert[NumOut](math.Trunc(float64(orig)))
}

// Converts a float to an integer by rounding to the nearest integer.
// Returns an error if the conversion would result in a loss of precision.
func Round[NumOut Number, NumIn Float](orig NumIn) (converted NumOut, err error) {
	return Convert[NumOut](math.Round(float64(orig)))
}

// Same as Truncate but panics if there is an error.
func MustTruncate[NumOut Number, NumIn Float](orig NumIn) NumOut {
	converted, err := Truncate[NumOut, NumIn](orig)
	if err != nil {
		doPanic(err, orig, converted)
	}
	return converted
}

// Same as Round but panics if there is an error.
func MustRound[NumOut Number, NumIn Float](orig NumIn) NumOut {
	converted, err := Round[NumOut, NumIn](orig)
	if err != nil {
		doPanic(err, orig, converted)
	}
	return converted
}

func doPanic[NumOut Number, NumIn Number](err error, orig NumIn, converted NumOut) {
	panic(fmt.Sprintf("safecast: %v for %v (%T) to %T", err, orig, orig, converted))
}