diff --git a/cast_test.go b/cast_test.go index f69a05b3..1fb9cfb1 100644 --- a/cast_test.go +++ b/cast_test.go @@ -6,6 +6,7 @@ package cast_test import ( + "errors" "fmt" "testing" "time" @@ -239,3 +240,224 @@ type MyUint32 uint32 type MyUint64 uint64 type MyFloat32 float32 type MyFloat64 float64 + +func TestMust(t *testing.T) { + t.Parallel() + c := qt.New(t) + + // Valid values pass through + c.Assert(cast.Must[int](123, nil), qt.Equals, 123) + c.Assert(cast.Must[string]("hello", nil), qt.Equals, "hello") + c.Assert(cast.Must[bool](true, nil), qt.Equals, true) + + // Panic occurs when error is non-nil + c.Assert(func() { + cast.Must[int](0, errors.New("cannot cast")) + }, qt.PanicMatches, "cannot cast") + + c.Assert(func() { + cast.Must[string]("", errors.New("string error")) + }, qt.PanicMatches, "string error") + + c.Assert(func() { + cast.Must[int](cast.ToIntE("invalid_number")) + }, qt.PanicMatches, "unable to cast .*") +} + +func TestToAndToE_AllBasicTypes(t *testing.T) { + t.Parallel() + c := qt.New(t) + + // string + { + v, err := cast.ToE[string](123) + c.Assert(err, qt.IsNil) + c.Assert(v, qt.Equals, "123") + c.Assert(cast.To[string](123), qt.Equals, "123") + + type uncastableStruct struct{ A int } + _, err = cast.ToE[string](uncastableStruct{A: 1}) + c.Assert(err, qt.IsNotNil) + c.Assert(cast.To[string](uncastableStruct{A: 1}), qt.Equals, "") + } + + // bool + { + v, err := cast.ToE[bool]("true") + c.Assert(err, qt.IsNil) + c.Assert(v, qt.Equals, true) + c.Assert(cast.To[bool]("true"), qt.Equals, true) + + _, err = cast.ToE[bool]("invalid") + c.Assert(err, qt.IsNotNil) + c.Assert(cast.To[bool]("invalid"), qt.Equals, false) + } + + // int + { + v, err := cast.ToE[int]("123") + c.Assert(err, qt.IsNil) + c.Assert(v, qt.Equals, 123) + c.Assert(cast.To[int]("123"), qt.Equals, 123) + + _, err = cast.ToE[int]("bad") + c.Assert(err, qt.IsNotNil) + c.Assert(cast.To[int]("bad"), qt.Equals, 0) + } + + // int8 + { + v, err := cast.ToE[int8]("12") + c.Assert(err, qt.IsNil) + c.Assert(v, qt.Equals, int8(12)) + c.Assert(cast.To[int8]("12"), qt.Equals, int8(12)) + + _, err = cast.ToE[int8]("bad") + c.Assert(err, qt.IsNotNil) + c.Assert(cast.To[int8]("bad"), qt.Equals, int8(0)) + } + + // int16 + { + v, err := cast.ToE[int16]("1234") + c.Assert(err, qt.IsNil) + c.Assert(v, qt.Equals, int16(1234)) + c.Assert(cast.To[int16]("1234"), qt.Equals, int16(1234)) + + _, err = cast.ToE[int16]("bad") + c.Assert(err, qt.IsNotNil) + c.Assert(cast.To[int16]("bad"), qt.Equals, int16(0)) + } + + // int32 + { + v, err := cast.ToE[int32]("12345") + c.Assert(err, qt.IsNil) + c.Assert(v, qt.Equals, int32(12345)) + c.Assert(cast.To[int32]("12345"), qt.Equals, int32(12345)) + + _, err = cast.ToE[int32]("bad") + c.Assert(err, qt.IsNotNil) + c.Assert(cast.To[int32]("bad"), qt.Equals, int32(0)) + } + + // int64 + { + v, err := cast.ToE[int64]("123456") + c.Assert(err, qt.IsNil) + c.Assert(v, qt.Equals, int64(123456)) + c.Assert(cast.To[int64]("123456"), qt.Equals, int64(123456)) + + _, err = cast.ToE[int64]("bad") + c.Assert(err, qt.IsNotNil) + c.Assert(cast.To[int64]("bad"), qt.Equals, int64(0)) + } + + // uint + { + v, err := cast.ToE[uint]("123") + c.Assert(err, qt.IsNil) + c.Assert(v, qt.Equals, uint(123)) + c.Assert(cast.To[uint]("123"), qt.Equals, uint(123)) + + _, err = cast.ToE[uint]("-1") + c.Assert(err, qt.IsNotNil) + c.Assert(cast.To[uint]("-1"), qt.Equals, uint(0)) + } + + // uint8 + { + v, err := cast.ToE[uint8]("12") + c.Assert(err, qt.IsNil) + c.Assert(v, qt.Equals, uint8(12)) + c.Assert(cast.To[uint8]("12"), qt.Equals, uint8(12)) + + _, err = cast.ToE[uint8]("-1") + c.Assert(err, qt.IsNotNil) + c.Assert(cast.To[uint8]("-1"), qt.Equals, uint8(0)) + } + + // uint16 + { + v, err := cast.ToE[uint16]("1234") + c.Assert(err, qt.IsNil) + c.Assert(v, qt.Equals, uint16(1234)) + c.Assert(cast.To[uint16]("1234"), qt.Equals, uint16(1234)) + + _, err = cast.ToE[uint16]("-1") + c.Assert(err, qt.IsNotNil) + c.Assert(cast.To[uint16]("-1"), qt.Equals, uint16(0)) + } + + // uint32 + { + v, err := cast.ToE[uint32]("12345") + c.Assert(err, qt.IsNil) + c.Assert(v, qt.Equals, uint32(12345)) + c.Assert(cast.To[uint32]("12345"), qt.Equals, uint32(12345)) + + _, err = cast.ToE[uint32]("-1") + c.Assert(err, qt.IsNotNil) + c.Assert(cast.To[uint32]("-1"), qt.Equals, uint32(0)) + } + + // uint64 + { + v, err := cast.ToE[uint64]("123456") + c.Assert(err, qt.IsNil) + c.Assert(v, qt.Equals, uint64(123456)) + c.Assert(cast.To[uint64]("123456"), qt.Equals, uint64(123456)) + + _, err = cast.ToE[uint64]("-1") + c.Assert(err, qt.IsNotNil) + c.Assert(cast.To[uint64]("-1"), qt.Equals, uint64(0)) + } + + // float32 + { + v, err := cast.ToE[float32]("12.34") + c.Assert(err, qt.IsNil) + c.Assert(v, qt.Equals, float32(12.34)) + c.Assert(cast.To[float32]("12.34"), qt.Equals, float32(12.34)) + + _, err = cast.ToE[float32]("bad") + c.Assert(err, qt.IsNotNil) + c.Assert(cast.To[float32]("bad"), qt.Equals, float32(0)) + } + + // float64 + { + v, err := cast.ToE[float64]("12.34") + c.Assert(err, qt.IsNil) + c.Assert(v, qt.Equals, float64(12.34)) + c.Assert(cast.To[float64]("12.34"), qt.Equals, float64(12.34)) + + _, err = cast.ToE[float64]("bad") + c.Assert(err, qt.IsNotNil) + c.Assert(cast.To[float64]("bad"), qt.Equals, float64(0)) + } + + // time.Time + { + v, err := cast.ToE[time.Time]("2020-01-01T00:00:00Z") + c.Assert(err, qt.IsNil) + c.Assert(v.UTC(), qt.Equals, time.Date(2020, 1, 1, 0, 0, 0, 0, time.UTC)) + c.Assert(cast.To[time.Time]("2020-01-01T00:00:00Z").UTC(), qt.Equals, time.Date(2020, 1, 1, 0, 0, 0, 0, time.UTC)) + + _, err = cast.ToE[time.Time]("invalid") + c.Assert(err, qt.IsNotNil) + c.Assert(cast.To[time.Time]("invalid"), qt.Equals, time.Time{}) + } + + // time.Duration + { + v, err := cast.ToE[time.Duration]("5s") + c.Assert(err, qt.IsNil) + c.Assert(v, qt.Equals, 5*time.Second) + c.Assert(cast.To[time.Duration]("5s"), qt.Equals, 5*time.Second) + + _, err = cast.ToE[time.Duration]("invalid") + c.Assert(err, qt.IsNotNil) + c.Assert(cast.To[time.Duration]("invalid"), qt.Equals, time.Duration(0)) + } +} diff --git a/internal/time_test.go b/internal/time_test.go new file mode 100644 index 00000000..04c74c08 --- /dev/null +++ b/internal/time_test.go @@ -0,0 +1,205 @@ +package internal_test + +import ( + "fmt" + "testing" + "time" + + qt "github.com/frankban/quicktest" + + "github.com/spf13/cast/internal" +) + +func TestTimeFormatHasTimezone(t *testing.T) { + c := qt.New(t) + + tests := []struct { + name string + format internal.TimeFormat + expected bool + }{ + { + name: "NoTimezone", + format: internal.TimeFormat{Format: "2006-01-02", Typ: internal.TimeFormatNoTimezone}, + expected: false, + }, + { + name: "NamedTimezone", + format: internal.TimeFormat{Format: time.RFC1123, Typ: internal.TimeFormatNamedTimezone}, + expected: false, + }, + { + name: "NumericTimezone", + format: internal.TimeFormat{Format: time.RFC3339, Typ: internal.TimeFormatNumericTimezone}, + expected: true, + }, + { + name: "NumericAndNamedTimezone", + format: internal.TimeFormat{Format: "2006-01-02 15:04:05.999999999 -0700 MST", Typ: internal.TimeFormatNumericAndNamedTimezone}, + expected: true, + }, + { + name: "TimeOnly", + format: internal.TimeFormat{Format: time.Kitchen, Typ: internal.TimeFormatTimeOnly}, + expected: false, + }, + { + name: "NegativeBoundary", + format: internal.TimeFormat{Format: "custom", Typ: internal.TimeFormatType(-1)}, + expected: false, + }, + { + name: "UpperBoundBoundary", + format: internal.TimeFormat{Format: "custom", Typ: internal.TimeFormatType(5)}, + expected: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + c.Assert(tt.format.HasTimezone(), qt.Equals, tt.expected) + }) + } + + for i, tf := range internal.TimeFormats { + t.Run(fmt.Sprintf("PredefinedFormat_%d_%s", i, tf.Format), func(t *testing.T) { + expected := tf.Typ == internal.TimeFormatNumericTimezone || tf.Typ == internal.TimeFormatNumericAndNamedTimezone + c.Assert(tf.HasTimezone(), qt.Equals, expected) + }) + } +} + +func TestTimeFormatTypeString(t *testing.T) { + c := qt.New(t) + + tests := []struct { + typ internal.TimeFormatType + expected string + }{ + {internal.TimeFormatNoTimezone, "TimeFormatNoTimezone"}, + {internal.TimeFormatNamedTimezone, "TimeFormatNamedTimezone"}, + {internal.TimeFormatNumericTimezone, "TimeFormatNumericTimezone"}, + {internal.TimeFormatNumericAndNamedTimezone, "TimeFormatNumericAndNamedTimezone"}, + {internal.TimeFormatTimeOnly, "TimeFormatTimeOnly"}, + {internal.TimeFormatType(-1), "TimeFormatType(-1)"}, + {internal.TimeFormatType(5), "TimeFormatType(5)"}, + {internal.TimeFormatType(100), "TimeFormatType(100)"}, + } + + for _, tt := range tests { + t.Run(tt.expected, func(t *testing.T) { + c.Assert(tt.typ.String(), qt.Equals, tt.expected) + }) + } +} + +func TestParseDateWith(t *testing.T) { + c := qt.New(t) + + ny, err := time.LoadLocation("America/New_York") + c.Assert(err, qt.IsNil) + + t.Run("NoTimezone with nil location defaults to Local", func(t *testing.T) { + formats := []internal.TimeFormat{ + {Format: "2006-01-02", Typ: internal.TimeFormatNoTimezone}, + } + res, err := internal.ParseDateWith("2023-05-15", nil, formats) + c.Assert(err, qt.IsNil) + c.Assert(res.Location(), qt.Equals, time.Local) + c.Assert(res.Year(), qt.Equals, 2023) + c.Assert(res.Month(), qt.Equals, time.Month(5)) + c.Assert(res.Day(), qt.Equals, 15) + }) + + t.Run("NoTimezone with specified location", func(t *testing.T) { + formats := []internal.TimeFormat{ + {Format: "2006-01-02 15:04:05", Typ: internal.TimeFormatNoTimezone}, + } + res, err := internal.ParseDateWith("2023-05-15 10:30:00", ny, formats) + c.Assert(err, qt.IsNil) + c.Assert(res.Location(), qt.Equals, ny) + c.Assert(res.Hour(), qt.Equals, 10) + c.Assert(res.Minute(), qt.Equals, 30) + }) + + t.Run("NamedTimezone with nil location defaults to Local", func(t *testing.T) { + formats := []internal.TimeFormat{ + {Format: time.RFC1123, Typ: internal.TimeFormatNamedTimezone}, + } + res, err := internal.ParseDateWith("Mon, 15 May 2023 10:30:00 UTC", nil, formats) + c.Assert(err, qt.IsNil) + c.Assert(res.Location(), qt.Equals, time.Local) + c.Assert(res.Year(), qt.Equals, 2023) + }) + + t.Run("NamedTimezone with specified location", func(t *testing.T) { + formats := []internal.TimeFormat{ + {Format: time.RFC1123, Typ: internal.TimeFormatNamedTimezone}, + } + res, err := internal.ParseDateWith("Mon, 15 May 2023 10:30:00 UTC", time.UTC, formats) + c.Assert(err, qt.IsNil) + c.Assert(res.Location(), qt.Equals, time.UTC) + }) + + t.Run("NumericTimezone preserves parsed location", func(t *testing.T) { + formats := []internal.TimeFormat{ + {Format: time.RFC3339, Typ: internal.TimeFormatNumericTimezone}, + } + res, err := internal.ParseDateWith("2023-05-15T10:30:00+04:00", time.UTC, formats) + c.Assert(err, qt.IsNil) + _, offset := res.Zone() + c.Assert(offset, qt.Equals, 4*3600) + }) + + t.Run("NumericAndNamedTimezone preserves parsed location", func(t *testing.T) { + formats := []internal.TimeFormat{ + {Format: "2006-01-02 15:04:05.999999999 -0700 MST", Typ: internal.TimeFormatNumericAndNamedTimezone}, + } + res, err := internal.ParseDateWith("2023-05-15 10:30:00.123456789 -0700 MST", time.UTC, formats) + c.Assert(err, qt.IsNil) + c.Assert(res.Nanosecond(), qt.Equals, 123456789) + _, offset := res.Zone() + c.Assert(offset, qt.Equals, -7*3600) + }) + + t.Run("TimeOnly format", func(t *testing.T) { + formats := []internal.TimeFormat{ + {Format: time.Kitchen, Typ: internal.TimeFormatTimeOnly}, + } + res, err := internal.ParseDateWith("3:04PM", time.UTC, formats) + c.Assert(err, qt.IsNil) + c.Assert(res.Hour(), qt.Equals, 15) + c.Assert(res.Minute(), qt.Equals, 4) + }) + + t.Run("Multiple formats fallback", func(t *testing.T) { + formats := []internal.TimeFormat{ + {Format: "2006-01-02", Typ: internal.TimeFormatNoTimezone}, + {Format: time.RFC3339, Typ: internal.TimeFormatNumericTimezone}, + } + res, err := internal.ParseDateWith("2023-05-15T10:30:00Z", time.UTC, formats) + c.Assert(err, qt.IsNil) + c.Assert(res.Year(), qt.Equals, 2023) + }) + + t.Run("Parse failure unmatched format", func(t *testing.T) { + formats := []internal.TimeFormat{ + {Format: "2006-01-02", Typ: internal.TimeFormatNoTimezone}, + } + _, err := internal.ParseDateWith("not-a-date", time.UTC, formats) + c.Assert(err, qt.Not(qt.IsNil)) + c.Assert(err.Error(), qt.Equals, "unable to parse date: not-a-date") + }) + + t.Run("Empty formats slice", func(t *testing.T) { + _, err := internal.ParseDateWith("2023-05-15", time.UTC, nil) + c.Assert(err, qt.Not(qt.IsNil)) + c.Assert(err.Error(), qt.Equals, "unable to parse date: 2023-05-15") + }) + + t.Run("Empty string input", func(t *testing.T) { + _, err := internal.ParseDateWith("", time.UTC, internal.TimeFormats) + c.Assert(err, qt.Not(qt.IsNil)) + c.Assert(err.Error(), qt.Equals, "unable to parse date: ") + }) +} diff --git a/map_test.go b/map_test.go index a66c27ae..76be87da 100644 --- a/map_test.go +++ b/map_test.go @@ -227,6 +227,7 @@ func TestStringMapBool(t *testing.T) { func TestStringMapInt(t *testing.T) { testCases := []testCase{ {map[any]any{"v1": 1, "v2": 222}, map[string]int{"v1": 1, "v2": 222}, false}, + {map[any]int{"v1": 1, "v2": 222}, map[string]int{"v1": 1, "v2": 222}, false}, {map[string]any{"v1": 342, "v2": 5141}, map[string]int{"v1": 342, "v2": 5141}, false}, {map[string]int{"v1": 33, "v2": 88}, map[string]int{"v1": 33, "v2": 88}, false}, {map[string]int32{"v1": int32(33), "v2": int32(88)}, map[string]int{"v1": 33, "v2": 88}, false}, @@ -238,6 +239,7 @@ func TestStringMapInt(t *testing.T) { {nil, map[string]int{}, true}, {testing.T{}, map[string]int{}, true}, {"", map[string]int{}, true}, + {map[string]string{"v1": "foo"}, map[string]int{}, true}, } runMapTests(t, testCases, cast.ToStringMapInt, cast.ToStringMapIntE) @@ -246,6 +248,7 @@ func TestStringMapInt(t *testing.T) { func TestStringMapInt64(t *testing.T) { testCases := []testCase{ {map[any]any{"v1": int32(8), "v2": int32(888)}, map[string]int64{"v1": int64(8), "v2": int64(888)}, false}, + {map[any]int64{"v1": int64(8), "v2": int64(888)}, map[string]int64{"v1": int64(8), "v2": int64(888)}, false}, {map[string]any{"v1": int64(45), "v2": int64(67)}, map[string]int64{"v1": 45, "v2": 67}, false}, {map[string]int64{"v1": 33, "v2": 88}, map[string]int64{"v1": 33, "v2": 88}, false}, {map[string]int{"v1": 33, "v2": 88}, map[string]int64{"v1": 33, "v2": 88}, false}, @@ -258,6 +261,7 @@ func TestStringMapInt64(t *testing.T) { {nil, map[string]int64{}, true}, {testing.T{}, map[string]int64{}, true}, {"", map[string]int64{}, true}, + {map[string]string{"v1": "foo"}, map[string]int64{}, true}, } runMapTests(t, testCases, cast.ToStringMapInt64, cast.ToStringMapInt64E) diff --git a/number_internal_test.go b/number_internal_test.go index 6d333d05..79c46884 100644 --- a/number_internal_test.go +++ b/number_internal_test.go @@ -6,6 +6,7 @@ package cast import ( + "errors" "regexp" "strconv" "strings" @@ -185,3 +186,58 @@ func BenchmarkDecimal(b *testing.B) { }) } } + +type internalMockFloat64Provider struct { + val float64 +} + +func (m internalMockFloat64Provider) Float64() float64 { + return m.val +} + +type internalMockFloat64EProvider struct { + val float64 + err error +} + +func (m internalMockFloat64EProvider) Float64() (float64, error) { + return m.val, m.err +} + +func TestTrimZeroDecimal_Additional(t *testing.T) { + c := qt.New(t) + + // Triggers fallback line 521 (return s) + c.Assert(trimZeroDecimal(""), qt.Equals, "") + c.Assert(trimZeroDecimal("0"), qt.Equals, "0") + c.Assert(trimZeroDecimal("000"), qt.Equals, "000") + c.Assert(trimZeroDecimal("."), qt.Equals, ".") + c.Assert(trimZeroDecimal("123"), qt.Equals, "123") + c.Assert(trimZeroDecimal("abc"), qt.Equals, "abc") +} + +func TestToUnsignedNumberE_Float64Providers(t *testing.T) { + c := qt.New(t) + + // float64Provider positive + v, err := toUnsignedNumberE[float64](internalMockFloat64Provider{val: 42.5}, parseFloat[float64]) + c.Assert(err, qt.IsNil) + c.Assert(v, qt.Equals, 42.5) + + // float64Provider negative + _, err = toUnsignedNumberE[float64](internalMockFloat64Provider{val: -42.5}, parseFloat[float64]) + c.Assert(err, qt.Equals, errNegativeNotAllowed) + + // float64EProvider positive + v, err = toUnsignedNumberE[float64](internalMockFloat64EProvider{val: 42.5, err: nil}, parseFloat[float64]) + c.Assert(err, qt.IsNil) + c.Assert(v, qt.Equals, 42.5) + + // float64EProvider negative + _, err = toUnsignedNumberE[float64](internalMockFloat64EProvider{val: -42.5, err: nil}, parseFloat[float64]) + c.Assert(err, qt.Equals, errNegativeNotAllowed) + + // float64EProvider error + _, err = toUnsignedNumberE[float64](internalMockFloat64EProvider{err: errors.New("err")}, parseFloat[float64]) + c.Assert(err, qt.IsNotNil) +} diff --git a/number_test.go b/number_test.go index 9d84d18d..398998d4 100644 --- a/number_test.go +++ b/number_test.go @@ -7,6 +7,7 @@ package cast_test import ( "encoding/json" + "errors" "math" "math/big" "reflect" @@ -308,6 +309,15 @@ func TestNumber(t *testing.T) { c.Assert(v, qt.Equals, testCase.expected) }) + t.Run("ToBasic", func(t *testing.T) { + t.Parallel() + + c := qt.New(t) + + v := ctx.to(testCase.input) + c.Assert(v, qt.Equals, testCase.expected) + }) + t.Run("ToTypeE", func(t *testing.T) { t.Parallel() @@ -336,6 +346,20 @@ func TestNumber(t *testing.T) { } }) + t.Run("ToBasicE", func(t *testing.T) { + t.Parallel() + + c := qt.New(t) + + v, err := ctx.toErr(testCase.input) + if testCase.expectError { + c.Assert(err, qt.IsNotNil) + } else { + c.Assert(err, qt.IsNil) + c.Assert(v, qt.Equals, testCase.expected) + } + }) + t.Run("Pointer", func(t *testing.T) { t.Run("ToType", func(t *testing.T) { t.Parallel() @@ -355,6 +379,15 @@ func TestNumber(t *testing.T) { c.Assert(v, qt.Equals, testCase.expected) }) + t.Run("ToBasic", func(t *testing.T) { + t.Parallel() + + c := qt.New(t) + + v := ctx.to(&testCase.input) + c.Assert(v, qt.Equals, testCase.expected) + }) + t.Run("ToTypeE", func(t *testing.T) { t.Parallel() @@ -382,6 +415,20 @@ func TestNumber(t *testing.T) { c.Assert(v, qt.Equals, testCase.expected) } }) + + t.Run("ToBasicE", func(t *testing.T) { + t.Parallel() + + c := qt.New(t) + + v, err := ctx.toErr(&testCase.input) + if testCase.expectError { + c.Assert(err, qt.IsNotNil) + } else { + c.Assert(err, qt.IsNil) + c.Assert(v, qt.Equals, testCase.expected) + } + }) }) }) }) @@ -464,3 +511,294 @@ func BenchmarkNumber(b *testing.B) { }) } } + +type mockFloat64Provider struct { + val float64 +} + +func (m mockFloat64Provider) Float64() float64 { + return m.val +} + +type mockFloat64EProvider struct { + val float64 + err error +} + +func (m mockFloat64EProvider) Float64() (float64, error) { + return m.val, m.err +} + +func TestFloat64Providers_Public(t *testing.T) { + t.Parallel() + c := qt.New(t) + + // mockFloat64Provider success on ToFloat64 / ToFloat64E + pValid := mockFloat64Provider{val: 42.5} + c.Assert(cast.ToFloat64(pValid), qt.Equals, 42.5) + vF64, err := cast.ToFloat64E(pValid) + c.Assert(err, qt.IsNil) + c.Assert(vF64, qt.Equals, 42.5) + + // Converting float64Provider to non-float64 number fails: + _, err = cast.ToIntE(pValid) + c.Assert(err, qt.IsNotNil) + _, err = cast.ToInt64E(pValid) + c.Assert(err, qt.IsNotNil) + _, err = cast.ToFloat32E(pValid) + c.Assert(err, qt.IsNotNil) + _, err = cast.ToUintE(pValid) + c.Assert(err, qt.IsNotNil) + _, err = cast.ToUint64E(pValid) + c.Assert(err, qt.IsNotNil) + + // mockFloat64EProvider success + pEValid := mockFloat64EProvider{val: 84.25, err: nil} + vFE64, err := cast.ToFloat64E(pEValid) + c.Assert(err, qt.IsNil) + c.Assert(vFE64, qt.Equals, 84.25) + c.Assert(cast.ToFloat64(pEValid), qt.Equals, 84.25) + + // mockFloat64EProvider error + pEErr := mockFloat64EProvider{err: errors.New("provider failed")} + _, err = cast.ToFloat64E(pEErr) + c.Assert(err, qt.IsNotNil) + c.Assert(cast.ToFloat64(pEErr), qt.Equals, float64(0)) + + // Converting float64EProvider to non-float64 number fails: + _, err = cast.ToIntE(pEValid) + c.Assert(err, qt.IsNotNil) + _, err = cast.ToInt64E(pEValid) + c.Assert(err, qt.IsNotNil) + _, err = cast.ToFloat32E(pEValid) + c.Assert(err, qt.IsNotNil) + _, err = cast.ToUintE(pEValid) + c.Assert(err, qt.IsNotNil) + _, err = cast.ToUint64E(pEValid) + c.Assert(err, qt.IsNotNil) +} + +func TestToUnsignedNumber_NegativeTime(t *testing.T) { + t.Parallel() + c := qt.New(t) + + // Negative time.Weekday + v1, err := cast.ToUintE(time.Weekday(-1)) + c.Assert(err, qt.IsNotNil) + c.Assert(v1, qt.Equals, uint(0)) + c.Assert(cast.ToUint(time.Weekday(-1)), qt.Equals, uint(0)) + + v1_8, err := cast.ToUint8E(time.Weekday(-1)) + c.Assert(err, qt.IsNotNil) + c.Assert(v1_8, qt.Equals, uint8(0)) + + v1_16, err := cast.ToUint16E(time.Weekday(-1)) + c.Assert(err, qt.IsNotNil) + c.Assert(v1_16, qt.Equals, uint16(0)) + + v1_32, err := cast.ToUint32E(time.Weekday(-1)) + c.Assert(err, qt.IsNotNil) + c.Assert(v1_32, qt.Equals, uint32(0)) + + v1_64, err := cast.ToUint64E(time.Weekday(-1)) + c.Assert(err, qt.IsNotNil) + c.Assert(v1_64, qt.Equals, uint64(0)) + + // Negative time.Month + v2, err := cast.ToUintE(time.Month(-1)) + c.Assert(err, qt.IsNotNil) + c.Assert(v2, qt.Equals, uint(0)) + c.Assert(cast.ToUint(time.Month(-1)), qt.Equals, uint(0)) + + v2_8, err := cast.ToUint8E(time.Month(-1)) + c.Assert(err, qt.IsNotNil) + c.Assert(v2_8, qt.Equals, uint8(0)) + + v2_16, err := cast.ToUint16E(time.Month(-1)) + c.Assert(err, qt.IsNotNil) + c.Assert(v2_16, qt.Equals, uint16(0)) + + v2_32, err := cast.ToUint32E(time.Month(-1)) + c.Assert(err, qt.IsNotNil) + c.Assert(v2_32, qt.Equals, uint32(0)) + + v2_64, err := cast.ToUint64E(time.Month(-1)) + c.Assert(err, qt.IsNotNil) + c.Assert(v2_64, qt.Equals, uint64(0)) +} + +func TestJSONNumber_Errors(t *testing.T) { + t.Parallel() + c := qt.New(t) + + invalidJSON := json.Number("not_a_valid_number") + + // Errors in integer casting + _, err := cast.ToIntE(invalidJSON) + c.Assert(err, qt.IsNotNil) + c.Assert(cast.ToInt(invalidJSON), qt.Equals, 0) + + _, err = cast.ToInt64E(invalidJSON) + c.Assert(err, qt.IsNotNil) + c.Assert(cast.ToInt64(invalidJSON), qt.Equals, int64(0)) + + _, err = cast.ToInt32E(invalidJSON) + c.Assert(err, qt.IsNotNil) + _, err = cast.ToInt16E(invalidJSON) + c.Assert(err, qt.IsNotNil) + _, err = cast.ToInt8E(invalidJSON) + c.Assert(err, qt.IsNotNil) + + // Errors in float casting + _, err = cast.ToFloat64E(invalidJSON) + c.Assert(err, qt.IsNotNil) + c.Assert(cast.ToFloat64(invalidJSON), qt.Equals, float64(0)) + + _, err = cast.ToFloat32E(invalidJSON) + c.Assert(err, qt.IsNotNil) + c.Assert(cast.ToFloat32(invalidJSON), qt.Equals, float32(0)) + + // Error in basic.go:ToBoolE (covers basic.go:59!) + b, err := cast.ToBoolE(invalidJSON) + c.Assert(err, qt.IsNotNil) + c.Assert(b, qt.Equals, false) + c.Assert(cast.ToBool(invalidJSON), qt.Equals, false) + + // Valid json.Number with ToBoolE + b, err = cast.ToBoolE(json.Number("1")) + c.Assert(err, qt.IsNil) + c.Assert(b, qt.Equals, true) + + b, err = cast.ToBoolE(json.Number("0")) + c.Assert(err, qt.IsNil) + c.Assert(b, qt.Equals, false) +} + +func TestNumber_FormatsAndOverflow(t *testing.T) { + t.Parallel() + c := qt.New(t) + + // Hexadecimal + c.Assert(cast.ToInt("0x10"), qt.Equals, 16) + c.Assert(cast.ToUint("0x10"), qt.Equals, uint(16)) + c.Assert(cast.ToInt("-0x10"), qt.Equals, -16) + c.Assert(cast.ToUint("+0x10"), qt.Equals, uint(16)) + _, err := cast.ToUintE("-0x10") + c.Assert(err, qt.IsNotNil) + + // Octal (0o and legacy 0 prefix) + c.Assert(cast.ToInt("0o77"), qt.Equals, 63) + c.Assert(cast.ToUint("0o77"), qt.Equals, uint(63)) + c.Assert(cast.ToInt("-0o77"), qt.Equals, -63) + c.Assert(cast.ToInt("077"), qt.Equals, 63) + c.Assert(cast.ToUint("077"), qt.Equals, uint(63)) + + // Binary + c.Assert(cast.ToInt("0b1010"), qt.Equals, 10) + c.Assert(cast.ToUint("0b1010"), qt.Equals, uint(10)) + c.Assert(cast.ToInt("-0b1010"), qt.Equals, -10) + _, err = cast.ToUintE("-0b1010") + c.Assert(err, qt.IsNotNil) + + // Float overflow (ErrRange) + _, err = cast.ToFloat32E("1e40") + c.Assert(err, qt.IsNotNil) + _, err = cast.ToFloat64E("1e309") + c.Assert(err, qt.IsNotNil) + + // Float hex notation + c.Assert(cast.ToFloat64("0x10p0"), qt.Equals, float64(16)) + c.Assert(cast.ToFloat64("0x1p-2"), qt.Equals, float64(0.25)) + + // Whitespace rejects + _, err = cast.ToIntE(" 123 ") + c.Assert(err, qt.IsNotNil) + _, err = cast.ToUintE(" 123 ") + c.Assert(err, qt.IsNotNil) + _, err = cast.ToFloat64E(" 123.45 ") + c.Assert(err, qt.IsNotNil) + + // Decimal trim special cases + c.Assert(cast.ToInt(".5"), qt.Equals, 0) + c.Assert(cast.ToInt("+8."), qt.Equals, 8) + c.Assert(cast.ToInt("+0.25"), qt.Equals, 0) + c.Assert(cast.ToInt("-0.25"), qt.Equals, 0) + c.Assert(cast.ToUint("+8."), qt.Equals, uint(8)) +} + +type customStringer struct { + text string +} + +func (s customStringer) String() string { + return s.text +} + +func TestPointersAndInterfaces(t *testing.T) { + t.Parallel() + c := qt.New(t) + + // Nil pointers + var pi *int + c.Assert(cast.ToInt(pi), qt.Equals, 0) + vInt, err := cast.ToIntE(pi) + c.Assert(err, qt.IsNil) + c.Assert(vInt, qt.Equals, 0) + + var pu *uint + c.Assert(cast.ToUint(pu), qt.Equals, uint(0)) + vUint, err := cast.ToUintE(pu) + c.Assert(err, qt.IsNil) + c.Assert(vUint, qt.Equals, uint(0)) + + var pf *float64 + c.Assert(cast.ToFloat64(pf), qt.Equals, float64(0)) + vFloat, err := cast.ToFloat64E(pf) + c.Assert(err, qt.IsNil) + c.Assert(vFloat, qt.Equals, float64(0)) + + var ps *string + c.Assert(cast.ToString(ps), qt.Equals, "") + vStr, err := cast.ToStringE(ps) + c.Assert(err, qt.IsNil) + c.Assert(vStr, qt.Equals, "") + + var pb *bool + c.Assert(cast.ToBool(pb), qt.Equals, false) + vBool, err := cast.ToBoolE(pb) + c.Assert(err, qt.IsNil) + c.Assert(vBool, qt.Equals, false) + + // Multi-level pointers + val := 99 + p1 := &val + p2 := &p1 + p3 := &p2 + c.Assert(cast.ToInt(p3), qt.Equals, 99) + vMulti, err := cast.ToIntE(p3) + c.Assert(err, qt.IsNil) + c.Assert(vMulti, qt.Equals, 99) + + // Multi-level nil pointer + var pNil *int + pNil2 := &pNil + c.Assert(cast.ToInt(pNil2), qt.Equals, 0) + + // Stringer in ToStringE + cs := customStringer{text: "hello world"} + c.Assert(cast.ToString(cs), qt.Equals, "hello world") + sRes, err := cast.ToStringE(cs) + c.Assert(err, qt.IsNil) + c.Assert(sRes, qt.Equals, "hello world") + + // Stringer passed to ToIntE fails (cast does not parse Stringer to number) + _, err = cast.ToIntE(cs) + c.Assert(err, qt.IsNotNil) + + // error interface in ToStringE + myErr := errors.New("something went wrong") + c.Assert(cast.ToString(myErr), qt.Equals, "something went wrong") + eRes, err := cast.ToStringE(myErr) + c.Assert(err, qt.IsNil) + c.Assert(eRes, qt.Equals, "something went wrong") +} diff --git a/slice_test.go b/slice_test.go index fb7597cc..578f3fdd 100644 --- a/slice_test.go +++ b/slice_test.go @@ -282,3 +282,223 @@ func TestDurationSlice(t *testing.T) { runSliceTests(t, testCases, cast.ToDurationSlice, cast.ToDurationSliceE) } + +func runSliceETests[T cast.Basic](t *testing.T, testCases []testCase, toErr func(i any) ([]T, error)) { + for _, testCase := range testCases { + // TODO: remove after minimum Go version is >=1.22 + testCase := testCase + + t.Run("", func(t *testing.T) { + t.Parallel() + + t.Run("Value", func(t *testing.T) { + t.Run("ToTypeE", func(t *testing.T) { + t.Parallel() + + c := qt.New(t) + + v, err := toErr(testCase.input) + if testCase.expectError { + c.Assert(err, qt.IsNotNil) + } else { + c.Assert(err, qt.IsNil) + c.Assert(v, qt.DeepEquals, testCase.expected) + } + }) + }) + + t.Run("Pointer", func(t *testing.T) { + t.Run("ToTypeE", func(t *testing.T) { + t.Parallel() + + c := qt.New(t) + + v, err := toErr(&testCase.input) + if testCase.expectError { + c.Assert(err, qt.IsNotNil) + } else { + c.Assert(err, qt.IsNil) + c.Assert(v, qt.DeepEquals, testCase.expected) + } + }) + }) + }) + } +} + +func TestInt8Slice(t *testing.T) { + testCases := []testCase{ + {[]int8{-128, 0, 127}, []int8{-128, 0, 127}, false}, + {[]any{int8(1), int(2), "3"}, []int8{1, 2, 3}, false}, + {[]int{1, 3}, []int8{1, 3}, false}, + {[]string{"1", "3"}, []int8{1, 3}, false}, + {[2]int{1, 3}, []int8{1, 3}, false}, + {[]float64{1.0, 3.0}, []int8{1, 3}, false}, + {[]bool{true, false}, []int8{1, 0}, false}, + {[]any{}, []int8{}, false}, + + // Failure cases + {nil, nil, true}, + {testing.T{}, nil, true}, + {[]string{"foo", "bar"}, nil, true}, + {[]string{"9223372036854775808"}, nil, true}, // Overflow math.MaxInt64 + {[]string{"-9223372036854775809"}, nil, true}, // Underflow math.MinInt64 + } + + runSliceETests(t, testCases, cast.ToInt8SliceE) +} + +func TestInt16Slice(t *testing.T) { + testCases := []testCase{ + {[]int16{-32768, 0, 32767}, []int16{-32768, 0, 32767}, false}, + {[]any{int16(1), int(2), "3"}, []int16{1, 2, 3}, false}, + {[]int{1, 3}, []int16{1, 3}, false}, + {[]string{"1", "3"}, []int16{1, 3}, false}, + {[2]int{1, 3}, []int16{1, 3}, false}, + {[]float64{1.0, 3.0}, []int16{1, 3}, false}, + {[]bool{true, false}, []int16{1, 0}, false}, + {[]any{}, []int16{}, false}, + + // Failure cases + {nil, nil, true}, + {testing.T{}, nil, true}, + {[]string{"foo", "bar"}, nil, true}, + {[]string{"9223372036854775808"}, nil, true}, // Overflow math.MaxInt64 + {[]string{"-9223372036854775809"}, nil, true}, // Underflow math.MinInt64 + } + + runSliceETests(t, testCases, cast.ToInt16SliceE) +} + +func TestInt32Slice(t *testing.T) { + testCases := []testCase{ + {[]int32{-2147483648, 0, 2147483647}, []int32{-2147483648, 0, 2147483647}, false}, + {[]any{int32(1), int(2), "3"}, []int32{1, 2, 3}, false}, + {[]int{1, 3}, []int32{1, 3}, false}, + {[]string{"1", "3"}, []int32{1, 3}, false}, + {[2]int{1, 3}, []int32{1, 3}, false}, + {[]float64{1.0, 3.0}, []int32{1, 3}, false}, + {[]bool{true, false}, []int32{1, 0}, false}, + {[]any{}, []int32{}, false}, + + // Failure cases + {nil, nil, true}, + {testing.T{}, nil, true}, + {[]string{"foo", "bar"}, nil, true}, + {[]string{"9223372036854775808"}, nil, true}, // Overflow math.MaxInt64 + {[]string{"-9223372036854775809"}, nil, true}, // Underflow math.MinInt64 + } + + runSliceETests(t, testCases, cast.ToInt32SliceE) +} + +func TestUint8Slice(t *testing.T) { + testCases := []testCase{ + {[]uint8{0, 1, 255}, []uint8{0, 1, 255}, false}, + {[]any{uint8(1), int(2), "3"}, []uint8{1, 2, 3}, false}, + {[]int{1, 3}, []uint8{1, 3}, false}, + {[]string{"1", "3"}, []uint8{1, 3}, false}, + {[2]int{1, 3}, []uint8{1, 3}, false}, + {[]float64{1.0, 3.0}, []uint8{1, 3}, false}, + {[]bool{true, false}, []uint8{1, 0}, false}, + {[]any{}, []uint8{}, false}, + + // Failure cases + {nil, nil, true}, + {testing.T{}, nil, true}, + {[]string{"foo", "bar"}, nil, true}, + {[]int{-1}, nil, true}, // Negative value to uint8 + {[]string{"-1"}, nil, true}, // Negative string to uint8 + {[]string{"18446744073709551616"}, nil, true}, // Overflow math.MaxUint64 + } + + runSliceETests(t, testCases, cast.ToUint8SliceE) +} + +func TestUint16Slice(t *testing.T) { + testCases := []testCase{ + {[]uint16{0, 1, 65535}, []uint16{0, 1, 65535}, false}, + {[]any{uint16(1), int(2), "3"}, []uint16{1, 2, 3}, false}, + {[]int{1, 3}, []uint16{1, 3}, false}, + {[]string{"1", "3"}, []uint16{1, 3}, false}, + {[2]int{1, 3}, []uint16{1, 3}, false}, + {[]float64{1.0, 3.0}, []uint16{1, 3}, false}, + {[]bool{true, false}, []uint16{1, 0}, false}, + {[]any{}, []uint16{}, false}, + + // Failure cases + {nil, nil, true}, + {testing.T{}, nil, true}, + {[]string{"foo", "bar"}, nil, true}, + {[]int{-1}, nil, true}, // Negative value to uint16 + {[]string{"-1"}, nil, true}, // Negative string to uint16 + {[]string{"18446744073709551616"}, nil, true}, // Overflow math.MaxUint64 + } + + runSliceETests(t, testCases, cast.ToUint16SliceE) +} + +func TestUint32Slice(t *testing.T) { + testCases := []testCase{ + {[]uint32{0, 1, 4294967295}, []uint32{0, 1, 4294967295}, false}, + {[]any{uint32(1), int(2), "3"}, []uint32{1, 2, 3}, false}, + {[]int{1, 3}, []uint32{1, 3}, false}, + {[]string{"1", "3"}, []uint32{1, 3}, false}, + {[2]int{1, 3}, []uint32{1, 3}, false}, + {[]float64{1.0, 3.0}, []uint32{1, 3}, false}, + {[]bool{true, false}, []uint32{1, 0}, false}, + {[]any{}, []uint32{}, false}, + + // Failure cases + {nil, nil, true}, + {testing.T{}, nil, true}, + {[]string{"foo", "bar"}, nil, true}, + {[]int{-1}, nil, true}, // Negative value to uint32 + {[]string{"-1"}, nil, true}, // Negative string to uint32 + {[]string{"18446744073709551616"}, nil, true}, // Overflow math.MaxUint64 + } + + runSliceETests(t, testCases, cast.ToUint32SliceE) +} + +func TestUint64Slice(t *testing.T) { + testCases := []testCase{ + {[]uint64{0, 1, 18446744073709551615}, []uint64{0, 1, 18446744073709551615}, false}, + {[]any{uint64(1), int(2), "3"}, []uint64{1, 2, 3}, false}, + {[]int{1, 3}, []uint64{1, 3}, false}, + {[]string{"1", "3"}, []uint64{1, 3}, false}, + {[2]int{1, 3}, []uint64{1, 3}, false}, + {[]float64{1.0, 3.0}, []uint64{1, 3}, false}, + {[]bool{true, false}, []uint64{1, 0}, false}, + {[]any{}, []uint64{}, false}, + + // Failure cases + {nil, nil, true}, + {testing.T{}, nil, true}, + {[]string{"foo", "bar"}, nil, true}, + {[]int{-1}, nil, true}, // Negative value to uint64 + {[]string{"18446744073709551616"}, nil, true}, // Overflow math.MaxUint64 + {[]string{"-1"}, nil, true}, // Negative string to uint64 + } + + runSliceETests(t, testCases, cast.ToUint64SliceE) +} + +func TestFloat32Slice(t *testing.T) { + testCases := []testCase{ + {[]float32{1.5, 3.5}, []float32{1.5, 3.5}, false}, + {[]any{float32(1.5), int(2), "3.5"}, []float32{1.5, 2.0, 3.5}, false}, + {[]int{1, 3}, []float32{1.0, 3.0}, false}, + {[]string{"1.5", "3.5"}, []float32{1.5, 3.5}, false}, + {[2]float64{1.5, 3.5}, []float32{1.5, 3.5}, false}, + {[]bool{true, false}, []float32{1.0, 0.0}, false}, + {[]any{}, []float32{}, false}, + + // Failure cases + {nil, nil, true}, + {testing.T{}, nil, true}, + {[]string{"foo", "bar"}, nil, true}, + } + + runSliceETests(t, testCases, cast.ToFloat32SliceE) +} diff --git a/time_test.go b/time_test.go index a22ea980..3d2a10de 100644 --- a/time_test.go +++ b/time_test.go @@ -7,6 +7,7 @@ package cast_test import ( "encoding/json" + "errors" "fmt" "path" "testing" @@ -290,3 +291,230 @@ func locationEqual(a, b *time.Location) bool { return tA.Equal(tB) } + +func TestStringToDate(t *testing.T) { + c := qt.New(t) + + t.Run("Valid date formats in UTC", func(t *testing.T) { + d, err := cast.StringToDate("2009-11-10 23:00:00 +0000 UTC") + c.Assert(err, qt.IsNil) + c.Assert(d, qt.Equals, time.Date(2009, 11, 10, 23, 0, 0, 0, time.UTC)) + + d2, err := cast.StringToDate("2009-11-10") + c.Assert(err, qt.IsNil) + c.Assert(d2, qt.Equals, time.Date(2009, 11, 10, 0, 0, 0, 0, time.UTC)) + + d3, err := cast.StringToDate("2009-11-10T23:00:00Z") + c.Assert(err, qt.IsNil) + c.Assert(d3, qt.Equals, time.Date(2009, 11, 10, 23, 0, 0, 0, time.UTC)) + + d4, err := cast.StringToDate("2016-03-06 15:28:01-00:00") + c.Assert(err, qt.IsNil) + c.Assert(d4.Equal(time.Date(2016, 3, 6, 15, 28, 1, 0, time.UTC)), qt.IsTrue) + }) + + t.Run("Invalid date returns error", func(t *testing.T) { + _, err := cast.StringToDate("invalid-date") + c.Assert(err, qt.Not(qt.IsNil)) + c.Assert(err.Error(), qt.Equals, "unable to parse date: invalid-date") + + _, err = cast.StringToDate("") + c.Assert(err, qt.Not(qt.IsNil)) + }) +} + +func TestToTimeInDefaultLocation(t *testing.T) { + c := qt.New(t) + + loc, err := time.LoadLocation("America/New_York") + c.Assert(err, qt.IsNil) + + t.Run("Success with UTC", func(t *testing.T) { + d := cast.ToTimeInDefaultLocation("2020-01-01", time.UTC) + c.Assert(d, qt.Equals, time.Date(2020, 1, 1, 0, 0, 0, 0, time.UTC)) + }) + + t.Run("Success with custom location", func(t *testing.T) { + d := cast.ToTimeInDefaultLocation("2020-01-01 10:00:00", loc) + c.Assert(d, qt.Equals, time.Date(2020, 1, 1, 10, 0, 0, 0, loc)) + }) + + t.Run("Invalid input returns zero time", func(t *testing.T) { + d := cast.ToTimeInDefaultLocation("invalid-time-val", time.UTC) + c.Assert(d, qt.Equals, time.Time{}) + }) +} + +type timeMockFloat64Provider struct { + val float64 +} + +func (m timeMockFloat64Provider) Float64() float64 { + return m.val +} + +type timeMockFloat64EProvider struct { + val float64 + err error +} + +func (m timeMockFloat64EProvider) Float64() (float64, error) { + return m.val, m.err +} + +func TestDurationProvidersAndEdgeCases(t *testing.T) { + c := qt.New(t) + + t.Run("float64Provider success", func(t *testing.T) { + d, err := cast.ToDurationE(timeMockFloat64Provider{val: 5000}) + c.Assert(err, qt.IsNil) + c.Assert(d, qt.Equals, time.Duration(5000)) + }) + + t.Run("float64EProvider success", func(t *testing.T) { + d, err := cast.ToDurationE(timeMockFloat64EProvider{val: 2500, err: nil}) + c.Assert(err, qt.IsNil) + c.Assert(d, qt.Equals, time.Duration(2500)) + }) + + t.Run("float64EProvider error branch", func(t *testing.T) { + d, err := cast.ToDurationE(timeMockFloat64EProvider{val: 0, err: errors.New("provider failure")}) + c.Assert(err, qt.Not(qt.IsNil)) + c.Assert(d, qt.Equals, time.Duration(0)) + c.Assert(err.Error(), qt.Contains, "time.Duration") + }) + + t.Run("String durations edge cases", func(t *testing.T) { + d, err := cast.ToDurationE("-500") + c.Assert(err, qt.IsNil) + c.Assert(d, qt.Equals, time.Duration(-500)) + + d, err = cast.ToDurationE("-5s") + c.Assert(err, qt.IsNil) + c.Assert(d, qt.Equals, -5*time.Second) + + _, err = cast.ToDurationE("5m-invalid") + c.Assert(err, qt.Not(qt.IsNil)) + + _, err = cast.ToDurationE("") + c.Assert(err, qt.Not(qt.IsNil)) + }) + + t.Run("Alias unhandled type fallback", func(t *testing.T) { + type MyBool bool + _, err := cast.ToDurationE(MyBool(true)) + c.Assert(err, qt.Not(qt.IsNil)) + c.Assert(err.Error(), qt.Contains, "unable to cast true of type bool to time.Duration") + + type unhandledStruct struct{} + _, err = cast.ToDurationE(unhandledStruct{}) + c.Assert(err, qt.Not(qt.IsNil)) + c.Assert(err.Error(), qt.Contains, "to time.Duration") + }) + + t.Run("Negative numbers to duration", func(t *testing.T) { + d, err := cast.ToDurationE(int(-100)) + c.Assert(err, qt.IsNil) + c.Assert(d, qt.Equals, time.Duration(-100)) + + d, err = cast.ToDurationE(float64(-200)) + c.Assert(err, qt.IsNil) + c.Assert(d, qt.Equals, time.Duration(-200)) + + d, err = cast.ToDurationE(int64(-300)) + c.Assert(err, qt.IsNil) + c.Assert(d, qt.Equals, time.Duration(-300)) + }) +} + +func TestTimeEdgeCases(t *testing.T) { + c := qt.New(t) + + t.Run("json.Number zero decimal trimming", func(t *testing.T) { + tm, err := cast.ToTimeE(json.Number("1482597504.000000")) + c.Assert(err, qt.IsNil) + c.Assert(tm, qt.Equals, time.Unix(1482597504, 0)) + }) + + t.Run("json.Number empty or invalid string", func(t *testing.T) { + _, err := cast.ToTimeE(json.Number("")) + c.Assert(err, qt.Not(qt.IsNil)) + + _, err = cast.ToTimeE(json.Number("abc")) + c.Assert(err, qt.Not(qt.IsNil)) + }) + + t.Run("Negative timestamp integers", func(t *testing.T) { + tm, err := cast.ToTimeE(int64(-1000)) + c.Assert(err, qt.IsNil) + c.Assert(tm, qt.Equals, time.Unix(-1000, 0)) + + tm, err = cast.ToTimeE(int(-1000)) + c.Assert(err, qt.IsNil) + c.Assert(tm, qt.Equals, time.Unix(-1000, 0)) + + tm, err = cast.ToTimeE(int32(-1000)) + c.Assert(err, qt.IsNil) + c.Assert(tm, qt.Equals, time.Unix(-1000, 0)) + }) + + t.Run("Pointer to int timestamp", func(t *testing.T) { + val := 1482597504 + tm, err := cast.ToTimeE(&val) + c.Assert(err, qt.IsNil) + c.Assert(tm, qt.Equals, time.Unix(1482597504, 0)) + + var nilInt *int + tm, err = cast.ToTimeE(nilInt) + c.Assert(err, qt.IsNil) + c.Assert(tm, qt.Equals, time.Time{}) + }) + + t.Run("Pointer to json.Number", func(t *testing.T) { + jn := json.Number("1482597504") + tm, err := cast.ToTimeE(&jn) + c.Assert(err, qt.IsNil) + c.Assert(tm, qt.Equals, time.Unix(1482597504, 0)) + }) + + t.Run("Unsupported types", func(t *testing.T) { + _, err := cast.ToTimeE(true) + c.Assert(err, qt.Not(qt.IsNil)) + + _, err = cast.ToTimeE(float64(1482597504)) + c.Assert(err, qt.Not(qt.IsNil)) + + _, err = cast.ToTimeE([]byte("2020-01-01")) + c.Assert(err, qt.Not(qt.IsNil)) + }) +} + +func TestTimeConcurrentSafety(t *testing.T) { + c := qt.New(t) + + const goroutines = 50 + const iterations = 100 + done := make(chan bool, goroutines) + + for g := 0; g < goroutines; g++ { + go func(id int) { + defer func() { done <- true }() + for i := 0; i < iterations; i++ { + _, err1 := cast.ToTimeE("2023-05-15T10:30:00Z") + _, err2 := cast.ToDurationE("5s") + _, err3 := cast.StringToDate("2023-05-15") + _, err4 := cast.ToDurationE(500) + d5 := cast.ToTimeInDefaultLocation("2023-05-15 12:00:00", time.UTC) + if err1 != nil || err2 != nil || err3 != nil || err4 != nil || d5.IsZero() { + t.Errorf("unexpected error in concurrent test") + return + } + } + }(g) + } + + for g := 0; g < goroutines; g++ { + <-done + } + c.Assert(true, qt.IsTrue) +}