Commit 1d9f67da authored by Russ Cox's avatar Russ Cox

time: deal a bit better with time zones in Parse

* Document Parse's zone interpretation.
* Add ParseInLocation (API change).
* Recognize "wrong" time zone names, like daylight savings time in winter.
* Disambiguate time zone names using offset (like winter EST vs summer EST in Sydney).

The final two are backwards-incompatible changes, but I believe
they are both buggy behavior in the Go 1.0 versions; the old results
were more wrong than the new ones.

Fixes #3604.
Fixes #3653.
Fixes #4001.

R=adg
CC=golang-dev
https://golang.org/cl/7288052
parent 873c7d39
...@@ -67,6 +67,38 @@ func ExampleTime_Format() { ...@@ -67,6 +67,38 @@ func ExampleTime_Format() {
// Nov 10, 2009 at 11:00pm (UTC) // Nov 10, 2009 at 11:00pm (UTC)
} }
func ExampleParse() {
const longForm = "Jan 2, 2006 at 3:04pm (MST)"
t, _ := time.Parse(longForm, "Feb 3, 2013 at 7:54pm (PST)")
fmt.Println(t)
// Note: without explicit zone, returns time in UTC.
const shortForm = "2006-Jan-02"
t, _ = time.Parse(shortForm, "2013-Feb-03")
fmt.Println(t)
// Output:
// 2013-02-03 19:54:00 -0800 PST
// 2013-02-03 00:00:00 +0000 UTC
}
func ExampleParseInLocation() {
loc, _ := time.LoadLocation("Europe/Berlin")
const longForm = "Jan 2, 2006 at 3:04pm (MST)"
t, _ := time.ParseInLocation(longForm, "Jul 9, 2012 at 5:02am (CEST)", loc)
fmt.Println(t)
// Note: without explicit zone, returns time in given location.
const shortForm = "2006-Jan-02"
t, _ = time.ParseInLocation(shortForm, "2012-Jul-09", loc)
fmt.Println(t)
// Output:
// 2012-07-09 05:02:00 +0200 CEST
// 2012-07-09 00:00:00 +0200 CEST
}
func ExampleTime_Round() { func ExampleTime_Round() {
t := time.Date(0, 0, 0, 12, 15, 30, 918273645, time.UTC) t := time.Date(0, 0, 0, 12, 15, 30, 918273645, time.UTC)
round := []time.Duration{ round := []time.Duration{
......
...@@ -641,7 +641,37 @@ func skip(value, prefix string) (string, error) { ...@@ -641,7 +641,37 @@ func skip(value, prefix string) (string, error) {
// 0, this time is before the zero Time). // 0, this time is before the zero Time).
// Years must be in the range 0000..9999. The day of the week is checked // Years must be in the range 0000..9999. The day of the week is checked
// for syntax but it is otherwise ignored. // for syntax but it is otherwise ignored.
//
// In the absence of a time zone indicator, Parse returns a time in UTC.
//
// When parsing a time with a zone offset like -0700, if the offset corresponds
// to a time zone used by the current location (Local), then Parse uses that
// location and zone in the returned time. Otherwise it records the time as
// being in a fabricated location with time fixed at the given zone offset.
//
// When parsing a time with a zone abbreviation like MST, if the zone abbreviation
// has a defined offset in the current location, then that offset is used.
// The zone abbreviation "UTC" is recognized as UTC regardless of location.
// If the zone abbreviation is unknown, Parse records the time as being
// in a fabricated location with the given zone abbreviation and a zero offset.
// This choice means that such a time can be parse and reformatted with the
// same layout losslessly, but the exact instant used in the representation will
// differ by the actual zone offset. To avoid such problems, prefer time layouts
// that use a numeric zone offset, or use ParseInLocation.
func Parse(layout, value string) (Time, error) { func Parse(layout, value string) (Time, error) {
return parse(layout, value, UTC, Local)
}
// ParseInLocation is like Parse but differs in two important ways.
// First, in the absence of time zone information, Parse interprets a time as UTC;
// ParseInLocation interprets the time as in the given location.
// Second, when given a zone offset or abbreviation, Parse tries to match it
// against the Local location; ParseInLocation uses the given location.
func ParseInLocation(layout, value string, loc *Location) (Time, error) {
return parse(layout, value, loc, loc)
}
func parse(layout, value string, defaultLocation, local *Location) (Time, error) {
alayout, avalue := layout, value alayout, avalue := layout, value
rangeErrString := "" // set if a value is out of range rangeErrString := "" // set if a value is out of range
amSet := false // do we need to subtract 12 from the hour for midnight? amSet := false // do we need to subtract 12 from the hour for midnight?
...@@ -892,20 +922,19 @@ func Parse(layout, value string) (Time, error) { ...@@ -892,20 +922,19 @@ func Parse(layout, value string) (Time, error) {
hour = 0 hour = 0
} }
// TODO: be more aggressive checking day?
if z != nil { if z != nil {
return Date(year, Month(month), day, hour, min, sec, nsec, z), nil return Date(year, Month(month), day, hour, min, sec, nsec, z), nil
} }
t := Date(year, Month(month), day, hour, min, sec, nsec, UTC)
if zoneOffset != -1 { if zoneOffset != -1 {
t := Date(year, Month(month), day, hour, min, sec, nsec, UTC)
t.sec -= int64(zoneOffset) t.sec -= int64(zoneOffset)
// Look for local zone with the given offset. // Look for local zone with the given offset.
// If that zone was in effect at the given time, use it. // If that zone was in effect at the given time, use it.
name, offset, _, _, _ := Local.lookup(t.sec + internalToUnix) name, offset, _, _, _ := local.lookup(t.sec + internalToUnix)
if offset == zoneOffset && (zoneName == "" || name == zoneName) { if offset == zoneOffset && (zoneName == "" || name == zoneName) {
t.loc = Local t.loc = local
return t, nil return t, nil
} }
...@@ -915,16 +944,14 @@ func Parse(layout, value string) (Time, error) { ...@@ -915,16 +944,14 @@ func Parse(layout, value string) (Time, error) {
} }
if zoneName != "" { if zoneName != "" {
t := Date(year, Month(month), day, hour, min, sec, nsec, UTC)
// Look for local zone with the given offset. // Look for local zone with the given offset.
// If that zone was in effect at the given time, use it. // If that zone was in effect at the given time, use it.
offset, _, ok := Local.lookupName(zoneName) offset, _, ok := local.lookupName(zoneName, t.sec+internalToUnix)
if ok { if ok {
name, off, _, _, _ := Local.lookup(t.sec + internalToUnix - int64(offset)) t.sec -= int64(offset)
if name == zoneName && off == offset { t.loc = local
t.sec -= int64(offset) return t, nil
t.loc = Local
return t, nil
}
} }
// Otherwise, create fake zone with unknown offset. // Otherwise, create fake zone with unknown offset.
...@@ -932,8 +959,8 @@ func Parse(layout, value string) (Time, error) { ...@@ -932,8 +959,8 @@ func Parse(layout, value string) (Time, error) {
return t, nil return t, nil
} }
// Otherwise, fall back to UTC. // Otherwise, fall back to default.
return t, nil return Date(year, Month(month), day, hour, min, sec, nsec, defaultLocation), nil
} }
func parseNanoseconds(value string, nbytes int) (ns int, rangeErrString string, err error) { func parseNanoseconds(value string, nbytes int) (ns int, rangeErrString string, err error) {
......
...@@ -478,6 +478,7 @@ var parseTests = []ParseTest{ ...@@ -478,6 +478,7 @@ var parseTests = []ParseTest{
{"RubyDate", RubyDate, "Thu Feb 04 21:00:57 -0800 2010", true, true, 1, 0}, {"RubyDate", RubyDate, "Thu Feb 04 21:00:57 -0800 2010", true, true, 1, 0},
{"RFC850", RFC850, "Thursday, 04-Feb-10 21:00:57 PST", true, true, 1, 0}, {"RFC850", RFC850, "Thursday, 04-Feb-10 21:00:57 PST", true, true, 1, 0},
{"RFC1123", RFC1123, "Thu, 04 Feb 2010 21:00:57 PST", true, true, 1, 0}, {"RFC1123", RFC1123, "Thu, 04 Feb 2010 21:00:57 PST", true, true, 1, 0},
{"RFC1123", RFC1123, "Thu, 04 Feb 2010 22:00:57 PDT", true, true, 1, 0},
{"RFC1123Z", RFC1123Z, "Thu, 04 Feb 2010 21:00:57 -0800", true, true, 1, 0}, {"RFC1123Z", RFC1123Z, "Thu, 04 Feb 2010 21:00:57 -0800", true, true, 1, 0},
{"RFC3339", RFC3339, "2010-02-04T21:00:57-08:00", true, false, 1, 0}, {"RFC3339", RFC3339, "2010-02-04T21:00:57-08:00", true, false, 1, 0},
{"custom: \"2006-01-02 15:04:05-07\"", "2006-01-02 15:04:05-07", "2010-02-04 21:00:57-08", true, false, 1, 0}, {"custom: \"2006-01-02 15:04:05-07\"", "2006-01-02 15:04:05-07", "2010-02-04 21:00:57-08", true, false, 1, 0},
...@@ -533,6 +534,42 @@ func TestParse(t *testing.T) { ...@@ -533,6 +534,42 @@ func TestParse(t *testing.T) {
} }
} }
func TestParseInSydney(t *testing.T) {
loc, err := LoadLocation("Australia/Sydney")
if err != nil {
t.Fatal(err)
}
// Check that Parse (and ParseInLocation) understand
// that Feb EST and Aug EST are different time zones in Sydney
// even though both are called EST.
t1, err := ParseInLocation("Jan 02 2006 MST", "Feb 01 2013 EST", loc)
if err != nil {
t.Fatal(err)
}
t2 := Date(2013, February, 1, 00, 00, 00, 0, loc)
if t1 != t2 {
t.Fatalf("ParseInLocation(Feb 01 2013 EST, Sydney) = %v, want %v", t1, t2)
}
_, offset := t1.Zone()
if offset != 11*60*60 {
t.Fatalf("ParseInLocation(Feb 01 2013 EST, Sydney).Zone = _, %d, want _, %d", offset, 11*60*60)
}
t1, err = ParseInLocation("Jan 02 2006 MST", "Aug 01 2013 EST", loc)
if err != nil {
t.Fatal(err)
}
t2 = Date(2013, August, 1, 00, 00, 00, 0, loc)
if t1 != t2 {
t.Fatalf("ParseInLocation(Aug 01 2013 EST, Sydney) = %v, want %v", t1, t2)
}
_, offset = t1.Zone()
if offset != 10*60*60 {
t.Fatalf("ParseInLocation(Aug 01 2013 EST, Sydney).Zone = _, %d, want _, %d", offset, 10*60*60)
}
}
var rubyTests = []ParseTest{ var rubyTests = []ParseTest{
{"RubyDate", RubyDate, "Thu Feb 04 21:00:57 -0800 2010", true, true, 1, 0}, {"RubyDate", RubyDate, "Thu Feb 04 21:00:57 -0800 2010", true, true, 1, 0},
// Ignore the time zone in the test. If it parses, it'll be OK. // Ignore the time zone in the test. If it parses, it'll be OK.
......
...@@ -145,15 +145,36 @@ func (l *Location) lookup(sec int64) (name string, offset int, isDST bool, start ...@@ -145,15 +145,36 @@ func (l *Location) lookup(sec int64) (name string, offset int, isDST bool, start
} }
// lookupName returns information about the time zone with // lookupName returns information about the time zone with
// the given name (such as "EST"). // the given name (such as "EST") at the given pseudo-Unix time
func (l *Location) lookupName(name string) (offset int, isDST bool, ok bool) { // (what the given time of day would be in UTC).
func (l *Location) lookupName(name string, unix int64) (offset int, isDST bool, ok bool) {
l = l.get() l = l.get()
// First try for a zone with the right name that was actually
// in effect at the given time. (In Sydney, Australia, both standard
// and daylight-savings time are abbreviated "EST". Using the
// offset helps us pick the right one for the given time.
// It's not perfect: during the backward transition we might pick
// either one.)
for i := range l.zone {
zone := &l.zone[i]
if zone.name == name {
nam, offset, isDST, _, _ := l.lookup(unix - int64(zone.offset))
if nam == zone.name {
return offset, isDST, true
}
}
}
// Otherwise fall back to an ordinary name match.
for i := range l.zone { for i := range l.zone {
zone := &l.zone[i] zone := &l.zone[i]
if zone.name == name { if zone.name == name {
return zone.offset, zone.isDST, true return zone.offset, zone.isDST, true
} }
} }
// Otherwise, give up.
return return
} }
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment