├── .github └── workflows │ └── ci.yaml ├── CHANGELOG.md ├── LICENSE ├── Makefile ├── README.md ├── diff_test.go ├── format.go ├── format_test.go ├── go.mod ├── go.sum ├── parse.go ├── parse_test.go └── timefmt.go /.github/workflows/ci.yaml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | tags: 8 | - v* 9 | pull_request: 10 | 11 | permissions: 12 | contents: read 13 | 14 | jobs: 15 | test: 16 | name: Test 17 | runs-on: ubuntu-latest 18 | strategy: 19 | matrix: 20 | go: [1.23.x, 1.22.x, 1.21.x] 21 | fail-fast: false 22 | steps: 23 | - name: Checkout code 24 | uses: actions/checkout@v4 25 | - name: Setup Go 26 | uses: actions/setup-go@v5 27 | with: 28 | go-version: ${{ matrix.go }} 29 | - name: Test 30 | run: make test 31 | - name: Test with GOARCH=386 32 | run: env GOARCH=386 go test -v ./... 33 | - name: Test Coverage 34 | run: | 35 | go test -cover ./... | grep -F 100.0% || { 36 | go test -cover ./... 37 | echo Coverage decreased! 38 | exit 1 39 | } >&2 40 | - name: Lint 41 | run: make lint 42 | 43 | release: 44 | name: Release 45 | needs: test 46 | if: startsWith(github.ref, 'refs/tags/v') 47 | permissions: 48 | contents: write 49 | runs-on: ubuntu-latest 50 | steps: 51 | - name: Checkout code 52 | uses: actions/checkout@v4 53 | - name: Setup release body 54 | run: sed -n '/\[${{ github.ref_name }}\]/,/^$/{//!p}' CHANGELOG.md >release-body.txt 55 | - name: Create release 56 | uses: ncipollo/release-action@v1 57 | with: 58 | name: Release ${{ github.ref_name }} 59 | bodyFile: release-body.txt 60 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | ## [v0.1.6](https://github.com/itchyny/timefmt-go/compare/v0.1.5..v0.1.6) (2024-06-01) 3 | * support parsing week directives (`%A`, `%a`, `%w`, `%u`, `%V`, `%U`, `%W`) 4 | * validate range of values on parsing directives 5 | * fix formatting `%l` to show `12` at midnight 6 | 7 | ## [v0.1.5](https://github.com/itchyny/timefmt-go/compare/v0.1.4..v0.1.5) (2022-12-01) 8 | * support parsing time zone offset with name using both `%z` and `%Z` 9 | 10 | ## [v0.1.4](https://github.com/itchyny/timefmt-go/compare/v0.1.3..v0.1.4) (2022-09-01) 11 | * improve documents 12 | * drop support for Go 1.16 13 | 14 | ## [v0.1.3](https://github.com/itchyny/timefmt-go/compare/v0.1.2..v0.1.3) (2021-04-14) 15 | * implement `ParseInLocation` for configuring the default location 16 | 17 | ## [v0.1.2](https://github.com/itchyny/timefmt-go/compare/v0.1.1..v0.1.2) (2021-02-22) 18 | * implement parsing/formatting time zone offset with colons (`%:z`, `%::z`, `%:::z`) 19 | * recognize `Z` as UTC on parsing time zone offset (`%z`) 20 | * fix padding on formatting time zone offset (`%z`) 21 | 22 | ## [v0.1.1](https://github.com/itchyny/timefmt-go/compare/v0.1.0..v0.1.1) (2020-09-01) 23 | * fix overflow check in 32-bit architecture 24 | 25 | ## [v0.1.0](https://github.com/itchyny/timefmt-go/compare/2c02364..v0.1.0) (2020-08-16) 26 | * initial implementation 27 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2020-2022 itchyny 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | GOBIN ?= $(shell go env GOPATH)/bin 2 | 3 | .PHONY: all 4 | all: test 5 | 6 | .PHONY: test 7 | test: 8 | go test -v -race ./... 9 | 10 | .PHONY: lint 11 | lint: $(GOBIN)/staticcheck 12 | go vet ./... 13 | staticcheck -checks all,-ST1000 ./... 14 | 15 | $(GOBIN)/staticcheck: 16 | go install honnef.co/go/tools/cmd/staticcheck@latest 17 | 18 | .PHONY: clean 19 | clean: 20 | go clean 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # timefmt-go 2 | [![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) 3 | [![Go Report Card](https://goreportcard.com/badge/github.com/itchyny/timefmt-go)](https://goreportcard.com/report/github.com/itchyny/timefmt-go) 4 | [![MIT License](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/itchyny/timefmt-go/blob/main/LICENSE) 5 | [![release](https://img.shields.io/github/release/itchyny/timefmt-go/all.svg)](https://github.com/itchyny/timefmt-go/releases) 6 | [![pkg.go.dev](https://pkg.go.dev/badge/github.com/itchyny/timefmt-go)](https://pkg.go.dev/github.com/itchyny/timefmt-go) 7 | 8 | ### Efficient time formatting library (strftime, strptime) for Golang 9 | This is a Go language package for formatting and parsing date time strings. 10 | 11 | ```go 12 | package main 13 | 14 | import ( 15 | "fmt" 16 | "log" 17 | 18 | "github.com/itchyny/timefmt-go" 19 | ) 20 | 21 | func main() { 22 | t, err := timefmt.Parse("2020/07/24 09:07:29", "%Y/%m/%d %H:%M:%S") 23 | if err != nil { 24 | log.Fatal(err) 25 | } 26 | fmt.Println(t) // 2020-07-24 09:07:29 +0000 UTC 27 | 28 | str := timefmt.Format(t, "%Y/%m/%d %H:%M:%S") 29 | fmt.Println(str) // 2020/07/24 09:07:29 30 | 31 | str = timefmt.Format(t, "%a, %d %b %Y %T %z") 32 | fmt.Println(str) // Fri, 24 Jul 2020 09:07:29 +0000 33 | } 34 | ``` 35 | 36 | Please refer to [`man 3 strftime`](https://linux.die.net/man/3/strftime) and 37 | [`man 3 strptime`](https://linux.die.net/man/3/strptime) for formatters. 38 | As an extension, `%f` directive is supported for zero-padded microseconds, which originates from Python. 39 | Note that `E` and `O` modifier characters are not supported. 40 | 41 | ## Comparison to other libraries 42 | - This library 43 | - provides both formatting and parsing functions in pure Go language, 44 | - depends only on the Go standard libraries not to grow up dependency. 45 | - `Format` (`strftime`) implements glibc extensions including 46 | - width specifier like `%6Y %10B %4Z` (limited to 1024 bytes), 47 | - omitting padding modifier like `%-y-%-m-%-d`, 48 | - space padding modifier like `%_y-%_m-%_d`, 49 | - upper case modifier like `%^a %^b`, 50 | - swapping case modifier like `%#Z`, 51 | - time zone offset modifier like `%:z %::z %:::z`, 52 | - and its performance is very good. 53 | - `AppendFormat` is provided for reducing allocations. 54 | - `Parse` (`strptime`) allows to parse 55 | - composed directives like `%F %T`, 56 | - century years like `%C %y`, 57 | - week directives like `%W %a` and `%G-W%V-%u`. 58 | - `ParseInLocation` is provided for configuring the default location. 59 | 60 | ![](https://user-images.githubusercontent.com/375258/88606920-de475c80-d0b8-11ea-8d40-cbfee9e35c2e.jpg) 61 | 62 | ## Bug Tracker 63 | Report bug at [Issues・itchyny/timefmt-go - GitHub](https://github.com/itchyny/timefmt-go/issues). 64 | 65 | ## Author 66 | itchyny () 67 | 68 | ## License 69 | This software is released under the MIT License, see LICENSE. 70 | -------------------------------------------------------------------------------- /diff_test.go: -------------------------------------------------------------------------------- 1 | package timefmt_test 2 | 3 | import "strings" 4 | 5 | type stringBuilder struct { 6 | strings.Builder 7 | } 8 | 9 | func (sb *stringBuilder) writeDiff(s string) { 10 | sb.WriteString("\x1b[1;4m") 11 | sb.WriteString(s) 12 | sb.WriteString("\x1b[0m") 13 | } 14 | 15 | func diff(expected, got string) string { 16 | var sbx, sby stringBuilder 17 | xs := strings.Split(expected, " ") 18 | ys := strings.Split(got, " ") 19 | for i, j := 0, 0; ; i, j = i+1, j+1 { 20 | if i >= len(xs) { 21 | if j >= len(ys) { 22 | break 23 | } 24 | if j > 0 { 25 | sby.writeDiff(" ") 26 | } 27 | sby.writeDiff(ys[j]) 28 | continue 29 | } else if j >= len(ys) { 30 | if i > 0 { 31 | sbx.writeDiff(" ") 32 | } 33 | sbx.writeDiff(xs[i]) 34 | continue 35 | } 36 | if xs[i] == "" { 37 | if ys[j] != "" { 38 | sbx.writeDiff(" ") 39 | j-- 40 | continue 41 | } 42 | } else if ys[j] == "" { 43 | sby.writeDiff(" ") 44 | i-- 45 | continue 46 | } 47 | if i > 0 { 48 | sbx.WriteByte(' ') 49 | } 50 | if j > 0 { 51 | sby.WriteByte(' ') 52 | } 53 | if xs[i] == ys[j] { 54 | sbx.WriteString(xs[i]) 55 | sby.WriteString(ys[j]) 56 | } else { 57 | sbx.writeDiff(xs[i]) 58 | sby.writeDiff(ys[j]) 59 | } 60 | } 61 | return "diff:\nexpected: " + sbx.String() + "\n got: " + sby.String() 62 | } 63 | -------------------------------------------------------------------------------- /format.go: -------------------------------------------------------------------------------- 1 | package timefmt 2 | 3 | import ( 4 | "strconv" 5 | "time" 6 | ) 7 | 8 | // Format time to string using the format. 9 | func Format(t time.Time, format string) string { 10 | return string(AppendFormat(make([]byte, 0, 64), t, format)) 11 | } 12 | 13 | // AppendFormat appends formatted time string to the buffer. 14 | func AppendFormat(buf []byte, t time.Time, format string) []byte { 15 | year, month, day := t.Date() 16 | hour, minute, second := t.Clock() 17 | var width, colons int 18 | var padding byte 19 | var pending string 20 | var upper, swap bool 21 | for i := 0; i < len(format); i++ { 22 | if b := format[i]; b == '%' { 23 | if i++; i == len(format) { 24 | buf = append(buf, '%') 25 | break 26 | } 27 | b, width, padding, upper, swap = format[i], 0, '0', false, false 28 | L: 29 | switch b { 30 | case '-': 31 | if pending != "" { 32 | buf = append(buf, '-') 33 | break 34 | } 35 | if i++; i == len(format) { 36 | goto K 37 | } 38 | padding = ^paddingMask 39 | b = format[i] 40 | goto L 41 | case '_': 42 | if i++; i == len(format) { 43 | goto K 44 | } 45 | padding = ' ' | ^paddingMask 46 | b = format[i] 47 | goto L 48 | case '^': 49 | if i++; i == len(format) { 50 | goto K 51 | } 52 | upper = true 53 | b = format[i] 54 | goto L 55 | case '#': 56 | if i++; i == len(format) { 57 | goto K 58 | } 59 | swap = true 60 | b = format[i] 61 | goto L 62 | case '0': 63 | if i++; i == len(format) { 64 | goto K 65 | } 66 | padding = '0' | ^paddingMask 67 | b = format[i] 68 | goto L 69 | case '1', '2', '3', '4', '5', '6', '7', '8', '9': 70 | width = int(b & 0x0F) 71 | for i++; i < len(format); i++ { 72 | if b = format[i]; b <= '9' && '0' <= b { 73 | width = min(width*10+int(b&0x0F), 1024) 74 | } else { 75 | break 76 | } 77 | } 78 | if padding == ^paddingMask { 79 | padding = ' ' | ^paddingMask 80 | } 81 | if i == len(format) { 82 | goto K 83 | } 84 | goto L 85 | case 'Y': 86 | buf = appendInt(buf, year, or(width, 4), padding) 87 | case 'y': 88 | buf = appendInt(buf, year%100, max(width, 2), padding) 89 | case 'C': 90 | buf = appendInt(buf, year/100, max(width, 2), padding) 91 | case 'g': 92 | year, _ := t.ISOWeek() 93 | buf = appendInt(buf, year%100, max(width, 2), padding) 94 | case 'G': 95 | year, _ := t.ISOWeek() 96 | buf = appendInt(buf, year, or(width, 4), padding) 97 | case 'm': 98 | buf = appendInt(buf, int(month), max(width, 2), padding) 99 | case 'B': 100 | buf = appendString(buf, longMonthNames[month-1], width, padding, upper, swap) 101 | case 'b', 'h': 102 | buf = appendString(buf, shortMonthNames[month-1], width, padding, upper, swap) 103 | case 'A': 104 | buf = appendString(buf, longWeekNames[t.Weekday()], width, padding, upper, swap) 105 | case 'a': 106 | buf = appendString(buf, shortWeekNames[t.Weekday()], width, padding, upper, swap) 107 | case 'w': 108 | buf = appendInt(buf, int(t.Weekday()), width, padding) 109 | case 'u': 110 | buf = appendInt(buf, or(int(t.Weekday()), 7), width, padding) 111 | case 'V': 112 | _, week := t.ISOWeek() 113 | buf = appendInt(buf, week, max(width, 2), padding) 114 | case 'U': 115 | week := (t.YearDay() + 6 - int(t.Weekday())) / 7 116 | buf = appendInt(buf, week, max(width, 2), padding) 117 | case 'W': 118 | week := t.YearDay() 119 | if int(t.Weekday()) > 0 { 120 | week -= int(t.Weekday()) - 7 121 | } 122 | week /= 7 123 | buf = appendInt(buf, week, max(width, 2), padding) 124 | case 'e': 125 | if padding < ^paddingMask { 126 | padding = ' ' 127 | } 128 | fallthrough 129 | case 'd': 130 | buf = appendInt(buf, day, max(width, 2), padding) 131 | case 'j': 132 | buf = appendInt(buf, t.YearDay(), max(width, 3), padding) 133 | case 'k': 134 | if padding < ^paddingMask { 135 | padding = ' ' 136 | } 137 | fallthrough 138 | case 'H': 139 | buf = appendInt(buf, hour, max(width, 2), padding) 140 | case 'l': 141 | if padding < ^paddingMask { 142 | padding = ' ' 143 | } 144 | fallthrough 145 | case 'I': 146 | buf = appendInt(buf, or(hour%12, 12), max(width, 2), padding) 147 | case 'P': 148 | swap = !(upper || swap) 149 | fallthrough 150 | case 'p': 151 | if hour < 12 { 152 | buf = appendString(buf, "AM", width, padding, upper, swap) 153 | } else { 154 | buf = appendString(buf, "PM", width, padding, upper, swap) 155 | } 156 | case 'M': 157 | buf = appendInt(buf, minute, max(width, 2), padding) 158 | case 'S': 159 | buf = appendInt(buf, second, max(width, 2), padding) 160 | case 's': 161 | if padding < ^paddingMask { 162 | padding = ' ' 163 | } 164 | buf = appendInt(buf, int(t.Unix()), width, padding) 165 | case 'f': 166 | buf = appendInt(buf, t.Nanosecond()/1000, or(width, 6), padding) 167 | case 'Z', 'z': 168 | name, offset := t.Zone() 169 | if b == 'Z' && name != "" { 170 | buf = appendString(buf, name, width, padding, upper, swap) 171 | break 172 | } 173 | i := len(buf) 174 | if padding != ^paddingMask { 175 | for ; width > 1; width-- { 176 | buf = append(buf, padding&paddingMask) 177 | } 178 | } 179 | j := len(buf) 180 | if offset < 0 { 181 | buf = append(buf, '-') 182 | offset = -offset 183 | } else { 184 | buf = append(buf, '+') 185 | } 186 | k := len(buf) 187 | buf = appendInt(buf, offset/3600, 2, padding) 188 | if buf[k] == ' ' { 189 | buf[k-1], buf[k] = buf[k], buf[k-1] 190 | } 191 | if offset %= 3600; colons <= 2 || offset != 0 { 192 | if colons != 0 { 193 | buf = append(buf, ':') 194 | } 195 | buf = appendInt(buf, offset/60, 2, '0') 196 | if offset %= 60; colons == 2 || colons == 3 && offset != 0 { 197 | buf = append(buf, ':') 198 | buf = appendInt(buf, offset, 2, '0') 199 | } 200 | } 201 | colons = 0 202 | if k = min(len(buf)-j-1, j-i); k > 0 { 203 | copy(buf[j-k:], buf[j:]) 204 | buf = buf[:len(buf)-k] 205 | if padding&paddingMask == '0' { 206 | buf[i], buf[j-k] = buf[j-k], buf[i] 207 | } 208 | } 209 | case ':': 210 | if pending != "" { 211 | buf = append(buf, ':') 212 | } else { 213 | colons = 1 214 | M: 215 | for i++; i < len(format); i++ { 216 | switch format[i] { 217 | case ':': 218 | colons++ 219 | case 'z': 220 | if colons > 3 { 221 | i++ 222 | break M 223 | } 224 | b = 'z' 225 | goto L 226 | default: 227 | break M 228 | } 229 | } 230 | buf = appendLast(buf, format[:i], width, padding) 231 | i-- 232 | colons = 0 233 | } 234 | case 't': 235 | buf = appendString(buf, "\t", width, padding, false, false) 236 | case 'n': 237 | buf = appendString(buf, "\n", width, padding, false, false) 238 | case '%': 239 | buf = appendString(buf, "%", width, padding, false, false) 240 | default: 241 | if pending == "" { 242 | var ok bool 243 | if pending, ok = compositions[b]; ok { 244 | swap = false 245 | break 246 | } 247 | buf = appendLast(buf, format[:i], width-1, padding) 248 | } 249 | buf = append(buf, b) 250 | } 251 | if pending != "" { 252 | b, pending, width, padding = pending[0], pending[1:], 0, '0' 253 | goto L 254 | } 255 | } else { 256 | buf = append(buf, b) 257 | } 258 | } 259 | return buf 260 | K: 261 | return appendLast(buf, format, width, padding) 262 | } 263 | 264 | func appendInt(buf []byte, num, width int, padding byte) []byte { 265 | if padding != ^paddingMask { 266 | padding &= paddingMask 267 | switch width { 268 | case 2: 269 | if num < 10 { 270 | buf = append(buf, padding) 271 | goto L1 272 | } else if num < 100 { 273 | goto L2 274 | } else if num < 1000 { 275 | goto L3 276 | } else if num < 10000 { 277 | goto L4 278 | } 279 | case 4: 280 | if num < 1000 { 281 | buf = append(buf, padding) 282 | if num < 100 { 283 | buf = append(buf, padding) 284 | if num < 10 { 285 | buf = append(buf, padding) 286 | goto L1 287 | } 288 | goto L2 289 | } 290 | goto L3 291 | } else if num < 10000 { 292 | goto L4 293 | } 294 | default: 295 | i := len(buf) 296 | for ; width > 1; width-- { 297 | buf = append(buf, padding) 298 | } 299 | j := len(buf) 300 | buf = strconv.AppendInt(buf, int64(num), 10) 301 | if k := min(len(buf)-j-1, j-i); k > 0 { 302 | copy(buf[j-k:], buf[j:]) 303 | buf = buf[:len(buf)-k] 304 | } 305 | return buf 306 | } 307 | } 308 | if num < 100 { 309 | if num < 10 { 310 | goto L1 311 | } 312 | goto L2 313 | } else if num < 10000 { 314 | if num < 1000 { 315 | goto L3 316 | } 317 | goto L4 318 | } 319 | return strconv.AppendInt(buf, int64(num), 10) 320 | L4: 321 | buf = append(buf, byte(num/1000)|'0') 322 | num %= 1000 323 | L3: 324 | buf = append(buf, byte(num/100)|'0') 325 | num %= 100 326 | L2: 327 | buf = append(buf, byte(num/10)|'0') 328 | num %= 10 329 | L1: 330 | return append(buf, byte(num)|'0') 331 | } 332 | 333 | func appendString(buf []byte, str string, width int, padding byte, upper, swap bool) []byte { 334 | if width > len(str) && padding != ^paddingMask { 335 | if padding < ^paddingMask { 336 | padding = ' ' 337 | } else { 338 | padding &= paddingMask 339 | } 340 | for width -= len(str); width > 0; width-- { 341 | buf = append(buf, padding) 342 | } 343 | } 344 | switch { 345 | case swap: 346 | if str[1] < 'a' { 347 | for _, b := range []byte(str) { 348 | buf = append(buf, b|0x20) 349 | } 350 | break 351 | } 352 | fallthrough 353 | case upper: 354 | for _, b := range []byte(str) { 355 | buf = append(buf, b&0x5F) 356 | } 357 | default: 358 | buf = append(buf, str...) 359 | } 360 | return buf 361 | } 362 | 363 | func appendLast(buf []byte, format string, width int, padding byte) []byte { 364 | for i := len(format) - 1; i >= 0; i-- { 365 | if format[i] == '%' { 366 | buf = appendString(buf, format[i:], width, padding, false, false) 367 | break 368 | } 369 | } 370 | return buf 371 | } 372 | 373 | func or(x, y int) int { 374 | if x != 0 { 375 | return x 376 | } 377 | return y 378 | } 379 | 380 | const paddingMask byte = 0x7F 381 | 382 | var longMonthNames = []string{ 383 | "January", 384 | "February", 385 | "March", 386 | "April", 387 | "May", 388 | "June", 389 | "July", 390 | "August", 391 | "September", 392 | "October", 393 | "November", 394 | "December", 395 | } 396 | 397 | var shortMonthNames = []string{ 398 | "Jan", 399 | "Feb", 400 | "Mar", 401 | "Apr", 402 | "May", 403 | "Jun", 404 | "Jul", 405 | "Aug", 406 | "Sep", 407 | "Oct", 408 | "Nov", 409 | "Dec", 410 | } 411 | 412 | var longWeekNames = []string{ 413 | "Sunday", 414 | "Monday", 415 | "Tuesday", 416 | "Wednesday", 417 | "Thursday", 418 | "Friday", 419 | "Saturday", 420 | } 421 | 422 | var shortWeekNames = []string{ 423 | "Sun", 424 | "Mon", 425 | "Tue", 426 | "Wed", 427 | "Thu", 428 | "Fri", 429 | "Sat", 430 | } 431 | 432 | var compositions = map[byte]string{ 433 | 'c': "a b e H:M:S Y", 434 | '+': "a b e H:M:S Z Y", 435 | 'F': "Y-m-d", 436 | 'D': "m/d/y", 437 | 'x': "m/d/y", 438 | 'v': "e-b-Y", 439 | 'T': "H:M:S", 440 | 'X': "H:M:S", 441 | 'r': "I:M:S p", 442 | 'R': "H:M", 443 | } 444 | -------------------------------------------------------------------------------- /format_test.go: -------------------------------------------------------------------------------- 1 | package timefmt_test 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | "testing" 7 | "time" 8 | 9 | "github.com/itchyny/timefmt-go" 10 | ) 11 | 12 | var formatTestCases = []struct { 13 | format string 14 | t time.Time 15 | expected string 16 | }{ 17 | { 18 | format: "%Y", 19 | t: time.Date(2020, time.January, 1, 0, 0, 0, 0, time.UTC), 20 | expected: "2020", 21 | }, 22 | { 23 | format: "%Y %1Y %2Y %3Y %4Y %5Y %-Y", 24 | t: time.Date(1000, time.January, 1, 0, 0, 0, 0, time.UTC), 25 | expected: "1000 1000 1000 1000 1000 01000 1000", 26 | }, 27 | { 28 | format: "%Y %1Y %2Y %3Y %4Y %5Y %-Y", 29 | t: time.Date(999, time.January, 1, 0, 0, 0, 0, time.UTC), 30 | expected: "0999 999 999 999 0999 00999 999", 31 | }, 32 | { 33 | format: "%Y %1Y %2Y %3Y %4Y %5Y %-Y", 34 | t: time.Date(100, time.January, 1, 0, 0, 0, 0, time.UTC), 35 | expected: "0100 100 100 100 0100 00100 100", 36 | }, 37 | { 38 | format: "%Y %1Y %2Y %3Y %4Y %5Y %-Y", 39 | t: time.Date(99, time.January, 1, 0, 0, 0, 0, time.UTC), 40 | expected: "0099 99 99 099 0099 00099 99", 41 | }, 42 | { 43 | format: "%Y %1Y %2Y %3Y %4Y %5Y %-Y", 44 | t: time.Date(9999, time.January, 1, 0, 0, 0, 0, time.UTC), 45 | expected: "9999 9999 9999 9999 9999 09999 9999", 46 | }, 47 | { 48 | format: "%Y %1Y %2Y %3Y %4Y %5Y %-Y", 49 | t: time.Date(10000, time.January, 1, 0, 0, 0, 0, time.UTC), 50 | expected: "10000 10000 10000 10000 10000 10000 10000", 51 | }, 52 | { 53 | format: "[%Y]", 54 | t: time.Date(2020, time.January, 1, 0, 0, 0, 0, time.UTC), 55 | expected: "[2020]", 56 | }, 57 | { 58 | format: "%y %_y %-y %4y %_4y %04y %_04y %0_4y", 59 | t: time.Date(2020, time.January, 1, 0, 0, 0, 0, time.UTC), 60 | expected: "20 20 20 0020 20 0020 0020 20", 61 | }, 62 | { 63 | format: "%y %_y %-y %4y %_4y %04y %_04y %0_4y", 64 | t: time.Date(2009, time.January, 1, 0, 0, 0, 0, time.UTC), 65 | expected: "09 9 9 0009 9 0009 0009 9", 66 | }, 67 | { 68 | format: "%C", 69 | t: time.Date(2009, time.January, 1, 0, 0, 0, 0, time.UTC), 70 | expected: "20", 71 | }, 72 | { 73 | format: "%C%y", 74 | t: time.Date(1758, time.January, 1, 0, 0, 0, 0, time.UTC), 75 | expected: "1758", 76 | }, 77 | { 78 | format: "%Y-%m", 79 | t: time.Date(2020, time.May, 1, 0, 0, 0, 0, time.UTC), 80 | expected: "2020-05", 81 | }, 82 | { 83 | format: "%Y-%m-%d", 84 | t: time.Date(2020, time.September, 10, 0, 0, 0, 0, time.UTC), 85 | expected: "2020-09-10", 86 | }, 87 | { 88 | format: "%-Y-%-m-%-d", 89 | t: time.Date(999, time.September, 8, 0, 0, 0, 0, time.UTC), 90 | expected: "999-9-8", 91 | }, 92 | { 93 | format: "%_Y-%_m-%_d", 94 | t: time.Date(999, time.September, 8, 0, 0, 0, 0, time.UTC), 95 | expected: " 999- 9- 8", 96 | }, 97 | { 98 | format: "%8Y-%12m-%16d", 99 | t: time.Date(2020, time.September, 12, 0, 0, 0, 0, time.UTC), 100 | expected: "00002020-000000000009-0000000000000012", 101 | }, 102 | { 103 | format: "%_8Y-%_12m-%_16d", 104 | t: time.Date(2020, time.September, 12, 0, 0, 0, 0, time.UTC), 105 | expected: " 2020- 9- 12", 106 | }, 107 | { 108 | format: "%Y/%m/%d", 109 | t: time.Date(2020, time.October, 9, 0, 0, 0, 0, time.UTC), 110 | expected: "2020/10/09", 111 | }, 112 | { 113 | format: "%e %-e %_e %4e %04e", 114 | t: time.Date(2020, time.January, 9, 0, 0, 0, 0, time.UTC), 115 | expected: " 9 9 9 9 0009", 116 | }, 117 | { 118 | format: "%B %_B %^B %#B %12B %^12B %012B %0^12B", 119 | t: time.Date(2020, time.October, 1, 0, 0, 0, 0, time.UTC), 120 | expected: "October October OCTOBER OCTOBER October OCTOBER 00000October 00000OCTOBER", 121 | }, 122 | { 123 | format: "%b %^b %#b %8b %^8b %08b %^08b %#08b", 124 | t: time.Date(2020, time.September, 1, 0, 0, 0, 0, time.UTC), 125 | expected: "Sep SEP SEP Sep SEP 00000Sep 00000SEP 00000SEP", 126 | }, 127 | { 128 | format: "%h %^h %#h %8h %^8h %08h %^08h", 129 | t: time.Date(2020, time.November, 1, 0, 0, 0, 0, time.UTC), 130 | expected: "Nov NOV NOV Nov NOV 00000Nov 00000NOV", 131 | }, 132 | { 133 | format: "%A %a %w %u", 134 | t: time.Date(2020, time.January, 1, 0, 0, 0, 0, time.UTC), 135 | expected: "Wednesday Wed 3 3", 136 | }, 137 | { 138 | format: "%A %a %w %u", 139 | t: time.Date(2020, time.January, 2, 0, 0, 0, 0, time.UTC), 140 | expected: "Thursday Thu 4 4", 141 | }, 142 | { 143 | format: "%A %a %w %u", 144 | t: time.Date(2020, time.January, 4, 0, 0, 0, 0, time.UTC), 145 | expected: "Saturday Sat 6 6", 146 | }, 147 | { 148 | format: "%A %a %w %u", 149 | t: time.Date(2020, time.January, 5, 0, 0, 0, 0, time.UTC), 150 | expected: "Sunday Sun 0 7", 151 | }, 152 | { 153 | format: "%A %a %w %u", 154 | t: time.Date(2020, time.January, 6, 0, 0, 0, 0, time.UTC), 155 | expected: "Monday Mon 1 1", 156 | }, 157 | { 158 | format: "%^A %#A %8A %^8A %08A %^08A %^a %#a %8a %^8a %08a %^08a", 159 | t: time.Date(2020, time.January, 6, 0, 0, 0, 0, time.UTC), 160 | expected: "MONDAY MONDAY Monday MONDAY 00Monday 00MONDAY MON MON Mon MON 00000Mon 00000MON", 161 | }, 162 | { 163 | format: "%4w %4u %-4w %-4u %_4w %_4u %04w %04u", 164 | t: time.Date(2020, time.January, 6, 0, 0, 0, 0, time.UTC), 165 | expected: "0001 0001 1 1 1 1 0001 0001", 166 | }, 167 | { 168 | format: "%g %G %a %V %U %W", 169 | t: time.Date(2020, time.January, 1, 0, 0, 0, 0, time.UTC), 170 | expected: "20 2020 Wed 01 00 00", 171 | }, 172 | { 173 | format: "%g %G %a %V %U %W", 174 | t: time.Date(2020, time.January, 4, 0, 0, 0, 0, time.UTC), 175 | expected: "20 2020 Sat 01 00 00", 176 | }, 177 | { 178 | format: "%g %G %a %V %U %W", 179 | t: time.Date(2020, time.January, 5, 0, 0, 0, 0, time.UTC), 180 | expected: "20 2020 Sun 01 01 00", 181 | }, 182 | { 183 | format: "%g %G %a %V %U %W", 184 | t: time.Date(2020, time.January, 6, 0, 0, 0, 0, time.UTC), 185 | expected: "20 2020 Mon 02 01 01", 186 | }, 187 | { 188 | format: "%-V %-U %-W %_V %_U %_W %4V %4U %4W %_4V %_4U %_4W %04V %04U %04W", 189 | t: time.Date(2020, time.January, 6, 0, 0, 0, 0, time.UTC), 190 | expected: "2 1 1 2 1 1 0002 0001 0001 2 1 1 0002 0001 0001", 191 | }, 192 | { 193 | format: "%g %G %a %V %U %W", 194 | t: time.Date(2009, time.December, 31, 0, 0, 0, 0, time.UTC), 195 | expected: "09 2009 Thu 53 52 52", 196 | }, 197 | { 198 | format: "%g %G %a %V %U %W", 199 | t: time.Date(2010, time.January, 1, 0, 0, 0, 0, time.UTC), 200 | expected: "09 2009 Fri 53 00 00", 201 | }, 202 | { 203 | format: "%g %G %a %V %U %W", 204 | t: time.Date(2010, time.January, 2, 0, 0, 0, 0, time.UTC), 205 | expected: "09 2009 Sat 53 00 00", 206 | }, 207 | { 208 | format: "%g %G %a %V %U %W", 209 | t: time.Date(2010, time.January, 3, 0, 0, 0, 0, time.UTC), 210 | expected: "09 2009 Sun 53 01 00", 211 | }, 212 | { 213 | format: "%g %G %a %V %U %W", 214 | t: time.Date(2010, time.January, 4, 0, 0, 0, 0, time.UTC), 215 | expected: "10 2010 Mon 01 01 01", 216 | }, 217 | { 218 | format: "%Y-%j-%-j", 219 | t: time.Date(2020, time.January, 1, 0, 0, 0, 0, time.UTC), 220 | expected: "2020-001-1", 221 | }, 222 | { 223 | format: "%Y-%j-%-j", 224 | t: time.Date(2020, time.January, 10, 0, 0, 0, 0, time.UTC), 225 | expected: "2020-010-10", 226 | }, 227 | { 228 | format: "%Y-%j-%-j", 229 | t: time.Date(2020, time.February, 2, 0, 0, 0, 0, time.UTC), 230 | expected: "2020-033-33", 231 | }, 232 | { 233 | format: "%Y-%j-%-j", 234 | t: time.Date(2020, time.April, 8, 0, 0, 0, 0, time.UTC), 235 | expected: "2020-099-99", 236 | }, 237 | { 238 | format: "%Y-%j-%-j", 239 | t: time.Date(2020, time.April, 9, 0, 0, 0, 0, time.UTC), 240 | expected: "2020-100-100", 241 | }, 242 | { 243 | format: "%Y-%j-%-j", 244 | t: time.Date(2020, time.April, 10, 0, 0, 0, 0, time.UTC), 245 | expected: "2020-101-101", 246 | }, 247 | { 248 | format: "%Y-%j-%-j", 249 | t: time.Date(2020, time.August, 1, 0, 0, 0, 0, time.UTC), 250 | expected: "2020-214-214", 251 | }, 252 | { 253 | format: "%Y-%j-%-j", 254 | t: time.Date(2020, time.December, 31, 0, 0, 0, 0, time.UTC), 255 | expected: "2020-366-366", 256 | }, 257 | { 258 | format: "%Y-%m-%d %H:%M:%S.%f", 259 | t: time.Date(2020, time.September, 8, 7, 6, 5, 43210000, time.UTC), 260 | expected: "2020-09-08 07:06:05.043210", 261 | }, 262 | { 263 | format: "%-y-%-m-%-d %-H:%-M:%-S.%-f", 264 | t: time.Date(2002, time.September, 8, 7, 6, 5, 43210000, time.UTC), 265 | expected: "2-9-8 7:6:5.43210", 266 | }, 267 | { 268 | format: "%_y-%_m-%_d %_H:%_M:%_S.%_f", 269 | t: time.Date(2002, time.September, 8, 7, 6, 5, 43210000, time.UTC), 270 | expected: " 2- 9- 8 7: 6: 5. 43210", 271 | }, 272 | { 273 | format: "%4y-%4m-%4d %4H:%4M:%4S.%8f", 274 | t: time.Date(2002, time.September, 8, 7, 6, 5, 43210000, time.UTC), 275 | expected: "0002-0009-0008 0007:0006:0005.00043210", 276 | }, 277 | { 278 | format: "%04y-%04m-%04d %04H:%04M:%04S.%08f", 279 | t: time.Date(2002, time.September, 8, 7, 6, 5, 43210000, time.UTC), 280 | expected: "0002-0009-0008 0007:0006:0005.00043210", 281 | }, 282 | { 283 | format: "%H:%M:%S.%f", 284 | t: time.Date(2020, time.January, 1, 1, 2, 3, 450000000, time.UTC), 285 | expected: "01:02:03.450000", 286 | }, 287 | { 288 | format: "%H:%M:%S %k %I %l %p %P", 289 | t: time.Date(2020, time.January, 1, 0, 2, 3, 0, time.UTC), 290 | expected: "00:02:03 0 12 12 AM am", 291 | }, 292 | { 293 | format: "%H:%M:%S %k %I %l %p %P", 294 | t: time.Date(2020, time.January, 1, 1, 2, 3, 0, time.UTC), 295 | expected: "01:02:03 1 01 1 AM am", 296 | }, 297 | { 298 | format: "%H:%M:%S %k %I %l %p %P", 299 | t: time.Date(2020, time.January, 1, 11, 2, 3, 0, time.UTC), 300 | expected: "11:02:03 11 11 11 AM am", 301 | }, 302 | { 303 | format: "%H:%M:%S %k %I %l %p %P", 304 | t: time.Date(2020, time.January, 1, 12, 2, 3, 0, time.UTC), 305 | expected: "12:02:03 12 12 12 PM pm", 306 | }, 307 | { 308 | format: "%H:%M:%S %k %I %l %p %P", 309 | t: time.Date(2020, time.January, 1, 13, 2, 3, 0, time.UTC), 310 | expected: "13:02:03 13 01 1 PM pm", 311 | }, 312 | { 313 | format: "%H:%M:%S %k %I %l %p %P", 314 | t: time.Date(2020, time.January, 1, 23, 2, 3, 0, time.UTC), 315 | expected: "23:02:03 23 11 11 PM pm", 316 | }, 317 | { 318 | format: "%-H:%-M:%-S %-k %-I %-l %-p %-P", 319 | t: time.Date(2020, time.January, 1, 13, 2, 3, 0, time.UTC), 320 | expected: "13:2:3 13 1 1 PM pm", 321 | }, 322 | { 323 | format: "%4H:%4M:%4S %4k %4I %4l %4p %4P", 324 | t: time.Date(2020, time.January, 1, 13, 2, 3, 0, time.UTC), 325 | expected: "0013:0002:0003 13 0001 1 PM pm", 326 | }, 327 | { 328 | format: "%_H:%_M:%_S %_k %_I %_l %_p %_P", 329 | t: time.Date(2020, time.January, 1, 13, 2, 3, 0, time.UTC), 330 | expected: "13: 2: 3 13 1 1 PM pm", 331 | }, 332 | { 333 | format: "%_4H:%_4M:%_4S %_4k %_4I %_4l %_4p %_4P", 334 | t: time.Date(2020, time.January, 1, 13, 2, 3, 0, time.UTC), 335 | expected: " 13: 2: 3 13 1 1 PM pm", 336 | }, 337 | { 338 | format: "%-4H:%-4M:%-4S %-4k %-4I %-4l %-4p %-4P", 339 | t: time.Date(2020, time.January, 1, 13, 2, 3, 0, time.UTC), 340 | expected: " 13: 2: 3 13 1 1 PM pm", 341 | }, 342 | { 343 | format: "%04H:%04M:%04S %04k %04I %04l %04p %04P", 344 | t: time.Date(2020, time.January, 1, 13, 2, 3, 0, time.UTC), 345 | expected: "0013:0002:0003 0013 0001 0001 00PM 00pm", 346 | }, 347 | { 348 | format: "%p %P %^p %^P %#p %#P %#^p %_4p %_4P %04p %04P %^04p %^04P", 349 | t: time.Date(2020, time.January, 1, 11, 2, 3, 0, time.UTC), 350 | expected: "AM am AM AM am AM am AM am 00AM 00am 00AM 00AM", 351 | }, 352 | { 353 | format: "%p %P %^p %^P %#p %#P %#^p %_4p %_4P %04p %04P %^04p %^04P", 354 | t: time.Date(2020, time.January, 1, 23, 2, 3, 0, time.UTC), 355 | expected: "PM pm PM PM pm PM pm PM pm 00PM 00pm 00PM 00PM", 356 | }, 357 | { 358 | format: "%k %-k %_k %4k %0k %04k", 359 | t: time.Date(2020, time.January, 1, 9, 0, 0, 0, time.UTC), 360 | expected: " 9 9 9 9 09 0009", 361 | }, 362 | { 363 | format: "%l %-l %_l %4l %0l %04l", 364 | t: time.Date(2020, time.January, 1, 20, 0, 0, 0, time.UTC), 365 | expected: " 8 8 8 8 08 0008", 366 | }, 367 | { 368 | format: "%s %12s %_12s %012s", 369 | t: time.Date(2020, time.August, 30, 5, 30, 32, 0, time.UTC), 370 | expected: "1598765432 1598765432 1598765432 001598765432", 371 | }, 372 | { 373 | format: "%R %r %T %D %x %X", 374 | t: time.Date(2020, time.February, 9, 23, 14, 15, 0, time.UTC), 375 | expected: "23:14 11:14:15 PM 23:14:15 02/09/20 02/09/20 23:14:15", 376 | }, 377 | { 378 | format: "%F %T", 379 | t: time.Date(2020, time.February, 9, 23, 14, 15, 0, time.UTC), 380 | expected: "2020-02-09 23:14:15", 381 | }, 382 | { 383 | format: "%9F %9T", 384 | t: time.Date(2020, time.February, 9, 23, 14, 15, 0, time.UTC), 385 | expected: "2020-02-09 23:14:15", 386 | }, 387 | { 388 | format: "%v %X", 389 | t: time.Date(2020, time.July, 9, 23, 14, 15, 0, time.UTC), 390 | expected: " 9-Jul-2020 23:14:15", 391 | }, 392 | { 393 | format: "%c", 394 | t: time.Date(2020, time.February, 9, 23, 14, 15, 0, time.UTC), 395 | expected: "Sun Feb 9 23:14:15 2020", 396 | }, 397 | { 398 | format: "%-c", 399 | t: time.Date(2020, time.February, 9, 23, 4, 5, 0, time.UTC), 400 | expected: "Sun Feb 9 23:04:05 2020", 401 | }, 402 | { 403 | format: "%^c", 404 | t: time.Date(2020, time.February, 9, 23, 4, 5, 0, time.UTC), 405 | expected: "SUN FEB 9 23:04:05 2020", 406 | }, 407 | { 408 | format: "%#c", 409 | t: time.Date(2020, time.February, 9, 23, 4, 5, 0, time.UTC), 410 | expected: "Sun Feb 9 23:04:05 2020", 411 | }, 412 | { 413 | format: "%+", 414 | t: time.Date(2020, time.February, 9, 23, 4, 5, 0, time.UTC), 415 | expected: "Sun Feb 9 23:04:05 UTC 2020", 416 | }, 417 | { 418 | format: "%^+", 419 | t: time.Date(2020, time.February, 9, 23, 4, 5, 0, time.UTC), 420 | expected: "SUN FEB 9 23:04:05 UTC 2020", 421 | }, 422 | { 423 | format: "%#+", 424 | t: time.Date(2020, time.February, 9, 23, 4, 5, 0, time.UTC), 425 | expected: "Sun Feb 9 23:04:05 UTC 2020", 426 | }, 427 | { 428 | format: "%F %T %z %-z %_4z %04z", 429 | t: time.Date(2020, time.July, 24, 23, 14, 15, 0, time.UTC), 430 | expected: "2020-07-24 23:14:15 +0000 +000 +000 +0000", 431 | }, 432 | { 433 | format: "%F %T %z", 434 | t: time.Date(2020, time.July, 24, 23, 14, 15, 0, time.FixedZone("", -8*60*60)), 435 | expected: "2020-07-24 23:14:15 -0800", 436 | }, 437 | { 438 | format: "%F %T %z", 439 | t: time.Date(2020, time.July, 24, 23, 14, 15, 0, time.FixedZone("", 9*60*60)), 440 | expected: "2020-07-24 23:14:15 +0900", 441 | }, 442 | { 443 | format: "%F %T %z", 444 | t: time.Date(2020, time.July, 24, 23, 14, 15, 0, time.FixedZone("", (5*60+30)*60)), 445 | expected: "2020-07-24 23:14:15 +0530", 446 | }, 447 | { 448 | format: "%F %T %Z", 449 | t: time.Date(2020, time.July, 24, 23, 14, 15, 0, time.FixedZone("JST", 9*60*60)), 450 | expected: "2020-07-24 23:14:15 JST", 451 | }, 452 | { 453 | format: "%F %T %Z", 454 | t: time.Date(2020, time.July, 24, 23, 14, 15, 0, time.FixedZone("", 9*60*60)), 455 | expected: "2020-07-24 23:14:15 +0900", 456 | }, 457 | { 458 | format: "%Z %^Z %#Z %#^Z %^#Z", 459 | t: time.Date(2020, time.July, 24, 23, 14, 15, 0, time.FixedZone("JST", 9*60*60)), 460 | expected: "JST JST jst jst jst", 461 | }, 462 | { 463 | format: "%8Z %08Z %8z %_8z %-z %08z %2z %3z %4z %5z %6z %6:z %7:z %:%Z", 464 | t: time.Date(2020, time.July, 24, 23, 14, 15, 0, time.FixedZone("JST", 9*60*60)), 465 | expected: " JST 00000JST +0000900 +900 +900 +0000900 +0900 +0900 +0900 +0900 +00900 +09:00 +009:00 %:JST", 466 | }, 467 | { 468 | format: "%8Z %08Z %8z %_8z %-z %08z %4z %5z %6z %6:z %7:z", 469 | t: time.Date(2020, time.July, 24, 23, 14, 15, 0, time.FixedZone("HAST", -10*60*60)), 470 | expected: " HAST 0000HAST -0001000 -1000 -1000 -0001000 -1000 -1000 -01000 -10:00 -010:00", 471 | }, 472 | { 473 | format: "%8z %_8z %-z %08z %4z %5z %6z %6:z %7:z", 474 | t: time.Date(2020, time.July, 24, 23, 14, 15, 0, time.FixedZone("", -(5*60+30)*60)), 475 | expected: "-0000530 -530 -530 -0000530 -0530 -0530 -00530 -05:30 -005:30", 476 | }, 477 | { 478 | format: "%8z %_8z %-z %08z %4z %5z %6z %6:z %7:z", 479 | t: time.Date(2020, time.July, 24, 23, 14, 15, 0, time.FixedZone("", 30*60)), 480 | expected: "+0000030 +030 +030 +0000030 +0030 +0030 +00030 +00:30 +000:30", 481 | }, 482 | { 483 | format: "%:z %::z %:::z %::::z %:Z %-:z %_::z %8z %08:z %_8:z %_8::z %8:::z %-:::z %:-z %:", 484 | t: time.Date(2020, time.July, 24, 23, 14, 15, 0, time.UTC), 485 | expected: "+00:00 +00:00:00 +00 %::::z %:Z +0:00 +0:00:00 +0000000 +0000:00 +0:00 +0:00:00 +0000000 +0 %:-z %:", 486 | }, 487 | { 488 | format: "%:z %::z %:::z %::::z %:Z %-:z %_::z %8z %08:z %_8:z %_8::z %8:::z %-:::z %:-z %:", 489 | t: time.Date(2020, time.July, 24, 23, 14, 15, 0, time.FixedZone("JST", 9*60*60)), 490 | expected: "+09:00 +09:00:00 +09 %::::z %:Z +9:00 +9:00:00 +0000900 +0009:00 +9:00 +9:00:00 +0000009 +9 %:-z %:", 491 | }, 492 | { 493 | format: "%:z %::z %:::z %::::z %:Z %-:z %_::z %8z %08:z %_8:z %_8::z %8:::z %-:::z %:-z %:", 494 | t: time.Date(2020, time.July, 24, 23, 14, 15, 0, time.FixedZone("HAST", -(10*60+30)*60)), 495 | expected: "-10:30 -10:30:00 -10:30 %::::z %:Z -10:30 -10:30:00 -0001030 -0010:30 -10:30 -10:30:00 -0010:30 -10:30 %:-z %:", 496 | }, 497 | { 498 | format: "%:z %::z %:::z %::::z %:Z %-:z %_::z %8z %08:z %_8:z %_8::z %8:::z %-:::z %:-z %:", 499 | t: time.Date(2020, time.July, 24, 23, 14, 15, 0, time.FixedZone("", 60*60+2)), 500 | expected: "+01:00 +01:00:02 +01:00:02 %::::z %:Z +1:00 +1:00:02 +0000100 +0001:00 +1:00 +1:00:02 +01:00:02 +1:00:02 %:-z %:", 501 | }, 502 | { 503 | format: "%:z %::z %:::z %::::z %:Z %-:z %_::z %8z %08:z %_8:z %_8::z %8:::z %-:::z %:-z %:", 504 | t: time.Date(2020, time.July, 24, 23, 14, 15, 0, time.FixedZone("", -9*60*60-62)), 505 | expected: "-09:01 -09:01:02 -09:01:02 %::::z %:Z -9:01 -9:01:02 -0000901 -0009:01 -9:01 -9:01:02 -09:01:02 -9:01:02 %:-z %:", 506 | }, 507 | { 508 | format: "%:: %6: %z", 509 | t: time.Date(2020, time.July, 24, 23, 14, 15, 0, time.FixedZone("", 9*60*60)), 510 | expected: "%:: %6: +0900", 511 | }, 512 | { 513 | format: "%H%%%M%t%S%n%f", 514 | t: time.Date(2020, time.January, 1, 1, 2, 3, 450000000, time.UTC), 515 | expected: "01%02\t03\n450000", 516 | }, 517 | { 518 | format: "%t,%4t,%04t,%n,%4n,%04n,%4", 519 | t: time.Date(2020, time.January, 1, 1, 1, 1, 0, time.UTC), 520 | expected: "\t, \t,000\t,\n, \n,000\n, %4", 521 | }, 522 | { 523 | format: "%1Y %1y %1C %1g %1G %1m %1V %1U %1d %1j %1H %1M %1S %1f", 524 | t: time.Date(2009, time.January, 2, 3, 4, 5, 6000, time.UTC), 525 | expected: "2009 09 20 09 2009 01 01 00 02 002 03 04 05 6", 526 | }, 527 | { 528 | format: "%2Y %2y %2C %2g %2G %2m %2V %2U %2d %2j %2H %2M %2S %2f", 529 | t: time.Date(2009, time.January, 2, 3, 4, 5, 6000, time.UTC), 530 | expected: "2009 09 20 09 2009 01 01 00 02 002 03 04 05 06", 531 | }, 532 | { 533 | format: "%3Y %3y %3C %3g %3G %3m %3V %3U %3d %3j %3H %3M %3S %3f", 534 | t: time.Date(2009, time.January, 2, 3, 4, 5, 6000, time.UTC), 535 | expected: "2009 009 020 009 2009 001 001 000 002 002 003 004 005 006", 536 | }, 537 | { 538 | format: "%4Y %4y %4C %4g %4G %4m %4V %4U %4d %4j %4H %4M %4S %4f", 539 | t: time.Date(2009, time.January, 2, 3, 4, 5, 60000000, time.UTC), 540 | expected: "2009 0009 0020 0009 2009 0001 0001 0000 0002 0002 0003 0004 0005 60000", 541 | }, 542 | { 543 | format: "%1d %2d %3d %4d %5d %6d %7d %8d %9d %10d", 544 | t: time.Date(2020, time.January, 1, 1, 1, 1, 0, time.UTC), 545 | expected: "01 01 001 0001 00001 000001 0000001 00000001 000000001 0000000001", 546 | }, 547 | { 548 | format: "%10000Y", 549 | t: time.Date(2020, time.January, 1, 1, 1, 1, 0, time.UTC), 550 | expected: fmt.Sprintf("%01024d", 2020), 551 | }, 552 | { 553 | format: "%922337203685477580Y", 554 | t: time.Date(2020, time.January, 1, 1, 1, 1, 0, time.UTC), 555 | expected: fmt.Sprintf("%01024d", 2020), 556 | }, 557 | { 558 | format: "%9223372036854775809Y", 559 | t: time.Date(2020, time.January, 1, 1, 1, 1, 0, time.UTC), 560 | expected: fmt.Sprintf("%01024d", 2020), 561 | }, 562 | { 563 | format: "%18446744073709551630Y", 564 | t: time.Date(2020, time.January, 1, 1, 1, 1, 0, time.UTC), 565 | expected: fmt.Sprintf("%01024d", 2020), 566 | }, 567 | { 568 | format: "%!%.%[%]%|%$%-", 569 | expected: "%!%.%[%]%|%$%-", 570 | }, 571 | { 572 | format: "%4_", 573 | expected: " %4_", 574 | }, 575 | { 576 | format: "%09_", 577 | expected: "00000%09_", 578 | }, 579 | { 580 | format: "%^", 581 | expected: "%^", 582 | }, 583 | { 584 | format: "%#", 585 | expected: "%#", 586 | }, 587 | { 588 | format: "%0", 589 | expected: "%0", 590 | }, 591 | { 592 | format: "%4", 593 | expected: " %4", 594 | }, 595 | { 596 | format: "%-9", 597 | expected: " %-9", 598 | }, 599 | { 600 | format: "%-9^", 601 | expected: " %-9^", 602 | }, 603 | { 604 | format: "%-09^", 605 | expected: "0000%-09^", 606 | }, 607 | { 608 | format: "%06", 609 | expected: "000%06", 610 | }, 611 | { 612 | format: "%6J", 613 | expected: " %6J", 614 | }, 615 | { 616 | format: "%", 617 | expected: "%", 618 | }, 619 | } 620 | 621 | func TestFormat(t *testing.T) { 622 | for _, tc := range formatTestCases { 623 | var name string 624 | if len(tc.expected) < 1000 { 625 | name = tc.expected + "/" + tc.format 626 | } else { 627 | name = strings.ReplaceAll(tc.expected+"/"+tc.format, strings.Repeat("0", 30), "0.") 628 | } 629 | t.Run(name, func(t *testing.T) { 630 | got := timefmt.Format(tc.t, tc.format) 631 | if got != tc.expected { 632 | t.Error(diff(tc.expected, got)) 633 | } 634 | }) 635 | } 636 | } 637 | 638 | func TestAppendFormat(t *testing.T) { 639 | tm := time.Date(2020, time.January, 2, 3, 4, 5, 0, time.UTC) 640 | buf := timefmt.AppendFormat(make([]byte, 0, 64), tm, "%c") 641 | if got, expected := string(buf), "Thu Jan 2 03:04:05 2020"; got != expected { 642 | t.Errorf("expected: %q, got: %q", expected, got) 643 | } 644 | } 645 | 646 | func ExampleFormat() { 647 | t := time.Date(2020, time.July, 24, 9, 7, 29, 0, time.UTC) 648 | str := timefmt.Format(t, "%Y-%m-%d %H:%M:%S") 649 | fmt.Println(str) 650 | // Output: 2020-07-24 09:07:29 651 | } 652 | 653 | func ExampleAppendFormat() { 654 | t := time.Date(2020, time.July, 24, 9, 7, 29, 0, time.UTC) 655 | buf := make([]byte, 0, 64) 656 | buf = append(buf, '(') 657 | buf = timefmt.AppendFormat(buf, t, "%Y-%m-%d %H:%M:%S") 658 | buf = append(buf, ')') 659 | fmt.Println(string(buf)) 660 | // Output: (2020-07-24 09:07:29) 661 | } 662 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/itchyny/timefmt-go 2 | 3 | go 1.21 4 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/itchyny/timefmt-go/da88686095e6d8f42faaed13b1520a414c00c06c/go.sum -------------------------------------------------------------------------------- /parse.go: -------------------------------------------------------------------------------- 1 | package timefmt 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "math" 7 | "time" 8 | ) 9 | 10 | // Parse time string using the format. 11 | func Parse(source, format string) (t time.Time, err error) { 12 | return parse(source, format, time.UTC, time.Local) 13 | } 14 | 15 | // ParseInLocation parses time string with the default location. 16 | // The location is also used to parse the time zone name (%Z). 17 | func ParseInLocation(source, format string, loc *time.Location) (t time.Time, err error) { 18 | return parse(source, format, loc, loc) 19 | } 20 | 21 | func parse(source, format string, loc, base *time.Location) (t time.Time, err error) { 22 | year, month, day, hour, minute, second, nanosecond := 1900, 1, 0, 0, 0, 0, 0 23 | defer func() { 24 | if err != nil { 25 | err = fmt.Errorf("failed to parse %q with %q: %w", source, format, err) 26 | } 27 | }() 28 | var j, week, weekday, yday, colons int 29 | century, weekstart := -1, time.Weekday(-1) 30 | var pm, hasISOYear, hasZoneName, hasZoneOffset bool 31 | var pending string 32 | for i, l := 0, len(source); i < len(format); i++ { 33 | if b := format[i]; b == '%' { 34 | i++ 35 | if i == len(format) { 36 | err = errors.New(`stray "%"`) 37 | return 38 | } 39 | b = format[i] 40 | L: 41 | switch b { 42 | case 'Y': 43 | if year, j, err = parseNumber(source, j, 4, 0, 9999, 'Y'); err != nil { 44 | return 45 | } 46 | case 'y': 47 | if year, j, err = parseNumber(source, j, 2, 0, 99, 'y'); err != nil { 48 | return 49 | } 50 | if year < 69 { 51 | year += 2000 52 | } else { 53 | year += 1900 54 | } 55 | case 'C': 56 | if century, j, err = parseNumber(source, j, 2, 0, 99, 'C'); err != nil { 57 | return 58 | } 59 | case 'g': 60 | if year, j, err = parseNumber(source, j, 2, 0, 99, b); err != nil { 61 | return 62 | } 63 | year += 2000 64 | hasISOYear = true 65 | case 'G': 66 | if year, j, err = parseNumber(source, j, 4, 0, 9999, b); err != nil { 67 | return 68 | } 69 | hasISOYear = true 70 | case 'm': 71 | if month, j, err = parseNumber(source, j, 2, 1, 12, 'm'); err != nil { 72 | return 73 | } 74 | case 'B': 75 | if month, j, err = parseAny(source, j, longMonthNames, 'B'); err != nil { 76 | return 77 | } 78 | case 'b', 'h': 79 | if month, j, err = parseAny(source, j, shortMonthNames, b); err != nil { 80 | return 81 | } 82 | case 'A': 83 | if weekday, j, err = parseAny(source, j, longWeekNames, 'A'); err != nil { 84 | return 85 | } 86 | case 'a': 87 | if weekday, j, err = parseAny(source, j, shortWeekNames, 'a'); err != nil { 88 | return 89 | } 90 | case 'w': 91 | if weekday, j, err = parseNumber(source, j, 1, 0, 6, 'w'); err != nil { 92 | return 93 | } 94 | weekday++ 95 | case 'u': 96 | if weekday, j, err = parseNumber(source, j, 1, 1, 7, 'u'); err != nil { 97 | return 98 | } 99 | weekday = weekday%7 + 1 100 | case 'V': 101 | if week, j, err = parseNumber(source, j, 2, 1, 53, b); err != nil { 102 | return 103 | } 104 | weekstart = time.Thursday 105 | weekday = or(weekday, 2) 106 | case 'U': 107 | if week, j, err = parseNumber(source, j, 2, 0, 53, b); err != nil { 108 | return 109 | } 110 | weekstart = time.Sunday 111 | weekday = or(weekday, 1) 112 | case 'W': 113 | if week, j, err = parseNumber(source, j, 2, 0, 53, b); err != nil { 114 | return 115 | } 116 | weekstart = time.Monday 117 | weekday = or(weekday, 2) 118 | case 'e': 119 | if j < l && source[j] == ' ' { 120 | j++ 121 | } 122 | fallthrough 123 | case 'd': 124 | if day, j, err = parseNumber(source, j, 2, 1, 31, b); err != nil { 125 | return 126 | } 127 | case 'j': 128 | if yday, j, err = parseNumber(source, j, 3, 1, 366, 'j'); err != nil { 129 | return 130 | } 131 | case 'k': 132 | if j < l && source[j] == ' ' { 133 | j++ 134 | } 135 | fallthrough 136 | case 'H': 137 | if hour, j, err = parseNumber(source, j, 2, 0, 23, b); err != nil { 138 | return 139 | } 140 | case 'l': 141 | if j < l && source[j] == ' ' { 142 | j++ 143 | } 144 | fallthrough 145 | case 'I': 146 | if hour, j, err = parseNumber(source, j, 2, 1, 12, b); err != nil { 147 | return 148 | } 149 | if hour == 12 { 150 | hour = 0 151 | } 152 | case 'P', 'p': 153 | var ampm int 154 | if ampm, j, err = parseAny(source, j, []string{"AM", "PM"}, b); err != nil { 155 | return 156 | } 157 | pm = ampm == 2 158 | case 'M': 159 | if minute, j, err = parseNumber(source, j, 2, 0, 59, 'M'); err != nil { 160 | return 161 | } 162 | case 'S': 163 | if second, j, err = parseNumber(source, j, 2, 0, 60, 'S'); err != nil { 164 | return 165 | } 166 | case 's': 167 | var unix int 168 | if unix, j, err = parseNumber(source, j, 10, 0, math.MaxInt, 's'); err != nil { 169 | return 170 | } 171 | t = time.Unix(int64(unix), 0).In(time.UTC) 172 | var mon time.Month 173 | year, mon, day = t.Date() 174 | hour, minute, second = t.Clock() 175 | month = int(mon) 176 | case 'f': 177 | microsecond, i := 0, j 178 | if microsecond, j, err = parseNumber(source, j, 6, 0, 999999, 'f'); err != nil { 179 | return 180 | } 181 | for i = j - i; i < 6; i++ { 182 | microsecond *= 10 183 | } 184 | nanosecond = microsecond * 1000 185 | case 'Z': 186 | i := j 187 | for ; j < l; j++ { 188 | if c := source[j]; c < 'A' || 'Z' < c { 189 | break 190 | } 191 | } 192 | t, err = time.ParseInLocation("MST", source[i:j], base) 193 | if err != nil { 194 | err = fmt.Errorf(`cannot parse %q with "%%Z"`, source[i:j]) 195 | return 196 | } 197 | if hasZoneOffset { 198 | name, _ := t.Zone() 199 | _, offset := locationZone(loc) 200 | loc = time.FixedZone(name, offset) 201 | } else { 202 | loc = t.Location() 203 | } 204 | hasZoneName = true 205 | case 'z': 206 | if j >= l { 207 | err = parseZFormatError(colons) 208 | return 209 | } 210 | sign := 1 211 | switch source[j] { 212 | case '-': 213 | sign = -1 214 | fallthrough 215 | case '+': 216 | hour, minute, second, i := 0, 0, 0, j 217 | if hour, j, _ = parseNumber(source, j+1, 2, 0, 23, 'z'); j != i+3 { 218 | err = parseZFormatError(colons) 219 | return 220 | } 221 | if j >= l || source[j] != ':' { 222 | if colons > 0 { 223 | err = expectedColonForZFormatError(colons) 224 | return 225 | } 226 | } else if j++; colons == 0 { 227 | colons = 4 228 | } 229 | i = j 230 | if minute, j, _ = parseNumber(source, j, 2, 0, 59, 'z'); j != i+2 { 231 | if colons > 0 { 232 | err = parseZFormatError(colons & 3) 233 | return 234 | } 235 | j = i 236 | } else if colons > 1 { 237 | if j >= l || source[j] != ':' { 238 | if colons == 2 { 239 | err = expectedColonForZFormatError(colons) 240 | return 241 | } 242 | } else { 243 | i = j 244 | if second, j, _ = parseNumber(source, j+1, 2, 0, 59, 'z'); j != i+3 { 245 | if colons == 2 { 246 | err = parseZFormatError(colons) 247 | return 248 | } 249 | j = i 250 | } 251 | } 252 | } 253 | var name string 254 | if hasZoneName { 255 | name, _ = locationZone(loc) 256 | } 257 | loc, colons = time.FixedZone(name, sign*((hour*60+minute)*60+second)), 0 258 | hasZoneOffset = true 259 | case 'Z': 260 | loc, colons, j = time.UTC, 0, j+1 261 | default: 262 | err = parseZFormatError(colons) 263 | return 264 | } 265 | case ':': 266 | if pending != "" { 267 | if j >= l || source[j] != b { 268 | err = expectedFormatError(b) 269 | return 270 | } 271 | j++ 272 | } else { 273 | for colons = 1; colons <= 2; colons++ { 274 | if i++; i == len(format) { 275 | break 276 | } else if b = format[i]; b == 'z' { 277 | goto L 278 | } else if b != ':' || colons == 2 { 279 | break 280 | } 281 | } 282 | err = expectedZAfterColonError(colons) 283 | return 284 | } 285 | case 't', 'n': 286 | i := j 287 | K: 288 | for ; j < l; j++ { 289 | switch source[j] { 290 | case ' ', '\t', '\n', '\v', '\f', '\r': 291 | default: 292 | break K 293 | } 294 | } 295 | if i == j { 296 | err = fmt.Errorf(`expected a space for "%%%c"`, b) 297 | return 298 | } 299 | case '%': 300 | if j >= l || source[j] != b { 301 | err = expectedFormatError(b) 302 | return 303 | } 304 | j++ 305 | default: 306 | if pending == "" { 307 | var ok bool 308 | if pending, ok = compositions[b]; ok { 309 | break 310 | } 311 | err = fmt.Errorf(`unexpected format "%%%c"`, b) 312 | return 313 | } 314 | if j >= l || source[j] != b { 315 | err = expectedFormatError(b) 316 | return 317 | } 318 | j++ 319 | } 320 | if pending != "" { 321 | b, pending = pending[0], pending[1:] 322 | goto L 323 | } 324 | } else if j >= l || source[j] != b { 325 | err = expectedFormatError(b) 326 | return 327 | } else { 328 | j++ 329 | } 330 | } 331 | if j < len(source) { 332 | err = fmt.Errorf("unparsed string %q", source[j:]) 333 | return 334 | } 335 | if pm { 336 | hour += 12 337 | } 338 | if century >= 0 { 339 | year = century*100 + year%100 340 | } 341 | if day == 0 { 342 | if yday > 0 { 343 | if hasISOYear { 344 | err = errors.New(`use "%Y" to parse non-ISO year for "%j"`) 345 | return 346 | } 347 | return time.Date(year, time.January, yday, hour, minute, second, nanosecond, loc), nil 348 | } 349 | if weekstart >= time.Sunday { 350 | if weekstart == time.Thursday { 351 | if !hasISOYear { 352 | err = errors.New(`use "%G" to parse ISO year for "%V"`) 353 | return 354 | } 355 | } else if hasISOYear { 356 | err = errors.New(`use "%Y" to parse non-ISO year for "%U" or "%W"`) 357 | return 358 | } 359 | if weekstart > time.Sunday && weekday == 1 { 360 | week++ 361 | } 362 | t := time.Date(year, time.January, -int(weekstart), hour, minute, second, nanosecond, loc) 363 | return t.AddDate(0, 0, week*7-int(t.Weekday())+weekday-1), nil 364 | } 365 | day = 1 366 | } 367 | return time.Date(year, time.Month(month), day, hour, minute, second, nanosecond, loc), nil 368 | } 369 | 370 | func locationZone(loc *time.Location) (name string, offset int) { 371 | return time.Date(2000, time.January, 1, 0, 0, 0, 0, loc).Zone() 372 | } 373 | 374 | type parseFormatError byte 375 | 376 | func (err parseFormatError) Error() string { 377 | return fmt.Sprintf(`cannot parse "%%%c"`, byte(err)) 378 | } 379 | 380 | type expectedFormatError byte 381 | 382 | func (err expectedFormatError) Error() string { 383 | return fmt.Sprintf("expected %q", byte(err)) 384 | } 385 | 386 | type parseZFormatError int 387 | 388 | func (err parseZFormatError) Error() string { 389 | return `cannot parse "%` + `::z"`[2-err:] 390 | } 391 | 392 | type expectedColonForZFormatError int 393 | 394 | func (err expectedColonForZFormatError) Error() string { 395 | return `expected ':' for "%` + `::z"`[2-err:] 396 | } 397 | 398 | type expectedZAfterColonError int 399 | 400 | func (err expectedZAfterColonError) Error() string { 401 | return `expected 'z' after "%` + `::"`[2-err:] 402 | } 403 | 404 | func parseNumber(source string, index, size, minimum, maximum int, format byte) (int, int, error) { 405 | var value int 406 | i := index 407 | for size = min(i+size, len(source)); i < size; i++ { 408 | if b := source[i]; '0' <= b && b <= '9' { 409 | value = value*10 + int(b&0x0F) 410 | } else { 411 | break 412 | } 413 | } 414 | if i == index || value < minimum || maximum < value { 415 | return 0, 0, parseFormatError(format) 416 | } 417 | return value, i, nil 418 | } 419 | 420 | func parseAny(source string, index int, candidates []string, format byte) (int, int, error) { 421 | L: 422 | for i, xs := range candidates { 423 | j := index 424 | for k := 0; k < len(xs); k, j = k+1, j+1 { 425 | if j >= len(source) { 426 | continue L 427 | } 428 | if x, y := xs[k], source[j]; x != y && x|0x20 != y|0x20 { 429 | continue L 430 | } 431 | } 432 | return i + 1, j, nil 433 | } 434 | return 0, 0, parseFormatError(format) 435 | } 436 | -------------------------------------------------------------------------------- /parse_test.go: -------------------------------------------------------------------------------- 1 | package timefmt_test 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "log" 7 | "strings" 8 | "testing" 9 | "time" 10 | 11 | "github.com/itchyny/timefmt-go" 12 | ) 13 | 14 | var parseTestCases = []struct { 15 | source string 16 | format string 17 | t time.Time 18 | parseErr error 19 | }{ 20 | { 21 | source: "2020", 22 | format: "%Y", 23 | t: time.Date(2020, time.January, 1, 0, 0, 0, 0, time.UTC), 24 | }, 25 | { 26 | source: "[2020]", 27 | format: "[%Y]", 28 | t: time.Date(2020, time.January, 1, 0, 0, 0, 0, time.UTC), 29 | }, 30 | { 31 | source: "20", 32 | format: "%Y", 33 | t: time.Date(20, time.January, 1, 0, 0, 0, 0, time.UTC), 34 | }, 35 | { 36 | source: "2", 37 | format: "%Y", 38 | t: time.Date(2, time.January, 1, 0, 0, 0, 0, time.UTC), 39 | }, 40 | { 41 | source: "20", 42 | format: "%Y", 43 | t: time.Date(20, time.January, 1, 0, 0, 0, 0, time.UTC), 44 | }, 45 | { 46 | source: "20xxx", 47 | format: "%Y", 48 | parseErr: errors.New(`unparsed string "xxx"`), 49 | }, 50 | { 51 | source: "a", 52 | format: "%Y", 53 | parseErr: errors.New(`cannot parse "%Y"`), 54 | }, 55 | { 56 | source: "20", 57 | format: "%C", 58 | t: time.Date(2000, time.January, 1, 0, 0, 0, 0, time.UTC), 59 | }, 60 | { 61 | source: "1758", 62 | format: "%C%y", 63 | t: time.Date(1758, time.January, 1, 0, 0, 0, 0, time.UTC), 64 | }, 65 | { 66 | source: "0000", 67 | format: "%C%y", 68 | t: time.Date(0, time.January, 1, 0, 0, 0, 0, time.UTC), 69 | }, 70 | { 71 | source: "9999", 72 | format: "%C%y", 73 | t: time.Date(9999, time.January, 1, 0, 0, 0, 0, time.UTC), 74 | }, 75 | { 76 | source: "xx", 77 | format: "%C", 78 | parseErr: errors.New(`cannot parse "%C"`), 79 | }, 80 | { 81 | source: "68", 82 | format: "%y", 83 | t: time.Date(2068, time.January, 1, 0, 0, 0, 0, time.UTC), 84 | }, 85 | { 86 | source: "69", 87 | format: "%y", 88 | t: time.Date(1969, time.January, 1, 0, 0, 0, 0, time.UTC), 89 | }, 90 | { 91 | source: "xx", 92 | format: "%y", 93 | parseErr: errors.New(`cannot parse "%y"`), 94 | }, 95 | { 96 | source: "2020-05", 97 | format: "%Y-%m", 98 | t: time.Date(2020, time.May, 1, 0, 0, 0, 0, time.UTC), 99 | }, 100 | { 101 | source: "2020/1", 102 | format: "%Y/%m", 103 | t: time.Date(2020, time.January, 1, 0, 0, 0, 0, time.UTC), 104 | }, 105 | { 106 | source: "2020/9", 107 | format: "%Y/%m", 108 | t: time.Date(2020, time.September, 1, 0, 0, 0, 0, time.UTC), 109 | }, 110 | { 111 | source: "2020/10/09", 112 | format: "%Y/%m/%d", 113 | t: time.Date(2020, time.October, 9, 0, 0, 0, 0, time.UTC), 114 | }, 115 | { 116 | source: "2020/10/1", 117 | format: "%Y/%m/%d", 118 | t: time.Date(2020, time.October, 1, 0, 0, 0, 0, time.UTC), 119 | }, 120 | { 121 | source: "2020-12-12", 122 | format: "%Y-%m-%d", 123 | t: time.Date(2020, time.December, 12, 0, 0, 0, 0, time.UTC), 124 | }, 125 | { 126 | source: "2020-1-1", 127 | format: "%Y-%m-%d", 128 | t: time.Date(2020, time.January, 1, 0, 0, 0, 0, time.UTC), 129 | }, 130 | { 131 | source: "[2020-1-1]", 132 | format: "[%Y-%m-%d]", 133 | t: time.Date(2020, time.January, 1, 0, 0, 0, 0, time.UTC), 134 | }, 135 | { 136 | source: "2020-", 137 | format: "%Y-%m-%d", 138 | parseErr: errors.New(`cannot parse "%m"`), 139 | }, 140 | { 141 | source: "2020-1-", 142 | format: "%Y-%m-%d", 143 | parseErr: errors.New(`cannot parse "%d"`), 144 | }, 145 | { 146 | source: "201111", 147 | format: "%y%m%d", 148 | t: time.Date(2020, time.November, 11, 0, 0, 0, 0, time.UTC), 149 | }, 150 | { 151 | source: "1-1-1", 152 | format: "%y-%m-%d", 153 | t: time.Date(2001, time.January, 1, 0, 0, 0, 0, time.UTC), 154 | }, 155 | { 156 | source: "9-9-9", 157 | format: "%y-%m-%d", 158 | t: time.Date(2009, time.September, 9, 0, 0, 0, 0, time.UTC), 159 | }, 160 | { 161 | source: "0000-01-01", 162 | format: "%Y-%m-%d", 163 | t: time.Date(0, time.January, 1, 0, 0, 0, 0, time.UTC), 164 | }, 165 | { 166 | source: "9999-12-31", 167 | format: "%Y-%m-%d", 168 | t: time.Date(9999, time.December, 31, 0, 0, 0, 0, time.UTC), 169 | }, 170 | { 171 | source: "2020-00-01", 172 | format: "%Y-%m-%d", 173 | parseErr: errors.New(`cannot parse "%m"`), 174 | }, 175 | { 176 | source: "2020-13-01", 177 | format: "%Y-%m-%d", 178 | parseErr: errors.New(`cannot parse "%m"`), 179 | }, 180 | { 181 | source: "2020-99-01", 182 | format: "%Y-%m-%d", 183 | parseErr: errors.New(`cannot parse "%m"`), 184 | }, 185 | { 186 | source: "2020-10-00", 187 | format: "%Y-%m-%d", 188 | parseErr: errors.New(`cannot parse "%d"`), 189 | }, 190 | { 191 | source: "2020-10-32", 192 | format: "%Y-%m-%d", 193 | parseErr: errors.New(`cannot parse "%d"`), 194 | }, 195 | { 196 | source: "2020 02 9", 197 | format: "%Y %m %e", 198 | t: time.Date(2020, time.February, 9, 0, 0, 0, 0, time.UTC), 199 | }, 200 | { 201 | source: "2020 10 99", 202 | format: "%Y %m %e", 203 | parseErr: errors.New(`cannot parse "%e"`), 204 | }, 205 | { 206 | source: "Jan", 207 | format: "%b", 208 | t: time.Date(1900, time.January, 1, 0, 0, 0, 0, time.UTC), 209 | }, 210 | { 211 | source: "Ja", 212 | format: "%b", 213 | parseErr: errors.New(`cannot parse "%b"`), 214 | }, 215 | { 216 | source: "Jul", 217 | format: "%b", 218 | t: time.Date(1900, time.July, 1, 0, 0, 0, 0, time.UTC), 219 | }, 220 | { 221 | source: "Sep", 222 | format: "%b", 223 | t: time.Date(1900, time.September, 1, 0, 0, 0, 0, time.UTC), 224 | }, 225 | { 226 | source: "September", 227 | format: "%B", 228 | t: time.Date(1900, time.September, 1, 0, 0, 0, 0, time.UTC), 229 | }, 230 | { 231 | source: "Sep", 232 | format: "%B", 233 | parseErr: errors.New(`cannot parse "%B"`), 234 | }, 235 | { 236 | source: "Sep", 237 | format: "%h", 238 | t: time.Date(1900, time.September, 1, 0, 0, 0, 0, time.UTC), 239 | }, 240 | { 241 | source: "100", 242 | format: "%j", 243 | t: time.Date(1900, time.April, 10, 0, 0, 0, 0, time.UTC), 244 | }, 245 | { 246 | source: ".10", 247 | format: "%j", 248 | parseErr: errors.New(`cannot parse "%j"`), 249 | }, 250 | { 251 | source: "20203", 252 | format: "%Y%j", 253 | t: time.Date(2020, time.January, 3, 0, 0, 0, 0, time.UTC), 254 | }, 255 | { 256 | source: "2020366", 257 | format: "%Y%j", 258 | t: time.Date(2020, time.December, 31, 0, 0, 0, 0, time.UTC), 259 | }, 260 | { 261 | source: "2020-9-33", 262 | format: "%Y-%m-%j", 263 | t: time.Date(2020, time.February, 2, 0, 0, 0, 0, time.UTC), 264 | }, 265 | { 266 | source: "2020-0", 267 | format: "%Y-%j", 268 | parseErr: errors.New(`cannot parse "%j"`), 269 | }, 270 | { 271 | source: "2020-367", 272 | format: "%Y-%j", 273 | parseErr: errors.New(`cannot parse "%j"`), 274 | }, 275 | { 276 | source: "2024-1", 277 | format: "%G-%j", 278 | parseErr: errors.New(`use "%Y" to parse non-ISO year for "%j"`), 279 | }, 280 | { 281 | source: "MAY", 282 | format: "%b", 283 | t: time.Date(1900, time.May, 1, 0, 0, 0, 0, time.UTC), 284 | }, 285 | { 286 | source: "SATURDAY", 287 | format: "%A", 288 | t: time.Date(1900, time.January, 1, 0, 0, 0, 0, time.UTC), 289 | }, 290 | { 291 | source: "[sunday]", 292 | format: "[%A]", 293 | t: time.Date(1900, time.January, 1, 0, 0, 0, 0, time.UTC), 294 | }, 295 | { 296 | source: "[Mon]", 297 | format: "[%a]", 298 | t: time.Date(1900, time.January, 1, 0, 0, 0, 0, time.UTC), 299 | }, 300 | { 301 | source: "Teu", 302 | format: "%a", 303 | parseErr: errors.New(`cannot parse "%a"`), 304 | }, 305 | { 306 | source: "mondaymon1", 307 | format: "%A%a%w", 308 | t: time.Date(1900, time.January, 1, 0, 0, 0, 0, time.UTC), 309 | }, 310 | { 311 | source: "mooday", 312 | format: "%A", 313 | parseErr: errors.New(`cannot parse "%A"`), 314 | }, 315 | { 316 | source: "0", 317 | format: "%w", 318 | t: time.Date(1900, time.January, 1, 0, 0, 0, 0, time.UTC), 319 | }, 320 | { 321 | source: "6", 322 | format: "%w", 323 | t: time.Date(1900, time.January, 1, 0, 0, 0, 0, time.UTC), 324 | }, 325 | { 326 | source: "7", 327 | format: "%w", 328 | parseErr: errors.New(`cannot parse "%w"`), 329 | }, 330 | { 331 | source: "", 332 | format: "%w", 333 | parseErr: errors.New(`cannot parse "%w"`), 334 | }, 335 | { 336 | source: "0", 337 | format: "%u", 338 | parseErr: errors.New(`cannot parse "%u"`), 339 | }, 340 | { 341 | source: "1", 342 | format: "%u", 343 | t: time.Date(1900, time.January, 1, 0, 0, 0, 0, time.UTC), 344 | }, 345 | { 346 | source: "7", 347 | format: "%u", 348 | t: time.Date(1900, time.January, 1, 0, 0, 0, 0, time.UTC), 349 | }, 350 | { 351 | source: "8", 352 | format: "%u", 353 | parseErr: errors.New(`cannot parse "%u"`), 354 | }, 355 | { 356 | source: "", 357 | format: "%u", 358 | parseErr: errors.New(`cannot parse "%u"`), 359 | }, 360 | { 361 | source: "20", 362 | format: "%g", 363 | t: time.Date(2020, time.January, 1, 0, 0, 0, 0, time.UTC), 364 | }, 365 | { 366 | source: "99", 367 | format: "%g", 368 | t: time.Date(2099, time.January, 1, 0, 0, 0, 0, time.UTC), 369 | }, 370 | { 371 | source: "xx", 372 | format: "%g", 373 | parseErr: errors.New(`cannot parse "%g"`), 374 | }, 375 | { 376 | source: "2009", 377 | format: "%G", 378 | t: time.Date(2009, time.January, 1, 0, 0, 0, 0, time.UTC), 379 | }, 380 | { 381 | source: "0000", 382 | format: "%G", 383 | t: time.Date(0, time.January, 1, 0, 0, 0, 0, time.UTC), 384 | }, 385 | { 386 | source: "9999", 387 | format: "%G", 388 | t: time.Date(9999, time.January, 1, 0, 0, 0, 0, time.UTC), 389 | }, 390 | { 391 | source: "xxxx", 392 | format: "%G", 393 | parseErr: errors.New(`cannot parse "%G"`), 394 | }, 395 | { 396 | source: "2017 3", 397 | format: "%G %V", 398 | t: time.Date(2017, time.January, 16, 0, 0, 0, 0, time.UTC), 399 | }, 400 | { 401 | source: "2018 03 Sun", 402 | format: "%G %V %a", 403 | t: time.Date(2018, time.January, 21, 0, 0, 0, 0, time.UTC), 404 | }, 405 | { 406 | source: "2018 10", 407 | format: "%G %V", 408 | t: time.Date(2018, time.March, 5, 0, 0, 0, 0, time.UTC), 409 | }, 410 | { 411 | source: "2019-W01-1", 412 | format: "%G-W%V-%u", 413 | t: time.Date(2018, time.December, 31, 0, 0, 0, 0, time.UTC), 414 | }, 415 | { 416 | source: "2019 20", 417 | format: "%G %V", 418 | t: time.Date(2019, time.May, 13, 0, 0, 0, 0, time.UTC), 419 | }, 420 | { 421 | source: "2020 30 Tuesday", 422 | format: "%G %V %A", 423 | t: time.Date(2020, time.July, 21, 0, 0, 0, 0, time.UTC), 424 | }, 425 | { 426 | source: "Fri 53 20", 427 | format: "%a %V %g", 428 | t: time.Date(2021, time.January, 1, 0, 0, 0, 0, time.UTC), 429 | }, 430 | { 431 | source: "Sun 50 2021", 432 | format: "%a %V %G", 433 | t: time.Date(2021, time.December, 19, 0, 0, 0, 0, time.UTC), 434 | }, 435 | { 436 | source: "Saturday 53 2021", 437 | format: "%A %V %G", 438 | t: time.Date(2022, time.January, 8, 0, 0, 0, 0, time.UTC), 439 | }, 440 | { 441 | source: "2023 10 Mon", 442 | format: "%G %V %a", 443 | t: time.Date(2023, time.March, 6, 0, 0, 0, 0, time.UTC), 444 | }, 445 | { 446 | source: "2024W107", 447 | format: "%GW%V%u", 448 | t: time.Date(2024, time.March, 10, 0, 0, 0, 0, time.UTC), 449 | }, 450 | { 451 | source: "2024 W00 7", 452 | format: "%G W%V %u", 453 | parseErr: errors.New(`cannot parse "%V"`), 454 | }, 455 | { 456 | source: "2024 W54 7", 457 | format: "%G W%V %u", 458 | parseErr: errors.New(`cannot parse "%V"`), 459 | }, 460 | { 461 | source: "2024 20 135", 462 | format: "%G %V %j", 463 | parseErr: errors.New(`use "%Y" to parse non-ISO year for "%j"`), 464 | }, 465 | { 466 | source: "2018 50 Sun", 467 | format: "%Y %V %a", 468 | parseErr: errors.New(`use "%G" to parse ISO year for "%V"`), 469 | }, 470 | { 471 | source: "xx", 472 | format: "%V", 473 | parseErr: errors.New(`cannot parse "%V"`), 474 | }, 475 | { 476 | source: "2017 3", 477 | format: "%Y %U", 478 | t: time.Date(2017, time.January, 15, 0, 0, 0, 0, time.UTC), 479 | }, 480 | { 481 | source: "2018 03 Sun", 482 | format: "%Y %W %a", 483 | t: time.Date(2018, time.January, 21, 0, 0, 0, 0, time.UTC), 484 | }, 485 | { 486 | source: "2018 10", 487 | format: "%Y %U", 488 | t: time.Date(2018, time.March, 11, 0, 0, 0, 0, time.UTC), 489 | }, 490 | { 491 | source: "2019 1 0", 492 | format: "%Y %U %w", 493 | t: time.Date(2019, time.January, 6, 0, 0, 0, 0, time.UTC), 494 | }, 495 | { 496 | source: "2019 20", 497 | format: "%Y %U", 498 | t: time.Date(2019, time.May, 19, 0, 0, 0, 0, time.UTC), 499 | }, 500 | { 501 | source: "2020 30 Sat", 502 | format: "%Y %U %a", 503 | t: time.Date(2020, time.August, 1, 0, 0, 0, 0, time.UTC), 504 | }, 505 | { 506 | source: "Fri 53 2020", 507 | format: "%a %U %Y", 508 | t: time.Date(2021, time.January, 8, 0, 0, 0, 0, time.UTC), 509 | }, 510 | { 511 | source: "Fri 00 2021", 512 | format: "%a %U %Y", 513 | t: time.Date(2021, time.January, 1, 0, 0, 0, 0, time.UTC), 514 | }, 515 | { 516 | source: "Sun 50 2021", 517 | format: "%a %U %Y", 518 | t: time.Date(2021, time.December, 12, 0, 0, 0, 0, time.UTC), 519 | }, 520 | { 521 | source: "Saturday 00 2022", 522 | format: "%A %U %Y", 523 | t: time.Date(2022, time.January, 1, 0, 0, 0, 0, time.UTC), 524 | }, 525 | { 526 | source: "2023 10 Mon", 527 | format: "%Y %U %a", 528 | t: time.Date(2023, time.March, 6, 0, 0, 0, 0, time.UTC), 529 | }, 530 | { 531 | source: "2024 10 1", 532 | format: "%Y %U %w", 533 | t: time.Date(2024, time.March, 11, 0, 0, 0, 0, time.UTC), 534 | }, 535 | { 536 | source: "2024 54 6", 537 | format: "%Y %U %w", 538 | parseErr: errors.New(`cannot parse "%U"`), 539 | }, 540 | { 541 | source: "24 10 Sun", 542 | format: "%g %U %a", 543 | parseErr: errors.New(`use "%Y" to parse non-ISO year for "%U" or "%W"`), 544 | }, 545 | { 546 | source: "xx", 547 | format: "%U", 548 | parseErr: errors.New(`cannot parse "%U"`), 549 | }, 550 | { 551 | source: "2017 3", 552 | format: "%Y %W", 553 | t: time.Date(2017, time.January, 16, 0, 0, 0, 0, time.UTC), 554 | }, 555 | { 556 | source: "2018 03 Sun", 557 | format: "%Y %W %a", 558 | t: time.Date(2018, time.January, 21, 0, 0, 0, 0, time.UTC), 559 | }, 560 | { 561 | source: "2018 10", 562 | format: "%Y %W", 563 | t: time.Date(2018, time.March, 5, 0, 0, 0, 0, time.UTC), 564 | }, 565 | { 566 | source: "2019 1 2", 567 | format: "%Y %W %u", 568 | t: time.Date(2019, time.January, 8, 0, 0, 0, 0, time.UTC), 569 | }, 570 | { 571 | source: "2019 20", 572 | format: "%Y %W", 573 | t: time.Date(2019, time.May, 20, 0, 0, 0, 0, time.UTC), 574 | }, 575 | { 576 | source: "2020 30 Friday", 577 | format: "%Y %W %A", 578 | t: time.Date(2020, time.July, 31, 0, 0, 0, 0, time.UTC), 579 | }, 580 | { 581 | source: "Fri 53 2020", 582 | format: "%a %W %Y", 583 | t: time.Date(2021, time.January, 8, 0, 0, 0, 0, time.UTC), 584 | }, 585 | { 586 | source: "Fri 00 2021", 587 | format: "%a %W %Y", 588 | t: time.Date(2021, time.January, 1, 0, 0, 0, 0, time.UTC), 589 | }, 590 | { 591 | source: "Sun 50 2021", 592 | format: "%a %W %Y", 593 | t: time.Date(2021, time.December, 19, 0, 0, 0, 0, time.UTC), 594 | }, 595 | { 596 | source: "Saturday 00 2022", 597 | format: "%A %W %Y", 598 | t: time.Date(2022, time.January, 1, 0, 0, 0, 0, time.UTC), 599 | }, 600 | { 601 | source: "2023 10 Mon", 602 | format: "%Y %W %a", 603 | t: time.Date(2023, time.March, 6, 0, 0, 0, 0, time.UTC), 604 | }, 605 | { 606 | source: "2024 10 Tue", 607 | format: "%Y %W %a", 608 | t: time.Date(2024, time.March, 5, 0, 0, 0, 0, time.UTC), 609 | }, 610 | { 611 | source: "2024 20 135", 612 | format: "%Y %W %j", 613 | t: time.Date(2024, time.May, 14, 0, 0, 0, 0, time.UTC), 614 | }, 615 | { 616 | source: "2024-05-19 W20", 617 | format: "%F W%W", 618 | t: time.Date(2024, time.May, 19, 0, 0, 0, 0, time.UTC), 619 | }, 620 | { 621 | source: "2024 54 6", 622 | format: "%Y %W %w", 623 | parseErr: errors.New(`cannot parse "%W"`), 624 | }, 625 | { 626 | source: "2024 10 Sun", 627 | format: "%G %W %a", 628 | parseErr: errors.New(`use "%Y" to parse non-ISO year for "%U" or "%W"`), 629 | }, 630 | { 631 | source: "xx", 632 | format: "%W", 633 | parseErr: errors.New(`cannot parse "%W"`), 634 | }, 635 | { 636 | source: "2020-09-08 07:06:05", 637 | format: "%Y-%m-%d %H:%M:%S", 638 | t: time.Date(2020, time.September, 8, 7, 6, 5, 0, time.UTC), 639 | }, 640 | { 641 | source: "1:2:3.456", 642 | format: "%H:%M:%S.%f", 643 | t: time.Date(1900, time.January, 1, 1, 2, 3, 456000000, time.UTC), 644 | }, 645 | { 646 | source: "1213145678912", 647 | format: "%H%M%S%f%d", 648 | t: time.Date(1900, time.January, 2, 12, 13, 14, 567891000, time.UTC), 649 | }, 650 | { 651 | source: "00:00:00.000000", 652 | format: "%H:%M:%S.%f", 653 | t: time.Date(1900, time.January, 1, 0, 0, 0, 0, time.UTC), 654 | }, 655 | { 656 | source: "23:59:59.999999", 657 | format: "%H:%M:%S.%f", 658 | t: time.Date(1900, time.January, 1, 23, 59, 59, 999999000, time.UTC), 659 | }, 660 | { 661 | source: "24:00:00", 662 | format: "%H:%M:%S", 663 | parseErr: errors.New(`cannot parse "%H"`), 664 | }, 665 | { 666 | source: "99:00:00", 667 | format: "%H:%M:%S", 668 | parseErr: errors.New(`cannot parse "%H"`), 669 | }, 670 | { 671 | source: "23:60:00", 672 | format: "%H:%M:%S", 673 | parseErr: errors.New(`cannot parse "%M"`), 674 | }, 675 | { 676 | source: "23:99:00", 677 | format: "%H:%M:%S", 678 | parseErr: errors.New(`cannot parse "%M"`), 679 | }, 680 | { 681 | source: "23:00:61", 682 | format: "%H:%M:%S", 683 | parseErr: errors.New(`cannot parse "%S"`), 684 | }, 685 | { 686 | source: "23:00:99", 687 | format: "%H:%M:%S", 688 | parseErr: errors.New(`cannot parse "%S"`), 689 | }, 690 | { 691 | source: "xx", 692 | format: "%H", 693 | parseErr: errors.New(`cannot parse "%H"`), 694 | }, 695 | { 696 | source: "xx", 697 | format: "%M", 698 | parseErr: errors.New(`cannot parse "%M"`), 699 | }, 700 | { 701 | source: "xx", 702 | format: "%S", 703 | parseErr: errors.New(`cannot parse "%S"`), 704 | }, 705 | { 706 | source: "1:2:3.", 707 | format: "%H:%M:%S.%f", 708 | parseErr: errors.New(`cannot parse "%f"`), 709 | }, 710 | { 711 | source: "12:13:14 AM", 712 | format: "%I:%M:%S %p", 713 | t: time.Date(1900, time.January, 1, 0, 13, 14, 0, time.UTC), 714 | }, 715 | { 716 | source: "01:14:15PM", 717 | format: "%I:%M:%S%p", 718 | t: time.Date(1900, time.January, 1, 13, 14, 15, 0, time.UTC), 719 | }, 720 | { 721 | source: "PM 11:14:15", 722 | format: "%p %I:%M:%S", 723 | t: time.Date(1900, time.January, 1, 23, 14, 15, 0, time.UTC), 724 | }, 725 | { 726 | source: "12:13:14 PM", 727 | format: "%I:%M:%S %p", 728 | t: time.Date(1900, time.January, 1, 12, 13, 14, 0, time.UTC), 729 | }, 730 | { 731 | source: "00:00:00 AM", 732 | format: "%I:%M:%S %p", 733 | parseErr: errors.New(`cannot parse "%I"`), 734 | }, 735 | { 736 | source: "13:00:00 AM", 737 | format: "%I:%M:%S %p", 738 | parseErr: errors.New(`cannot parse "%I"`), 739 | }, 740 | { 741 | source: "xx", 742 | format: "%I", 743 | parseErr: errors.New(`cannot parse "%I"`), 744 | }, 745 | { 746 | source: "am 9:10:11", 747 | format: "%P %k:%M:%S", 748 | t: time.Date(1900, time.January, 1, 9, 10, 11, 0, time.UTC), 749 | }, 750 | { 751 | source: " 9:10:11 pm", 752 | format: "%l:%M:%S %P", 753 | t: time.Date(1900, time.January, 1, 21, 10, 11, 0, time.UTC), 754 | }, 755 | { 756 | source: "24:10:11 pm", 757 | format: "%l:%M:%S %P", 758 | parseErr: errors.New(`cannot parse "%l"`), 759 | }, 760 | { 761 | source: "1598765432Z", 762 | format: "%s%z", 763 | t: time.Date(2020, time.August, 30, 5, 30, 32, 0, time.UTC), 764 | }, 765 | { 766 | source: "2147483647Z", 767 | format: "%s%z", 768 | t: time.Date(2038, time.January, 19, 3, 14, 7, 0, time.UTC), 769 | }, 770 | { 771 | source: ".", 772 | format: "%s", 773 | parseErr: errors.New(`cannot parse "%s"`), 774 | }, 775 | { 776 | source: "23:14", 777 | format: "%R", 778 | t: time.Date(1900, time.January, 1, 23, 14, 0, 0, time.UTC), 779 | }, 780 | { 781 | source: "23:", 782 | format: "%R", 783 | parseErr: errors.New(`cannot parse "%M"`), 784 | }, 785 | { 786 | source: "3:14:15 PM", 787 | format: "%r", 788 | t: time.Date(1900, time.January, 1, 15, 14, 15, 0, time.UTC), 789 | }, 790 | { 791 | source: "3:1415 PM", 792 | format: "%r", 793 | parseErr: errors.New("expected ':'"), 794 | }, 795 | { 796 | source: "3:14:15PM", 797 | format: "%r", 798 | parseErr: errors.New("expected ' '"), 799 | }, 800 | { 801 | source: "2/20/21 23:14:15", 802 | format: "%D %T", 803 | t: time.Date(2021, time.February, 20, 23, 14, 15, 0, time.UTC), 804 | }, 805 | { 806 | source: "02/09/20 23:14:15", 807 | format: "%x %X", 808 | t: time.Date(2020, time.February, 9, 23, 14, 15, 0, time.UTC), 809 | }, 810 | { 811 | source: "2020-02-09 \t\n\v\f\r 23:14:15", 812 | format: "%F%t%T", 813 | t: time.Date(2020, time.February, 9, 23, 14, 15, 0, time.UTC), 814 | }, 815 | { 816 | source: "2020-02-0923:14:15", 817 | format: "%F%t%T", 818 | parseErr: errors.New(`expected a space for "%t"`), 819 | }, 820 | { 821 | source: " 9-Jul-2020 23:14:15", 822 | format: "%v %X", 823 | t: time.Date(2020, time.July, 9, 23, 14, 15, 0, time.UTC), 824 | }, 825 | { 826 | source: "Sun Feb 9 23:14:15 2020", 827 | format: "%c", 828 | t: time.Date(2020, time.February, 9, 23, 14, 15, 0, time.UTC), 829 | }, 830 | { 831 | source: "Sun Feb 9 23:14:15 UTC 2020", 832 | format: "%+", 833 | t: time.Date(2020, time.February, 9, 23, 14, 15, 0, time.UTC), 834 | }, 835 | { 836 | source: "2020-07-24 23:14:15 +0000", 837 | format: "%F %T %z", 838 | t: time.Date(2020, time.July, 24, 23, 14, 15, 0, time.FixedZone("", 0)), 839 | }, 840 | { 841 | source: "2020-07-24T23:14:15Z", 842 | format: "%FT%T%z", 843 | t: time.Date(2020, time.July, 24, 23, 14, 15, 0, time.UTC), 844 | }, 845 | { 846 | source: "2020-07-24 23:14:15 -0800", 847 | format: "%F %T %z", 848 | t: time.Date(2020, time.July, 24, 23, 14, 15, 0, time.FixedZone("", -8*60*60)), 849 | }, 850 | { 851 | source: "2020-07-24 23:14:15 +0900", 852 | format: "%F %T %z", 853 | t: time.Date(2020, time.July, 24, 23, 14, 15, 0, time.FixedZone("", 9*60*60)), 854 | }, 855 | { 856 | source: "2020-07-24 23:14:15 +0530", 857 | format: "%F %T %z", 858 | t: time.Date(2020, time.July, 24, 23, 14, 15, 0, time.FixedZone("", (5*60+30)*60)), 859 | }, 860 | { 861 | source: "2020-07-24 23:14:15 +04:30", 862 | format: "%F %T %z", 863 | t: time.Date(2020, time.July, 24, 23, 14, 15, 0, time.FixedZone("", (4*60+30)*60)), 864 | }, 865 | { 866 | source: "2020-07-24 23:14:15 +05:43:21", 867 | format: "%F %T %z", 868 | t: time.Date(2020, time.July, 24, 23, 14, 15, 0, time.FixedZone("", (5*60+43)*60+21)), 869 | }, 870 | { 871 | source: "2020-07-24 23:14:15 +05:43zzz", 872 | format: "%F %T %zzzz", 873 | t: time.Date(2020, time.July, 24, 23, 14, 15, 0, time.FixedZone("", (5*60+43)*60)), 874 | }, 875 | { 876 | source: "2020-07-24 23:14:15 +05:43:", 877 | format: "%F %T %z:", 878 | t: time.Date(2020, time.July, 24, 23, 14, 15, 0, time.FixedZone("", (5*60+43)*60)), 879 | }, 880 | { 881 | source: "2020-07-24 23:14:15 +05:43:0", 882 | format: "%F %T %z:0", 883 | t: time.Date(2020, time.July, 24, 23, 14, 15, 0, time.FixedZone("", (5*60+43)*60)), 884 | }, 885 | { 886 | source: "2020-07-24 23:14:15 +05", 887 | format: "%F %T %z", 888 | t: time.Date(2020, time.July, 24, 23, 14, 15, 0, time.FixedZone("", 5*60*60)), 889 | }, 890 | { 891 | source: "2020-07-24T23:14:15+05Z", 892 | format: "%FT%T%z%z", 893 | t: time.Date(2020, time.July, 24, 23, 14, 15, 0, time.UTC), 894 | }, 895 | { 896 | source: "2020-07-24 23:14:15 ", 897 | format: "%F %T %z", 898 | parseErr: errors.New(`cannot parse "%z"`), 899 | }, 900 | { 901 | source: "2020-07-24 23:14:15 +", 902 | format: "%F %T %z", 903 | parseErr: errors.New(`cannot parse "%z"`), 904 | }, 905 | { 906 | source: "2020-07-24 23:14:15 +0", 907 | format: "%F %T %z", 908 | parseErr: errors.New(`cannot parse "%z"`), 909 | }, 910 | { 911 | source: "2020-07-24 23:14:15 +053", 912 | format: "%F %T %z", 913 | parseErr: errors.New(`unparsed string "3"`), 914 | }, 915 | { 916 | source: "2020-07-24 23:14:15 +04:3", 917 | format: "%F %T %z", 918 | parseErr: errors.New(`cannot parse "%z"`), 919 | }, 920 | { 921 | source: "2020-07-24 23:14:15 +04:30:", 922 | format: "%F %T %z", 923 | parseErr: errors.New(`unparsed string ":"`), 924 | }, 925 | { 926 | source: "2020-07-24 23:14:15 +04:30:0", 927 | format: "%F %T %z", 928 | parseErr: errors.New(`unparsed string ":0"`), 929 | }, 930 | { 931 | source: "2020-07-24 23:14:15 +04:3:00", 932 | format: "%F %T %z", 933 | parseErr: errors.New(`cannot parse "%z"`), 934 | }, 935 | { 936 | source: "2020-07-24 23:14:15 +0430:10", 937 | format: "%F %T %z", 938 | parseErr: errors.New(`unparsed string ":10"`), 939 | }, 940 | { 941 | source: "2020-07-24 23:14:15 +04:3010", 942 | format: "%F %T %z", 943 | parseErr: errors.New(`unparsed string "10"`), 944 | }, 945 | { 946 | source: "2020-07-24 23:14:15 +0:30", 947 | format: "%F %T %z", 948 | parseErr: errors.New(`cannot parse "%z"`), 949 | }, 950 | { 951 | source: "2020-07-24 23:14:15 +003a", 952 | format: "%F %T %z", 953 | parseErr: errors.New(`unparsed string "3a"`), 954 | }, 955 | { 956 | source: "2020-07-24 23:14:15 $0000", 957 | format: "%F %T %z", 958 | parseErr: errors.New(`cannot parse "%z"`), 959 | }, 960 | { 961 | source: "2020-07-24 23:14:15 +05:43:2a", 962 | format: "%F %T %z", 963 | parseErr: errors.New(`unparsed string ":2a"`), 964 | }, 965 | { 966 | source: "2020-07-24 23:14:15 +05:30%", 967 | format: "%F %T %:z%%", 968 | t: time.Date(2020, time.July, 24, 23, 14, 15, 0, time.FixedZone("", (5*60+30)*60)), 969 | }, 970 | { 971 | source: "2020-07-24 23:14:15 Z", 972 | format: "%F %T %:z", 973 | t: time.Date(2020, time.July, 24, 23, 14, 15, 0, time.UTC), 974 | }, 975 | { 976 | source: "2020-07-24 23:14:15 +05", 977 | format: "%F %T %:z", 978 | parseErr: errors.New(`expected ':' for "%:z"`), 979 | }, 980 | { 981 | source: "2020-07-24 23:14:15 +05-30", 982 | format: "%F %T %:z", 983 | parseErr: errors.New(`expected ':' for "%:z"`), 984 | }, 985 | { 986 | source: "2020-07-24 23:14:15 +0530", 987 | format: "%F %T %:z", 988 | parseErr: errors.New(`expected ':' for "%:z"`), 989 | }, 990 | { 991 | source: "2020-07-24 23:14:15 *05:30", 992 | format: "%F %T %:z", 993 | parseErr: errors.New(`cannot parse "%:z"`), 994 | }, 995 | { 996 | source: "2020-07-24 23:14:15 +0x:30", 997 | format: "%F %T %:z", 998 | parseErr: errors.New(`cannot parse "%:z"`), 999 | }, 1000 | { 1001 | source: "2020-07-24 23:14:15 +00:3x", 1002 | format: "%F %T %:z", 1003 | parseErr: errors.New(`cannot parse "%:z"`), 1004 | }, 1005 | { 1006 | source: "2020-07-24 23:14:15 ", 1007 | format: "%F %T %:", 1008 | parseErr: errors.New(`expected 'z' after "%:"`), 1009 | }, 1010 | { 1011 | source: "2020-07-24 23:14:15 ", 1012 | format: "%F %T %:H", 1013 | parseErr: errors.New(`expected 'z' after "%:"`), 1014 | }, 1015 | { 1016 | source: "2020-07-24 23:14:15 +05:30:10", 1017 | format: "%F %T %::z", 1018 | t: time.Date(2020, time.July, 24, 23, 14, 15, 0, time.FixedZone("", (5*60+30)*60+10)), 1019 | }, 1020 | { 1021 | source: "2020-07-24 23:14:15 -05:30:10", 1022 | format: "%F %T %::z", 1023 | t: time.Date(2020, time.July, 24, 23, 14, 15, 0, time.FixedZone("", -((5*60+30)*60+10))), 1024 | }, 1025 | { 1026 | source: "2020-07-24 23:14:15 ::-05:30::", 1027 | format: "%F %T ::%:z::", 1028 | t: time.Date(2020, time.July, 24, 23, 14, 15, 0, time.FixedZone("", -(5*60+30)*60)), 1029 | }, 1030 | { 1031 | source: "2020-07-24 23:14:15 -05:30:10 -04:20 +0300", 1032 | format: "%F %T %::z %:z %z", 1033 | t: time.Date(2020, time.July, 24, 23, 14, 15, 0, time.FixedZone("", 3*60*60)), 1034 | }, 1035 | { 1036 | source: "2020-07-24 23:14:15 +2400", 1037 | format: "%F %T %z", 1038 | parseErr: errors.New(`cannot parse "%z"`), 1039 | }, 1040 | { 1041 | source: "2020-07-24 23:14:15 -24:00", 1042 | format: "%F %T %:z", 1043 | parseErr: errors.New(`cannot parse "%:z"`), 1044 | }, 1045 | { 1046 | source: "2020-07-24 23:14:15 -24:00:00", 1047 | format: "%F %T %::z", 1048 | parseErr: errors.New(`cannot parse "%::z"`), 1049 | }, 1050 | { 1051 | source: "2020-07-24 23:14:15 +12:60", 1052 | format: "%F %T %z", 1053 | parseErr: errors.New(`cannot parse "%z"`), 1054 | }, 1055 | { 1056 | source: "2020-07-24 23:14:15 -12:00:60", 1057 | format: "%F %T %::z", 1058 | parseErr: errors.New(`cannot parse "%::z"`), 1059 | }, 1060 | { 1061 | source: "2020-07-24 23:14:15 +05", 1062 | format: "%F %T %::z", 1063 | parseErr: errors.New(`expected ':' for "%::z"`), 1064 | }, 1065 | { 1066 | source: "2020-07-24 23:14:15 +05:30:0", 1067 | format: "%F %T %::z", 1068 | parseErr: errors.New(`cannot parse "%::z"`), 1069 | }, 1070 | { 1071 | source: "2020-07-24 23:14:15 +05:30:0x", 1072 | format: "%F %T %::z", 1073 | parseErr: errors.New(`cannot parse "%::z"`), 1074 | }, 1075 | { 1076 | source: "2020-07-24 23:14:15 /05:30:00", 1077 | format: "%F %T %::z", 1078 | parseErr: errors.New(`cannot parse "%::z"`), 1079 | }, 1080 | { 1081 | source: "2020-07-24 23:14:15 +05300000", 1082 | format: "%F %T %::z", 1083 | parseErr: errors.New(`expected ':' for "%::z"`), 1084 | }, 1085 | { 1086 | source: "2020-07-24 23:14:15 +05-30:00", 1087 | format: "%F %T %::z", 1088 | parseErr: errors.New(`expected ':' for "%::z"`), 1089 | }, 1090 | { 1091 | source: "2020-07-24 23:14:15 +05:30-00", 1092 | format: "%F %T %::z", 1093 | parseErr: errors.New(`expected ':' for "%::z"`), 1094 | }, 1095 | { 1096 | source: "2020-07-24 23:14:15 +05:30", 1097 | format: "%F %T %::z", 1098 | parseErr: errors.New(`expected ':' for "%::z"`), 1099 | }, 1100 | { 1101 | source: "2020-07-24 23:14:15 ", 1102 | format: "%F %T %::", 1103 | parseErr: errors.New(`expected 'z' after "%::"`), 1104 | }, 1105 | { 1106 | source: "2020-07-24 23:14:15 ", 1107 | format: "%F %T %::Z", 1108 | parseErr: errors.New(`expected 'z' after "%::"`), 1109 | }, 1110 | { 1111 | source: "2020-07-24 23:14:15 ", 1112 | format: "%F %T %:::", 1113 | parseErr: errors.New(`expected 'z' after "%::"`), 1114 | }, 1115 | { 1116 | source: "2020-07-24 23:14:15 UTC", 1117 | format: "%F %T %Z", 1118 | t: time.Date(2020, time.July, 24, 23, 14, 15, 0, time.FixedZone("UTC", 0)), 1119 | }, 1120 | { 1121 | source: "X", 1122 | format: "%Z", 1123 | parseErr: errors.New(`cannot parse "X" with "%Z"`), 1124 | }, 1125 | { 1126 | source: "2020-07-24 23:14:15 +0530 (AAA)", 1127 | format: "%F %T %z (%Z)", 1128 | t: time.Date(2020, time.July, 24, 23, 14, 15, 0, time.FixedZone("AAA", (5*60+30)*60)), 1129 | }, 1130 | { 1131 | source: "2020-07-24 23:14:15 (AAA) +0530", 1132 | format: "%F %T (%Z) %z", 1133 | t: time.Date(2020, time.July, 24, 23, 14, 15, 0, time.FixedZone("AAA", (5*60+30)*60)), 1134 | }, 1135 | { 1136 | source: "01%02\t03\n450000", 1137 | format: "%H%%%M%t%S%n%f", 1138 | t: time.Date(1900, time.January, 1, 1, 2, 3, 450000000, time.UTC), 1139 | }, 1140 | { 1141 | source: "pp", 1142 | format: "%p", 1143 | parseErr: errors.New(`cannot parse "%p"`), 1144 | }, 1145 | { 1146 | source: "pp", 1147 | format: "%P", 1148 | parseErr: errors.New(`cannot parse "%P"`), 1149 | }, 1150 | { 1151 | format: "%E", 1152 | parseErr: errors.New(`unexpected format "%E"`), 1153 | }, 1154 | { 1155 | format: "%", 1156 | parseErr: errors.New(`stray "%"`), 1157 | }, 1158 | { 1159 | source: "", 1160 | format: "%%", 1161 | parseErr: errors.New("expected '%'"), 1162 | }, 1163 | { 1164 | source: "", 1165 | format: "x", 1166 | parseErr: errors.New("expected 'x'"), 1167 | }, 1168 | } 1169 | 1170 | func TestParse(t *testing.T) { 1171 | for _, tc := range parseTestCases { 1172 | t.Run(tc.source+"/"+tc.format, func(t *testing.T) { 1173 | got, err := timefmt.Parse(tc.source, tc.format) 1174 | if tc.parseErr == nil { 1175 | if err != nil { 1176 | t.Fatalf("expected no error but got: %v", err) 1177 | } 1178 | if !got.Equal(tc.t) { 1179 | t.Errorf("expected: %v, got: %v", tc.t, got) 1180 | } 1181 | name, offset := tc.t.Zone() 1182 | gotName, gotOffset := got.Zone() 1183 | if name != gotName || offset != gotOffset { 1184 | t.Errorf("expected zone: name = %s, offset = %d, got zone: name = %s, offset = %d", 1185 | name, offset, 1186 | gotName, gotOffset, 1187 | ) 1188 | } 1189 | } else { 1190 | if err == nil { 1191 | t.Fatalf("expected error %v but got: %v", tc.parseErr, err) 1192 | } 1193 | if !strings.Contains(err.Error(), tc.parseErr.Error()) { 1194 | t.Errorf("expected: %v, got: %v", tc.parseErr, err) 1195 | } 1196 | } 1197 | }) 1198 | } 1199 | } 1200 | 1201 | func ExampleParse() { 1202 | str := "2020-07-24 09:07:29" 1203 | t, err := timefmt.Parse(str, "%Y-%m-%d %H:%M:%S") 1204 | if err != nil { 1205 | log.Fatal(err) 1206 | } 1207 | fmt.Println(t) 1208 | // Output: 2020-07-24 09:07:29 +0000 UTC 1209 | } 1210 | 1211 | func ExampleParseInLocation() { 1212 | loc := time.FixedZone("JST", 9*60*60) 1213 | str := "2020-07-24 09:07:29" 1214 | t, err := timefmt.ParseInLocation(str, "%Y-%m-%d %H:%M:%S", loc) 1215 | if err != nil { 1216 | log.Fatal(err) 1217 | } 1218 | fmt.Println(t) 1219 | // Output: 2020-07-24 09:07:29 +0900 JST 1220 | } 1221 | -------------------------------------------------------------------------------- /timefmt.go: -------------------------------------------------------------------------------- 1 | // Package timefmt provides functions for formatting and parsing date time strings. 2 | package timefmt 3 | --------------------------------------------------------------------------------