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
							
								
									8c9545af88
								
							
						
					
					
						commit
						0dff35cb8d
					
				
							
								
								
									
										5
									
								
								cast.go
									
									
									
									
									
								
							
							
						
						
									
										5
									
								
								cast.go
									
									
									
									
									
								
							@ -20,6 +20,11 @@ func ToTime(i interface{}) time.Time {
 | 
			
		||||
	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.
 | 
			
		||||
func ToDuration(i interface{}) time.Duration {
 | 
			
		||||
	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)
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
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.
 | 
			
		||||
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)
 | 
			
		||||
 | 
			
		||||
	switch v := i.(type) {
 | 
			
		||||
	case time.Time:
 | 
			
		||||
		return v, nil
 | 
			
		||||
	case string:
 | 
			
		||||
		return StringToDate(v)
 | 
			
		||||
		return StringToDateInDefaultLocation(v, location)
 | 
			
		||||
	case int:
 | 
			
		||||
		return time.Unix(int64(v), 0), nil
 | 
			
		||||
	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
 | 
			
		||||
// returned.
 | 
			
		||||
func StringToDate(s string) (time.Time, error) {
 | 
			
		||||
	return parseDateWith(s, []string{
 | 
			
		||||
		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,
 | 
			
		||||
	})
 | 
			
		||||
	return StringToDateInDefaultLocation(s, time.UTC)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func parseDateWith(s string, dates []string) (d time.Time, e error) {
 | 
			
		||||
	for _, dateType := range dates {
 | 
			
		||||
		if d, e = time.Parse(dateType, s); e == nil {
 | 
			
		||||
// StringToDateInDefaultLocation casts an empty interface to a time.Time,
 | 
			
		||||
// interpreting inputs without a timezone to be in the given location,
 | 
			
		||||
// 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
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
		Loading…
	
		Reference in New Issue
	
	Block a user