p1: /v2/scrape, /v1/fetch, auth, CLI, contract tests

Single Fetcher.Fetch on the Guard that holds DenyPrefixes. skipTlsVerification
is ignored. No 502. keys add writes the secret only to --out. Release binary
has no -test-allow-prefix.
This commit is contained in:
2026-09-22 21:54:01 -07:00
parent 6cb206fb27
commit 9444a35322
311 changed files with 238557 additions and 92 deletions
+37
View File
@@ -0,0 +1,37 @@
# Changelog
## [v0.1.8](https://github.com/itchyny/timefmt-go/compare/v0.1.7..v0.1.8) (2026-04-01)
* fix parsing negative year and Unix time (`%Y`, `%G`, `%s`)
* fix formatting negative year, century, Unix time (`%Y`, `%G`, `%C`, `%y`, `%g`, `%s`)
* fix `%g` parsing to use the same two-digit year threshold 69 as `%y`
* fix `%s` formatting and parsing on 32-bit platforms
* support parsing time zone offset with `%:::z`
* improve performance of parsing/formatting compound directives
## [v0.1.7](https://github.com/itchyny/timefmt-go/compare/v0.1.6..v0.1.7) (2025-10-01)
* refactor code using built-in `min` and `max` functions
## [v0.1.6](https://github.com/itchyny/timefmt-go/compare/v0.1.5..v0.1.6) (2024-06-01)
* support parsing week directives (`%A`, `%a`, `%w`, `%u`, `%V`, `%U`, `%W`)
* validate range of values on parsing directives
* fix formatting `%l` to show `12` at midnight
## [v0.1.5](https://github.com/itchyny/timefmt-go/compare/v0.1.4..v0.1.5) (2022-12-01)
* support parsing time zone offset with name using both `%z` and `%Z`
## [v0.1.4](https://github.com/itchyny/timefmt-go/compare/v0.1.3..v0.1.4) (2022-09-01)
* improve documents
* drop support for Go 1.16
## [v0.1.3](https://github.com/itchyny/timefmt-go/compare/v0.1.2..v0.1.3) (2021-04-14)
* implement `ParseInLocation` for configuring the default location
## [v0.1.2](https://github.com/itchyny/timefmt-go/compare/v0.1.1..v0.1.2) (2021-02-22)
* implement parsing/formatting time zone offset with colons (`%:z`, `%::z`, `%:::z`)
* recognize `Z` as UTC on parsing time zone offset (`%z`)
* fix padding on formatting time zone offset (`%z`)
## [v0.1.1](https://github.com/itchyny/timefmt-go/compare/v0.1.0..v0.1.1) (2020-09-01)
* fix overflow check in 32-bit architecture
## [v0.1.0](https://github.com/itchyny/timefmt-go/compare/2c02364..v0.1.0) (2020-08-16)
* initial implementation
+21
View File
@@ -0,0 +1,21 @@
The MIT License (MIT)
Copyright (c) 2020-2025 itchyny
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+20
View File
@@ -0,0 +1,20 @@
GOBIN ?= $(shell go env GOPATH)/bin
.PHONY: all
all: test
.PHONY: test
test:
go test -v -race ./...
.PHONY: lint
lint: $(GOBIN)/staticcheck
go vet ./...
staticcheck -checks all,-ST1000 ./...
$(GOBIN)/staticcheck:
go install honnef.co/go/tools/cmd/staticcheck@latest
.PHONY: clean
clean:
go clean
+69
View File
@@ -0,0 +1,69 @@
# timefmt-go
[![CI Status](https://github.com/itchyny/timefmt-go/actions/workflows/ci.yaml/badge.svg?branch=main)](https://github.com/itchyny/timefmt-go/actions?query=branch:main)
[![Go Report Card](https://goreportcard.com/badge/github.com/itchyny/timefmt-go)](https://goreportcard.com/report/github.com/itchyny/timefmt-go)
[![MIT License](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/itchyny/timefmt-go/blob/main/LICENSE)
[![release](https://img.shields.io/github/release/itchyny/timefmt-go/all.svg)](https://github.com/itchyny/timefmt-go/releases)
[![pkg.go.dev](https://pkg.go.dev/badge/github.com/itchyny/timefmt-go)](https://pkg.go.dev/github.com/itchyny/timefmt-go)
### Efficient time formatting library (strftime, strptime) for Golang
This is a Go language package for formatting and parsing date time strings.
```go
package main
import (
"fmt"
"log"
"github.com/itchyny/timefmt-go"
)
func main() {
t, err := timefmt.Parse("2020/07/24 09:07:29", "%Y/%m/%d %H:%M:%S")
if err != nil {
log.Fatal(err)
}
fmt.Println(t) // 2020-07-24 09:07:29 +0000 UTC
str := timefmt.Format(t, "%Y/%m/%d %H:%M:%S")
fmt.Println(str) // 2020/07/24 09:07:29
str = timefmt.Format(t, "%a, %d %b %Y %T %z")
fmt.Println(str) // Fri, 24 Jul 2020 09:07:29 +0000
}
```
Please refer to [`man 3 strftime`](https://linux.die.net/man/3/strftime) and
[`man 3 strptime`](https://linux.die.net/man/3/strptime) for formatters.
As an extension, `%f` directive is supported for zero-padded microseconds, which originates from Python.
Note that `E` and `O` modifier characters are not supported.
## Comparison to other libraries
- This library
- provides both formatting and parsing functions in pure Go language,
- depends only on the Go standard libraries not to grow up dependency.
- `Format` (`strftime`) implements glibc extensions including
- width specifier like `%6Y %10B %4Z` (limited to 1024 bytes),
- omitting padding modifier like `%-y-%-m-%-d`,
- space padding modifier like `%_y-%_m-%_d`,
- upper case modifier like `%^a %^b`,
- swapping case modifier like `%#Z`,
- time zone offset modifier like `%:z %::z %:::z`,
- and its performance is very good.
- `AppendFormat` is provided for reducing allocations.
- `Parse` (`strptime`) allows to parse
- composed directives like `%F %T`,
- century years like `%C %y`,
- week directives like `%W %a` and `%G-W%V-%u`.
- `ParseInLocation` is provided for configuring the default location.
![](https://user-images.githubusercontent.com/375258/88606920-de475c80-d0b8-11ea-8d40-cbfee9e35c2e.jpg)
## Bug Tracker
Report bug at [Issues・itchyny/timefmt-go - GitHub](https://github.com/itchyny/timefmt-go/issues).
## Author
itchyny (<https://github.com/itchyny>)
## License
This software is released under the MIT License, see LICENSE.
+462
View File
@@ -0,0 +1,462 @@
package timefmt
import (
"strconv"
"time"
)
// Format time to string using the format.
func Format(t time.Time, format string) string {
return string(AppendFormat(make([]byte, 0, 64), t, format))
}
// AppendFormat appends formatted time string to the buffer.
func AppendFormat(buf []byte, t time.Time, format string) []byte {
year, month, day := t.Date()
hour, minute, second := t.Clock()
var width, colons int
var padding byte
var pending string
var upper, swap bool
for i := 0; i < len(format); i++ {
if b := format[i]; b == '%' {
if i++; i == len(format) {
buf = append(buf, '%')
break
}
b, width, padding, upper, swap = format[i], 0, '0', false, false
L:
switch b {
case '-':
if pending != "" {
buf = append(buf, '-')
break
}
if i++; i == len(format) {
goto K
}
padding = ^paddingMask
b = format[i]
goto L
case '_':
if i++; i == len(format) {
goto K
}
padding = ' ' | ^paddingMask
b = format[i]
goto L
case '^':
if i++; i == len(format) {
goto K
}
upper = true
b = format[i]
goto L
case '#':
if i++; i == len(format) {
goto K
}
swap = true
b = format[i]
goto L
case '0':
if i++; i == len(format) {
goto K
}
padding = '0' | ^paddingMask
b = format[i]
goto L
case '1', '2', '3', '4', '5', '6', '7', '8', '9':
width = int(b & 0x0F)
for i++; i < len(format); i++ {
if b = format[i]; b <= '9' && '0' <= b {
width = min(width*10+int(b&0x0F), 1024)
} else {
break
}
}
if padding == ^paddingMask {
padding = ' ' | ^paddingMask
}
if i == len(format) {
goto K
}
goto L
case 'Y':
buf = appendInt(buf, year, or(width, 4), padding)
case 'y':
buf = appendInt(buf, abs(year%100), max(width, 2), padding)
case 'C':
c := year / 100
z := year < 0 && c == 0
if z {
c = -1
}
buf = appendInt(buf, c, max(width, 2), padding)
if z {
buf[len(buf)-1] = '0'
}
case 'g':
year, _ := t.ISOWeek()
buf = appendInt(buf, abs(year%100), max(width, 2), padding)
case 'G':
year, _ := t.ISOWeek()
buf = appendInt(buf, year, or(width, 4), padding)
case 'm':
buf = appendInt(buf, int(month), max(width, 2), padding)
case 'B':
buf = appendString(buf, longMonthNames[month-1], width, padding, upper, swap)
case 'b', 'h':
buf = appendString(buf, shortMonthNames[month-1], width, padding, upper, swap)
case 'A':
buf = appendString(buf, longWeekNames[t.Weekday()], width, padding, upper, swap)
case 'a':
buf = appendString(buf, shortWeekNames[t.Weekday()], width, padding, upper, swap)
case 'w':
buf = appendInt(buf, int(t.Weekday()), width, padding)
case 'u':
buf = appendInt(buf, or(int(t.Weekday()), 7), width, padding)
case 'V':
_, week := t.ISOWeek()
buf = appendInt(buf, week, max(width, 2), padding)
case 'U':
week := (t.YearDay() + 6 - int(t.Weekday())) / 7
buf = appendInt(buf, week, max(width, 2), padding)
case 'W':
week := t.YearDay()
if int(t.Weekday()) > 0 {
week -= int(t.Weekday()) - 7
}
week /= 7
buf = appendInt(buf, week, max(width, 2), padding)
case 'e':
if padding < ^paddingMask {
padding = ' '
}
fallthrough
case 'd':
buf = appendInt(buf, day, max(width, 2), padding)
case 'j':
buf = appendInt(buf, t.YearDay(), max(width, 3), padding)
case 'k':
if padding < ^paddingMask {
padding = ' '
}
fallthrough
case 'H':
buf = appendInt(buf, hour, max(width, 2), padding)
case 'l':
if padding < ^paddingMask {
padding = ' '
}
fallthrough
case 'I':
buf = appendInt(buf, or(hour%12, 12), max(width, 2), padding)
case 'P':
swap = !(upper || swap)
fallthrough
case 'p':
if hour < 12 {
buf = appendString(buf, "AM", width, padding, upper, swap)
} else {
buf = appendString(buf, "PM", width, padding, upper, swap)
}
case 'M':
buf = appendInt(buf, minute, max(width, 2), padding)
case 'S':
buf = appendInt(buf, second, max(width, 2), padding)
case 's':
if padding < ^paddingMask {
padding = ' '
}
buf = appendInt64(buf, t.Unix(), width, padding)
case 'f':
buf = appendInt(buf, t.Nanosecond()/1000, or(width, 6), padding)
case 'Z', 'z':
name, offset := t.Zone()
if b == 'Z' && name != "" {
buf = appendString(buf, name, width, padding, upper, swap)
break
}
i := len(buf)
if padding != ^paddingMask {
for ; width > 1; width-- {
buf = append(buf, padding&paddingMask)
}
}
j := len(buf)
if offset < 0 {
buf = append(buf, '-')
offset = -offset
} else {
buf = append(buf, '+')
}
k := len(buf)
buf = appendInt(buf, offset/3600, 2, padding)
if buf[k] == ' ' {
buf[k-1], buf[k] = buf[k], buf[k-1]
}
if offset %= 3600; colons <= 2 || offset != 0 {
if colons != 0 {
buf = append(buf, ':')
}
buf = appendInt(buf, offset/60, 2, '0')
if offset %= 60; colons == 2 || colons == 3 && offset != 0 {
buf = append(buf, ':')
buf = appendInt(buf, offset, 2, '0')
}
}
colons = 0
if k = min(len(buf)-j-1, j-i); k > 0 {
copy(buf[j-k:], buf[j:])
buf = buf[:len(buf)-k]
if padding&paddingMask == '0' {
buf[i], buf[j-k] = buf[j-k], buf[i]
}
}
case ':':
if pending != "" {
buf = append(buf, ':')
} else {
colons = 1
M:
for i++; i < len(format); i++ {
switch format[i] {
case ':':
colons++
case 'z':
if colons > 3 {
i++
break M
}
b = 'z'
goto L
default:
break M
}
}
buf = appendLast(buf, format[:i], width, padding)
i--
colons = 0
}
case 't':
buf = appendString(buf, "\t", width, padding, false, false)
case 'n':
buf = appendString(buf, "\n", width, padding, false, false)
case '%':
buf = appendString(buf, "%", width, padding, false, false)
case 'c':
pending, swap = "a b e H:M:S Y", false
case '+':
pending, swap = "a b e H:M:S Z Y", false
case 'v':
pending, swap = "e-b-Y", false
case 'r':
pending, swap = "I:M:S p", false
case 'F':
pending = "Y-m-d"
case 'D', 'x':
pending = "m/d/y"
case 'T', 'X':
pending = "H:M:S"
case 'R':
pending = "H:M"
default:
if pending == "" {
buf = appendLast(buf, format[:i], width-1, padding)
}
buf = append(buf, b)
}
if pending != "" {
b, pending, width, padding = pending[0], pending[1:], 0, '0'
goto L
}
} else {
buf = append(buf, b)
}
}
return buf
K:
return appendLast(buf, format, width, padding)
}
const smalls = "" +
"00010203040506070809" +
"10111213141516171819" +
"20212223242526272829" +
"30313233343536373839" +
"40414243444546474849" +
"50515253545556575859" +
"60616263646566676869" +
"70717273747576777879" +
"80818283848586878889" +
"90919293949596979899"
func appendInt(buf []byte, num, width int, padding byte) []byte {
var digits int
switch {
default:
fallthrough
case num < 0:
return appendInt64(buf, int64(num), width, padding)
case num < 100 && width == 2 && padding == '0':
return append(buf, smalls[num*2:num*2+2]...)
case num < 10:
digits = 1
case num < 100:
digits = 2
case num < 1000:
digits = 3
case num < 10000:
digits = 4
}
if padding != ^paddingMask {
padding &= paddingMask
for ; width > digits; width-- {
buf = append(buf, padding)
}
}
switch digits {
case 4:
buf = append(buf, byte(num/1000)|'0')
num %= 1000
fallthrough
case 3:
buf = append(buf, byte(num/100)|'0')
num %= 100
fallthrough
case 2:
buf = append(buf, byte(num/10)|'0')
num %= 10
fallthrough
default:
return append(buf, byte(num)|'0')
}
}
func appendInt64(buf []byte, num int64, width int, padding byte) []byte {
if padding == ^paddingMask {
return strconv.AppendInt(buf, num, 10)
}
padding &= paddingMask
i := len(buf)
for ; width > 1; width-- {
buf = append(buf, padding)
}
j := len(buf)
buf = strconv.AppendInt(buf, num, 10)
if k := min(len(buf)-j-1, j-i); k > 0 {
copy(buf[j-k:], buf[j:])
buf = buf[:len(buf)-k]
if j -= k; buf[i] == '0' && buf[j] == '-' {
buf[i], buf[j] = '-', '0'
}
}
return buf
}
func appendString(buf []byte, str string, width int, padding byte, upper, swap bool) []byte {
if width > len(str) && padding != ^paddingMask {
if padding < ^paddingMask {
padding = ' '
} else {
padding &= paddingMask
}
for width -= len(str); width > 0; width-- {
buf = append(buf, padding)
}
}
switch {
case swap:
if str[1] < 'a' {
for _, b := range []byte(str) {
buf = append(buf, b|0x20)
}
break
}
fallthrough
case upper:
for _, b := range []byte(str) {
buf = append(buf, b&0x5F)
}
default:
buf = append(buf, str...)
}
return buf
}
func appendLast(buf []byte, format string, width int, padding byte) []byte {
for i := len(format) - 1; i >= 0; i-- {
if format[i] == '%' {
buf = appendString(buf, format[i:], width, padding, false, false)
break
}
}
return buf
}
func or(x, y int) int {
if x != 0 {
return x
}
return y
}
func abs(x int) int {
if x >= 0 {
return x
}
return -x
}
const paddingMask byte = 0x7F
var longMonthNames = []string{
"January",
"February",
"March",
"April",
"May",
"June",
"July",
"August",
"September",
"October",
"November",
"December",
}
var shortMonthNames = []string{
"Jan",
"Feb",
"Mar",
"Apr",
"May",
"Jun",
"Jul",
"Aug",
"Sep",
"Oct",
"Nov",
"Dec",
}
var longWeekNames = []string{
"Sunday",
"Monday",
"Tuesday",
"Wednesday",
"Thursday",
"Friday",
"Saturday",
}
var shortWeekNames = []string{
"Sun",
"Mon",
"Tue",
"Wed",
"Thu",
"Fri",
"Sat",
}
+474
View File
@@ -0,0 +1,474 @@
package timefmt
import (
"errors"
"fmt"
"time"
)
// Parse time string using the format.
func Parse(source, format string) (t time.Time, err error) {
return parse(source, format, time.UTC, time.Local)
}
// ParseInLocation parses time string with the default location.
// The location is also used to parse the time zone name (%Z).
func ParseInLocation(source, format string, loc *time.Location) (t time.Time, err error) {
return parse(source, format, loc, loc)
}
func parse(source, format string, loc, base *time.Location) (t time.Time, err error) {
year, month, day, hour, minute, second, nanosecond := 1900, 1, 0, 0, 0, 0, 0
defer func() {
if err != nil {
err = fmt.Errorf("failed to parse %q with %q: %w", source, format, err)
}
}()
var j, week, weekday, yday, colons, sign int
century, weekstart := -1, time.Weekday(-1)
var pm, hasISOYear, hasZoneName, hasZoneOffset bool
var pending string
for i, l := 0, len(source); i < len(format); i++ {
if b := format[i]; b == '%' {
if i++; i == len(format) {
err = errors.New(`stray "%"`)
return
}
b = format[i]
L:
switch b {
case 'G':
hasISOYear = true
fallthrough
case 'Y':
sign, j = parseSign(source, j, l)
if year, j, err = parseInt(source, j, 4, 0, 9999, b); err != nil {
return
}
year *= sign
case 'g':
hasISOYear = true
fallthrough
case 'y':
if year, j, err = parseInt(source, j, 2, 0, 99, b); err != nil {
return
}
if year < 69 {
year += 2000
} else {
year += 1900
}
case 'C':
sign, j = parseSign(source, j, l)
if sign < 0 {
err = errors.New(`negative century is not supported for "%C"`)
return
}
if century, j, err = parseInt(source, j, 2, 0, 99, 'C'); err != nil {
return
}
case 'm':
if month, j, err = parseInt(source, j, 2, 1, 12, 'm'); err != nil {
return
}
case 'B':
if month, j, err = parseAny(source, j, longMonthNames, 'B'); err != nil {
return
}
case 'b', 'h':
if month, j, err = parseAny(source, j, shortMonthNames, b); err != nil {
return
}
case 'A':
if weekday, j, err = parseAny(source, j, longWeekNames, 'A'); err != nil {
return
}
case 'a':
if weekday, j, err = parseAny(source, j, shortWeekNames, 'a'); err != nil {
return
}
case 'w':
if weekday, j, err = parseInt(source, j, 1, 0, 6, 'w'); err != nil {
return
}
weekday++
case 'u':
if weekday, j, err = parseInt(source, j, 1, 1, 7, 'u'); err != nil {
return
}
weekday = weekday%7 + 1
case 'V':
if week, j, err = parseInt(source, j, 2, 1, 53, b); err != nil {
return
}
weekstart = time.Thursday
weekday = or(weekday, 2)
case 'U':
if week, j, err = parseInt(source, j, 2, 0, 53, b); err != nil {
return
}
weekstart = time.Sunday
weekday = or(weekday, 1)
case 'W':
if week, j, err = parseInt(source, j, 2, 0, 53, b); err != nil {
return
}
weekstart = time.Monday
weekday = or(weekday, 2)
case 'e':
if j < l && source[j] == ' ' {
j++
}
fallthrough
case 'd':
if day, j, err = parseInt(source, j, 2, 1, 31, b); err != nil {
return
}
case 'j':
if yday, j, err = parseInt(source, j, 3, 1, 366, 'j'); err != nil {
return
}
case 'k':
if j < l && source[j] == ' ' {
j++
}
fallthrough
case 'H':
if hour, j, err = parseInt(source, j, 2, 0, 23, b); err != nil {
return
}
case 'l':
if j < l && source[j] == ' ' {
j++
}
fallthrough
case 'I':
if hour, j, err = parseInt(source, j, 2, 1, 12, b); err != nil {
return
}
if hour == 12 {
hour = 0
}
case 'P', 'p':
var ampm int
if ampm, j, err = parseAny(source, j, []string{"AM", "PM"}, b); err != nil {
return
}
pm = ampm == 2
case 'M':
if minute, j, err = parseInt(source, j, 2, 0, 59, 'M'); err != nil {
return
}
case 'S':
if second, j, err = parseInt(source, j, 2, 0, 60, 'S'); err != nil {
return
}
case 's':
sign, j = parseSign(source, j, l)
var unix int64
if unix, j, err = parseInt64(source, j, 19, 's'); err != nil {
return
}
t = time.Unix(int64(sign)*unix, 0).In(time.UTC)
var mon time.Month
year, mon, day = t.Date()
hour, minute, second = t.Clock()
month = int(mon)
case 'f':
microsecond, i := 0, j
if microsecond, j, err = parseInt(source, j, 6, 0, 999999, 'f'); err != nil {
return
}
for i = j - i; i < 6; i++ {
microsecond *= 10
}
nanosecond = microsecond * 1000
case 'Z':
i := j
for ; j < l; j++ {
if c := source[j]; c < 'A' || 'Z' < c {
break
}
}
t, err = time.ParseInLocation("MST", source[i:j], base)
if err != nil {
err = fmt.Errorf(`cannot parse %q with "%%Z"`, source[i:j])
return
}
if hasZoneOffset {
name, _ := t.Zone()
_, offset := locationZone(loc)
loc = time.FixedZone(name, offset)
} else {
loc = t.Location()
}
hasZoneName = true
case 'z':
if j >= l {
err = parseZFormatError(colons)
return
}
sign = 1
switch source[j] {
case '-':
sign = -1
fallthrough
case '+':
hour, minute, second, i := 0, 0, 0, j+1
if hour, j, _ = parseInt(source, i, 2, 0, 23, 'z'); j != i+2 {
err = parseZFormatError(colons)
return
}
if j >= l || source[j] != ':' {
if colons > 0 && colons < 3 {
err = expectedColonForZFormatError(colons)
return
}
} else if j++; colons == 0 {
colons = 4
}
i = j
if minute, j, _ = parseInt(source, i, 2, 0, 59, 'z'); j != i+2 {
if colons > 0 && colons != 3 {
err = parseZFormatError(colons & 3)
return
}
j = i
} else if colons > 1 {
if j >= l || source[j] != ':' {
if colons < 3 {
err = expectedColonForZFormatError(colons)
return
}
} else {
i = j + 1
if second, j, _ = parseInt(source, i, 2, 0, 59, 'z'); j != i+2 {
if colons < 3 {
err = parseZFormatError(colons)
return
}
j = i - 1
}
}
}
var name string
if hasZoneName {
name, _ = locationZone(loc)
}
loc, colons = time.FixedZone(name, sign*((hour*60+minute)*60+second)), 0
hasZoneOffset = true
case 'Z':
loc, colons, j = time.UTC, 0, j+1
default:
err = parseZFormatError(colons)
return
}
case ':':
if pending != "" {
if j >= l || source[j] != b {
err = expectedFormatError(b)
return
}
j++
} else {
for colons = 1; colons <= 3; colons++ {
if i++; i == len(format) {
break
} else if b = format[i]; b == 'z' {
goto L
} else if b != ':' || colons == 3 {
break
}
}
err = expectedZAfterColonError(colons)
return
}
case 't', 'n':
i := j
K:
for ; j < l; j++ {
switch source[j] {
case ' ', '\t', '\n', '\v', '\f', '\r':
default:
break K
}
}
if i == j {
err = fmt.Errorf(`expected a space for "%%%c"`, b)
return
}
case '%':
if j >= l || source[j] != b {
err = expectedFormatError(b)
return
}
j++
case 'c':
pending = "a b e H:M:S Y"
case '+':
pending = "a b e H:M:S Z Y"
case 'v':
pending = "e-b-Y"
case 'r':
pending = "I:M:S p"
case 'F':
pending = "Y-m-d"
case 'D', 'x':
pending = "m/d/y"
case 'T', 'X':
pending = "H:M:S"
case 'R':
pending = "H:M"
default:
if pending == "" {
err = fmt.Errorf(`unexpected format "%%%c"`, b)
return
}
if j >= l || source[j] != b {
err = expectedFormatError(b)
return
}
j++
}
if pending != "" {
b, pending = pending[0], pending[1:]
goto L
}
} else if j >= l || source[j] != b {
err = expectedFormatError(b)
return
} else {
j++
}
}
if j < len(source) {
err = fmt.Errorf("unparsed string %q", source[j:])
return
}
if pm {
hour += 12
}
if century >= 0 {
year = century*100 + year%100
}
if day == 0 {
if yday > 0 {
if hasISOYear {
err = errors.New(`use "%Y" to parse non-ISO year for "%j"`)
return
}
return time.Date(year, time.January, yday, hour, minute, second, nanosecond, loc), nil
}
if weekstart >= time.Sunday {
if weekstart == time.Thursday {
if !hasISOYear {
err = errors.New(`use "%G" to parse ISO year for "%V"`)
return
}
} else if hasISOYear {
err = errors.New(`use "%Y" to parse non-ISO year for "%U" or "%W"`)
return
}
if weekstart > time.Sunday && weekday == 1 {
week++
}
t := time.Date(year, time.January, -int(weekstart), hour, minute, second, nanosecond, loc)
return t.AddDate(0, 0, week*7-int(t.Weekday())+weekday-1), nil
}
day = 1
}
return time.Date(year, time.Month(month), day, hour, minute, second, nanosecond, loc), nil
}
func locationZone(loc *time.Location) (name string, offset int) {
return time.Date(2000, time.January, 1, 0, 0, 0, 0, loc).Zone()
}
type parseFormatError byte
func (err parseFormatError) Error() string {
return fmt.Sprintf(`cannot parse "%%%c"`, byte(err))
}
type expectedFormatError byte
func (err expectedFormatError) Error() string {
return fmt.Sprintf("expected %q", byte(err))
}
type parseZFormatError int
func (err parseZFormatError) Error() string {
return `cannot parse "%` + `:::z"`[3-err:]
}
type expectedColonForZFormatError int
func (err expectedColonForZFormatError) Error() string {
return `expected ':' for "%` + `:::z"`[3-err:]
}
type expectedZAfterColonError int
func (err expectedZAfterColonError) Error() string {
return `expected 'z' after "%` + `:::"`[3-err:]
}
func parseSign(source string, index, l int) (int, int) {
if index < l && source[index] == '-' {
return -1, index + 1
}
return 1, index
}
// parseInt parses an integer from source. This is intentionally not a
// wrapper of parseInt64 to avoid the overhead of int64 arithmetic and
// function call indirection in the hot path (~20% slower in benchmarks).
func parseInt(source string, index, size, minimum, maximum int, format byte) (int, int, error) {
var value int
i := index
for size = min(i+size, len(source)); i < size; i++ {
if b := source[i] - '0'; b < 10 {
value = value*10 + int(b)
} else {
break
}
}
if i == index || value < minimum || maximum < value {
return 0, 0, parseFormatError(format)
}
return value, i, nil
}
func parseInt64(source string, index, size int, format byte) (int64, int, error) {
var value int64
i := index
for size = min(i+size, len(source)); i < size; i++ {
if b := source[i] - '0'; b < 10 {
value = value*10 + int64(b)
} else {
break
}
}
if i == index {
return 0, 0, parseFormatError(format)
}
return value, i, nil
}
func parseAny(source string, index int, candidates []string, format byte) (int, int, error) {
L:
for i, xs := range candidates {
j := index
for k := 0; k < len(xs); k, j = k+1, j+1 {
if j >= len(source) {
continue L
}
if x, y := xs[k], source[j]; x != y && x|0x20 != y|0x20 {
continue L
}
}
return i + 1, j, nil
}
return 0, 0, parseFormatError(format)
}
+2
View File
@@ -0,0 +1,2 @@
// Package timefmt provides functions for formatting and parsing date time strings.
package timefmt