]> Cypherpunks repositories - gostls13.git/commitdiff
clean up handling of numeric time zones
authorRob Pike <r@golang.org>
Thu, 14 Jan 2010 00:57:38 +0000 (11:57 +1100)
committerRob Pike <r@golang.org>
Thu, 14 Jan 2010 00:57:38 +0000 (11:57 +1100)
allow formatting of ruby-style times.

Fixes #518.

R=rsc
CC=golang-dev
https://golang.org/cl/186119

src/pkg/time/format.go
src/pkg/time/time_test.go

index 745d9fb153c9849933902d1fbe6119ea4bc489a5..0e885d4652f518dcea96f4584f1534aff3468b26 100644 (file)
@@ -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
index 5036ceb13ea79ffed697d83c0aa6f63147b49458..af1e50fa2bc97549f44a39aefda69a257ec61258 100644 (file)
@@ -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 {