Day of year is 002 or __2, in contrast to day-in-month 2 or 02 or _2.
This means there is no way to print a variable-width day-of-year,
but that's probably OK.
Fixes #25689.
Change-Id: I1425d412cb7d2d360e9b3bf74e89566714e2477a
Reviewed-on: https://go-review.googlesource.com/c/go/+/122876
Run-TryBot: Russ Cox <rsc@golang.org>
TryBot-Result: Gobot Gobot <gobot@golang.org>
Reviewed-by: Rob Pike <r@golang.org>
ErrLocation = errLocation
ReadFile = readFile
LoadTzinfo = loadTzinfo
+ NextStdChunk = nextStdChunk
)
+
+// StdChunkNames maps from nextStdChunk results to the matched strings.
+var StdChunkNames = map[int]string{
+ 0: "",
+ stdLongMonth: "January",
+ stdMonth: "Jan",
+ stdNumMonth: "1",
+ stdZeroMonth: "01",
+ stdLongWeekDay: "Monday",
+ stdWeekDay: "Mon",
+ stdDay: "2",
+ stdUnderDay: "_2",
+ stdZeroDay: "02",
+ stdUnderYearDay: "__2",
+ stdZeroYearDay: "002",
+ stdHour: "15",
+ stdHour12: "3",
+ stdZeroHour12: "03",
+ stdMinute: "4",
+ stdZeroMinute: "04",
+ stdSecond: "5",
+ stdZeroSecond: "05",
+ stdLongYear: "2006",
+ stdYear: "06",
+ stdPM: "PM",
+ stdpm: "pm",
+ stdTZ: "MST",
+ stdISO8601TZ: "Z0700",
+ stdISO8601SecondsTZ: "Z070000",
+ stdISO8601ShortTZ: "Z07",
+ stdISO8601ColonTZ: "Z07:00",
+ stdISO8601ColonSecondsTZ: "Z07:00:00",
+ stdNumTZ: "-0700",
+ stdNumSecondsTz: "-070000",
+ stdNumShortTZ: "-07",
+ stdNumColonTZ: "-07:00",
+ stdNumColonSecondsTZ: "-07:00:00",
+ stdFracSecond0 | 1<<stdArgShift: ".0",
+ stdFracSecond0 | 2<<stdArgShift: ".00",
+ stdFracSecond0 | 3<<stdArgShift: ".000",
+ stdFracSecond0 | 4<<stdArgShift: ".0000",
+ stdFracSecond0 | 5<<stdArgShift: ".00000",
+ stdFracSecond0 | 6<<stdArgShift: ".000000",
+ stdFracSecond0 | 7<<stdArgShift: ".0000000",
+ stdFracSecond0 | 8<<stdArgShift: ".00000000",
+ stdFracSecond0 | 9<<stdArgShift: ".000000000",
+ stdFracSecond9 | 1<<stdArgShift: ".9",
+ stdFracSecond9 | 2<<stdArgShift: ".99",
+ stdFracSecond9 | 3<<stdArgShift: ".999",
+ stdFracSecond9 | 4<<stdArgShift: ".9999",
+ stdFracSecond9 | 5<<stdArgShift: ".99999",
+ stdFracSecond9 | 6<<stdArgShift: ".999999",
+ stdFracSecond9 | 7<<stdArgShift: ".9999999",
+ stdFracSecond9 | 8<<stdArgShift: ".99999999",
+ stdFracSecond9 | 9<<stdArgShift: ".999999999",
+}
// The recognized day of week formats are "Mon" and "Monday".
// The recognized month formats are "Jan" and "January".
//
+// The formats 2, _2, and 02 are unpadded, space-padded, and zero-padded
+// day of month. The formats __2 and 002 are space-padded and zero-padded
+// three-character day of year; there is no unpadded day of year format.
+//
// Text in the format string that is not recognized as part of the reference
// time is echoed verbatim during Format and expected to appear verbatim
// in the input to Parse.
stdDay // "2"
stdUnderDay // "_2"
stdZeroDay // "02"
+ stdUnderYearDay // "__2"
+ stdZeroYearDay // "002"
stdHour = iota + stdNeedClock // "15"
stdHour12 // "3"
stdZeroHour12 // "03"
}
}
- case '0': // 01, 02, 03, 04, 05, 06
+ case '0': // 01, 02, 03, 04, 05, 06, 002
if len(layout) >= i+2 && '1' <= layout[i+1] && layout[i+1] <= '6' {
return layout[0:i], std0x[layout[i+1]-'1'], layout[i+2:]
}
+ if len(layout) >= i+3 && layout[i+1] == '0' && layout[i+2] == '2' {
+ return layout[0:i], stdZeroYearDay, layout[i+3:]
+ }
case '1': // 15, 1
if len(layout) >= i+2 && layout[i+1] == '5' {
}
return layout[0:i], stdDay, layout[i+1:]
- case '_': // _2, _2006
+ case '_': // _2, _2006, __2
if len(layout) >= i+2 && layout[i+1] == '2' {
//_2006 is really a literal _, followed by stdLongYear
if len(layout) >= i+5 && layout[i+1:i+5] == "2006" {
}
return layout[0:i], stdUnderDay, layout[i+2:]
}
+ if len(layout) >= i+3 && layout[i+1] == '_' && layout[i+2] == '2' {
+ return layout[0:i], stdUnderYearDay, layout[i+3:]
+ }
case '3':
return layout[0:i], stdHour12, layout[i+1:]
year int = -1
month Month
day int
+ yday int
hour int = -1
min int
sec int
// Compute year, month, day if needed.
if year < 0 && std&stdNeedDate != 0 {
- year, month, day, _ = absDate(abs, true)
+ year, month, day, yday = absDate(abs, true)
+ yday++
}
// Compute hour, minute, second if needed.
b = appendInt(b, day, 0)
case stdZeroDay:
b = appendInt(b, day, 2)
+ case stdUnderYearDay:
+ if yday < 100 {
+ b = append(b, ' ')
+ if yday < 10 {
+ b = append(b, ' ')
+ }
+ }
+ b = appendInt(b, yday, 0)
+ case stdZeroYearDay:
+ b = appendInt(b, yday, 3)
case stdHour:
b = appendInt(b, hour, 2)
case stdHour12:
return '0' <= c && c <= '9'
}
-// getnum parses s[0:1] or s[0:2] (fixed forces the latter)
+// getnum parses s[0:1] or s[0:2] (fixed forces s[0:2])
// as a decimal integer and returns the integer and the
// remainder of the string.
func getnum(s string, fixed bool) (int, string, error) {
return int(s[0]-'0')*10 + int(s[1]-'0'), s[2:], nil
}
+// getnum3 parses s[0:1], s[0:2], or s[0:3] (fixed forces s[0:3])
+// as a decimal integer and returns the integer and the remainder
+// of the string.
+func getnum3(s string, fixed bool) (int, string, error) {
+ var n, i int
+ for i = 0; i < 3 && isDigit(s, i); i++ {
+ n = n*10 + int(s[i]-'0')
+ }
+ if i == 0 || fixed && i != 3 {
+ return 0, s, errBad
+ }
+ return n, s[i:], nil
+}
+
func cutspace(s string) string {
for len(s) > 0 && s[0] == ' ' {
s = s[1:]
// Time being constructed.
var (
year int
- month int = 1 // January
- day int = 1
+ month int = -1
+ day int = -1
+ yday int = -1
hour int
min int
sec int
value = value[1:]
}
day, value, err = getnum(value, std == stdZeroDay)
- if day < 0 {
- // Note that we allow any one- or two-digit day here.
- rangeErrString = "day"
+ // Note that we allow any one- or two-digit day here.
+ // The month, day, year combination is validated after we've completed parsing.
+ case stdUnderYearDay, stdZeroYearDay:
+ for i := 0; i < 2; i++ {
+ if std == stdUnderYearDay && len(value) > 0 && value[0] == ' ' {
+ value = value[1:]
+ }
}
+ yday, value, err = getnum3(value, std == stdZeroYearDay)
+ // Note that we allow any one-, two-, or three-digit year-day here.
+ // The year-day, year combination is validated after we've completed parsing.
case stdHour:
hour, value, err = getnum(value, false)
if hour < 0 || 24 <= hour {
hour = 0
}
+ // Convert yday to day, month.
+ if yday >= 0 {
+ var d int
+ var m int
+ if isLeap(year) {
+ if yday == 31+29 {
+ m = int(February)
+ d = 29
+ } else if yday > 31+29 {
+ yday--
+ }
+ }
+ if yday < 1 || yday > 365 {
+ return Time{}, &ParseError{alayout, avalue, "", value, ": day-of-year out of range"}
+ }
+ if m == 0 {
+ m = yday/31 + 1
+ if int(daysBefore[m]) < yday {
+ m++
+ }
+ d = yday - int(daysBefore[m-1])
+ }
+ // If month, day already seen, yday's m, d must match.
+ // Otherwise, set them from m, d.
+ if month >= 0 && month != m {
+ return Time{}, &ParseError{alayout, avalue, "", value, ": day-of-year does not match month"}
+ }
+ month = m
+ if day >= 0 && day != d {
+ return Time{}, &ParseError{alayout, avalue, "", value, ": day-of-year does not match day"}
+ }
+ day = d
+ } else {
+ if month < 0 {
+ month = int(January)
+ }
+ if day < 0 {
+ day = 1
+ }
+ }
+
// Validate the day of the month.
if day < 1 || day > daysIn(Month(month), year) {
return Time{}, &ParseError{alayout, avalue, "", value, ": day out of range"}
. "time"
)
+var nextStdChunkTests = []string{
+ "(2006)-(01)-(02)T(15):(04):(05)(Z07:00)",
+ "(2006)-(01)-(02) (002) (15):(04):(05)",
+ "(2006)-(01) (002) (15):(04):(05)",
+ "(2006)-(002) (15):(04):(05)",
+ "(2006)(002)(01) (15):(04):(05)",
+ "(2006)(002)(04) (15):(04):(05)",
+}
+
+func TestNextStdChunk(t *testing.T) {
+ // Most bugs in Parse or Format boil down to problems with
+ // the exact detection of format chunk boundaries in the
+ // helper function nextStdChunk (here called as NextStdChunk).
+ // This test checks nextStdChunk's behavior directly,
+ // instead of needing to test it only indirectly through Parse/Format.
+
+ // markChunks returns format with each detected
+ // 'format chunk' parenthesized.
+ // For example showChunks("2006-01-02") == "(2006)-(01)-(02)".
+ markChunks := func(format string) string {
+ // Note that NextStdChunk and StdChunkNames
+ // are not part of time's public API.
+ // They are exported in export_test for this test.
+ out := ""
+ for s := format; s != ""; {
+ prefix, std, suffix := NextStdChunk(s)
+ out += prefix
+ if std > 0 {
+ out += "(" + StdChunkNames[std] + ")"
+ }
+ s = suffix
+ }
+ return out
+ }
+
+ noParens := func(r rune) rune {
+ if r == '(' || r == ')' {
+ return -1
+ }
+ return r
+ }
+
+ for _, marked := range nextStdChunkTests {
+ // marked is an expected output from markChunks.
+ // If we delete the parens and pass it through markChunks,
+ // we should get the original back.
+ format := strings.Map(noParens, marked)
+ out := markChunks(format)
+ if out != marked {
+ t.Errorf("nextStdChunk parses %q as %q, want %q", format, out, marked)
+ }
+ }
+}
+
type TimeFormatTest struct {
time Time
formattedValue string
{"StampMilli", StampMilli, "Feb 4 21:00:57.012"},
{"StampMicro", StampMicro, "Feb 4 21:00:57.012345"},
{"StampNano", StampNano, "Feb 4 21:00:57.012345600"},
+ {"YearDay", "Jan 2 002 __2 2", "Feb 4 035 35 4"},
}
func TestFormat(t *testing.T) {
{"", "Jan _2 15:04:05.999", "Feb 4 21:00:57.012345678", false, false, -1, 9},
{"", "Jan _2 15:04:05.999999999", "Feb 4 21:00:57.0123", false, false, -1, 4},
{"", "Jan _2 15:04:05.999999999", "Feb 4 21:00:57.012345678", false, false, -1, 9},
+
+ // Day of year.
+ {"", "2006-01-02 002 15:04:05", "2010-02-04 035 21:00:57", false, false, 1, 0},
+ {"", "2006-01 002 15:04:05", "2010-02 035 21:00:57", false, false, 1, 0},
+ {"", "2006-002 15:04:05", "2010-035 21:00:57", false, false, 1, 0},
+ {"", "200600201 15:04:05", "201003502 21:00:57", false, false, 1, 0},
+ {"", "200600204 15:04:05", "201003504 21:00:57", false, false, 1, 0},
}
func TestParse(t *testing.T) {
// issue 21113
{"_2 Jan 06 15:04 MST", "4 --- 00 00:00 GMT", "cannot parse"},
{"_2 January 06 15:04 MST", "4 --- 00 00:00 GMT", "cannot parse"},
+
+ // invalid or mismatched day-of-year
+ {"Jan _2 002 2006", "Feb 4 034 2006", "day-of-year does not match day"},
+ {"Jan _2 002 2006", "Feb 4 004 2006", "day-of-year does not match month"},
}
func TestParseErrors(t *testing.T) {
}
func TestYearDay(t *testing.T) {
- for _, loc := range yearDayLocations {
+ for i, loc := range yearDayLocations {
for _, ydt := range yearDayTests {
dt := Date(ydt.year, Month(ydt.month), ydt.day, 0, 0, 0, 0, loc)
yday := dt.YearDay()
if yday != ydt.yday {
- t.Errorf("got %d, expected %d for %d-%02d-%02d in %v",
- yday, ydt.yday, ydt.year, ydt.month, ydt.day, loc)
+ t.Errorf("Date(%d-%02d-%02d in %v).YearDay() = %d, want %d",
+ ydt.year, ydt.month, ydt.day, loc, yday, ydt.yday)
+ continue
+ }
+
+ if ydt.year < 0 || ydt.year > 9999 {
+ continue
+ }
+ f := fmt.Sprintf("%04d-%02d-%02d %03d %+.2d00",
+ ydt.year, ydt.month, ydt.day, ydt.yday, (i-2)*4)
+ dt1, err := Parse("2006-01-02 002 -0700", f)
+ if err != nil {
+ t.Errorf(`Parse("2006-01-02 002 -0700", %q): %v`, f, err)
+ continue
+ }
+ if !dt1.Equal(dt) {
+ t.Errorf(`Parse("2006-01-02 002 -0700", %q) = %v, want %v`, f, dt1, dt)
}
}
}