diff options
Diffstat (limited to 'src/time/format_rfc3339.go')
-rw-r--r-- | src/time/format_rfc3339.go | 188 |
1 files changed, 188 insertions, 0 deletions
diff --git a/src/time/format_rfc3339.go b/src/time/format_rfc3339.go new file mode 100644 index 0000000..1151666 --- /dev/null +++ b/src/time/format_rfc3339.go @@ -0,0 +1,188 @@ +// Copyright 2022 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 + +import "errors" + +// RFC 3339 is the most commonly used format. +// +// It is implicitly used by the Time.(Marshal|Unmarshal)(Text|JSON) methods. +// Also, according to analysis on https://go.dev/issue/52746, +// RFC 3339 accounts for 57% of all explicitly specified time formats, +// with the second most popular format only being used 8% of the time. +// The overwhelming use of RFC 3339 compared to all other formats justifies +// the addition of logic to optimize formatting and parsing. + +func (t Time) appendFormatRFC3339(b []byte, nanos bool) []byte { + _, offset, abs := t.locabs() + + // Format date. + year, month, day, _ := absDate(abs, true) + b = appendInt(b, year, 4) + b = append(b, '-') + b = appendInt(b, int(month), 2) + b = append(b, '-') + b = appendInt(b, day, 2) + + b = append(b, 'T') + + // Format time. + hour, min, sec := absClock(abs) + b = appendInt(b, hour, 2) + b = append(b, ':') + b = appendInt(b, min, 2) + b = append(b, ':') + b = appendInt(b, sec, 2) + + if nanos { + std := stdFracSecond(stdFracSecond9, 9, '.') + b = appendNano(b, t.Nanosecond(), std) + } + + if offset == 0 { + return append(b, 'Z') + } + + // Format zone. + zone := offset / 60 // convert to minutes + if zone < 0 { + b = append(b, '-') + zone = -zone + } else { + b = append(b, '+') + } + b = appendInt(b, zone/60, 2) + b = append(b, ':') + b = appendInt(b, zone%60, 2) + return b +} + +func (t Time) appendStrictRFC3339(b []byte) ([]byte, error) { + n0 := len(b) + b = t.appendFormatRFC3339(b, true) + + // Not all valid Go timestamps can be serialized as valid RFC 3339. + // Explicitly check for these edge cases. + // See https://go.dev/issue/4556 and https://go.dev/issue/54580. + num2 := func(b []byte) byte { return 10*(b[0]-'0') + (b[1] - '0') } + switch { + case b[n0+len("9999")] != '-': // year must be exactly 4 digits wide + return b, errors.New("year outside of range [0,9999]") + case b[len(b)-1] != 'Z': + c := b[len(b)-len("Z07:00")] + if ('0' <= c && c <= '9') || num2(b[len(b)-len("07:00"):]) >= 24 { + return b, errors.New("timezone hour outside of range [0,23]") + } + } + return b, nil +} + +func parseRFC3339[bytes []byte | string](s bytes, local *Location) (Time, bool) { + // parseUint parses s as an unsigned decimal integer and + // verifies that it is within some range. + // If it is invalid or out-of-range, + // it sets ok to false and returns the min value. + ok := true + parseUint := func(s bytes, min, max int) (x int) { + for _, c := range []byte(s) { + if c < '0' || '9' < c { + ok = false + return min + } + x = x*10 + int(c) - '0' + } + if x < min || max < x { + ok = false + return min + } + return x + } + + // Parse the date and time. + if len(s) < len("2006-01-02T15:04:05") { + return Time{}, false + } + year := parseUint(s[0:4], 0, 9999) // e.g., 2006 + month := parseUint(s[5:7], 1, 12) // e.g., 01 + day := parseUint(s[8:10], 1, daysIn(Month(month), year)) // e.g., 02 + hour := parseUint(s[11:13], 0, 23) // e.g., 15 + min := parseUint(s[14:16], 0, 59) // e.g., 04 + sec := parseUint(s[17:19], 0, 59) // e.g., 05 + if !ok || !(s[4] == '-' && s[7] == '-' && s[10] == 'T' && s[13] == ':' && s[16] == ':') { + return Time{}, false + } + s = s[19:] + + // Parse the fractional second. + var nsec int + if len(s) >= 2 && s[0] == '.' && isDigit(s, 1) { + n := 2 + for ; n < len(s) && isDigit(s, n); n++ { + } + nsec, _, _ = parseNanoseconds(s, n) + s = s[n:] + } + + // Parse the time zone. + t := Date(year, Month(month), day, hour, min, sec, nsec, UTC) + if len(s) != 1 || s[0] != 'Z' { + if len(s) != len("-07:00") { + return Time{}, false + } + hr := parseUint(s[1:3], 0, 23) // e.g., 07 + mm := parseUint(s[4:6], 0, 59) // e.g., 00 + if !ok || !((s[0] == '-' || s[0] == '+') && s[3] == ':') { + return Time{}, false + } + zoneOffset := (hr*60 + mm) * 60 + if s[0] == '-' { + zoneOffset *= -1 + } + t.addSec(-int64(zoneOffset)) + + // Use local zone with the given offset if possible. + if _, offset, _, _, _ := local.lookup(t.unixSec()); offset == zoneOffset { + t.setLoc(local) + } else { + t.setLoc(FixedZone("", zoneOffset)) + } + } + return t, true +} + +func parseStrictRFC3339(b []byte) (Time, error) { + t, ok := parseRFC3339(b, Local) + if !ok { + t, err := Parse(RFC3339, string(b)) + if err != nil { + return Time{}, err + } + + // The parse template syntax cannot correctly validate RFC 3339. + // Explicitly check for cases that Parse is unable to validate for. + // See https://go.dev/issue/54580. + num2 := func(b []byte) byte { return 10*(b[0]-'0') + (b[1] - '0') } + switch { + // TODO(https://go.dev/issue/54580): Strict parsing is disabled for now. + // Enable this again with a GODEBUG opt-out. + case true: + return t, nil + case b[len("2006-01-02T")+1] == ':': // hour must be two digits + return Time{}, &ParseError{RFC3339, string(b), "15", string(b[len("2006-01-02T"):][:1]), ""} + case b[len("2006-01-02T15:04:05")] == ',': // sub-second separator must be a period + return Time{}, &ParseError{RFC3339, string(b), ".", ",", ""} + case b[len(b)-1] != 'Z': + switch { + case num2(b[len(b)-len("07:00"):]) >= 24: // timezone hour must be in range + return Time{}, &ParseError{RFC3339, string(b), "Z07:00", string(b[len(b)-len("Z07:00"):]), ": timezone hour out of range"} + case num2(b[len(b)-len("00"):]) >= 60: // timezone minute must be in range + return Time{}, &ParseError{RFC3339, string(b), "Z07:00", string(b[len(b)-len("Z07:00"):]), ": timezone minute out of range"} + } + default: // unknown error; should not occur + return Time{}, &ParseError{RFC3339, string(b), RFC3339, string(b), ""} + } + } + return t, nil +} |