From d9283b27a2546bfb62c64c2ae15a1d480ff28eac Mon Sep 17 00:00:00 2001 From: Rob Pike Date: Thu, 14 Jan 2010 11:57:38 +1100 Subject: [PATCH] clean up handling of numeric time zones allow formatting of ruby-style times. Fixes #518. R=rsc CC=golang-dev https://golang.org/cl/186119 --- src/pkg/time/format.go | 113 +++++++++++++++++++++++--------------- src/pkg/time/time_test.go | 31 ++++++----- 2 files changed, 88 insertions(+), 56 deletions(-) diff --git a/src/pkg/time/format.go b/src/pkg/time/format.go index 745d9fb153..0e885d4652 100644 --- a/src/pkg/time/format.go +++ b/src/pkg/time/format.go @@ -11,6 +11,8 @@ const ( numeric = iota alphabetic separator + plus + minus ) // These are predefined layouts for use in Time.Format. @@ -25,6 +27,7 @@ const ( const ( ANSIC = "Mon Jan _2 15:04:05 2006" UnixDate = "Mon Jan _2 15:04:05 MST 2006" + RubyDate = "Mon Jan 02 15:04:05 -0700 2006" RFC850 = "Monday, 02-Jan-06 15:04:05 MST" RFC1123 = "Mon, 02 Jan 2006 15:04:05 MST" Kitchen = "3:04PM" @@ -56,7 +59,8 @@ const ( stdPM = "PM" stdpm = "pm" stdTZ = "MST" - stdISO8601TZ = "Z" + stdISO8601TZ = "Z" // prints Z for UTC + stdNumTZ = "0700" // always numeric ) var longDayNames = []string{ @@ -126,8 +130,12 @@ func charType(c uint8) int { return numeric case c == '_': // underscore; treated like a number when printing return numeric - case 'a' <= c && c < 'z', 'A' <= c && c <= 'Z': + case 'a' <= c && c <= 'z', 'A' <= c && c <= 'Z': return alphabetic + case c == '+': + return plus + case c == '-': + return minus } return separator } @@ -198,19 +206,31 @@ func (t *Time) Format(layout string) string { p = zeroPad(t.Second) case stdZulu: p = zeroPad(t.Hour) + zeroPad(t.Minute) - case stdISO8601TZ: - // Rather ugly special case, required because the time zone is too broken down - // in this format to recognize easily. We cheat and take "Z" to mean "the time + case stdISO8601TZ, stdNumTZ: + // Ugly special case. We cheat and take "Z" to mean "the time // zone as formatted for ISO 8601". - if t.ZoneOffset == 0 { + zone := t.ZoneOffset / 60 // conver to minutes + if p == stdISO8601TZ && t.ZoneOffset == 0 { p = "Z" } else { - zone := t.ZoneOffset / 60 // minutes - if zone < 0 { - p = "-" - zone = -zone + // If the reference time is stdNumTZ (0700), the sign has already been + // emitted but may be wrong. For stdISO8601TZ we must print it. + if p == stdNumTZ && b.Len() > 0 { + soFar := b.Bytes() + if soFar[len(soFar)-1] == '-' && zone >= 0 { + // fix the sign + soFar[len(soFar)-1] = '+' + } else { + zone = -zone + } + p = "" } else { - p = "+" + if zone < 0 { + p = "-" + zone = -zone + } else { + p = "+" + } } p += zeroPad(zone / 60) p += zeroPad(zone % 60) @@ -294,8 +314,8 @@ func Parse(alayout, avalue string) (*Time, os.Error) { rangeErrString := "" // set if a value is out of range pmSet := false // do we need to add 12 to the hour? // Each iteration steps along one piece - nextIsYear := false // whether next item is a Year; means we saw a minus sign. layout, value := alayout, avalue + sign := "" // pending + or - from previous iteration for len(layout) > 0 && len(value) > 0 { c := layout[0] pieceType := charType(c) @@ -303,10 +323,12 @@ func Parse(alayout, avalue string) (*Time, os.Error) { for i = 0; i < len(layout) && charType(layout[i]) == pieceType; i++ { } reference := layout[0:i] + prevLayout := layout layout = layout[i:] - if reference == "Z" { + // Ugly time zone handling. + if reference == "Z" || reference == "z" { // Special case for ISO8601 time zone: "Z" or "-0800" - if value[0] == 'Z' { + if reference == "Z" && value[0] == 'Z' { i = 1 } else if len(value) >= 5 { i = 5 @@ -316,6 +338,13 @@ func Parse(alayout, avalue string) (*Time, os.Error) { } else { c = value[0] if charType(c) != pieceType { + // could be a minus sign introducing a negative year + if c == '-' && pieceType != minus { + value = value[1:] + sign = "-" + layout = prevLayout // don't consume reference item + continue + } return nil, &ParseError{Layout: alayout, Value: avalue, Message: formatErr + alayout} } for i = 0; i < len(value) && charType(value[i]) == pieceType; i++ { @@ -323,21 +352,17 @@ func Parse(alayout, avalue string) (*Time, os.Error) { } p := value[0:i] value = value[i:] - // Separators must match but: - // - initial run of spaces is treated as a single space - // - there could be a following minus sign for negative years - if pieceType == separator { - if len(p) != len(reference) { - // must be exactly a following minus sign - pp := collapseSpaces(p) - rr := collapseSpaces(reference) - if pp != rr { - if len(pp) != len(rr)+1 || p[len(pp)-1] != '-' { - return nil, &ParseError{Layout: alayout, Value: avalue, Message: formatErr + alayout} - } - nextIsYear = true - continue - } + switch pieceType { + case separator: + // Separators must match but initial run of spaces is treated as a single space. + if collapseSpaces(p) != collapseSpaces(reference) { + return nil, &ParseError{Layout: alayout, Value: avalue, Message: formatErr + alayout} + } + continue + case plus, minus: + if len(p) == 1 { // ++ or -- don't count as signs. + sign = p + continue } } var err os.Error @@ -351,9 +376,8 @@ func Parse(alayout, avalue string) (*Time, os.Error) { } case stdLongYear: t.Year, err = strconv.Atoi64(p) - if nextIsYear { + if sign == "-" { t.Year = -t.Year - nextIsYear = false } case stdMonth: t.Month, err = lookup(shortMonthNames, p) @@ -403,19 +427,25 @@ func Parse(alayout, avalue string) (*Time, os.Error) { if err != nil { t.Minute, err = strconv.Atoi(p[2:4]) } - case stdISO8601TZ: - if p == "Z" { - t.Zone = "UTC" - break + case stdISO8601TZ, stdNumTZ: + if reference == stdISO8601TZ { + if p == "Z" { + t.Zone = "UTC" + break + } + // len(p) known to be 5: "-0800" + sign = p[0:1] + p = p[1:] + } else { + // len(p) known to be 4: "0800" and sign is set } - // len(p) known to be 5: "-0800" var hr, min int - hr, err = strconv.Atoi(p[1:3]) + hr, err = strconv.Atoi(p[0:2]) if err != nil { - min, err = strconv.Atoi(p[3:5]) + min, err = strconv.Atoi(p[2:4]) } t.ZoneOffset = (hr*60 + min) * 60 // offset is in seconds - switch p[0] { + switch sign[0] { case '+': case '-': t.ZoneOffset = -t.ZoneOffset @@ -463,16 +493,13 @@ func Parse(alayout, avalue string) (*Time, os.Error) { } } } - if nextIsYear { - // Means we didn't see a year when we were expecting one - return nil, &ParseError{Layout: alayout, Value: value, Message: formatErr + alayout} - } if rangeErrString != "" { return nil, &ParseError{alayout, avalue, reference, p, ": " + rangeErrString + " out of range"} } if err != nil { return nil, &ParseError{alayout, avalue, reference, p, ""} } + sign = "" } if pmSet && t.Hour < 12 { t.Hour += 12 diff --git a/src/pkg/time/time_test.go b/src/pkg/time/time_test.go index 5036ceb13e..af1e50fa2b 100644 --- a/src/pkg/time/time_test.go +++ b/src/pkg/time/time_test.go @@ -132,6 +132,7 @@ type FormatTest struct { var formatTests = []FormatTest{ FormatTest{"ANSIC", ANSIC, "Thu Feb 4 21:00:57 2010"}, FormatTest{"UnixDate", UnixDate, "Thu Feb 4 21:00:57 PST 2010"}, + FormatTest{"RubyDate", RubyDate, "Thu Feb 04 21:00:57 -0800 2010"}, FormatTest{"RFC850", RFC850, "Thursday, 04-Feb-10 21:00:57 PST"}, FormatTest{"RFC1123", RFC1123, "Thu, 04 Feb 2010 21:00:57 PST"}, FormatTest{"ISO8601", ISO8601, "2010-02-04T21:00:57-0800"}, @@ -152,22 +153,26 @@ func TestFormat(t *testing.T) { } type ParseTest struct { - name string - format string - value string - hasTZ bool // contains a time zone - hasWD bool // contains a weekday + name string + format string + value string + hasTZ bool // contains a time zone + hasWD bool // contains a weekday + yearSign int64 // sign of year } var parseTests = []ParseTest{ - ParseTest{"ANSIC", ANSIC, "Thu Feb 4 21:00:57 2010", false, true}, - ParseTest{"UnixDate", UnixDate, "Thu Feb 4 21:00:57 PST 2010", true, true}, - ParseTest{"RFC850", RFC850, "Thursday, 04-Feb-10 21:00:57 PST", true, true}, - ParseTest{"RFC1123", RFC1123, "Thu, 04 Feb 2010 21:00:57 PST", true, true}, - ParseTest{"ISO8601", ISO8601, "2010-02-04T21:00:57-0800", true, false}, + ParseTest{"ANSIC", ANSIC, "Thu Feb 4 21:00:57 2010", false, true, 1}, + ParseTest{"UnixDate", UnixDate, "Thu Feb 4 21:00:57 PST 2010", true, true, 1}, + ParseTest{"RubyDate", RubyDate, "Thu Feb 04 21:00:57 -0800 2010", true, true, 1}, + ParseTest{"RFC850", RFC850, "Thursday, 04-Feb-10 21:00:57 PST", true, true, 1}, + ParseTest{"RFC1123", RFC1123, "Thu, 04 Feb 2010 21:00:57 PST", true, true, 1}, + ParseTest{"ISO8601", ISO8601, "2010-02-04T21:00:57-0800", true, false, 1}, + // Negative year + ParseTest{"ANSIC", ANSIC, "Thu Feb 4 21:00:57 -2010", false, true, -1}, // Amount of white space should not matter. - ParseTest{"ANSIC", ANSIC, "Thu Feb 4 21:00:57 2010", false, true}, - ParseTest{"ANSIC", ANSIC, "Thu Feb 4 21:00:57 2010", false, true}, + ParseTest{"ANSIC", ANSIC, "Thu Feb 4 21:00:57 2010", false, true, 1}, + ParseTest{"ANSIC", ANSIC, "Thu Feb 4 21:00:57 2010", false, true, 1}, } func TestParse(t *testing.T) { @@ -183,7 +188,7 @@ func TestParse(t *testing.T) { func checkTime(time *Time, test *ParseTest, t *testing.T) { // The time should be Thu Feb 4 21:00:57 PST 2010 - if time.Year != 2010 { + if test.yearSign*time.Year != 2010 { t.Errorf("%s: bad year: %d not %d\n", test.name, time.Year, 2010) } if time.Month != 2 {