Skip to content

Commit 6a9da69

Browse files
committed
time: add support for day-of-year in Format and Parse
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 <[email protected]> TryBot-Result: Gobot Gobot <[email protected]> Reviewed-by: Rob Pike <[email protected]>
1 parent 9586c09 commit 6a9da69

File tree

4 files changed

+237
-12
lines changed

4 files changed

+237
-12
lines changed

src/time/export_test.go

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,4 +35,61 @@ var (
3535
ErrLocation = errLocation
3636
ReadFile = readFile
3737
LoadTzinfo = loadTzinfo
38+
NextStdChunk = nextStdChunk
3839
)
40+
41+
// StdChunkNames maps from nextStdChunk results to the matched strings.
42+
var StdChunkNames = map[int]string{
43+
0: "",
44+
stdLongMonth: "January",
45+
stdMonth: "Jan",
46+
stdNumMonth: "1",
47+
stdZeroMonth: "01",
48+
stdLongWeekDay: "Monday",
49+
stdWeekDay: "Mon",
50+
stdDay: "2",
51+
stdUnderDay: "_2",
52+
stdZeroDay: "02",
53+
stdUnderYearDay: "__2",
54+
stdZeroYearDay: "002",
55+
stdHour: "15",
56+
stdHour12: "3",
57+
stdZeroHour12: "03",
58+
stdMinute: "4",
59+
stdZeroMinute: "04",
60+
stdSecond: "5",
61+
stdZeroSecond: "05",
62+
stdLongYear: "2006",
63+
stdYear: "06",
64+
stdPM: "PM",
65+
stdpm: "pm",
66+
stdTZ: "MST",
67+
stdISO8601TZ: "Z0700",
68+
stdISO8601SecondsTZ: "Z070000",
69+
stdISO8601ShortTZ: "Z07",
70+
stdISO8601ColonTZ: "Z07:00",
71+
stdISO8601ColonSecondsTZ: "Z07:00:00",
72+
stdNumTZ: "-0700",
73+
stdNumSecondsTz: "-070000",
74+
stdNumShortTZ: "-07",
75+
stdNumColonTZ: "-07:00",
76+
stdNumColonSecondsTZ: "-07:00:00",
77+
stdFracSecond0 | 1<<stdArgShift: ".0",
78+
stdFracSecond0 | 2<<stdArgShift: ".00",
79+
stdFracSecond0 | 3<<stdArgShift: ".000",
80+
stdFracSecond0 | 4<<stdArgShift: ".0000",
81+
stdFracSecond0 | 5<<stdArgShift: ".00000",
82+
stdFracSecond0 | 6<<stdArgShift: ".000000",
83+
stdFracSecond0 | 7<<stdArgShift: ".0000000",
84+
stdFracSecond0 | 8<<stdArgShift: ".00000000",
85+
stdFracSecond0 | 9<<stdArgShift: ".000000000",
86+
stdFracSecond9 | 1<<stdArgShift: ".9",
87+
stdFracSecond9 | 2<<stdArgShift: ".99",
88+
stdFracSecond9 | 3<<stdArgShift: ".999",
89+
stdFracSecond9 | 4<<stdArgShift: ".9999",
90+
stdFracSecond9 | 5<<stdArgShift: ".99999",
91+
stdFracSecond9 | 6<<stdArgShift: ".999999",
92+
stdFracSecond9 | 7<<stdArgShift: ".9999999",
93+
stdFracSecond9 | 8<<stdArgShift: ".99999999",
94+
stdFracSecond9 | 9<<stdArgShift: ".999999999",
95+
}

src/time/format.go

Lines changed: 96 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,10 @@ import "errors"
4848
// The recognized day of week formats are "Mon" and "Monday".
4949
// The recognized month formats are "Jan" and "January".
5050
//
51+
// The formats 2, _2, and 02 are unpadded, space-padded, and zero-padded
52+
// day of month. The formats __2 and 002 are space-padded and zero-padded
53+
// three-character day of year; there is no unpadded day of year format.
54+
//
5155
// Text in the format string that is not recognized as part of the reference
5256
// time is echoed verbatim during Format and expected to appear verbatim
5357
// in the input to Parse.
@@ -96,6 +100,8 @@ const (
96100
stdDay // "2"
97101
stdUnderDay // "_2"
98102
stdZeroDay // "02"
103+
stdUnderYearDay // "__2"
104+
stdZeroYearDay // "002"
99105
stdHour = iota + stdNeedClock // "15"
100106
stdHour12 // "3"
101107
stdZeroHour12 // "03"
@@ -170,10 +176,13 @@ func nextStdChunk(layout string) (prefix string, std int, suffix string) {
170176
}
171177
}
172178

173-
case '0': // 01, 02, 03, 04, 05, 06
179+
case '0': // 01, 02, 03, 04, 05, 06, 002
174180
if len(layout) >= i+2 && '1' <= layout[i+1] && layout[i+1] <= '6' {
175181
return layout[0:i], std0x[layout[i+1]-'1'], layout[i+2:]
176182
}
183+
if len(layout) >= i+3 && layout[i+1] == '0' && layout[i+2] == '2' {
184+
return layout[0:i], stdZeroYearDay, layout[i+3:]
185+
}
177186

178187
case '1': // 15, 1
179188
if len(layout) >= i+2 && layout[i+1] == '5' {
@@ -187,14 +196,17 @@ func nextStdChunk(layout string) (prefix string, std int, suffix string) {
187196
}
188197
return layout[0:i], stdDay, layout[i+1:]
189198

190-
case '_': // _2, _2006
199+
case '_': // _2, _2006, __2
191200
if len(layout) >= i+2 && layout[i+1] == '2' {
192201
//_2006 is really a literal _, followed by stdLongYear
193202
if len(layout) >= i+5 && layout[i+1:i+5] == "2006" {
194203
return layout[0 : i+1], stdLongYear, layout[i+5:]
195204
}
196205
return layout[0:i], stdUnderDay, layout[i+2:]
197206
}
207+
if len(layout) >= i+3 && layout[i+1] == '_' && layout[i+2] == '2' {
208+
return layout[0:i], stdUnderYearDay, layout[i+3:]
209+
}
198210

199211
case '3':
200212
return layout[0:i], stdHour12, layout[i+1:]
@@ -503,6 +515,7 @@ func (t Time) AppendFormat(b []byte, layout string) []byte {
503515
year int = -1
504516
month Month
505517
day int
518+
yday int
506519
hour int = -1
507520
min int
508521
sec int
@@ -520,7 +533,8 @@ func (t Time) AppendFormat(b []byte, layout string) []byte {
520533

521534
// Compute year, month, day if needed.
522535
if year < 0 && std&stdNeedDate != 0 {
523-
year, month, day, _ = absDate(abs, true)
536+
year, month, day, yday = absDate(abs, true)
537+
yday++
524538
}
525539

526540
// Compute hour, minute, second if needed.
@@ -560,6 +574,16 @@ func (t Time) AppendFormat(b []byte, layout string) []byte {
560574
b = appendInt(b, day, 0)
561575
case stdZeroDay:
562576
b = appendInt(b, day, 2)
577+
case stdUnderYearDay:
578+
if yday < 100 {
579+
b = append(b, ' ')
580+
if yday < 10 {
581+
b = append(b, ' ')
582+
}
583+
}
584+
b = appendInt(b, yday, 0)
585+
case stdZeroYearDay:
586+
b = appendInt(b, yday, 3)
563587
case stdHour:
564588
b = appendInt(b, hour, 2)
565589
case stdHour12:
@@ -688,7 +712,7 @@ func isDigit(s string, i int) bool {
688712
return '0' <= c && c <= '9'
689713
}
690714

691-
// getnum parses s[0:1] or s[0:2] (fixed forces the latter)
715+
// getnum parses s[0:1] or s[0:2] (fixed forces s[0:2])
692716
// as a decimal integer and returns the integer and the
693717
// remainder of the string.
694718
func getnum(s string, fixed bool) (int, string, error) {
@@ -704,6 +728,20 @@ func getnum(s string, fixed bool) (int, string, error) {
704728
return int(s[0]-'0')*10 + int(s[1]-'0'), s[2:], nil
705729
}
706730

731+
// getnum3 parses s[0:1], s[0:2], or s[0:3] (fixed forces s[0:3])
732+
// as a decimal integer and returns the integer and the remainder
733+
// of the string.
734+
func getnum3(s string, fixed bool) (int, string, error) {
735+
var n, i int
736+
for i = 0; i < 3 && isDigit(s, i); i++ {
737+
n = n*10 + int(s[i]-'0')
738+
}
739+
if i == 0 || fixed && i != 3 {
740+
return 0, s, errBad
741+
}
742+
return n, s[i:], nil
743+
}
744+
707745
func cutspace(s string) string {
708746
for len(s) > 0 && s[0] == ' ' {
709747
s = s[1:]
@@ -792,8 +830,9 @@ func parse(layout, value string, defaultLocation, local *Location) (Time, error)
792830
// Time being constructed.
793831
var (
794832
year int
795-
month int = 1 // January
796-
day int = 1
833+
month int = -1
834+
day int = -1
835+
yday int = -1
797836
hour int
798837
min int
799838
sec int
@@ -861,10 +900,17 @@ func parse(layout, value string, defaultLocation, local *Location) (Time, error)
861900
value = value[1:]
862901
}
863902
day, value, err = getnum(value, std == stdZeroDay)
864-
if day < 0 {
865-
// Note that we allow any one- or two-digit day here.
866-
rangeErrString = "day"
903+
// Note that we allow any one- or two-digit day here.
904+
// The month, day, year combination is validated after we've completed parsing.
905+
case stdUnderYearDay, stdZeroYearDay:
906+
for i := 0; i < 2; i++ {
907+
if std == stdUnderYearDay && len(value) > 0 && value[0] == ' ' {
908+
value = value[1:]
909+
}
867910
}
911+
yday, value, err = getnum3(value, std == stdZeroYearDay)
912+
// Note that we allow any one-, two-, or three-digit year-day here.
913+
// The year-day, year combination is validated after we've completed parsing.
868914
case stdHour:
869915
hour, value, err = getnum(value, false)
870916
if hour < 0 || 24 <= hour {
@@ -1044,6 +1090,47 @@ func parse(layout, value string, defaultLocation, local *Location) (Time, error)
10441090
hour = 0
10451091
}
10461092

1093+
// Convert yday to day, month.
1094+
if yday >= 0 {
1095+
var d int
1096+
var m int
1097+
if isLeap(year) {
1098+
if yday == 31+29 {
1099+
m = int(February)
1100+
d = 29
1101+
} else if yday > 31+29 {
1102+
yday--
1103+
}
1104+
}
1105+
if yday < 1 || yday > 365 {
1106+
return Time{}, &ParseError{alayout, avalue, "", value, ": day-of-year out of range"}
1107+
}
1108+
if m == 0 {
1109+
m = yday/31 + 1
1110+
if int(daysBefore[m]) < yday {
1111+
m++
1112+
}
1113+
d = yday - int(daysBefore[m-1])
1114+
}
1115+
// If month, day already seen, yday's m, d must match.
1116+
// Otherwise, set them from m, d.
1117+
if month >= 0 && month != m {
1118+
return Time{}, &ParseError{alayout, avalue, "", value, ": day-of-year does not match month"}
1119+
}
1120+
month = m
1121+
if day >= 0 && day != d {
1122+
return Time{}, &ParseError{alayout, avalue, "", value, ": day-of-year does not match day"}
1123+
}
1124+
day = d
1125+
} else {
1126+
if month < 0 {
1127+
month = int(January)
1128+
}
1129+
if day < 0 {
1130+
day = 1
1131+
}
1132+
}
1133+
10471134
// Validate the day of the month.
10481135
if day < 1 || day > daysIn(Month(month), year) {
10491136
return Time{}, &ParseError{alayout, avalue, "", value, ": day out of range"}

src/time/format_test.go

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,60 @@ import (
1313
. "time"
1414
)
1515

16+
var nextStdChunkTests = []string{
17+
"(2006)-(01)-(02)T(15):(04):(05)(Z07:00)",
18+
"(2006)-(01)-(02) (002) (15):(04):(05)",
19+
"(2006)-(01) (002) (15):(04):(05)",
20+
"(2006)-(002) (15):(04):(05)",
21+
"(2006)(002)(01) (15):(04):(05)",
22+
"(2006)(002)(04) (15):(04):(05)",
23+
}
24+
25+
func TestNextStdChunk(t *testing.T) {
26+
// Most bugs in Parse or Format boil down to problems with
27+
// the exact detection of format chunk boundaries in the
28+
// helper function nextStdChunk (here called as NextStdChunk).
29+
// This test checks nextStdChunk's behavior directly,
30+
// instead of needing to test it only indirectly through Parse/Format.
31+
32+
// markChunks returns format with each detected
33+
// 'format chunk' parenthesized.
34+
// For example showChunks("2006-01-02") == "(2006)-(01)-(02)".
35+
markChunks := func(format string) string {
36+
// Note that NextStdChunk and StdChunkNames
37+
// are not part of time's public API.
38+
// They are exported in export_test for this test.
39+
out := ""
40+
for s := format; s != ""; {
41+
prefix, std, suffix := NextStdChunk(s)
42+
out += prefix
43+
if std > 0 {
44+
out += "(" + StdChunkNames[std] + ")"
45+
}
46+
s = suffix
47+
}
48+
return out
49+
}
50+
51+
noParens := func(r rune) rune {
52+
if r == '(' || r == ')' {
53+
return -1
54+
}
55+
return r
56+
}
57+
58+
for _, marked := range nextStdChunkTests {
59+
// marked is an expected output from markChunks.
60+
// If we delete the parens and pass it through markChunks,
61+
// we should get the original back.
62+
format := strings.Map(noParens, marked)
63+
out := markChunks(format)
64+
if out != marked {
65+
t.Errorf("nextStdChunk parses %q as %q, want %q", format, out, marked)
66+
}
67+
}
68+
}
69+
1670
type TimeFormatTest struct {
1771
time Time
1872
formattedValue string
@@ -61,6 +115,7 @@ var formatTests = []FormatTest{
61115
{"StampMilli", StampMilli, "Feb 4 21:00:57.012"},
62116
{"StampMicro", StampMicro, "Feb 4 21:00:57.012345"},
63117
{"StampNano", StampNano, "Feb 4 21:00:57.012345600"},
118+
{"YearDay", "Jan 2 002 __2 2", "Feb 4 035 35 4"},
64119
}
65120

66121
func TestFormat(t *testing.T) {
@@ -180,6 +235,13 @@ var parseTests = []ParseTest{
180235
{"", "Jan _2 15:04:05.999", "Feb 4 21:00:57.012345678", false, false, -1, 9},
181236
{"", "Jan _2 15:04:05.999999999", "Feb 4 21:00:57.0123", false, false, -1, 4},
182237
{"", "Jan _2 15:04:05.999999999", "Feb 4 21:00:57.012345678", false, false, -1, 9},
238+
239+
// Day of year.
240+
{"", "2006-01-02 002 15:04:05", "2010-02-04 035 21:00:57", false, false, 1, 0},
241+
{"", "2006-01 002 15:04:05", "2010-02 035 21:00:57", false, false, 1, 0},
242+
{"", "2006-002 15:04:05", "2010-035 21:00:57", false, false, 1, 0},
243+
{"", "200600201 15:04:05", "201003502 21:00:57", false, false, 1, 0},
244+
{"", "200600204 15:04:05", "201003504 21:00:57", false, false, 1, 0},
183245
}
184246

185247
func TestParse(t *testing.T) {
@@ -485,6 +547,10 @@ var parseErrorTests = []ParseErrorTest{
485547
// issue 21113
486548
{"_2 Jan 06 15:04 MST", "4 --- 00 00:00 GMT", "cannot parse"},
487549
{"_2 January 06 15:04 MST", "4 --- 00 00:00 GMT", "cannot parse"},
550+
551+
// invalid or mismatched day-of-year
552+
{"Jan _2 002 2006", "Feb 4 034 2006", "day-of-year does not match day"},
553+
{"Jan _2 002 2006", "Feb 4 004 2006", "day-of-year does not match month"},
488554
}
489555

490556
func TestParseErrors(t *testing.T) {

src/time/time_test.go

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -522,13 +522,28 @@ var yearDayLocations = []*Location{
522522
}
523523

524524
func TestYearDay(t *testing.T) {
525-
for _, loc := range yearDayLocations {
525+
for i, loc := range yearDayLocations {
526526
for _, ydt := range yearDayTests {
527527
dt := Date(ydt.year, Month(ydt.month), ydt.day, 0, 0, 0, 0, loc)
528528
yday := dt.YearDay()
529529
if yday != ydt.yday {
530-
t.Errorf("got %d, expected %d for %d-%02d-%02d in %v",
531-
yday, ydt.yday, ydt.year, ydt.month, ydt.day, loc)
530+
t.Errorf("Date(%d-%02d-%02d in %v).YearDay() = %d, want %d",
531+
ydt.year, ydt.month, ydt.day, loc, yday, ydt.yday)
532+
continue
533+
}
534+
535+
if ydt.year < 0 || ydt.year > 9999 {
536+
continue
537+
}
538+
f := fmt.Sprintf("%04d-%02d-%02d %03d %+.2d00",
539+
ydt.year, ydt.month, ydt.day, ydt.yday, (i-2)*4)
540+
dt1, err := Parse("2006-01-02 002 -0700", f)
541+
if err != nil {
542+
t.Errorf(`Parse("2006-01-02 002 -0700", %q): %v`, f, err)
543+
continue
544+
}
545+
if !dt1.Equal(dt) {
546+
t.Errorf(`Parse("2006-01-02 002 -0700", %q) = %v, want %v`, f, dt1, dt)
532547
}
533548
}
534549
}

0 commit comments

Comments
 (0)