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:
+37
@@ -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
@@ -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
@@ -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
@@ -0,0 +1,69 @@
|
||||
# timefmt-go
|
||||
[](https://github.com/itchyny/timefmt-go/actions?query=branch:main)
|
||||
[](https://goreportcard.com/report/github.com/itchyny/timefmt-go)
|
||||
[](https://github.com/itchyny/timefmt-go/blob/main/LICENSE)
|
||||
[](https://github.com/itchyny/timefmt-go/releases)
|
||||
[](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.
|
||||
|
||||

|
||||
|
||||
## 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
@@ -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
@@ -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
@@ -0,0 +1,2 @@
|
||||
// Package timefmt provides functions for formatting and parsing date time strings.
|
||||
package timefmt
|
||||
Reference in New Issue
Block a user