forked from mirror/cast
Compare commits
2 Commits
master
...
date-with-
Author | SHA1 | Date |
---|---|---|
Bjørn Erik Pedersen | f31dc0aaab | |
Heewa Barfchin | b1aa5f0c52 |
|
@ -8,9 +8,14 @@ go:
|
||||||
- tip
|
- tip
|
||||||
os:
|
os:
|
||||||
- linux
|
- linux
|
||||||
|
- osx
|
||||||
|
- windows
|
||||||
matrix:
|
matrix:
|
||||||
allow_failures:
|
allow_failures:
|
||||||
- go: tip
|
- go: tip
|
||||||
|
exclude:
|
||||||
|
- os: windows
|
||||||
|
go: tip
|
||||||
fast_finish: true
|
fast_finish: true
|
||||||
script:
|
script:
|
||||||
- make check
|
- if [[ "$TRAVIS_OS_NAME" == "windows" ]]; then go test -v -race ./...; else make check; fi
|
||||||
|
|
12
Makefile
12
Makefile
|
@ -1,15 +1,15 @@
|
||||||
GOVERSION := $(shell go version | cut -d ' ' -f 3 | cut -d '.' -f 2)
|
GOVERSION := $(shell go version | cut -d ' ' -f 3 | cut -d '.' -f 2)
|
||||||
|
|
||||||
.PHONY: check fmt lint test test-race vet test-cover-html help
|
.PHONY: check fmt test test-race vet test-cover-html help
|
||||||
.DEFAULT_GOAL := help
|
.DEFAULT_GOAL := help
|
||||||
|
|
||||||
check: test-race fmt vet lint ## Run tests and linters
|
check: test-race fmt vet ## Run tests and linters
|
||||||
|
|
||||||
test: ## Run tests
|
test: ## Run tests
|
||||||
go test ./...
|
go test ./...
|
||||||
|
|
||||||
test-race: ## Run tests with race detector
|
test-race: ## Run tests with race detector
|
||||||
go test -race ./...
|
go test -v -race ./...
|
||||||
|
|
||||||
fmt: ## Run gofmt linter
|
fmt: ## Run gofmt linter
|
||||||
ifeq "$(GOVERSION)" "12"
|
ifeq "$(GOVERSION)" "12"
|
||||||
|
@ -20,12 +20,6 @@ ifeq "$(GOVERSION)" "12"
|
||||||
done
|
done
|
||||||
endif
|
endif
|
||||||
|
|
||||||
lint: ## Run golint linter
|
|
||||||
@for d in `go list` ; do \
|
|
||||||
if [ "`golint $$d | tee /dev/stderr`" ]; then \
|
|
||||||
echo "^ golint errors!" && echo && exit 1; \
|
|
||||||
fi \
|
|
||||||
done
|
|
||||||
|
|
||||||
vet: ## Run go vet linter
|
vet: ## Run go vet linter
|
||||||
@if [ "`go vet | tee /dev/stderr`" ]; then \
|
@if [ "`go vet | tee /dev/stderr`" ]; then \
|
||||||
|
|
8
cast.go
8
cast.go
|
@ -20,6 +20,14 @@ func ToTime(i interface{}) time.Time {
|
||||||
return v
|
return v
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ToTimeInDefaultLocationE casts an empty interface to time.Time,
|
||||||
|
// interpreting inputs without a timezone to be in the given location.
|
||||||
|
// To fall back to the local timezone, use time.Local as the last argument.
|
||||||
|
func ToTimeInDefaultLocation(i interface{}, location *time.Location) time.Time {
|
||||||
|
v, _ := ToTimeInDefaultLocationE(i, location)
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
|
||||||
// ToDuration casts an interface to a time.Duration type.
|
// ToDuration casts an interface to a time.Duration type.
|
||||||
func ToDuration(i interface{}) time.Duration {
|
func ToDuration(i interface{}) time.Duration {
|
||||||
v, _ := ToDurationE(i)
|
v, _ := ToDurationE(i)
|
||||||
|
|
149
cast_test.go
149
cast_test.go
|
@ -8,10 +8,12 @@ package cast
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"html/template"
|
"html/template"
|
||||||
|
"path"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestToUintE(t *testing.T) {
|
func TestToUintE(t *testing.T) {
|
||||||
|
@ -1173,7 +1175,7 @@ func TestIndirectPointers(t *testing.T) {
|
||||||
assert.Equal(t, ToInt(z), 13)
|
assert.Equal(t, ToInt(z), 13)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestToTimeEE(t *testing.T) {
|
func TestToTime(t *testing.T) {
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
input interface{}
|
input interface{}
|
||||||
expect time.Time
|
expect time.Time
|
||||||
|
@ -1285,3 +1287,148 @@ func TestToDurationE(t *testing.T) {
|
||||||
assert.Equal(t, test.expect, v, errmsg)
|
assert.Equal(t, test.expect, v, errmsg)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestToTimeWithTimezones(t *testing.T) {
|
||||||
|
|
||||||
|
est, err := time.LoadLocation("EST")
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
irn, err := time.LoadLocation("Iran")
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// Test same local time in different timezones
|
||||||
|
utc2016 := time.Date(2016, time.January, 1, 3, 1, 0, 0, time.UTC)
|
||||||
|
est2016 := time.Date(2016, time.January, 1, 3, 1, 0, 0, est)
|
||||||
|
irn2016 := time.Date(2016, time.January, 1, 3, 1, 0, 0, irn)
|
||||||
|
loc2016 := time.Date(2016, time.January, 1, 3, 1, 0, 0, time.Local)
|
||||||
|
|
||||||
|
for i, format := range timeFormats {
|
||||||
|
format := format
|
||||||
|
if format.typ == timeFormatShort {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
nameBase := fmt.Sprintf("%d;timeFormatType=%d;%s", i, format.typ, format.format)
|
||||||
|
|
||||||
|
t.Run(path.Join(nameBase), func(t *testing.T) {
|
||||||
|
est2016str := est2016.Format(format.format)
|
||||||
|
loc2016str := loc2016.Format(format.format)
|
||||||
|
|
||||||
|
t.Run("without default location", func(t *testing.T) {
|
||||||
|
assert := require.New(t)
|
||||||
|
converted, err := ToTimeE(est2016str)
|
||||||
|
assert.NoError(err)
|
||||||
|
if format.hasNumericTimezone() {
|
||||||
|
assertTimeEqual(t, est2016, converted)
|
||||||
|
assertLocationEqual(t, est, converted.Location())
|
||||||
|
} else {
|
||||||
|
assertTimeEqual(t, utc2016, converted)
|
||||||
|
assertLocationEqual(t, time.UTC, converted.Location())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("local timezone without a default location", func(t *testing.T) {
|
||||||
|
assert := require.New(t)
|
||||||
|
converted, err := ToTimeE(loc2016str)
|
||||||
|
assert.NoError(err)
|
||||||
|
if format.hasAnyTimezone() {
|
||||||
|
// Local timezone strings can be either named or numeric and
|
||||||
|
// time.Parse connects the dots.
|
||||||
|
assertTimeEqual(t, loc2016, converted)
|
||||||
|
assertLocationEqual(t, time.Local, converted.Location())
|
||||||
|
} else {
|
||||||
|
assertTimeEqual(t, utc2016, converted)
|
||||||
|
assertLocationEqual(t, time.UTC, converted.Location())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("nil default location", func(t *testing.T) {
|
||||||
|
assert := require.New(t)
|
||||||
|
|
||||||
|
converted, err := ToTimeInDefaultLocationE(est2016str, nil)
|
||||||
|
assert.NoError(err)
|
||||||
|
if format.hasNumericTimezone() {
|
||||||
|
assertTimeEqual(t, est2016, converted)
|
||||||
|
assertLocationEqual(t, est, converted.Location())
|
||||||
|
} else {
|
||||||
|
assertTimeEqual(t, utc2016, converted)
|
||||||
|
assertLocationEqual(t, time.UTC, converted.Location())
|
||||||
|
}
|
||||||
|
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("default location not UTC", func(t *testing.T) {
|
||||||
|
assert := require.New(t)
|
||||||
|
|
||||||
|
converted, err := ToTimeInDefaultLocationE(est2016str, irn)
|
||||||
|
assert.NoError(err)
|
||||||
|
if format.hasNumericTimezone() {
|
||||||
|
assertTimeEqual(t, est2016, converted)
|
||||||
|
assertLocationEqual(t, est, converted.Location())
|
||||||
|
} else {
|
||||||
|
assertTimeEqual(t, irn2016, converted)
|
||||||
|
assertLocationEqual(t, irn, converted.Location())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("time in the local timezone default location not UTC", func(t *testing.T) {
|
||||||
|
assert := require.New(t)
|
||||||
|
|
||||||
|
converted, err := ToTimeInDefaultLocationE(loc2016str, irn)
|
||||||
|
assert.NoError(err)
|
||||||
|
|
||||||
|
if format.hasNumericTimezone() {
|
||||||
|
assertTimeEqual(t, loc2016, converted)
|
||||||
|
assertLocationEqual(t, time.Local, converted.Location())
|
||||||
|
} else {
|
||||||
|
assertTimeEqual(t, irn2016, converted)
|
||||||
|
assertLocationEqual(t, irn, converted.Location())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func assertTimeEqual(t *testing.T, expected, actual time.Time) {
|
||||||
|
t.Helper()
|
||||||
|
require.True(t, expected.Equal(actual), fmt.Sprintf("expected\n%s\ngot\n%s", expected, actual))
|
||||||
|
format := "2006-01-02 15:04:05.999999999 -0700"
|
||||||
|
require.Equal(t, expected.Format(format), actual.Format(format))
|
||||||
|
}
|
||||||
|
|
||||||
|
func assertLocationEqual(t *testing.T, expected, actual *time.Location) {
|
||||||
|
t.Helper()
|
||||||
|
require.True(t, locationEqual(expected, actual), fmt.Sprintf("Expected location '%s', got '%s'", expected, actual))
|
||||||
|
}
|
||||||
|
|
||||||
|
func locationEqual(a, b *time.Location) bool {
|
||||||
|
// A note about comparing time.Locations:
|
||||||
|
// - can't only compare pointers
|
||||||
|
// - can't compare loc.String() because locations with the same
|
||||||
|
// name can have different offsets
|
||||||
|
// - can't use reflect.DeepEqual because time.Location has internal
|
||||||
|
// caches
|
||||||
|
|
||||||
|
if a == b {
|
||||||
|
return true
|
||||||
|
} else if a == nil || b == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if they're equal by parsing times with a format that doesn't
|
||||||
|
// include a timezone, which will interpret it as being a local time in
|
||||||
|
// the given zone, and comparing the resulting local times.
|
||||||
|
tA, err := time.ParseInLocation("2006-01-02", "2016-01-01", a)
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
tB, err := time.ParseInLocation("2006-01-02", "2016-01-01", b)
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return tA.Equal(tB)
|
||||||
|
}
|
||||||
|
|
121
caste.go
121
caste.go
|
@ -20,13 +20,20 @@ var errNegativeNotAllowed = errors.New("unable to cast negative value")
|
||||||
|
|
||||||
// ToTimeE casts an interface to a time.Time type.
|
// ToTimeE casts an interface to a time.Time type.
|
||||||
func ToTimeE(i interface{}) (tim time.Time, err error) {
|
func ToTimeE(i interface{}) (tim time.Time, err error) {
|
||||||
|
return ToTimeInDefaultLocationE(i, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ToTimeInDefaultLocationE casts an empty interface to time.Time,
|
||||||
|
// interpreting inputs without a timezone to be in the given location.
|
||||||
|
// To fall back to the local timezone, use time.Local as the last argument.
|
||||||
|
func ToTimeInDefaultLocationE(i interface{}, location *time.Location) (tim time.Time, err error) {
|
||||||
i = indirect(i)
|
i = indirect(i)
|
||||||
|
|
||||||
switch v := i.(type) {
|
switch v := i.(type) {
|
||||||
case time.Time:
|
case time.Time:
|
||||||
return v, nil
|
return v, nil
|
||||||
case string:
|
case string:
|
||||||
return StringToDate(v)
|
return StringToDateInDefaultLocation(v, location)
|
||||||
case int:
|
case int:
|
||||||
return time.Unix(int64(v), 0), nil
|
return time.Unix(int64(v), 0), nil
|
||||||
case int64:
|
case int64:
|
||||||
|
@ -1204,43 +1211,97 @@ func ToDurationSliceE(i interface{}) ([]time.Duration, error) {
|
||||||
// predefined list of formats. If no suitable format is found, an error is
|
// predefined list of formats. If no suitable format is found, an error is
|
||||||
// returned.
|
// returned.
|
||||||
func StringToDate(s string) (time.Time, error) {
|
func StringToDate(s string) (time.Time, error) {
|
||||||
return parseDateWith(s, []string{
|
return parseDateWith(s, nil, timeFormats)
|
||||||
time.RFC3339,
|
|
||||||
"2006-01-02T15:04:05", // iso8601 without timezone
|
|
||||||
time.RFC1123Z,
|
|
||||||
time.RFC1123,
|
|
||||||
time.RFC822Z,
|
|
||||||
time.RFC822,
|
|
||||||
time.RFC850,
|
|
||||||
time.ANSIC,
|
|
||||||
time.UnixDate,
|
|
||||||
time.RubyDate,
|
|
||||||
"2006-01-02 15:04:05.999999999 -0700 MST", // Time.String()
|
|
||||||
"2006-01-02",
|
|
||||||
"02 Jan 2006",
|
|
||||||
"2006-01-02T15:04:05-0700", // RFC3339 without timezone hh:mm colon
|
|
||||||
"2006-01-02 15:04:05 -07:00",
|
|
||||||
"2006-01-02 15:04:05 -0700",
|
|
||||||
"2006-01-02 15:04:05Z07:00", // RFC3339 without T
|
|
||||||
"2006-01-02 15:04:05Z0700", // RFC3339 without T or timezone hh:mm colon
|
|
||||||
"2006-01-02 15:04:05",
|
|
||||||
time.Kitchen,
|
|
||||||
time.Stamp,
|
|
||||||
time.StampMilli,
|
|
||||||
time.StampMicro,
|
|
||||||
time.StampNano,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func parseDateWith(s string, dates []string) (d time.Time, e error) {
|
// StringToDateInDefaultLocation to parse a string into a time.Time type using a
|
||||||
for _, dateType := range dates {
|
// predefined list of formats, interpreting inputs without a timezone to be in
|
||||||
if d, e = time.Parse(dateType, s); e == nil {
|
// the given location.
|
||||||
|
// To fall back to the local timezone, use time.Local as the last argument.
|
||||||
|
func StringToDateInDefaultLocation(s string, location *time.Location) (time.Time, error) {
|
||||||
|
return parseDateWith(s, location, timeFormats)
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseDateWith(s string, location *time.Location, formats []timeFormat) (d time.Time, e error) {
|
||||||
|
for _, format := range formats {
|
||||||
|
if d, e = time.Parse(format.format, s); e == nil {
|
||||||
|
|
||||||
|
// Some time formats have a zone name, but no offset, so it gets
|
||||||
|
// put in that zone name (not the default one passed in to us), but
|
||||||
|
// without that zone's offset. So set the location manually.
|
||||||
|
// Note that we only do this when we get a location in the new *InDefaultLocation
|
||||||
|
// variants to avoid breaking existing behaviour in ToTime, however
|
||||||
|
// weird that existing behaviour may be.
|
||||||
|
if location != nil && !format.hasNumericTimezone() {
|
||||||
|
year, month, day := d.Date()
|
||||||
|
hour, min, sec := d.Clock()
|
||||||
|
d = time.Date(year, month, day, hour, min, sec, d.Nanosecond(), location)
|
||||||
|
}
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return d, fmt.Errorf("unable to parse date: %s", s)
|
return d, fmt.Errorf("unable to parse date: %s", s)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type timeFormatType int
|
||||||
|
|
||||||
|
const (
|
||||||
|
timeFormatShort timeFormatType = iota // time or date only, no timezone
|
||||||
|
timeFormatNoTimezone
|
||||||
|
|
||||||
|
// All below have some kind of timezone information, a name and/or offset.
|
||||||
|
timeFormatNamedTimezone
|
||||||
|
|
||||||
|
// All below have what we consider to be solid timezone information.
|
||||||
|
timeFormatNumericAndNamedTimezone
|
||||||
|
timeFormatNumericTimezone
|
||||||
|
)
|
||||||
|
|
||||||
|
type timeFormat struct {
|
||||||
|
format string
|
||||||
|
typ timeFormatType
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f timeFormat) hasNumericTimezone() bool {
|
||||||
|
return f.typ >= timeFormatNumericAndNamedTimezone
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f timeFormat) hasAnyTimezone() bool {
|
||||||
|
return f.typ >= timeFormatNamedTimezone
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
timeFormats = []timeFormat{
|
||||||
|
{time.RFC3339, timeFormatNumericTimezone},
|
||||||
|
{"2006-01-02T15:04:05", timeFormatNoTimezone}, // iso8601 without timezone
|
||||||
|
{time.RFC1123Z, timeFormatNumericTimezone},
|
||||||
|
{time.RFC1123, timeFormatNamedTimezone},
|
||||||
|
{time.RFC822Z, timeFormatNumericTimezone},
|
||||||
|
{time.RFC822, timeFormatNamedTimezone},
|
||||||
|
{time.RFC850, timeFormatNamedTimezone},
|
||||||
|
{"2006-01-02 15:04:05.999999999 -0700 MST", timeFormatNumericAndNamedTimezone}, // Time.String()
|
||||||
|
{"2006-01-02T15:04:05-0700", timeFormatNumericTimezone}, // RFC3339 without timezone hh:mm colon
|
||||||
|
{"2006-01-02 15:04:05Z0700", timeFormatNumericTimezone}, // RFC3339 without T or timezone hh:mm colon
|
||||||
|
{"2006-01-02 15:04:05", timeFormatNoTimezone},
|
||||||
|
{time.ANSIC, timeFormatNoTimezone},
|
||||||
|
// Must try RubyDate before UnixDate, see:
|
||||||
|
// https://github.com/golang/go/issues/32358
|
||||||
|
{time.RubyDate, timeFormatNumericTimezone},
|
||||||
|
{time.UnixDate, timeFormatNamedTimezone},
|
||||||
|
{"2006-01-02 15:04:05Z07:00", timeFormatNumericTimezone},
|
||||||
|
{"2006-01-02", timeFormatShort},
|
||||||
|
{"02 Jan 2006", timeFormatShort},
|
||||||
|
{"2006-01-02 15:04:05 -07:00", timeFormatNumericTimezone},
|
||||||
|
{"2006-01-02 15:04:05 -0700", timeFormatNumericTimezone},
|
||||||
|
{time.Kitchen, timeFormatShort},
|
||||||
|
{time.Stamp, timeFormatShort},
|
||||||
|
{time.StampMilli, timeFormatShort},
|
||||||
|
{time.StampMicro, timeFormatShort},
|
||||||
|
{time.StampNano, timeFormatShort},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
// jsonStringToObject attempts to unmarshall a string as JSON into
|
// jsonStringToObject attempts to unmarshall a string as JSON into
|
||||||
// the object passed as pointer.
|
// the object passed as pointer.
|
||||||
func jsonStringToObject(s string, v interface{}) error {
|
func jsonStringToObject(s string, v interface{}) error {
|
||||||
|
|
Loading…
Reference in New Issue