pax_global_header00006660000000000000000000000064143421110150014502gustar00rootroot0000000000000052 comment=8eb94fd93fc611d6ff21c384791f07db7fe417b3 timefmt-go-0.1.5/000077500000000000000000000000001434211101500135555ustar00rootroot00000000000000timefmt-go-0.1.5/.github/000077500000000000000000000000001434211101500151155ustar00rootroot00000000000000timefmt-go-0.1.5/.github/workflows/000077500000000000000000000000001434211101500171525ustar00rootroot00000000000000timefmt-go-0.1.5/.github/workflows/ci.yaml000066400000000000000000000012671434211101500204370ustar00rootroot00000000000000name: CI on: push: branches: - main pull_request: jobs: test: name: Test runs-on: ubuntu-latest strategy: matrix: go: [1.19.x, 1.18.x, 1.17.x] steps: - name: Checkout code uses: actions/checkout@v3 - name: Setup Go uses: actions/setup-go@v3 with: go-version: ${{ matrix.go }} - name: Test run: make test - name: Test with GOARCH=386 run: env GOARCH=386 go test -v ./... - name: Test Coverage run: | go test -cover ./... | grep -F 100.0% || { go test -cover ./... echo Coverage decreased! exit 1 } >&2 - name: Lint run: make lint timefmt-go-0.1.5/CHANGELOG.md000066400000000000000000000017341434211101500153730ustar00rootroot00000000000000# Changelog ## [v0.1.5](https://github.com/itchyny/timefmt-go/compare/v0.1.4..v0.1.5) (2022-12-01) * support parsing time zone offset with name using both `%z` and `%Z` ## [v0.1.4](https://github.com/itchyny/timefmt-go/compare/v0.1.3..v0.1.4) (2022-09-01) * improve documents * drop support for Go 1.16 ## [v0.1.3](https://github.com/itchyny/timefmt-go/compare/v0.1.2..v0.1.3) (2021-04-14) * implement `ParseInLocation` for configuring the default location ## [v0.1.2](https://github.com/itchyny/timefmt-go/compare/v0.1.1..v0.1.2) (2021-02-22) * implement parsing/formatting time zone offset with colons (`%:z`, `%::z`, `%:::z`) * recognize `Z` as UTC on parsing time zone offset (`%z`) * fix padding on formatting time zone offset (`%z`) ## [v0.1.1](https://github.com/itchyny/timefmt-go/compare/v0.1.0..v0.1.1) (2020-09-01) * fix overflow check in 32-bit architecture ## [v0.1.0](https://github.com/itchyny/timefmt-go/compare/2c02364..v0.1.0) (2020-08-16) * initial implementation timefmt-go-0.1.5/LICENSE000066400000000000000000000020671434211101500145670ustar00rootroot00000000000000The MIT License (MIT) Copyright (c) 2020-2022 itchyny Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. timefmt-go-0.1.5/Makefile000066400000000000000000000004621434211101500152170ustar00rootroot00000000000000GOBIN ?= $(shell go env GOPATH)/bin .PHONY: all all: test .PHONY: test test: go test -v -race ./... .PHONY: lint lint: $(GOBIN)/staticcheck go vet ./... staticcheck -checks all,-ST1000 ./... $(GOBIN)/staticcheck: go install honnef.co/go/tools/cmd/staticcheck@latest .PHONY: clean clean: go clean timefmt-go-0.1.5/README.md000066400000000000000000000053511434211101500150400ustar00rootroot00000000000000# timefmt-go [![CI Status](https://github.com/itchyny/timefmt-go/workflows/CI/badge.svg)](https://github.com/itchyny/timefmt-go/actions) [![Go Report Card](https://goreportcard.com/badge/github.com/itchyny/timefmt-go)](https://goreportcard.com/report/github.com/itchyny/timefmt-go) [![MIT License](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/itchyny/timefmt-go/blob/main/LICENSE) [![release](https://img.shields.io/github/release/itchyny/timefmt-go/all.svg)](https://github.com/itchyny/timefmt-go/releases) [![pkg.go.dev](https://pkg.go.dev/badge/github.com/itchyny/timefmt-go)](https://pkg.go.dev/github.com/itchyny/timefmt-go) ### Efficient time formatting library (strftime, strptime) for Golang This is a Go language package for formatting and parsing date time strings. ```go package main import ( "fmt" "log" "github.com/itchyny/timefmt-go" ) func main() { t, err := timefmt.Parse("2020/07/24 09:07:29", "%Y/%m/%d %H:%M:%S") if err != nil { log.Fatal(err) } fmt.Println(t) // 2020-07-24 09:07:29 +0000 UTC str := timefmt.Format(t, "%Y/%m/%d %H:%M:%S") fmt.Println(str) // 2020/07/24 09:07:29 str = timefmt.Format(t, "%a, %d %b %Y %T %z") fmt.Println(str) // Fri, 24 Jul 2020 09:07:29 +0000 } ``` Please refer to [`man 3 strftime`](https://linux.die.net/man/3/strftime) and [`man 3 strptime`](https://linux.die.net/man/3/strptime) for formatters. As an extension, `%f` directive is supported for zero-padded microseconds, which originates from Python. Note that `E` and `O` modifier characters are not supported. ## Comparison to other libraries - This library - provides both formatting and parsing functions in pure Go language, - depends only on the Go standard libraries not to grow up dependency. - `Format` (`strftime`) implements glibc extensions including - width specifier like `%6Y %10B %4Z` (limited to 1024 bytes), - omitting padding modifier like `%-y-%-m-%-d`, - space padding modifier like `%_y-%_m-%_d`, - upper case modifier like `%^a %^b`, - swapping case modifier like `%#Z`, - time zone offset modifier like `%:z %::z %:::z`, - and its performance is very good. - `AppendFormat` is provided for reducing allocations. - `Parse` (`strptime`) allows to parse - composed directives like `%F %T`, - century years like `%C %y`, - week names like `%A` `%a` (parsed results are discarded). - `ParseInLocation` is provided for configuring the default location. ![](https://user-images.githubusercontent.com/375258/88606920-de475c80-d0b8-11ea-8d40-cbfee9e35c2e.jpg) ## Bug Tracker Report bug at [Issues・itchyny/timefmt-go - GitHub](https://github.com/itchyny/timefmt-go/issues). ## Author itchyny (https://github.com/itchyny) ## License This software is released under the MIT License, see LICENSE. timefmt-go-0.1.5/diff_test.go000066400000000000000000000020751434211101500160570ustar00rootroot00000000000000package timefmt_test import "strings" type stringBuilder struct { strings.Builder } func (sb *stringBuilder) writeDiff(s string) { sb.WriteString("\x1b[1;4m") sb.WriteString(s) sb.WriteString("\x1b[0m") } func diff(expected, got string) string { var sbx, sby stringBuilder xs := strings.Split(expected, " ") ys := strings.Split(got, " ") for i, j := 0, 0; ; i, j = i+1, j+1 { if i >= len(xs) { if j >= len(ys) { break } if j > 0 { sby.writeDiff(" ") } sby.writeDiff(ys[j]) continue } else if j >= len(ys) { if i > 0 { sbx.writeDiff(" ") } sbx.writeDiff(xs[i]) continue } if xs[i] == "" { if ys[j] != "" { sbx.writeDiff(" ") j-- continue } } else if ys[j] == "" { sby.writeDiff(" ") i-- continue } if i > 0 { sbx.WriteByte(' ') } if j > 0 { sby.WriteByte(' ') } if xs[i] == ys[j] { sbx.WriteString(xs[i]) sby.WriteString(ys[j]) } else { sbx.writeDiff(xs[i]) sby.writeDiff(ys[j]) } } return "diff:\nexpected: " + sbx.String() + "\n got: " + sby.String() } timefmt-go-0.1.5/format.go000066400000000000000000000246121434211101500154010ustar00rootroot00000000000000package timefmt import ( "math" "strconv" "time" ) // Format time to string using the format. func Format(t time.Time, format string) string { return string(AppendFormat(make([]byte, 0, 64), t, format)) } // AppendFormat appends formatted time string to the buffer. func AppendFormat(buf []byte, t time.Time, format string) []byte { year, month, day := t.Date() hour, min, sec := t.Clock() var width, colons int var padding byte var pending string var upper, swap bool for i := 0; i < len(format); i++ { if b := format[i]; b == '%' { if i++; i == len(format) { buf = append(buf, '%') break } b, width, padding, upper, swap = format[i], 0, '0', false, false L: switch b { case '-': if pending != "" { buf = append(buf, '-') break } if i++; i == len(format) { goto K } padding = ^paddingMask b = format[i] goto L case '_': if i++; i == len(format) { goto K } padding = ' ' | ^paddingMask b = format[i] goto L case '^': if i++; i == len(format) { goto K } upper = true b = format[i] goto L case '#': if i++; i == len(format) { goto K } swap = true b = format[i] goto L case '0': if i++; i == len(format) { goto K } padding = '0' | ^paddingMask b = format[i] goto L case '1', '2', '3', '4', '5', '6', '7', '8', '9': width = int(b & 0x0F) 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 } } else { break } } if width > maxWidth { width = maxWidth } if padding == ^paddingMask { padding = ' ' | ^paddingMask } if i == len(format) { goto K } goto L case 'Y': if width == 0 { width = 4 } buf = appendInt(buf, year, width, padding) case 'y': if width < 2 { width = 2 } buf = appendInt(buf, year%100, width, padding) case 'C': if width < 2 { width = 2 } buf = appendInt(buf, year/100, width, padding) case 'g': if width < 2 { width = 2 } year, _ := t.ISOWeek() buf = appendInt(buf, year%100, width, padding) case 'G': if width == 0 { width = 4 } year, _ := t.ISOWeek() buf = appendInt(buf, year, width, padding) case 'm': if width < 2 { width = 2 } buf = appendInt(buf, int(month), width, padding) case 'B': buf = appendString(buf, longMonthNames[month-1], width, padding, upper, swap) case 'b', 'h': buf = appendString(buf, shortMonthNames[month-1], width, padding, upper, swap) case 'A': buf = appendString(buf, longWeekNames[t.Weekday()], width, padding, upper, swap) case 'a': buf = appendString(buf, shortWeekNames[t.Weekday()], width, padding, upper, swap) case 'w': for ; width > 1; width-- { buf = append(buf, padding&paddingMask) } buf = append(buf, '0'+byte(t.Weekday())) 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)) case 'V': if width < 2 { width = 2 } _, week := t.ISOWeek() buf = appendInt(buf, week, width, padding) case 'U': if width < 2 { width = 2 } week := (t.YearDay() + 6 - int(t.Weekday())) / 7 buf = appendInt(buf, week, width, 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) case 'e': if padding < ^paddingMask { padding = ' ' } fallthrough case 'd': if width < 2 { width = 2 } buf = appendInt(buf, day, width, padding) case 'j': if width < 3 { width = 3 } buf = appendInt(buf, t.YearDay(), width, padding) case 'k': if padding < ^paddingMask { padding = ' ' } fallthrough case 'H': if width < 2 { width = 2 } buf = appendInt(buf, hour, width, 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) 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) 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) case 'S': if width < 2 { width = 2 } buf = appendInt(buf, sec, width, padding) case 's': if padding < ^paddingMask { padding = ' ' } buf = appendInt(buf, int(t.Unix()), width, padding) case 'f': if width == 0 { width = 6 } buf = appendInt(buf, t.Nanosecond()/1000, width, padding) case 'Z', 'z': name, offset := t.Zone() if b == 'Z' && name != "" { buf = appendString(buf, name, width, padding, upper, swap) break } i := len(buf) if padding != ^paddingMask { for ; width > 1; width-- { buf = append(buf, padding&paddingMask) } } j := len(buf) if offset < 0 { buf = append(buf, '-') offset = -offset } else { buf = append(buf, '+') } k := len(buf) buf = appendInt(buf, offset/3600, 2, padding) if buf[k] == ' ' { buf[k-1], buf[k] = buf[k], buf[k-1] } if k = offset % 3600; colons <= 2 || k != 0 { if colons != 0 { buf = append(buf, ':') } buf = appendInt(buf, k/60, 2, '0') if k %= 60; colons == 2 || colons == 3 && k != 0 { buf = append(buf, ':') buf = appendInt(buf, k, 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 padding&paddingMask == '0' { for ; k > i; k-- { buf[k-1], buf[k] = buf[k], buf[k-1] } } } case ':': if pending != "" { buf = append(buf, ':') } else { colons = 1 M: for i++; i < len(format); i++ { switch format[i] { case ':': colons++ case 'z': if colons > 3 { i++ break M } b = 'z' goto L default: break M } } buf = appendLast(buf, format[:i], width, padding) i-- colons = 0 } case 't': buf = appendString(buf, "\t", width, padding, false, false) case 'n': buf = appendString(buf, "\n", width, padding, false, false) case '%': buf = appendString(buf, "%", width, padding, false, false) 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) } if pending != "" { b, pending, width, padding = pending[0], pending[1:], 0, '0' goto L } } else { buf = append(buf, b) } } return buf K: return appendLast(buf, format, width, padding) } func appendInt(buf []byte, num, width int, padding byte) []byte { 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] } } if num < 100 { if num < 10 { goto L1 } goto L2 } else if num < 10000 { if num < 1000 { goto L3 } 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') } func appendString(buf []byte, str string, width int, padding byte, upper, swap bool) []byte { if width > len(str) && padding != ^paddingMask { if padding < ^paddingMask { padding = ' ' } else { padding &= paddingMask } for width -= len(str); width > 0; width-- { buf = append(buf, padding) } } switch { case swap: if str[len(str)-1] < 'a' { for _, b := range []byte(str) { buf = append(buf, b|0x20) } break } fallthrough case upper: for _, b := range []byte(str) { buf = append(buf, b&0x5F) } default: buf = append(buf, str...) } return buf } func appendLast(buf []byte, format string, width int, padding byte) []byte { for i := len(format) - 1; i >= 0; i-- { if format[i] == '%' { buf = appendString(buf, format[i:], width, padding, false, false) break } } return buf } const paddingMask byte = 0x7F var longMonthNames = []string{ "January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December", } var shortMonthNames = []string{ "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec", } var longWeekNames = []string{ "Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", } var shortWeekNames = []string{ "Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat", } 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", } timefmt-go-0.1.5/format_test.go000066400000000000000000000474521434211101500164470ustar00rootroot00000000000000package timefmt_test import ( "fmt" "strings" "testing" "time" "github.com/itchyny/timefmt-go" ) var formatTestCases = []struct { format string t time.Time expected string }{ { format: "%Y", t: time.Date(2020, time.January, 1, 0, 0, 0, 0, time.UTC), expected: "2020", }, { format: "%Y %1Y %2Y %3Y %4Y %5Y %-Y", t: time.Date(1000, time.January, 1, 0, 0, 0, 0, time.UTC), expected: "1000 1000 1000 1000 1000 01000 1000", }, { format: "%Y %1Y %2Y %3Y %4Y %5Y %-Y", t: time.Date(999, time.January, 1, 0, 0, 0, 0, time.UTC), expected: "0999 999 999 999 0999 00999 999", }, { format: "%Y %1Y %2Y %3Y %4Y %5Y %-Y", t: time.Date(100, time.January, 1, 0, 0, 0, 0, time.UTC), expected: "0100 100 100 100 0100 00100 100", }, { format: "%Y %1Y %2Y %3Y %4Y %5Y %-Y", t: time.Date(99, time.January, 1, 0, 0, 0, 0, time.UTC), expected: "0099 99 99 099 0099 00099 99", }, { format: "%Y %1Y %2Y %3Y %4Y %5Y %-Y", t: time.Date(9999, time.January, 1, 0, 0, 0, 0, time.UTC), expected: "9999 9999 9999 9999 9999 09999 9999", }, { format: "%Y %1Y %2Y %3Y %4Y %5Y %-Y", t: time.Date(10000, time.January, 1, 0, 0, 0, 0, time.UTC), expected: "10000 10000 10000 10000 10000 10000 10000", }, { format: "[%Y]", t: time.Date(2020, time.January, 1, 0, 0, 0, 0, time.UTC), expected: "[2020]", }, { format: "%y %_y %-y %4y %_4y %04y %_04y %0_4y", t: time.Date(2020, time.January, 1, 0, 0, 0, 0, time.UTC), expected: "20 20 20 0020 20 0020 0020 20", }, { format: "%y %_y %-y %4y %_4y %04y %_04y %0_4y", t: time.Date(2009, time.January, 1, 0, 0, 0, 0, time.UTC), expected: "09 9 9 0009 9 0009 0009 9", }, { format: "%C", t: time.Date(2009, time.January, 1, 0, 0, 0, 0, time.UTC), expected: "20", }, { format: "%C%y", t: time.Date(1758, time.January, 1, 0, 0, 0, 0, time.UTC), expected: "1758", }, { format: "%Y-%m", t: time.Date(2020, time.May, 1, 0, 0, 0, 0, time.UTC), expected: "2020-05", }, { format: "%Y-%m-%d", t: time.Date(2020, time.September, 10, 0, 0, 0, 0, time.UTC), expected: "2020-09-10", }, { format: "%-Y-%-m-%-d", t: time.Date(999, time.September, 8, 0, 0, 0, 0, time.UTC), expected: "999-9-8", }, { format: "%_Y-%_m-%_d", t: time.Date(999, time.September, 8, 0, 0, 0, 0, time.UTC), expected: " 999- 9- 8", }, { format: "%8Y-%12m-%16d", t: time.Date(2020, time.September, 12, 0, 0, 0, 0, time.UTC), expected: "00002020-000000000009-0000000000000012", }, { format: "%_8Y-%_12m-%_16d", t: time.Date(2020, time.September, 12, 0, 0, 0, 0, time.UTC), expected: " 2020- 9- 12", }, { format: "%Y/%m/%d", t: time.Date(2020, time.October, 9, 0, 0, 0, 0, time.UTC), expected: "2020/10/09", }, { format: "%e %-e %_e %4e %04e", t: time.Date(2020, time.January, 9, 0, 0, 0, 0, time.UTC), expected: " 9 9 9 9 0009", }, { format: "%B %_B %^B %#B %12B %^12B %012B %0^12B", t: time.Date(2020, time.October, 1, 0, 0, 0, 0, time.UTC), expected: "October October OCTOBER OCTOBER October OCTOBER 00000October 00000OCTOBER", }, { format: "%b %^b %#b %8b %^8b %08b %^08b %#08b", t: time.Date(2020, time.September, 1, 0, 0, 0, 0, time.UTC), expected: "Sep SEP SEP Sep SEP 00000Sep 00000SEP 00000SEP", }, { format: "%h %^h %#h %8h %^8h %08h %^08h", t: time.Date(2020, time.November, 1, 0, 0, 0, 0, time.UTC), expected: "Nov NOV NOV Nov NOV 00000Nov 00000NOV", }, { format: "%A %a %w %u", t: time.Date(2020, time.January, 1, 0, 0, 0, 0, time.UTC), expected: "Wednesday Wed 3 3", }, { format: "%A %a %w %u", t: time.Date(2020, time.January, 2, 0, 0, 0, 0, time.UTC), expected: "Thursday Thu 4 4", }, { format: "%A %a %w %u", t: time.Date(2020, time.January, 4, 0, 0, 0, 0, time.UTC), expected: "Saturday Sat 6 6", }, { format: "%A %a %w %u", t: time.Date(2020, time.January, 5, 0, 0, 0, 0, time.UTC), expected: "Sunday Sun 0 7", }, { format: "%A %a %w %u", t: time.Date(2020, time.January, 6, 0, 0, 0, 0, time.UTC), expected: "Monday Mon 1 1", }, { format: "%^A %#A %8A %^8A %08A %^08A %^a %#a %8a %^8a %08a %^08a", t: time.Date(2020, time.January, 6, 0, 0, 0, 0, time.UTC), expected: "MONDAY MONDAY Monday MONDAY 00Monday 00MONDAY MON MON Mon MON 00000Mon 00000MON", }, { format: "%4w %4u %-4w %-4u %_4w %_4u %04w %04u", t: time.Date(2020, time.January, 6, 0, 0, 0, 0, time.UTC), expected: "0001 0001 1 1 1 1 0001 0001", }, { format: "%g %G %a %V %U %W", t: time.Date(2020, time.January, 1, 0, 0, 0, 0, time.UTC), expected: "20 2020 Wed 01 00 00", }, { format: "%g %G %a %V %U %W", t: time.Date(2020, time.January, 4, 0, 0, 0, 0, time.UTC), expected: "20 2020 Sat 01 00 00", }, { format: "%g %G %a %V %U %W", t: time.Date(2020, time.January, 5, 0, 0, 0, 0, time.UTC), expected: "20 2020 Sun 01 01 00", }, { format: "%g %G %a %V %U %W", t: time.Date(2020, time.January, 6, 0, 0, 0, 0, time.UTC), expected: "20 2020 Mon 02 01 01", }, { format: "%-V %-U %-W %_V %_U %_W %4V %4U %4W %_4V %_4U %_4W %04V %04U %04W", t: time.Date(2020, time.January, 6, 0, 0, 0, 0, time.UTC), expected: "2 1 1 2 1 1 0002 0001 0001 2 1 1 0002 0001 0001", }, { format: "%g %G %a %V %U %W", t: time.Date(2009, time.December, 31, 0, 0, 0, 0, time.UTC), expected: "09 2009 Thu 53 52 52", }, { format: "%g %G %a %V %U %W", t: time.Date(2010, time.January, 1, 0, 0, 0, 0, time.UTC), expected: "09 2009 Fri 53 00 00", }, { format: "%g %G %a %V %U %W", t: time.Date(2010, time.January, 2, 0, 0, 0, 0, time.UTC), expected: "09 2009 Sat 53 00 00", }, { format: "%g %G %a %V %U %W", t: time.Date(2010, time.January, 3, 0, 0, 0, 0, time.UTC), expected: "09 2009 Sun 53 01 00", }, { format: "%g %G %a %V %U %W", t: time.Date(2010, time.January, 4, 0, 0, 0, 0, time.UTC), expected: "10 2010 Mon 01 01 01", }, { format: "%Y-%j-%-j", t: time.Date(2020, time.January, 1, 0, 0, 0, 0, time.UTC), expected: "2020-001-1", }, { format: "%Y-%j-%-j", t: time.Date(2020, time.January, 10, 0, 0, 0, 0, time.UTC), expected: "2020-010-10", }, { format: "%Y-%j-%-j", t: time.Date(2020, time.February, 2, 0, 0, 0, 0, time.UTC), expected: "2020-033-33", }, { format: "%Y-%j-%-j", t: time.Date(2020, time.April, 8, 0, 0, 0, 0, time.UTC), expected: "2020-099-99", }, { format: "%Y-%j-%-j", t: time.Date(2020, time.April, 9, 0, 0, 0, 0, time.UTC), expected: "2020-100-100", }, { format: "%Y-%j-%-j", t: time.Date(2020, time.April, 10, 0, 0, 0, 0, time.UTC), expected: "2020-101-101", }, { format: "%Y-%j-%-j", t: time.Date(2020, time.August, 1, 0, 0, 0, 0, time.UTC), expected: "2020-214-214", }, { format: "%Y-%j-%-j", t: time.Date(2020, time.December, 31, 0, 0, 0, 0, time.UTC), expected: "2020-366-366", }, { format: "%Y-%m-%d %H:%M:%S.%f", t: time.Date(2020, time.September, 8, 7, 6, 5, 43210000, time.UTC), expected: "2020-09-08 07:06:05.043210", }, { format: "%-y-%-m-%-d %-H:%-M:%-S.%-f", t: time.Date(2002, time.September, 8, 7, 6, 5, 43210000, time.UTC), expected: "2-9-8 7:6:5.43210", }, { format: "%_y-%_m-%_d %_H:%_M:%_S.%_f", t: time.Date(2002, time.September, 8, 7, 6, 5, 43210000, time.UTC), expected: " 2- 9- 8 7: 6: 5. 43210", }, { format: "%4y-%4m-%4d %4H:%4M:%4S.%8f", t: time.Date(2002, time.September, 8, 7, 6, 5, 43210000, time.UTC), expected: "0002-0009-0008 0007:0006:0005.00043210", }, { format: "%04y-%04m-%04d %04H:%04M:%04S.%08f", t: time.Date(2002, time.September, 8, 7, 6, 5, 43210000, time.UTC), expected: "0002-0009-0008 0007:0006:0005.00043210", }, { format: "%H:%M:%S.%f", t: time.Date(2020, time.January, 1, 1, 2, 3, 450000000, time.UTC), expected: "01:02:03.450000", }, { format: "%I:%M:%S %p %P", t: time.Date(2020, time.January, 1, 0, 2, 3, 0, time.UTC), expected: "12:02:03 AM am", }, { format: "%I:%M:%S %p %P", t: time.Date(2020, time.January, 1, 1, 2, 3, 0, time.UTC), expected: "01:02:03 AM am", }, { format: "%I:%M:%S %p %P", t: time.Date(2020, time.January, 1, 11, 2, 3, 0, time.UTC), expected: "11:02:03 AM am", }, { format: "%I:%M:%S %p %P", t: time.Date(2020, time.January, 1, 12, 2, 3, 0, time.UTC), expected: "12:02:03 PM pm", }, { format: "%I:%M:%S %p %P", t: time.Date(2020, time.January, 1, 13, 2, 3, 0, time.UTC), expected: "01:02:03 PM pm", }, { format: "%I:%M:%S %p %P", t: time.Date(2020, time.January, 1, 23, 2, 3, 0, time.UTC), expected: "11:02:03 PM pm", }, { format: "%-I:%-M:%-S %-p %-P", t: time.Date(2020, time.January, 1, 13, 2, 3, 0, time.UTC), expected: "1:2:3 PM pm", }, { format: "%4I:%4M:%4S %4p %4P", t: time.Date(2020, time.January, 1, 13, 2, 3, 0, time.UTC), expected: "0001:0002:0003 PM pm", }, { format: "%_I:%_M:%_S %_p %_P", t: time.Date(2020, time.January, 1, 13, 2, 3, 0, time.UTC), expected: " 1: 2: 3 PM pm", }, { format: "%_4I:%_4M:%_4S %_4p %_4P", t: time.Date(2020, time.January, 1, 13, 2, 3, 0, time.UTC), expected: " 1: 2: 3 PM pm", }, { format: "%-4I:%-4M:%-4S %-4p %-4P", t: time.Date(2020, time.January, 1, 13, 2, 3, 0, time.UTC), expected: " 1: 2: 3 PM pm", }, { format: "%04I:%04M:%04S %04p %04P", t: time.Date(2020, time.January, 1, 13, 2, 3, 0, time.UTC), expected: "0001:0002:0003 00PM 00pm", }, { format: "%p %P %^p %^P %#p %#P %#^p %_4p %_4P %04p %04P %^04p %^04P", t: time.Date(2020, time.January, 1, 11, 2, 3, 0, time.UTC), expected: "AM am AM AM am AM am AM am 00AM 00am 00AM 00AM", }, { format: "%p %P %^p %^P %#p %#P %#^p %_4p %_4P %04p %04P %^04p %^04P", t: time.Date(2020, time.January, 1, 23, 2, 3, 0, time.UTC), expected: "PM pm PM PM pm PM pm PM pm 00PM 00pm 00PM 00PM", }, { format: "%k %-k %_k %4k %0k %04k", t: time.Date(2020, time.January, 1, 9, 0, 0, 0, time.UTC), expected: " 9 9 9 9 09 0009", }, { format: "%l %-l %_l %4l %0l %04l", t: time.Date(2020, time.January, 1, 20, 0, 0, 0, time.UTC), expected: " 8 8 8 8 08 0008", }, { format: "%s %12s %_12s %012s", t: time.Date(2020, time.August, 30, 5, 30, 32, 0, time.UTC), expected: "1598765432 1598765432 1598765432 001598765432", }, { format: "%R %r %T %D %x %X", t: time.Date(2020, time.February, 9, 23, 14, 15, 0, time.UTC), expected: "23:14 11:14:15 PM 23:14:15 02/09/20 02/09/20 23:14:15", }, { format: "%F %T", t: time.Date(2020, time.February, 9, 23, 14, 15, 0, time.UTC), expected: "2020-02-09 23:14:15", }, { format: "%9F %9T", t: time.Date(2020, time.February, 9, 23, 14, 15, 0, time.UTC), expected: "2020-02-09 23:14:15", }, { format: "%v %X", t: time.Date(2020, time.July, 9, 23, 14, 15, 0, time.UTC), expected: " 9-Jul-2020 23:14:15", }, { format: "%c", t: time.Date(2020, time.February, 9, 23, 14, 15, 0, time.UTC), expected: "Sun Feb 9 23:14:15 2020", }, { format: "%-c", t: time.Date(2020, time.February, 9, 23, 4, 5, 0, time.UTC), expected: "Sun Feb 9 23:04:05 2020", }, { format: "%^c", t: time.Date(2020, time.February, 9, 23, 4, 5, 0, time.UTC), expected: "SUN FEB 9 23:04:05 2020", }, { format: "%#c", t: time.Date(2020, time.February, 9, 23, 4, 5, 0, time.UTC), expected: "Sun Feb 9 23:04:05 2020", }, { format: "%+", t: time.Date(2020, time.February, 9, 23, 4, 5, 0, time.UTC), expected: "Sun Feb 9 23:04:05 UTC 2020", }, { format: "%^+", t: time.Date(2020, time.February, 9, 23, 4, 5, 0, time.UTC), expected: "SUN FEB 9 23:04:05 UTC 2020", }, { format: "%#+", t: time.Date(2020, time.February, 9, 23, 4, 5, 0, time.UTC), expected: "Sun Feb 9 23:04:05 UTC 2020", }, { format: "%F %T %z %-z %_4z %04z", t: time.Date(2020, time.July, 24, 23, 14, 15, 0, time.UTC), expected: "2020-07-24 23:14:15 +0000 +000 +000 +0000", }, { format: "%F %T %z", t: time.Date(2020, time.July, 24, 23, 14, 15, 0, time.FixedZone("", -8*60*60)), expected: "2020-07-24 23:14:15 -0800", }, { format: "%F %T %z", t: time.Date(2020, time.July, 24, 23, 14, 15, 0, time.FixedZone("", 9*60*60)), expected: "2020-07-24 23:14:15 +0900", }, { format: "%F %T %z", t: time.Date(2020, time.July, 24, 23, 14, 15, 0, time.FixedZone("", (5*60+30)*60)), expected: "2020-07-24 23:14:15 +0530", }, { format: "%F %T %Z", t: time.Date(2020, time.July, 24, 23, 14, 15, 0, time.FixedZone("JST", 9*60*60)), expected: "2020-07-24 23:14:15 JST", }, { format: "%F %T %Z", t: time.Date(2020, time.July, 24, 23, 14, 15, 0, time.FixedZone("", 9*60*60)), expected: "2020-07-24 23:14:15 +0900", }, { format: "%Z %^Z %#Z %#^Z %^#Z", t: time.Date(2020, time.July, 24, 23, 14, 15, 0, time.FixedZone("JST", 9*60*60)), expected: "JST JST jst jst jst", }, { format: "%8Z %08Z %8z %_8z %-z %08z %2z %3z %4z %5z %6z %6:z %7:z %:%Z", t: time.Date(2020, time.July, 24, 23, 14, 15, 0, time.FixedZone("JST", 9*60*60)), expected: " JST 00000JST +0000900 +900 +900 +0000900 +0900 +0900 +0900 +0900 +00900 +09:00 +009:00 %:JST", }, { format: "%8Z %08Z %8z %_8z %-z %08z %4z %5z %6z %6:z %7:z", t: time.Date(2020, time.July, 24, 23, 14, 15, 0, time.FixedZone("HAST", -10*60*60)), expected: " HAST 0000HAST -0001000 -1000 -1000 -0001000 -1000 -1000 -01000 -10:00 -010:00", }, { format: "%8z %_8z %-z %08z %4z %5z %6z %6:z %7:z", t: time.Date(2020, time.July, 24, 23, 14, 15, 0, time.FixedZone("", -(5*60+30)*60)), expected: "-0000530 -530 -530 -0000530 -0530 -0530 -00530 -05:30 -005:30", }, { format: "%8z %_8z %-z %08z %4z %5z %6z %6:z %7:z", t: time.Date(2020, time.July, 24, 23, 14, 15, 0, time.FixedZone("", 30*60)), expected: "+0000030 +030 +030 +0000030 +0030 +0030 +00030 +00:30 +000:30", }, { format: "%:z %::z %:::z %::::z %:Z %-:z %_::z %8z %08:z %_8:z %_8::z %8:::z %-:::z %:-z %:", t: time.Date(2020, time.July, 24, 23, 14, 15, 0, time.UTC), 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 %:", }, { format: "%:z %::z %:::z %::::z %:Z %-:z %_::z %8z %08:z %_8:z %_8::z %8:::z %-:::z %:-z %:", t: time.Date(2020, time.July, 24, 23, 14, 15, 0, time.FixedZone("JST", 9*60*60)), 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 %:", }, { format: "%:z %::z %:::z %::::z %:Z %-:z %_::z %8z %08:z %_8:z %_8::z %8:::z %-:::z %:-z %:", t: time.Date(2020, time.July, 24, 23, 14, 15, 0, time.FixedZone("HAST", -(10*60+30)*60)), 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 %:", }, { format: "%:z %::z %:::z %::::z %:Z %-:z %_::z %8z %08:z %_8:z %_8::z %8:::z %-:::z %:-z %:", t: time.Date(2020, time.July, 24, 23, 14, 15, 0, time.FixedZone("", 60*60+2)), 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 %:", }, { format: "%:z %::z %:::z %::::z %:Z %-:z %_::z %8z %08:z %_8:z %_8::z %8:::z %-:::z %:-z %:", t: time.Date(2020, time.July, 24, 23, 14, 15, 0, time.FixedZone("", -9*60*60-62)), 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 %:", }, { format: "%:: %6: %z", t: time.Date(2020, time.July, 24, 23, 14, 15, 0, time.FixedZone("", 9*60*60)), expected: "%:: %6: +0900", }, { format: "%H%%%M%t%S%n%f", t: time.Date(2020, time.January, 1, 1, 2, 3, 450000000, time.UTC), expected: "01%02\t03\n450000", }, { format: "%t,%4t,%04t,%n,%4n,%04n,%4", t: time.Date(2020, time.January, 1, 1, 1, 1, 0, time.UTC), expected: "\t, \t,000\t,\n, \n,000\n, %4", }, { format: "%1Y %1y %1C %1g %1G %1m %1V %1U %1d %1j %1H %1M %1S %1f", t: time.Date(2009, time.January, 2, 3, 4, 5, 6000, time.UTC), expected: "2009 09 20 09 2009 01 01 00 02 002 03 04 05 6", }, { format: "%2Y %2y %2C %2g %2G %2m %2V %2U %2d %2j %2H %2M %2S %2f", t: time.Date(2009, time.January, 2, 3, 4, 5, 6000, time.UTC), expected: "2009 09 20 09 2009 01 01 00 02 002 03 04 05 06", }, { format: "%3Y %3y %3C %3g %3G %3m %3V %3U %3d %3j %3H %3M %3S %3f", t: time.Date(2009, time.January, 2, 3, 4, 5, 6000, time.UTC), expected: "2009 009 020 009 2009 001 001 000 002 002 003 004 005 006", }, { format: "%4Y %4y %4C %4g %4G %4m %4V %4U %4d %4j %4H %4M %4S %4f", t: time.Date(2009, time.January, 2, 3, 4, 5, 60000000, time.UTC), expected: "2009 0009 0020 0009 2009 0001 0001 0000 0002 0002 0003 0004 0005 60000", }, { format: "%1d %2d %3d %4d %5d %6d %7d %8d %9d %10d", t: time.Date(2020, time.January, 1, 1, 1, 1, 0, time.UTC), expected: "01 01 001 0001 00001 000001 0000001 00000001 000000001 0000000001", }, { format: "%10000Y", t: time.Date(2020, time.January, 1, 1, 1, 1, 0, time.UTC), expected: fmt.Sprintf("%01024d", 2020), }, { format: "%922337203685477580Y", t: time.Date(2020, time.January, 1, 1, 1, 1, 0, time.UTC), expected: fmt.Sprintf("%01024d", 2020), }, { format: "%9223372036854775809Y", t: time.Date(2020, time.January, 1, 1, 1, 1, 0, time.UTC), expected: fmt.Sprintf("%01024d", 2020), }, { format: "%18446744073709551630Y", t: time.Date(2020, time.January, 1, 1, 1, 1, 0, time.UTC), expected: fmt.Sprintf("%01024d", 2020), }, { format: "%!%.%[%]%|%$%-", expected: "%!%.%[%]%|%$%-", }, { format: "%4_", expected: " %4_", }, { format: "%09_", expected: "00000%09_", }, { format: "%^", expected: "%^", }, { format: "%#", expected: "%#", }, { format: "%0", expected: "%0", }, { format: "%4", expected: " %4", }, { format: "%-9", expected: " %-9", }, { format: "%-9^", expected: " %-9^", }, { format: "%-09^", expected: "0000%-09^", }, { format: "%06", expected: "000%06", }, { format: "%6J", expected: " %6J", }, { format: "%", expected: "%", }, } func TestFormat(t *testing.T) { for _, tc := range formatTestCases { var name string if len(tc.expected) < 1000 { name = tc.expected + "/" + tc.format } else { name = strings.ReplaceAll(tc.expected+"/"+tc.format, strings.Repeat("0", 30), "0.") } t.Run(name, func(t *testing.T) { got := timefmt.Format(tc.t, tc.format) if got != tc.expected { t.Error(diff(tc.expected, got)) } }) } } func TestAppendFormat(t *testing.T) { tm := time.Date(2020, time.January, 2, 3, 4, 5, 0, time.UTC) buf := timefmt.AppendFormat(make([]byte, 0, 64), tm, "%c") if got, expected := string(buf), "Thu Jan 2 03:04:05 2020"; got != expected { t.Errorf("expected: %q, got: %q", expected, got) } } func ExampleFormat() { t := time.Date(2020, time.July, 24, 9, 7, 29, 0, time.UTC) str := timefmt.Format(t, "%Y-%m-%d %H:%M:%S") fmt.Println(str) // Output: 2020-07-24 09:07:29 } func ExampleAppendFormat() { t := time.Date(2020, time.July, 24, 9, 7, 29, 0, time.UTC) buf := make([]byte, 0, 64) buf = append(buf, '(') buf = timefmt.AppendFormat(buf, t, "%Y-%m-%d %H:%M:%S") buf = append(buf, ')') fmt.Println(string(buf)) // Output: (2020-07-24 09:07:29) } timefmt-go-0.1.5/go.mod000066400000000000000000000000561434211101500146640ustar00rootroot00000000000000module github.com/itchyny/timefmt-go go 1.17 timefmt-go-0.1.5/parse.go000066400000000000000000000217261434211101500152260ustar00rootroot00000000000000package timefmt import ( "errors" "fmt" "time" ) // Parse time string using the format. func Parse(source, format string) (t time.Time, err error) { return parse(source, format, time.UTC, time.Local) } // ParseInLocation parses time string with the default location. // The location is also used to parse the time zone name (%Z). func ParseInLocation(source, format string, loc *time.Location) (t time.Time, err error) { return parse(source, format, loc, loc) } func parse(source, format string, loc, base *time.Location) (t time.Time, err error) { year, month, day, hour, min, sec, nsec := 1900, 1, 1, 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, 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 %") return } b = format[i] L: switch b { case 'Y': if year, j, err = parseNumber(source, j, 4, 'Y'); err != nil { return } case 'y': if year, j, err = parseNumber(source, j, 2, 'y'); err != nil { return } if year < 69 { year += 2000 } else { year += 1900 } case 'C': if century, j, err = parseNumber(source, j, 2, 'C'); err != nil { 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 { return } case 'm': if month, j, err = parseNumber(source, j, 2, 'm'); err != nil { return } case 'B': if month, j, err = lookup(source, j, longMonthNames, 'B'); err != nil { return } case 'b', 'h': if month, j, err = lookup(source, j, shortMonthNames, b); err != nil { return } case 'A': if _, j, err = lookup(source, j, longWeekNames, 'A'); err != nil { return } case 'a': if _, j, err = lookup(source, j, shortWeekNames, 'a'); err != nil { return } case 'w': if j >= l || source[j] < '0' || '6' < source[j] { err = parseFormatError(b) return } j++ case 'u': if j >= l || source[j] < '1' || '7' < source[j] { err = parseFormatError(b) return } j++ case 'V', 'U', 'W': if _, j, err = parseNumber(source, j, 2, b); err != nil { return } case 'e': if j < l && source[j] == ' ' { j++ } fallthrough case 'd': if day, j, err = parseNumber(source, j, 2, b); err != nil { return } case 'j': if yday, j, err = parseNumber(source, j, 3, 'j'); err != nil { return } case 'k': if j < l && source[j] == ' ' { j++ } fallthrough case 'H': if hour, j, err = parseNumber(source, j, 2, b); err != nil { return } case 'l': if j < l && source[j] == ' ' { j++ } fallthrough case 'I': if hour, j, err = parseNumber(source, j, 2, b); err != nil { return } if hour == 12 { hour = 0 } case 'p', 'P': var ampm int if ampm, j, err = lookup(source, j, []string{"AM", "PM"}, 'p'); err != nil { return } pm = ampm == 2 case 'M': if min, j, err = parseNumber(source, j, 2, 'M'); err != nil { return } case 'S': if sec, j, err = parseNumber(source, j, 2, 'S'); err != nil { return } case 's': var unix int if unix, j, err = parseNumber(source, j, 10, 's'); err != nil { return } t = time.Unix(int64(unix), 0).In(time.UTC) var mon time.Month year, mon, day = t.Date() hour, min, sec = t.Clock() month = int(mon) case 'f': var usec, k, d int if usec, k, err = parseNumber(source, j, 6, 'f'); err != nil { return } for j, d = k, k-j; d < 6; d++ { usec *= 10 } nsec = usec * 1000 case 'Z': k := j for ; k < l; k++ { if c := source[k]; c < 'A' || 'Z' < c { break } } t, err = time.ParseInLocation("MST", source[j:k], base) if err != nil { err = fmt.Errorf(`cannot parse %q with "%%Z"`, source[j:k]) return } if hasZoneOffset { name, _ := t.Zone() _, offset := locationZone(loc) loc = time.FixedZone(name, offset) } else { loc = t.Location() } hasZoneName = true j = k case 'z': if j >= l { err = parseZFormatError(colons) return } 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 { 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") 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 { err = parseZFormatError(colons & 3) return } } if j = k; 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) return } } else { j = k } } var name string if hasZoneName { name, _ = locationZone(loc) } loc, colons = time.FixedZone(name, sign*((hour*60+min)*60+sec)), 0 hasZoneOffset = true case 'Z': loc, colons, j = time.UTC, 0, j+1 default: err = parseZFormatError(colons) return } case ':': if pending != "" { if j >= l || source[j] != b { err = expectedFormatError(b) return } j++ } else { 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 } goto L } case 't', 'n': k := j K: for ; k < l; k++ { switch source[k] { case ' ', '\t', '\n', '\v', '\f', '\r': default: break K } } if k == 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++ default: if pending == "" { var ok bool if pending, ok = compositions[b]; ok { break } err = fmt.Errorf(`unexpected format: "%%%c"`, b) return } if j >= l || source[j] != b { err = expectedFormatError(b) return } j++ } if pending != "" { b, pending = pending[0], pending[1:] goto L } } else if j >= len(source) || source[j] != b { err = expectedFormatError(b) return } else { j++ } } if j < len(source) { err = fmt.Errorf("unconverted string: %q", source[j:]) return } if pm { hour += 12 } 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 } return time.Date(year, time.Month(month), day, hour, min, sec, nsec, loc), nil } func locationZone(loc *time.Location) (name string, offset int) { return time.Date(2000, time.January, 1, 0, 0, 0, 0, loc).Zone() } type parseFormatError byte func (err parseFormatError) Error() string { return fmt.Sprintf("cannot parse %%%c", byte(err)) } type expectedFormatError byte func (err expectedFormatError) Error() string { return fmt.Sprintf("expected %q", byte(err)) } type parseZFormatError int func (err parseZFormatError) Error() string { switch int(err) { case 0: return "cannot parse %z" case 1: return "cannot parse %:z" default: return "cannot parse %::z" } } 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 } i := min for ; i < size; i++ { if b := source[i]; '0' <= b && b <= '9' { val = val*10 + int(b&0x0F) } else { break } } if i == min { return 0, 0, parseFormatError(format) } return val, i, nil } func lookup(source string, min int, candidates []string, format byte) (int, int, error) { L: for i, xs := range candidates { j := min 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') { continue L } } return i + 1, j, nil } return 0, 0, parseFormatError(format) } timefmt-go-0.1.5/parse_test.go000066400000000000000000000440721434211101500162640ustar00rootroot00000000000000package timefmt_test import ( "errors" "fmt" "log" "strings" "testing" "time" "github.com/itchyny/timefmt-go" ) var parseTestCases = []struct { source string format string t time.Time parseErr error }{ { source: "2020", format: "%Y", t: time.Date(2020, time.January, 1, 0, 0, 0, 0, time.UTC), }, { source: "[2020]", format: "[%Y]", t: time.Date(2020, time.January, 1, 0, 0, 0, 0, time.UTC), }, { source: "20", format: "%Y", t: time.Date(20, time.January, 1, 0, 0, 0, 0, time.UTC), }, { source: "2", format: "%Y", t: time.Date(2, time.January, 1, 0, 0, 0, 0, time.UTC), }, { source: "20", format: "%Y", t: time.Date(20, time.January, 1, 0, 0, 0, 0, time.UTC), }, { source: "20xxx", format: "%Y", parseErr: errors.New(`unconverted string: "xxx"`), }, { source: "a", format: "%Y", parseErr: errors.New("cannot parse %Y"), }, { source: "20", format: "%C", t: time.Date(2000, time.January, 1, 0, 0, 0, 0, time.UTC), }, { source: "1758", format: "%C%y", t: time.Date(1758, time.January, 1, 0, 0, 0, 0, time.UTC), }, { source: "xx", format: "%C", parseErr: errors.New("cannot parse %C"), }, { source: "68", format: "%y", t: time.Date(2068, time.January, 1, 0, 0, 0, 0, time.UTC), }, { source: "69", format: "%y", t: time.Date(1969, time.January, 1, 0, 0, 0, 0, time.UTC), }, { source: "xx", format: "%y", parseErr: errors.New("cannot parse %y"), }, { source: "2020-05", format: "%Y-%m", t: time.Date(2020, time.May, 1, 0, 0, 0, 0, time.UTC), }, { source: "2020/1", format: "%Y/%m", t: time.Date(2020, time.January, 1, 0, 0, 0, 0, time.UTC), }, { source: "2020/9", format: "%Y/%m", t: time.Date(2020, time.September, 1, 0, 0, 0, 0, time.UTC), }, { source: "2020/10/09", format: "%Y/%m/%d", t: time.Date(2020, time.October, 9, 0, 0, 0, 0, time.UTC), }, { source: "2020/10/1", format: "%Y/%m/%d", t: time.Date(2020, time.October, 1, 0, 0, 0, 0, time.UTC), }, { source: "2020-12-12", format: "%Y-%m-%d", t: time.Date(2020, time.December, 12, 0, 0, 0, 0, time.UTC), }, { source: "2020-1-1", format: "%Y-%m-%d", t: time.Date(2020, time.January, 1, 0, 0, 0, 0, time.UTC), }, { source: "[2020-1-1]", format: "[%Y-%m-%d]", t: time.Date(2020, time.January, 1, 0, 0, 0, 0, time.UTC), }, { source: "2020-", format: "%Y-%m-%d", parseErr: errors.New("cannot parse %m"), }, { source: "2020-1-", format: "%Y-%m-%d", parseErr: errors.New("cannot parse %d"), }, { source: "201111", format: "%y%m%d", t: time.Date(2020, time.November, 11, 0, 0, 0, 0, time.UTC), }, { source: "9-9-9", format: "%y-%m-%d", t: time.Date(2009, time.September, 9, 0, 0, 0, 0, time.UTC), }, { source: "2020 02 9", format: "%Y %m %e", t: time.Date(2020, time.February, 9, 0, 0, 0, 0, time.UTC), }, { source: "Jan", format: "%b", t: time.Date(1900, time.January, 1, 0, 0, 0, 0, time.UTC), }, { source: "Ja", format: "%b", parseErr: errors.New("cannot parse %b"), }, { source: "Jul", format: "%b", t: time.Date(1900, time.July, 1, 0, 0, 0, 0, time.UTC), }, { source: "Sep", format: "%b", t: time.Date(1900, time.September, 1, 0, 0, 0, 0, time.UTC), }, { source: "September", format: "%B", t: time.Date(1900, time.September, 1, 0, 0, 0, 0, time.UTC), }, { source: "Sep", format: "%B", parseErr: errors.New("cannot parse %B"), }, { source: "Sep", format: "%h", t: time.Date(1900, time.September, 1, 0, 0, 0, 0, time.UTC), }, { source: "100", format: "%j", t: time.Date(1900, time.April, 10, 0, 0, 0, 0, time.UTC), }, { source: ".10", format: "%j", parseErr: errors.New("cannot parse %j"), }, { source: "20203", format: "%Y%j", t: time.Date(2020, time.January, 3, 0, 0, 0, 0, time.UTC), }, { source: "2020366", format: "%Y%j", t: time.Date(2020, time.December, 31, 0, 0, 0, 0, time.UTC), }, { source: "2020-9-33", format: "%Y-%m-%j", t: time.Date(2020, time.February, 2, 0, 0, 0, 0, time.UTC), }, { source: "MAY", format: "%b", t: time.Date(1900, time.May, 1, 0, 0, 0, 0, time.UTC), }, { source: "SATURDAY", format: "%A", t: time.Date(1900, time.January, 1, 0, 0, 0, 0, time.UTC), }, { source: "[sunday]", format: "[%A]", t: time.Date(1900, time.January, 1, 0, 0, 0, 0, time.UTC), }, { source: "[Mon]", format: "[%a]", t: time.Date(1900, time.January, 1, 0, 0, 0, 0, time.UTC), }, { source: "Teu", format: "%a", parseErr: errors.New("cannot parse %a"), }, { source: "mondaymon1", format: "%A%a%w", t: time.Date(1900, time.January, 1, 0, 0, 0, 0, time.UTC), }, { source: "mooday", format: "%A", parseErr: errors.New("cannot parse %A"), }, { source: "0", format: "%w", t: time.Date(1900, time.January, 1, 0, 0, 0, 0, time.UTC), }, { source: "6", format: "%w", t: time.Date(1900, time.January, 1, 0, 0, 0, 0, time.UTC), }, { source: "7", format: "%w", parseErr: errors.New("cannot parse %w"), }, { source: "0", format: "%u", parseErr: errors.New("cannot parse %u"), }, { source: "1", format: "%u", t: time.Date(1900, time.January, 1, 0, 0, 0, 0, time.UTC), }, { source: "7", format: "%u", t: time.Date(1900, time.January, 1, 0, 0, 0, 0, time.UTC), }, { source: "8", format: "%u", parseErr: errors.New("cannot parse %u"), }, { source: "20 01", format: "%g %V", t: time.Date(2020, time.January, 1, 0, 0, 0, 0, time.UTC), }, { source: "xx", format: "%g", parseErr: errors.New("cannot parse %g"), }, { source: "xx", format: "%V", parseErr: errors.New("cannot parse %V"), }, { source: "2009 01 00 00", format: "%G %V %U %W", t: time.Date(2009, time.January, 1, 0, 0, 0, 0, time.UTC), }, { source: "xxxx", format: "%G", parseErr: errors.New("cannot parse %G"), }, { source: "2020-09-08 07:06:05", format: "%Y-%m-%d %H:%M:%S", t: time.Date(2020, time.September, 8, 7, 6, 5, 0, time.UTC), }, { source: "1:2:3.45", format: "%H:%M:%S.%f", t: time.Date(1900, time.January, 1, 1, 2, 3, 450000000, time.UTC), }, { source: "1213145678912", format: "%H%M%S%f%d", t: time.Date(1900, time.January, 2, 12, 13, 14, 567891000, time.UTC), }, { source: "xx", format: "%H", parseErr: errors.New("cannot parse %H"), }, { source: "xx", format: "%M", parseErr: errors.New("cannot parse %M"), }, { source: "xx", format: "%S", parseErr: errors.New("cannot parse %S"), }, { source: "1:2:3.", format: "%H:%M:%S.%f", parseErr: errors.New("cannot parse %f"), }, { source: "12:13:14 AM", format: "%I:%M:%S %p", t: time.Date(1900, time.January, 1, 0, 13, 14, 0, time.UTC), }, { source: "01:14:15PM", format: "%I:%M:%S%p", t: time.Date(1900, time.January, 1, 13, 14, 15, 0, time.UTC), }, { source: "PM 11:14:15", format: "%p %I:%M:%S", t: time.Date(1900, time.January, 1, 23, 14, 15, 0, time.UTC), }, { source: "12:13:14 PM", format: "%I:%M:%S %p", t: time.Date(1900, time.January, 1, 12, 13, 14, 0, time.UTC), }, { source: "xx", format: "%I", parseErr: errors.New("cannot parse %I"), }, { source: "am 9:10:11", format: "%P %k:%M:%S", t: time.Date(1900, time.January, 1, 9, 10, 11, 0, time.UTC), }, { source: " 9:10:11 pm", format: "%l:%M:%S %P", t: time.Date(1900, time.January, 1, 21, 10, 11, 0, time.UTC), }, { source: "1598765432", format: "%s", t: time.Date(2020, time.August, 30, 5, 30, 32, 0, time.UTC), }, { source: ".", format: "%s", parseErr: errors.New("cannot parse %s"), }, { source: "23:14", format: "%R", t: time.Date(1900, time.January, 1, 23, 14, 0, 0, time.UTC), }, { source: "23:", format: "%R", parseErr: errors.New("cannot parse %M"), }, { source: "3:14:15 PM", format: "%r", t: time.Date(1900, time.January, 1, 15, 14, 15, 0, time.UTC), }, { source: "3:1415 PM", format: "%r", parseErr: errors.New("expected ':'"), }, { source: "3:14:15PM", format: "%r", parseErr: errors.New("expected ' '"), }, { source: "2/20/21 23:14:15", format: "%D %T", t: time.Date(2021, time.February, 20, 23, 14, 15, 0, time.UTC), }, { source: "02/09/20 23:14:15", format: "%x %X", t: time.Date(2020, time.February, 9, 23, 14, 15, 0, time.UTC), }, { source: "2020-02-09 \t\n\v\f\r 23:14:15", format: "%F%t%T", t: time.Date(2020, time.February, 9, 23, 14, 15, 0, time.UTC), }, { source: "2020-02-0923:14:15", format: "%F%t%T", parseErr: errors.New("expected a space for %t"), }, { source: " 9-Jul-2020 23:14:15", format: "%v %X", t: time.Date(2020, time.July, 9, 23, 14, 15, 0, time.UTC), }, { source: "Sun Feb 9 23:14:15 2020", format: "%c", t: time.Date(2020, time.February, 9, 23, 14, 15, 0, time.UTC), }, { source: "Sun Feb 9 23:14:15 UTC 2020", format: "%+", t: time.Date(2020, time.February, 9, 23, 14, 15, 0, time.UTC), }, { source: "2020-07-24 23:14:15 +0000", format: "%F %T %z", t: time.Date(2020, time.July, 24, 23, 14, 15, 0, time.FixedZone("", 0)), }, { source: "2020-07-24T23:14:15Z", format: "%FT%T%z", t: time.Date(2020, time.July, 24, 23, 14, 15, 0, time.UTC), }, { source: "2020-07-24 23:14:15 -0800", format: "%F %T %z", t: time.Date(2020, time.July, 24, 23, 14, 15, 0, time.FixedZone("", -8*60*60)), }, { source: "2020-07-24 23:14:15 +0900", format: "%F %T %z", t: time.Date(2020, time.July, 24, 23, 14, 15, 0, time.FixedZone("", 9*60*60)), }, { source: "2020-07-24 23:14:15 +0530", format: "%F %T %z", t: time.Date(2020, time.July, 24, 23, 14, 15, 0, time.FixedZone("", (5*60+30)*60)), }, { source: "2020-07-24 23:14:15 +04:30", format: "%F %T %z", t: time.Date(2020, time.July, 24, 23, 14, 15, 0, time.FixedZone("", (4*60+30)*60)), }, { source: "2020-07-24 23:14:15 +05:43:21", format: "%F %T %z", t: time.Date(2020, time.July, 24, 23, 14, 15, 0, time.FixedZone("", (5*60+43)*60+21)), }, { source: "2020-07-24 23:14:15 +05:43zzz", format: "%F %T %zzzz", t: time.Date(2020, time.July, 24, 23, 14, 15, 0, time.FixedZone("", (5*60+43)*60)), }, { source: "2020-07-24 23:14:15 +05:43:", format: "%F %T %z:", t: time.Date(2020, time.July, 24, 23, 14, 15, 0, time.FixedZone("", (5*60+43)*60)), }, { source: "2020-07-24 23:14:15 +05:43:0", format: "%F %T %z:0", t: time.Date(2020, time.July, 24, 23, 14, 15, 0, time.FixedZone("", (5*60+43)*60)), }, { source: "2020-07-24 23:14:15 +05", format: "%F %T %z", t: time.Date(2020, time.July, 24, 23, 14, 15, 0, time.FixedZone("", 5*60*60)), }, { source: "2020-07-24T23:14:15+05Z", format: "%FT%T%z%z", t: time.Date(2020, time.July, 24, 23, 14, 15, 0, time.UTC), }, { source: "2020-07-24 23:14:15 ", format: "%F %T %z", parseErr: errors.New("cannot parse %z"), }, { source: "2020-07-24 23:14:15 +", format: "%F %T %z", parseErr: errors.New("cannot parse %z"), }, { source: "2020-07-24 23:14:15 +0", format: "%F %T %z", parseErr: errors.New("cannot parse %z"), }, { source: "2020-07-24 23:14:15 +053", format: "%F %T %z", parseErr: errors.New(`unconverted string: "3"`), }, { source: "2020-07-24 23:14:15 +04:3", format: "%F %T %z", parseErr: errors.New("cannot parse %z"), }, { source: "2020-07-24 23:14:15 +04:30:", format: "%F %T %z", parseErr: errors.New(`unconverted string: ":"`), }, { source: "2020-07-24 23:14:15 +04:30:0", format: "%F %T %z", parseErr: errors.New(`unconverted string: ":0"`), }, { source: "2020-07-24 23:14:15 +04:3:00", format: "%F %T %z", parseErr: errors.New("cannot parse %z"), }, { source: "2020-07-24 23:14:15 +0430:10", format: "%F %T %z", parseErr: errors.New(`unconverted string: ":10"`), }, { source: "2020-07-24 23:14:15 +04:3010", format: "%F %T %z", parseErr: errors.New(`unconverted string: "10"`), }, { source: "2020-07-24 23:14:15 +0:30", format: "%F %T %z", parseErr: errors.New("cannot parse %z"), }, { source: "2020-07-24 23:14:15 +003a", format: "%F %T %z", parseErr: errors.New(`unconverted string: "3a"`), }, { source: "2020-07-24 23:14:15 $0000", format: "%F %T %z", parseErr: errors.New("cannot parse %z"), }, { source: "2020-07-24 23:14:15 +05:43:2a", format: "%F %T %z", parseErr: errors.New(`unconverted string: ":2a"`), }, { source: "2020-07-24 23:14:15 +05:30%", format: "%F %T %:z%%", t: time.Date(2020, time.July, 24, 23, 14, 15, 0, time.FixedZone("", (5*60+30)*60)), }, { source: "2020-07-24 23:14:15 Z", format: "%F %T %:z", t: time.Date(2020, time.July, 24, 23, 14, 15, 0, time.UTC), }, { source: "2020-07-24 23:14:15 +05", format: "%F %T %:z", parseErr: errors.New("expected ':' for %:z"), }, { source: "2020-07-24 23:14:15 +05-30", format: "%F %T %:z", parseErr: errors.New("expected ':' for %:z"), }, { source: "2020-07-24 23:14:15 +0530", format: "%F %T %:z", parseErr: errors.New(`expected ':' for %:z`), }, { source: "2020-07-24 23:14:15 *05:30", format: "%F %T %:z", parseErr: errors.New("cannot parse %:z"), }, { source: "2020-07-24 23:14:15 +0x:30", format: "%F %T %:z", parseErr: errors.New("cannot parse %:z"), }, { source: "2020-07-24 23:14:15 +00:3x", format: "%F %T %:z", parseErr: errors.New("cannot parse %:z"), }, { source: "2020-07-24 23:14:15 ", format: "%F %T %:", parseErr: errors.New(`expected 'z' after "%:"`), }, { source: "2020-07-24 23:14:15 ", format: "%F %T %:H", parseErr: errors.New(`expected 'z' after "%:"`), }, { source: "2020-07-24 23:14:15 +05:30:10", format: "%F %T %::z", t: time.Date(2020, time.July, 24, 23, 14, 15, 0, time.FixedZone("", (5*60+30)*60+10)), }, { source: "2020-07-24 23:14:15 -05:30:10", format: "%F %T %::z", t: time.Date(2020, time.July, 24, 23, 14, 15, 0, time.FixedZone("", -((5*60+30)*60+10))), }, { source: "2020-07-24 23:14:15 ::-05:30::", format: "%F %T ::%:z::", t: time.Date(2020, time.July, 24, 23, 14, 15, 0, time.FixedZone("", -(5*60+30)*60)), }, { source: "2020-07-24 23:14:15 -05:30:10 -04:20 +0300", format: "%F %T %::z %:z %z", t: time.Date(2020, time.July, 24, 23, 14, 15, 0, time.FixedZone("", 3*60*60)), }, { source: "2020-07-24 23:14:15 +05", format: "%F %T %::z", parseErr: errors.New("expected ':' for %::z"), }, { source: "2020-07-24 23:14:15 +05:30:0", format: "%F %T %::z", parseErr: errors.New("cannot parse %::z"), }, { source: "2020-07-24 23:14:15 +05:30:0x", format: "%F %T %::z", parseErr: errors.New("cannot parse %::z"), }, { source: "2020-07-24 23:14:15 /05:30:00", format: "%F %T %::z", parseErr: errors.New("cannot parse %::z"), }, { source: "2020-07-24 23:14:15 +05300000", format: "%F %T %::z", parseErr: errors.New("expected ':' for %::z"), }, { source: "2020-07-24 23:14:15 +05-30:00", format: "%F %T %::z", parseErr: errors.New("expected ':' for %::z"), }, { source: "2020-07-24 23:14:15 +05:30-00", format: "%F %T %::z", parseErr: errors.New("expected ':' for %::z"), }, { source: "2020-07-24 23:14:15 +05:30", format: "%F %T %::z", parseErr: errors.New("expected ':' for %::z"), }, { source: "2020-07-24 23:14:15 ", format: "%F %T %::", parseErr: errors.New(`expected 'z' after "%::"`), }, { source: "2020-07-24 23:14:15 ", format: "%F %T %::Z", parseErr: errors.New(`expected 'z' after "%::"`), }, { source: "2020-07-24 23:14:15 UTC", format: "%F %T %Z", t: time.Date(2020, time.July, 24, 23, 14, 15, 0, time.FixedZone("UTC", 0)), }, { source: "X", format: "%Z", parseErr: errors.New(`cannot parse "X" with "%Z"`), }, { source: "2020-07-24 23:14:15 +0530 (AAA)", format: "%F %T %z (%Z)", t: time.Date(2020, time.July, 24, 23, 14, 15, 0, time.FixedZone("AAA", (5*60+30)*60)), }, { source: "2020-07-24 23:14:15 (AAA) +0530", format: "%F %T (%Z) %z", t: time.Date(2020, time.July, 24, 23, 14, 15, 0, time.FixedZone("AAA", (5*60+30)*60)), }, { source: "01%02\t03\n450000", format: "%H%%%M%t%S%n%f", t: time.Date(1900, time.January, 1, 1, 2, 3, 450000000, time.UTC), }, { source: "pp", format: "%p", parseErr: errors.New("cannot parse %p"), }, { format: "%E", parseErr: errors.New(`unexpected format: "%E"`), }, { format: "%", parseErr: errors.New("stray %"), }, { source: "", format: "%%", parseErr: errors.New("expected '%'"), }, { source: "", format: "x", parseErr: errors.New("expected 'x'"), }, } func TestParse(t *testing.T) { for _, tc := range parseTestCases { t.Run(tc.source+"/"+tc.format, func(t *testing.T) { got, err := timefmt.Parse(tc.source, tc.format) if tc.parseErr == nil { if err != nil { t.Fatalf("expected no error but got: %v", err) } if !got.Equal(tc.t) { t.Errorf("expected: %v, got: %v", tc.t, got) } name, offset := tc.t.Zone() gotName, gotOffset := got.Zone() if name != gotName || offset != gotOffset { t.Errorf("expected zone: name = %s, offset = %d, got zone: name = %s, offset = %d", name, offset, gotName, gotOffset, ) } } else { if err == nil { t.Fatalf("expected error %v but got: %v", tc.parseErr, err) } if !strings.Contains(err.Error(), tc.parseErr.Error()) { t.Errorf("expected: %v, got: %v", tc.parseErr, err) } } }) } } func ExampleParse() { str := "2020-07-24 09:07:29" t, err := timefmt.Parse(str, "%Y-%m-%d %H:%M:%S") if err != nil { log.Fatal(err) } fmt.Println(t) // Output: 2020-07-24 09:07:29 +0000 UTC } func ExampleParseInLocation() { loc := time.FixedZone("JST", 9*60*60) str := "2020-07-24 09:07:29" t, err := timefmt.ParseInLocation(str, "%Y-%m-%d %H:%M:%S", loc) if err != nil { log.Fatal(err) } fmt.Println(t) // Output: 2020-07-24 09:07:29 +0900 JST } timefmt-go-0.1.5/timefmt.go000066400000000000000000000001441434211101500155500ustar00rootroot00000000000000// Package timefmt provides functions for formatting and parsing date time strings. package timefmt