Add ToTimeInDefaultLocation/E
Go's time parsing uses UTC when the format doesn't have a tiemzone, and has even weirder behavior when it has a zone name but no numeric offset. A caller to `cast.ToTime` won't know if the returned time was explicitly in UTC, or defaulted there, so the caller cannot fix it. These new functions allow a user to supply a different timezone to default to, with nil using the local zone.
This commit is contained in:
parent
c01685bb84
commit
b1aa5f0c52
5
cast.go
5
cast.go
@ -20,6 +20,11 @@ func ToTime(i interface{}) time.Time {
|
|||||||
return v
|
return v
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
169
cast_test.go
169
cast_test.go
@ -1285,3 +1285,172 @@ func TestToDurationE(t *testing.T) {
|
|||||||
assert.Equal(t, test.expect, v, errmsg)
|
assert.Equal(t, test.expect, v, errmsg)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestToTime(t *testing.T) {
|
||||||
|
est, err := time.LoadLocation("EST")
|
||||||
|
if !assert.NoError(t, err) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
irn, err := time.LoadLocation("Iran")
|
||||||
|
if !assert.NoError(t, err) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
swd, err := time.LoadLocation("Europe/Stockholm")
|
||||||
|
if !assert.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
|
||||||
|
utc2016 := time.Date(2016, time.January, 1, 0, 0, 0, 0, time.UTC)
|
||||||
|
est2016 := time.Date(2016, time.January, 1, 0, 0, 0, 0, est)
|
||||||
|
irn2016 := time.Date(2016, time.January, 1, 0, 0, 0, 0, irn)
|
||||||
|
swd2016 := time.Date(2016, time.January, 1, 0, 0, 0, 0, swd)
|
||||||
|
|
||||||
|
for _, format := range timeFormats {
|
||||||
|
t.Logf("Checking time format '%s', has timezone: %v", format.format, format.hasTimezone)
|
||||||
|
|
||||||
|
est2016str := est2016.Format(format.format)
|
||||||
|
if !assert.NotEmpty(t, est2016str) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
swd2016str := swd2016.Format(format.format)
|
||||||
|
if !assert.NotEmpty(t, swd2016str) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test conversion without a default location
|
||||||
|
converted, err := ToTimeE(est2016str)
|
||||||
|
if assert.NoError(t, 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 UTC.
|
||||||
|
assertTimeEqual(t, utc2016, converted)
|
||||||
|
assertLocationEqual(t, time.UTC, converted.Location())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test conversion of a time in the local timezone without a default
|
||||||
|
// location
|
||||||
|
converted, err = ToTimeE(swd2016str)
|
||||||
|
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 UTC.
|
||||||
|
assertTimeEqual(t, utc2016, converted)
|
||||||
|
assertLocationEqual(t, time.UTC, converted.Location())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Conversion with a nil default location sould have same behavior
|
||||||
|
converted, err = ToTimeInDefaultLocationE(est2016str, nil)
|
||||||
|
if assert.NoError(t, 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 local timezone.
|
||||||
|
assertTimeEqual(t, swd2016, converted)
|
||||||
|
assertLocationEqual(t, swd, converted.Location())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test conversion with a default location that isn't UTC
|
||||||
|
converted, err = ToTimeInDefaultLocationE(est2016str, irn)
|
||||||
|
if assert.NoError(t, 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())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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 {
|
||||||
|
if !expected.Equal(actual) {
|
||||||
|
return assert.Fail(t, fmt.Sprintf("Expected time '%s', got '%s'", expected, actual), msgAndArgs...)
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func assertLocationEqual(t *testing.T, expected, actual *time.Location, msgAndArgs ...interface{}) bool {
|
||||||
|
if !locationEqual(expected, actual) {
|
||||||
|
return assert.Fail(t, fmt.Sprintf("Expected location '%s', got '%s'", expected, actual), msgAndArgs...)
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func locationEqual(a, b *time.Location) bool {
|
||||||
|
// A note about comparring time.Locations:
|
||||||
|
// - can't only compare pointers
|
||||||
|
// - can't compare loc.String() because locations with the same
|
||||||
|
// name can have different offsets
|
||||||
|
// - can't use reflect.DeepEqual because time.Location has internal
|
||||||
|
// caches
|
||||||
|
|
||||||
|
if a == b {
|
||||||
|
return true
|
||||||
|
} else if a == nil || b == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if they're equal by parsing times with a format that doesn't
|
||||||
|
// include a timezone, which will interpret it as being a local time in
|
||||||
|
// the given zone, and comparing the resulting local times.
|
||||||
|
tA, err := time.ParseInLocation("2006-01-02", "2016-01-01", a)
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
tB, err := time.ParseInLocation("2006-01-02", "2016-01-01", b)
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return tA.Equal(tB)
|
||||||
|
}
|
||||||
|
98
caste.go
98
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, time.UTC)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ToTimeInDefaultLocationE casts an empty interface to time.Time,
|
||||||
|
// interpreting inputs without a timezone to be in the given location,
|
||||||
|
// or the local timezone if nil.
|
||||||
|
func ToTimeInDefaultLocationE(i 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,37 +1211,68 @@ 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 StringToDateInDefaultLocation(s, time.UTC)
|
||||||
time.RFC3339,
|
}
|
||||||
"2006-01-02T15:04:05", // iso8601 without timezone
|
|
||||||
time.RFC1123Z,
|
// StringToDateInDefaultLocation casts an empty interface to a time.Time,
|
||||||
time.RFC1123,
|
// interpreting inputs without a timezone to be in the given location,
|
||||||
time.RFC822Z,
|
// or the local timezone if nil.
|
||||||
time.RFC822,
|
func StringToDateInDefaultLocation(s string, location *time.Location) (time.Time, error) {
|
||||||
time.RFC850,
|
if location == nil {
|
||||||
time.ANSIC,
|
location = time.Local
|
||||||
time.UnixDate,
|
}
|
||||||
time.RubyDate,
|
return parseDateWith(s, location, timeFormats)
|
||||||
"2006-01-02 15:04:05.999999999 -0700 MST", // Time.String()
|
}
|
||||||
"2006-01-02",
|
|
||||||
"02 Jan 2006",
|
type timeFormat struct {
|
||||||
"2006-01-02T15:04:05-0700", // RFC3339 without timezone hh:mm colon
|
format string
|
||||||
"2006-01-02 15:04:05 -07:00",
|
hasTimezone bool
|
||||||
"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
|
var (
|
||||||
"2006-01-02 15:04:05",
|
timeFormats = []timeFormat{
|
||||||
time.Kitchen,
|
timeFormat{time.RFC3339, true},
|
||||||
time.Stamp,
|
timeFormat{"2006-01-02T15:04:05", false}, // iso8601 without timezone
|
||||||
time.StampMilli,
|
timeFormat{time.RFC1123Z, true},
|
||||||
time.StampMicro,
|
timeFormat{time.RFC1123, false},
|
||||||
time.StampNano,
|
timeFormat{time.RFC822Z, true},
|
||||||
})
|
timeFormat{time.RFC822, false},
|
||||||
|
|
||||||
|
timeFormat{time.RFC850, true},
|
||||||
|
timeFormat{"2006-01-02 15:04:05.999999999 -0700 MST", true}, // Time.String()
|
||||||
|
timeFormat{"2006-01-02T15:04:05-0700", true}, // RFC3339 without 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", false},
|
||||||
|
|
||||||
|
timeFormat{time.ANSIC, false},
|
||||||
|
timeFormat{time.UnixDate, false},
|
||||||
|
timeFormat{time.RubyDate, true},
|
||||||
|
timeFormat{"2006-01-02 15:04:05Z07:00", true},
|
||||||
|
timeFormat{"2006-01-02", false},
|
||||||
|
timeFormat{"02 Jan 2006", false},
|
||||||
|
timeFormat{"2006-01-02 15:04:05 -07:00", true},
|
||||||
|
timeFormat{"2006-01-02 15:04:05 -0700", true},
|
||||||
|
timeFormat{time.Kitchen, false},
|
||||||
|
timeFormat{time.Stamp, false},
|
||||||
|
timeFormat{time.StampMilli, false},
|
||||||
|
timeFormat{time.StampMicro, false},
|
||||||
|
timeFormat{time.StampNano, false},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
func parseDateWith(s string, defaultLocation *time.Location, formats []timeFormat) (d time.Time, e error) {
|
||||||
|
for _, format := range formats {
|
||||||
|
if d, e = time.Parse(format.format, s); e == nil {
|
||||||
|
|
||||||
|
// Some time formats have a zone name, but no offset, so it gets
|
||||||
|
// put in that zone name (not the default one passed in to us), but
|
||||||
|
// without that zone's offset. So set the location manually.
|
||||||
|
if !format.hasTimezone && defaultLocation != nil {
|
||||||
|
year, month, day := d.Date()
|
||||||
|
hour, min, sec := d.Clock()
|
||||||
|
d = time.Date(year, month, day, hour, min, sec, d.Nanosecond(), defaultLocation)
|
||||||
}
|
}
|
||||||
|
|
||||||
func parseDateWith(s string, dates []string) (d time.Time, e error) {
|
|
||||||
for _, dateType := range dates {
|
|
||||||
if d, e = time.Parse(dateType, s); e == nil {
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user