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
diff --git a/src/pkg/time/format.go b/src/pkg/time/format.go
index 745d9fb..0e885d4 100644
--- a/src/pkg/time/format.go
+++ b/src/pkg/time/format.go
@@ -11,6 +11,8 @@
 	numeric = iota
 	alphabetic
 	separator
+	plus
+	minus
 )
 
 // These are predefined layouts for use in Time.Format.
@@ -25,6 +27,7 @@
 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 @@
 	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 @@
 		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 @@
 			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 @@
 	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 @@
 		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 @@
 		} 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 @@
 		}
 		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 @@
 			}
 		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 @@
 			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 @@
 				}
 			}
 		}
-		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 5036ceb..af1e50f 100644
--- a/src/pkg/time/time_test.go
+++ b/src/pkg/time/time_test.go
@@ -132,6 +132,7 @@
 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 @@
 }
 
 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 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 {