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, |  | ||||||
| 		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 casts an empty interface to a time.Time,
 | ||||||
| 	for _, dateType := range dates { | // interpreting inputs without a timezone to be in the given location,
 | ||||||
| 		if d, e = time.Parse(dateType, s); e == nil { | // or the local timezone if nil.
 | ||||||
|  | func StringToDateInDefaultLocation(s string, location *time.Location) (time.Time, error) { | ||||||
|  | 	if location == nil { | ||||||
|  | 		location = time.Local | ||||||
|  | 	} | ||||||
|  | 	return parseDateWith(s, location, timeFormats) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | type timeFormat struct { | ||||||
|  | 	format      string | ||||||
|  | 	hasTimezone bool | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | var ( | ||||||
|  | 	timeFormats = []timeFormat{ | ||||||
|  | 		timeFormat{time.RFC3339, true}, | ||||||
|  | 		timeFormat{"2006-01-02T15:04:05", false}, // iso8601 without timezone
 | ||||||
|  | 		timeFormat{time.RFC1123Z, true}, | ||||||
|  | 		timeFormat{time.RFC1123, false}, | ||||||
|  | 		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) | ||||||
|  | 			} | ||||||
|  | 
 | ||||||
| 			return | 			return | ||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
|  | |||||||
		Loading…
	
		Reference in New Issue
	
	Block a user