Commit d9283b27 authored by Rob Pike's avatar Rob Pike

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
parent e8afb6d8
...@@ -11,6 +11,8 @@ const ( ...@@ -11,6 +11,8 @@ const (
numeric = iota numeric = iota
alphabetic alphabetic
separator separator
plus
minus
) )
// These are predefined layouts for use in Time.Format. // These are predefined layouts for use in Time.Format.
...@@ -25,6 +27,7 @@ const ( ...@@ -25,6 +27,7 @@ const (
const ( const (
ANSIC = "Mon Jan _2 15:04:05 2006" ANSIC = "Mon Jan _2 15:04:05 2006"
UnixDate = "Mon Jan _2 15:04:05 MST 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" RFC850 = "Monday, 02-Jan-06 15:04:05 MST"
RFC1123 = "Mon, 02 Jan 2006 15:04:05 MST" RFC1123 = "Mon, 02 Jan 2006 15:04:05 MST"
Kitchen = "3:04PM" Kitchen = "3:04PM"
...@@ -56,7 +59,8 @@ const ( ...@@ -56,7 +59,8 @@ const (
stdPM = "PM" stdPM = "PM"
stdpm = "pm" stdpm = "pm"
stdTZ = "MST" stdTZ = "MST"
stdISO8601TZ = "Z" stdISO8601TZ = "Z" // prints Z for UTC
stdNumTZ = "0700" // always numeric
) )
var longDayNames = []string{ var longDayNames = []string{
...@@ -126,8 +130,12 @@ func charType(c uint8) int { ...@@ -126,8 +130,12 @@ func charType(c uint8) int {
return numeric return numeric
case c == '_': // underscore; treated like a number when printing case c == '_': // underscore; treated like a number when printing
return numeric return numeric
case 'a' <= c && c < 'z', 'A' <= c && c <= 'Z': case 'a' <= c && c <= 'z', 'A' <= c && c <= 'Z':
return alphabetic return alphabetic
case c == '+':
return plus
case c == '-':
return minus
} }
return separator return separator
} }
...@@ -198,20 +206,32 @@ func (t *Time) Format(layout string) string { ...@@ -198,20 +206,32 @@ func (t *Time) Format(layout string) string {
p = zeroPad(t.Second) p = zeroPad(t.Second)
case stdZulu: case stdZulu:
p = zeroPad(t.Hour) + zeroPad(t.Minute) p = zeroPad(t.Hour) + zeroPad(t.Minute)
case stdISO8601TZ: case stdISO8601TZ, stdNumTZ:
// Rather ugly special case, required because the time zone is too broken down // Ugly special case. We cheat and take "Z" to mean "the time
// in this format to recognize easily. We cheat and take "Z" to mean "the time
// zone as formatted for ISO 8601". // 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" p = "Z"
} else { } else {
zone := t.ZoneOffset / 60 // minutes // 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 {
if zone < 0 { if zone < 0 {
p = "-" p = "-"
zone = -zone zone = -zone
} else { } else {
p = "+" p = "+"
} }
}
p += zeroPad(zone / 60) p += zeroPad(zone / 60)
p += zeroPad(zone % 60) p += zeroPad(zone % 60)
} }
...@@ -294,8 +314,8 @@ func Parse(alayout, avalue string) (*Time, os.Error) { ...@@ -294,8 +314,8 @@ func Parse(alayout, avalue string) (*Time, os.Error) {
rangeErrString := "" // set if a value is out of range rangeErrString := "" // set if a value is out of range
pmSet := false // do we need to add 12 to the hour? pmSet := false // do we need to add 12 to the hour?
// Each iteration steps along one piece // Each iteration steps along one piece
nextIsYear := false // whether next item is a Year; means we saw a minus sign.
layout, value := alayout, avalue layout, value := alayout, avalue
sign := "" // pending + or - from previous iteration
for len(layout) > 0 && len(value) > 0 { for len(layout) > 0 && len(value) > 0 {
c := layout[0] c := layout[0]
pieceType := charType(c) pieceType := charType(c)
...@@ -303,10 +323,12 @@ func Parse(alayout, avalue string) (*Time, os.Error) { ...@@ -303,10 +323,12 @@ func Parse(alayout, avalue string) (*Time, os.Error) {
for i = 0; i < len(layout) && charType(layout[i]) == pieceType; i++ { for i = 0; i < len(layout) && charType(layout[i]) == pieceType; i++ {
} }
reference := layout[0:i] reference := layout[0:i]
prevLayout := layout
layout = layout[i:] layout = layout[i:]
if reference == "Z" { // Ugly time zone handling.
if reference == "Z" || reference == "z" {
// Special case for ISO8601 time zone: "Z" or "-0800" // Special case for ISO8601 time zone: "Z" or "-0800"
if value[0] == 'Z' { if reference == "Z" && value[0] == 'Z' {
i = 1 i = 1
} else if len(value) >= 5 { } else if len(value) >= 5 {
i = 5 i = 5
...@@ -316,6 +338,13 @@ func Parse(alayout, avalue string) (*Time, os.Error) { ...@@ -316,6 +338,13 @@ func Parse(alayout, avalue string) (*Time, os.Error) {
} else { } else {
c = value[0] c = value[0]
if charType(c) != pieceType { 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} return nil, &ParseError{Layout: alayout, Value: avalue, Message: formatErr + alayout}
} }
for i = 0; i < len(value) && charType(value[i]) == pieceType; i++ { for i = 0; i < len(value) && charType(value[i]) == pieceType; i++ {
...@@ -323,21 +352,17 @@ func Parse(alayout, avalue string) (*Time, os.Error) { ...@@ -323,21 +352,17 @@ func Parse(alayout, avalue string) (*Time, os.Error) {
} }
p := value[0:i] p := value[0:i]
value = value[i:] value = value[i:]
// Separators must match but: switch pieceType {
// - initial run of spaces is treated as a single space case separator:
// - there could be a following minus sign for negative years // Separators must match but initial run of spaces is treated as a single space.
if pieceType == separator { if collapseSpaces(p) != collapseSpaces(reference) {
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} return nil, &ParseError{Layout: alayout, Value: avalue, Message: formatErr + alayout}
} }
nextIsYear = true
continue continue
} case plus, minus:
if len(p) == 1 { // ++ or -- don't count as signs.
sign = p
continue
} }
} }
var err os.Error var err os.Error
...@@ -351,9 +376,8 @@ func Parse(alayout, avalue string) (*Time, os.Error) { ...@@ -351,9 +376,8 @@ func Parse(alayout, avalue string) (*Time, os.Error) {
} }
case stdLongYear: case stdLongYear:
t.Year, err = strconv.Atoi64(p) t.Year, err = strconv.Atoi64(p)
if nextIsYear { if sign == "-" {
t.Year = -t.Year t.Year = -t.Year
nextIsYear = false
} }
case stdMonth: case stdMonth:
t.Month, err = lookup(shortMonthNames, p) t.Month, err = lookup(shortMonthNames, p)
...@@ -403,19 +427,25 @@ func Parse(alayout, avalue string) (*Time, os.Error) { ...@@ -403,19 +427,25 @@ func Parse(alayout, avalue string) (*Time, os.Error) {
if err != nil { if err != nil {
t.Minute, err = strconv.Atoi(p[2:4]) t.Minute, err = strconv.Atoi(p[2:4])
} }
case stdISO8601TZ: case stdISO8601TZ, stdNumTZ:
if reference == stdISO8601TZ {
if p == "Z" { if p == "Z" {
t.Zone = "UTC" t.Zone = "UTC"
break break
} }
// len(p) known to be 5: "-0800" // 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
}
var hr, min int var hr, min int
hr, err = strconv.Atoi(p[1:3]) hr, err = strconv.Atoi(p[0:2])
if err != nil { 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 t.ZoneOffset = (hr*60 + min) * 60 // offset is in seconds
switch p[0] { switch sign[0] {
case '+': case '+':
case '-': case '-':
t.ZoneOffset = -t.ZoneOffset t.ZoneOffset = -t.ZoneOffset
...@@ -463,16 +493,13 @@ func Parse(alayout, avalue string) (*Time, os.Error) { ...@@ -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 != "" { if rangeErrString != "" {
return nil, &ParseError{alayout, avalue, reference, p, ": " + rangeErrString + " out of range"} return nil, &ParseError{alayout, avalue, reference, p, ": " + rangeErrString + " out of range"}
} }
if err != nil { if err != nil {
return nil, &ParseError{alayout, avalue, reference, p, ""} return nil, &ParseError{alayout, avalue, reference, p, ""}
} }
sign = ""
} }
if pmSet && t.Hour < 12 { if pmSet && t.Hour < 12 {
t.Hour += 12 t.Hour += 12
......
...@@ -132,6 +132,7 @@ type FormatTest struct { ...@@ -132,6 +132,7 @@ type FormatTest struct {
var formatTests = []FormatTest{ var formatTests = []FormatTest{
FormatTest{"ANSIC", ANSIC, "Thu Feb 4 21:00:57 2010"}, FormatTest{"ANSIC", ANSIC, "Thu Feb 4 21:00:57 2010"},
FormatTest{"UnixDate", UnixDate, "Thu Feb 4 21:00:57 PST 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{"RFC850", RFC850, "Thursday, 04-Feb-10 21:00:57 PST"},
FormatTest{"RFC1123", RFC1123, "Thu, 04 Feb 2010 21:00:57 PST"}, FormatTest{"RFC1123", RFC1123, "Thu, 04 Feb 2010 21:00:57 PST"},
FormatTest{"ISO8601", ISO8601, "2010-02-04T21:00:57-0800"}, FormatTest{"ISO8601", ISO8601, "2010-02-04T21:00:57-0800"},
...@@ -157,17 +158,21 @@ type ParseTest struct { ...@@ -157,17 +158,21 @@ type ParseTest struct {
value string value string
hasTZ bool // contains a time zone hasTZ bool // contains a time zone
hasWD bool // contains a weekday hasWD bool // contains a weekday
yearSign int64 // sign of year
} }
var parseTests = []ParseTest{ var parseTests = []ParseTest{
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{"UnixDate", UnixDate, "Thu Feb 4 21:00:57 PST 2010", true, true}, ParseTest{"UnixDate", UnixDate, "Thu Feb 4 21:00:57 PST 2010", true, true, 1},
ParseTest{"RFC850", RFC850, "Thursday, 04-Feb-10 21:00:57 PST", true, true}, ParseTest{"RubyDate", RubyDate, "Thu Feb 04 21:00:57 -0800 2010", true, true, 1},
ParseTest{"RFC1123", RFC1123, "Thu, 04 Feb 2010 21:00:57 PST", true, true}, ParseTest{"RFC850", RFC850, "Thursday, 04-Feb-10 21:00:57 PST", true, true, 1},
ParseTest{"ISO8601", ISO8601, "2010-02-04T21:00:57-0800", true, false}, 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. // 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, 1},
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},
} }
func TestParse(t *testing.T) { func TestParse(t *testing.T) {
...@@ -183,7 +188,7 @@ func TestParse(t *testing.T) { ...@@ -183,7 +188,7 @@ func TestParse(t *testing.T) {
func checkTime(time *Time, test *ParseTest, t *testing.T) { func checkTime(time *Time, test *ParseTest, t *testing.T) {
// The time should be Thu Feb 4 21:00:57 PST 2010 // 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) t.Errorf("%s: bad year: %d not %d\n", test.name, time.Year, 2010)
} }
if time.Month != 2 { if time.Month != 2 {
......
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