diff --git a/cvt.go b/cvt.go index 848097d..f739769 100644 --- a/cvt.go +++ b/cvt.go @@ -168,3 +168,16 @@ func Float32(v interface{}, def ...float32) float32 { return 0 } + +// String convert an interface to a string type, with default value +func String(v interface{}, def ...string) string { + if v, err := StringE(v); err == nil { + return v + } + + if len(def) > 0 { + return def[0] + } + + return "" +} diff --git a/cvt_test.go b/cvt_test.go index 28e142c..78ede2b 100644 --- a/cvt_test.go +++ b/cvt_test.go @@ -1,9 +1,12 @@ package cvt_test import ( + "errors" "fmt" "math" + "math/big" "testing" + "time" "github.com/stretchr/testify/assert" @@ -1241,3 +1244,65 @@ func TestFloat32_BaseLine(t *testing.T) { assert.Equal(t, tt.expect, v, msg) } } + +func TestString_HasDefault(t *testing.T) { + tests := []struct { + input interface{} + def string + expect string + }{ + // supported value, def is not used, def != expect + {uint64(8), "xxx", "8"}, + {float32(8.31), "xxx", "8.31"}, + {float64(-8.31), "xxx", "-8.31"}, + {true, "xxx", "true"}, + {int64(-8), "xxx", "-8"}, + {[]byte("8.01"), "xxx", "8.01"}, + {[]rune("我❤️中国"), "xxx", "我❤️中国"}, + {nil, "xxx", ""}, + {aliasTypeInt_0, "xxx", "0"}, + {&aliasTypeString_8d15_minus, "xxx", "-8.15"}, + {aliasTypeBool_true, "xxx", "true"}, + {errors.New("errors"), "xxx", "errors"}, + {time.Friday, "xxx", "Friday"}, + {big.NewInt(123), "xxx", "123"}, + {TestMarshalJSON{}, "xxx", "MarshalJSON"}, + {&TestMarshalJSON{}, "xxx", "MarshalJSON"}, + + // unsupported value, def == expect + {testing.T{}, "xxx", "xxx"}, + {&testing.T{}, "xxx", "xxx"}, + {[]int{}, "xxx", "xxx"}, + {[]string{}, "xxx", "xxx"}, + {[...]string{}, "xxx", "xxx"}, + {map[int]string{}, "xxx", "xxx"}, + } + + 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.String(tt.input, tt.def) + assert.Equal(t, tt.expect, v, msg) + } +} + +func TestString_BaseLine(t *testing.T) { + tests := []struct { + input interface{} + expect string + }{ + {testing.T{}, ""}, + {&testing.T{}, ""}, + {[]int{}, ""}, + {[]string{}, ""}, + {[...]string{}, ""}, + {map[int]string{}, ""}, + } + + for i, tt := range tests { + msg := fmt.Sprintf("i = %d, input[%+v], expect[%+v]", i, tt.input, tt.expect) + + v := cvt.String(tt.input) + assert.Equal(t, tt.expect, v, msg) + } +} diff --git a/cvte.go b/cvte.go index 6342a83..a809047 100644 --- a/cvte.go +++ b/cvte.go @@ -1,6 +1,7 @@ package cvt import ( + "encoding/json" "errors" "fmt" "math" @@ -311,6 +312,48 @@ func Float32E(val interface{}) (float32, error) { return float32(v), nil } +// StringE convert an interface to a string type +func StringE(val interface{}) (string, error) { + v, _, rv := Indirect(val) + + // interface implements + switch vv := val.(type) { + case fmt.Stringer: + return vv.String(), nil + case error: + return vv.Error(), nil + case json.Marshaler: + vvv, e := vv.MarshalJSON() + if e == nil { + return string(vvv), nil + } + } + + // source type + switch vv := v.(type) { + case nil: + return "", nil + case bool: + return strconv.FormatBool(vv), nil + case string: + return vv, nil + case []byte: + return string(vv), nil + case []rune: + return string(vv), nil + case uint, uint8, uint16, uint32, uint64, uintptr: + return strconv.FormatUint(rv.Uint(), 10), nil + case int, int8, int16, int32, int64: + return strconv.FormatInt(rv.Int(), 10), nil + case float64: + return strconv.FormatFloat(vv, 'f', -1, 64), nil + case float32: + return strconv.FormatFloat(float64(vv), 'f', -1, 32), nil + } + + return "", newErr(val, "string") +} + // Indirect returns the value with base type func Indirect(a interface{}) (val interface{}, k reflect.Kind, v reflect.Value) { if a == nil { diff --git a/cvte_test.go b/cvte_test.go index 137fc58..ddd4429 100644 --- a/cvte_test.go +++ b/cvte_test.go @@ -1,9 +1,13 @@ package cvt_test import ( + "errors" "fmt" + "html/template" "math" + "math/big" "testing" + "time" "github.com/stretchr/testify/assert" @@ -38,6 +42,12 @@ var ( aliasTypeString_off AliasTypeString = "off" ) +type TestMarshalJSON struct{} + +func (TestMarshalJSON) MarshalJSON() ([]byte, error) { + return []byte("MarshalJSON"), nil +} + func TestBoolE(t *testing.T) { tests := []struct { input interface{} @@ -1208,3 +1218,87 @@ func TestFloat32E(t *testing.T) { assert.Equal(t, tt.expect, v, msg) } } + +func TestStringE(t *testing.T) { + tests := []struct { + input interface{} + expect string + isErr bool + }{ + {int(8), "8", false}, + {int8(8), "8", false}, + {int16(8), "8", false}, + {int32(8), "8", false}, + {int64(8), "8", false}, + {uint(8), "8", false}, + {uint8(8), "8", false}, + {uint16(8), "8", false}, + {uint32(8), "8", false}, + {uint64(8), "8", false}, + {float32(8.31), "8.31", false}, + {float64(8.31), "8.31", false}, + {true, "true", false}, + {false, "false", false}, + {int(-8), "-8", false}, + {int8(-8), "-8", false}, + {int16(-8), "-8", false}, + {int32(-8), "-8", false}, + {int64(-8), "-8", false}, + {float32(-8.31), "-8.31", false}, + {float64(-8.31), "-8.31", false}, + {[]byte("-8"), "-8", false}, + {[]byte("-8.01"), "-8.01", false}, + {[]byte("8"), "8", false}, + {[]byte("8.00"), "8.00", false}, + {[]byte("8.01"), "8.01", false}, + {[]rune("我❤️中国"), "我❤️中国", false}, + {nil, "", false}, + {aliasTypeInt_0, "0", false}, + {&aliasTypeInt_0, "0", false}, + {aliasTypeInt_1, "1", false}, + {&aliasTypeInt_1, "1", false}, + {aliasTypeString_0, "0", false}, + {&aliasTypeString_0, "0", false}, + {aliasTypeString_1, "1", false}, + {&aliasTypeString_1, "1", false}, + {aliasTypeString_8d15, "8.15", false}, + {&aliasTypeString_8d15, "8.15", false}, + {aliasTypeString_8d15_minus, "-8.15", false}, + {&aliasTypeString_8d15_minus, "-8.15", false}, + {aliasTypeBool_true, "true", false}, + {&aliasTypeBool_true, "true", false}, + {aliasTypeBool_false, "false", false}, + {&aliasTypeBool_false, "false", false}, + {errors.New("errors"), "errors", false}, + {time.Friday, "Friday", false}, + {big.NewInt(123), "123", false}, + {TestMarshalJSON{}, "MarshalJSON", false}, + {&TestMarshalJSON{}, "MarshalJSON", false}, + {template.URL("http://host.foo"), "http://host.foo", false}, + + // errors + {testing.T{}, "", true}, + {&testing.T{}, "", true}, + {[]int{}, "", true}, + {[]string{}, "", true}, + {[...]string{}, "", true}, + {map[int]string{}, "", 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.StringE(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.String(tt.input) + assert.Equal(t, tt.expect, v, msg) + } +}