pax_global_header00006660000000000000000000000064150576040530014517gustar00rootroot0000000000000052 comment=fc73346bfc4e6597bc520fb6eea04360299e77d2 spf13-cast-3efe057/000077500000000000000000000000001505760405300140235ustar00rootroot00000000000000spf13-cast-3efe057/.editorconfig000066400000000000000000000003201505760405300164730ustar00rootroot00000000000000root = true [*] charset = utf-8 end_of_line = lf indent_size = 4 indent_style = space insert_final_newline = true trim_trailing_whitespace = true [*.go] indent_style = tab [{*.yml,*.yaml}] indent_size = 2 spf13-cast-3efe057/.github/000077500000000000000000000000001505760405300153635ustar00rootroot00000000000000spf13-cast-3efe057/.github/.editorconfig000066400000000000000000000000411505760405300200330ustar00rootroot00000000000000[{*.yml,*.yaml}] indent_size = 2 spf13-cast-3efe057/.github/dependabot.yaml000066400000000000000000000003031505760405300203500ustar00rootroot00000000000000version: 2 updates: - package-ecosystem: gomod directory: / schedule: interval: daily - package-ecosystem: github-actions directory: / schedule: interval: daily spf13-cast-3efe057/.github/workflows/000077500000000000000000000000001505760405300174205ustar00rootroot00000000000000spf13-cast-3efe057/.github/workflows/analysis-scorecard.yaml000066400000000000000000000022351505760405300240740ustar00rootroot00000000000000name: OpenSSF Scorecard on: branch_protection_rule: push: branches: [master] schedule: - cron: "30 0 * * 5" permissions: contents: read jobs: analyze: name: Analyze runs-on: ubuntu-latest permissions: actions: read contents: read id-token: write security-events: write steps: - name: Checkout repository uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 with: persist-credentials: false - name: Run analysis uses: ossf/scorecard-action@05b42c624433fc40578a4040d5cf5e36ddca8cde # v2.4.2 with: results_file: results.sarif results_format: sarif publish_results: true - name: Upload results as artifact uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 with: name: OpenSSF Scorecard results path: results.sarif retention-days: 5 - name: Upload results to GitHub Security tab uses: github/codeql-action/upload-sarif@f1f6e5f6af878fb37288ce1c627459e94dbf7d01 # v3.29.5 with: sarif_file: results.sarif spf13-cast-3efe057/.github/workflows/ci.yaml000066400000000000000000000031561505760405300207040ustar00rootroot00000000000000name: CI on: push: branches: [master] pull_request: permissions: contents: read jobs: test: name: Test runs-on: ${{ matrix.os }} strategy: # Fail fast is disabled because there are Go version specific features and tests # that should be able to fail independently. fail-fast: false matrix: os: [ubuntu-latest, macos-latest, windows-latest] go: ["1.21", "1.22", "1.23", "1.24"] steps: - name: Checkout repository uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 - name: Set up Go uses: actions/setup-go@44694675825211faa026b3c33043df3e48a5fa00 # v6.0.0 with: go-version: ${{ matrix.go }} - name: Test run: go test -race -v ./... lint: name: Lint runs-on: ubuntu-latest steps: - name: Checkout repository uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 - name: Set up Go uses: actions/setup-go@44694675825211faa026b3c33043df3e48a5fa00 # v6.0.0 with: go-version: "1.24" - name: Lint uses: golangci/golangci-lint-action@4afd733a84b1f43292c63897423277bb7f4313a9 # v8.0.0 with: version: v2.1.6 dependency-review: name: Dependency review runs-on: ubuntu-latest if: github.event_name == 'pull_request' steps: - name: Checkout repository uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 - name: Dependency Review uses: actions/dependency-review-action@595b5aeba73380359d98a5e087f648dbb0edce1b # v4.7.3 spf13-cast-3efe057/.gitignore000066400000000000000000000004141505760405300160120ustar00rootroot00000000000000# Compiled Object files, Static and Dynamic libs (Shared Objects) *.o *.a *.so # Folders _obj _test # Architecture specific extensions/prefixes *.[568vq] [568vq].out *.cgo1.go *.cgo2.c _cgo_defun.c _cgo_gotypes.go _cgo_export.* _testmain.go *.exe *.test *.bench spf13-cast-3efe057/.golangci.yaml000066400000000000000000000011521505760405300165470ustar00rootroot00000000000000version: "2" run: timeout: 10m linters: enable: - errcheck - govet - ineffassign - misspell - nolintlint # - revive - unused disable: - staticcheck settings: misspell: locale: US nolintlint: allow-unused: false # report any unused nolint directives require-specific: false # don't require nolint directives to be specific about which linter is being skipped formatters: enable: - gci - gofmt # - gofumpt - goimports # - golines settings: gci: sections: - standard - default - localmodule spf13-cast-3efe057/LICENSE000066400000000000000000000020671505760405300150350ustar00rootroot00000000000000The MIT License (MIT) Copyright (c) 2014 Steve Francia Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.spf13-cast-3efe057/Makefile000066400000000000000000000021361505760405300154650ustar00rootroot00000000000000GOVERSION := $(shell go version | cut -d ' ' -f 3 | cut -d '.' -f 2) .PHONY: check fmt lint test test-race vet test-cover-html help .DEFAULT_GOAL := help check: test-race fmt vet lint ## Run tests and linters test: ## Run tests go test ./... test-race: ## Run tests with race detector go test -race ./... fmt: ## Run gofmt linter ifeq "$(GOVERSION)" "12" @for d in `go list` ; do \ if [ "`gofmt -l -s $$GOPATH/src/$$d | tee /dev/stderr`" ]; then \ echo "^ improperly formatted go files" && echo && exit 1; \ fi \ done endif lint: ## Run golint linter @for d in `go list` ; do \ if [ "`golint $$d | tee /dev/stderr`" ]; then \ echo "^ golint errors!" && echo && exit 1; \ fi \ done vet: ## Run go vet linter @if [ "`go vet | tee /dev/stderr`" ]; then \ echo "^ go vet errors!" && echo && exit 1; \ fi test-cover-html: ## Generate test coverage report go test -coverprofile=coverage.out -covermode=count go tool cover -func=coverage.out help: @grep -E '^[a-zA-Z0-9_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}' spf13-cast-3efe057/README.md000066400000000000000000000063061505760405300153070ustar00rootroot00000000000000# cast [![GitHub Workflow Status](https://img.shields.io/github/actions/workflow/status/spf13/cast/ci.yaml?style=flat-square)](https://github.com/spf13/cast/actions/workflows/ci.yaml) [![go.dev reference](https://img.shields.io/badge/go.dev-reference-007d9c?logo=go&logoColor=white&style=flat-square)](https://pkg.go.dev/mod/github.com/spf13/cast) ![GitHub go.mod Go version](https://img.shields.io/github/go-mod/go-version/spf13/cast?style=flat-square&color=61CFDD) [![OpenSSF Scorecard](https://api.securityscorecards.dev/projects/github.com/spf13/cast/badge?style=flat-square)](https://deps.dev/go/github.com%252Fspf13%252Fcast) Easy and safe casting from one type to another in Go Don’t Panic! ... Cast ## What is Cast? Cast is a library to convert between different go types in a consistent and easy way. Cast provides simple functions to easily convert a number to a string, an interface into a bool, etc. Cast does this intelligently when an obvious conversion is possible. It doesn’t make any attempts to guess what you meant, for example you can only convert a string to an int when it is a string representation of an int such as “8”. Cast was developed for use in [Hugo](https://gohugo.io), a website engine which uses YAML, TOML or JSON for meta data. ## Why use Cast? When working with dynamic data in Go you often need to cast or convert the data from one type into another. Cast goes beyond just using type assertion (though it uses that when possible) to provide a very straightforward and convenient library. If you are working with interfaces to handle things like dynamic content you’ll need an easy way to convert an interface into a given type. This is the library for you. If you are taking in data from YAML, TOML or JSON or other formats which lack full types, then Cast is the library for you. ## Usage Cast provides a handful of To_____ methods. These methods will always return the desired type. **If input is provided that will not convert to that type, the 0 or nil value for that type will be returned**. Cast also provides identical methods To_____E. These return the same result as the To_____ methods, plus an additional error which tells you if it successfully converted. Using these methods you can tell the difference between when the input matched the zero value or when the conversion failed and the zero value was returned. The following examples are merely a sample of what is available. Please review the code for a complete set. ### Example ‘ToString’: cast.ToString("mayonegg") // "mayonegg" cast.ToString(8) // "8" cast.ToString(8.31) // "8.31" cast.ToString([]byte("one time")) // "one time" cast.ToString(nil) // "" var foo interface{} = "one more time" cast.ToString(foo) // "one more time" ### Example ‘ToInt’: cast.ToInt(8) // 8 cast.ToInt(8.31) // 8 cast.ToInt("8") // 8 cast.ToInt(true) // 1 cast.ToInt(false) // 0 var eight interface{} = 8 cast.ToInt(eight) // 8 cast.ToInt(nil) // 0 ## License The project is licensed under the [MIT License](LICENSE). spf13-cast-3efe057/alias.go000066400000000000000000000041431505760405300154450ustar00rootroot00000000000000// Copyright © 2014 Steve Francia . // // Use of this source code is governed by an MIT-style // license that can be found in the LICENSE file. package cast import ( "reflect" "slices" ) var kindNames = []string{ reflect.String: "string", reflect.Bool: "bool", reflect.Int: "int", reflect.Int8: "int8", reflect.Int16: "int16", reflect.Int32: "int32", reflect.Int64: "int64", reflect.Uint: "uint", reflect.Uint8: "uint8", reflect.Uint16: "uint16", reflect.Uint32: "uint32", reflect.Uint64: "uint64", reflect.Float32: "float32", reflect.Float64: "float64", } var kinds = map[reflect.Kind]func(reflect.Value) any{ reflect.String: func(v reflect.Value) any { return v.String() }, reflect.Bool: func(v reflect.Value) any { return v.Bool() }, reflect.Int: func(v reflect.Value) any { return int(v.Int()) }, reflect.Int8: func(v reflect.Value) any { return int8(v.Int()) }, reflect.Int16: func(v reflect.Value) any { return int16(v.Int()) }, reflect.Int32: func(v reflect.Value) any { return int32(v.Int()) }, reflect.Int64: func(v reflect.Value) any { return v.Int() }, reflect.Uint: func(v reflect.Value) any { return uint(v.Uint()) }, reflect.Uint8: func(v reflect.Value) any { return uint8(v.Uint()) }, reflect.Uint16: func(v reflect.Value) any { return uint16(v.Uint()) }, reflect.Uint32: func(v reflect.Value) any { return uint32(v.Uint()) }, reflect.Uint64: func(v reflect.Value) any { return v.Uint() }, reflect.Float32: func(v reflect.Value) any { return float32(v.Float()) }, reflect.Float64: func(v reflect.Value) any { return v.Float() }, } // resolveAlias attempts to resolve a named type to its underlying basic type (if possible). // // Pointers are expected to be indirected by this point. func resolveAlias(i any) (any, bool) { if i == nil { return nil, false } t := reflect.TypeOf(i) // Not a named type if t.Name() == "" || slices.Contains(kindNames, t.Name()) { return i, false } resolve, ok := kinds[t.Kind()] if !ok { // Not a supported kind return i, false } v := reflect.ValueOf(i) return resolve(v), true } spf13-cast-3efe057/alias_test.go000066400000000000000000000037731505760405300165140ustar00rootroot00000000000000// Copyright © 2014 Steve Francia . // // Use of this source code is governed by an MIT-style // license that can be found in the LICENSE file. package cast import ( "testing" qt "github.com/frankban/quicktest" ) func TestAlias(t *testing.T) { type MyStruct struct{} type MyString string type MyOtherString MyString type MyAliasString = MyOtherString type MyBool bool type MyOtherBool MyBool type MyAliasBool = MyOtherBool type MyInt int type MyInt8 int8 type MyInt16 int16 type MyInt32 int32 type MyInt64 int64 type MyUint uint type MyUint8 uint8 type MyUint16 uint16 type MyUint32 uint32 type MyUint64 uint64 type MyFloat32 float32 type MyFloat64 float64 var myStruct *MyStruct testCases := []struct { input any expectedValue any expectedOk bool }{ {"string", "string", false}, // Already resolved {MyStruct{}, MyStruct{}, false}, // Non-resolvable {nil, nil, false}, {&MyStruct{}, &MyStruct{}, false}, {myStruct, myStruct, false}, {MyString("string"), "string", true}, {MyOtherString("string"), "string", true}, {MyAliasString("string"), "string", true}, {MyBool(true), true, true}, {MyOtherBool(true), true, true}, {MyAliasBool(true), true, true}, {MyInt(1234), int(1234), true}, {MyInt8(123), int8(123), true}, {MyInt16(1234), int16(1234), true}, {MyInt32(1234), int32(1234), true}, {MyInt64(1234), int64(1234), true}, {MyUint(1234), uint(1234), true}, {MyUint8(123), uint8(123), true}, {MyUint16(1234), uint16(1234), true}, {MyUint32(1234), uint32(1234), true}, {MyUint64(1234), uint64(1234), true}, {MyFloat32(1.0), float32(1.0), true}, {MyFloat64(1.0), float64(1.0), true}, } for _, testCase := range testCases { // TODO: remove after minimum Go version is >=1.22 testCase := testCase t.Run("", func(t *testing.T) { c := qt.New(t) actualValue, ok := resolveAlias(testCase.input) c.Assert(actualValue, qt.Equals, testCase.expectedValue) c.Assert(ok, qt.Equals, testCase.expectedOk) }) } } spf13-cast-3efe057/basic.go000066400000000000000000000052011505760405300154310ustar00rootroot00000000000000// Copyright © 2014 Steve Francia . // // Use of this source code is governed by an MIT-style // license that can be found in the LICENSE file. package cast import ( "encoding/json" "fmt" "html/template" "strconv" "time" ) // ToBoolE casts any value to a bool type. func ToBoolE(i any) (bool, error) { i, _ = indirect(i) switch b := i.(type) { case bool: return b, nil case nil: return false, nil case int: return b != 0, nil case int8: return b != 0, nil case int16: return b != 0, nil case int32: return b != 0, nil case int64: return b != 0, nil case uint: return b != 0, nil case uint8: return b != 0, nil case uint16: return b != 0, nil case uint32: return b != 0, nil case uint64: return b != 0, nil case float32: return b != 0, nil case float64: return b != 0, nil case time.Duration: return b != 0, nil case string: return strconv.ParseBool(b) case json.Number: v, err := ToInt64E(b) if err == nil { return v != 0, nil } return false, fmt.Errorf(errorMsg, i, i, false) default: if i, ok := resolveAlias(i); ok { return ToBoolE(i) } return false, fmt.Errorf(errorMsg, i, i, false) } } // ToStringE casts any value to a string type. func ToStringE(i any) (string, error) { switch s := i.(type) { case string: return s, nil case bool: return strconv.FormatBool(s), nil case float64: return strconv.FormatFloat(s, 'f', -1, 64), nil case float32: return strconv.FormatFloat(float64(s), 'f', -1, 32), nil case int: return strconv.Itoa(s), nil case int8: return strconv.FormatInt(int64(s), 10), nil case int16: return strconv.FormatInt(int64(s), 10), nil case int32: return strconv.FormatInt(int64(s), 10), nil case int64: return strconv.FormatInt(s, 10), nil case uint: return strconv.FormatUint(uint64(s), 10), nil case uint8: return strconv.FormatUint(uint64(s), 10), nil case uint16: return strconv.FormatUint(uint64(s), 10), nil case uint32: return strconv.FormatUint(uint64(s), 10), nil case uint64: return strconv.FormatUint(s, 10), nil case json.Number: return s.String(), nil case []byte: return string(s), nil case template.HTML: return string(s), nil case template.URL: return string(s), nil case template.JS: return string(s), nil case template.CSS: return string(s), nil case template.HTMLAttr: return string(s), nil case nil: return "", nil case fmt.Stringer: return s.String(), nil case error: return s.Error(), nil default: if i, ok := indirect(i); ok { return ToStringE(i) } if i, ok := resolveAlias(i); ok { return ToStringE(i) } return "", fmt.Errorf(errorMsg, i, i, "") } } spf13-cast-3efe057/basic_test.go000066400000000000000000000067251505760405300165040ustar00rootroot00000000000000// Copyright © 2014 Steve Francia . // // Use of this source code is governed by an MIT-style // license that can be found in the LICENSE file. package cast_test import ( "encoding/json" "html/template" "testing" "time" "github.com/spf13/cast" ) func TestBool(t *testing.T) { var ptr *bool testCases := []testCase{ {0, false, false}, {int(0), false, false}, {int8(0), false, false}, {int16(0), false, false}, {int32(0), false, false}, {int64(0), false, false}, {uint(0), false, false}, {uint8(0), false, false}, {uint16(0), false, false}, {uint32(0), false, false}, {uint64(0), false, false}, {float32(0), false, false}, {float32(0.0), false, false}, {float64(0), false, false}, {float64(0.0), false, false}, {time.Duration(0), false, false}, {json.Number("0"), false, false}, {nil, false, false}, {ptr, false, false}, {"false", false, false}, {"FALSE", false, false}, {"False", false, false}, {"f", false, false}, {"F", false, false}, {false, false, false}, {"true", true, false}, {"TRUE", true, false}, {"True", true, false}, {"t", true, false}, {"T", true, false}, {1, true, false}, {int(1), true, false}, {int8(1), true, false}, {int16(1), true, false}, {int32(1), true, false}, {int64(1), true, false}, {uint(1), true, false}, {uint8(1), true, false}, {uint16(1), true, false}, {uint32(1), true, false}, {uint64(1), true, false}, {float32(1), true, false}, {float32(1.0), true, false}, {float64(1), true, false}, {float64(1.0), true, false}, {time.Duration(1), true, false}, {json.Number("1"), true, false}, {json.Number("1.0"), true, false}, {true, true, false}, {-1, true, false}, {int(-1), true, false}, {int8(-1), true, false}, {int16(-1), true, false}, {int32(-1), true, false}, {int64(-1), true, false}, // Alias {MyBool(true), true, false}, {MyBool(false), false, false}, // Failure cases {"test", false, true}, {testing.T{}, false, true}, } runTests(t, testCases, cast.ToBool, cast.ToBoolE) } func BenchmarkToBool(b *testing.B) { for i := 0; i < b.N; i++ { if !cast.ToBool(true) { b.Fatal("ToBool returned false") } } } func TestString(t *testing.T) { type Key struct { k string } key := &Key{"foo"} var ptr *string testCases := []testCase{ {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}, {json.Number("8"), "8", false}, {true, "true", false}, {false, "false", false}, {nil, "", false}, {ptr, "", false}, {[]byte("one time"), "one time", false}, {"one more time", "one more time", false}, {template.HTML("one time"), "one time", false}, {template.URL("http://somehost.foo"), "http://somehost.foo", false}, {template.JS("(1+2)"), "(1+2)", false}, {template.CSS("a"), "a", false}, {template.HTMLAttr("a"), "a", false}, // Alias {MyString("foo"), "foo", false}, // Stringer and error {foo{val: "bar"}, "bar", false}, {fu{val: "bar"}, "bar", false}, // Failure cases {testing.T{}, "", true}, {key, "", true}, } runTests(t, testCases, cast.ToString, cast.ToStringE) } type foo struct { val string } func (x foo) String() string { return x.val } type fu struct { val string } func (x fu) Error() string { return x.val } spf13-cast-3efe057/cast.go000066400000000000000000000037521505760405300153130ustar00rootroot00000000000000// Copyright © 2014 Steve Francia . // // Use of this source code is governed by an MIT-style // license that can be found in the LICENSE file. // Package cast provides easy and safe casting in Go. package cast import "time" const errorMsg = "unable to cast %#v of type %T to %T" const errorMsgWith = "unable to cast %#v of type %T to %T: %w" // Basic is a type parameter constraint for functions accepting basic types. // // It represents the supported basic types this package can cast to. type Basic interface { string | bool | Number | time.Time | time.Duration } // ToE casts any value to a [Basic] type. func ToE[T Basic](i any) (T, error) { var t T var v any var err error switch any(t).(type) { case string: v, err = ToStringE(i) case bool: v, err = ToBoolE(i) case int: v, err = toNumberE[int](i, parseInt[int]) case int8: v, err = toNumberE[int8](i, parseInt[int8]) case int16: v, err = toNumberE[int16](i, parseInt[int16]) case int32: v, err = toNumberE[int32](i, parseInt[int32]) case int64: v, err = toNumberE[int64](i, parseInt[int64]) case uint: v, err = toUnsignedNumberE[uint](i, parseUint[uint]) case uint8: v, err = toUnsignedNumberE[uint8](i, parseUint[uint8]) case uint16: v, err = toUnsignedNumberE[uint16](i, parseUint[uint16]) case uint32: v, err = toUnsignedNumberE[uint32](i, parseUint[uint32]) case uint64: v, err = toUnsignedNumberE[uint64](i, parseUint[uint64]) case float32: v, err = toNumberE[float32](i, parseFloat[float32]) case float64: v, err = toNumberE[float64](i, parseFloat[float64]) case time.Time: v, err = ToTimeE(i) case time.Duration: v, err = ToDurationE(i) } if err != nil { return t, err } return v.(T), nil } // Must is a helper that wraps a call to a cast function and panics if the error is non-nil. func Must[T any](i any, err error) T { if err != nil { panic(err) } return i.(T) } // To casts any value to a [Basic] type. func To[T Basic](i any) T { v, _ := ToE[T](i) return v } spf13-cast-3efe057/cast_test.go000066400000000000000000000110671505760405300163500ustar00rootroot00000000000000// Copyright © 2014 Steve Francia . // // Use of this source code is governed by an MIT-style // license that can be found in the LICENSE file. package cast_test import ( "fmt" "testing" "time" qt "github.com/frankban/quicktest" "github.com/spf13/cast" ) type testCase struct { input any expected any expectError bool } func runTests[T cast.Basic](t *testing.T, testCases []testCase, to func(i any) T, toErr func(i any) (T, error)) { var typ T _, isTime := any(typ).(time.Time) res := func(i any) any { return i } if isTime { res = func(i any) any { return i.(time.Time).UTC() } } for _, testCase := range testCases { // TODO: remove after minimum Go version is >=1.22 testCase := testCase t.Run("", func(t *testing.T) { t.Parallel() t.Run("Value", func(t *testing.T) { t.Run("ToType", func(t *testing.T) { t.Parallel() c := qt.New(t) v := to(testCase.input) c.Assert(res(v), qt.Equals, testCase.expected) }) t.Run("To", func(t *testing.T) { t.Parallel() c := qt.New(t) v := cast.To[T](testCase.input) c.Assert(res(v), qt.Equals, testCase.expected) }) t.Run("ToTypeE", func(t *testing.T) { t.Parallel() c := qt.New(t) v, err := toErr(testCase.input) if testCase.expectError { c.Assert(err, qt.IsNotNil) } else { c.Assert(err, qt.IsNil) c.Assert(res(v), qt.Equals, testCase.expected) } }) t.Run("ToE", func(t *testing.T) { t.Parallel() c := qt.New(t) v, err := cast.ToE[T](testCase.input) if testCase.expectError { c.Assert(err, qt.IsNotNil) } else { c.Assert(err, qt.IsNil) c.Assert(res(v), qt.Equals, testCase.expected) } }) }) t.Run("Pointer", func(t *testing.T) { t.Run("ToType", func(t *testing.T) { t.Parallel() c := qt.New(t) v := to(&testCase.input) c.Assert(res(v), qt.Equals, testCase.expected) }) t.Run("To", func(t *testing.T) { t.Parallel() c := qt.New(t) v := cast.To[T](&testCase.input) c.Assert(res(v), qt.Equals, testCase.expected) }) t.Run("ToTypeE", func(t *testing.T) { t.Parallel() c := qt.New(t) v, err := toErr(&testCase.input) if testCase.expectError { c.Assert(err, qt.IsNotNil) } else { c.Assert(err, qt.IsNil) c.Assert(res(v), qt.Equals, testCase.expected) } }) t.Run("ToE", func(t *testing.T) { t.Parallel() c := qt.New(t) v, err := cast.ToE[T](&testCase.input) if testCase.expectError { c.Assert(err, qt.IsNotNil) } else { c.Assert(err, qt.IsNil) c.Assert(res(v), qt.Equals, testCase.expected) } }) }) }) } } func Example() { // Cast a value to another type { v, err := cast.ToIntE("1234") if err != nil { panic(err) } fmt.Printf("%T(%v)\n", v, v) } // Alternatively, you can use the generic [ToE] function for [Basic] types { v, err := cast.ToE[int]("4321") if err != nil { panic(err) } fmt.Printf("%T(%v)\n", v, v) } // You can suppress errors by using the non-error versions { v := cast.ToInt("9876") fmt.Printf("%T(%v)\n", v, v) } // Similarly, there is a generic [To] function for [Basic] types { v := cast.To[int]("6789") fmt.Printf("%T(%v)\n", v, v) } // Finally, you can use [Must] to panic if there is an error. { v := cast.Must[int](cast.ToE[int]("5555")) fmt.Printf("%T(%v)\n", v, v) } // Output: // int(1234) // int(4321) // int(9876) // int(6789) // int(5555) } func BenchmarkCast(b *testing.B) { b.Run("Bool", func(b *testing.B) { for i := 0; i < b.N; i++ { cast.ToBool("true") } }) b.Run("String", func(b *testing.B) { for i := 0; i < b.N; i++ { cast.ToString(123456789) } }) b.Run("Number", func(b *testing.B) { for i := 0; i < b.N; i++ { cast.ToNumber[int]("123456789") } }) b.Run("Int64", func(b *testing.B) { for i := 0; i < b.N; i++ { cast.ToInt64("123456789") } }) b.Run("BoolSlice", func(b *testing.B) { for i := 0; i < b.N; i++ { cast.ToBoolSlice([]string{"true", "false", "TRUE", "false"}) } }) b.Run("StringSlice", func(b *testing.B) { for i := 0; i < b.N; i++ { cast.ToStringSlice([]int{123456789, 123456789, 123456789, 123456789}) } }) } // Alias types for alias testing type MyString string type MyBool bool type MyInt int type MyInt8 int8 type MyInt16 int16 type MyInt32 int32 type MyInt64 int64 type MyUint uint type MyUint8 uint8 type MyUint16 uint16 type MyUint32 uint32 type MyUint64 uint64 type MyFloat32 float32 type MyFloat64 float64 spf13-cast-3efe057/generator/000077500000000000000000000000001505760405300160115ustar00rootroot00000000000000spf13-cast-3efe057/generator/go.mod000066400000000000000000000001331505760405300171140ustar00rootroot00000000000000module github.com/spf13/cast/generator go 1.24.3 require github.com/dave/jennifer v1.7.1 spf13-cast-3efe057/generator/go.sum000066400000000000000000000002471505760405300171470ustar00rootroot00000000000000github.com/dave/jennifer v1.7.1 h1:B4jJJDHelWcDhlRQxWeo0Npa/pYKBLrirAQoTN45txo= github.com/dave/jennifer v1.7.1/go.mod h1:nXbxhEmQfOZhWml3D1cDK5M1FLnMSozpbFN/m3RmGZc= spf13-cast-3efe057/generator/main.go000066400000000000000000000065101505760405300172660ustar00rootroot00000000000000package main import ( "fmt" "strings" . "github.com/dave/jennifer/jen" ) //go:generate sh -c "go run . > ../zz_generated.go" var toFuncs = []struct { name string returnType *Statement }{ {"ToBool", Bool()}, {"ToString", String()}, {"ToTime", Qual("time", "Time")}, {"ToTimeInDefaultLocation", Qual("time", "Time")}, {"ToDuration", Qual("time", "Duration")}, {"ToInt", Int()}, {"ToInt8", Int8()}, {"ToInt16", Int16()}, {"ToInt32", Int32()}, {"ToInt64", Int64()}, {"ToUint", Uint()}, {"ToUint8", Uint8()}, {"ToUint16", Uint16()}, {"ToUint32", Uint32()}, {"ToUint64", Uint64()}, {"ToFloat32", Float32()}, {"ToFloat64", Float64()}, {"ToStringMapString", Map(String()).String()}, {"ToStringMapStringSlice", Map(String()).Index().String()}, {"ToStringMapBool", Map(String()).Bool()}, {"ToStringMapInt", Map(String()).Int()}, {"ToStringMapInt64", Map(String()).Int64()}, {"ToStringMap", Map(String()).Any()}, {"ToSlice", Index().Any()}, {"ToBoolSlice", Index().Bool()}, {"ToStringSlice", Index().String()}, {"ToIntSlice", Index().Int()}, {"ToInt64Slice", Index().Int64()}, {"ToUintSlice", Index().Uint()}, {"ToFloat64Slice", Index().Float64()}, {"ToDurationSlice", Index().Qual("time", "Duration")}, } var toSliceFuncs = []struct { typeName string returnType *Statement }{ {"bool", Bool()}, // {"time", Qual("time", "Time")}, {"duration", Qual("time", "Duration")}, {"int", Int()}, {"int8", Int8()}, {"int16", Int16()}, {"int32", Int32()}, {"int64", Int64()}, {"uint", Uint()}, {"uint8", Uint8()}, {"uint16", Uint16()}, {"uint32", Uint32()}, {"uint64", Uint64()}, {"float32", Float32()}, {"float64", Float64()}, } func main() { file := NewFile("cast") file.HeaderComment("Code generated by cast generator. DO NOT EDIT.") for _, fn := range toFuncs { if fn.name == "ToTimeInDefaultLocation" { toFuncWithParams(file, fn.name, fn.returnType, Id("location").Op("*").Qual("time", "Location")) } else { toFunc(file, fn.name, fn.returnType) } } for _, fn := range toSliceFuncs { toSliceEFunc(file, fn.typeName, fn.returnType) } fmt.Printf("%#v", file) } func toFunc(file *File, funcName string, returnType *Statement) { toFuncWithParams(file, funcName, returnType) } func toFuncWithParams(file *File, funcName string, returnType *Statement, args ...*Statement) { file.Comment(fmt.Sprintf("%s casts any value to a(n) %s type.", funcName, returnType.GoString())) varI := Id("i") arguments := []Code{varI.Clone().Any()} for _, arg := range args { arguments = append(arguments, arg) } file.Func(). Id(funcName).Params(arguments...).Params(returnType). BlockFunc(func(g *Group) { varV := Id("v") arguments := []Code{varI} for _, arg := range args { arguments = append(arguments, (*arg)[0]) } g.List(varV, Id("_")).Op(":=").Id(funcName + "E").Call(arguments...) g.Return(varV) }) } func toSliceEFunc(file *File, typeName string, returnType *Statement) { funcName := "To" + strings.ToUpper(typeName[:1]) + typeName[1:] + "SliceE" sliceReturnType := Index().Add(returnType) file.Comment(fmt.Sprintf("%s casts any value to a(n) %s type.", funcName, sliceReturnType.GoString())) varI := Id("i") file.Func(). Id(funcName).Params(varI.Clone().Any()).Params(sliceReturnType, Error()). BlockFunc(func(g *Group) { g.Return(Id("toSliceE").Types(returnType).Call(varI)) }) } spf13-cast-3efe057/go.mod000066400000000000000000000004251505760405300151320ustar00rootroot00000000000000module github.com/spf13/cast go 1.21.0 require github.com/frankban/quicktest v1.14.6 require ( github.com/google/go-cmp v0.5.9 // indirect github.com/kr/pretty v0.3.1 // indirect github.com/kr/text v0.2.0 // indirect github.com/rogpeppe/go-internal v1.9.0 // indirect ) spf13-cast-3efe057/go.sum000066400000000000000000000020131505760405300151520ustar00rootroot00000000000000github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= spf13-cast-3efe057/indirect.go000066400000000000000000000014721505760405300161570ustar00rootroot00000000000000// Copyright © 2014 Steve Francia . // // Use of this source code is governed by an MIT-style // license that can be found in the LICENSE file. package cast import ( "reflect" ) // From html/template/content.go // Copyright 2011 The Go Authors. All rights reserved. // indirect returns the value, after dereferencing as many times // as necessary to reach the base type (or nil). func indirect(i any) (any, bool) { if i == nil { return nil, false } if t := reflect.TypeOf(i); t.Kind() != reflect.Ptr { // Avoid creating a reflect.Value if it's not a pointer. return i, false } v := reflect.ValueOf(i) for v.Kind() == reflect.Ptr || (v.Kind() == reflect.Interface && v.Elem().Kind() == reflect.Ptr) { if v.IsNil() { return nil, true } v = v.Elem() } return v.Interface(), true } spf13-cast-3efe057/indirect_test.go000066400000000000000000000021011505760405300172040ustar00rootroot00000000000000// Copyright © 2014 Steve Francia . // // Use of this source code is governed by an MIT-style // license that can be found in the LICENSE file. package cast import ( "testing" qt "github.com/frankban/quicktest" ) func TestIndirect(t *testing.T) { c := qt.New(t) x := 13 y := &x z := &y var ptr *string var ptrptr **string var i any var n = int(13) var i2 any = n var i3 any = &n var b *bool var i4 any = b testCases := []struct { input any expected any expectedOk bool }{ {x, 13, false}, {y, 13, true}, {z, 13, true}, {ptr, nil, true}, {ptrptr, nil, true}, {i, nil, false}, {&i, nil, true}, {i2, 13, false}, {&i2, 13, true}, {i3, 13, true}, {&i3, 13, true}, {i4, nil, true}, {&i4, nil, true}, {nil, nil, false}, } for _, testCase := range testCases { // TODO: remove after minimum Go version is >=1.22 testCase := testCase t.Run("", func(t *testing.T) { v, ok := indirect(testCase.input) c.Assert(v, qt.Equals, testCase.expected) c.Assert(ok, qt.Equals, testCase.expectedOk) }) } } spf13-cast-3efe057/internal/000077500000000000000000000000001505760405300156375ustar00rootroot00000000000000spf13-cast-3efe057/internal/time.go000066400000000000000000000052101505760405300171220ustar00rootroot00000000000000package internal import ( "fmt" "time" ) //go:generate stringer -type=TimeFormatType type TimeFormatType int const ( TimeFormatNoTimezone TimeFormatType = iota TimeFormatNamedTimezone TimeFormatNumericTimezone TimeFormatNumericAndNamedTimezone TimeFormatTimeOnly ) type TimeFormat struct { Format string Typ TimeFormatType } func (f TimeFormat) HasTimezone() bool { // We don't include the formats with only named timezones, see // https://github.com/golang/go/issues/19694#issuecomment-289103522 return f.Typ >= TimeFormatNumericTimezone && f.Typ <= TimeFormatNumericAndNamedTimezone } var TimeFormats = []TimeFormat{ // Keep common formats at the top. {"2006-01-02", TimeFormatNoTimezone}, {time.RFC3339, TimeFormatNumericTimezone}, {"2006-01-02T15:04:05", TimeFormatNoTimezone}, // iso8601 without timezone {time.RFC1123Z, TimeFormatNumericTimezone}, {time.RFC1123, TimeFormatNamedTimezone}, {time.RFC822Z, TimeFormatNumericTimezone}, {time.RFC822, TimeFormatNamedTimezone}, {time.RFC850, TimeFormatNamedTimezone}, {"2006-01-02 15:04:05.999999999 -0700 MST", TimeFormatNumericAndNamedTimezone}, // Time.String() {"2006-01-02T15:04:05-0700", TimeFormatNumericTimezone}, // RFC3339 without timezone hh:mm colon {"2006-01-02 15:04:05Z0700", TimeFormatNumericTimezone}, // RFC3339 without T or timezone hh:mm colon {"2006-01-02 15:04:05", TimeFormatNoTimezone}, {time.ANSIC, TimeFormatNoTimezone}, {time.UnixDate, TimeFormatNamedTimezone}, {time.RubyDate, TimeFormatNumericTimezone}, {"2006-01-02 15:04:05Z07:00", TimeFormatNumericTimezone}, {"02 Jan 2006", TimeFormatNoTimezone}, {"2006-01-02 15:04:05 -07:00", TimeFormatNumericTimezone}, {"2006-01-02 15:04:05 -0700", TimeFormatNumericTimezone}, {time.Kitchen, TimeFormatTimeOnly}, {time.Stamp, TimeFormatTimeOnly}, {time.StampMilli, TimeFormatTimeOnly}, {time.StampMicro, TimeFormatTimeOnly}, {time.StampNano, TimeFormatTimeOnly}, } func ParseDateWith(s string, location *time.Location, formats []TimeFormat) (d time.Time, e error) { for _, format := range formats { if d, e = time.Parse(format.Format, s); e == nil { // Some time formats have a zone name, but no offset, so it gets // put in that zone name (not the default one passed in to us), but // without that zone's offset. So set the location manually. if format.Typ <= TimeFormatNamedTimezone { if location == nil { location = time.Local } year, month, day := d.Date() hour, min, sec := d.Clock() d = time.Date(year, month, day, hour, min, sec, d.Nanosecond(), location) } return } } return d, fmt.Errorf("unable to parse date: %s", s) } spf13-cast-3efe057/internal/timeformattype_string.go000066400000000000000000000016521505760405300226310ustar00rootroot00000000000000// Code generated by "stringer -type=TimeFormatType"; DO NOT EDIT. package internal import "strconv" func _() { // An "invalid array index" compiler error signifies that the constant values have changed. // Re-run the stringer command to generate them again. var x [1]struct{} _ = x[TimeFormatNoTimezone-0] _ = x[TimeFormatNamedTimezone-1] _ = x[TimeFormatNumericTimezone-2] _ = x[TimeFormatNumericAndNamedTimezone-3] _ = x[TimeFormatTimeOnly-4] } const _TimeFormatType_name = "TimeFormatNoTimezoneTimeFormatNamedTimezoneTimeFormatNumericTimezoneTimeFormatNumericAndNamedTimezoneTimeFormatTimeOnly" var _TimeFormatType_index = [...]uint8{0, 20, 43, 68, 101, 119} func (i TimeFormatType) String() string { if i < 0 || i >= TimeFormatType(len(_TimeFormatType_index)-1) { return "TimeFormatType(" + strconv.FormatInt(int64(i), 10) + ")" } return _TimeFormatType_name[_TimeFormatType_index[i]:_TimeFormatType_index[i+1]] } spf13-cast-3efe057/map.go000066400000000000000000000104161505760405300151310ustar00rootroot00000000000000// Copyright © 2014 Steve Francia . // // Use of this source code is governed by an MIT-style // license that can be found in the LICENSE file. package cast import ( "encoding/json" "fmt" "reflect" ) func toMapE[K comparable, V any](i any, keyFn func(any) K, valFn func(any) V) (map[K]V, error) { m := map[K]V{} if i == nil { return m, fmt.Errorf(errorMsg, i, i, m) } switch v := i.(type) { case map[K]V: return v, nil case map[K]any: for k, val := range v { m[k] = valFn(val) } return m, nil case map[any]V: for k, val := range v { m[keyFn(k)] = val } return m, nil case map[any]any: for k, val := range v { m[keyFn(k)] = valFn(val) } return m, nil case string: err := jsonStringToObject(v, &m) return m, err default: return m, fmt.Errorf(errorMsg, i, i, m) } } func toStringMapE[T any](i any, fn func(any) T) (map[string]T, error) { return toMapE(i, ToString, fn) } // ToStringMapStringE casts any value to a map[string]string type. func ToStringMapStringE(i any) (map[string]string, error) { return toStringMapE(i, ToString) } // ToStringMapStringSliceE casts any value to a map[string][]string type. func ToStringMapStringSliceE(i any) (map[string][]string, error) { m := map[string][]string{} switch v := i.(type) { case map[string][]string: return v, nil case map[string][]any: for k, val := range v { m[ToString(k)] = ToStringSlice(val) } return m, nil case map[string]string: for k, val := range v { m[ToString(k)] = []string{val} } case map[string]any: for k, val := range v { switch vt := val.(type) { case []any: m[ToString(k)] = ToStringSlice(vt) case []string: m[ToString(k)] = vt default: m[ToString(k)] = []string{ToString(val)} } } return m, nil case map[any][]string: for k, val := range v { m[ToString(k)] = ToStringSlice(val) } return m, nil case map[any]string: for k, val := range v { m[ToString(k)] = ToStringSlice(val) } return m, nil case map[any][]any: for k, val := range v { m[ToString(k)] = ToStringSlice(val) } return m, nil case map[any]any: for k, val := range v { key, err := ToStringE(k) if err != nil { return m, fmt.Errorf(errorMsg, i, i, m) } value, err := ToStringSliceE(val) if err != nil { return m, fmt.Errorf(errorMsg, i, i, m) } m[key] = value } case string: err := jsonStringToObject(v, &m) return m, err default: return m, fmt.Errorf(errorMsg, i, i, m) } return m, nil } // ToStringMapBoolE casts any value to a map[string]bool type. func ToStringMapBoolE(i any) (map[string]bool, error) { return toStringMapE(i, ToBool) } // ToStringMapE casts any value to a map[string]any type. func ToStringMapE(i any) (map[string]any, error) { fn := func(i any) any { return i } return toStringMapE(i, fn) } func toStringMapIntE[T int | int64](i any, fn func(any) T, fnE func(any) (T, error)) (map[string]T, error) { m := map[string]T{} if i == nil { return nil, fmt.Errorf(errorMsg, i, i, m) } switch v := i.(type) { case map[string]T: return v, nil case map[string]any: for k, val := range v { m[k] = fn(val) } return m, nil case map[any]T: for k, val := range v { m[ToString(k)] = val } return m, nil case map[any]any: for k, val := range v { m[ToString(k)] = fn(val) } return m, nil case string: err := jsonStringToObject(v, &m) return m, err } if reflect.TypeOf(i).Kind() != reflect.Map { return m, fmt.Errorf(errorMsg, i, i, m) } mVal := reflect.ValueOf(m) v := reflect.ValueOf(i) for _, keyVal := range v.MapKeys() { val, err := fnE(v.MapIndex(keyVal).Interface()) if err != nil { return m, fmt.Errorf(errorMsg, i, i, m) } mVal.SetMapIndex(keyVal, reflect.ValueOf(val)) } return m, nil } // ToStringMapIntE casts any value to a map[string]int type. func ToStringMapIntE(i any) (map[string]int, error) { return toStringMapIntE(i, ToInt, ToIntE) } // ToStringMapInt64E casts any value to a map[string]int64 type. func ToStringMapInt64E(i any) (map[string]int64, error) { return toStringMapIntE(i, ToInt64, ToInt64E) } // jsonStringToObject attempts to unmarshall a string as JSON into // the object passed as pointer. func jsonStringToObject(s string, v any) error { data := []byte(s) return json.Unmarshal(data, v) } spf13-cast-3efe057/map_test.go000066400000000000000000000247251505760405300162000ustar00rootroot00000000000000// Copyright © 2014 Steve Francia . // // Use of this source code is governed by an MIT-style // license that can be found in the LICENSE file. package cast_test import ( "testing" qt "github.com/frankban/quicktest" "github.com/spf13/cast" ) func runMapTests[K comparable, V cast.Basic | any](t *testing.T, testCases []testCase, to func(i any) map[K]V, toErr func(i any) (map[K]V, error)) { for _, testCase := range testCases { // TODO: remove after minimum Go version is >=1.22 testCase := testCase t.Run("", func(t *testing.T) { t.Parallel() t.Run("Value", func(t *testing.T) { t.Run("ToType", func(t *testing.T) { t.Parallel() c := qt.New(t) v := to(testCase.input) if v == nil { return } c.Assert(v, qt.DeepEquals, testCase.expected) }) // t.Run("To", func(t *testing.T) { // return // t.Parallel() // c := qt.New(t) // v := cast.To[T](testCase.input) // c.Assert(v, qt.DeepEquals, testCase.expected) // }) t.Run("ToTypeE", func(t *testing.T) { t.Parallel() c := qt.New(t) v, err := toErr(testCase.input) if testCase.expectError { c.Assert(err, qt.IsNotNil) } else { c.Assert(err, qt.IsNil) c.Assert(v, qt.DeepEquals, testCase.expected) } }) // t.Run("ToE", func(t *testing.T) { // return // t.Parallel() // c := qt.New(t) // v, err := cast.ToE[T](testCase.input) // if testCase.expectError { // c.Assert(err, qt.IsNotNil) // } else { // c.Assert(err, qt.IsNil) // c.Assert(v, qt.DeepEquals, testCase.expected) // } // }) }) // t.Run("Pointer", func(t *testing.T) { // t.Run("ToType", func(t *testing.T) { // t.Parallel() // c := qt.New(t) // v := to(&testCase.input) // if v == nil { // return // } // c.Assert(v, qt.DeepEquals, testCase.expected) // }) // // t.Run("To", func(t *testing.T) { // // return // // t.Parallel() // // c := qt.New(t) // // v := cast.To[T](&testCase.input) // // c.Assert(v, qt.DeepEquals, testCase.expected) // // }) // t.Run("ToTypeE", func(t *testing.T) { // t.Parallel() // c := qt.New(t) // v, err := toErr(&testCase.input) // if testCase.expectError { // c.Assert(err, qt.IsNotNil) // } else { // c.Assert(err, qt.IsNil) // c.Assert(v, qt.DeepEquals, testCase.expected) // } // }) // // t.Run("ToE", func(t *testing.T) { // // return // // t.Parallel() // // c := qt.New(t) // // v, err := cast.ToE[T](&testCase.input) // // if testCase.expectError { // // c.Assert(err, qt.IsNotNil) // // } else { // // c.Assert(err, qt.IsNil) // // c.Assert(v, qt.DeepEquals, testCase.expected) // // } // // }) // }) }) } } func TestStringMapStringSlice(t *testing.T) { // ToStringMapString inputs/outputs var stringMapString = map[string]string{"key 1": "value 1", "key 2": "value 2", "key 3": "value 3"} var stringMapInterface = map[string]any{"key 1": "value 1", "key 2": "value 2", "key 3": "value 3"} var interfaceMapString = map[any]string{"key 1": "value 1", "key 2": "value 2", "key 3": "value 3"} var interfaceMapInterface = map[any]any{"key 1": "value 1", "key 2": "value 2", "key 3": "value 3"} // ToStringMapStringSlice inputs/outputs var stringMapStringSlice = map[string][]string{"key 1": {"value 1", "value 2", "value 3"}, "key 2": {"value 1", "value 2", "value 3"}, "key 3": {"value 1", "value 2", "value 3"}} var stringMapInterfaceSlice = map[string][]any{"key 1": {"value 1", "value 2", "value 3"}, "key 2": {"value 1", "value 2", "value 3"}, "key 3": {"value 1", "value 2", "value 3"}} var stringMapInterfaceInterfaceSlice = map[string]any{"key 1": []any{"value 1", "value 2", "value 3"}, "key 2": []any{"value 1", "value 2", "value 3"}, "key 3": []any{"value 1", "value 2", "value 3"}} var stringMapStringSingleSliceFieldsResult = map[string][]string{"key 1": {"value", "1"}, "key 2": {"value", "2"}, "key 3": {"value", "3"}} var interfaceMapStringSlice = map[any][]string{"key 1": {"value 1", "value 2", "value 3"}, "key 2": {"value 1", "value 2", "value 3"}, "key 3": {"value 1", "value 2", "value 3"}} var interfaceMapInterfaceSlice = map[any][]any{"key 1": {"value 1", "value 2", "value 3"}, "key 2": {"value 1", "value 2", "value 3"}, "key 3": {"value 1", "value 2", "value 3"}} var stringMapStringSliceMultiple = map[string][]string{"key 1": {"value 1", "value 2", "value 3"}, "key 2": {"value 1", "value 2", "value 3"}, "key 3": {"value 1", "value 2", "value 3"}} var stringMapStringSliceSingle = map[string][]string{"key 1": {"value 1"}, "key 2": {"value 2"}, "key 3": {"value 3"}} var stringMapInterface1 = map[string]any{"key 1": []string{"value 1"}, "key 2": []string{"value 2"}} var stringMapInterfaceResult1 = map[string][]string{"key 1": {"value 1"}, "key 2": {"value 2"}} var jsonStringMapString = `{"key 1": "value 1", "key 2": "value 2"}` var jsonStringMapStringArray = `{"key 1": ["value 1"], "key 2": ["value 2", "value 3"]}` var jsonStringMapStringArrayResult = map[string][]string{"key 1": {"value 1"}, "key 2": {"value 2", "value 3"}} type Key struct { k string } testCases := []testCase{ {stringMapStringSlice, stringMapStringSlice, false}, {stringMapInterfaceSlice, stringMapStringSlice, false}, {stringMapInterfaceInterfaceSlice, stringMapStringSlice, false}, {stringMapStringSliceMultiple, stringMapStringSlice, false}, {stringMapStringSliceMultiple, stringMapStringSlice, false}, {stringMapString, stringMapStringSliceSingle, false}, {stringMapInterface, stringMapStringSliceSingle, false}, {stringMapInterface1, stringMapInterfaceResult1, false}, {interfaceMapStringSlice, stringMapStringSlice, false}, {interfaceMapInterfaceSlice, stringMapStringSlice, false}, {interfaceMapString, stringMapStringSingleSliceFieldsResult, false}, {interfaceMapInterface, stringMapStringSingleSliceFieldsResult, false}, {jsonStringMapStringArray, jsonStringMapStringArrayResult, false}, // Failure cases {nil, nil, true}, {testing.T{}, nil, true}, {map[any]any{"foo": testing.T{}}, nil, true}, {map[any]any{Key{"foo"}: "bar"}, nil, true}, // ToStringE(Key{"foo"}) should fail {jsonStringMapString, nil, true}, {"", nil, true}, } runMapTests(t, testCases, cast.ToStringMapStringSlice, cast.ToStringMapStringSliceE) } func TestStringMap(t *testing.T) { testCases := []testCase{ {map[any]any{"tag": "tags", "group": "groups"}, map[string]any{"tag": "tags", "group": "groups"}, false}, {map[string]any{"tag": "tags", "group": "groups"}, map[string]any{"tag": "tags", "group": "groups"}, false}, {`{"tag": "tags", "group": "groups"}`, map[string]any{"tag": "tags", "group": "groups"}, false}, {`{"tag": "tags", "group": true}`, map[string]any{"tag": "tags", "group": true}, false}, // Failure cases {nil, nil, true}, {testing.T{}, nil, true}, {"", nil, true}, } runMapTests(t, testCases, cast.ToStringMap, cast.ToStringMapE) } func TestStringMapBool(t *testing.T) { testCases := []testCase{ {map[any]any{"v1": true, "v2": false}, map[string]bool{"v1": true, "v2": false}, false}, {map[string]any{"v1": true, "v2": false}, map[string]bool{"v1": true, "v2": false}, false}, {map[string]bool{"v1": true, "v2": false}, map[string]bool{"v1": true, "v2": false}, false}, {`{"v1": true, "v2": false}`, map[string]bool{"v1": true, "v2": false}, false}, // Failure cases {nil, nil, true}, {testing.T{}, nil, true}, {"", nil, true}, } runMapTests(t, testCases, cast.ToStringMapBool, cast.ToStringMapBoolE) } func TestStringMapInt(t *testing.T) { testCases := []testCase{ {map[any]any{"v1": 1, "v2": 222}, map[string]int{"v1": 1, "v2": 222}, false}, {map[string]any{"v1": 342, "v2": 5141}, map[string]int{"v1": 342, "v2": 5141}, false}, {map[string]int{"v1": 33, "v2": 88}, map[string]int{"v1": 33, "v2": 88}, false}, {map[string]int32{"v1": int32(33), "v2": int32(88)}, map[string]int{"v1": 33, "v2": 88}, false}, {map[string]uint16{"v1": uint16(33), "v2": uint16(88)}, map[string]int{"v1": 33, "v2": 88}, false}, {map[string]float64{"v1": float64(8.22), "v2": float64(43.32)}, map[string]int{"v1": 8, "v2": 43}, false}, {`{"v1": 67, "v2": 56}`, map[string]int{"v1": 67, "v2": 56}, false}, // Failure cases {nil, nil, true}, {testing.T{}, nil, true}, {"", nil, true}, } runMapTests(t, testCases, cast.ToStringMapInt, cast.ToStringMapIntE) } func TestStringMapInt64(t *testing.T) { testCases := []testCase{ {map[any]any{"v1": int32(8), "v2": int32(888)}, map[string]int64{"v1": int64(8), "v2": int64(888)}, false}, {map[string]any{"v1": int64(45), "v2": int64(67)}, map[string]int64{"v1": 45, "v2": 67}, false}, {map[string]int64{"v1": 33, "v2": 88}, map[string]int64{"v1": 33, "v2": 88}, false}, {map[string]int{"v1": 33, "v2": 88}, map[string]int64{"v1": 33, "v2": 88}, false}, {map[string]int32{"v1": int32(33), "v2": int32(88)}, map[string]int64{"v1": 33, "v2": 88}, false}, {map[string]uint16{"v1": uint16(33), "v2": uint16(88)}, map[string]int64{"v1": 33, "v2": 88}, false}, {map[string]float64{"v1": float64(8.22), "v2": float64(43.32)}, map[string]int64{"v1": 8, "v2": 43}, false}, {`{"v1": 67, "v2": 56}`, map[string]int64{"v1": 67, "v2": 56}, false}, // Failure cases {nil, nil, true}, {testing.T{}, nil, true}, {"", nil, true}, } runMapTests(t, testCases, cast.ToStringMapInt64, cast.ToStringMapInt64E) } func TestStringMapString(t *testing.T) { var stringMapString = map[string]string{"key 1": "value 1", "key 2": "value 2", "key 3": "value 3"} var stringMapInterface = map[string]any{"key 1": "value 1", "key 2": "value 2", "key 3": "value 3"} var interfaceMapString = map[any]string{"key 1": "value 1", "key 2": "value 2", "key 3": "value 3"} var interfaceMapInterface = map[any]any{"key 1": "value 1", "key 2": "value 2", "key 3": "value 3"} var jsonString = `{"key 1": "value 1", "key 2": "value 2", "key 3": "value 3"}` var invalidJsonString = `{"key 1": "value 1", "key 2": "value 2", "key 3": "value 3"` var emptyString = "" testCases := []testCase{ {stringMapString, stringMapString, false}, {stringMapInterface, stringMapString, false}, {interfaceMapString, stringMapString, false}, {interfaceMapInterface, stringMapString, false}, {jsonString, stringMapString, false}, // Failure cases {nil, nil, true}, {testing.T{}, nil, true}, {invalidJsonString, nil, true}, {emptyString, nil, true}, } runMapTests(t, testCases, cast.ToStringMapString, cast.ToStringMapStringE) } spf13-cast-3efe057/number.go000066400000000000000000000240101505760405300156370ustar00rootroot00000000000000// Copyright © 2014 Steve Francia . // // Use of this source code is governed by an MIT-style // license that can be found in the LICENSE file. package cast import ( "encoding/json" "errors" "fmt" "regexp" "strconv" "strings" "time" ) var errNegativeNotAllowed = errors.New("unable to cast negative value") type float64EProvider interface { Float64() (float64, error) } type float64Provider interface { Float64() float64 } // Number is a type parameter constraint for functions accepting number types. // // It represents the supported number types this package can cast to. type Number interface { int | int8 | int16 | int32 | int64 | uint | uint8 | uint16 | uint32 | uint64 | float32 | float64 } type integer interface { int | int8 | int16 | int32 | int64 } type unsigned interface { uint | uint8 | uint16 | uint32 | uint64 } type float interface { float32 | float64 } // ToNumberE casts any value to a [Number] type. func ToNumberE[T Number](i any) (T, error) { var t T switch any(t).(type) { case int: return toNumberE[T](i, parseNumber[T]) case int8: return toNumberE[T](i, parseNumber[T]) case int16: return toNumberE[T](i, parseNumber[T]) case int32: return toNumberE[T](i, parseNumber[T]) case int64: return toNumberE[T](i, parseNumber[T]) case uint: return toUnsignedNumberE[T](i, parseNumber[T]) case uint8: return toUnsignedNumberE[T](i, parseNumber[T]) case uint16: return toUnsignedNumberE[T](i, parseNumber[T]) case uint32: return toUnsignedNumberE[T](i, parseNumber[T]) case uint64: return toUnsignedNumberE[T](i, parseNumber[T]) case float32: return toNumberE[T](i, parseNumber[T]) case float64: return toNumberE[T](i, parseNumber[T]) default: return 0, fmt.Errorf("unknown number type: %T", t) } } // ToNumber casts any value to a [Number] type. func ToNumber[T Number](i any) T { v, _ := ToNumberE[T](i) return v } // toNumber's semantics differ from other "to" functions. // It returns false as the second parameter if the conversion fails. // This is to signal other callers that they should proceed with their own conversions. func toNumber[T Number](i any) (T, bool) { i, _ = indirect(i) switch s := i.(type) { case T: return s, true case int: return T(s), true case int8: return T(s), true case int16: return T(s), true case int32: return T(s), true case int64: return T(s), true case uint: return T(s), true case uint8: return T(s), true case uint16: return T(s), true case uint32: return T(s), true case uint64: return T(s), true case float32: return T(s), true case float64: return T(s), true case bool: if s { return 1, true } return 0, true case nil: return 0, true case time.Weekday: return T(s), true case time.Month: return T(s), true } return 0, false } func toNumberE[T Number](i any, parseFn func(string) (T, error)) (T, error) { n, ok := toNumber[T](i) if ok { return n, nil } i, _ = indirect(i) switch s := i.(type) { case string: if s == "" { return 0, nil } v, err := parseFn(s) if err != nil { return 0, fmt.Errorf(errorMsgWith, i, i, n, err) } return v, nil case json.Number: if s == "" { return 0, nil } v, err := parseFn(string(s)) if err != nil { return 0, fmt.Errorf(errorMsgWith, i, i, n, err) } return v, nil case float64EProvider: if _, ok := any(n).(float64); !ok { return 0, fmt.Errorf(errorMsg, i, i, n) } v, err := s.Float64() if err != nil { return 0, fmt.Errorf(errorMsg, i, i, n) } return T(v), nil case float64Provider: if _, ok := any(n).(float64); !ok { return 0, fmt.Errorf(errorMsg, i, i, n) } return T(s.Float64()), nil default: if i, ok := resolveAlias(i); ok { return toNumberE(i, parseFn) } return 0, fmt.Errorf(errorMsg, i, i, n) } } func toUnsignedNumber[T Number](i any) (T, bool, bool) { i, _ = indirect(i) switch s := i.(type) { case T: return s, true, true case int: if s < 0 { return 0, false, false } return T(s), true, true case int8: if s < 0 { return 0, false, false } return T(s), true, true case int16: if s < 0 { return 0, false, false } return T(s), true, true case int32: if s < 0 { return 0, false, false } return T(s), true, true case int64: if s < 0 { return 0, false, false } return T(s), true, true case uint: return T(s), true, true case uint8: return T(s), true, true case uint16: return T(s), true, true case uint32: return T(s), true, true case uint64: return T(s), true, true case float32: if s < 0 { return 0, false, false } return T(s), true, true case float64: if s < 0 { return 0, false, false } return T(s), true, true case bool: if s { return 1, true, true } return 0, true, true case nil: return 0, true, true case time.Weekday: if s < 0 { return 0, false, false } return T(s), true, true case time.Month: if s < 0 { return 0, false, false } return T(s), true, true } return 0, true, false } func toUnsignedNumberE[T Number](i any, parseFn func(string) (T, error)) (T, error) { n, valid, ok := toUnsignedNumber[T](i) if ok { return n, nil } i, _ = indirect(i) if !valid { return 0, errNegativeNotAllowed } switch s := i.(type) { case string: if s == "" { return 0, nil } v, err := parseFn(s) if err != nil { return 0, fmt.Errorf(errorMsgWith, i, i, n, err) } return v, nil case json.Number: if s == "" { return 0, nil } v, err := parseFn(string(s)) if err != nil { return 0, fmt.Errorf(errorMsgWith, i, i, n, err) } return v, nil case float64EProvider: if _, ok := any(n).(float64); !ok { return 0, fmt.Errorf(errorMsg, i, i, n) } v, err := s.Float64() if err != nil { return 0, fmt.Errorf(errorMsg, i, i, n) } if v < 0 { return 0, errNegativeNotAllowed } return T(v), nil case float64Provider: if _, ok := any(n).(float64); !ok { return 0, fmt.Errorf(errorMsg, i, i, n) } v := s.Float64() if v < 0 { return 0, errNegativeNotAllowed } return T(v), nil default: if i, ok := resolveAlias(i); ok { return toUnsignedNumberE(i, parseFn) } return 0, fmt.Errorf(errorMsg, i, i, n) } } func parseNumber[T Number](s string) (T, error) { var t T switch any(t).(type) { case int: v, err := parseInt[int](s) return T(v), err case int8: v, err := parseInt[int8](s) return T(v), err case int16: v, err := parseInt[int16](s) return T(v), err case int32: v, err := parseInt[int32](s) return T(v), err case int64: v, err := parseInt[int64](s) return T(v), err case uint: v, err := parseUint[uint](s) return T(v), err case uint8: v, err := parseUint[uint8](s) return T(v), err case uint16: v, err := parseUint[uint16](s) return T(v), err case uint32: v, err := parseUint[uint32](s) return T(v), err case uint64: v, err := parseUint[uint64](s) return T(v), err case float32: v, err := strconv.ParseFloat(s, 32) return T(v), err case float64: v, err := strconv.ParseFloat(s, 64) return T(v), err default: return 0, fmt.Errorf("unknown number type: %T", t) } } func parseInt[T integer](s string) (T, error) { v, err := strconv.ParseInt(trimDecimal(s), 0, 0) if err != nil { return 0, err } return T(v), nil } func parseUint[T unsigned](s string) (T, error) { v, err := strconv.ParseUint(strings.TrimLeft(trimDecimal(s), "+"), 0, 0) if err != nil { return 0, err } return T(v), nil } func parseFloat[T float](s string) (T, error) { var t T var v any var err error switch any(t).(type) { case float32: n, e := strconv.ParseFloat(s, 32) v = float32(n) err = e case float64: n, e := strconv.ParseFloat(s, 64) v = float64(n) err = e } return v.(T), err } // ToFloat64E casts an interface to a float64 type. func ToFloat64E(i any) (float64, error) { return toNumberE[float64](i, parseFloat[float64]) } // ToFloat32E casts an interface to a float32 type. func ToFloat32E(i any) (float32, error) { return toNumberE[float32](i, parseFloat[float32]) } // ToInt64E casts an interface to an int64 type. func ToInt64E(i any) (int64, error) { return toNumberE[int64](i, parseInt[int64]) } // ToInt32E casts an interface to an int32 type. func ToInt32E(i any) (int32, error) { return toNumberE[int32](i, parseInt[int32]) } // ToInt16E casts an interface to an int16 type. func ToInt16E(i any) (int16, error) { return toNumberE[int16](i, parseInt[int16]) } // ToInt8E casts an interface to an int8 type. func ToInt8E(i any) (int8, error) { return toNumberE[int8](i, parseInt[int8]) } // ToIntE casts an interface to an int type. func ToIntE(i any) (int, error) { return toNumberE[int](i, parseInt[int]) } // ToUintE casts an interface to a uint type. func ToUintE(i any) (uint, error) { return toUnsignedNumberE[uint](i, parseUint[uint]) } // ToUint64E casts an interface to a uint64 type. func ToUint64E(i any) (uint64, error) { return toUnsignedNumberE[uint64](i, parseUint[uint64]) } // ToUint32E casts an interface to a uint32 type. func ToUint32E(i any) (uint32, error) { return toUnsignedNumberE[uint32](i, parseUint[uint32]) } // ToUint16E casts an interface to a uint16 type. func ToUint16E(i any) (uint16, error) { return toUnsignedNumberE[uint16](i, parseUint[uint16]) } // ToUint8E casts an interface to a uint type. func ToUint8E(i any) (uint8, error) { return toUnsignedNumberE[uint8](i, parseUint[uint8]) } func trimZeroDecimal(s string) string { var foundZero bool for i := len(s); i > 0; i-- { switch s[i-1] { case '.': if foundZero { return s[:i-1] } case '0': foundZero = true default: return s } } return s } var stringNumberRe = regexp.MustCompile(`^([-+]?\d*)(\.\d*)?$`) // see [BenchmarkDecimal] for details about the implementation func trimDecimal(s string) string { if !strings.Contains(s, ".") { return s } matches := stringNumberRe.FindStringSubmatch(s) if matches != nil { // matches[1] is the captured integer part with sign s = matches[1] // handle special cases switch s { case "-", "+": s += "0" case "": s = "0" } return s } return s } spf13-cast-3efe057/number_internal_test.go000066400000000000000000000103311505760405300205730ustar00rootroot00000000000000// Copyright © 2014 Steve Francia . // // Use of this source code is governed by an MIT-style // license that can be found in the LICENSE file. package cast import ( "regexp" "strconv" "strings" "testing" qt "github.com/frankban/quicktest" ) func BenchmarkToInt(b *testing.B) { convert := func(num52 interface{}) { if v := ToInt(num52); v != 52 { b.Fatalf("ToInt returned wrong value, got %d, want %d", v, 32) } } for i := 0; i < b.N; i++ { convert("52") convert(52.0) convert(uint64(52)) } } func BenchmarkTrimZeroDecimal(b *testing.B) { for i := 0; i < b.N; i++ { trimZeroDecimal("") trimZeroDecimal("123") trimZeroDecimal("120") trimZeroDecimal("120.00") } } func TestTrimZeroDecimal(t *testing.T) { c := qt.New(t) c.Assert(trimZeroDecimal("10.0"), qt.Equals, "10") c.Assert(trimZeroDecimal("10.00"), qt.Equals, "10") c.Assert(trimZeroDecimal("10.010"), qt.Equals, "10.010") c.Assert(trimZeroDecimal("0.0000000000"), qt.Equals, "0") c.Assert(trimZeroDecimal("0.00000000001"), qt.Equals, "0.00000000001") } func TestTrimDecimal(t *testing.T) { testCases := []struct { input string expected string }{ {"10.0", "10"}, {"10.010", "10"}, {"00000.00001", "00000"}, {"-0001.0", "-0001"}, {".5", "0"}, {"+12.", "+12"}, {"+.25", "+0"}, {"-.25", "-0"}, {"0.0000000000", "0"}, {"0.0000000001", "0"}, {"10.0000000000", "10"}, {"10.0000000001", "10"}, {"10000000000000.0000000000", "10000000000000"}, {"10...17", "10...17"}, {"10.foobar", "10.foobar"}, {"10.0i", "10.0i"}, {"10.0E9", "10.0E9"}, } for _, testCase := range testCases { // TODO: remove after minimum Go version is >=1.22 testCase := testCase t.Run(testCase.input, func(t *testing.T) { c := qt.New(t) c.Assert(trimDecimal(testCase.input), qt.Equals, testCase.expected) }) } } // Analysis (in the order of performance): // // - Trimming decimals based on decimal point yields a lot of incorrectly parsed values. // - Parsing to float might be better, but we still need to cast the number, it might overflow, problematic. // - Regex parsing is an order of magnitude slower, but it yields correct results. func BenchmarkDecimal(b *testing.B) { testCases := []struct { input string expectError bool }{ {"10.0", false}, {"10.00", false}, {"10.010", false}, {"0.0000000000", false}, {"0.0000000001", false}, {"10.0000000000", false}, {"10.0000000001", false}, {"10000000000000.0000000000", false}, // {"10...17", true}, // {"10.foobar", true}, // {"10.0i", true}, // {"10.0E9", true}, } trimDecimalString := func(s string) string { // trim the decimal part (if any) if i := strings.Index(s, "."); i >= 0 { s = s[:i] } return s } re := regexp.MustCompile(`^([-+]?\d*)(\.\d*)?$`) trimDecimalRegex := func(s string) string { matches := re.FindStringSubmatch(s) if matches != nil { // matches[1] is the captured integer part with sign return matches[1] } return s } for _, testCase := range testCases { // TODO: remove after minimum Go version is >=1.22 testCase := testCase b.Run(testCase.input, func(b *testing.B) { b.Run("ParseFloat", func(b *testing.B) { // TODO: use b.Loop() once updated to Go 1.24 for i := 0; i < b.N; i++ { v, err := strconv.ParseFloat(testCase.input, 64) if (err != nil) != testCase.expectError { if err != nil { b.Fatal(err) } b.Fatal("expected error, but got none") } n := int64(v) _ = n } }) b.Run("TrimDecimalString", func(b *testing.B) { // TODO: use b.Loop() once updated to Go 1.24 for i := 0; i < b.N; i++ { v, err := strconv.ParseInt(trimDecimalString(testCase.input), 0, 0) if (err != nil) != testCase.expectError { if err != nil { b.Fatal(err) } b.Fatal("expected error, but got none") } _ = v } }) b.Run("TrimDecimalRegex", func(b *testing.B) { // TODO: use b.Loop() once updated to Go 1.24 for i := 0; i < b.N; i++ { v, err := strconv.ParseInt(trimDecimalRegex(testCase.input), 0, 0) if (err != nil) != testCase.expectError { if err != nil { b.Fatal(err) } b.Fatal("expected error, but got none") } _ = v } }) }) } } spf13-cast-3efe057/number_test.go000066400000000000000000000331211505760405300167010ustar00rootroot00000000000000// Copyright © 2014 Steve Francia . // // Use of this source code is governed by an MIT-style // license that can be found in the LICENSE file. package cast_test import ( "encoding/json" "math" "math/big" "reflect" "testing" "time" qt "github.com/frankban/quicktest" "github.com/spf13/cast" ) type numberContext struct { to func(any) any toErr func(any) (any, error) specific func(any) any generic func(any) any specificErr func(any) (any, error) genericErr func(any) (any, error) // Order of samples: // zero, one, 8, -8, 8.3, -8.3, min, max, underflow string, overflow string samples []any } func toAny[T cast.Number](fn func(any) T) func(i any) any { return func(i any) any { return fn(i) } } func toAnyErr[T cast.Number](fn func(any) (T, error)) func(i any) (any, error) { return func(i any) (any, error) { return fn(i) } } var numberContexts = map[string]numberContext{ "int": { to: toAny(cast.To[int]), toErr: toAnyErr(cast.ToE[int]), specific: toAny(cast.ToInt), generic: toAny(cast.ToNumber[int]), specificErr: toAnyErr(cast.ToIntE), genericErr: toAnyErr(cast.ToNumberE[int]), samples: []any{int(0), int(1), int(8), int(-8), int(8), int(-8), MyInt(8), math.MinInt, math.MaxInt, new(big.Int).Sub(big.NewInt(math.MinInt), big.NewInt(1)).String(), new(big.Int).Add(big.NewInt(math.MaxInt), big.NewInt(1)).String()}, }, "int8": { to: toAny(cast.To[int8]), toErr: toAnyErr(cast.ToE[int8]), specific: toAny(cast.ToInt8), generic: toAny(cast.ToNumber[int8]), specificErr: toAnyErr(cast.ToInt8E), genericErr: toAnyErr(cast.ToNumberE[int8]), samples: []any{int8(0), int8(1), int8(8), int8(-8), int8(8), int8(-8), MyInt8(8), int8(math.MinInt8), int8(math.MaxInt8), "-129", "128"}, }, "int16": { to: toAny(cast.To[int16]), toErr: toAnyErr(cast.ToE[int16]), specific: toAny(cast.ToInt16), generic: toAny(cast.ToNumber[int16]), specificErr: toAnyErr(cast.ToInt16E), genericErr: toAnyErr(cast.ToNumberE[int16]), samples: []any{int16(0), int16(1), int16(8), int16(-8), int16(8), int16(-8), MyInt16(8), int16(math.MinInt16), int16(math.MaxInt16), "-32769", "32768"}, }, "int32": { to: toAny(cast.To[int32]), toErr: toAnyErr(cast.ToE[int32]), specific: toAny(cast.ToInt32), generic: toAny(cast.ToNumber[int32]), specificErr: toAnyErr(cast.ToInt32E), genericErr: toAnyErr(cast.ToNumberE[int32]), samples: []any{int32(0), int32(1), int32(8), int32(-8), int32(8), int32(-8), MyInt32(8), int32(math.MinInt32), int32(math.MaxInt32), "-2147483649", "2147483648"}, }, "int64": { to: toAny(cast.To[int64]), toErr: toAnyErr(cast.ToE[int64]), specific: toAny(cast.ToInt64), generic: toAny(cast.ToNumber[int64]), specificErr: toAnyErr(cast.ToInt64E), genericErr: toAnyErr(cast.ToNumberE[int64]), samples: []any{int64(0), int64(1), int64(8), int64(-8), int64(8), int64(-8), MyInt64(8), int64(math.MinInt64), int64(math.MaxInt64), "-9223372036854775809", "9223372036854775808"}, }, "uint": { to: toAny(cast.To[uint]), toErr: toAnyErr(cast.ToE[uint]), specific: toAny(cast.ToUint), generic: toAny(cast.ToNumber[uint]), specificErr: toAnyErr(cast.ToUintE), genericErr: toAnyErr(cast.ToNumberE[uint]), samples: []any{uint(0), uint(1), uint(8), uint(0), uint(8), uint(0), MyUint(8), uint(0), uint(math.MaxUint), nil, nil}, }, "uint8": { to: toAny(cast.To[uint8]), toErr: toAnyErr(cast.ToE[uint8]), specific: toAny(cast.ToUint8), generic: toAny(cast.ToNumber[uint8]), specificErr: toAnyErr(cast.ToUint8E), genericErr: toAnyErr(cast.ToNumberE[uint8]), samples: []any{uint8(0), uint8(1), uint8(8), uint8(0), uint8(8), uint8(0), MyUint8(8), uint8(0), uint8(math.MaxUint8), "-1", "256"}, }, "uint16": { to: toAny(cast.To[uint16]), toErr: toAnyErr(cast.ToE[uint16]), specific: toAny(cast.ToUint16), generic: toAny(cast.ToNumber[uint16]), specificErr: toAnyErr(cast.ToUint16E), genericErr: toAnyErr(cast.ToNumberE[uint16]), samples: []any{uint16(0), uint16(1), uint16(8), uint16(0), uint16(8), uint16(0), MyUint16(8), uint16(0), uint16(math.MaxUint16), "-1", "65536"}, }, "uint32": { to: toAny(cast.To[uint32]), toErr: toAnyErr(cast.ToE[uint32]), specific: toAny(cast.ToUint32), generic: toAny(cast.ToNumber[uint32]), specificErr: toAnyErr(cast.ToUint32E), genericErr: toAnyErr(cast.ToNumberE[uint32]), samples: []any{uint32(0), uint32(1), uint32(8), uint32(0), uint32(8), uint32(0), MyUint32(8), uint32(0), uint32(math.MaxUint32), "-1", "4294967296"}, }, "uint64": { to: toAny(cast.To[uint64]), toErr: toAnyErr(cast.ToE[uint64]), specific: toAny(cast.ToUint64), generic: toAny(cast.ToNumber[uint64]), specificErr: toAnyErr(cast.ToUint64E), genericErr: toAnyErr(cast.ToNumberE[uint64]), samples: []any{uint64(0), uint64(1), uint64(8), uint64(0), uint64(8), uint64(0), MyUint64(8), uint64(0), uint64(math.MaxUint64), "-1", "18446744073709551616"}, }, "float32": { to: toAny(cast.To[float32]), toErr: toAnyErr(cast.ToE[float32]), specific: toAny(cast.ToFloat32), generic: toAny(cast.ToNumber[float32]), specificErr: toAnyErr(cast.ToFloat32E), genericErr: toAnyErr(cast.ToNumberE[float32]), samples: []any{float32(0), float32(1), float32(8), float32(-8), float32(8.31), float32(-8.31), MyFloat32(8), float32(-math.MaxFloat32), float32(math.MaxFloat32), nil, nil}, }, "float64": { to: toAny(cast.To[float64]), toErr: toAnyErr(cast.ToE[float64]), specific: toAny(cast.ToFloat64), generic: toAny(cast.ToNumber[float64]), specificErr: toAnyErr(cast.ToFloat64E), genericErr: toAnyErr(cast.ToNumberE[float64]), samples: []any{float64(0), float64(1), float64(8), float64(-8), float64(8.31), float64(-8.31), MyFloat64(8), float64(-math.MaxFloat64), float64(math.MaxFloat64), nil, nil}, }, } // TODO: separate test and failure cases? // Kinda hard to track cases right now. func generateNumberTestCases(samples []any) []testCase { zero := samples[0] one := samples[1] eight := samples[2] eightNegative := samples[3] eightPoint31 := samples[4] eightPoint31Negative := samples[5] aliasEight := samples[6] min := samples[7] max := samples[8] underflowString := samples[9] overflowString := samples[10] _ = min _ = max _ = underflowString _ = overflowString kind := reflect.TypeOf(zero).Kind() isSint := kind == reflect.Int || kind == reflect.Int8 || kind == reflect.Int16 || kind == reflect.Int32 || kind == reflect.Int64 isUint := kind == reflect.Uint || kind == reflect.Uint8 || kind == reflect.Uint16 || kind == reflect.Uint32 || kind == reflect.Uint64 isInt := isSint || isUint // Some precision is lost when converting from float64 to float32. eightPoint31_32 := eightPoint31 eightPoint31Negative_32 := eightPoint31Negative if kind == reflect.Float64 { eightPoint31_32 = float64(float32(eightPoint31.(float64))) eightPoint31Negative_32 = float64(float32(eightPoint31Negative.(float64))) } testCases := []testCase{ // Positive numbers {int(8), eight, false}, {int8(8), eight, false}, {int16(8), eight, false}, {int32(8), eight, false}, {int64(8), eight, false}, {time.Weekday(8), eight, false}, {time.Month(8), eight, false}, {uint(8), eight, false}, {uint8(8), eight, false}, {uint16(8), eight, false}, {uint32(8), eight, false}, {uint64(8), eight, false}, {float32(8.31), eightPoint31_32, false}, {float64(8.31), eightPoint31, false}, {cast.ToString(max), max, false}, // Negative numbers {int(-8), eightNegative, isUint}, {int8(-8), eightNegative, isUint}, {int16(-8), eightNegative, isUint}, {int32(-8), eightNegative, isUint}, {int64(-8), eightNegative, isUint}, {float32(-8.31), eightPoint31Negative_32, isUint}, {float64(-8.31), eightPoint31Negative, isUint}, // Other basic types {true, one, false}, {false, zero, false}, {"8", eight, false}, {"-8", eightNegative, isUint}, {"8.31", eightPoint31, false}, {"-8.31", eightPoint31Negative, isUint}, {"", zero, false}, {nil, zero, false}, // JSON {json.Number("8"), eight, false}, {json.Number("-8"), eightNegative, isUint}, {json.Number("8.31"), eightPoint31, false}, {json.Number("-8.31"), eightPoint31Negative, isUint}, {json.Number(""), zero, false}, // Aliases {aliasEight, eight, false}, {MyString("8"), eight, false}, {MyBool(true), one, false}, // Failure cases {"test", zero, true}, {testing.T{}, zero, true}, {"10...17", zero, true}, {"10.foobar", zero, true}, {"10.0i", zero, true}, } if isInt { testCases = append( testCases, testCase{".5", zero, false}, testCase{"+8.", eight, false}, testCase{"+.25", zero, false}, testCase{"-.25", zero, isUint}, testCase{"10.0E9", zero, true}, ) } else if kind == reflect.Float32 { testCases = append(testCases, testCase{"10.0E9", float32(10000000000.000000), false}) } else if kind == reflect.Float64 { testCases = append(testCases, testCase{"10.0E9", float64(10000000000.000000), false}) } if isUint && underflowString != nil { testCases = append(testCases, testCase{underflowString, zero, true}) } if kind == reflect.Uint64 && isUint && overflowString != nil { testCases = append(testCases, testCase{overflowString, zero, true}) } return testCases } func TestNumber(t *testing.T) { t.Parallel() for typeName, ctx := range numberContexts { // TODO: remove after minimum Go version is >=1.22 typeName := typeName ctx := ctx t.Run(typeName, func(t *testing.T) { t.Parallel() testCases := generateNumberTestCases(ctx.samples) for _, testCase := range testCases { // TODO: remove after minimum Go version is >=1.22 testCase := testCase t.Run("", func(t *testing.T) { t.Parallel() t.Run("Value", func(t *testing.T) { t.Run("ToType", func(t *testing.T) { t.Parallel() c := qt.New(t) v := ctx.specific(testCase.input) c.Assert(v, qt.Equals, testCase.expected) }) t.Run("To", func(t *testing.T) { t.Parallel() c := qt.New(t) v := ctx.generic(testCase.input) c.Assert(v, qt.Equals, testCase.expected) }) t.Run("ToTypeE", func(t *testing.T) { t.Parallel() c := qt.New(t) v, err := ctx.specificErr(testCase.input) if testCase.expectError { c.Assert(err, qt.IsNotNil) } else { c.Assert(err, qt.IsNil) c.Assert(v, qt.Equals, testCase.expected) } }) t.Run("ToE", func(t *testing.T) { t.Parallel() c := qt.New(t) v, err := ctx.genericErr(testCase.input) if testCase.expectError { c.Assert(err, qt.IsNotNil) } else { c.Assert(err, qt.IsNil) c.Assert(v, qt.Equals, testCase.expected) } }) t.Run("Pointer", func(t *testing.T) { t.Run("ToType", func(t *testing.T) { t.Parallel() c := qt.New(t) v := ctx.specific(&testCase.input) c.Assert(v, qt.Equals, testCase.expected) }) t.Run("To", func(t *testing.T) { t.Parallel() c := qt.New(t) v := ctx.generic(&testCase.input) c.Assert(v, qt.Equals, testCase.expected) }) t.Run("ToTypeE", func(t *testing.T) { t.Parallel() c := qt.New(t) v, err := ctx.specificErr(&testCase.input) if testCase.expectError { c.Assert(err, qt.IsNotNil) } else { c.Assert(err, qt.IsNil) c.Assert(v, qt.Equals, testCase.expected) } }) t.Run("ToE", func(t *testing.T) { t.Parallel() c := qt.New(t) v, err := ctx.genericErr(&testCase.input) if testCase.expectError { c.Assert(err, qt.IsNotNil) } else { c.Assert(err, qt.IsNil) c.Assert(v, qt.Equals, testCase.expected) } }) }) }) }) } }) } } func BenchmarkNumber(b *testing.B) { type testCase struct { name string input any specific func(any) (any, error) generic func(any) (any, error) } var testCases []testCase // TODO: sort keys before iterating (once Go version is updated) for typeName, ctx := range numberContexts { testCases = append( testCases, testCase{ name: typeName, input: "123", specific: ctx.specificErr, generic: ctx.genericErr, }, testCase{ name: typeName, input: "1234567890123", specific: ctx.specificErr, generic: ctx.genericErr, }, testCase{ name: typeName, input: "-123", specific: ctx.specificErr, generic: ctx.genericErr, }, testCase{ name: typeName, input: "-1234567890123", specific: ctx.specificErr, generic: ctx.genericErr, }, testCase{ name: typeName, input: "0000000000123", specific: ctx.specificErr, generic: ctx.genericErr, }, testCase{ name: typeName, input: "00000000001234567890123", specific: ctx.specificErr, generic: ctx.genericErr, }, ) } for _, testCase := range testCases { // TODO: remove after minimum Go version is >=1.22 testCase := testCase b.Run(testCase.name, func(b *testing.B) { b.Run("Specific", func(b *testing.B) { // TODO: use b.Loop() once updated to Go 1.24 for i := 0; i < b.N; i++ { _, _ = testCase.specific(testCase.input) } }) b.Run("Generic", func(b *testing.B) { // TODO: use b.Loop() once updated to Go 1.24 for i := 0; i < b.N; i++ { _, _ = testCase.generic(testCase.input) } }) }) } } spf13-cast-3efe057/slice.go000066400000000000000000000035171505760405300154570ustar00rootroot00000000000000// Copyright © 2014 Steve Francia . // // Use of this source code is governed by an MIT-style // license that can be found in the LICENSE file. package cast import ( "fmt" "reflect" "strings" ) // ToSliceE casts any value to a []any type. func ToSliceE(i any) ([]any, error) { i, _ = indirect(i) var s []any switch v := i.(type) { case []any: // TODO: use slices.Clone return append(s, v...), nil case []map[string]any: for _, u := range v { s = append(s, u) } return s, nil default: return s, fmt.Errorf(errorMsg, i, i, s) } } func toSliceE[T Basic](i any) ([]T, error) { v, ok, err := toSliceEOk[T](i) if err != nil { return nil, err } if !ok { return nil, fmt.Errorf(errorMsg, i, i, []T{}) } return v, nil } func toSliceEOk[T Basic](i any) ([]T, bool, error) { i, _ = indirect(i) if i == nil { return nil, true, fmt.Errorf(errorMsg, i, i, []T{}) } switch v := i.(type) { case []T: // TODO: clone slice return v, true, nil } kind := reflect.TypeOf(i).Kind() switch kind { case reflect.Slice, reflect.Array: s := reflect.ValueOf(i) a := make([]T, s.Len()) for j := 0; j < s.Len(); j++ { val, err := ToE[T](s.Index(j).Interface()) if err != nil { return nil, true, fmt.Errorf(errorMsg, i, i, []T{}) } a[j] = val } return a, true, nil default: return nil, false, nil } } // ToStringSliceE casts any value to a []string type. func ToStringSliceE(i any) ([]string, error) { if a, ok, err := toSliceEOk[string](i); ok { if err != nil { return nil, err } return a, nil } var a []string switch v := i.(type) { case string: return strings.Fields(v), nil case any: str, err := ToStringE(v) if err != nil { return nil, fmt.Errorf(errorMsg, i, i, a) } return []string{str}, nil default: return nil, fmt.Errorf(errorMsg, i, i, a) } } spf13-cast-3efe057/slice_test.go000066400000000000000000000162271505760405300165200ustar00rootroot00000000000000// Copyright © 2014 Steve Francia . // // Use of this source code is governed by an MIT-style // license that can be found in the LICENSE file. package cast_test import ( "errors" "testing" "time" qt "github.com/frankban/quicktest" "github.com/spf13/cast" ) func runSliceTests[T cast.Basic | any](t *testing.T, testCases []testCase, to func(i any) []T, toErr func(i any) ([]T, error)) { for _, testCase := range testCases { // TODO: remove after minimum Go version is >=1.22 testCase := testCase t.Run("", func(t *testing.T) { t.Parallel() t.Run("Value", func(t *testing.T) { t.Run("ToType", func(t *testing.T) { t.Parallel() c := qt.New(t) v := to(testCase.input) if v == nil { return } c.Assert(v, qt.DeepEquals, testCase.expected) }) // t.Run("To", func(t *testing.T) { // return // t.Parallel() // c := qt.New(t) // v := cast.To[T](testCase.input) // c.Assert(v, qt.DeepEquals, testCase.expected) // }) t.Run("ToTypeE", func(t *testing.T) { t.Parallel() c := qt.New(t) v, err := toErr(testCase.input) if testCase.expectError { c.Assert(err, qt.IsNotNil) } else { c.Assert(err, qt.IsNil) c.Assert(v, qt.DeepEquals, testCase.expected) } }) // t.Run("ToE", func(t *testing.T) { // return // t.Parallel() // c := qt.New(t) // v, err := cast.ToE[T](testCase.input) // if testCase.expectError { // c.Assert(err, qt.IsNotNil) // } else { // c.Assert(err, qt.IsNil) // c.Assert(v, qt.DeepEquals, testCase.expected) // } // }) }) t.Run("Pointer", func(t *testing.T) { t.Run("ToType", func(t *testing.T) { t.Parallel() c := qt.New(t) v := to(&testCase.input) if v == nil { return } c.Assert(v, qt.DeepEquals, testCase.expected) }) // t.Run("To", func(t *testing.T) { // return // t.Parallel() // c := qt.New(t) // v := cast.To[T](&testCase.input) // c.Assert(v, qt.DeepEquals, testCase.expected) // }) t.Run("ToTypeE", func(t *testing.T) { t.Parallel() c := qt.New(t) v, err := toErr(&testCase.input) if testCase.expectError { c.Assert(err, qt.IsNotNil) } else { c.Assert(err, qt.IsNil) c.Assert(v, qt.DeepEquals, testCase.expected) } }) // t.Run("ToE", func(t *testing.T) { // return // t.Parallel() // c := qt.New(t) // v, err := cast.ToE[T](&testCase.input) // if testCase.expectError { // c.Assert(err, qt.IsNotNil) // } else { // c.Assert(err, qt.IsNil) // c.Assert(v, qt.DeepEquals, testCase.expected) // } // }) }) }) } } func TestBoolSlice(t *testing.T) { testCases := []testCase{ {[]bool{true, false, true}, []bool{true, false, true}, false}, {[]any{true, false, true}, []bool{true, false, true}, false}, {[]int{1, 0, 1}, []bool{true, false, true}, false}, {[]string{"true", "false", "true"}, []bool{true, false, true}, false}, // Failure cases {nil, nil, true}, {testing.T{}, nil, true}, {[]string{"foo", "bar"}, nil, true}, } runSliceTests(t, testCases, cast.ToBoolSlice, cast.ToBoolSliceE) } func TestIntSlice(t *testing.T) { testCases := []testCase{ {[]int{1, 3}, []int{1, 3}, false}, {[]any{1.2, 3.2}, []int{1, 3}, false}, {[]string{"2", "3"}, []int{2, 3}, false}, {[2]string{"2", "3"}, []int{2, 3}, false}, // Failure cases {nil, nil, true}, {testing.T{}, nil, true}, {[]string{"foo", "bar"}, nil, true}, } runSliceTests(t, testCases, cast.ToIntSlice, cast.ToIntSliceE) } func TestInt64Slice(t *testing.T) { testCases := []testCase{ {[]int{1, 3}, []int64{1, 3}, false}, {[]any{1.2, 3.2}, []int64{1, 3}, false}, {[]string{"2", "3"}, []int64{2, 3}, false}, {[2]string{"2", "3"}, []int64{2, 3}, false}, // Failure cases {nil, nil, true}, {testing.T{}, nil, true}, {[]string{"foo", "bar"}, nil, true}, } runSliceTests(t, testCases, cast.ToInt64Slice, cast.ToInt64SliceE) } func TestFloat64Slice(t *testing.T) { testCases := []testCase{ {[]int{1, 3}, []float64{1, 3}, false}, {[]float64{1.2, 3.2}, []float64{1.2, 3.2}, false}, {[]any{1.2, 3.2}, []float64{1.2, 3.2}, false}, {[]string{"2", "3"}, []float64{2, 3}, false}, {[]string{"1.2", "3.2"}, []float64{1.2, 3.2}, false}, {[2]string{"2", "3"}, []float64{2, 3}, false}, {[2]string{"1.2", "3.2"}, []float64{1.2, 3.2}, false}, {[]int32{1, 3}, []float64{1.0, 3.0}, false}, {[]int64{1, 3}, []float64{1.0, 3.0}, false}, {[]bool{true, false}, []float64{1.0, 0.0}, false}, // Failure cases {nil, nil, true}, {testing.T{}, nil, true}, {[]string{"foo", "bar"}, nil, true}, } runSliceTests(t, testCases, cast.ToFloat64Slice, cast.ToFloat64SliceE) } func TestUintSlice(t *testing.T) { testCases := []testCase{ {[]uint{1, 3}, []uint{1, 3}, false}, {[]any{1, 3}, []uint{1, 3}, false}, {[]string{"2", "3"}, []uint{2, 3}, false}, {[]int{1, 3}, []uint{1, 3}, false}, {[]int32{1, 3}, []uint{1, 3}, false}, {[]int64{1, 3}, []uint{1, 3}, false}, {[]float32{1.0, 3.0}, []uint{1, 3}, false}, {[]float64{1.0, 3.0}, []uint{1, 3}, false}, {[]bool{true, false}, []uint{1, 0}, false}, // Failure cases {nil, nil, true}, {testing.T{}, nil, true}, {[]string{"foo", "bar"}, nil, true}, } runSliceTests(t, testCases, cast.ToUintSlice, cast.ToUintSliceE) } func TestSlice(t *testing.T) { testCases := []testCase{ {[]any{1, 3}, []any{1, 3}, false}, {[]map[string]any{{"k1": 1}, {"k2": 2}}, []any{map[string]any{"k1": 1}, map[string]any{"k2": 2}}, false}, // Failure cases {nil, nil, true}, {testing.T{}, nil, true}, } runSliceTests(t, testCases, cast.ToSlice, cast.ToSliceE) } func TestStringSlice(t *testing.T) { testCases := []testCase{ {[]int{1, 2}, []string{"1", "2"}, false}, {[]int8{int8(1), int8(2)}, []string{"1", "2"}, false}, {[]int32{int32(1), int32(2)}, []string{"1", "2"}, false}, {[]int64{int64(1), int64(2)}, []string{"1", "2"}, false}, {[]uint{uint(1), uint(2)}, []string{"1", "2"}, false}, {[]uint8{uint8(1), uint8(2)}, []string{"1", "2"}, false}, {[]uint32{uint32(1), uint32(2)}, []string{"1", "2"}, false}, {[]uint64{uint64(1), uint64(2)}, []string{"1", "2"}, false}, {[]float32{float32(1.01), float32(2.01)}, []string{"1.01", "2.01"}, false}, {[]float64{float64(1.01), float64(2.01)}, []string{"1.01", "2.01"}, false}, {[]string{"a", "b"}, []string{"a", "b"}, false}, {[]any{1, 3}, []string{"1", "3"}, false}, {any(1), []string{"1"}, false}, {[]error{errors.New("a"), errors.New("b")}, []string{"a", "b"}, false}, // Failure cases {nil, nil, true}, {testing.T{}, nil, true}, } runSliceTests(t, testCases, cast.ToStringSlice, cast.ToStringSliceE) } func TestDurationSlice(t *testing.T) { testCases := []testCase{ {[]string{"1s", "1m"}, []time.Duration{time.Second, time.Minute}, false}, {[]int{1, 2}, []time.Duration{1, 2}, false}, {[]any{1, 3}, []time.Duration{1, 3}, false}, {[]time.Duration{1, 3}, []time.Duration{1, 3}, false}, // errors {nil, nil, true}, {testing.T{}, nil, true}, {[]string{"invalid"}, nil, true}, } runSliceTests(t, testCases, cast.ToDurationSlice, cast.ToDurationSliceE) } spf13-cast-3efe057/time.go000066400000000000000000000065401505760405300153150ustar00rootroot00000000000000// Copyright © 2014 Steve Francia . // // Use of this source code is governed by an MIT-style // license that can be found in the LICENSE file. package cast import ( "encoding/json" "errors" "fmt" "strings" "time" "github.com/spf13/cast/internal" ) // ToTimeE any value to a [time.Time] type. func ToTimeE(i any) (time.Time, error) { return ToTimeInDefaultLocationE(i, time.UTC) } // ToTimeInDefaultLocationE casts an empty interface to [time.Time], // interpreting inputs without a timezone to be in the given location, // or the local timezone if nil. func ToTimeInDefaultLocationE(i any, location *time.Location) (tim time.Time, err error) { i, _ = indirect(i) switch v := i.(type) { case time.Time: return v, nil case string: return StringToDateInDefaultLocation(v, location) case json.Number: // Originally this used ToInt64E, but adding string float conversion broke ToTime. // the behavior of ToTime would have changed if we continued using it. // For now, using json.Number's own Int64 method should be good enough to preserve backwards compatibility. v = json.Number(trimZeroDecimal(string(v))) s, err1 := v.Int64() if err1 != nil { return time.Time{}, fmt.Errorf(errorMsg, i, i, time.Time{}) } return time.Unix(s, 0), nil case int: return time.Unix(int64(v), 0), nil case int32: return time.Unix(int64(v), 0), nil case int64: return time.Unix(v, 0), nil case uint: return time.Unix(int64(v), 0), nil case uint32: return time.Unix(int64(v), 0), nil case uint64: return time.Unix(int64(v), 0), nil case nil: return time.Time{}, nil default: return time.Time{}, fmt.Errorf(errorMsg, i, i, time.Time{}) } } // ToDurationE casts any value to a [time.Duration] type. func ToDurationE(i any) (time.Duration, error) { i, _ = indirect(i) switch s := i.(type) { case time.Duration: return s, nil case int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64: v, err := ToInt64E(s) if err != nil { // TODO: once there is better error handling, this should be easier return 0, errors.New(strings.ReplaceAll(err.Error(), " int64", "time.Duration")) } return time.Duration(v), nil case float32, float64, float64EProvider, float64Provider: v, err := ToFloat64E(s) if err != nil { // TODO: once there is better error handling, this should be easier return 0, errors.New(strings.ReplaceAll(err.Error(), " float64", "time.Duration")) } return time.Duration(v), nil case string: if !strings.ContainsAny(s, "nsuµmh") { return time.ParseDuration(s + "ns") } return time.ParseDuration(s) case nil: return time.Duration(0), nil default: if i, ok := resolveAlias(i); ok { return ToDurationE(i) } return 0, fmt.Errorf(errorMsg, i, i, time.Duration(0)) } } // StringToDate attempts to parse a string into a [time.Time] type using a // predefined list of formats. // // If no suitable format is found, an error is returned. func StringToDate(s string) (time.Time, error) { return internal.ParseDateWith(s, time.UTC, internal.TimeFormats) } // StringToDateInDefaultLocation casts an empty interface to a [time.Time], // interpreting inputs without a timezone to be in the given location, // or the local timezone if nil. func StringToDateInDefaultLocation(s string, location *time.Location) (time.Time, error) { return internal.ParseDateWith(s, location, internal.TimeFormats) } spf13-cast-3efe057/time_test.go000066400000000000000000000252461505760405300163600ustar00rootroot00000000000000// Copyright © 2014 Steve Francia . // // Use of this source code is governed by an MIT-style // license that can be found in the LICENSE file. package cast_test import ( "encoding/json" "fmt" "path" "testing" "time" qt "github.com/frankban/quicktest" "github.com/spf13/cast" "github.com/spf13/cast/internal" ) func TestTime(t *testing.T) { var ptr *time.Time testCases := []testCase{ {"2009-11-10 23:00:00 +0000 UTC", time.Date(2009, 11, 10, 23, 0, 0, 0, time.UTC), false}, // Time.String() {"Tue Nov 10 23:00:00 2009", time.Date(2009, 11, 10, 23, 0, 0, 0, time.UTC), false}, // ANSIC {"Tue Nov 10 23:00:00 UTC 2009", time.Date(2009, 11, 10, 23, 0, 0, 0, time.UTC), false}, // UnixDate {"Tue Nov 10 23:00:00 +0000 2009", time.Date(2009, 11, 10, 23, 0, 0, 0, time.UTC), false}, // RubyDate {"10 Nov 09 23:00 UTC", time.Date(2009, 11, 10, 23, 0, 0, 0, time.UTC), false}, // RFC822 {"10 Nov 09 23:00 +0000", time.Date(2009, 11, 10, 23, 0, 0, 0, time.UTC), false}, // RFC822Z {"Tuesday, 10-Nov-09 23:00:00 UTC", time.Date(2009, 11, 10, 23, 0, 0, 0, time.UTC), false}, // RFC850 {"Tue, 10 Nov 2009 23:00:00 UTC", time.Date(2009, 11, 10, 23, 0, 0, 0, time.UTC), false}, // RFC1123 {"Tue, 10 Nov 2009 23:00:00 +0000", time.Date(2009, 11, 10, 23, 0, 0, 0, time.UTC), false}, // RFC1123Z {"2009-11-10T23:00:00Z", time.Date(2009, 11, 10, 23, 0, 0, 0, time.UTC), false}, // RFC3339 {"2018-10-21T23:21:29+0200", time.Date(2018, 10, 21, 21, 21, 29, 0, time.UTC), false}, // RFC3339 without timezone hh:mm colon {"2009-11-10T23:00:00Z", time.Date(2009, 11, 10, 23, 0, 0, 0, time.UTC), false}, // RFC3339Nano {"11:00PM", time.Date(0, 1, 1, 23, 0, 0, 0, time.UTC), false}, // Kitchen {"Nov 10 23:00:00", time.Date(0, 11, 10, 23, 0, 0, 0, time.UTC), false}, // Stamp {"Nov 10 23:00:00.000", time.Date(0, 11, 10, 23, 0, 0, 0, time.UTC), false}, // StampMilli {"Nov 10 23:00:00.000000", time.Date(0, 11, 10, 23, 0, 0, 0, time.UTC), false}, // StampMicro {"Nov 10 23:00:00.000000000", time.Date(0, 11, 10, 23, 0, 0, 0, time.UTC), false}, // StampNano {"2016-03-06 15:28:01-00:00", time.Date(2016, 3, 6, 15, 28, 1, 0, time.UTC), false}, // RFC3339 without T {"2016-03-06 15:28:01-0000", time.Date(2016, 3, 6, 15, 28, 1, 0, time.UTC), false}, // RFC3339 without T or timezone hh:mm colon {"2016-03-06 15:28:01", time.Date(2016, 3, 6, 15, 28, 1, 0, time.UTC), false}, {"2016-03-06 15:28:01 -0000", time.Date(2016, 3, 6, 15, 28, 1, 0, time.UTC), false}, {"2016-03-06 15:28:01 -00:00", time.Date(2016, 3, 6, 15, 28, 1, 0, time.UTC), false}, {"2016-03-06 15:28:01 +0900", time.Date(2016, 3, 6, 6, 28, 1, 0, time.UTC), false}, {"2016-03-06 15:28:01 +09:00", time.Date(2016, 3, 6, 6, 28, 1, 0, time.UTC), false}, {"2006-01-02", time.Date(2006, 1, 2, 0, 0, 0, 0, time.UTC), false}, {"02 Jan 2006", time.Date(2006, 1, 2, 0, 0, 0, 0, time.UTC), false}, {1472574600, time.Date(2016, 8, 30, 16, 30, 0, 0, time.UTC), false}, {int(1482597504), time.Date(2016, 12, 24, 16, 38, 24, 0, time.UTC), false}, {int64(1234567890), time.Date(2009, 2, 13, 23, 31, 30, 0, time.UTC), false}, {int32(1234567890), time.Date(2009, 2, 13, 23, 31, 30, 0, time.UTC), false}, {uint(1482597504), time.Date(2016, 12, 24, 16, 38, 24, 0, time.UTC), false}, {uint64(1234567890), time.Date(2009, 2, 13, 23, 31, 30, 0, time.UTC), false}, {uint32(1234567890), time.Date(2009, 2, 13, 23, 31, 30, 0, time.UTC), false}, {json.Number("1234567890"), time.Date(2009, 2, 13, 23, 31, 30, 0, time.UTC), false}, {time.Date(2009, 2, 13, 23, 31, 30, 0, time.UTC), time.Date(2009, 2, 13, 23, 31, 30, 0, time.UTC), false}, {ptr, time.Time{}, false}, // Failure cases {"2006", time.Time{}, true}, {json.Number("123.4567890"), time.Time{}, true}, {testing.T{}, time.Time{}, true}, } runTests(t, testCases, cast.ToTime, cast.ToTimeE) } func TestDuration(t *testing.T) { type MyDuration time.Duration var ptr *time.Duration var expected time.Duration = 5 testCases := []testCase{ {time.Duration(5), expected, false}, {int(5), expected, false}, {int64(5), expected, false}, {int32(5), expected, false}, {int16(5), expected, false}, {int8(5), expected, false}, {uint(5), expected, false}, {uint64(5), expected, false}, {uint32(5), expected, false}, {uint16(5), expected, false}, {uint8(5), expected, false}, {float64(5), expected, false}, {float32(5), expected, false}, {json.Number("5"), expected, false}, {string("5"), expected, false}, {string("5ns"), expected, false}, {string("5us"), time.Microsecond * expected, false}, {string("5µs"), time.Microsecond * expected, false}, {string("5ms"), time.Millisecond * expected, false}, {string("5s"), time.Second * expected, false}, {string("5m"), time.Minute * expected, false}, {string("5h"), time.Hour * expected, false}, {0, time.Duration(0), false}, {ptr, time.Duration(0), false}, // Aliases {MyInt(5), expected, false}, {MyString("5"), expected, false}, {MyDuration(5), expected, false}, // Failure cases {"test", time.Duration(0), true}, {testing.T{}, time.Duration(0), true}, } runTests(t, testCases, cast.ToDuration, cast.ToDurationE) } func TestTimeWithTimezones(t *testing.T) { c := qt.New(t) est, err := time.LoadLocation("EST") c.Assert(err, qt.IsNil) irn, err := time.LoadLocation("Iran") c.Assert(err, qt.IsNil) swd, err := time.LoadLocation("Europe/Stockholm") c.Assert(err, qt.IsNil) // Test same local time in different timezones utc2016 := time.Date(2016, time.January, 1, 0, 0, 0, 0, time.UTC) est2016 := time.Date(2016, time.January, 1, 0, 0, 0, 0, est) irn2016 := time.Date(2016, time.January, 1, 0, 0, 0, 0, irn) swd2016 := time.Date(2016, time.January, 1, 0, 0, 0, 0, swd) loc2016 := time.Date(2016, time.January, 1, 0, 0, 0, 0, time.Local) for i, format := range internal.TimeFormats { format := format if format.Typ == internal.TimeFormatTimeOnly { continue } nameBase := fmt.Sprintf("%d;timeFormatType=%d;%s", i, format.Typ, format.Format) t.Run(path.Join(nameBase), func(t *testing.T) { est2016str := est2016.Format(format.Format) swd2016str := swd2016.Format(format.Format) t.Run("without default location", func(t *testing.T) { c := qt.New(t) converted, err := cast.ToTimeE(est2016str) c.Assert(err, qt.IsNil) if format.HasTimezone() { // Converting inputs with a timezone should preserve it assertTimeEqual(t, est2016, converted) assertLocationEqual(t, est, converted.Location()) } else { // Converting inputs without a timezone should be interpreted // as a local time in UTC. assertTimeEqual(t, utc2016, converted) assertLocationEqual(t, time.UTC, converted.Location()) } }) t.Run("local timezone without a default location", func(t *testing.T) { c := qt.New(t) converted, err := cast.ToTimeE(swd2016str) c.Assert(err, qt.IsNil) if format.HasTimezone() { // Converting inputs with a timezone should preserve it assertTimeEqual(t, swd2016, converted) assertLocationEqual(t, swd, converted.Location()) } else { // Converting inputs without a timezone should be interpreted // as a local time in UTC. assertTimeEqual(t, utc2016, converted) assertLocationEqual(t, time.UTC, converted.Location()) } }) t.Run("nil default location", func(t *testing.T) { c := qt.New(t) converted, err := cast.ToTimeInDefaultLocationE(est2016str, nil) c.Assert(err, qt.IsNil) if format.HasTimezone() { // Converting inputs with a timezone should preserve it assertTimeEqual(t, est2016, converted) assertLocationEqual(t, est, converted.Location()) } else { // Converting inputs without a timezone should be interpreted // as a local time in the local timezone. assertTimeEqual(t, loc2016, converted) assertLocationEqual(t, time.Local, converted.Location()) } }) t.Run("default location not UTC", func(t *testing.T) { c := qt.New(t) converted, err := cast.ToTimeInDefaultLocationE(est2016str, irn) c.Assert(err, qt.IsNil) if format.HasTimezone() { // Converting inputs with a timezone should preserve it assertTimeEqual(t, est2016, converted) assertLocationEqual(t, est, converted.Location()) } else { // Converting inputs without a timezone should be interpreted // as a local time in the given location. assertTimeEqual(t, irn2016, converted) assertLocationEqual(t, irn, converted.Location()) } }) t.Run("time in the local timezone default location not UTC", func(t *testing.T) { c := qt.New(t) converted, err := cast.ToTimeInDefaultLocationE(swd2016str, irn) c.Assert(err, qt.IsNil) if format.HasTimezone() { // Converting inputs with a timezone should preserve it assertTimeEqual(t, swd2016, converted) assertLocationEqual(t, swd, converted.Location()) } else { // Converting inputs without a timezone should be interpreted // as a local time in the given location. assertTimeEqual(t, irn2016, converted) assertLocationEqual(t, irn, converted.Location()) } }) }) } } func BenchmarkCommonTimeLayouts(b *testing.B) { for i := 0; i < b.N; i++ { for _, commonLayout := range []string{"2019-04-29", "2017-05-30T00:00:00Z"} { _, err := cast.StringToDateInDefaultLocation(commonLayout, time.UTC) if err != nil { b.Fatal(err) } } } } func assertTimeEqual(t *testing.T, expected, actual time.Time) { t.Helper() // Compare the dates using a numeric zone as there are cases where // time.Parse will assign a dummy location. qt.Assert(t, actual.Format(time.RFC1123Z), qt.Equals, expected.Format(time.RFC1123Z)) } func assertLocationEqual(t *testing.T, expected, actual *time.Location) { t.Helper() qt.Assert(t, locationEqual(expected, actual), qt.IsTrue) } func locationEqual(a, b *time.Location) bool { // A note about comparring time.Locations: // - can't only compare pointers // - can't compare loc.String() because locations with the same // name can have different offsets // - can't use reflect.DeepEqual because time.Location has internal // caches if a == b { return true } else if a == nil || b == nil { return false } // Check if they're equal by parsing times with a format that doesn't // include a timezone, which will interpret it as being a local time in // the given zone, and comparing the resulting local times. tA, err := time.ParseInLocation("2006-01-02", "2016-01-01", a) if err != nil { return false } tB, err := time.ParseInLocation("2006-01-02", "2016-01-01", b) if err != nil { return false } return tA.Equal(tB) } spf13-cast-3efe057/zz_generated.go000066400000000000000000000134101505760405300170320ustar00rootroot00000000000000// Code generated by cast generator. DO NOT EDIT. package cast import "time" // ToBool casts any value to a(n) bool type. func ToBool(i any) bool { v, _ := ToBoolE(i) return v } // ToString casts any value to a(n) string type. func ToString(i any) string { v, _ := ToStringE(i) return v } // ToTime casts any value to a(n) time.Time type. func ToTime(i any) time.Time { v, _ := ToTimeE(i) return v } // ToTimeInDefaultLocation casts any value to a(n) time.Time type. func ToTimeInDefaultLocation(i any, location *time.Location) time.Time { v, _ := ToTimeInDefaultLocationE(i, location) return v } // ToDuration casts any value to a(n) time.Duration type. func ToDuration(i any) time.Duration { v, _ := ToDurationE(i) return v } // ToInt casts any value to a(n) int type. func ToInt(i any) int { v, _ := ToIntE(i) return v } // ToInt8 casts any value to a(n) int8 type. func ToInt8(i any) int8 { v, _ := ToInt8E(i) return v } // ToInt16 casts any value to a(n) int16 type. func ToInt16(i any) int16 { v, _ := ToInt16E(i) return v } // ToInt32 casts any value to a(n) int32 type. func ToInt32(i any) int32 { v, _ := ToInt32E(i) return v } // ToInt64 casts any value to a(n) int64 type. func ToInt64(i any) int64 { v, _ := ToInt64E(i) return v } // ToUint casts any value to a(n) uint type. func ToUint(i any) uint { v, _ := ToUintE(i) return v } // ToUint8 casts any value to a(n) uint8 type. func ToUint8(i any) uint8 { v, _ := ToUint8E(i) return v } // ToUint16 casts any value to a(n) uint16 type. func ToUint16(i any) uint16 { v, _ := ToUint16E(i) return v } // ToUint32 casts any value to a(n) uint32 type. func ToUint32(i any) uint32 { v, _ := ToUint32E(i) return v } // ToUint64 casts any value to a(n) uint64 type. func ToUint64(i any) uint64 { v, _ := ToUint64E(i) return v } // ToFloat32 casts any value to a(n) float32 type. func ToFloat32(i any) float32 { v, _ := ToFloat32E(i) return v } // ToFloat64 casts any value to a(n) float64 type. func ToFloat64(i any) float64 { v, _ := ToFloat64E(i) return v } // ToStringMapString casts any value to a(n) map[string]string type. func ToStringMapString(i any) map[string]string { v, _ := ToStringMapStringE(i) return v } // ToStringMapStringSlice casts any value to a(n) map[string][]string type. func ToStringMapStringSlice(i any) map[string][]string { v, _ := ToStringMapStringSliceE(i) return v } // ToStringMapBool casts any value to a(n) map[string]bool type. func ToStringMapBool(i any) map[string]bool { v, _ := ToStringMapBoolE(i) return v } // ToStringMapInt casts any value to a(n) map[string]int type. func ToStringMapInt(i any) map[string]int { v, _ := ToStringMapIntE(i) return v } // ToStringMapInt64 casts any value to a(n) map[string]int64 type. func ToStringMapInt64(i any) map[string]int64 { v, _ := ToStringMapInt64E(i) return v } // ToStringMap casts any value to a(n) map[string]any type. func ToStringMap(i any) map[string]any { v, _ := ToStringMapE(i) return v } // ToSlice casts any value to a(n) []any type. func ToSlice(i any) []any { v, _ := ToSliceE(i) return v } // ToBoolSlice casts any value to a(n) []bool type. func ToBoolSlice(i any) []bool { v, _ := ToBoolSliceE(i) return v } // ToStringSlice casts any value to a(n) []string type. func ToStringSlice(i any) []string { v, _ := ToStringSliceE(i) return v } // ToIntSlice casts any value to a(n) []int type. func ToIntSlice(i any) []int { v, _ := ToIntSliceE(i) return v } // ToInt64Slice casts any value to a(n) []int64 type. func ToInt64Slice(i any) []int64 { v, _ := ToInt64SliceE(i) return v } // ToUintSlice casts any value to a(n) []uint type. func ToUintSlice(i any) []uint { v, _ := ToUintSliceE(i) return v } // ToFloat64Slice casts any value to a(n) []float64 type. func ToFloat64Slice(i any) []float64 { v, _ := ToFloat64SliceE(i) return v } // ToDurationSlice casts any value to a(n) []time.Duration type. func ToDurationSlice(i any) []time.Duration { v, _ := ToDurationSliceE(i) return v } // ToBoolSliceE casts any value to a(n) []bool type. func ToBoolSliceE(i any) ([]bool, error) { return toSliceE[bool](i) } // ToDurationSliceE casts any value to a(n) []time.Duration type. func ToDurationSliceE(i any) ([]time.Duration, error) { return toSliceE[time.Duration](i) } // ToIntSliceE casts any value to a(n) []int type. func ToIntSliceE(i any) ([]int, error) { return toSliceE[int](i) } // ToInt8SliceE casts any value to a(n) []int8 type. func ToInt8SliceE(i any) ([]int8, error) { return toSliceE[int8](i) } // ToInt16SliceE casts any value to a(n) []int16 type. func ToInt16SliceE(i any) ([]int16, error) { return toSliceE[int16](i) } // ToInt32SliceE casts any value to a(n) []int32 type. func ToInt32SliceE(i any) ([]int32, error) { return toSliceE[int32](i) } // ToInt64SliceE casts any value to a(n) []int64 type. func ToInt64SliceE(i any) ([]int64, error) { return toSliceE[int64](i) } // ToUintSliceE casts any value to a(n) []uint type. func ToUintSliceE(i any) ([]uint, error) { return toSliceE[uint](i) } // ToUint8SliceE casts any value to a(n) []uint8 type. func ToUint8SliceE(i any) ([]uint8, error) { return toSliceE[uint8](i) } // ToUint16SliceE casts any value to a(n) []uint16 type. func ToUint16SliceE(i any) ([]uint16, error) { return toSliceE[uint16](i) } // ToUint32SliceE casts any value to a(n) []uint32 type. func ToUint32SliceE(i any) ([]uint32, error) { return toSliceE[uint32](i) } // ToUint64SliceE casts any value to a(n) []uint64 type. func ToUint64SliceE(i any) ([]uint64, error) { return toSliceE[uint64](i) } // ToFloat32SliceE casts any value to a(n) []float32 type. func ToFloat32SliceE(i any) ([]float32, error) { return toSliceE[float32](i) } // ToFloat64SliceE casts any value to a(n) []float64 type. func ToFloat64SliceE(i any) ([]float64, error) { return toSliceE[float64](i) }