forked from mirror/cast
Adjust timezone logic
This commit is contained in:
parent
e4dda5f5f1
commit
22b2b540ce
224
cast_test.go
224
cast_test.go
|
@ -9,10 +9,12 @@ import (
|
||||||
"errors"
|
"errors"
|
||||||
"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) {
|
||||||
|
@ -1181,7 +1183,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
|
||||||
|
@ -1294,143 +1296,139 @@ func TestToDurationE(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestToTime(t *testing.T) {
|
func TestToTimeWithTimezones(t *testing.T) {
|
||||||
|
|
||||||
est, err := time.LoadLocation("EST")
|
est, err := time.LoadLocation("EST")
|
||||||
if !assert.NoError(t, err) {
|
require.NoError(t, err)
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
irn, err := time.LoadLocation("Iran")
|
irn, err := time.LoadLocation("Iran")
|
||||||
if !assert.NoError(t, err) {
|
require.NoError(t, err)
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
swd, err := time.LoadLocation("Europe/Stockholm")
|
swd, err := time.LoadLocation("Europe/Stockholm")
|
||||||
if !assert.NoError(t, err) {
|
require.NoError(t, err)
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// time.Parse*() fns handle the target & local timezones being the same
|
|
||||||
// differently, so make sure we use one of the timezones as local by
|
|
||||||
// temporarily change it.
|
|
||||||
if !locationEqual(time.Local, swd) {
|
|
||||||
var originalLocation *time.Location
|
|
||||||
originalLocation, time.Local = time.Local, swd
|
|
||||||
defer func() {
|
|
||||||
time.Local = originalLocation
|
|
||||||
}()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test same local time in different timezones
|
// Test same local time in different timezones
|
||||||
utc2016 := time.Date(2016, time.January, 1, 0, 0, 0, 0, time.UTC)
|
utc2016 := time.Date(2016, time.January, 1, 0, 0, 0, 0, time.UTC)
|
||||||
est2016 := time.Date(2016, time.January, 1, 0, 0, 0, 0, est)
|
est2016 := time.Date(2016, time.January, 1, 0, 0, 0, 0, est)
|
||||||
irn2016 := time.Date(2016, time.January, 1, 0, 0, 0, 0, irn)
|
irn2016 := time.Date(2016, time.January, 1, 0, 0, 0, 0, irn)
|
||||||
swd2016 := time.Date(2016, time.January, 1, 0, 0, 0, 0, swd)
|
swd2016 := time.Date(2016, time.January, 1, 0, 0, 0, 0, swd)
|
||||||
|
loc2016 := time.Date(2016, time.January, 1, 0, 0, 0, 0, time.Local)
|
||||||
|
|
||||||
for _, format := range timeFormats {
|
for i, format := range timeFormats {
|
||||||
t.Logf("Checking time format '%s', has timezone: %v", format.format, format.hasTimezone)
|
format := format
|
||||||
|
if format.typ == timeFormatTimeOnly {
|
||||||
est2016str := est2016.Format(format.format)
|
|
||||||
if !assert.NotEmpty(t, est2016str) {
|
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
swd2016str := swd2016.Format(format.format)
|
nameBase := fmt.Sprintf("%d;timeFormatType=%d;%s", i, format.typ, format.format)
|
||||||
if !assert.NotEmpty(t, swd2016str) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test conversion without a default location
|
t.Run(path.Join(nameBase), func(t *testing.T) {
|
||||||
converted, err := ToTimeE(est2016str)
|
est2016str := est2016.Format(format.format)
|
||||||
if assert.NoError(t, err) {
|
swd2016str := swd2016.Format(format.format)
|
||||||
if format.hasTimezone {
|
|
||||||
// Converting inputs with a timezone should preserve it
|
|
||||||
assertTimeEqual(t, est2016, converted)
|
|
||||||
assertLocationEqual(t, est, converted.Location())
|
|
||||||
} else {
|
|
||||||
// Converting inputs without a timezone should be interpreted
|
|
||||||
// as a local time in UTC.
|
|
||||||
assertTimeEqual(t, utc2016, converted)
|
|
||||||
assertLocationEqual(t, time.UTC, converted.Location())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test conversion of a time in the local timezone without a default
|
t.Run("without default location", func(t *testing.T) {
|
||||||
// location
|
assert := require.New(t)
|
||||||
converted, err = ToTimeE(swd2016str)
|
converted, err := ToTimeE(est2016str)
|
||||||
if assert.NoError(t, err) {
|
assert.NoError(err)
|
||||||
if format.hasTimezone {
|
if format.hasTimezone() {
|
||||||
// Converting inputs with a timezone should preserve it
|
// Converting inputs with a timezone should preserve it
|
||||||
assertTimeEqual(t, swd2016, converted)
|
assertTimeEqual(t, est2016, converted)
|
||||||
assertLocationEqual(t, swd, converted.Location())
|
assertLocationEqual(t, est, converted.Location())
|
||||||
} else {
|
} else {
|
||||||
// Converting inputs without a timezone should be interpreted
|
// Converting inputs without a timezone should be interpreted
|
||||||
// as a local time in UTC.
|
// as a local time in UTC.
|
||||||
assertTimeEqual(t, utc2016, converted)
|
assertTimeEqual(t, utc2016, converted)
|
||||||
assertLocationEqual(t, time.UTC, converted.Location())
|
assertLocationEqual(t, time.UTC, converted.Location())
|
||||||
}
|
}
|
||||||
}
|
})
|
||||||
|
|
||||||
// Conversion with a nil default location sould have same behavior
|
t.Run("local timezone without a default location", func(t *testing.T) {
|
||||||
converted, err = ToTimeInDefaultLocationE(est2016str, nil)
|
assert := require.New(t)
|
||||||
if assert.NoError(t, err) {
|
converted, err := ToTimeE(swd2016str)
|
||||||
if format.hasTimezone {
|
assert.NoError(err)
|
||||||
// Converting inputs with a timezone should preserve it
|
if format.hasTimezone() {
|
||||||
assertTimeEqual(t, est2016, converted)
|
// Converting inputs with a timezone should preserve it
|
||||||
assertLocationEqual(t, est, converted.Location())
|
assertTimeEqual(t, swd2016, converted)
|
||||||
} else {
|
assertLocationEqual(t, swd, converted.Location())
|
||||||
// Converting inputs without a timezone should be interpreted
|
} else {
|
||||||
// as a local time in the local timezone.
|
// Converting inputs without a timezone should be interpreted
|
||||||
assertTimeEqual(t, swd2016, converted)
|
// as a local time in UTC.
|
||||||
assertLocationEqual(t, swd, converted.Location())
|
assertTimeEqual(t, utc2016, converted)
|
||||||
}
|
assertLocationEqual(t, time.UTC, converted.Location())
|
||||||
}
|
}
|
||||||
|
})
|
||||||
|
|
||||||
// Test conversion with a default location that isn't UTC
|
t.Run("nil default location", func(t *testing.T) {
|
||||||
converted, err = ToTimeInDefaultLocationE(est2016str, irn)
|
assert := require.New(t)
|
||||||
if assert.NoError(t, err) {
|
|
||||||
if format.hasTimezone {
|
converted, err := ToTimeInDefaultLocationE(est2016str, nil)
|
||||||
// Converting inputs with a timezone should preserve it
|
assert.NoError(err)
|
||||||
assertTimeEqual(t, est2016, converted)
|
if format.hasTimezone() {
|
||||||
assertLocationEqual(t, est, converted.Location())
|
// Converting inputs with a timezone should preserve it
|
||||||
} else {
|
assertTimeEqual(t, est2016, converted)
|
||||||
// Converting inputs without a timezone should be interpreted
|
assertLocationEqual(t, est, converted.Location())
|
||||||
// as a local time in the given location.
|
} else {
|
||||||
assertTimeEqual(t, irn2016, converted)
|
// Converting inputs without a timezone should be interpreted
|
||||||
assertLocationEqual(t, irn, converted.Location())
|
// as a local time in the local timezone.
|
||||||
}
|
assertTimeEqual(t, loc2016, converted)
|
||||||
}
|
assertLocationEqual(t, time.Local, converted.Location())
|
||||||
|
}
|
||||||
|
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("default location not UTC", func(t *testing.T) {
|
||||||
|
assert := require.New(t)
|
||||||
|
|
||||||
|
converted, err := ToTimeInDefaultLocationE(est2016str, irn)
|
||||||
|
assert.NoError(err)
|
||||||
|
if format.hasTimezone() {
|
||||||
|
// Converting inputs with a timezone should preserve it
|
||||||
|
assertTimeEqual(t, est2016, converted)
|
||||||
|
assertLocationEqual(t, est, converted.Location())
|
||||||
|
} else {
|
||||||
|
// Converting inputs without a timezone should be interpreted
|
||||||
|
// as a local time in the given location.
|
||||||
|
assertTimeEqual(t, irn2016, converted)
|
||||||
|
assertLocationEqual(t, irn, converted.Location())
|
||||||
|
}
|
||||||
|
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("time in the local timezone default location not UTC", func(t *testing.T) {
|
||||||
|
assert := require.New(t)
|
||||||
|
|
||||||
|
converted, err := ToTimeInDefaultLocationE(swd2016str, irn)
|
||||||
|
assert.NoError(err)
|
||||||
|
if format.hasTimezone() {
|
||||||
|
// Converting inputs with a timezone should preserve it
|
||||||
|
assertTimeEqual(t, swd2016, converted)
|
||||||
|
assertLocationEqual(t, swd, converted.Location())
|
||||||
|
} else {
|
||||||
|
// Converting inputs without a timezone should be interpreted
|
||||||
|
// as a local time in the given location.
|
||||||
|
assertTimeEqual(t, irn2016, converted)
|
||||||
|
assertLocationEqual(t, irn, converted.Location())
|
||||||
|
}
|
||||||
|
|
||||||
|
})
|
||||||
|
|
||||||
|
})
|
||||||
|
|
||||||
// Test conversion of a time in the local timezone with a default
|
|
||||||
// location that isn't UTC
|
|
||||||
converted, err = ToTimeInDefaultLocationE(swd2016str, irn)
|
|
||||||
if assert.NoError(t, err) {
|
|
||||||
if format.hasTimezone {
|
|
||||||
// Converting inputs with a timezone should preserve it
|
|
||||||
assertTimeEqual(t, swd2016, converted)
|
|
||||||
assertLocationEqual(t, swd, converted.Location())
|
|
||||||
} else {
|
|
||||||
// Converting inputs without a timezone should be interpreted
|
|
||||||
// as a local time in the given location.
|
|
||||||
assertTimeEqual(t, irn2016, converted)
|
|
||||||
assertLocationEqual(t, irn, converted.Location())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func assertTimeEqual(t *testing.T, expected, actual time.Time, msgAndArgs ...interface{}) bool {
|
func assertTimeEqual(t *testing.T, expected, actual time.Time) {
|
||||||
if !expected.Equal(actual) {
|
t.Helper()
|
||||||
return assert.Fail(t, fmt.Sprintf("Expected time '%s', got '%s'", expected, actual), msgAndArgs...)
|
// Compare the dates using a numeric zone as there are cases where
|
||||||
}
|
// time.Parse will assign a dummy location.
|
||||||
return true
|
// TODO(bep)
|
||||||
|
//require.Equal(t, expected, actual)
|
||||||
|
require.Equal(t, expected.Format(time.RFC1123Z), actual.Format(time.RFC1123Z))
|
||||||
}
|
}
|
||||||
|
|
||||||
func assertLocationEqual(t *testing.T, expected, actual *time.Location, msgAndArgs ...interface{}) bool {
|
func assertLocationEqual(t *testing.T, expected, actual *time.Location) {
|
||||||
if !locationEqual(expected, actual) {
|
t.Helper()
|
||||||
return assert.Fail(t, fmt.Sprintf("Expected location '%s', got '%s'", expected, actual), msgAndArgs...)
|
require.True(t, locationEqual(expected, actual), fmt.Sprintf("Expected location '%s', got '%s'", expected, actual))
|
||||||
}
|
|
||||||
return true
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func locationEqual(a, b *time.Location) bool {
|
func locationEqual(a, b *time.Location) bool {
|
||||||
|
|
85
caste.go
85
caste.go
|
@ -1246,66 +1246,81 @@ 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 StringToDateInDefaultLocation(s, time.UTC)
|
return parseDateWith(s, time.UTC, timeFormats)
|
||||||
}
|
}
|
||||||
|
|
||||||
// StringToDateInDefaultLocation casts an empty interface to a time.Time,
|
// StringToDateInDefaultLocation casts an empty interface to a time.Time,
|
||||||
// interpreting inputs without a timezone to be in the given location,
|
// interpreting inputs without a timezone to be in the given location,
|
||||||
// or the local timezone if nil.
|
// or the local timezone if nil.
|
||||||
func StringToDateInDefaultLocation(s string, location *time.Location) (time.Time, error) {
|
func StringToDateInDefaultLocation(s string, location *time.Location) (time.Time, error) {
|
||||||
if location == nil {
|
|
||||||
location = time.Local
|
|
||||||
}
|
|
||||||
return parseDateWith(s, location, timeFormats)
|
return parseDateWith(s, location, timeFormats)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type timeFormatType int
|
||||||
|
|
||||||
|
const (
|
||||||
|
timeFormatNoTimezone timeFormatType = iota
|
||||||
|
timeFormatNamedTimezone
|
||||||
|
timeFormatNumericTimezone
|
||||||
|
timeFormatNumericAndNamedTimezone
|
||||||
|
timeFormatTimeOnly
|
||||||
|
)
|
||||||
|
|
||||||
type timeFormat struct {
|
type timeFormat struct {
|
||||||
format string
|
format string
|
||||||
hasTimezone bool
|
typ timeFormatType
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f timeFormat) hasTimezone() bool {
|
||||||
|
// We don't include the formats with only named timezones, see
|
||||||
|
// https://github.com/golang/go/issues/19694#issuecomment-289103522
|
||||||
|
return f.typ >= timeFormatNumericTimezone && f.typ <= timeFormatNumericAndNamedTimezone
|
||||||
}
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
timeFormats = []timeFormat{
|
timeFormats = []timeFormat{
|
||||||
timeFormat{time.RFC3339, true},
|
timeFormat{time.RFC3339, timeFormatNumericTimezone},
|
||||||
timeFormat{"2006-01-02T15:04:05", false}, // iso8601 without timezone
|
timeFormat{"2006-01-02T15:04:05", timeFormatNoTimezone}, // iso8601 without timezone
|
||||||
timeFormat{time.RFC1123Z, true},
|
timeFormat{time.RFC1123Z, timeFormatNumericTimezone},
|
||||||
timeFormat{time.RFC1123, false},
|
timeFormat{time.RFC1123, timeFormatNamedTimezone},
|
||||||
timeFormat{time.RFC822Z, true},
|
timeFormat{time.RFC822Z, timeFormatNumericTimezone},
|
||||||
timeFormat{time.RFC822, false},
|
timeFormat{time.RFC822, timeFormatNamedTimezone},
|
||||||
|
timeFormat{time.RFC850, timeFormatNamedTimezone},
|
||||||
timeFormat{time.RFC850, true},
|
timeFormat{"2006-01-02 15:04:05.999999999 -0700 MST", timeFormatNumericAndNamedTimezone}, // Time.String()
|
||||||
timeFormat{"2006-01-02 15:04:05.999999999 -0700 MST", true}, // Time.String()
|
timeFormat{"2006-01-02T15:04:05-0700", timeFormatNumericTimezone}, // RFC3339 without timezone hh:mm colon
|
||||||
timeFormat{"2006-01-02T15:04:05-0700", true}, // RFC3339 without timezone hh:mm colon
|
timeFormat{"2006-01-02 15:04:05Z0700", timeFormatNumericTimezone}, // RFC3339 without T or timezone hh:mm colon
|
||||||
timeFormat{"2006-01-02 15:04:05Z0700", true}, // RFC3339 without T or timezone hh:mm colon
|
timeFormat{"2006-01-02 15:04:05", timeFormatNoTimezone},
|
||||||
timeFormat{"2006-01-02 15:04:05", false},
|
timeFormat{time.ANSIC, timeFormatNoTimezone},
|
||||||
|
timeFormat{time.UnixDate, timeFormatNamedTimezone},
|
||||||
timeFormat{time.ANSIC, false},
|
timeFormat{time.RubyDate, timeFormatNumericTimezone},
|
||||||
timeFormat{time.UnixDate, false},
|
timeFormat{"2006-01-02 15:04:05Z07:00", timeFormatNumericTimezone},
|
||||||
timeFormat{time.RubyDate, true},
|
timeFormat{"2006-01-02", timeFormatNoTimezone},
|
||||||
timeFormat{"2006-01-02 15:04:05Z07:00", true},
|
timeFormat{"02 Jan 2006", timeFormatNoTimezone},
|
||||||
timeFormat{"2006-01-02", false},
|
timeFormat{"2006-01-02 15:04:05 -07:00", 1},
|
||||||
timeFormat{"02 Jan 2006", false},
|
timeFormat{"2006-01-02 15:04:05 -0700", 1},
|
||||||
timeFormat{"2006-01-02 15:04:05 -07:00", true},
|
timeFormat{time.Kitchen, timeFormatTimeOnly},
|
||||||
timeFormat{"2006-01-02 15:04:05 -0700", true},
|
timeFormat{time.Stamp, timeFormatTimeOnly},
|
||||||
timeFormat{time.Kitchen, false},
|
timeFormat{time.StampMilli, timeFormatTimeOnly},
|
||||||
timeFormat{time.Stamp, false},
|
timeFormat{time.StampMicro, timeFormatTimeOnly},
|
||||||
timeFormat{time.StampMilli, false},
|
timeFormat{time.StampNano, timeFormatTimeOnly},
|
||||||
timeFormat{time.StampMicro, false},
|
|
||||||
timeFormat{time.StampNano, false},
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
func parseDateWith(s string, defaultLocation *time.Location, formats []timeFormat) (d time.Time, e error) {
|
func parseDateWith(s string, location *time.Location, formats []timeFormat) (d time.Time, e error) {
|
||||||
|
|
||||||
for _, format := range formats {
|
for _, format := range formats {
|
||||||
if d, e = time.Parse(format.format, s); e == nil {
|
if d, e = time.Parse(format.format, s); e == nil {
|
||||||
|
|
||||||
// Some time formats have a zone name, but no offset, so it gets
|
// 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
|
// put in that zone name (not the default one passed in to us), but
|
||||||
// without that zone's offset. So set the location manually.
|
// without that zone's offset. So set the location manually.
|
||||||
if !format.hasTimezone && defaultLocation != nil {
|
if format.typ <= timeFormatNamedTimezone {
|
||||||
|
if location == nil {
|
||||||
|
location = time.Local
|
||||||
|
}
|
||||||
year, month, day := d.Date()
|
year, month, day := d.Date()
|
||||||
hour, min, sec := d.Clock()
|
hour, min, sec := d.Clock()
|
||||||
d = time.Date(year, month, day, hour, min, sec, d.Nanosecond(), defaultLocation)
|
d = time.Date(year, month, day, hour, min, sec, d.Nanosecond(), location)
|
||||||
}
|
}
|
||||||
|
|
||||||
return
|
return
|
||||||
|
|
|
@ -0,0 +1,27 @@
|
||||||
|
// Code generated by "stringer -type timeFormatType"; DO NOT EDIT.
|
||||||
|
|
||||||
|
package cast
|
||||||
|
|
||||||
|
import "strconv"
|
||||||
|
|
||||||
|
func _() {
|
||||||
|
// An "invalid array index" compiler error signifies that the constant values have changed.
|
||||||
|
// Re-run the stringer command to generate them again.
|
||||||
|
var x [1]struct{}
|
||||||
|
_ = x[timeFormatNoTimezone-0]
|
||||||
|
_ = x[timeFormatNamedTimezone-1]
|
||||||
|
_ = x[timeFormatNumericTimezone-2]
|
||||||
|
_ = x[timeFormatNumericAndNamedTimezone-3]
|
||||||
|
_ = x[timeFormatTimeOnly-4]
|
||||||
|
}
|
||||||
|
|
||||||
|
const _timeFormatType_name = "timeFormatNoTimezonetimeFormatNamedTimezonetimeFormatNumericTimezonetimeFormatNumericAndNamedTimezonetimeFormatTimeOnly"
|
||||||
|
|
||||||
|
var _timeFormatType_index = [...]uint8{0, 20, 43, 68, 101, 119}
|
||||||
|
|
||||||
|
func (i timeFormatType) String() string {
|
||||||
|
if i < 0 || i >= timeFormatType(len(_timeFormatType_index)-1) {
|
||||||
|
return "timeFormatType(" + strconv.FormatInt(int64(i), 10) + ")"
|
||||||
|
}
|
||||||
|
return _timeFormatType_name[_timeFormatType_index[i]:_timeFormatType_index[i+1]]
|
||||||
|
}
|
Loading…
Reference in New Issue