diff --git a/cvt.go b/cvt.go new file mode 100644 index 0000000..4e55ad7 --- /dev/null +++ b/cvt.go @@ -0,0 +1,14 @@ +package cvt + +// Bool convert an interface to a bool type, with default value +func Bool(v interface{}, def ...bool) bool { + if v, err := BoolE(v); err == nil { + return v + } + + if len(def) > 0 { + return def[0] + } + + return false +} diff --git a/cvt_test.go b/cvt_test.go new file mode 100644 index 0000000..55028ef --- /dev/null +++ b/cvt_test.go @@ -0,0 +1,113 @@ +package cvt_test + +import ( + "fmt" + "testing" + + "github.com/stretchr/testify/assert" + + "github.com/shockerli/cvt" +) + +func TestBool_HasDefault(t *testing.T) { + tests := []struct { + input interface{} + def bool + expect bool + }{ + // supported value, def is not used, def != expect + {0, true, false}, + {float64(0.00), true, false}, + {int(0.00), true, false}, + {int64(0.00), true, false}, + {uint(0.00), true, false}, + {uint64(0.00), true, false}, + {uint8(0.00), true, false}, + {nil, true, false}, + {"false", true, false}, + {"FALSE", true, false}, + {"False", true, false}, + {"f", true, false}, + {"F", true, false}, + {false, true, false}, + {"off", true, false}, + {"Off", true, false}, + {[]byte("Off"), true, false}, + {aliasTypeInt_0, true, false}, + {&aliasTypeInt_0, true, false}, + {aliasTypeString_0, true, false}, + {&aliasTypeString_0, true, false}, + {aliasTypeString_off, true, false}, + {&aliasTypeString_off, true, false}, + + {[]int{}, true, false}, + {[]string{}, true, false}, + {[...]string{}, true, false}, + {map[int]int{}, true, false}, + {map[string]string{}, true, false}, + + {"true", false, true}, + {"TRUE", false, true}, + {"True", false, true}, + {"t", false, true}, + {"T", false, true}, + {1, false, true}, + {true, false, true}, + {-1, false, true}, + {"on", false, true}, + {"On", false, true}, + {0.01, false, true}, + {aliasTypeInt_1, false, true}, + {&aliasTypeInt_1, false, true}, + {aliasTypeString_1, false, true}, + {&aliasTypeString_1, false, true}, + {aliasTypeString_on, false, true}, + {&aliasTypeString_on, false, true}, + + {[]int{1, 2, 3}, false, true}, + {[]string{"a", "b", "c"}, false, true}, + {[...]string{"a", "b", "c"}, false, true}, + {map[int]int{1: 111, 2: 222}, false, true}, + {map[string]string{"a": "aaa"}, false, true}, + + // unsupported value, def == expect + {"hello", true, true}, + {"hello", false, false}, + {testing.T{}, true, true}, + {testing.T{}, false, false}, + {&testing.T{}, true, true}, + {&testing.T{}, false, false}, + } + + for i, tt := range tests { + msg := fmt.Sprintf("i = %d, input[%+v], def[%+v], expect[%+v]", i, tt.input, tt.def, tt.expect) + + v := cvt.Bool(tt.input, tt.def) + assert.Equal(t, tt.expect, v, msg) + } +} + +func TestBool_BaseLine(t *testing.T) { + tests := []struct { + input interface{} + expect bool + }{ + {testing.T{}, false}, + {&testing.T{}, false}, + {[]int{}, false}, + {[]int{1, 2, 3}, true}, + {[]string{}, false}, + {[]string{"a", "b", "c"}, true}, + {[...]string{}, false}, + {map[int]string{}, false}, + {aliasTypeString_8d15_minus, true}, + {&aliasTypeString_8d15_minus, true}, + } + + for i, tt := range tests { + msg := fmt.Sprintf("i = %d, input[%+v], expect[%+v]", i, tt.input, tt.expect) + + v := cvt.Bool(tt.input) + assert.Equal(t, tt.expect, v, msg) + } +} diff --git a/cvte.go b/cvte.go new file mode 100644 index 0000000..0017f14 --- /dev/null +++ b/cvte.go @@ -0,0 +1,117 @@ +package cvt + +import ( + "fmt" + "reflect" + "strconv" + "strings" +) + +// BoolE convert an interface to a bool type +func BoolE(val interface{}) (bool, error) { + v, rk, rv := Indirect(val) + + switch vv := v.(type) { + case bool: + return vv, nil + case int, int8, int16, int32, int64: + return rv.Int() != 0, nil + case uint, uint8, uint16, uint32, uint64: + return rv.Uint() != 0, nil + case float32, float64: + return rv.Float() != 0, nil + case []byte: + return str2bool(string(vv)) + case string: + return str2bool(vv) + case nil: + return false, nil + } + + switch rk { + // by elem length + case reflect.Array, reflect.Slice, reflect.Map: + return rv.Len() > 0, nil + } + + return false, err(val, "bool") +} + +// returns the boolean value represented by the string +func str2bool(str string) (bool, error) { + if val, err := strconv.ParseBool(str); err == nil { + return val, nil + } else if val, err := strconv.ParseFloat(str, 64); err == nil { + return val != 0, nil + } + + switch strings.ToLower(strings.TrimSpace(str)) { + case "on": + return true, nil + case "off": + return false, nil + default: + return false, err(str, "bool") + } +} + +// Indirect returns the value with base type +func Indirect(a interface{}) (val interface{}, k reflect.Kind, v reflect.Value) { + if a == nil { + return + } + + t := reflect.TypeOf(a) + v = reflect.ValueOf(a) + k = t.Kind() + + switch t.Kind() { + case reflect.Ptr: + for v.Kind() == reflect.Ptr && !v.IsNil() { + v = v.Elem() + } + return Indirect(v.Interface()) + case reflect.Bool: + val = v.Bool() + case reflect.Int: + val = int(v.Int()) + case reflect.Int8: + val = int8(v.Int()) + case reflect.Int16: + val = int16(v.Int()) + case reflect.Int32: + val = int32(v.Int()) + case reflect.Int64: + val = v.Int() + case reflect.Uint: + val = uint(v.Uint()) + case reflect.Uint8: + val = uint8(v.Uint()) + case reflect.Uint16: + val = uint16(v.Uint()) + case reflect.Uint32: + val = uint32(v.Uint()) + case reflect.Uint64: + val = v.Uint() + case reflect.Uintptr: + val = uintptr(v.Uint()) + case reflect.Float32: + val = float32(v.Float()) + case reflect.Float64: + val = v.Float() + case reflect.Complex64: + val = complex64(v.Complex()) + case reflect.Complex128: + val = v.Complex() + case reflect.String: + val = v.String() + default: + val = v.Interface() + } + + return +} + +func err(val interface{}, t string) error { + return fmt.Errorf("unable to convert %#v of type %T to %s", val, val, t) +} diff --git a/cvte_test.go b/cvte_test.go new file mode 100644 index 0000000..27daa90 --- /dev/null +++ b/cvte_test.go @@ -0,0 +1,121 @@ +package cvt_test + +import ( + "fmt" + "testing" + + "github.com/stretchr/testify/assert" + + "github.com/shockerli/cvt" +) + +// alias type: int +type AliasTypeInt int + +var ( + aliasTypeInt_0 AliasTypeInt = 0 + aliasTypeInt_1 AliasTypeInt = 1 +) + +// alias type: string +type AliasTypeString string + +var ( + aliasTypeString_0 AliasTypeString = "0" + aliasTypeString_1 AliasTypeString = "1" + aliasTypeString_8d15 AliasTypeString = "8.15" + aliasTypeString_8d15_minus AliasTypeString = "-8.15" + aliasTypeString_on AliasTypeString = "on" + aliasTypeString_off AliasTypeString = "off" +) + +func TestBoolE(t *testing.T) { + tests := []struct { + input interface{} + expect bool + isErr bool + }{ + // true/scale + {0, false, false}, + {float64(0.00), false, false}, + {int(0.00), false, false}, + {int64(0.00), false, false}, + {uint(0.00), false, false}, + {uint64(0.00), false, false}, + {uint8(0.00), false, false}, + {nil, false, false}, + {"false", false, false}, + {"FALSE", false, false}, + {"False", false, false}, + {"f", false, false}, + {"F", false, false}, + {false, false, false}, + {"off", false, false}, + {"Off", false, false}, + {"0", false, false}, + {"0.00", false, false}, + {[]byte("Off"), false, false}, + {aliasTypeInt_0, false, false}, + {&aliasTypeInt_0, false, false}, + {aliasTypeString_0, false, false}, + {&aliasTypeString_0, false, false}, + {aliasTypeString_off, false, false}, + {&aliasTypeString_off, false, false}, + + // false/slice/array/map + {[]int{}, false, false}, + {[]string{}, false, false}, + {[...]string{}, false, false}, + {map[int]int{}, false, false}, + {map[string]string{}, false, false}, + + // true/scale + {"true", true, false}, + {"TRUE", true, false}, + {"True", true, false}, + {"t", true, false}, + {"T", true, false}, + {1, true, false}, + {true, true, false}, + {-1, true, false}, + {"on", true, false}, + {"On", true, false}, + {0.01, true, false}, + {"0.01", true, false}, + {aliasTypeInt_1, true, false}, + {&aliasTypeInt_1, true, false}, + {aliasTypeString_1, true, false}, + {&aliasTypeString_1, true, false}, + {aliasTypeString_on, true, false}, + {&aliasTypeString_on, true, false}, + + // true/slice/array/map + {[]int{1, 2, 3}, true, false}, + {[]string{"a", "b", "c"}, true, false}, + {[...]string{"a", "b", "c"}, true, false}, + {map[int]int{1: 111, 2: 222}, true, false}, + {map[string]string{"a": "aaa"}, true, false}, + + // errors + {"hello", false, true}, + {testing.T{}, false, true}, + {&testing.T{}, false, true}, + } + + for i, tt := range tests { + msg := fmt.Sprintf("i = %d, input[%+v], expect[%+v], isErr[%+v]", i, tt.input, tt.expect, tt.isErr) + + v, err := cvt.BoolE(tt.input) + if tt.isErr { + assert.Error(t, err, msg) + continue + } + + assert.NoError(t, err, msg) + assert.Equal(t, tt.expect, v, msg) + + // Non-E test with no default value: + v = cvt.Bool(tt.input) + assert.Equal(t, tt.expect, v, msg) + } +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..e053fa2 --- /dev/null +++ b/go.mod @@ -0,0 +1,10 @@ +module github.com/shockerli/cvt + +go 1.15 + +require ( + github.com/davecgh/go-spew v1.1.1 + github.com/stretchr/objx v0.3.0 // indirect + github.com/stretchr/testify v1.7.0 + gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..246b4aa --- /dev/null +++ b/go.sum @@ -0,0 +1,17 @@ +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/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 h1:4G4v2dO3VZwixGIRoQ5Lfboy6nUhCyYzaqnIAPPhYs4= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.3.0 h1:NGXK3lHquSN08v5vWalVI/L8XU9hdzE/G6xsrze47As= +github.com/stretchr/objx v0.3.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo= +gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=