time: optimize parsing of common fixed-width layouts
Add specialized parsing paths for the exact fixed-width forms of DateTime, DateOnly, and TimeOnly. Values outside those forms continue through the general parser, preserving fractional-second handling and existing errors.
On linux/amd64, representative results are:
ParseDateTime 70.8 ns/op -> 22.2 ns/op (-69%)
ParseDateOnly 42.4 ns/op -> 14.6 ns/op (-66%)
ParseTimeOnly 37.2 ns/op -> 14.5 ns/op (-61%)
All paths remain allocation-free. Parsing unrelated layouts and the existing RFC3339 fast paths is unchanged within benchmark noise.
Add differential tests and fuzz coverage comparing the specialized paths with the general parser.
Fixes #80437
diff --git a/src/time/format.go b/src/time/format.go
index cf5d6c7..f710ecf 100644
--- a/src/time/format.go
+++ b/src/time/format.go
@@ -1024,11 +1024,24 @@
// differ by the actual zone offset. To avoid such problems, prefer time layouts
// that use a numeric zone offset, or use [ParseInLocation].
func Parse(layout, value string) (Time, error) {
- // Optimize for RFC3339 as it accounts for over half of all representations.
- if layout == RFC3339 || layout == RFC3339Nano {
+ switch layout {
+ case RFC3339, RFC3339Nano:
+ // Optimize for RFC3339 as it accounts for over half of all representations.
if t, ok := parseRFC3339(value, Local); ok {
return t, nil
}
+ case DateTime:
+ if t, ok := parseDateTime(value, UTC); ok {
+ return t, nil
+ }
+ case DateOnly:
+ if t, ok := parseDateOnly(value, UTC); ok {
+ return t, nil
+ }
+ case TimeOnly:
+ if t, ok := parseTimeOnly(value, UTC); ok {
+ return t, nil
+ }
}
return parse(layout, value, UTC, Local)
}
@@ -1039,11 +1052,24 @@
// Second, when given a zone offset or abbreviation, Parse tries to match it
// against the Local location; ParseInLocation uses the given location.
func ParseInLocation(layout, value string, loc *Location) (Time, error) {
- // Optimize for RFC3339 as it accounts for over half of all representations.
- if layout == RFC3339 || layout == RFC3339Nano {
+ switch layout {
+ case RFC3339, RFC3339Nano:
+ // Optimize for RFC3339 as it accounts for over half of all representations.
if t, ok := parseRFC3339(value, loc); ok {
return t, nil
}
+ case DateTime:
+ if t, ok := parseDateTime(value, loc); ok {
+ return t, nil
+ }
+ case DateOnly:
+ if t, ok := parseDateOnly(value, loc); ok {
+ return t, nil
+ }
+ case TimeOnly:
+ if t, ok := parseTimeOnly(value, loc); ok {
+ return t, nil
+ }
}
return parse(layout, value, loc, loc)
}
diff --git a/src/time/format_datetime.go b/src/time/format_datetime.go
new file mode 100644
index 0000000..9d1cbda
--- /dev/null
+++ b/src/time/format_datetime.go
@@ -0,0 +1,92 @@
+// Copyright 2026 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package time
+
+// These parsers optimize the exact fixed-width forms of DateTime, DateOnly,
+// and TimeOnly. Parse and ParseInLocation fall back to the general parser for
+// other accepted forms, such as times with fractional seconds.
+
+func parseDateTime(value string, loc *Location) (Time, bool) {
+ if len(value) != len(DateTime) ||
+ value[4] != '-' || value[7] != '-' || value[10] != ' ' ||
+ value[13] != ':' || value[16] != ':' {
+ return Time{}, false
+ }
+ year, month, day, ok := parseDateFields(value)
+ if !ok {
+ return Time{}, false
+ }
+ hour, min, sec, ok := parseTimeFields(value[11:])
+ if !ok {
+ return Time{}, false
+ }
+ return Date(year, month, day, hour, min, sec, 0, loc), true
+}
+
+func parseDateOnly(value string, loc *Location) (Time, bool) {
+ if len(value) != len(DateOnly) || value[4] != '-' || value[7] != '-' {
+ return Time{}, false
+ }
+ year, month, day, ok := parseDateFields(value)
+ if !ok {
+ return Time{}, false
+ }
+ return Date(year, month, day, 0, 0, 0, 0, loc), true
+}
+
+func parseTimeOnly(value string, loc *Location) (Time, bool) {
+ if len(value) != len(TimeOnly) || value[2] != ':' || value[5] != ':' {
+ return Time{}, false
+ }
+ hour, min, sec, ok := parseTimeFields(value)
+ if !ok {
+ return Time{}, false
+ }
+ return Date(0, January, 1, hour, min, sec, 0, loc), true
+}
+
+func parseDateFields(value string) (year int, month Month, day int, ok bool) {
+ year, ok = parseFixedDigits(value[:4])
+ if !ok {
+ return 0, 0, 0, false
+ }
+ monthNumber, ok := parseFixedDigits(value[5:7])
+ if !ok || monthNumber < 1 || monthNumber > 12 {
+ return 0, 0, 0, false
+ }
+ month = Month(monthNumber)
+ day, ok = parseFixedDigits(value[8:10])
+ if !ok || day < 1 || day > daysIn(month, year) {
+ return 0, 0, 0, false
+ }
+ return year, month, day, true
+}
+
+func parseTimeFields(value string) (hour, min, sec int, ok bool) {
+ hour, ok = parseFixedDigits(value[:2])
+ if !ok || hour >= 24 {
+ return 0, 0, 0, false
+ }
+ min, ok = parseFixedDigits(value[3:5])
+ if !ok || min >= 60 {
+ return 0, 0, 0, false
+ }
+ sec, ok = parseFixedDigits(value[6:8])
+ if !ok || sec >= 60 {
+ return 0, 0, 0, false
+ }
+ return hour, min, sec, true
+}
+
+func parseFixedDigits(value string) (int, bool) {
+ var n int
+ for i := range len(value) {
+ if value[i] < '0' || value[i] > '9' {
+ return 0, false
+ }
+ n = n*10 + int(value[i]-'0')
+ }
+ return n, true
+}
diff --git a/src/time/format_test.go b/src/time/format_test.go
index 2537c76..3f87c84 100644
--- a/src/time/format_test.go
+++ b/src/time/format_test.go
@@ -366,6 +366,62 @@
}
}
+var commonParseLocation = FixedZone("Common", 4*60*60+30*60)
+
+func testParseCommonLayout(t *testing.T, layout, value string) {
+ t.Helper()
+
+ check := func(name string, got Time, gotErr error, want Time, wantErr error) {
+ t.Helper()
+ switch {
+ case (gotErr == nil) != (wantErr == nil):
+ t.Fatalf("%s(%q, %q) error mismatch:\n\tgot: %v\n\twant: %v", name, layout, value, gotErr, wantErr)
+ case gotErr != nil && gotErr.Error() != wantErr.Error():
+ t.Fatalf("%s(%q, %q) error mismatch:\n\tgot: %v\n\twant: %v", name, layout, value, gotErr, wantErr)
+ case got != want:
+ t.Fatalf("%s(%q, %q) value mismatch:\n\tgot: %v\n\twant: %v", name, layout, value, got, want)
+ }
+ }
+
+ got, gotErr := Parse(layout, value)
+ want, wantErr := ParseAny(layout, value, UTC, Local)
+ check("Parse", got, gotErr, want, wantErr)
+
+ got, gotErr = ParseInLocation(layout, value, commonParseLocation)
+ want, wantErr = ParseAny(layout, value, commonParseLocation, commonParseLocation)
+ check("ParseInLocation", got, gotErr, want, wantErr)
+}
+
+func TestParseCommonLayouts(t *testing.T) {
+ tests := []struct {
+ layout string
+ value string
+ }{
+ {DateTime, "0000-01-01 00:00:00"},
+ {DateTime, "2000-02-29 23:59:59"},
+ {DateTime, "9999-12-31 23:59:59"},
+ {DateTime, "2000-02-29 23:59:59.123456789"},
+ {DateTime, "2000-02-29 23:59:59"},
+ {DateTime, "2001-02-29 23:59:59"},
+ {DateTime, "2000-02-29T23:59:59"},
+ {DateOnly, "0000-01-01"},
+ {DateOnly, "2000-02-29"},
+ {DateOnly, "9999-12-31"},
+ {DateOnly, "2001-02-29"},
+ {DateOnly, "2000/02/29"},
+ {TimeOnly, "00:00:00"},
+ {TimeOnly, "23:59:59"},
+ {TimeOnly, "23:59:59.123456789"},
+ {TimeOnly, "24:00:00"},
+ {TimeOnly, "23-59-59"},
+ }
+ for _, test := range tests {
+ t.Run(test.layout+"/"+test.value, func(t *testing.T) {
+ testParseCommonLayout(t, test.layout, test.value)
+ })
+ }
+}
+
// All parsed with ANSIC.
var dayOutOfRangeTests = []struct {
date string
@@ -1091,3 +1147,17 @@
}
})
}
+
+func FuzzParseCommonLayouts(f *testing.F) {
+ f.Add(uint8(0), "2000-02-29 23:59:59")
+ f.Add(uint8(0), "2000-02-29 23:59:59.123456789")
+ f.Add(uint8(1), "2000-02-29")
+ f.Add(uint8(1), "2001-02-29")
+ f.Add(uint8(2), "23:59:59")
+ f.Add(uint8(2), "23:59:59.123456789")
+
+ f.Fuzz(func(t *testing.T, layoutIndex uint8, value string) {
+ layouts := [...]string{DateTime, DateOnly, TimeOnly}
+ testParseCommonLayout(t, layouts[int(layoutIndex)%len(layouts)], value)
+ })
+}
diff --git a/src/time/time_test.go b/src/time/time_test.go
index 5cd7471..4dcea6a 100644
--- a/src/time/time_test.go
+++ b/src/time/time_test.go
@@ -1581,6 +1581,24 @@
}
}
+func BenchmarkParseDateTime(b *testing.B) {
+ for i := 0; i < b.N; i++ {
+ Parse(DateTime, "2026-07-16 16:42:31")
+ }
+}
+
+func BenchmarkParseDateOnly(b *testing.B) {
+ for i := 0; i < b.N; i++ {
+ Parse(DateOnly, "2026-07-16")
+ }
+}
+
+func BenchmarkParseTimeOnly(b *testing.B) {
+ for i := 0; i < b.N; i++ {
+ Parse(TimeOnly, "16:42:31")
+ }
+}
+
const testdataRFC3339UTC = "2020-08-22T11:27:43.123456789Z"
func BenchmarkParseRFC3339UTC(b *testing.B) {
| Inspect html for hidden footers to help with email filtering. To unsubscribe visit settings. |
Add specialized parsing paths for the exact fixed-width forms of DateTime, DateOnly, and TimeOnly. Values outside those forms continue through the general parser, preserving fractional-second handling and existing errors.ParseDateTime 70.8 ns/op -> 22.2 ns/op (-69%)// These parsers optimize the exact fixed-width forms of DateTime, DateOnly,given that these share the same general format, shouldn't it be possible to share code with parseRFC3339 ?
| Inspect html for hidden footers to help with email filtering. To unsubscribe visit settings. |
| Inspect html for hidden footers to help with email filtering. To unsubscribe visit settings. |
Add specialized parsing paths for the exact fixed-width forms ofDone. Wrapped the commit message at 72 columns.
Done. Replaced the hand-written benchmark summary with fresh benchstat output using 10 samples per side.
// These parsers optimize the exact fixed-width forms of DateTime, DateOnly,Done. Added a generic fixed-width date/time field parser shared by DateTime and parseRFC3339, while keeping the subset parsers specialized to avoid regressing the existing RFC3339 fast path.
| Inspect html for hidden footers to help with email filtering. To unsubscribe visit settings. |
| Commit-Queue | +1 |
ParseTimeOnly 35.92n ± 2% 13.90n ± 3% -61.30% (p=0.000 n=10)This should also include the RFC3339 benchmarks since the implementation changes.
and include -benchmem.
func parseDateTimeFields[bytes []byte | string](value bytes) (year int, month Month, day, hour, min, sec int, ok bool) {why can't this call the other 2 funcs instead of duplicating the code?
| Inspect html for hidden footers to help with email filtering. To unsubscribe visit settings. |