pax_global_header00006660000000000000000000000064147643327210014523gustar00rootroot0000000000000052 comment=b2329de595e3af90fd9bab525234a595911fbce8 go-safecast-1.6.0/000077500000000000000000000000001476433272100137235ustar00rootroot00000000000000go-safecast-1.6.0/.editorconfig000066400000000000000000000010121476433272100163720ustar00rootroot00000000000000# More information about this file # https://editorconfig.org root = true [*] charset = utf-8 end_of_line = lf [*.md] tab_width = 2 trim_trailing_whitespace = false # can be used for indentation with double spaces # Not enforced, markdownlint already reports it #insert_final_newline = true [*.{yml,yaml}] tab_width = 2 trim_trailing_whitespace = true # Not enforced, markdownlint already reports it #insert_final_newline = true [*.toml,.ini] tab_width = 2 trim_trailing_whitespace = true insert_final_newline = true go-safecast-1.6.0/.gitattributes000066400000000000000000000003161476433272100166160ustar00rootroot00000000000000# ensure that line endings for Windows builds are properly formatted # see https://github.com/golangci/golangci-lint-action?tab=readme-ov-file#how-to-use # at "Multiple OS Example" section *.go text eol=lf go-safecast-1.6.0/.github/000077500000000000000000000000001476433272100152635ustar00rootroot00000000000000go-safecast-1.6.0/.github/dependabot.yml000066400000000000000000000007361476433272100201210ustar00rootroot00000000000000--- version: 2 updates: # Maintain dependencies for GitHub Actions # These would open PR, these PR would be tested with the CI # They will have to be merged manually by a maintainer - package-ecosystem: github-actions directory: / open-pull-requests-limit: 10 # avoid spam, if no one reacts schedule: interval: weekly time: '11:00' groups: all: patterns: - "*" # Group all updates into a single larger pull request. go-safecast-1.6.0/.github/workflows/000077500000000000000000000000001476433272100173205ustar00rootroot00000000000000go-safecast-1.6.0/.github/workflows/files.yml000066400000000000000000000022131476433272100211430ustar00rootroot00000000000000--- name: Files Lint on: pull_request: push: branches: - main permissions: contents: read # for actions/checkout to fetch code jobs: action-lint: runs-on: ubuntu-latest steps: - name: checkout-action uses: actions/checkout@v4.2.2 - name: actionlint uses: raven-actions/actionlint@v2.0.0 yamllint: runs-on: ubuntu-latest steps: - name: checkout-action uses: actions/checkout@v4.2.2 - name: yamllint uses: ibiqlik/action-yamllint@v3 typos: runs-on: ubuntu-latest steps: - name: checkout-action uses: actions/checkout@v4.2.2 - name: typos-action uses: crate-ci/typos@v1.30.2 markdownlint: runs-on: ubuntu-latest steps: - name: checkout-action uses: actions/checkout@v4.2.2 - name: markdownlint-cli2-action uses: DavidAnson/markdownlint-cli2-action@v19.1.0 editorconfig: runs-on: ubuntu-latest steps: - name: checkout-action uses: actions/checkout@v4.2.2 - name: editorconfig-checker-action uses: editorconfig-checker/action-editorconfig-checker@v2 go-safecast-1.6.0/.github/workflows/go.yml000066400000000000000000000043471476433272100204600ustar00rootroot00000000000000--- name: Go on: pull_request: push: branches: - main jobs: go-test-lint: name: Test & Lint strategy: fail-fast: false # continue to run all jobs even if one fails matrix: include: - {os: ubuntu-latest, GOOS: linux, GOARCH: amd64, go-version: stable} - {os: ubuntu-latest, GOOS: linux, GOARCH: amd64, go-version: oldstable} - {os: ubuntu-latest, GOOS: linux, GOARCH: 386, go-version: stable} - {os: macos-latest, GOOS: darwin, GOARCH: amd64, go-version: stable} - {os: macos-latest, GOOS: darwin, GOARCH: arm64, go-version: stable} - {os: windows-latest, GOOS: windows, GOARCH: amd64, go-version: stable} - {os: windows-latest, GOOS: windows, GOARCH: 386, go-version: stable} permissions: contents: read # for actions/checkout to fetch code pull-requests: read # for golangci/golangci-lint-action to fetch pull requests runs-on: ${{ matrix.os }} env: GOARCH: ${{ matrix.GOARCH }} GOOS: ${{ matrix.GOOS }} steps: - name: checkout-action uses: actions/checkout@v4 - name: Set up Go uses: actions/setup-go@v5 with: go-version: ${{ matrix.go-version }} - name: golangci-lint uses: golangci/golangci-lint-action@v6 with: version: latest - name: Run tests run: go test go-coverage: name: Code Coverage # This one is limited to one arch as we don't need to report coverage multiple times permissions: contents: read # for actions/checkout to fetch code runs-on: ubuntu-latest steps: - name: checkout-action uses: actions/checkout@v4.2.2 - name: Set up Go uses: actions/setup-go@v5 - name: Run tests run: go test -coverprofile=coverage.txt - name: Upload results to Codecov uses: codecov/codecov-action@v5 with: token: ${{ secrets.CODECOV_TOKEN }} - name: Code Climate Coverage Action uses: paambaati/codeclimate-action@v9 env: CC_TEST_REPORTER_ID: ${{secrets.CC_TEST_REPORTER_ID}} with: coverageLocations: coverage.txt:gocov prefix: github.com/ccoveille/go-safecast go-safecast-1.6.0/.golangci.yml000066400000000000000000000130241476433272100163070ustar00rootroot00000000000000--- # golangci-lint configuration file made by @ccoVeille # Source: https://github.com/ccoVeille/golangci-lint-config-examples/ # Author: @ccoVeille # License: MIT # Variant: 03-safe # Version: v1.0.0 # linters: # some linters are enabled by default # https://golangci-lint.run/usage/linters/ # # enable some extra linters enable: # Errcheck is a program for checking for unchecked errors in Go code. - errcheck # Linter for Go source code that specializes in simplifying code. - gosimple # Vet examines Go source code and reports suspicious constructs. - govet # gosec is a security linter for Go code. - gosec # Detects when assignments to existing variables are not used. - ineffassign # It's a set of rules from staticcheck. See https://staticcheck.io/ - staticcheck # Fast, configurable, extensible, flexible, and beautiful linter for Go. # Drop-in replacement of golint. - revive # check imports order and makes it always deterministic. - gci # make sure to use t.Helper() when needed - thelper # checks if package imports are in a list of acceptable packages. - depguard # mirror suggests rewrites to avoid unnecessary []byte/string conversion - mirror # detect the possibility to use variables/constants from the Go standard library. - usestdlibvars # Finds commonly misspelled English words. - misspell # Checks for duplicate words in the source code. - dupword linters-settings: gci: # define the section orders for imports sections: # Standard section: captures all standard packages. - standard # Default section: catchall that is not standard or custom - default # linters that related to local tool, so they should be separated - localmodule depguard: rules: # enforce the library has no dependencies except the standard library code: files: - "!$test" # depguard alias for all files except the test ones allow: - "$gostd" # depguard alias for all standard libraries # enforce the test files have no dependencies except the standard library and the library itself test: files: - "$test" # depguard alias for all the test files allow: - "$gostd" # depguard alias for all standard libraries - "github.com/ccoveille/go-safecast" revive: rules: # these are the default revive rules # you can remove the whole "rules" node if you want # BUT # ! /!\ they all need to be present when you want to add more rules than the default ones # otherwise, you won't have the default rules, but only the ones you define in the "rules" node # Blank import should be only in a main or test package, or have a comment justifying it. - name: blank-imports # context.Context() should be the first parameter of a function when provided as argument. - name: context-as-argument arguments: - allowTypesBefore: "*testing.T" # Basic types should not be used as a key in `context.WithValue` - name: context-keys-type # Importing with `.` makes the programs much harder to understand - name: dot-imports # Empty blocks make code less readable and could be a symptom of a bug or unfinished refactoring. - name: empty-block # for better readability, variables of type `error` must be named with the prefix `err`. - name: error-naming # for better readability, the errors should be last in the list of returned values by a function. - name: error-return # for better readability, error messages should not be capitalized or end with punctuation or a newline. - name: error-strings # report when replacing `errors.New(fmt.Sprintf())` with `fmt.Errorf()` is possible - name: errorf # enforces conventions on source file names. - name: filename-format # incrementing an integer variable by 1 is recommended to be done using the `++` operator - name: increment-decrement # highlights redundant else-blocks that can be eliminated from the code - name: indent-error-flow # This rule suggests a shorter way of writing ranges that do not use the second value. - name: range # receiver names in a method should reflect the struct name (p for Person, for example) - name: receiver-naming # redefining built in names (true, false, append, make) can lead to bugs very difficult to detect. - name: redefines-builtin-id # redundant else-blocks that can be eliminated from the code. - name: superfluous-else # prevent confusing name for variables when using `time` package - name: time-naming # warns when an exported function or method returns a value of an un-exported type. - name: unexported-return # spots and proposes to remove unreachable code. also helps to spot errors - name: unreachable-code # Functions or methods with unused parameters can be a symptom of an unfinished refactoring or a bug. - name: unused-parameter # report when a variable declaration can be simplified - name: var-declaration # warns when initialism, variable or package naming conventions are not followed. - name: var-naming misspell: # Correct spellings using locale preferences for US or UK. # Setting locale to US will correct the British spelling of 'colour' to 'color'. # Default ("") is to use a neutral variety of English. locale: US go-safecast-1.6.0/.ls-lint.yml000066400000000000000000000010611476433272100161040ustar00rootroot00000000000000--- # ls-lint configuration file. More information on the file format can be found on https://ls-lint.org/ ls: .md: screamingsnakecase # README.md or CODE_OF_CONDUCT.md # DEACTIVATED: Go files should are linted via golangci-lint revive filename-format rule # .go: snakecase ignore: # .git folder cannot be linted - .git # .github folder contains configuration files with specific name, and should not be linted - .github # dot files are usually configuration files with specific name - .ls-lint.yml - .markdownlint.yml - .yamllint.yml go-safecast-1.6.0/.markdownlint.yml000066400000000000000000000003561476433272100172410ustar00rootroot00000000000000--- # Default state for all rules default: true MD013: # Overload the default value (120) line_length: 200 MD033: allowed_elements: #
are useful for spoilers - summary - details go-safecast-1.6.0/.yamllint.yml000066400000000000000000000005231476433272100163550ustar00rootroot00000000000000--- extends: default ignore: # These files are imported by vale from an external repository .vale/styles/RedHat/ rules: line-length: max: 200 allow-non-breakable-words: true allow-non-breakable-inline-mappings: true truthy: # the node "on:" present in each GitHub Actions workflow file ignore: | .github/ go-safecast-1.6.0/LICENSE000066400000000000000000000020521476433272100147270ustar00rootroot00000000000000MIT License Copyright (c) 2024 ccoVeille 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. go-safecast-1.6.0/README.md000066400000000000000000000111271476433272100152040ustar00rootroot00000000000000# 🪄 go-safecast: safe numbers conversion [![Go Report Card](https://goreportcard.com/badge/github.com/ccoveille/go-safecast)](https://goreportcard.com/report/github.com/ccoveille/go-safecast) [![GoDoc](https://godoc.org/github.com/ccoVeille/go-safecast?status.svg)](https://godoc.org/github.com/ccoVeille/go-safecast) [![codecov](https://codecov.io/gh/ccoVeille/go-safecast/graph/badge.svg?token=VW0VO503U6)](https://codecov.io/gh/ccoVeille/go-safecast) [![Code Climate](https://codeclimate.com/github/ccoVeille/go-safecast.png)](https://codeclimate.com/github/ccoVeille/go-safecast) [![Go Imports](https://img.shields.io/github/search?query=%22%5C%22github.com%2Fccoveille%2Fgo-safecast%5C%22%22%20language%3Ago%20%20-is%3Afork%20-is%3Aarchived%20&label=Go%20imports)](https://github.com/search?q=%22%5C%22github.com%2Fccoveille%2Fgo-safecast%5C%22%22+language%3Ago++-is%3Afork+-is%3Aarchived+&type=code) ![GitHub Repo stars](https://img.shields.io/github/stars/ccoveille/go-safecast) go-safecast solves the type conversion issues in Go In Go, integer type conversion can lead to a silent and unexpected behavior and errors if not handled carefully. This package helps to convert any number to another, and report an error when if there would be a [loss or overflow in the conversion](#conversion-issues) ## Usage ```go package main import ( "fmt" "math" "github.com/ccoveille/go-safecast" ) func main() { var a int a = 42 b, err := safecast.ToUint8(a) // everything is fine if err != nil { fmt.Println(err) } fmt.Println(b) // Output: 42 a = 255 + 1 _, err = safecast.ToUint8(a) // 256 is greater than uint8 maximum value if err != nil { fmt.Println(err) // Output: conversion issue: 256 (int) is greater than 255 (uint8): maximum value for this type exceeded } a = -1 _, err = safecast.ToUint8(a) // -1 cannot fit in uint8 if err != nil { fmt.Println(err) // Output: conversion issue: -1 (int) is less than 0 (uint8): minimum value for this type exceeded } str := "\x99" // ASCII code 153 for Trademark symbol e := str[0] _, err = safecast.ToInt8(e) if err != nil { fmt.Println(err) // Output: conversion issue: 153 (uint8) is greater than 127 (int8): maximum value for this type exceeded } } ``` [Go Playground](https://go.dev/play/p/nelJshulOnj) ## Conversion issues Issues can happen when converting between signed and unsigned integers, or when converting to a smaller integer type. ```go package main import "fmt" func main() { var a int64 a = 42 b := uint8(a) fmt.Println(b) // 42 a = 255 // this is the math.MaxUint8 b = uint8(a) fmt.Println(b) // 255 a = 255 + 1 b = uint8(a) fmt.Println(b) // 0 conversion overflow a = -1 b = uint8(a) fmt.Println(b) // 255 conversion overflow } ``` [Go Playground](https://go.dev/play/p/DHfNUcZBvVn) So you need to adapt your code to write something like this. ```go package main import "fmt" func main() { var a int64 a = 42 if a < 0 || a > math.MaxUint8 { log.Println("overflow") // Output: overflow } fmt.Println(b) // 42 a = 255 // this is the math.MaxUint8 b = uint8(a) fmt.Println(b) // 255 a = 255 + 1 b = uint8(a) if a < 0 || a > math.MaxUint8 { log.Println("overflow") // Output: overflow } fmt.Println(b) // Output: 0 a = -1 b = uint8(a) if a < 0 || a > math.MaxUint8 { log.Println("overflow") // Output: overflow } fmt.Println(b) // Output:255 } ``` [Go Playground](https://go.dev/play/p/qAHGyy4NCLP) `go-safecast` is there to avoid boilerplate copy pasta. ## Motivation The gosec project raised this to my attention when the gosec [G115 rule was added](https://github.com/securego/gosec/pull/1149) > G115: Potential overflow when converting between integer types. This issue was way more complex than expected, and required multiple fixes. [CWE-190](https://cwe.mitre.org/data/definitions/190.html) explains in detail. But to sum it up, you can face: - infinite loop - access to wrong resource by id - grant access to someone who exhausted their quota The gosec G115 will now report issues in a lot of project. ## Alternatives Some libraries existed, but they were not able to cover all the use cases. - [github.com/rung/go-safecast](https://github.com/rung/go-safecast): Unmaintained, not architecture agnostic, do not support `uint` -> `int` conversion - [github.com/cybergarage/go-safecast](https://github.com/cybergarage/go-safecast) Work with pointer like `json.Marshall` ## Stargazers over time [![Stargazers over time](https://starchart.cc/ccoVeille/go-safecast.svg?variant=adaptive)](https://starchart.cc/ccoVeille/go-safecast) go-safecast-1.6.0/asserters.go000066400000000000000000000026661476433272100162770ustar00rootroot00000000000000package safecast import "math" func negative[T Number](t T) bool { return t < 0 } func sameSign[T1, T2 Number](a T1, b T2) bool { return negative(a) == negative(b) } func getUpperBoundary(value any) any { var upper any = math.Inf(1) switch value.(type) { case int8: upper = int8(math.MaxInt8) case int16: upper = int16(math.MaxInt16) case int32: upper = int32(math.MaxInt32) case int64: upper = int64(math.MaxInt64) case int: upper = int(math.MaxInt) case uint8: upper = uint8(math.MaxUint8) case uint32: upper = uint32(math.MaxUint32) case uint16: upper = uint16(math.MaxUint16) case uint64: upper = uint64(math.MaxUint64) case uint: upper = uint(math.MaxUint) // Note: there is no float64 boundary // because float64 cannot overflow case float32: upper = float32(math.MaxFloat32) } return upper } func getLowerBoundary(value any) any { var lower any = math.Inf(-1) switch value.(type) { case int64: lower = int64(math.MinInt64) case int32: lower = int32(math.MinInt32) case int16: lower = int16(math.MinInt16) case int8: lower = int8(math.MinInt8) case int: lower = int(math.MinInt) case uint: lower = uint(0) case uint8: lower = uint8(0) case uint16: lower = uint16(0) case uint32: lower = uint32(0) case uint64: lower = uint64(0) // Note: there is no float64 boundary // because float64 cannot overflow case float32: lower = float32(-math.MaxFloat32) } return lower } go-safecast-1.6.0/asserters_test.go000066400000000000000000000220451476433272100173270ustar00rootroot00000000000000package safecast_test import ( "errors" "math" "strings" "testing" "github.com/ccoveille/go-safecast" ) func assertEqual[V comparable](t *testing.T, expected, got V) { t.Helper() if expected == got { return } t.Errorf("Not equal: \n"+ "expected: %v (%T)\n"+ "actual : %v (%T)", expected, expected, got, got) } func requireError(t *testing.T, err error) { t.Helper() if err == nil { t.Fatal("expected error") } } func requireErrorIs(t *testing.T, err error, expected error) { t.Helper() requireError(t, err) if !errors.Is(err, expected) { t.Fatalf("unexpected error got %v, expected %v", err, expected) } } func requireErrorContains(t *testing.T, err error, text string) { t.Helper() requireErrorIs(t, err, safecast.ErrConversionIssue) errMessage := err.Error() if !strings.Contains(errMessage, text) { t.Fatalf("error message should contain %q: %q", text, errMessage) } } func assertNoError(t *testing.T, err error) { t.Helper() if err != nil { t.Errorf("expected no error, got %v", err) } } type caseInt8[in safecast.Number] struct { name string input in want int8 } func assertInt8OK[in safecast.Number](t *testing.T, tests []caseInt8[in]) { t.Helper() for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { got, err := safecast.ToInt8(tt.input) assertNoError(t, err) assertEqual(t, tt.want, got) }) } } func assertInt8Error[in safecast.Number](t *testing.T, tests []caseInt8[in]) { t.Helper() for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { got, err := safecast.ToInt8(tt.input) requireErrorIs(t, err, safecast.ErrConversionIssue) assertEqual(t, tt.want, got) }) } } func TestErrorMessage(t *testing.T) { _, err := safecast.ToUint8(-1) requireErrorIs(t, err, safecast.ErrConversionIssue) requireErrorIs(t, err, safecast.ErrExceedMinimumValue) requireErrorContains(t, err, "than 0 (uint8)") _, err = safecast.ToUint8(math.MaxInt16) requireErrorIs(t, err, safecast.ErrConversionIssue) requireErrorIs(t, err, safecast.ErrExceedMaximumValue) requireErrorContains(t, err, "than 255 (uint8)") _, err = safecast.ToInt8(-math.MaxInt16) requireErrorIs(t, err, safecast.ErrConversionIssue) requireErrorIs(t, err, safecast.ErrExceedMinimumValue) requireErrorContains(t, err, "than -128 (int8)") } type caseUint8[in safecast.Number] struct { name string input in want uint8 } func assertUint8OK[in safecast.Number](t *testing.T, tests []caseUint8[in]) { t.Helper() for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { got, err := safecast.ToUint8(tt.input) assertNoError(t, err) assertEqual(t, tt.want, got) }) } } func assertUint8Error[in safecast.Number](t *testing.T, tests []caseUint8[in]) { t.Helper() for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { t.Helper() got, err := safecast.ToUint8(tt.input) requireErrorIs(t, err, safecast.ErrConversionIssue) assertEqual(t, tt.want, got) }) } } type caseInt16[in safecast.Number] struct { name string input in want int16 } func assertInt16OK[in safecast.Number](t *testing.T, tests []caseInt16[in]) { t.Helper() for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { got, err := safecast.ToInt16(tt.input) assertNoError(t, err) assertEqual(t, tt.want, got) }) } } func assertInt16Error[in safecast.Number](t *testing.T, tests []caseInt16[in]) { t.Helper() for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { got, err := safecast.ToInt16(tt.input) requireErrorIs(t, err, safecast.ErrConversionIssue) assertEqual(t, tt.want, got) }) } } type caseUint16[in safecast.Number] struct { name string input in want uint16 } func assertUint16OK[in safecast.Number](t *testing.T, tests []caseUint16[in]) { t.Helper() for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { got, err := safecast.ToUint16(tt.input) assertNoError(t, err) assertEqual(t, tt.want, got) }) } } func assertUint16Error[in safecast.Number](t *testing.T, tests []caseUint16[in]) { t.Helper() for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { t.Helper() got, err := safecast.ToUint16(tt.input) requireErrorIs(t, err, safecast.ErrConversionIssue) assertEqual(t, tt.want, got) }) } } type caseInt32[in safecast.Number] struct { name string input in want int32 } func assertInt32OK[in safecast.Number](t *testing.T, tests []caseInt32[in]) { t.Helper() for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { got, err := safecast.ToInt32(tt.input) assertNoError(t, err) assertEqual(t, tt.want, got) }) } } func assertInt32Error[in safecast.Number](t *testing.T, tests []caseInt32[in]) { t.Helper() for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { got, err := safecast.ToInt32(tt.input) requireErrorIs(t, err, safecast.ErrConversionIssue) assertEqual(t, tt.want, got) }) } } type caseUint32[in safecast.Number] struct { name string input in want uint32 } func assertUint32OK[in safecast.Number](t *testing.T, tests []caseUint32[in]) { t.Helper() for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { got, err := safecast.ToUint32(tt.input) assertNoError(t, err) assertEqual(t, tt.want, got) }) } } func assertUint32Error[in safecast.Number](t *testing.T, tests []caseUint32[in]) { t.Helper() for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { t.Helper() got, err := safecast.ToUint32(tt.input) requireErrorIs(t, err, safecast.ErrConversionIssue) assertEqual(t, tt.want, got) }) } } type caseInt64[in safecast.Number] struct { name string input in want int64 } func assertInt64OK[in safecast.Number](t *testing.T, tests []caseInt64[in]) { t.Helper() for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { got, err := safecast.ToInt64(tt.input) assertNoError(t, err) assertEqual(t, tt.want, got) }) } } func assertInt64Error[in safecast.Number](t *testing.T, tests []caseInt64[in]) { t.Helper() for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { got, err := safecast.ToInt64(tt.input) requireErrorIs(t, err, safecast.ErrConversionIssue) assertEqual(t, tt.want, got) }) } } type caseUint64[in safecast.Number] struct { name string input in want uint64 } func assertUint64OK[in safecast.Number](t *testing.T, tests []caseUint64[in]) { t.Helper() for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { got, err := safecast.ToUint64(tt.input) assertNoError(t, err) assertEqual(t, tt.want, got) }) } } func assertUint64Error[in safecast.Number](t *testing.T, tests []caseUint64[in]) { t.Helper() for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { got, err := safecast.ToUint64(tt.input) requireErrorIs(t, err, safecast.ErrConversionIssue) assertEqual(t, tt.want, got) }) } } type caseInt[in safecast.Number] struct { name string input in want int } func assertIntOK[in safecast.Number](t *testing.T, tests []caseInt[in]) { t.Helper() for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { got, err := safecast.ToInt(tt.input) assertNoError(t, err) assertEqual(t, tt.want, got) }) } } func assertIntError[in safecast.Number](t *testing.T, tests []caseInt[in]) { t.Helper() for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { got, err := safecast.ToInt(tt.input) requireErrorIs(t, err, safecast.ErrConversionIssue) assertEqual(t, tt.want, got) }) } } type caseUint[in safecast.Number] struct { name string input in want uint } func assertUintOK[in safecast.Number](t *testing.T, tests []caseUint[in]) { t.Helper() for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { got, err := safecast.ToUint(tt.input) assertNoError(t, err) assertEqual(t, tt.want, got) }) } } func assertUintError[in safecast.Number](t *testing.T, tests []caseUint[in]) { t.Helper() for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { got, err := safecast.ToUint(tt.input) requireErrorIs(t, err, safecast.ErrConversionIssue) assertEqual(t, tt.want, got) }) } } type caseFloat32[in safecast.Number] struct { name string input in want float32 } func assertFloat32OK[in safecast.Number](t *testing.T, tests []caseFloat32[in]) { t.Helper() for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { got, err := safecast.ToFloat32(tt.input) assertNoError(t, err) assertEqual(t, tt.want, got) }) } } func assertFloat32Error[in safecast.Number](t *testing.T, tests []caseFloat32[in]) { t.Helper() for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { got, err := safecast.ToFloat32(tt.input) requireErrorIs(t, err, safecast.ErrConversionIssue) assertEqual(t, tt.want, got) }) } } type caseFloat64[in safecast.Number] struct { name string input in want float64 } func assertFloat64OK[in safecast.Number](t *testing.T, tests []caseFloat64[in]) { t.Helper() for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { got, err := safecast.ToFloat64(tt.input) assertNoError(t, err) assertEqual(t, tt.want, got) }) } } go-safecast-1.6.0/conversion.go000066400000000000000000000213451476433272100164440ustar00rootroot00000000000000package safecast import ( "errors" "fmt" "math" "reflect" "strconv" "strings" ) // Convert attempts to convert any value to the desired type // - If the conversion is possible, the converted value is returned. // - If the conversion results in a value outside the range of the desired type, an [ErrRangeOverflow] error is wrapped in the returned error. // - If the conversion exceeds the maximum value of the desired type, an [ErrExceedMaximumValue] error is wrapped in the returned error. // - If the conversion exceeds the minimum value of the desired type, an [ErrExceedMinimumValue] error is wrapped in the returned error. // - If the conversion is not possible for the desired type, an [ErrUnsupportedConversion] error is wrapped in the returned error. // - If the conversion fails from string, an [ErrStringConversion] error is wrapped in the returned error. // - If the conversion results in an error, an [ErrConversionIssue] error is wrapped in the returned error. func Convert[NumOut Number, NumIn Input](orig NumIn) (converted NumOut, err error) { v := reflect.ValueOf(orig) switch v.Kind() { case reflect.Int: return convertFromNumber[NumOut](int(v.Int())) case reflect.Uint: return convertFromNumber[NumOut](uint(v.Uint())) case reflect.Int8: //nolint:gosec // the int8 is confirmed return convertFromNumber[NumOut](int8(v.Int())) case reflect.Uint8: //nolint:gosec // the uint8 is confirmed return convertFromNumber[NumOut](uint8(v.Uint())) case reflect.Int16: //nolint:gosec // the int16 is confirmed return convertFromNumber[NumOut](int16(v.Int())) case reflect.Uint16: //nolint:gosec // the uint16 is confirmed return convertFromNumber[NumOut](uint16(v.Uint())) case reflect.Int32: //nolint:gosec // the int32 is confirmed return convertFromNumber[NumOut](int32(v.Int())) case reflect.Uint32: //nolint:gosec // the uint32 is confirmed return convertFromNumber[NumOut](uint32(v.Uint())) case reflect.Int64: return convertFromNumber[NumOut](int64(v.Int())) case reflect.Uint64: return convertFromNumber[NumOut](uint64(v.Uint())) case reflect.Float32: return convertFromNumber[NumOut](float32(v.Float())) case reflect.Float64: return convertFromNumber[NumOut](float64(v.Float())) case reflect.Bool: o := 0 if v.Bool() { o = 1 } return NumOut(o), nil case reflect.String: return convertFromString[NumOut](v.String()) } return 0, errorHelper{ err: fmt.Errorf("%w from %T", ErrUnsupportedConversion, orig), } } // MustConvert calls [Convert] to convert the value to the desired type, and panics if the conversion fails. func MustConvert[NumOut Number, NumIn Input](orig NumIn) NumOut { converted, err := Convert[NumOut](orig) if err != nil { panic(err) } return converted } func convertFromNumber[NumOut Number, NumIn Number](orig NumIn) (converted NumOut, err error) { converted = NumOut(orig) // floats could be compared directly switch any(converted).(type) { case float64: // float64 cannot overflow, so we don't have to worry about it return converted, nil case float32: origFloat64, isFloat64 := any(orig).(float64) if !isFloat64 { // only float64 can overflow float32 // everything else can be safely converted return converted, nil } // check boundary if math.Abs(origFloat64) < math.MaxFloat32 { // the value is within float32 range, there is no overflow return converted, nil } // TODO: check for numbers close to math.MaxFloat32 boundary := getUpperBoundary(converted) errBoundary := ErrExceedMaximumValue if negative(orig) { boundary = getLowerBoundary(converted) errBoundary = ErrExceedMinimumValue } return 0, errorHelper{ value: orig, err: errBoundary, boundary: boundary, } } errBoundary := ErrExceedMaximumValue boundary := getUpperBoundary(converted) if negative(orig) { errBoundary = ErrExceedMinimumValue boundary = getLowerBoundary(converted) } if !sameSign(orig, converted) { return 0, errorHelper{ value: orig, err: errBoundary, boundary: boundary, } } // convert back to the original type cast := NumIn(converted) // and compare base := orig switch f := any(orig).(type) { case float64: base = NumIn(math.Trunc(f)) case float32: base = NumIn(math.Trunc(float64(f))) } // exact match if cast == base { return converted, nil } return 0, errorHelper{ value: orig, err: errBoundary, boundary: boundary, } } func convertFromString[NumOut Number](s string) (converted NumOut, err error) { s = strings.TrimSpace(s) if b, err := strconv.ParseBool(s); err == nil { if b { return NumOut(1), nil } return NumOut(0), nil } if strings.Contains(s, ".") { o, err := strconv.ParseFloat(s, 64) if err != nil { return 0, errorHelper{ value: s, err: fmt.Errorf("%w %v to %T", ErrStringConversion, s, converted), } } return convertFromNumber[NumOut](o) } if strings.HasPrefix(s, "-") { o, err := strconv.ParseInt(s, 0, 64) if err != nil { if errors.Is(err, strconv.ErrRange) { return 0, errorHelper{ value: s, err: ErrExceedMinimumValue, boundary: math.MinInt, } } return 0, errorHelper{ value: s, err: fmt.Errorf("%w %v to %T", ErrStringConversion, s, converted), } } return convertFromNumber[NumOut](o) } o, err := strconv.ParseUint(s, 0, 64) if err != nil { if errors.Is(err, strconv.ErrRange) { return 0, errorHelper{ value: s, err: ErrExceedMaximumValue, boundary: uint(math.MaxUint), } } return 0, errorHelper{ value: s, err: fmt.Errorf("%w %v to %T", ErrStringConversion, s, converted), } } return convertFromNumber[NumOut](o) } // ToInt attempts to convert any [Type] value to an int. // If the conversion results in a value outside the range of an int, // an [ErrConversionIssue] error is returned. func ToInt[T Number](i T) (int, error) { return convertFromNumber[int](i) } // ToUint attempts to convert any [Number] value to an uint. // If the conversion results in a value outside the range of an uint, // an [ErrConversionIssue] error is returned. func ToUint[T Number](i T) (uint, error) { return convertFromNumber[uint](i) } // ToInt8 attempts to convert any [Number] value to an int8. // If the conversion results in a value outside the range of an int8, // an [ErrConversionIssue] error is returned. func ToInt8[T Number](i T) (int8, error) { return convertFromNumber[int8](i) } // ToUint8 attempts to convert any [Number] value to an uint8. // If the conversion results in a value outside the range of an uint8, // an [ErrConversionIssue] error is returned. func ToUint8[T Number](i T) (uint8, error) { return convertFromNumber[uint8](i) } // ToInt16 attempts to convert any [Number] value to an int16. // If the conversion results in a value outside the range of an int16, // an [ErrConversionIssue] error is returned. func ToInt16[T Number](i T) (int16, error) { return convertFromNumber[int16](i) } // ToUint16 attempts to convert any [Number] value to an uint16. // If the conversion results in a value outside the range of an uint16, // an [ErrConversionIssue] error is returned. func ToUint16[T Number](i T) (uint16, error) { return convertFromNumber[uint16](i) } // ToInt32 attempts to convert any [Number] value to an int32. // If the conversion results in a value outside the range of an int32, // an [ErrConversionIssue] error is returned. func ToInt32[T Number](i T) (int32, error) { return convertFromNumber[int32](i) } // ToUint32 attempts to convert any [Number] value to an uint32. // If the conversion results in a value outside the range of an uint32, // an [ErrConversionIssue] error is returned. func ToUint32[T Number](i T) (uint32, error) { return convertFromNumber[uint32](i) } // ToInt64 attempts to convert any [Number] value to an int64. // If the conversion results in a value outside the range of an int64, // an [ErrConversionIssue] error is returned. func ToInt64[T Number](i T) (int64, error) { return convertFromNumber[int64](i) } // ToUint64 attempts to convert any [Number] value to an uint64. // If the conversion results in a value outside the range of an uint64, // an [ErrConversionIssue] error is returned. func ToUint64[T Number](i T) (uint64, error) { return convertFromNumber[uint64](i) } // ToFloat32 attempts to convert any [Number] value to a float32. // If the conversion results in a value outside the range of a float32, // an [ErrConversionIssue] error is returned. func ToFloat32[T Number](i T) (float32, error) { return convertFromNumber[float32](i) } // ToFloat64 attempts to convert any [Number] value to a float64. // If the conversion results in a value outside the range of a float64, // an [ErrConversionIssue] error is returned. func ToFloat64[T Number](i T) (float64, error) { return convertFromNumber[float64](i) } go-safecast-1.6.0/conversion_64bit_test.go000066400000000000000000000041631476433272100205120ustar00rootroot00000000000000//go:build !386 && !arm package safecast_test // The tests in conversion_test.go are the ones that are not architecture dependent // The tests in conversion_64bit_test.go complete them for 64-bit systems // // This architecture dependent file covers the fact, you can reach a higher value with int and uint // on 64-bit systems, but you will get a compile error on 32-bit. // This is why it needs to be tested in an architecture dependent way. import ( "math" "testing" "github.com/ccoveille/go-safecast" ) func TestToInt32_64bit(t *testing.T) { t.Run("from int", func(t *testing.T) { assertInt32Error(t, []caseInt32[int]{ {name: "positive out of range", input: math.MaxInt32 + 1}, {name: "negative out of range", input: math.MinInt32 - 1}, }) }) } func TestToUint32_64bit(t *testing.T) { t.Run("from int", func(t *testing.T) { assertUint32Error(t, []caseUint32[int]{ {name: "positive out of range", input: math.MaxUint32 + 1}, {name: "negative value", input: -1}, }) }) } func TestToInt64_64bit(t *testing.T) { t.Run("from uint", func(t *testing.T) { assertInt64Error(t, []caseInt64[uint]{ {name: "positive out of range", input: math.MaxInt64 + 1}, }) }) } func TestToInt_64bit(t *testing.T) { t.Run("from uint", func(t *testing.T) { assertIntError(t, []caseInt[uint]{ {name: "positive out of range", input: math.MaxInt64 + 1}, }) }) t.Run("from float64", func(t *testing.T) { assertIntOK(t, []caseInt[float64]{ {name: "math.MinInt64", input: math.MinInt64, want: math.MinInt64}, // pass because of float imprecision }) }) } // TestConvert_64bit completes the [TestConvert] tests in conversion_test.go // it contains the tests that can only works on 64-bit systems func TestConvert_64bit(t *testing.T) { t.Run("to uint32", func(t *testing.T) { for name, tt := range map[string]struct { input uint64 want uint32 }{ "positive out of range": {input: uint64(math.MaxUint32 + 1), want: 0}, } { t.Run(name, func(t *testing.T) { got, err := safecast.Convert[uint32](tt.input) assertEqual(t, tt.want, got) requireErrorIs(t, err, safecast.ErrConversionIssue) }) } }) } go-safecast-1.6.0/conversion_test.go000066400000000000000000002165441476433272100175120ustar00rootroot00000000000000package safecast_test // The tests in conversion_test.go are the ones that are not architecture dependent // The tests in conversion_64bit_test.go complete them for 64-bit systems // // The architecture dependent file covers the fact, you can reach a higher value with int and uint // on 64-bit systems, but you will get a compile error on 32-bit. // This is why it needs to be tested in an architecture dependent way. import ( "errors" "math" "testing" "github.com/ccoveille/go-safecast" ) func TestToInt8(t *testing.T) { t.Run("from int", func(t *testing.T) { assertInt8OK(t, []caseInt8[int]{ {name: "zero", input: 0, want: 0}, {name: "positive within range", input: 100, want: 100}, {name: "negative within range", input: -100, want: -100}, }) assertInt8Error(t, []caseInt8[int]{ {name: "positive out of range", input: math.MaxInt8 + 1}, {name: "negative out of range", input: math.MinInt8 - 1}, }) }) t.Run("from int8", func(t *testing.T) { assertInt8OK(t, []caseInt8[int8]{ {name: "zero", input: 0, want: 0}, {name: "positive within range", input: 100, want: 100}, {name: "negative within range", input: -100, want: -100}, }) }) t.Run("from int16", func(t *testing.T) { assertInt8OK(t, []caseInt8[int16]{ {name: "zero", input: 0, want: 0}, {name: "positive within range", input: 100, want: 100}, {name: "negative within range", input: -100, want: -100}, }) assertInt8Error(t, []caseInt8[int16]{ {name: "positive out of range", input: math.MaxInt8 + 1}, {name: "negative out of range", input: math.MinInt8 - 1}, }) }) t.Run("from int32", func(t *testing.T) { assertInt8OK(t, []caseInt8[int32]{ {name: "zero", input: 0, want: 0}, {name: "positive within range", input: 100, want: 100}, {name: "negative within range", input: -100, want: -100}, }) assertInt8Error(t, []caseInt8[int32]{ {name: "positive out of range", input: math.MaxInt8 + 1}, {name: "negative out of range", input: math.MinInt8 - 1}, }) }) t.Run("from int64", func(t *testing.T) { assertInt8OK(t, []caseInt8[int64]{ {name: "zero", input: 0, want: 0}, {name: "positive within range", input: 100, want: 100}, {name: "negative within range", input: -100, want: -100}, }) assertInt8Error(t, []caseInt8[int64]{ {name: "positive out of range", input: math.MaxInt8 + 1}, {name: "negative out of range", input: math.MinInt8 - 1}, }) }) t.Run("from uint", func(t *testing.T) { assertInt8OK(t, []caseInt8[uint]{ {name: "zero", input: 0, want: 0}, {name: "positive within range", input: 100, want: 100}, }) assertInt8Error(t, []caseInt8[uint]{ {name: "positive out of range", input: math.MaxInt8 + 1}, }) }) t.Run("from uint8", func(t *testing.T) { assertInt8OK(t, []caseInt8[uint8]{ {name: "zero", input: 0, want: 0}, {name: "positive within range", input: 100, want: 100}, }) }) t.Run("from uint16", func(t *testing.T) { assertInt8OK(t, []caseInt8[uint16]{ {name: "zero", input: 0, want: 0}, {name: "positive within range", input: 100, want: 100}, }) assertInt8Error(t, []caseInt8[uint16]{ {name: "positive out of range", input: math.MaxInt8 + 1}, }) }) t.Run("from uint32", func(t *testing.T) { assertInt8OK(t, []caseInt8[uint32]{ {name: "zero", input: 0, want: 0}, {name: "positive within range", input: 100, want: 100}, }) assertInt8Error(t, []caseInt8[uint32]{ {name: "positive out of range", input: math.MaxInt8 + 1}, }) }) t.Run("from uint64", func(t *testing.T) { assertInt8OK(t, []caseInt8[uint64]{ {name: "zero", input: 0, want: 0}, {name: "positive within range", input: 100, want: 100}, }) assertInt8Error(t, []caseInt8[uint64]{ {name: "positive out of range", input: math.MaxInt8 + 1}, }) }) t.Run("from float32", func(t *testing.T) { assertInt8OK(t, []caseInt8[float32]{ {name: "zero", input: 0.0, want: 0}, {name: "rounded value", input: 1.1, want: 1}, }) assertInt8Error(t, []caseInt8[float32]{ {name: "positive out of range", input: math.MaxInt8 + 1}, }) }) t.Run("from float64", func(t *testing.T) { assertInt8OK(t, []caseInt8[float64]{ {name: "zero", input: 0.0, want: 0}, {name: "rounded value", input: 1.1, want: 1}, }) assertInt8Error(t, []caseInt8[float64]{ {name: "positive out of range", input: math.MaxInt8 + 1}, }) }) } func TestToUint8(t *testing.T) { t.Run("from int", func(t *testing.T) { assertUint8OK(t, []caseUint8[int]{ {name: "zero", input: 0, want: 0}, {name: "positive within range", input: 100, want: 100}, }) assertUint8Error(t, []caseUint8[int]{ {name: "positive out of range", input: math.MaxUint8 + 1}, {name: "negative value", input: -1}, }) }) t.Run("from int8", func(t *testing.T) { assertUint8OK(t, []caseUint8[int8]{ {name: "zero", input: 0, want: 0}, {name: "positive within range", input: 100, want: 100}, }) assertUint8Error(t, []caseUint8[int8]{ {name: "negative value", input: -1}, }) }) t.Run("from int16", func(t *testing.T) { assertUint8OK(t, []caseUint8[int16]{ {name: "zero", input: 0, want: 0}, {name: "positive within range", input: 100, want: 100}, }) assertUint8Error(t, []caseUint8[int16]{ {name: "positive out of range", input: 10000}, {name: "negative value", input: -1}, }) }) t.Run("from int32", func(t *testing.T) { assertUint8OK(t, []caseUint8[int32]{ {name: "zero", input: 0, want: 0}, {name: "positive within range", input: 100, want: 100}, }) assertUint8Error(t, []caseUint8[int32]{ {name: "positive out of range", input: 100000}, {name: "negative value", input: -1}, }) }) t.Run("from int64", func(t *testing.T) { assertUint8OK(t, []caseUint8[int64]{ {name: "zero", input: 0, want: 0}, {name: "positive within range", input: 100, want: 100}, }) assertUint8Error(t, []caseUint8[int64]{ {name: "positive out of range", input: 100000}, {name: "negative value", input: -1}, }) }) t.Run("from uint", func(t *testing.T) { assertUint8OK(t, []caseUint8[uint]{ {name: "zero", input: 0, want: 0}, {name: "positive within range", input: 100, want: 100}, }) assertUint8Error(t, []caseUint8[uint]{ {name: "positive out of range", input: math.MaxUint8 + 1}, }) }) t.Run("from uint8", func(t *testing.T) { assertUint8OK(t, []caseUint8[uint8]{ {name: "zero", input: 0, want: 0}, {name: "positive within range", input: 100, want: 100}, }) }) t.Run("from uint16", func(t *testing.T) { assertUint8OK(t, []caseUint8[uint16]{ {name: "zero", input: 0, want: 0}, {name: "positive within range", input: 100, want: 100}, }) assertUint8Error(t, []caseUint8[uint]{ {name: "positive out of range", input: math.MaxUint8 + 1}, }) }) t.Run("from uint32", func(t *testing.T) { assertUint8OK(t, []caseUint8[uint32]{ {name: "zero", input: 0, want: 0}, {name: "positive within range", input: 100, want: 100}, }) assertUint8Error(t, []caseUint8[uint]{ {name: "positive out of range", input: math.MaxUint8 + 1}, }) }) t.Run("from uint64", func(t *testing.T) { assertUint8OK(t, []caseUint8[uint64]{ {name: "zero", input: 0, want: 0}, {name: "positive within range", input: 100, want: 100}, }) assertUint8Error(t, []caseUint8[uint]{ {name: "positive out of range", input: math.MaxUint8 + 1}, }) }) t.Run("from float32", func(t *testing.T) { assertUint8OK(t, []caseUint8[float32]{ {name: "zero", input: 0.0, want: 0}, {name: "rounded value", input: 1.1, want: 1}, {name: "positive within range", input: 100.9, want: 100}, }) assertUint8Error(t, []caseUint8[float32]{ {name: "positive out of range", input: math.MaxUint8 + 1}, }) }) t.Run("from float64", func(t *testing.T) { assertUint8OK(t, []caseUint8[float64]{ {name: "zero", input: 0.0, want: 0}, {name: "rounded value", input: 1.1, want: 1}, {name: "positive within range", input: 100.9, want: 100}, }) assertUint8Error(t, []caseUint8[float64]{ {name: "positive out of range", input: math.MaxUint8 + 1}, }) }) } func TestToInt16(t *testing.T) { t.Run("from int", func(t *testing.T) { assertInt16OK(t, []caseInt16[int]{ {name: "zero", input: 0, want: 0}, {name: "positive within range", input: 10000, want: 10000}, {name: "negative within range", input: -10000, want: -10000}, }) assertInt16Error(t, []caseInt16[int]{ {name: "positive out of range", input: math.MaxInt16 + 1}, {name: "negative out of range", input: math.MinInt16 - 1}, }) }) t.Run("from int8", func(t *testing.T) { assertInt16OK(t, []caseInt16[int8]{ {name: "zero", input: 0, want: 0}, {name: "positive within range", input: 100, want: 100}, {name: "negative within range", input: -100, want: -100}, }) }) t.Run("from int16", func(t *testing.T) { assertInt16OK(t, []caseInt16[int16]{ {name: "zero", input: 0, want: 0}, {name: "positive within range", input: 10000, want: 10000}, {name: "negative within range", input: -10000, want: -10000}, }) }) t.Run("from int32", func(t *testing.T) { assertInt16OK(t, []caseInt16[int32]{ {name: "zero", input: 0, want: 0}, {name: "positive within range", input: 10000, want: 10000}, {name: "negative within range", input: -10000, want: -10000}, }) assertInt16Error(t, []caseInt16[int32]{ {name: "positive out of range", input: 100000}, {name: "negative out of range", input: -100000}, }) }) t.Run("from int64", func(t *testing.T) { assertInt16OK(t, []caseInt16[int64]{ {name: "zero", input: 0, want: 0}, {name: "positive within range", input: 10000, want: 10000}, {name: "negative within range", input: -10000, want: -10000}, }) assertInt16Error(t, []caseInt16[int64]{ {name: "positive out of range", input: 100000}, {name: "negative out of range", input: -100000}, }) }) t.Run("from uint", func(t *testing.T) { assertInt16OK(t, []caseInt16[uint]{ {name: "zero", input: 0, want: 0}, {name: "positive within range", input: 100, want: 100}, }) assertInt16Error(t, []caseInt16[uint]{ {name: "positive out of range", input: math.MaxInt16 + 1}, }) }) t.Run("from uint8", func(t *testing.T) { assertInt16OK(t, []caseInt16[uint8]{ {name: "zero", input: 0, want: 0}, {name: "positive within range", input: 100, want: 100}, }) }) t.Run("from uint16", func(t *testing.T) { assertInt16OK(t, []caseInt16[uint16]{ {name: "zero", input: 0, want: 0}, {name: "positive within range", input: 100, want: 100}, }) assertInt16Error(t, []caseInt16[uint16]{ {name: "positive out of range", input: math.MaxInt16 + 1}, }) }) t.Run("from uint32", func(t *testing.T) { assertInt16OK(t, []caseInt16[uint32]{ {name: "zero", input: 0, want: 0}, {name: "positive within range", input: 100, want: 100}, }) assertInt16Error(t, []caseInt16[uint32]{ {name: "positive out of range", input: math.MaxInt16 + 1}, }) }) t.Run("from uint64", func(t *testing.T) { assertInt16OK(t, []caseInt16[uint64]{ {name: "zero", input: 0, want: 0}, {name: "positive within range", input: 100, want: 100}, }) assertInt16Error(t, []caseInt16[uint64]{ {name: "positive out of range", input: math.MaxInt16 + 1}, }) }) t.Run("from float32", func(t *testing.T) { assertInt16OK(t, []caseInt16[float32]{ {name: "zero", input: 0.0, want: 0}, {name: "rounded value", input: 1.1, want: 1}, {name: "positive within range", input: 10000, want: 10000}, }) assertInt16Error(t, []caseInt16[float32]{ {name: "positive out of range", input: math.MaxInt16 + 1}, }) }) t.Run("from float64", func(t *testing.T) { assertInt16OK(t, []caseInt16[float64]{ {name: "zero", input: 0.0, want: 0}, {name: "rounded value", input: 1.1, want: 1}, {name: "positive within range", input: 10000, want: 10000}, }) assertInt16Error(t, []caseInt16[float64]{ {name: "positive out of range", input: math.MaxInt16 + 1}, }) }) } func TestToUint16(t *testing.T) { t.Run("from int", func(t *testing.T) { assertUint16OK(t, []caseUint16[int]{ {name: "zero", input: 0, want: 0}, {name: "positive within range", input: 10000, want: 10000}, }) assertUint16Error(t, []caseUint16[int]{ {name: "positive out of range", input: math.MaxUint16 + 1}, {name: "negative value", input: -1}, }) }) t.Run("from int8", func(t *testing.T) { assertUint16OK(t, []caseUint16[int8]{ {name: "zero", input: 0, want: 0}, {name: "positive within range", input: 100, want: 100}, }) }) t.Run("from int16", func(t *testing.T) { assertUint16OK(t, []caseUint16[int16]{ {name: "zero", input: 0, want: 0}, {name: "positive within range", input: 10000, want: 10000}, }) }) t.Run("from int32", func(t *testing.T) { assertUint16OK(t, []caseUint16[int32]{ {name: "zero", input: 0, want: 0}, {name: "positive within range", input: 10000, want: 10000}, }) assertUint16Error(t, []caseUint16[int32]{ {name: "positive out of range", input: 100000}, {name: "negative value", input: -1}, }) }) t.Run("from int64", func(t *testing.T) { assertUint16OK(t, []caseUint16[int64]{ {name: "zero", input: 0, want: 0}, {name: "positive within range", input: 10000, want: 10000}, }) assertUint16Error(t, []caseUint16[int64]{ {name: "positive out of range", input: 100000}, {name: "negative value", input: -1}, }) }) t.Run("from uint", func(t *testing.T) { assertUint16OK(t, []caseUint16[uint]{ {name: "zero", input: 0, want: 0}, {name: "positive within range", input: 100, want: 100}, }) assertUint16Error(t, []caseUint16[uint]{ {name: "positive out of range", input: math.MaxUint16 + 1}, }) }) t.Run("from uint8", func(t *testing.T) { assertUint16OK(t, []caseUint16[uint8]{ {name: "zero", input: 0, want: 0}, {name: "positive within range", input: 100, want: 100}, }) }) t.Run("from uint16", func(t *testing.T) { assertUint16OK(t, []caseUint16[uint16]{ {name: "zero", input: 0, want: 0}, {name: "positive within range", input: 100, want: 100}, }) }) t.Run("from uint32", func(t *testing.T) { assertUint16OK(t, []caseUint16[uint32]{ {name: "zero", input: 0, want: 0}, {name: "positive within range", input: 100, want: 100}, }) assertUint16Error(t, []caseUint16[uint32]{ {name: "positive out of range", input: math.MaxUint16 + 1}, }) }) t.Run("from uint64", func(t *testing.T) { assertUint16OK(t, []caseUint16[uint64]{ {name: "zero", input: 0, want: 0}, {name: "positive within range", input: 100, want: 100}, }) assertUint16Error(t, []caseUint16[uint64]{ {name: "positive out of range", input: math.MaxUint16 + 1}, }) }) t.Run("from float32", func(t *testing.T) { assertUint16OK(t, []caseUint16[float32]{ {name: "zero", input: 0.0, want: 0}, {name: "rounded value", input: 1.1, want: 1}, {name: "positive within range", input: 10000, want: 10000}, }) assertUint16Error(t, []caseUint16[float32]{ {name: "positive out of range", input: math.MaxUint16 + 1}, }) }) t.Run("from float64", func(t *testing.T) { assertUint16OK(t, []caseUint16[float64]{ {name: "zero", input: 0.0, want: 0}, {name: "rounded value", input: 1.1, want: 1}, {name: "positive within range", input: 10000, want: 10000}, }) assertUint16Error(t, []caseUint16[float32]{ {name: "positive out of range", input: math.MaxUint16 + 1}, }) }) } func TestToInt32(t *testing.T) { t.Run("from int", func(t *testing.T) { assertInt32OK(t, []caseInt32[int]{ {name: "zero", input: 0, want: 0}, {name: "positive within range", input: 10000, want: 10000}, {name: "negative within range", input: -10000, want: -10000}, }) // There are extra checks in [TestToInt32_64bit] // the tests are separated because they cannot work on i386 }) t.Run("from int8", func(t *testing.T) { assertInt32OK(t, []caseInt32[int8]{ {name: "zero", input: 0, want: 0}, {name: "positive within range", input: 100, want: 100}, {name: "negative within range", input: -100, want: -100}, }) }) t.Run("from int16", func(t *testing.T) { assertInt32OK(t, []caseInt32[int16]{ {name: "zero", input: 0, want: 0}, {name: "positive within range", input: 10000, want: 10000}, {name: "negative within range", input: -10000, want: -10000}, }) }) t.Run("from int32", func(t *testing.T) { assertInt32OK(t, []caseInt32[int32]{ {name: "zero", input: 0, want: 0}, {name: "positive within range", input: 10000, want: 10000}, {name: "negative within range", input: -10000, want: -10000}, }) }) t.Run("from int64", func(t *testing.T) { assertInt32OK(t, []caseInt32[int64]{ {name: "zero", input: 0, want: 0}, {name: "positive within range", input: 10000, want: 10000}, {name: "negative within range", input: -10000, want: -10000}, }) assertInt32Error(t, []caseInt32[int64]{ {name: "positive out of range", input: math.MaxInt32 + 1}, {name: "negative out of range", input: math.MinInt32 - 1}, }) }) t.Run("from uint", func(t *testing.T) { assertInt32OK(t, []caseInt32[uint]{ {name: "zero", input: 0, want: 0}, {name: "positive within range", input: 100, want: 100}, }) assertInt32Error(t, []caseInt32[uint]{ {name: "positive out of range", input: math.MaxInt32 + 1}, }) }) t.Run("from uint8", func(t *testing.T) { assertInt32OK(t, []caseInt32[uint8]{ {name: "zero", input: 0, want: 0}, {name: "positive within range", input: 100, want: 100}, }) }) t.Run("from uint16", func(t *testing.T) { assertInt32OK(t, []caseInt32[uint16]{ {name: "zero", input: 0, want: 0}, {name: "positive within range", input: math.MaxUint16, want: math.MaxUint16}, }) }) t.Run("from uint32", func(t *testing.T) { assertInt32OK(t, []caseInt32[uint32]{ {name: "zero", input: 0, want: 0}, {name: "positive within range", input: 100, want: 100}, }) assertInt32Error(t, []caseInt32[uint32]{ {name: "positive out of range", input: math.MaxInt32 + 1}, }) }) t.Run("from uint64", func(t *testing.T) { assertInt32OK(t, []caseInt32[uint64]{ {name: "zero", input: 0, want: 0}, {name: "positive within range", input: 100, want: 100}, }) assertInt32Error(t, []caseInt32[uint64]{ {name: "positive out of range", input: math.MaxInt32 + 1}, }) }) t.Run("from float32", func(t *testing.T) { assertInt32OK(t, []caseInt32[float32]{ {name: "zero", input: 0.0, want: 0}, {name: "rounded value", input: 1.1, want: 1}, {name: "positive within range", input: 10000, want: 10000}, }) assertInt32Error(t, []caseInt32[float32]{ {name: "positive out of range", input: math.MaxInt32 * 1.02}, // because of float imprecision, we have to exceed the min int64 to trigger the error }) }) t.Run("from float64", func(t *testing.T) { assertInt32OK(t, []caseInt32[float64]{ {name: "zero", input: 0.0, want: 0}, {name: "rounded value", input: 1.1, want: 1}, {name: "positive within range", input: 10000, want: 10000}, }) assertInt32Error(t, []caseInt32[float64]{ {name: "positive out of range", input: math.MaxInt32 * 1.02}, // because of float imprecision, we have to exceed the min int64 to trigger the error }) }) } func TestToUint32(t *testing.T) { t.Run("from int", func(t *testing.T) { assertUint32OK(t, []caseUint32[int]{ {name: "zero", input: 0, want: 0}, {name: "positive within range", input: 10000, want: 10000}, }) assertUint32Error(t, []caseUint32[int]{ // There are extra checks in [TestToUint32_64bit] // the tests are separated because they cannot work on i386 {name: "negative value", input: -1}, }) }) t.Run("from int8", func(t *testing.T) { assertUint32OK(t, []caseUint32[int8]{ {name: "zero", input: 0, want: 0}, {name: "positive within range", input: 100, want: 100}, }) assertUint32Error(t, []caseUint32[int8]{ {name: "negative value", input: -1}, }) }) t.Run("from int16", func(t *testing.T) { assertUint32OK(t, []caseUint32[int16]{ {name: "zero", input: 0, want: 0}, {name: "positive within range", input: 10000, want: 10000}, }) assertUint32Error(t, []caseUint32[int16]{ {name: "negative value", input: -1}, }) }) t.Run("from int32", func(t *testing.T) { assertUint32OK(t, []caseUint32[int32]{ {name: "zero", input: 0, want: 0}, {name: "positive within range", input: 10000, want: 10000}, }) assertUint32Error(t, []caseUint32[int32]{ {name: "negative value", input: -1}, }) }) t.Run("from int64", func(t *testing.T) { assertUint32OK(t, []caseUint32[int64]{ {name: "zero", input: 0, want: 0}, {name: "positive within range", input: 10000, want: 10000}, }) assertUint32Error(t, []caseUint32[int64]{ {name: "positive out of range", input: math.MaxUint32 + 1}, {name: "negative value", input: -1}, }) }) t.Run("from uint", func(t *testing.T) { assertUint32OK(t, []caseUint32[uint]{ {name: "zero", input: 0, want: 0}, {name: "positive within range", input: 100, want: 100}, }) // There are extra checks in [TestToUint32_64bit] // the tests are separated because they cannot work on i386 }) t.Run("from uint8", func(t *testing.T) { assertUint32OK(t, []caseUint32[uint8]{ {name: "zero", input: 0, want: 0}, {name: "positive within range", input: 100, want: 100}, }) }) t.Run("from uint16", func(t *testing.T) { assertUint32OK(t, []caseUint32[uint16]{ {name: "zero", input: 0, want: 0}, {name: "positive within range", input: 100, want: 100}, }) }) t.Run("from uint32", func(t *testing.T) { assertUint32OK(t, []caseUint32[uint32]{ {name: "zero", input: 0, want: 0}, {name: "positive within range", input: 100, want: 100}, }) }) t.Run("from uint64", func(t *testing.T) { assertUint32OK(t, []caseUint32[uint64]{ {name: "zero", input: 0, want: 0}, {name: "positive within range", input: 100, want: 100}, }) assertUint32Error(t, []caseUint32[uint64]{ {name: "positive out of range", input: math.MaxUint32 + 1}, }) }) t.Run("from float32", func(t *testing.T) { assertUint32OK(t, []caseUint32[float32]{ {name: "zero", input: 0.0, want: 0}, {name: "rounded value", input: 1.1, want: 1}, {name: "positive within range", input: 10000.9, want: 10000}, }) assertUint32Error(t, []caseUint32[float32]{ {name: "positive out of range", input: math.MaxUint32 * 1.02}, // because of float imprecision, we have to exceed the min int64 to trigger the error }) }) t.Run("from float64", func(t *testing.T) { assertUint32OK(t, []caseUint32[float64]{ {name: "zero", input: 0.0, want: 0}, {name: "rounded value", input: 1.1, want: 1}, {name: "positive within range", input: 10000.9, want: 10000}, }) assertUint32Error(t, []caseUint32[float64]{ {name: "positive out of range", input: math.MaxUint32 * 1.02}, // because of float imprecision, we have to exceed the min int64 to trigger the error {name: "negative value", input: -1}, }) }) } func TestToInt64(t *testing.T) { t.Run("from int", func(t *testing.T) { assertInt64OK(t, []caseInt64[int]{ {name: "zero", input: 0, want: 0}, {name: "positive within range", input: 10000, want: 10000}, {name: "negative within range", input: -10000, want: -10000}, }) }) t.Run("from int8", func(t *testing.T) { assertInt64OK(t, []caseInt64[int8]{ {name: "zero", input: 0, want: 0}, {name: "positive within range", input: 100, want: 100}, {name: "negative within range", input: -100, want: -100}, }) }) t.Run("from int16", func(t *testing.T) { assertInt64OK(t, []caseInt64[int16]{ {name: "zero", input: 0, want: 0}, {name: "positive within range", input: 10000, want: 10000}, {name: "negative within range", input: -10000, want: -10000}, }) }) t.Run("from int32", func(t *testing.T) { assertInt64OK(t, []caseInt64[int32]{ {name: "zero", input: 0, want: 0}, {name: "positive within range", input: 10000, want: 10000}, {name: "negative within range", input: -10000, want: -10000}, }) }) t.Run("from int64", func(t *testing.T) { assertInt64OK(t, []caseInt64[int64]{ {name: "zero", input: 0, want: 0}, {name: "positive within range", input: 10000, want: 10000}, {name: "negative within range", input: -10000, want: -10000}, }) }) t.Run("from uint", func(t *testing.T) { assertInt64OK(t, []caseInt64[uint]{ {name: "zero", input: 0, want: 0}, {name: "positive within range", input: 100, want: 100}, }) // There are extra checks in [TestToInt64_64bit] // the tests are separated because they cannot work on i386 }) t.Run("from uint8", func(t *testing.T) { assertInt64OK(t, []caseInt64[uint8]{ {name: "zero", input: 0, want: 0}, {name: "positive within range", input: 100, want: 100}, }) }) t.Run("from uint16", func(t *testing.T) { assertInt64OK(t, []caseInt64[uint16]{ {name: "zero", input: 0, want: 0}, {name: "positive within range", input: 100, want: 100}, }) }) t.Run("from uint32", func(t *testing.T) { assertInt64OK(t, []caseInt64[uint32]{ {name: "zero", input: 0, want: 0}, {name: "positive within range", input: 100, want: 100}, }) }) t.Run("from uint64", func(t *testing.T) { assertInt64OK(t, []caseInt64[uint64]{ {name: "zero", input: 0, want: 0}, {name: "positive within range", input: 100, want: 100}, }) assertInt64Error(t, []caseInt64[uint64]{ {name: "positive out of range", input: math.MaxInt64 + 1}, }) }) t.Run("from float32", func(t *testing.T) { assertInt64OK(t, []caseInt64[float32]{ {name: "zero", input: 0.0, want: 0}, {name: "rounded value", input: 1.1, want: 1}, {name: "positive within range", input: 10000.9, want: 10000}, {name: "max int16", input: math.MaxInt16, want: math.MaxInt16}, {name: "min int16", input: math.MinInt16, want: math.MinInt16}, {name: "max int32", input: math.MaxInt32, want: 2147483648}, // number differs due to float imprecision {name: "min int32", input: math.MinInt32, want: math.MinInt32}, }) assertInt64Error(t, []caseInt64[float32]{ {name: "out of range math.MaxInt64", input: math.MaxInt64 * 1.02}, // because of float imprecision, we have to exceed the min int64 to trigger the error {name: "out of range math.MaxUint64", input: math.MaxUint64}, {name: "out of range math.MinInt64", input: math.MinInt64 * 1.02}, // because of float imprecision, we have to exceed the min int64 to trigger the error {name: "out of range math.MaxFloat32", input: math.MaxFloat32}, {name: "out of range -math.MaxFloat32", input: -math.MaxFloat32}, }) }) t.Run("from float64", func(t *testing.T) { assertInt64OK(t, []caseInt64[float64]{ {name: "zero", input: 0.0, want: 0}, {name: "rounded value", input: 1.1, want: 1}, {name: "positive within range", input: 10000.9, want: 10000}, {name: "max int16", input: math.MaxInt16, want: math.MaxInt16}, {name: "min int16", input: math.MinInt16, want: math.MinInt16}, {name: "max int32", input: math.MaxInt32, want: math.MaxInt32}, {name: "min int32", input: math.MinInt32, want: math.MinInt32}, }) assertInt64Error(t, []caseInt64[float64]{ {name: "out of range math.MaxInt64 + 1", input: math.MaxInt64 * 1.02}, // because of float imprecision, we have to exceed the min int64 to trigger the error {name: "out of range math.MaxUint64", input: math.MaxUint64}, {name: "out of range math.MinInt64", input: math.MinInt64 * 1.02}, // because of float imprecision, we have to exceed the min int64 to trigger the error {name: "out of range math.MaxFloat32", input: math.MaxFloat32}, {name: "out of range -math.MaxFloat32", input: -math.MaxFloat32}, {name: "out of range math.MaxFloat64", input: math.MaxFloat64}, {name: "out of range -math.MaxFloat64", input: -math.MaxFloat64}, }) }) } func TestToUint64(t *testing.T) { t.Run("from int", func(t *testing.T) { assertUint64OK(t, []caseUint64[int]{ {name: "zero", input: 0, want: 0}, {name: "positive within range", input: 10000, want: 10000}, }) assertUint64Error(t, []caseUint64[int]{ {name: "negative value", input: -1}, }) }) t.Run("from int8", func(t *testing.T) { assertUint64OK(t, []caseUint64[int8]{ {name: "zero", input: 0, want: 0}, {name: "positive within range", input: 100, want: 100}, }) assertUint64Error(t, []caseUint64[int]{ {name: "negative value", input: -1}, }) }) t.Run("from int16", func(t *testing.T) { assertUint64OK(t, []caseUint64[int16]{ {name: "zero", input: 0, want: 0}, {name: "positive within range", input: 10000, want: 10000}, }) assertUint64Error(t, []caseUint64[int]{ {name: "negative value", input: -1}, }) }) t.Run("from int32", func(t *testing.T) { assertUint64OK(t, []caseUint64[int32]{ {name: "zero", input: 0, want: 0}, {name: "positive within range", input: 10000, want: 10000}, }) assertUint64Error(t, []caseUint64[int]{ {name: "negative value", input: -1}, }) }) t.Run("from int64", func(t *testing.T) { assertUint64OK(t, []caseUint64[int64]{ {name: "zero", input: 0, want: 0}, {name: "positive within range", input: 10000, want: 10000}, }) assertUint64Error(t, []caseUint64[int]{ {name: "negative value", input: -1}, }) }) t.Run("from uint", func(t *testing.T) { assertUint64OK(t, []caseUint64[uint]{ {name: "zero", input: 0, want: 0}, {name: "positive within range", input: 100, want: 100}, }) }) t.Run("from uint8", func(t *testing.T) { assertUint64OK(t, []caseUint64[uint8]{ {name: "zero", input: 0, want: 0}, {name: "positive within range", input: 100, want: 100}, }) }) t.Run("from uint16", func(t *testing.T) { assertUint64OK(t, []caseUint64[uint16]{ {name: "zero", input: 0, want: 0}, {name: "positive within range", input: 100, want: 100}, }) }) t.Run("from uint32", func(t *testing.T) { assertUint64OK(t, []caseUint64[uint32]{ {name: "zero", input: 0, want: 0}, {name: "positive within range", input: 100, want: 100}, }) }) t.Run("from uint64", func(t *testing.T) { assertUint64OK(t, []caseUint64[uint64]{ {name: "zero", input: 0, want: 0}, {name: "positive within range", input: 100, want: 100}, }) }) t.Run("from float32", func(t *testing.T) { assertUint64OK(t, []caseUint64[float32]{ {name: "zero", input: 0.0, want: 0}, {name: "rounded value", input: 1.1, want: 1}, {name: "positive within range", input: 10000.9, want: 10000}, }) assertUint64Error(t, []caseUint64[float32]{ {name: "negative value", input: -1}, {name: "out of range max uint64", input: math.MaxUint64 * 1.02}, // because of float imprecision, we have to exceed the min int64 to trigger the error }) }) t.Run("from float64", func(t *testing.T) { assertUint64OK(t, []caseUint64[float64]{ {name: "zero", input: 0.0, want: 0}, {name: "rounded value", input: 1.1, want: 1}, {name: "positive within range", input: 10000.9, want: 10000}, }) assertUint64Error(t, []caseUint64[float64]{ {name: "negative value", input: -1}, {name: "out of range max uint64", input: math.MaxUint64 * 1.02}, // because of float imprecision, we have to exceed the min int64 to trigger the error {name: "out of range max float32", input: math.MaxFloat32}, {name: "out of range max float64", input: math.MaxFloat64}, }) }) } func TestToInt(t *testing.T) { t.Run("from int", func(t *testing.T) { assertIntOK(t, []caseInt[int]{ {name: "zero", input: 0, want: 0}, {name: "positive within range", input: 10000, want: 10000}, {name: "negative within range", input: -10000, want: -10000}, }) }) t.Run("from int8", func(t *testing.T) { assertIntOK(t, []caseInt[int8]{ {name: "zero", input: 0, want: 0}, {name: "positive within range", input: 100, want: 100}, {name: "negative within range", input: -100, want: -100}, }) }) t.Run("from int16", func(t *testing.T) { assertIntOK(t, []caseInt[int16]{ {name: "zero", input: 0, want: 0}, {name: "positive within range", input: 10000, want: 10000}, {name: "negative within range", input: -10000, want: -10000}, }) }) t.Run("from int32", func(t *testing.T) { assertIntOK(t, []caseInt[int32]{ {name: "zero", input: 0, want: 0}, {name: "positive within range", input: 10000, want: 10000}, {name: "negative within range", input: -10000, want: -10000}, }) }) t.Run("from int64", func(t *testing.T) { assertIntOK(t, []caseInt[int64]{ {name: "zero", input: 0, want: 0}, {name: "positive within range", input: 10000, want: 10000}, {name: "negative within range", input: -10000, want: -10000}, }) }) t.Run("from uint", func(t *testing.T) { assertIntOK(t, []caseInt[uint]{ {name: "zero", input: 0, want: 0}, {name: "positive within range", input: 100, want: 100}, }) // There are extra checks in [TestToInt_64bit] // the tests are separated because they cannot work on i386 }) t.Run("from uint8", func(t *testing.T) { assertIntOK(t, []caseInt[uint8]{ {name: "zero", input: 0, want: 0}, {name: "positive within range", input: 100, want: 100}, }) }) t.Run("from uint16", func(t *testing.T) { assertIntOK(t, []caseInt[uint16]{ {name: "zero", input: 0, want: 0}, {name: "positive within range", input: 100, want: 100}, }) }) t.Run("from uint32", func(t *testing.T) { assertIntOK(t, []caseInt[uint32]{ {name: "zero", input: 0, want: 0}, {name: "positive within range", input: 100, want: 100}, }) }) t.Run("from uint64", func(t *testing.T) { assertIntOK(t, []caseInt[uint64]{ {name: "zero", input: 0, want: 0}, {name: "positive within range", input: 100, want: 100}, }) assertIntError(t, []caseInt[uint64]{ {name: "positive out of range", input: math.MaxInt64 + 1}, }) }) t.Run("from float32", func(t *testing.T) { assertIntOK(t, []caseInt[float32]{ {name: "zero", input: 0.0, want: 0}, {name: "rounded value", input: 1.1, want: 1}, {name: "positive within range", input: 10000.9, want: 10000}, }) assertIntError(t, []caseInt[float32]{ {name: "out of range math.MaxInt64 + 1", input: math.MaxInt64 * 1.02}, // because of float imprecision, we have to exceed the min int64 to trigger the error {name: "out of range math.MaxUint64", input: math.MaxUint64}, {name: "out of range math.MinInt64", input: math.MinInt64 * 1.02}, // because of float imprecision, we have to exceed the min int64 to trigger the error {name: "out of range math.MaxFloat32", input: math.MaxFloat32}, {name: "out of range -math.MaxFloat32", input: -math.MaxFloat32}, }) }) t.Run("from float64", func(t *testing.T) { assertIntOK(t, []caseInt[float64]{ {name: "zero", input: 0.0, want: 0}, {name: "rounded value", input: 1.1, want: 1}, {name: "positive within range", input: 10000.9, want: 10000}, // There are extra checks in [TestToInt_64bit] // the tests are separated because they cannot work on i386 }) assertIntError(t, []caseInt[float64]{ {name: "out of range math.MaxInt64 + 1", input: math.MaxInt64 * 1.02}, // because of float imprecision, we have to exceed the min int64 to trigger the error {name: "out of range math.MaxUint64", input: math.MaxUint64}, {name: "out of range math.MinInt64", input: math.MinInt64 * 1.02}, // because of float imprecision, we have to exceed the min int64 to trigger the error {name: "out of range math.MaxFloat32", input: math.MaxFloat32}, {name: "out of range -math.MaxFloat32", input: -math.MaxFloat32}, {name: "out of range math.MaxFloat64", input: math.MaxFloat64}, {name: "out of range -math.MaxFloat64", input: -math.MaxFloat64}, }) }) } func TestToUint(t *testing.T) { t.Run("from int", func(t *testing.T) { assertUintOK(t, []caseUint[int]{ {name: "zero", input: 0, want: 0}, {name: "positive within range", input: 10000, want: 10000}, }) assertUintError(t, []caseUint[int]{ {name: "negative value", input: -1}, }) }) t.Run("from int8", func(t *testing.T) { assertUintOK(t, []caseUint[int8]{ {name: "zero", input: 0, want: 0}, {name: "positive within range", input: 100, want: 100}, }) assertUintError(t, []caseUint[int8]{ {name: "negative value", input: -1}, }) }) t.Run("from int16", func(t *testing.T) { assertUintOK(t, []caseUint[int16]{ {name: "zero", input: 0, want: 0}, {name: "positive within range", input: 10000, want: 10000}, }) assertUintError(t, []caseUint[int16]{ {name: "negative value", input: -1}, }) }) t.Run("from int32", func(t *testing.T) { assertUintOK(t, []caseUint[int32]{ {name: "zero", input: 0, want: 0}, {name: "positive within range", input: 10000, want: 10000}, }) assertUintError(t, []caseUint[int32]{ {name: "negative value", input: -1}, }) }) t.Run("from int64", func(t *testing.T) { assertUintOK(t, []caseUint[int64]{ {name: "zero", input: 0, want: 0}, {name: "positive within range", input: 10000, want: 10000}, }) assertUintError(t, []caseUint[int64]{ {name: "negative value", input: -1}, }) }) t.Run("from uint", func(t *testing.T) { assertUintOK(t, []caseUint[uint]{ {name: "zero", input: 0, want: 0}, {name: "positive within range", input: 100, want: 100}, }) }) t.Run("from uint8", func(t *testing.T) { assertUintOK(t, []caseUint[uint8]{ {name: "zero", input: 0, want: 0}, {name: "positive within range", input: 100, want: 100}, }) }) t.Run("from uint16", func(t *testing.T) { assertUintOK(t, []caseUint[uint16]{ {name: "zero", input: 0, want: 0}, {name: "positive within range", input: 100, want: 100}, }) }) t.Run("from uint32", func(t *testing.T) { assertUintOK(t, []caseUint[uint32]{ {name: "zero", input: 0, want: 0}, {name: "positive within range", input: 100, want: 100}, }) }) t.Run("from uint64", func(t *testing.T) { assertUintOK(t, []caseUint[uint64]{ {name: "zero", input: 0, want: 0}, {name: "positive within range", input: 100, want: 100}, }) }) t.Run("from float32", func(t *testing.T) { assertUintOK(t, []caseUint[float32]{ {name: "zero", input: 0.0, want: 0}, {name: "rounded value", input: 1.1, want: 1}, {name: "positive within range", input: 10000.9, want: 10000}, }) assertUint64Error(t, []caseUint64[float32]{ {name: "negative value", input: -1}, {name: "out of range max uint64", input: math.MaxUint64 * 1.02}, // because of float imprecision, we have to exceed the min int64 to trigger the error {name: "out of range max float32", input: math.MaxFloat32}, }) }) t.Run("from float64", func(t *testing.T) { assertUintOK(t, []caseUint[float64]{ {name: "zero", input: 0.0, want: 0}, {name: "rounded value", input: 1.1, want: 1}, {name: "positive within range", input: 10000.9, want: 10000}, }) assertUintError(t, []caseUint[float64]{ {name: "negative value", input: -1}, {name: "out of range max uint64", input: math.MaxUint64 * 1.02}, // because of float imprecision, we have to exceed the min int64 to trigger the error {name: "out of range max float32", input: math.MaxFloat32}, {name: "out of range max float64", input: math.MaxFloat64}, }) }) type UintAlias uint t.Run("from type alias", func(t *testing.T) { assertUintOK(t, []caseUint[UintAlias]{ {name: "zero", input: 0, want: 0}, {name: "positive within range", input: 100, want: 100}, }) }) } func TestToFloat32(t *testing.T) { t.Run("from int", func(t *testing.T) { assertFloat32OK(t, []caseFloat32[int]{ {name: "zero", input: 0, want: 0.0}, {name: "negative within range", input: -100, want: -100.0}, {name: "positive within range", input: 100, want: 100.0}, }) }) t.Run("from int8", func(t *testing.T) { assertFloat32OK(t, []caseFloat32[int8]{ {name: "zero", input: 0, want: 0.0}, {name: "negative within range", input: -100, want: -100.0}, {name: "positive within range", input: 100, want: 100.0}, }) }) t.Run("from int16", func(t *testing.T) { assertFloat32OK(t, []caseFloat32[int16]{ {name: "zero", input: 0, want: 0.0}, {name: "negative within range", input: -100, want: -100.0}, {name: "positive within range", input: 100, want: 100.0}, }) }) t.Run("from int32", func(t *testing.T) { assertFloat32OK(t, []caseFloat32[int32]{ {name: "zero", input: 0, want: 0.0}, {name: "negative within range", input: -100, want: -100.0}, {name: "positive within range", input: 100, want: 100.0}, }) }) t.Run("from int64", func(t *testing.T) { assertFloat32OK(t, []caseFloat32[int64]{ {name: "zero", input: 0, want: 0.0}, {name: "negative within range", input: -100, want: -100.0}, {name: "positive within range", input: 100, want: 100.0}, }) }) t.Run("from uint", func(t *testing.T) { assertFloat32OK(t, []caseFloat32[uint]{ {name: "zero", input: 0, want: 0.0}, {name: "positive within range", input: 100, want: 100.0}, }) }) t.Run("from uint8", func(t *testing.T) { assertFloat32OK(t, []caseFloat32[uint8]{ {name: "zero", input: 0, want: 0.0}, {name: "positive within range", input: 100, want: 100.0}, }) }) t.Run("from uint16", func(t *testing.T) { assertFloat32OK(t, []caseFloat32[uint16]{ {name: "zero", input: 0, want: 0.0}, {name: "positive within range", input: 100, want: 100.0}, }) }) t.Run("from uint32", func(t *testing.T) { assertFloat32OK(t, []caseFloat32[uint32]{ {name: "zero", input: 0, want: 0.0}, {name: "positive within range", input: 100, want: 100.0}, }) }) t.Run("from uint64", func(t *testing.T) { assertFloat32OK(t, []caseFloat32[uint64]{ {name: "zero", input: 0, want: 0.0}, {name: "positive within range", input: 100, want: 100.0}, }) }) t.Run("from float32", func(t *testing.T) { assertFloat32OK(t, []caseFloat32[float32]{ {name: "zero", input: 0.0, want: 0.0}, {name: "rounded value", input: 1.1, want: 1.1}, {name: "negative within range", input: -100.9, want: -100.9}, {name: "positive within range", input: 100.9, want: 100.9}, }) }) t.Run("from float64", func(t *testing.T) { assertFloat32OK(t, []caseFloat32[float64]{ {name: "zero", input: 0.0, want: 0.0}, {name: "negative zero", input: math.Copysign(0, -1), want: -0}, {name: "almost zero", input: math.SmallestNonzeroFloat32, want: 1e-45}, {name: "almost negative zero", input: -math.SmallestNonzeroFloat32, want: -1e-45}, {name: "negative within range", input: -100.9, want: -100.9}, {name: "positive within range", input: 100.9, want: 100.9}, {name: "with imprecision due to conversion", input: 2.67428e+28, want: 2.67428e+28}, }) assertFloat32Error(t, []caseFloat32[float64]{ {name: "out of range max float32", input: math.MaxFloat32 * 1.02}, // because of float imprecision, we have to exceed the min int64 to trigger the error {name: "out of range min float32", input: -math.MaxFloat32 * 1.02}, // because of float imprecision, we have to exceed the min int64 to trigger the error }) }) } func TestToFloat64(t *testing.T) { t.Run("from int", func(t *testing.T) { assertFloat64OK(t, []caseFloat64[int]{ {name: "zero", input: 0, want: 0.0}, {name: "negative within range", input: -100, want: -100.0}, {name: "positive within range", input: 100, want: 100.0}, }) }) t.Run("from int8", func(t *testing.T) { assertFloat64OK(t, []caseFloat64[int8]{ {name: "zero", input: 0, want: 0.0}, {name: "negative within range", input: -100, want: -100.0}, {name: "positive within range", input: 100, want: 100.0}, }) }) t.Run("from int16", func(t *testing.T) { assertFloat64OK(t, []caseFloat64[int16]{ {name: "zero", input: 0, want: 0.0}, {name: "negative within range", input: -100, want: -100.0}, {name: "positive within range", input: 100, want: 100.0}, }) }) t.Run("from int32", func(t *testing.T) { assertFloat64OK(t, []caseFloat64[int32]{ {name: "zero", input: 0, want: 0.0}, {name: "negative within range", input: -100, want: -100.0}, {name: "positive within range", input: 100, want: 100.0}, }) }) t.Run("from int64", func(t *testing.T) { assertFloat64OK(t, []caseFloat64[int64]{ {name: "zero", input: 0, want: 0.0}, {name: "negative within range", input: -100, want: -100.0}, {name: "positive within range", input: 100, want: 100.0}, }) }) t.Run("from uint", func(t *testing.T) { assertFloat64OK(t, []caseFloat64[uint]{ {name: "zero", input: 0, want: 0.0}, {name: "positive within range", input: 100, want: 100.0}, }) }) t.Run("from uint8", func(t *testing.T) { assertFloat64OK(t, []caseFloat64[uint8]{ {name: "zero", input: 0, want: 0.0}, {name: "positive within range", input: 100, want: 100.0}, }) }) t.Run("from uint16", func(t *testing.T) { assertFloat64OK(t, []caseFloat64[uint16]{ {name: "zero", input: 0, want: 0.0}, {name: "positive within range", input: 100, want: 100.0}, }) }) t.Run("from uint32", func(t *testing.T) { assertFloat64OK(t, []caseFloat64[uint32]{ {name: "zero", input: 0, want: 0.0}, {name: "positive within range", input: 100, want: 100.0}, }) }) t.Run("from uint64", func(t *testing.T) { assertFloat64OK(t, []caseFloat64[uint64]{ {name: "zero", input: 0, want: 0.0}, {name: "positive within range", input: 100, want: 100.0}, }) }) t.Run("from float32", func(t *testing.T) { assertFloat64OK(t, []caseFloat64[float64]{ {name: "zero", input: 0.0, want: 0.0}, {name: "rounded value", input: 1.1, want: 1.1}, {name: "negative within range", input: -100.9, want: -100.9}, {name: "positive within range", input: 100.9, want: 100.9}, }) }) t.Run("from float64", func(t *testing.T) { assertFloat64OK(t, []caseFloat64[float64]{ {name: "zero", input: 0.0, want: 0.0}, {name: "negative within range", input: -100.9, want: -100.9}, {name: "positive within range", input: 100.0, want: 100.0}, }) }) } func Map[T any, U any](fn func(v T) U, input []T) []U { var output []U for _, v := range input { output = append(output, fn(v)) } return output } type MapTest[TypeInput safecast.Input, TypeOutput safecast.Number] struct { Input TypeInput ExpectedOutput TypeOutput ExpectedError error ErrorContains string } func (mt MapTest[I, O]) TestConvert(t *testing.T) { // configure a helper to validate there is no panic defer func(t *testing.T) { t.Helper() err := recover() if err != nil { t.Fatalf("panic with %v", err) } }(t) out, err := safecast.Convert[O](mt.Input) if mt.ExpectedError != nil { requireErrorIs(t, err, safecast.ErrConversionIssue) requireErrorIs(t, err, mt.ExpectedError) if mt.ErrorContains != "" { requireErrorContains(t, err, mt.ErrorContains) } return } assertNoError(t, err) assertEqual(t, mt.ExpectedOutput, out) } type TestableConvert interface { TestConvert(t *testing.T) } func TestConvert(t *testing.T) { t.Run("untyped integer", func(t *testing.T) { out, err := safecast.Convert[uint](42) assertNoError(t, err) assertEqual(t, uint(42), out) }) for name, c := range map[string]TestableConvert{ "int to float32": MapTest[int, float32]{Input: 42, ExpectedOutput: 42}, "int to float64": MapTest[int, float64]{Input: 42, ExpectedOutput: 42}, "int to int": MapTest[int, int]{Input: 42, ExpectedOutput: 42}, "int to int16": MapTest[int, int16]{Input: 42, ExpectedOutput: 42}, "int to int32": MapTest[int, int32]{Input: 42, ExpectedOutput: 42}, "int to int64": MapTest[int, int64]{Input: 42, ExpectedOutput: 42}, "int to int8": MapTest[int, int8]{Input: 42, ExpectedOutput: 42}, "int to uint": MapTest[int, uint]{Input: 42, ExpectedOutput: 42}, "int to uint16": MapTest[int, uint16]{Input: 42, ExpectedOutput: 42}, "int to uint32": MapTest[int, uint32]{Input: 42, ExpectedOutput: 42}, "int to uint64": MapTest[int, uint64]{Input: 42, ExpectedOutput: 42}, "int to uint8": MapTest[int, uint8]{Input: 42, ExpectedOutput: 42}, "int8 to float32": MapTest[int8, float32]{Input: 42, ExpectedOutput: 42}, "int8 to float64": MapTest[int8, float64]{Input: 42, ExpectedOutput: 42}, "int8 to int": MapTest[int8, int]{Input: 42, ExpectedOutput: 42}, "int8 to int16": MapTest[int8, int16]{Input: 42, ExpectedOutput: 42}, "int8 to int32": MapTest[int8, int32]{Input: 42, ExpectedOutput: 42}, "int8 to int64": MapTest[int8, int64]{Input: 42, ExpectedOutput: 42}, "int8 to int8": MapTest[int8, int8]{Input: 42, ExpectedOutput: 42}, "int8 to uint": MapTest[int8, uint]{Input: 42, ExpectedOutput: 42}, "int8 to uint16": MapTest[int8, uint16]{Input: 42, ExpectedOutput: 42}, "int8 to uint32": MapTest[int8, uint32]{Input: 42, ExpectedOutput: 42}, "int8 to uint64": MapTest[int8, uint64]{Input: 42, ExpectedOutput: 42}, "int8 to uint8": MapTest[int8, uint8]{Input: 42, ExpectedOutput: 42}, "int16 to float32": MapTest[int16, float32]{Input: 42, ExpectedOutput: 42}, "int16 to float64": MapTest[int16, float64]{Input: 42, ExpectedOutput: 42}, "int16 to int": MapTest[int16, int]{Input: 42, ExpectedOutput: 42}, "int16 to int16": MapTest[int16, int16]{Input: 42, ExpectedOutput: 42}, "int16 to int32": MapTest[int16, int32]{Input: 42, ExpectedOutput: 42}, "int16 to int64": MapTest[int16, int64]{Input: 42, ExpectedOutput: 42}, "int16 to int8": MapTest[int16, int8]{Input: 42, ExpectedOutput: 42}, "int16 to uint": MapTest[int16, uint]{Input: 42, ExpectedOutput: 42}, "int16 to uint16": MapTest[int16, uint16]{Input: 42, ExpectedOutput: 42}, "int16 to uint32": MapTest[int16, uint32]{Input: 42, ExpectedOutput: 42}, "int16 to uint64": MapTest[int16, uint64]{Input: 42, ExpectedOutput: 42}, "int16 to uint8": MapTest[int16, uint8]{Input: 42, ExpectedOutput: 42}, "int32 to float32": MapTest[int32, float32]{Input: 42, ExpectedOutput: 42}, "int32 to float64": MapTest[int32, float64]{Input: 42, ExpectedOutput: 42}, "int32 to int": MapTest[int32, int]{Input: 42, ExpectedOutput: 42}, "int32 to int16": MapTest[int32, int16]{Input: 42, ExpectedOutput: 42}, "int32 to int32": MapTest[int32, int32]{Input: 42, ExpectedOutput: 42}, "int32 to int64": MapTest[int32, int64]{Input: 42, ExpectedOutput: 42}, "int32 to int8": MapTest[int32, int8]{Input: 42, ExpectedOutput: 42}, "int32 to uint": MapTest[int32, uint]{Input: 42, ExpectedOutput: 42}, "int32 to uint16": MapTest[int32, uint16]{Input: 42, ExpectedOutput: 42}, "int32 to uint32": MapTest[int32, uint32]{Input: 42, ExpectedOutput: 42}, "int32 to uint64": MapTest[int32, uint64]{Input: 42, ExpectedOutput: 42}, "int32 to uint8": MapTest[int32, uint8]{Input: 42, ExpectedOutput: 42}, "int64 to float32": MapTest[int64, float32]{Input: 42, ExpectedOutput: 42}, "int64 to float64": MapTest[int64, float64]{Input: 42, ExpectedOutput: 42}, "int64 to int": MapTest[int64, int]{Input: 42, ExpectedOutput: 42}, "int64 to int16": MapTest[int64, int16]{Input: 42, ExpectedOutput: 42}, "int64 to int32": MapTest[int64, int32]{Input: 42, ExpectedOutput: 42}, "int64 to int64": MapTest[int64, int64]{Input: 42, ExpectedOutput: 42}, "int64 to int8": MapTest[int64, int8]{Input: 42, ExpectedOutput: 42}, "int64 to uint": MapTest[int64, uint]{Input: 42, ExpectedOutput: 42}, "int64 to uint16": MapTest[int64, uint16]{Input: 42, ExpectedOutput: 42}, "int64 to uint32": MapTest[int64, uint32]{Input: 42, ExpectedOutput: 42}, "int64 to uint64": MapTest[int64, uint64]{Input: 42, ExpectedOutput: 42}, "int64 to uint8": MapTest[int64, uint8]{Input: 42, ExpectedOutput: 42}, "uint to float32": MapTest[uint, float32]{Input: 42, ExpectedOutput: 42}, "uint to float64": MapTest[uint, float64]{Input: 42, ExpectedOutput: 42}, "uint to int": MapTest[uint, int]{Input: 42, ExpectedOutput: 42}, "uint to int16": MapTest[uint, int16]{Input: 42, ExpectedOutput: 42}, "uint to int32": MapTest[uint, int32]{Input: 42, ExpectedOutput: 42}, "uint to int64": MapTest[uint, int64]{Input: 42, ExpectedOutput: 42}, "uint to int8": MapTest[uint, int8]{Input: 42, ExpectedOutput: 42}, "uint to uint": MapTest[uint, uint]{Input: 42, ExpectedOutput: 42}, "uint to uint16": MapTest[uint, uint16]{Input: 42, ExpectedOutput: 42}, "uint to uint32": MapTest[uint, uint32]{Input: 42, ExpectedOutput: 42}, "uint to uint64": MapTest[uint, uint64]{Input: 42, ExpectedOutput: 42}, "uint to uint8": MapTest[uint, uint8]{Input: 42, ExpectedOutput: 42}, "uint8 to float32": MapTest[uint8, float32]{Input: 42, ExpectedOutput: 42}, "uint8 to float64": MapTest[uint8, float64]{Input: 42, ExpectedOutput: 42}, "uint8 to int": MapTest[uint8, int]{Input: 42, ExpectedOutput: 42}, "uint8 to int16": MapTest[uint8, int16]{Input: 42, ExpectedOutput: 42}, "uint8 to int32": MapTest[uint8, int32]{Input: 42, ExpectedOutput: 42}, "uint8 to int64": MapTest[uint8, int64]{Input: 42, ExpectedOutput: 42}, "uint8 to int8": MapTest[uint8, int8]{Input: 42, ExpectedOutput: 42}, "uint8 to uint": MapTest[uint8, uint]{Input: 42, ExpectedOutput: 42}, "uint8 to uint16": MapTest[uint8, uint16]{Input: 42, ExpectedOutput: 42}, "uint8 to uint32": MapTest[uint8, uint32]{Input: 42, ExpectedOutput: 42}, "uint8 to uint64": MapTest[uint8, uint64]{Input: 42, ExpectedOutput: 42}, "uint8 to uint8": MapTest[uint8, uint8]{Input: 42, ExpectedOutput: 42}, "uint16 to float32": MapTest[uint16, float32]{Input: 42, ExpectedOutput: 42}, "uint16 to float64": MapTest[uint16, float64]{Input: 42, ExpectedOutput: 42}, "uint16 to int": MapTest[uint16, int]{Input: 42, ExpectedOutput: 42}, "uint16 to int16": MapTest[uint16, int16]{Input: 42, ExpectedOutput: 42}, "uint16 to int32": MapTest[uint16, int32]{Input: 42, ExpectedOutput: 42}, "uint16 to int64": MapTest[uint16, int64]{Input: 42, ExpectedOutput: 42}, "uint16 to int8": MapTest[uint16, int8]{Input: 42, ExpectedOutput: 42}, "uint16 to uint": MapTest[uint16, uint]{Input: 42, ExpectedOutput: 42}, "uint16 to uint16": MapTest[uint16, uint16]{Input: 42, ExpectedOutput: 42}, "uint16 to uint32": MapTest[uint16, uint32]{Input: 42, ExpectedOutput: 42}, "uint16 to uint64": MapTest[uint16, uint64]{Input: 42, ExpectedOutput: 42}, "uint16 to uint8": MapTest[uint16, uint8]{Input: 42, ExpectedOutput: 42}, "uint32 to float32": MapTest[uint32, float32]{Input: 42, ExpectedOutput: 42}, "uint32 to float64": MapTest[uint32, float64]{Input: 42, ExpectedOutput: 42}, "uint32 to int": MapTest[uint32, int]{Input: 42, ExpectedOutput: 42}, "uint32 to int16": MapTest[uint32, int16]{Input: 42, ExpectedOutput: 42}, "uint32 to int32": MapTest[uint32, int32]{Input: 42, ExpectedOutput: 42}, "uint32 to int64": MapTest[uint32, int64]{Input: 42, ExpectedOutput: 42}, "uint32 to int8": MapTest[uint32, int8]{Input: 42, ExpectedOutput: 42}, "uint32 to uint": MapTest[uint32, uint]{Input: 42, ExpectedOutput: 42}, "uint32 to uint16": MapTest[uint32, uint16]{Input: 42, ExpectedOutput: 42}, "uint32 to uint32": MapTest[uint32, uint32]{Input: 42, ExpectedOutput: 42}, "uint32 to uint64": MapTest[uint32, uint64]{Input: 42, ExpectedOutput: 42}, "uint32 to uint8": MapTest[uint32, uint8]{Input: 42, ExpectedOutput: 42}, "uint64 to float32": MapTest[uint64, float32]{Input: 42, ExpectedOutput: 42}, "uint64 to float64": MapTest[uint64, float64]{Input: 42, ExpectedOutput: 42}, "uint64 to int": MapTest[uint64, int]{Input: 42, ExpectedOutput: 42}, "uint64 to int16": MapTest[uint64, int16]{Input: 42, ExpectedOutput: 42}, "uint64 to int32": MapTest[uint64, int32]{Input: 42, ExpectedOutput: 42}, "uint64 to int64": MapTest[uint64, int64]{Input: 42, ExpectedOutput: 42}, "uint64 to int8": MapTest[uint64, int8]{Input: 42, ExpectedOutput: 42}, "uint64 to uint": MapTest[uint64, uint]{Input: 42, ExpectedOutput: 42}, "uint64 to uint16": MapTest[uint64, uint16]{Input: 42, ExpectedOutput: 42}, "uint64 to uint32": MapTest[uint64, uint32]{Input: 42, ExpectedOutput: 42}, "uint64 to uint64": MapTest[uint64, uint64]{Input: 42, ExpectedOutput: 42}, "uint64 to uint8": MapTest[uint64, uint8]{Input: 42, ExpectedOutput: 42}, "float32 to int": MapTest[float32, int]{Input: 42, ExpectedOutput: 42}, "float32 to int8": MapTest[float32, int8]{Input: 42, ExpectedOutput: 42}, "float32 to int16": MapTest[float32, int16]{Input: 42, ExpectedOutput: 42}, "float32 to int32": MapTest[float32, int32]{Input: 42, ExpectedOutput: 42}, "float32 to int64": MapTest[float32, int64]{Input: 42, ExpectedOutput: 42}, "float32 to uint": MapTest[float32, uint]{Input: 42, ExpectedOutput: 42}, "float32 to uint8": MapTest[float32, uint8]{Input: 42, ExpectedOutput: 42}, "float32 to uint16": MapTest[float32, uint16]{Input: 42, ExpectedOutput: 42}, "float32 to uint32": MapTest[float32, uint32]{Input: 42, ExpectedOutput: 42}, "float32 to uint64": MapTest[float32, uint64]{Input: 42, ExpectedOutput: 42}, "float32 to float32": MapTest[float32, float32]{Input: 42, ExpectedOutput: 42}, "float32 to float64": MapTest[float32, float64]{Input: 42, ExpectedOutput: 42}, "float64 to int": MapTest[float64, int]{Input: 42, ExpectedOutput: 42}, "float64 to int8": MapTest[float64, int8]{Input: 42, ExpectedOutput: 42}, "float64 to int16": MapTest[float64, int16]{Input: 42, ExpectedOutput: 42}, "float64 to int32": MapTest[float64, int32]{Input: 42, ExpectedOutput: 42}, "float64 to int64": MapTest[float64, int64]{Input: 42, ExpectedOutput: 42}, "float64 to uint": MapTest[float64, uint]{Input: 42, ExpectedOutput: 42}, "float64 to uint8": MapTest[float64, uint8]{Input: 42, ExpectedOutput: 42}, "float64 to uint16": MapTest[float64, uint16]{Input: 42, ExpectedOutput: 42}, "float64 to uint32": MapTest[float64, uint32]{Input: 42, ExpectedOutput: 42}, "float64 to uint64": MapTest[float64, uint64]{Input: 42, ExpectedOutput: 42}, "float64 to float32": MapTest[float64, float32]{Input: 42, ExpectedOutput: 42}, "float64 to float64": MapTest[float64, float64]{Input: 42, ExpectedOutput: 42}, } { t.Run(name, func(t *testing.T) { c.TestConvert(t) }) } for name, c := range map[string]TestableConvert{ "string integer": MapTest[string, uint]{Input: "42", ExpectedOutput: 42}, "string with spaces": MapTest[string, uint]{Input: "42 ", ExpectedOutput: 42}, "string float": MapTest[string, uint]{Input: "42.0", ExpectedOutput: 42}, "string true": MapTest[string, uint]{Input: "true", ExpectedOutput: 1}, "string false": MapTest[string, uint]{Input: "false", ExpectedOutput: 0}, "string 10_0": MapTest[string, uint]{Input: "10_0", ExpectedOutput: 100}, "string binary": MapTest[string, uint]{Input: "0b101010", ExpectedOutput: 42}, "string short octal notation": MapTest[string, uint]{Input: "042", ExpectedOutput: 34}, "string octal": MapTest[string, uint]{Input: "0o42", ExpectedOutput: 34}, "string hexadecimal": MapTest[string, uint]{Input: "0x42", ExpectedOutput: 66}, "boolean true": MapTest[bool, uint]{Input: true, ExpectedOutput: 1}, "boolean false": MapTest[bool, uint]{Input: false, ExpectedOutput: 0}, "empty string": MapTest[string, uint]{Input: "", ExpectedError: safecast.ErrStringConversion}, "simple space": MapTest[string, uint]{Input: " ", ExpectedError: safecast.ErrStringConversion}, "simple dot": MapTest[string, uint]{Input: ".", ExpectedError: safecast.ErrStringConversion}, "simple dash": MapTest[string, uint]{Input: "-", ExpectedError: safecast.ErrStringConversion}, "invalid string": MapTest[string, uint]{Input: "abc", ExpectedError: safecast.ErrStringConversion}, "invalid string with dot": MapTest[string, uint]{Input: "ab.c", ExpectedError: safecast.ErrStringConversion}, "strings with leading +": MapTest[string, uint]{Input: "+42", ExpectedError: safecast.ErrStringConversion}, "invalid string multiple leading dashes": MapTest[string, uint]{Input: "--42", ExpectedError: safecast.ErrStringConversion}, "invalid string with dash": MapTest[string, uint]{Input: "-abc", ExpectedError: safecast.ErrStringConversion}, "invalid string with dash and dot": MapTest[string, uint]{Input: "-ab.c", ExpectedError: safecast.ErrStringConversion}, } { t.Run(name, func(t *testing.T) { c.TestConvert(t) }) } negativeZero := math.Copysign(0, -1) t.Run("convert to float32 near zero", func(t *testing.T) { for name, tt := range map[string]TestableConvert{ "negative untyped zero": MapTest[float64, float32]{Input: negativeZero, ExpectedOutput: float32(negativeZero)}, "smallest positive non-zero float32": MapTest[float64, float32]{Input: math.SmallestNonzeroFloat32, ExpectedOutput: 1e-45}, "smallest negative non-zero float32": MapTest[float64, float32]{Input: -math.SmallestNonzeroFloat32, ExpectedOutput: -1e-45}, "smallest positive non-zero float64": MapTest[float64, float32]{Input: math.SmallestNonzeroFloat64, ExpectedOutput: 4.9e-324}, "smallest negative non-zero float64": MapTest[float64, float32]{Input: -math.SmallestNonzeroFloat64, ExpectedOutput: -4.9e-324}, } { t.Run(name, func(t *testing.T) { tt.TestConvert(t) }) } }) t.Run("convert to float64 near zero", func(t *testing.T) { for name, tt := range map[string]TestableConvert{ "negative untyped zero": MapTest[float64, float64]{Input: negativeZero, ExpectedOutput: negativeZero}, "smallest positive non-zero float32": MapTest[float64, float64]{Input: math.SmallestNonzeroFloat32, ExpectedOutput: 1.401298464324817e-45}, "smallest negative non-zero float32": MapTest[float64, float64]{Input: -math.SmallestNonzeroFloat32, ExpectedOutput: -1.401298464324817e-45}, "smallest positive non-zero float64": MapTest[float64, float64]{Input: math.SmallestNonzeroFloat64, ExpectedOutput: 4.9e-324}, "smallest negative non-zero float64": MapTest[float64, float64]{Input: -math.SmallestNonzeroFloat64, ExpectedOutput: -4.9e-324}, } { t.Run(name, func(t *testing.T) { tt.TestConvert(t) }) } }) for name, c := range map[string]TestableConvert{ "upper bound overflows for int": MapTest[uint, int]{ Input: uint(math.MaxInt + 1), ExpectedError: safecast.ErrExceedMaximumValue, }, "upper bound overflows for int8": MapTest[uint, int8]{ Input: uint(math.MaxInt8 + 1), ExpectedError: safecast.ErrExceedMaximumValue, }, "upper bound overflows for int16": MapTest[uint, int16]{ Input: uint(math.MaxInt16 + 1), ExpectedError: safecast.ErrExceedMaximumValue, }, "upper bound overflows for int32": MapTest[uint, int32]{ Input: uint(math.MaxInt32 + 1), ExpectedError: safecast.ErrExceedMaximumValue, }, "upper bound overflows for int64": MapTest[float64, int64]{ Input: float64(math.MaxInt64 * 1.01), // using float64 here avoid issue when testing on 32-bit systems ExpectedError: safecast.ErrExceedMaximumValue, }, "upper bound overflows for uint": MapTest[float64, uint]{ Input: float64(math.MaxUint * 1.01), // using float64 here avoid issue when testing on 32-bit systems ExpectedError: safecast.ErrExceedMaximumValue, }, "upper bound overflows for uint8": MapTest[uint, uint8]{ Input: uint(math.MaxUint8 + 1), ExpectedError: safecast.ErrExceedMaximumValue, }, "upper bound overflows for uint16": MapTest[uint, uint16]{ Input: uint(math.MaxUint16 + 1), ExpectedError: safecast.ErrExceedMaximumValue, }, "upper bound overflows for uint32": MapTest[float64, uint32]{ Input: float64(math.MaxUint32 * 1.01), ExpectedError: safecast.ErrExceedMaximumValue, }, "upper bound overflows for uint64": MapTest[float64, uint64]{ Input: float64(math.MaxUint64 * 1.01), ExpectedError: safecast.ErrExceedMaximumValue, }, "upper bound overflows for int string": MapTest[string, int]{ Input: "9223372036854775808", // math.MaxInt64 + 1 ExpectedError: safecast.ErrExceedMaximumValue, }, "upper bound overflows for int8 string": MapTest[string, int8]{ Input: "129", // math.MaxInt8 + 1 ExpectedError: safecast.ErrExceedMaximumValue, }, "upper bound overflows for int16 string": MapTest[string, int16]{ Input: "32769", // math.MaxInt16 + 1 ExpectedError: safecast.ErrExceedMaximumValue, }, "upper bound overflows for int32 string": MapTest[string, int32]{ Input: "2147483648", // math.MaxInt32 + 1 ExpectedError: safecast.ErrExceedMaximumValue, }, "upper bound overflows for int64 string": MapTest[string, int64]{ Input: "9223372036854775808", // math.MaxInt64 + 1 ExpectedError: safecast.ErrExceedMaximumValue, }, "upper bound overflows for int64 string overflow": MapTest[string, int64]{ Input: "123456789012345678901234567890", // more characters than math.MaxInt64 represented as string ExpectedError: safecast.ErrExceedMaximumValue, }, } { t.Run(name, func(t *testing.T) { c.TestConvert(t) }) } for name, c := range map[string]TestableConvert{ "lower bound overflows for int": MapTest[float64, int]{ Input: float64(math.MinInt * 1.01), // the float64 conversion is used to avoid overflow on 32-bit ExpectedError: safecast.ErrExceedMinimumValue, }, "lower bound overflows for int8": MapTest[int, int8]{ Input: math.MinInt8 - 1, ExpectedError: safecast.ErrExceedMinimumValue, }, "lower bound overflows for int16": MapTest[int, int16]{ Input: math.MinInt16 - 1, ExpectedError: safecast.ErrExceedMinimumValue, }, "lower bound overflows for int32": MapTest[float64, int32]{ Input: float64(math.MinInt32 - 1), // the float64 conversion is used to avoid overflow on 32-bit, ExpectedError: safecast.ErrExceedMinimumValue, }, "lower bound overflows for int64": MapTest[float64, int64]{ Input: float64(math.MinInt64 * 1.01), // the float64 conversion is used to avoid overflow on 32-bit ExpectedError: safecast.ErrExceedMinimumValue, }, "lower bound overflows for float32": MapTest[float64, float32]{ Input: -float64(math.MaxFloat32 * 1.01), ExpectedError: safecast.ErrExceedMinimumValue, }, // Note: float64 cannot overflow "negative overflows uint": MapTest[int, uint]{ Input: -42, ExpectedError: safecast.ErrExceedMinimumValue, }, "negative overflows uint8": MapTest[int, uint8]{ Input: -42, ExpectedError: safecast.ErrExceedMinimumValue, }, "negative overflows uint16": MapTest[int, uint16]{ Input: -42, ExpectedError: safecast.ErrExceedMinimumValue, }, "negative overflows uint32": MapTest[int, uint32]{ Input: -42, ExpectedError: safecast.ErrExceedMinimumValue, }, "negative overflows uint64": MapTest[int, uint64]{ Input: -42, ExpectedError: safecast.ErrExceedMinimumValue, }, "lower bound overflows int from string": MapTest[string, int]{ Input: "-9223372036854775809", // math.MinInt64 - 1 ExpectedError: safecast.ErrExceedMinimumValue, }, "lower bound overflows int8 from string": MapTest[string, int8]{ Input: "-129", // math.MinInt8 - 1 ExpectedError: safecast.ErrExceedMinimumValue, }, "lower bound overflows int16 from string": MapTest[string, int16]{ Input: "-32769", // math.MinInt16 - 1 ExpectedError: safecast.ErrExceedMinimumValue, }, "lower bound overflows int32 from string": MapTest[string, int32]{ Input: "-2147483649", // math.MinInt32 - 1 ExpectedError: safecast.ErrExceedMinimumValue, }, "lower bound overflows int64 from string": MapTest[string, int64]{ Input: "-9223372036854775809", // math.MinInt64 - 1 ExpectedError: safecast.ErrExceedMinimumValue, }, "lower bound overflows int64 from string overflow": MapTest[string, int64]{ Input: "-123456789012345678901234567890", // more characters than math.MinInt64 represented as string ExpectedError: safecast.ErrExceedMinimumValue, }, "negative string overflows uint": MapTest[string, uint]{ Input: "-1", ExpectedError: safecast.ErrExceedMinimumValue, }, "negative string overflows uint8": MapTest[string, uint8]{ Input: "-1", ExpectedError: safecast.ErrExceedMinimumValue, }, "negative string overflows uint16": MapTest[string, uint16]{ Input: "-1", ExpectedError: safecast.ErrExceedMinimumValue, }, "negative string overflows uint32": MapTest[string, uint32]{ Input: "-1", ExpectedError: safecast.ErrExceedMinimumValue, }, "negative string overflows uint64": MapTest[string, uint64]{ Input: "-1", ExpectedError: safecast.ErrExceedMinimumValue, }, } { t.Run(name, func(t *testing.T) { c.TestConvert(t) }) } t.Run("aliases", func(t *testing.T) { // Type aliases are handled separately type ( // UintSimpleAlias is an alias UintSimpleAlias = uint // UintTypeAlias is a type alias UintTypeAlias uint // StringAlias is an alias StringAlias = string // StringTypeAlias is a type alias StringTypeAlias string // BoolAlias is an alias BoolAlias = bool // BoolTypeAlias is a type alias BoolTypeAlias bool ) for name, c := range map[string]TestableConvert{ "integer simple alias": MapTest[UintSimpleAlias, int8]{ Input: UintSimpleAlias(42), ExpectedOutput: int8(42), }, "integer type alias": MapTest[UintTypeAlias, int8]{ Input: UintTypeAlias(42), ExpectedOutput: int8(42), }, "string simple alias": MapTest[StringAlias, int8]{ Input: StringAlias("42"), ExpectedOutput: int8(42), }, "string type alias": MapTest[StringTypeAlias, int8]{ Input: StringTypeAlias("42"), ExpectedOutput: int8(42), }, "bool simple alias": MapTest[BoolAlias, int8]{ Input: BoolAlias(true), ExpectedOutput: int8(1), }, "bool type alias": MapTest[BoolTypeAlias, int8]{ Input: BoolTypeAlias(true), ExpectedOutput: int8(1), }, "simple alias overflows for int8": MapTest[UintSimpleAlias, int8]{ Input: UintSimpleAlias(255), ExpectedError: safecast.ErrExceedMaximumValue, ErrorContains: "255 (uint) is greater than 127 (int8)", }, "type alias overflows for int8": MapTest[UintTypeAlias, int8]{ Input: UintTypeAlias(255), ExpectedError: safecast.ErrExceedMaximumValue, ErrorContains: "255 (uint) is greater than 127 (int8)", }, } { t.Run(name, func(t *testing.T) { c.TestConvert(t) }) } }) } type MapMustConvertTest[TypeInput safecast.Input, TypeOutput safecast.Number] struct { Input TypeInput ExpectedOutput TypeOutput ExpectedError error } func (mt MapMustConvertTest[I, O]) TestConvert(t *testing.T) { defer func(t *testing.T) { t.Helper() r := recover() if mt.ExpectedError == nil && r == nil { return } if r == nil { t.Fatal("did not panic") } err, ok := r.(error) if !ok { t.Fatalf("panic value is not an error: %v", r) } if !errors.Is(err, safecast.ErrConversionIssue) { t.Fatalf("panic with unexpected error: %v", err) } if !errors.Is(err, mt.ExpectedError) { t.Fatalf("panic with unexpected error: %v", err) } }(t) out := safecast.MustConvert[O](mt.Input) assertEqual(t, mt.ExpectedOutput, out) } func TestMustConvert(t *testing.T) { // [TestConvert] tested all the cases // here we are simply checking that the function panic on errors t.Run("panic on error", func(t *testing.T) { for name, tt := range map[string]TestableConvert{ "negative": MapMustConvertTest[int, uint8]{Input: -1, ExpectedError: safecast.ErrExceedMinimumValue}, "overflow": MapMustConvertTest[int, uint8]{Input: math.MaxInt, ExpectedError: safecast.ErrExceedMaximumValue}, "string": MapMustConvertTest[string, uint8]{Input: "cats", ExpectedError: safecast.ErrStringConversion}, } { t.Run(name, func(t *testing.T) { tt.TestConvert(t) }) } }) t.Run("no panic", func(t *testing.T) { for name, tt := range map[string]TestableConvert{ "number": MapMustConvertTest[int, uint8]{Input: 42, ExpectedOutput: 42}, "string": MapMustConvertTest[string, uint8]{Input: "42", ExpectedOutput: 42}, "float": MapMustConvertTest[float64, uint8]{Input: 42.0, ExpectedOutput: 42}, "octal": MapMustConvertTest[string, uint8]{Input: "0o52", ExpectedOutput: 42}, } { t.Run(name, func(t *testing.T) { tt.TestConvert(t) }) } }) } go-safecast-1.6.0/doc.go000066400000000000000000000004641476433272100150230ustar00rootroot00000000000000package safecast // Package go-safecast solves the type conversion issues in Go // // In Go, integer type conversion can lead to unexpected behavior and errors if not handled carefully. // Issues can happen when converting between signed and unsigned integers, or when converting to a smaller integer type. go-safecast-1.6.0/errors.go000066400000000000000000000040271476433272100155710ustar00rootroot00000000000000package safecast import ( "errors" "fmt" ) // ErrConversionIssue is a generic error for type conversion issues // It is used to wrap other errors var ErrConversionIssue = errors.New("conversion issue") // ErrRangeOverflow is an error for when the value is outside the range of the desired type var ErrRangeOverflow = errors.New("range overflow") // ErrExceedMaximumValue is an error for when the value is greater than the maximum value of the desired type. var ErrExceedMaximumValue = errors.New("maximum value for this type exceeded") // ErrExceedMaximumValue is an error for when the value is less than the minimum value of the desired type. var ErrExceedMinimumValue = errors.New("minimum value for this type exceeded") // ErrUnsupportedConversion is an error for when the conversion is not supported from the provided type. var ErrUnsupportedConversion = errors.New("unsupported type") // ErrStringConversion is an error for when the conversion fails from string. var ErrStringConversion = errors.New("cannot convert from string") // errorHelper is a helper struct for error messages // It is used to wrap other errors, and provides additional information type errorHelper struct { value any boundary any err error } func (e errorHelper) Error() string { errMessage := ErrConversionIssue.Error() switch { case errors.Is(e.err, ErrExceedMaximumValue): errMessage = fmt.Sprintf("%s: %v (%T) is greater than %v (%T)", errMessage, e.value, e.value, e.boundary, e.boundary) case errors.Is(e.err, ErrExceedMinimumValue): errMessage = fmt.Sprintf("%s: %v (%T) is less than %v (%T)", errMessage, e.value, e.value, e.boundary, e.boundary) } if e.err != nil { errMessage = fmt.Sprintf("%s: %s", errMessage, e.err.Error()) } return errMessage } func (e errorHelper) Unwrap() []error { errs := []error{ErrConversionIssue} if e.err != nil { switch { case errors.Is(e.err, ErrExceedMaximumValue), errors.Is(e.err, ErrExceedMinimumValue): errs = append(errs, ErrRangeOverflow) } errs = append(errs, e.err) } return errs } go-safecast-1.6.0/examples_32bit_test.go000066400000000000000000000017371476433272100201420ustar00rootroot00000000000000//go:build i386 || arm package safecast_test // The tests in examples_test.go are the ones that are not architecture dependent // The tests in examples_32bit_test.go are for 32-bit systems // The tests in examples_64bit_test.go are for 64-bit systems // // The architecture dependent files cover the difference when dealing with [safecast.ToInt] // The error message is different on 32-bit and 64-bit systems // The max is 9223372036854775807 on 64-bit and 2147483647 on 32-bit // // The examples could have been skipped for 32-bit systems, // but I wanted the Examples to be launched on this architecture. import ( "fmt" "math" "github.com/ccoveille/go-safecast" ) func ExampleToInt() { a := uint64(42) i, err := safecast.ToInt(a) fmt.Println(i, err) b := float32(math.MaxFloat32) i, err = safecast.ToInt(b) fmt.Println(i, err) // Output: // 42 // 0 conversion issue: 3.4028235e+38 (float32) is greater than 2147483647 (int): maximum value for this type exceeded } go-safecast-1.6.0/examples_64bit_test.go000066400000000000000000000017501476433272100201420ustar00rootroot00000000000000//go:build !386 && !arm package safecast_test // The tests in examples_test.go are the ones that are not architecture dependent // The tests in examples_32bit_test.go are for 32-bit systems // The tests in examples_64bit_test.go are for 64-bit systems // // The architecture dependent files cover the difference when dealing with [safecast.ToInt] // The error message is different on 32-bit and 64-bit systems // The max is 9223372036854775807 on 64-bit and 2147483647 on 32-bit // // The examples could have been skipped for 32-bit systems, // but I wanted the Examples to be launched on this architecture. import ( "fmt" "math" "github.com/ccoveille/go-safecast" ) func ExampleToInt() { a := uint64(42) i, err := safecast.ToInt(a) fmt.Println(i, err) b := float32(math.MaxFloat32) i, err = safecast.ToInt(b) fmt.Println(i, err) // Output: // 42 // 0 conversion issue: 3.4028235e+38 (float32) is greater than 9223372036854775807 (int): maximum value for this type exceeded } go-safecast-1.6.0/examples_test.go000066400000000000000000000122011476433272100171230ustar00rootroot00000000000000package safecast_test import ( "fmt" "math" "github.com/ccoveille/go-safecast" ) // The tests in examples_test.go are the ones that are not architecture dependent // The tests in examples_32bit_test.go are for 32-bit systems // The tests in examples_64bit_test.go are for 64-bit systems // // The architecture dependent files cover the difference when dealing with [safecast.ToInt] // The error message is different on 32-bit and 64-bit systems // The max is 9223372036854775807 on 64-bit and 2147483647 on 32-bit // // The examples could have been skipped for 32-bit systems, // but I wanted the Examples to be launched on this architecture. func ExampleToInt8() { a := float64(42.42) i, err := safecast.ToInt8(a) fmt.Println(i, err) a = float64(200.42) i, err = safecast.ToInt8(a) fmt.Println(i, err) // Output: // 42 // 0 conversion issue: 200.42 (float64) is greater than 127 (int8): maximum value for this type exceeded } func ExampleToUint8() { a := int64(42) i, err := safecast.ToUint8(a) fmt.Println(i, err) a = int64(-1) i, err = safecast.ToUint8(a) fmt.Println(i, err) // Output: // 42 // 0 conversion issue: -1 (int64) is less than 0 (uint8): minimum value for this type exceeded } func ExampleToInt16() { a := int32(42) i, err := safecast.ToInt16(a) fmt.Println(i, err) a = int32(40000) i, err = safecast.ToInt16(a) fmt.Println(i, err) // Output: // 42 // 0 conversion issue: 40000 (int32) is greater than 32767 (int16): maximum value for this type exceeded } func ExampleToUint16() { a := int64(42) i, err := safecast.ToUint16(a) fmt.Println(i, err) a = int64(-1) i, err = safecast.ToUint16(a) fmt.Println(i, err) // Output: // 42 // 0 conversion issue: -1 (int64) is less than 0 (uint16): minimum value for this type exceeded } func ExampleToInt32() { a := int(42) i, err := safecast.ToInt32(a) fmt.Println(i, err) b := uint32(math.MaxInt32) + 1 i, err = safecast.ToInt32(b) fmt.Println(i, err) // Output: // 42 // 0 conversion issue: 2147483648 (uint32) is greater than 2147483647 (int32): maximum value for this type exceeded } func ExampleToUint32() { a := int16(42) i, err := safecast.ToUint32(a) fmt.Println(i, err) a = int16(-1) i, err = safecast.ToUint32(a) fmt.Println(i, err) // Output: // 42 // 0 conversion issue: -1 (int16) is less than 0 (uint32): minimum value for this type exceeded } func ExampleToInt64() { a := uint64(42) i, err := safecast.ToInt64(a) fmt.Println(i, err) a = uint64(math.MaxInt64) + 1 i, err = safecast.ToInt64(a) fmt.Println(i, err) // Output: // 42 // 0 conversion issue: 9223372036854775808 (uint64) is greater than 9223372036854775807 (int64): maximum value for this type exceeded } func ExampleToUint64() { a := int8(42) i, err := safecast.ToUint64(a) fmt.Println(i, err) a = int8(-1) i, err = safecast.ToUint64(a) fmt.Println(i, err) // Output: // 42 // 0 conversion issue: -1 (int8) is less than 0 (uint64): minimum value for this type exceeded } func ExampleToUint() { a := int8(42) i, err := safecast.ToUint(a) fmt.Println(i, err) a = int8(-1) i, err = safecast.ToUint(a) fmt.Println(i, err) // Output: // 42 // 0 conversion issue: -1 (int8) is less than 0 (uint): minimum value for this type exceeded } func ExampleToFloat32() { a := int8(42) i, err := safecast.ToFloat32(a) fmt.Println(i, err) b := math.MaxFloat64 i, err = safecast.ToFloat32(b) fmt.Println(i, err) // Output: // 42 // 0 conversion issue: 1.7976931348623157e+308 (float64) is greater than 3.4028235e+38 (float32): maximum value for this type exceeded } func ExampleToFloat64() { a := int8(42) i, err := safecast.ToFloat64(a) fmt.Println(i, err) b := math.MaxFloat64 i, err = safecast.ToFloat64(b) fmt.Println(i, err) // Output: // 42 // 1.7976931348623157e+308 } func ExampleConvert() { b, err := safecast.Convert[int8](true) fmt.Println(b, err) b, err = safecast.Convert[int8]("true") fmt.Println(b, err) c, err := safecast.Convert[int16](17.1) fmt.Println(c, err) c, err = safecast.Convert[int16](int64(17)) fmt.Println(c, err) d, err := safecast.Convert[int32]("17.1") fmt.Println(d, err) i, err := safecast.Convert[uint]("100_000") fmt.Println(i, err) i, err = safecast.Convert[uint]("0b11") fmt.Println(i, err) i, err = safecast.Convert[uint]("0x11") fmt.Println(i, err) a := int8(-1) i, err = safecast.Convert[uint](a) fmt.Println(i, err) i, err = safecast.Convert[uint]("-1") fmt.Println(i, err) i, err = safecast.Convert[uint]("abc") fmt.Println(i, err) i, err = safecast.Convert[uint]("-1.1") fmt.Println(i, err) i, err = safecast.Convert[uint](".12345E+5") fmt.Println(i, err) // Output: // 1 // 1 // 17 // 17 // 17 // 100000 // 3 // 17 // 0 conversion issue: -1 (int8) is less than 0 (uint): minimum value for this type exceeded // 0 conversion issue: -1 (int64) is less than 0 (uint): minimum value for this type exceeded // 0 conversion issue: cannot convert from string abc to uint // 0 conversion issue: -1.1 (float64) is less than 0 (uint): minimum value for this type exceeded // 12345 } go-safecast-1.6.0/go.mod000066400000000000000000000001431476433272100150270ustar00rootroot00000000000000module github.com/ccoveille/go-safecast go 1.21 retract ( v1.0.0 // Published accidentally. )go-safecast-1.6.0/go.sum000066400000000000000000000000001476433272100150440ustar00rootroot00000000000000go-safecast-1.6.0/readme_test.go000066400000000000000000000016051476433272100165500ustar00rootroot00000000000000package safecast_test import ( "fmt" "github.com/ccoveille/go-safecast" ) func Example() { var a int a = 42 b, err := safecast.ToUint8(a) // everything is fine if err != nil { fmt.Println(err) } fmt.Println(b) a = 255 + 1 _, err = safecast.ToUint8(a) // 256 is greater than uint8 maximum value if err != nil { fmt.Println(err) } a = -1 _, err = safecast.ToUint8(a) // -1 cannot fit in uint8 if err != nil { fmt.Println(err) } str := "\x99" // ASCII code 153 for Trademark symbol e := str[0] _, err = safecast.ToInt8(e) if err != nil { fmt.Println(err) } // Output: // 42 // conversion issue: 256 (int) is greater than 255 (uint8): maximum value for this type exceeded // conversion issue: -1 (int) is less than 0 (uint8): minimum value for this type exceeded // conversion issue: 153 (uint8) is greater than 127 (int8): maximum value for this type exceeded } go-safecast-1.6.0/types.go000066400000000000000000000020551476433272100154200ustar00rootroot00000000000000package safecast // This files is highly inspired from https://pkg.go.dev/golang.org/x/exp/constraints // I didn't import it as it would have added an unnecessary dependency // Signed is a constraint for all signed integers: int, int8, int16, int32, and int64 types. type Signed interface { ~int | ~int8 | ~int16 | ~int32 | ~int64 } // Unsigned is a constraint for all unsigned integers: uint, uint8, uint16, uint32, and uint64 types. // TODO: support uintpr type Unsigned interface { ~uint | ~uint8 | ~uint16 | ~uint32 | ~uint64 } // Integer is a constraint for the all unsigned and signed integers type Integer interface { Signed | Unsigned } // Float is a constraint for the float32 and float64 types. type Float interface { ~float32 | ~float64 } // Number is a constraint for all integers and floats // TODO: consider using complex, but not sure there is a need type Number interface { Integer | Float } // Input is a constraint for all types that can be used as input for [Convert], and [MustConvert] type Input interface { Number | ~string | ~bool }