diff --git a/go.mod b/go.mod index 6f20d951fd..a2c194e127 100644 --- a/go.mod +++ b/go.mod @@ -98,7 +98,7 @@ require ( github.com/gsterjov/go-libsecret v0.0.0-20161001094733-a6f4afe4910c // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/itchyny/gojq v0.12.9 // indirect - github.com/itchyny/timefmt-go v0.1.4 // indirect + github.com/itchyny/timefmt-go v0.1.8 // indirect github.com/jmespath/go-jmespath v0.4.0 // indirect github.com/json-iterator/go v1.1.12 // indirect github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect diff --git a/go.sum b/go.sum index 307b60824d..0d1fc00843 100644 --- a/go.sum +++ b/go.sum @@ -146,8 +146,8 @@ github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2 github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/itchyny/gojq v0.12.9 h1:biKpbKwMxVYhCU1d6mR7qMr3f0Hn9F5k5YykCVb3gmM= github.com/itchyny/gojq v0.12.9/go.mod h1:T4Ip7AETUXeGpD+436m+UEl3m3tokRgajd5pRfsR5oE= -github.com/itchyny/timefmt-go v0.1.4 h1:hFEfWVdwsEi+CY8xY2FtgWHGQaBaC3JeHd+cve0ynVM= -github.com/itchyny/timefmt-go v0.1.4/go.mod h1:nEP7L+2YmAbT2kZ2HfSs1d8Xtw9LY8D2stDBckWakZ8= +github.com/itchyny/timefmt-go v0.1.8 h1:1YEo1JvfXeAHKdjelbYr/uCuhkybaHCeTkH8Bo791OI= +github.com/itchyny/timefmt-go v0.1.8/go.mod h1:5E46Q+zj7vbTgWY8o5YkMeYb4I6GeWLFnetPy5oBrAI= github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg= github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= diff --git a/vendor/github.com/itchyny/timefmt-go/CHANGELOG.md b/vendor/github.com/itchyny/timefmt-go/CHANGELOG.md index 075ca8ef60..43b26213c4 100644 --- a/vendor/github.com/itchyny/timefmt-go/CHANGELOG.md +++ b/vendor/github.com/itchyny/timefmt-go/CHANGELOG.md @@ -1,4 +1,23 @@ # 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 diff --git a/vendor/github.com/itchyny/timefmt-go/LICENSE b/vendor/github.com/itchyny/timefmt-go/LICENSE index 84d6cb0339..de520bb31a 100644 --- a/vendor/github.com/itchyny/timefmt-go/LICENSE +++ b/vendor/github.com/itchyny/timefmt-go/LICENSE @@ -1,6 +1,6 @@ The MIT License (MIT) -Copyright (c) 2020-2022 itchyny +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 diff --git a/vendor/github.com/itchyny/timefmt-go/README.md b/vendor/github.com/itchyny/timefmt-go/README.md index f01af96112..9c028c746a 100644 --- a/vendor/github.com/itchyny/timefmt-go/README.md +++ b/vendor/github.com/itchyny/timefmt-go/README.md @@ -1,5 +1,5 @@ # timefmt-go -[![CI Status](https://github.com/itchyny/timefmt-go/workflows/CI/badge.svg)](https://github.com/itchyny/timefmt-go/actions) +[![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) @@ -54,7 +54,7 @@ Note that `E` and `O` modifier characters are not supported. - `Parse` (`strptime`) allows to parse - composed directives like `%F %T`, - century years like `%C %y`, - - week names like `%A` `%a` (parsed results are discarded). + - 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) @@ -63,7 +63,7 @@ Note that `E` and `O` modifier characters are not supported. Report bug at [Issues・itchyny/timefmt-go - GitHub](https://github.com/itchyny/timefmt-go/issues). ## Author -itchyny (https://github.com/itchyny) +itchyny () ## License This software is released under the MIT License, see LICENSE. diff --git a/vendor/github.com/itchyny/timefmt-go/format.go b/vendor/github.com/itchyny/timefmt-go/format.go index eea976ee9c..24ea7b2c52 100644 --- a/vendor/github.com/itchyny/timefmt-go/format.go +++ b/vendor/github.com/itchyny/timefmt-go/format.go @@ -1,7 +1,6 @@ package timefmt import ( - "math" "strconv" "time" ) @@ -14,7 +13,7 @@ func Format(t time.Time, format string) string { // AppendFormat appends formatted time string to the buffer. func AppendFormat(buf []byte, t time.Time, format string) []byte { year, month, day := t.Date() - hour, min, sec := t.Clock() + hour, minute, second := t.Clock() var width, colons int var padding byte var pending string @@ -69,21 +68,13 @@ func AppendFormat(buf []byte, t time.Time, format string) []byte { goto L case '1', '2', '3', '4', '5', '6', '7', '8', '9': width = int(b & 0x0F) - const maxWidth = 1024 for i++; i < len(format); i++ { - b = format[i] - if b <= '9' && '0' <= b { - width = width*10 + int(b&0x0F) - if width >= math.MaxInt/10 { - width = maxWidth - } + if b = format[i]; b <= '9' && '0' <= b { + width = min(width*10+int(b&0x0F), 1024) } else { break } } - if width > maxWidth { - width = maxWidth - } if padding == ^paddingMask { padding = ' ' | ^paddingMask } @@ -92,37 +83,27 @@ func AppendFormat(buf []byte, t time.Time, format string) []byte { } goto L case 'Y': - if width == 0 { - width = 4 - } - buf = appendInt(buf, year, width, padding) + buf = appendInt(buf, year, or(width, 4), padding) case 'y': - if width < 2 { - width = 2 - } - buf = appendInt(buf, year%100, width, padding) + buf = appendInt(buf, abs(year%100), max(width, 2), padding) case 'C': - if width < 2 { - width = 2 + c := year / 100 + z := year < 0 && c == 0 + if z { + c = -1 } - buf = appendInt(buf, year/100, width, padding) - case 'g': - if width < 2 { - width = 2 + buf = appendInt(buf, c, max(width, 2), padding) + if z { + buf[len(buf)-1] = '0' } + case 'g': year, _ := t.ISOWeek() - buf = appendInt(buf, year%100, width, padding) + buf = appendInt(buf, abs(year%100), max(width, 2), padding) case 'G': - if width == 0 { - width = 4 - } year, _ := t.ISOWeek() - buf = appendInt(buf, year, width, padding) + buf = appendInt(buf, year, or(width, 4), padding) case 'm': - if width < 2 { - width = 2 - } - buf = appendInt(buf, int(month), width, padding) + buf = appendInt(buf, int(month), max(width, 2), padding) case 'B': buf = appendString(buf, longMonthNames[month-1], width, padding, upper, swap) case 'b', 'h': @@ -132,121 +113,65 @@ func AppendFormat(buf []byte, t time.Time, format string) []byte { case 'a': buf = appendString(buf, shortWeekNames[t.Weekday()], width, padding, upper, swap) case 'w': - for ; width > 1; width-- { - buf = append(buf, padding&paddingMask) - } - buf = append(buf, '0'+byte(t.Weekday())) + buf = appendInt(buf, int(t.Weekday()), width, padding) case 'u': - w := int(t.Weekday()) - if w == 0 { - w = 7 - } - for ; width > 1; width-- { - buf = append(buf, padding&paddingMask) - } - buf = append(buf, '0'+byte(w)) + buf = appendInt(buf, or(int(t.Weekday()), 7), width, padding) case 'V': - if width < 2 { - width = 2 - } _, week := t.ISOWeek() - buf = appendInt(buf, week, width, padding) + buf = appendInt(buf, week, max(width, 2), padding) case 'U': - if width < 2 { - width = 2 - } week := (t.YearDay() + 6 - int(t.Weekday())) / 7 - buf = appendInt(buf, week, width, padding) + buf = appendInt(buf, week, max(width, 2), padding) case 'W': - if width < 2 { - width = 2 - } week := t.YearDay() if int(t.Weekday()) > 0 { week -= int(t.Weekday()) - 7 } week /= 7 - buf = appendInt(buf, week, width, padding) + buf = appendInt(buf, week, max(width, 2), padding) case 'e': if padding < ^paddingMask { padding = ' ' } fallthrough case 'd': - if width < 2 { - width = 2 - } - buf = appendInt(buf, day, width, padding) + buf = appendInt(buf, day, max(width, 2), padding) case 'j': - if width < 3 { - width = 3 - } - buf = appendInt(buf, t.YearDay(), width, padding) + buf = appendInt(buf, t.YearDay(), max(width, 3), padding) case 'k': if padding < ^paddingMask { padding = ' ' } fallthrough case 'H': - if width < 2 { - width = 2 - } - buf = appendInt(buf, hour, width, padding) + buf = appendInt(buf, hour, max(width, 2), padding) case 'l': - if width < 2 { - width = 2 - } if padding < ^paddingMask { padding = ' ' } - h := hour - if h > 12 { - h -= 12 - } - buf = appendInt(buf, h, width, padding) + fallthrough case 'I': - if width < 2 { - width = 2 - } - h := hour - if h > 12 { - h -= 12 - } else if h == 0 { - h = 12 - } - buf = appendInt(buf, h, width, padding) + 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 'P': - if hour < 12 { - buf = appendString(buf, "am", width, padding, upper, swap) - } else { - buf = appendString(buf, "pm", width, padding, upper, swap) - } case 'M': - if width < 2 { - width = 2 - } - buf = appendInt(buf, min, width, padding) + buf = appendInt(buf, minute, max(width, 2), padding) case 'S': - if width < 2 { - width = 2 - } - buf = appendInt(buf, sec, width, padding) + buf = appendInt(buf, second, max(width, 2), padding) case 's': if padding < ^paddingMask { padding = ' ' } - buf = appendInt(buf, int(t.Unix()), width, padding) + buf = appendInt64(buf, t.Unix(), width, padding) case 'f': - if width == 0 { - width = 6 - } - buf = appendInt(buf, t.Nanosecond()/1000, width, padding) + buf = appendInt(buf, t.Nanosecond()/1000, or(width, 6), padding) case 'Z', 'z': name, offset := t.Zone() if b == 'Z' && name != "" { @@ -271,32 +196,22 @@ func AppendFormat(buf []byte, t time.Time, format string) []byte { if buf[k] == ' ' { buf[k-1], buf[k] = buf[k], buf[k-1] } - if k = offset % 3600; colons <= 2 || k != 0 { + if offset %= 3600; colons <= 2 || offset != 0 { if colons != 0 { buf = append(buf, ':') } - buf = appendInt(buf, k/60, 2, '0') - if k %= 60; colons == 2 || colons == 3 && k != 0 { + buf = appendInt(buf, offset/60, 2, '0') + if offset %= 60; colons == 2 || colons == 3 && offset != 0 { buf = append(buf, ':') - buf = appendInt(buf, k, 2, '0') + buf = appendInt(buf, offset, 2, '0') } } colons = 0 - if i != j { - l := len(buf) - k = j + 1 - (l - j) - if k < i { - l = j + 1 + i - k - k = i - } else { - l = j + 1 - } - copy(buf[k:], buf[j:]) - buf = buf[:l] + 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' { - for ; k > i; k-- { - buf[k-1], buf[k] = buf[k], buf[k-1] - } + buf[i], buf[j-k] = buf[j-k], buf[i] } } case ':': @@ -330,13 +245,24 @@ func AppendFormat(buf []byte, t time.Time, format string) []byte { 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 == "" { - var ok bool - if pending, ok = compositions[b]; ok { - swap = false - break - } buf = appendLast(buf, format[:i], width-1, padding) } buf = append(buf, b) @@ -354,81 +280,79 @@ 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 - switch width { - case 2: - if num < 10 { - buf = append(buf, padding) - goto L1 - } else if num < 100 { - goto L2 - } else if num < 1000 { - goto L3 - } else if num < 10000 { - goto L4 - } - case 4: - if num < 1000 { - buf = append(buf, padding) - if num < 100 { - buf = append(buf, padding) - if num < 10 { - buf = append(buf, padding) - goto L1 - } - goto L2 - } - goto L3 - } else if num < 10000 { - goto L4 - } - default: - i := len(buf) - for ; width > 1; width-- { - buf = append(buf, padding) - } - j := len(buf) - buf = strconv.AppendInt(buf, int64(num), 10) - l := len(buf) - if j+1 == l || i == j { - return buf - } - k := j + 1 - (l - j) - if k < i { - l = j + 1 + i - k - k = i - } else { - l = j + 1 - } - copy(buf[k:], buf[j:]) - return buf[:l] + for ; width > digits; width-- { + buf = append(buf, padding) } } - if num < 100 { - if num < 10 { - goto L1 - } - goto L2 - } else if num < 10000 { - if num < 1000 { - goto L3 + 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' } - goto L4 } - return strconv.AppendInt(buf, int64(num), 10) -L4: - buf = append(buf, byte(num/1000)|'0') - num %= 1000 -L3: - buf = append(buf, byte(num/100)|'0') - num %= 100 -L2: - buf = append(buf, byte(num/10)|'0') - num %= 10 -L1: - return append(buf, byte(num)|'0') + return buf } func appendString(buf []byte, str string, width int, padding byte, upper, swap bool) []byte { @@ -444,7 +368,7 @@ func appendString(buf []byte, str string, width int, padding byte, upper, swap b } switch { case swap: - if str[len(str)-1] < 'a' { + if str[1] < 'a' { for _, b := range []byte(str) { buf = append(buf, b|0x20) } @@ -471,6 +395,20 @@ func appendLast(buf []byte, format string, width int, padding byte) []byte { 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{ @@ -522,16 +460,3 @@ var shortWeekNames = []string{ "Fri", "Sat", } - -var compositions = map[byte]string{ - 'c': "a b e H:M:S Y", - '+': "a b e H:M:S Z Y", - 'F': "Y-m-d", - 'D': "m/d/y", - 'x': "m/d/y", - 'v': "e-b-Y", - 'T': "H:M:S", - 'X': "H:M:S", - 'r': "I:M:S p", - 'R': "H:M", -} diff --git a/vendor/github.com/itchyny/timefmt-go/parse.go b/vendor/github.com/itchyny/timefmt-go/parse.go index ae21534fc3..ab60634662 100644 --- a/vendor/github.com/itchyny/timefmt-go/parse.go +++ b/vendor/github.com/itchyny/timefmt-go/parse.go @@ -18,31 +18,39 @@ func ParseInLocation(source, format string, loc *time.Location) (t time.Time, er } func parse(source, format string, loc, base *time.Location) (t time.Time, err error) { - year, month, day, hour, min, sec, nsec := 1900, 1, 1, 0, 0, 0, 0 + 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, century, yday, colons int - var pm bool + 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 == '%' { - i++ - if i == len(format) { - err = errors.New("stray %") + if i++; i == len(format) { + err = errors.New(`stray "%"`) return } b = format[i] L: switch b { + case 'G': + hasISOYear = true + fallthrough case 'Y': - if year, j, err = parseNumber(source, j, 4, 'Y'); err != nil { + 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 = parseNumber(source, j, 2, 'y'); err != nil { + if year, j, err = parseInt(source, j, 2, 0, 99, b); err != nil { return } if year < 69 { @@ -51,65 +59,73 @@ func parse(source, format string, loc, base *time.Location) (t time.Time, err er year += 1900 } case 'C': - if century, j, err = parseNumber(source, j, 2, 'C'); err != nil { + sign, j = parseSign(source, j, l) + if sign < 0 { + err = errors.New(`negative century is not supported for "%C"`) return } - case 'g': - if year, j, err = parseNumber(source, j, 2, b); err != nil { - return - } - year += 2000 - case 'G': - if year, j, err = parseNumber(source, j, 4, b); err != nil { + if century, j, err = parseInt(source, j, 2, 0, 99, 'C'); err != nil { return } case 'm': - if month, j, err = parseNumber(source, j, 2, 'm'); err != nil { + if month, j, err = parseInt(source, j, 2, 1, 12, 'm'); err != nil { return } case 'B': - if month, j, err = lookup(source, j, longMonthNames, 'B'); err != nil { + if month, j, err = parseAny(source, j, longMonthNames, 'B'); err != nil { return } case 'b', 'h': - if month, j, err = lookup(source, j, shortMonthNames, b); err != nil { + if month, j, err = parseAny(source, j, shortMonthNames, b); err != nil { return } case 'A': - if _, j, err = lookup(source, j, longWeekNames, 'A'); err != nil { + if weekday, j, err = parseAny(source, j, longWeekNames, 'A'); err != nil { return } case 'a': - if _, j, err = lookup(source, j, shortWeekNames, 'a'); err != nil { + if weekday, j, err = parseAny(source, j, shortWeekNames, 'a'); err != nil { return } case 'w': - if j >= l || source[j] < '0' || '6' < source[j] { - err = parseFormatError(b) + if weekday, j, err = parseInt(source, j, 1, 0, 6, 'w'); err != nil { return } - j++ + weekday++ case 'u': - if j >= l || source[j] < '1' || '7' < source[j] { - err = parseFormatError(b) + if weekday, j, err = parseInt(source, j, 1, 1, 7, 'u'); err != nil { return } - j++ - case 'V', 'U', 'W': - if _, j, err = parseNumber(source, j, 2, b); err != nil { + 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 = parseNumber(source, j, 2, b); err != nil { + if day, j, err = parseInt(source, j, 2, 1, 31, b); err != nil { return } case 'j': - if yday, j, err = parseNumber(source, j, 3, 'j'); err != nil { + if yday, j, err = parseInt(source, j, 3, 1, 366, 'j'); err != nil { return } case 'k': @@ -118,7 +134,7 @@ func parse(source, format string, loc, base *time.Location) (t time.Time, err er } fallthrough case 'H': - if hour, j, err = parseNumber(source, j, 2, b); err != nil { + if hour, j, err = parseInt(source, j, 2, 0, 23, b); err != nil { return } case 'l': @@ -127,111 +143,120 @@ func parse(source, format string, loc, base *time.Location) (t time.Time, err er } fallthrough case 'I': - if hour, j, err = parseNumber(source, j, 2, b); err != nil { + if hour, j, err = parseInt(source, j, 2, 1, 12, b); err != nil { return } if hour == 12 { hour = 0 } - case 'p', 'P': + case 'P', 'p': var ampm int - if ampm, j, err = lookup(source, j, []string{"AM", "PM"}, 'p'); err != nil { + if ampm, j, err = parseAny(source, j, []string{"AM", "PM"}, b); err != nil { return } pm = ampm == 2 case 'M': - if min, j, err = parseNumber(source, j, 2, 'M'); err != nil { + if minute, j, err = parseInt(source, j, 2, 0, 59, 'M'); err != nil { return } case 'S': - if sec, j, err = parseNumber(source, j, 2, 'S'); err != nil { + if second, j, err = parseInt(source, j, 2, 0, 60, 'S'); err != nil { return } case 's': - var unix int - if unix, j, err = parseNumber(source, j, 10, 's'); err != nil { + 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(unix), 0).In(time.UTC) + t = time.Unix(int64(sign)*unix, 0).In(time.UTC) var mon time.Month year, mon, day = t.Date() - hour, min, sec = t.Clock() + hour, minute, second = t.Clock() month = int(mon) case 'f': - var usec, k, d int - if usec, k, err = parseNumber(source, j, 6, 'f'); err != nil { + microsecond, i := 0, j + if microsecond, j, err = parseInt(source, j, 6, 0, 999999, 'f'); err != nil { return } - for j, d = k, k-j; d < 6; d++ { - usec *= 10 + for i = j - i; i < 6; i++ { + microsecond *= 10 } - nsec = usec * 1000 + nanosecond = microsecond * 1000 case 'Z': - k := j - for ; k < l; k++ { - if c := source[k]; c < 'A' || 'Z' < c { + i := j + for ; j < l; j++ { + if c := source[j]; c < 'A' || 'Z' < c { break } } - t, err = time.ParseInLocation("MST", source[j:k], base) + t, err = time.ParseInLocation("MST", source[i:j], base) if err != nil { - err = fmt.Errorf(`cannot parse %q with "%%Z"`, source[j:k]) + err = fmt.Errorf(`cannot parse %q with "%%Z"`, source[i:j]) return } - loc = t.Location() - j = k + 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 + sign = 1 switch source[j] { case '-': sign = -1 fallthrough case '+': - var hour, min, sec, k int - if hour, k, _ = parseNumber(source, j+1, 2, 'z'); k != j+3 { + 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 = k; j >= l || source[j] != ':' { - switch colons { - case 1: - err = errors.New("expected ':' for %:z") - return - case 2: - err = errors.New("expected ':' for %::z") + if j >= l || source[j] != ':' { + if colons > 0 && colons < 3 { + err = expectedColonForZFormatError(colons) return } } else if j++; colons == 0 { colons = 4 } - if min, k, _ = parseNumber(source, j, 2, 'z'); k != j+2 { - if colons == 0 { - k = j - } else { + 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 } - } - if j = k; colons > 1 { + j = i + } else if colons > 1 { if j >= l || source[j] != ':' { - if colons == 2 { - err = errors.New("expected ':' for %::z") - return - } - } else if sec, k, _ = parseNumber(source, j+1, 2, 'z'); k != j+3 { - if colons == 2 { - err = parseZFormatError(colons) + if colons < 3 { + err = expectedColonForZFormatError(colons) return } } else { - j = k + 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 + } } } - loc, colons = time.FixedZone("", sign*((hour*60+min)*60+sec)), 0 + 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: @@ -246,53 +271,57 @@ func parse(source, format string, loc, base *time.Location) (t time.Time, err er } j++ } else { - if i++; i == len(format) { - err = errors.New(`expected 'z' after "%:"`) - return - } else if b = format[i]; b == 'z' { - colons = 1 - } else if b != ':' { - err = errors.New(`expected 'z' after "%:"`) - return - } else if i++; i == len(format) { - err = errors.New(`expected 'z' after "%::"`) - return - } else if b = format[i]; b == 'z' { - colons = 2 - } else { - err = errors.New(`expected 'z' after "%::"`) - return + 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 + } } - goto L + err = expectedZAfterColonError(colons) + return } case 't', 'n': - k := j + i := j K: - for ; k < l; k++ { - switch source[k] { + for ; j < l; j++ { + switch source[j] { case ' ', '\t', '\n', '\v', '\f', '\r': default: break K } } - if k == j { - err = fmt.Errorf("expected a space for %%%c", b) + if i == j { + err = fmt.Errorf(`expected a space for "%%%c"`, b) return } - j = k 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 == "" { - var ok bool - if pending, ok = compositions[b]; ok { - break - } - err = fmt.Errorf(`unexpected format: "%%%c"`, b) + err = fmt.Errorf(`unexpected format "%%%c"`, b) return } if j >= l || source[j] != b { @@ -305,7 +334,7 @@ func parse(source, format string, loc, base *time.Location) (t time.Time, err er b, pending = pending[0], pending[1:] goto L } - } else if j >= len(source) || source[j] != b { + } else if j >= l || source[j] != b { err = expectedFormatError(b) return } else { @@ -313,25 +342,52 @@ func parse(source, format string, loc, base *time.Location) (t time.Time, err er } } if j < len(source) { - err = fmt.Errorf("unconverted string: %q", source[j:]) + err = fmt.Errorf("unparsed string %q", source[j:]) return } if pm { hour += 12 } - if century > 0 { + if century >= 0 { year = century*100 + year%100 } - if yday > 0 { - return time.Date(year, time.January, 1, hour, min, sec, nsec, loc).AddDate(0, 0, yday-1), nil + 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, min, sec, nsec, loc), nil + 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)) + return fmt.Sprintf(`cannot parse "%%%c"`, byte(err)) } type expectedFormatError byte @@ -343,46 +399,72 @@ func (err expectedFormatError) Error() string { type parseZFormatError int func (err parseZFormatError) Error() string { - switch int(err) { - case 0: - return "cannot parse %z" - case 1: - return "cannot parse %:z" - default: - return "cannot parse %::z" + 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 } -func parseNumber(source string, min, size int, format byte) (int, int, error) { - var val int - if l := len(source); min+size > l { - size = l - } else { - size += min +// 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 + } } - i := min - for ; i < size; i++ { - if b := source[i]; '0' <= b && b <= '9' { - val = val*10 + int(b&0x0F) + 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 == min { + if i == index { return 0, 0, parseFormatError(format) } - return val, i, nil + return value, i, nil } -func lookup(source string, min int, candidates []string, format byte) (int, int, error) { +func parseAny(source string, index int, candidates []string, format byte) (int, int, error) { L: for i, xs := range candidates { - j := min + 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|('a'-'A') != y|('a'-'A') { + if x, y := xs[k], source[j]; x != y && x|0x20 != y|0x20 { continue L } } diff --git a/vendor/github.com/itchyny/timefmt-go/timefmt.go b/vendor/github.com/itchyny/timefmt-go/timefmt.go new file mode 100644 index 0000000000..45bf6ae903 --- /dev/null +++ b/vendor/github.com/itchyny/timefmt-go/timefmt.go @@ -0,0 +1,2 @@ +// Package timefmt provides functions for formatting and parsing date time strings. +package timefmt diff --git a/vendor/modules.txt b/vendor/modules.txt index 780d04e44e..b67806bafc 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -310,8 +310,8 @@ github.com/inconshreveable/mousetrap # github.com/itchyny/gojq v0.12.9 ## explicit; go 1.17 github.com/itchyny/gojq -# github.com/itchyny/timefmt-go v0.1.4 -## explicit; go 1.17 +# github.com/itchyny/timefmt-go v0.1.8 +## explicit; go 1.24 github.com/itchyny/timefmt-go # github.com/jmespath/go-jmespath v0.4.0 ## explicit; go 1.14