├── .github ├── FUNDING.yml ├── dependabot.yml └── workflows │ ├── ci.yml │ └── lint.yml ├── .gitignore ├── .golangci.yml ├── Changes ├── LICENSE ├── Makefile ├── README.md ├── appenders.go ├── bench ├── bench_test.go ├── go.mod └── go.sum ├── extension.go ├── go.mod ├── go.sum ├── internal_test.go ├── options.go ├── specifications.go ├── strftime.go └── strftime_test.go /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: ["lestrrat"] 4 | patreon: # Replace with a single Patreon username 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: # Replace with a single Ko-fi username 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | otechie: # Replace with a single Otechie username 12 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] 13 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://help.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 5 | 6 | version: 2 7 | updates: 8 | - package-ecosystem: "gomod" # See documentation for possible values 9 | directory: "/" # Location of package manifests 10 | schedule: 11 | interval: "daily" 12 | target-branch: "master" 13 | - package-ecosystem: "github-actions" 14 | directory: "/" 15 | schedule: 16 | interval: "daily" 17 | target-branch: "master" 18 | 19 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: [push, pull_request] 3 | 4 | jobs: 5 | build: 6 | runs-on: ubuntu-latest 7 | strategy: 8 | matrix: 9 | go: [ '1.23', '1.22', '1.21' ] 10 | tags: [ 'strftime_native_errors', '' ] 11 | fail-fast: false 12 | name: "Go ${{ matrix.go }} test (tags: ${{ matrix.tags }})" 13 | steps: 14 | - name: Checkout repository 15 | uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1 16 | - name: Install Go stable version 17 | uses: actions/setup-go@0a12ed9d6a96ab950c8f026ed9f722fe0da7ef32 # v5.0.2 18 | with: 19 | go-version: ${{ matrix.go }} 20 | - name: Test with coverage 21 | run: make STRFTIME_TAGS=${{ matrix.tags }} cover 22 | - name: Upload code coverage to codecov 23 | if: matrix.go == '1.23' 24 | uses: codecov/codecov-action@b9fd7d16f6d7d1b5d2bec1a2887e65ceed900238 # v4.6.0 25 | with: 26 | file: ./coverage.out 27 | -------------------------------------------------------------------------------- /.github/workflows/lint.yml: -------------------------------------------------------------------------------- 1 | name: lint 2 | on: [push] 3 | jobs: 4 | golangci: 5 | name: lint 6 | runs-on: ubuntu-latest 7 | steps: 8 | - uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1 9 | - uses: actions/setup-go@0a12ed9d6a96ab950c8f026ed9f722fe0da7ef32 # v5.0.2 10 | with: 11 | go-version: 1.21 12 | check-latest: true 13 | - uses: golangci/golangci-lint-action@971e284b6050e8a5849b72094c50ab08da042db8 # v6.1.1 14 | with: 15 | version: v1.60.1 16 | - name: Run go vet 17 | run: | 18 | go vet ./... 19 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled Object files, Static and Dynamic libs (Shared Objects) 2 | *.o 3 | *.a 4 | *.so 5 | 6 | # Folders 7 | _obj 8 | _test 9 | 10 | # Architecture specific extensions/prefixes 11 | *.[568vq] 12 | [568vq].out 13 | 14 | *.cgo1.go 15 | *.cgo2.c 16 | _cgo_defun.c 17 | _cgo_gotypes.go 18 | _cgo_export.* 19 | 20 | _testmain.go 21 | 22 | *.exe 23 | *.test 24 | *.prof 25 | -------------------------------------------------------------------------------- /.golangci.yml: -------------------------------------------------------------------------------- 1 | issues: 2 | exclude-rules: 3 | - path: _test\.go 4 | linters: 5 | - errcheck 6 | -------------------------------------------------------------------------------- /Changes: -------------------------------------------------------------------------------- 1 | Changes 2 | ======= 3 | 4 | v1.1.0 - 28 Aug 2024 5 | [Miscellaneous] 6 | * github.com/pkg/errors has been removed (it has been two years :) 7 | * Updated build/test actions 8 | * Updated minimum required go version to go 1.21 9 | * Fix week number handling 10 | 11 | v1.0.6 - 20 Apr 2022 12 | [Miscellaneous] 13 | * Minimum go version is now go 1.13 14 | * github.com/pkg/errors is going to be phased out in steps. In this release, 15 | users may opt-in to using native errors using `fmt.Errorf("%w")` by 16 | specifying the tag `strftime_native_errors`. In the next release, the default 17 | will be to use native errors, but users will be able to opt-in to using 18 | github.com/pkg/errors using a tag. The version after will remove github.com/pkg/errors. 19 | 20 | This is something that we normally would do over a major version upgrade 21 | but since we do not expect this library to receive API breaking changes in the 22 | near future and thus no v2 is expected, we have decided to do this over few 23 | non-major releases. 24 | 25 | v1.0.5 26 | [New features] 27 | * `(strftime.Strftime).FormatBuffer([]byte, time.Time) []byte` has been added. 28 | This allows the user to provide the same underlying `[]byte` buffer for each 29 | call to `FormatBuffer`, which avoid allocation per call. 30 | * `%I` formatted midnight as `00`, where it should have been using `01` 31 | 32 | 33 | before v1.0.4 34 | 35 | Apparently we have failed to provide Changes prior to v1.0.5 :( 36 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016 lestrrat 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 | .PHONY: bench realclean cover viewcover test lint 2 | 3 | bench: 4 | go test -tags bench -benchmem -bench . 5 | @git checkout go.mod 6 | @rm go.sum 7 | 8 | realclean: 9 | rm coverage.out 10 | 11 | test: 12 | go test -v -race ./... 13 | 14 | cover: 15 | ifeq ($(strip $(STRFTIME_TAGS)),) 16 | go test -v -race -coverpkg=./... -coverprofile=coverage.out ./... 17 | else 18 | go test -v -tags $(STRFTIME_TAGS) -race -coverpkg=./... -coverprofile=coverage.out ./... 19 | endif 20 | 21 | viewcover: 22 | go tool cover -html=coverage.out 23 | 24 | lint: 25 | golangci-lint run ./... 26 | 27 | imports: 28 | goimports -w ./ 29 | 30 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # strftime 2 | 3 | Fast strftime for Go 4 | 5 | [![](https://github.com/lestrrat-go/strftime/workflows/CI/badge.svg?branch=master)](https://github.com/lestrrat-go/strftime/actions?query=branch%3Amaster) [![Go Reference](https://pkg.go.dev/badge/github.com/lestrrat-go/strftime.svg)](https://pkg.go.dev/github.com/lestrrat-go/strftime) 6 | 7 | # SYNOPSIS 8 | 9 | ```go 10 | f, err := strftime.New(`.... pattern ...`) 11 | if err := f.Format(buf, time.Now()); err != nil { 12 | log.Println(err.Error()) 13 | } 14 | ``` 15 | 16 | # DESCRIPTION 17 | 18 | The goals for this library are 19 | 20 | * Optimized for the same pattern being called repeatedly 21 | * Be flexible about destination to write the results out 22 | * Be as complete as possible in terms of conversion specifications 23 | 24 | # API 25 | 26 | ## Format(string, time.Time) (string, error) 27 | 28 | Takes the pattern and the time, and formats it. This function is a utility function that recompiles the pattern every time the function is called. If you know beforehand that you will be formatting the same pattern multiple times, consider using `New` to create a `Strftime` object and reuse it. 29 | 30 | ## New(string) (\*Strftime, error) 31 | 32 | Takes the pattern and creates a new `Strftime` object. 33 | 34 | ## obj.Pattern() string 35 | 36 | Returns the pattern string used to create this `Strftime` object 37 | 38 | ## obj.Format(io.Writer, time.Time) error 39 | 40 | Formats the time according to the pre-compiled pattern, and writes the result to the specified `io.Writer` 41 | 42 | ## obj.FormatString(time.Time) string 43 | 44 | Formats the time according to the pre-compiled pattern, and returns the result string. 45 | 46 | # SUPPORTED CONVERSION SPECIFICATIONS 47 | 48 | | pattern | description | 49 | |:--------|:------------| 50 | | %A | national representation of the full weekday name | 51 | | %a | national representation of the abbreviated weekday | 52 | | %B | national representation of the full month name | 53 | | %b | national representation of the abbreviated month name | 54 | | %C | (year / 100) as decimal number; single digits are preceded by a zero | 55 | | %c | national representation of time and date | 56 | | %D | equivalent to %m/%d/%y | 57 | | %d | day of the month as a decimal number (01-31) | 58 | | %e | the day of the month as a decimal number (1-31); single digits are preceded by a blank | 59 | | %F | equivalent to %Y-%m-%d | 60 | | %H | the hour (24-hour clock) as a decimal number (00-23) | 61 | | %h | same as %b | 62 | | %I | the hour (12-hour clock) as a decimal number (01-12) | 63 | | %j | the day of the year as a decimal number (001-366) | 64 | | %k | the hour (24-hour clock) as a decimal number (0-23); single digits are preceded by a blank | 65 | | %l | the hour (12-hour clock) as a decimal number (1-12); single digits are preceded by a blank | 66 | | %M | the minute as a decimal number (00-59) | 67 | | %m | the month as a decimal number (01-12) | 68 | | %n | a newline | 69 | | %p | national representation of either "ante meridiem" (a.m.) or "post meridiem" (p.m.) as appropriate. | 70 | | %R | equivalent to %H:%M | 71 | | %r | equivalent to %I:%M:%S %p | 72 | | %S | the second as a decimal number (00-60) | 73 | | %T | equivalent to %H:%M:%S | 74 | | %t | a tab | 75 | | %U | the week number of the year (Sunday as the first day of the week) as a decimal number (00-53) | 76 | | %u | the weekday (Monday as the first day of the week) as a decimal number (1-7) | 77 | | %V | the week number of the year (Monday as the first day of the week) as a decimal number (01-53) | 78 | | %v | equivalent to %e-%b-%Y | 79 | | %W | the week number of the year (Monday as the first day of the week) as a decimal number (00-53) | 80 | | %w | the weekday (Sunday as the first day of the week) as a decimal number (0-6) | 81 | | %X | national representation of the time | 82 | | %x | national representation of the date | 83 | | %Y | the year with century as a decimal number | 84 | | %y | the year without century as a decimal number (00-99) | 85 | | %Z | the time zone name | 86 | | %z | the time zone offset from UTC | 87 | | %% | a '%' | 88 | 89 | # EXTENSIONS / CUSTOM SPECIFICATIONS 90 | 91 | This library in general tries to be POSIX compliant, but sometimes you just need that 92 | extra specification or two that is relatively widely used but is not included in the 93 | POSIX specification. 94 | 95 | For example, POSIX does not specify how to print out milliseconds, 96 | but popular implementations allow `%f` or `%L` to achieve this. 97 | 98 | For those instances, `strftime.Strftime` can be configured to use a custom set of 99 | specifications: 100 | 101 | ``` 102 | ss := strftime.NewSpecificationSet() 103 | ss.Set('L', ...) // provide implementation for `%L` 104 | 105 | // pass this new specification set to the strftime instance 106 | p, err := strftime.New(`%L`, strftime.WithSpecificationSet(ss)) 107 | p.Format(..., time.Now()) 108 | ``` 109 | 110 | The implementation must implement the `Appender` interface, which is 111 | 112 | ``` 113 | type Appender interface { 114 | Append([]byte, time.Time) []byte 115 | } 116 | ``` 117 | 118 | For commonly used extensions such as the millisecond example and Unix timestamp, we provide a default 119 | implementation so the user can do one of the following: 120 | 121 | ``` 122 | // (1) Pass a specification byte and the Appender 123 | // This allows you to pass arbitrary Appenders 124 | p, err := strftime.New( 125 | `%L`, 126 | strftime.WithSpecification('L', strftime.Milliseconds), 127 | ) 128 | 129 | // (2) Pass an option that knows to use strftime.Milliseconds 130 | p, err := strftime.New( 131 | `%L`, 132 | strftime.WithMilliseconds('L'), 133 | ) 134 | ``` 135 | 136 | Similarly for Unix Timestamp: 137 | ``` 138 | // (1) Pass a specification byte and the Appender 139 | // This allows you to pass arbitrary Appenders 140 | p, err := strftime.New( 141 | `%s`, 142 | strftime.WithSpecification('s', strftime.UnixSeconds), 143 | ) 144 | 145 | // (2) Pass an option that knows to use strftime.UnixSeconds 146 | p, err := strftime.New( 147 | `%s`, 148 | strftime.WithUnixSeconds('s'), 149 | ) 150 | ``` 151 | 152 | If a common specification is missing, please feel free to submit a PR 153 | (but please be sure to be able to defend how "common" it is) 154 | 155 | ## List of available extensions 156 | 157 | - [`Milliseconds`](https://pkg.go.dev/github.com/lestrrat-go/strftime?tab=doc#Milliseconds) (related option: [`WithMilliseconds`](https://pkg.go.dev/github.com/lestrrat-go/strftime?tab=doc#WithMilliseconds)); 158 | 159 | - [`Microseconds`](https://pkg.go.dev/github.com/lestrrat-go/strftime?tab=doc#Microseconds) (related option: [`WithMicroseconds`](https://pkg.go.dev/github.com/lestrrat-go/strftime?tab=doc#WithMicroseconds)); 160 | 161 | - [`UnixSeconds`](https://pkg.go.dev/github.com/lestrrat-go/strftime?tab=doc#UnixSeconds) (related option: [`WithUnixSeconds`](https://pkg.go.dev/github.com/lestrrat-go/strftime?tab=doc#WithUnixSeconds)). 162 | 163 | 164 | # PERFORMANCE / OTHER LIBRARIES 165 | 166 | The following benchmarks were run separately because some libraries were using cgo on specific platforms (notabley, the fastly version) 167 | 168 | ``` 169 | // On my OS X 10.14.6, 2.3 GHz Intel Core i5, 16GB memory. 170 | // go version go1.13.4 darwin/amd64 171 | hummingbird% go test -tags bench -benchmem -bench . 172 | 173 | BenchmarkTebeka-4 297471 3905 ns/op 257 B/op 20 allocs/op 174 | BenchmarkJehiah-4 818444 1773 ns/op 256 B/op 17 allocs/op 175 | BenchmarkFastly-4 2330794 550 ns/op 80 B/op 5 allocs/op 176 | BenchmarkLestrrat-4 916365 1458 ns/op 80 B/op 2 allocs/op 177 | BenchmarkLestrratCachedString-4 2527428 546 ns/op 128 B/op 2 allocs/op 178 | BenchmarkLestrratCachedWriter-4 537422 2155 ns/op 192 B/op 3 allocs/op 179 | PASS 180 | ok github.com/lestrrat-go/strftime 25.618s 181 | ``` 182 | 183 | ``` 184 | // On a host on Google Cloud Platform, machine-type: f1-micro (vCPU x 1, memory: 0.6GB) 185 | // (Yes, I was being skimpy) 186 | // Linux 4.9.0-11-amd64 #1 SMP Debian 4.9.189-3+deb9u1 (2019-09-20) x86_64 GNU/Linux 187 | // go version go1.13.4 linux/amd64 188 | hummingbird% go test -tags bench -benchmem -bench . 189 | 190 | BenchmarkTebeka 254997 4726 ns/op 256 B/op 20 allocs/op 191 | BenchmarkJehiah 659289 1882 ns/op 256 B/op 17 allocs/op 192 | BenchmarkFastly 389150 3044 ns/op 224 B/op 13 allocs/op 193 | BenchmarkLestrrat 699069 1780 ns/op 80 B/op 2 allocs/op 194 | BenchmarkLestrratCachedString 2081594 589 ns/op 128 B/op 2 allocs/op 195 | BenchmarkLestrratCachedWriter 825763 1480 ns/op 192 B/op 3 allocs/op 196 | PASS 197 | ok github.com/lestrrat-go/strftime 11.355s 198 | ``` 199 | 200 | This library is much faster than other libraries *IF* you can reuse the format pattern. 201 | 202 | Here's the annotated list from the benchmark results. You can clearly see that (re)using a `Strftime` object 203 | and producing a string is the fastest. Writing to an `io.Writer` seems a bit sluggish, but since 204 | the one producing the string is doing almost exactly the same thing, we believe this is purely the overhead of 205 | writing to an `io.Writer` 206 | 207 | | Import Path | Score | Note | 208 | |:------------------------------------|--------:|:--------------------------------| 209 | | github.com/lestrrat-go/strftime | 3000000 | Using `FormatString()` (cached) | 210 | | github.com/fastly/go-utils/strftime | 2000000 | Pure go version on OS X | 211 | | github.com/lestrrat-go/strftime | 1000000 | Using `Format()` (NOT cached) | 212 | | github.com/jehiah/go-strftime | 1000000 | | 213 | | github.com/fastly/go-utils/strftime | 1000000 | cgo version on Linux | 214 | | github.com/lestrrat-go/strftime | 500000 | Using `Format()` (cached) | 215 | | github.com/tebeka/strftime | 300000 | | 216 | 217 | However, depending on your pattern, this speed may vary. If you find a particular pattern that seems sluggish, 218 | please send in patches or tests. 219 | 220 | Please also note that this benchmark only uses the subset of conversion specifications that are supported by *ALL* of the libraries compared. 221 | 222 | Somethings to consider when making performance comparisons in the future: 223 | 224 | * Can it write to io.Writer? 225 | * Which `%specification` does it handle? 226 | -------------------------------------------------------------------------------- /appenders.go: -------------------------------------------------------------------------------- 1 | package strftime 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "io" 7 | "strconv" 8 | "strings" 9 | "time" 10 | ) 11 | 12 | // These are all of the standard, POSIX compliant specifications. 13 | // Extensions should be in extensions.go 14 | var ( 15 | fullWeekDayName = StdlibFormat("Monday") 16 | abbrvWeekDayName = StdlibFormat("Mon") 17 | fullMonthName = StdlibFormat("January") 18 | abbrvMonthName = StdlibFormat("Jan") 19 | centuryDecimal = AppendFunc(appendCentury) 20 | timeAndDate = StdlibFormat("Mon Jan _2 15:04:05 2006") 21 | mdy = StdlibFormat("01/02/06") 22 | dayOfMonthZeroPad = StdlibFormat("02") 23 | dayOfMonthSpacePad = StdlibFormat("_2") 24 | ymd = StdlibFormat("2006-01-02") 25 | twentyFourHourClockZeroPad = &hourPadded{twelveHour: false, pad: '0'} 26 | twelveHourClockZeroPad = &hourPadded{twelveHour: true, pad: '0'} 27 | dayOfYear = AppendFunc(appendDayOfYear) 28 | twentyFourHourClockSpacePad = &hourPadded{twelveHour: false, pad: ' '} 29 | twelveHourClockSpacePad = &hourPadded{twelveHour: true, pad: ' '} 30 | minutesZeroPad = StdlibFormat("04") 31 | monthNumberZeroPad = StdlibFormat("01") 32 | newline = Verbatim("\n") 33 | ampm = StdlibFormat("PM") 34 | hm = StdlibFormat("15:04") 35 | imsp = hmsWAMPM{} 36 | secondsNumberZeroPad = StdlibFormat("05") 37 | hms = StdlibFormat("15:04:05") 38 | tab = Verbatim("\t") 39 | weekNumberSundayOrigin = weeknumberOffset(true) // week number of the year, Sunday first 40 | weekdayMondayOrigin = weekday(1) 41 | // monday as the first day, and 01 as the first value 42 | weekNumberMondayOriginOneOrigin = AppendFunc(appendWeekNumber) 43 | eby = StdlibFormat("_2-Jan-2006") 44 | // monday as the first day, and 00 as the first value 45 | weekNumberMondayOrigin = weeknumberOffset(false) // week number of the year, Monday first 46 | weekdaySundayOrigin = weekday(0) 47 | natReprTime = StdlibFormat("15:04:05") // national representation of the time XXX is this correct? 48 | natReprDate = StdlibFormat("01/02/06") // national representation of the date XXX is this correct? 49 | year = StdlibFormat("2006") // year with century 50 | yearNoCentury = StdlibFormat("06") // year w/o century 51 | timezone = StdlibFormat("MST") // time zone name 52 | timezoneOffset = StdlibFormat("-0700") // time zone ofset from UTC 53 | percent = Verbatim("%") 54 | ) 55 | 56 | // Appender is the interface that must be fulfilled by components that 57 | // implement the translation of specifications to actual time value. 58 | // 59 | // The Append method takes the accumulated byte buffer, and the time to 60 | // use to generate the textual representation. The resulting byte 61 | // sequence must be returned by this method, normally by using the 62 | // append() builtin function. 63 | type Appender interface { 64 | Append([]byte, time.Time) []byte 65 | } 66 | 67 | // AppendFunc is an utility type to allow users to create a 68 | // function-only version of an Appender 69 | type AppendFunc func([]byte, time.Time) []byte 70 | 71 | func (af AppendFunc) Append(b []byte, t time.Time) []byte { 72 | return af(b, t) 73 | } 74 | 75 | type appenderList []Appender 76 | 77 | type dumper interface { 78 | dump(io.Writer) 79 | } 80 | 81 | func (l appenderList) dump(out io.Writer) { 82 | var buf bytes.Buffer 83 | ll := len(l) 84 | for i, a := range l { 85 | if dumper, ok := a.(dumper); ok { 86 | dumper.dump(&buf) 87 | } else { 88 | fmt.Fprintf(&buf, "%#v", a) 89 | } 90 | 91 | if i < ll-1 { 92 | fmt.Fprintf(&buf, ",\n") 93 | } 94 | } 95 | if _, err := buf.WriteTo(out); err != nil { 96 | panic(err) 97 | } 98 | } 99 | 100 | // does the time.Format thing 101 | type stdlibFormat struct { 102 | s string 103 | } 104 | 105 | // StdlibFormat returns an Appender that simply goes through `time.Format()` 106 | // For example, if you know you want to display the abbreviated month name for %b, 107 | // you can create a StdlibFormat with the pattern `Jan` and register that 108 | // for specification `b`: 109 | // 110 | // a := StdlibFormat(`Jan`) 111 | // ss := NewSpecificationSet() 112 | // ss.Set('b', a) // does %b -> abbreviated month name 113 | func StdlibFormat(s string) Appender { 114 | return &stdlibFormat{s: s} 115 | } 116 | 117 | func (v stdlibFormat) Append(b []byte, t time.Time) []byte { 118 | return t.AppendFormat(b, v.s) 119 | } 120 | 121 | func (v stdlibFormat) str() string { 122 | return v.s 123 | } 124 | 125 | func (v stdlibFormat) canCombine() bool { 126 | return true 127 | } 128 | 129 | func (v stdlibFormat) combine(w combiner) Appender { 130 | return StdlibFormat(v.s + w.str()) 131 | } 132 | 133 | func (v stdlibFormat) dump(out io.Writer) { 134 | fmt.Fprintf(out, "stdlib: %s", v.s) 135 | } 136 | 137 | type verbatimw struct { 138 | s string 139 | } 140 | 141 | // Verbatim returns an Appender suitable for generating static text. 142 | // For static text, this method is slightly favorable than creating 143 | // your own appender, as adjacent verbatim blocks will be combined 144 | // at compile time to produce more efficient Appenders 145 | func Verbatim(s string) Appender { 146 | return &verbatimw{s: s} 147 | } 148 | 149 | func (v verbatimw) Append(b []byte, _ time.Time) []byte { 150 | return append(b, v.s...) 151 | } 152 | 153 | func (v verbatimw) canCombine() bool { 154 | return canCombine(v.s) 155 | } 156 | 157 | func (v verbatimw) combine(w combiner) Appender { 158 | if _, ok := w.(*stdlibFormat); ok { 159 | return StdlibFormat(v.s + w.str()) 160 | } 161 | return Verbatim(v.s + w.str()) 162 | } 163 | 164 | func (v verbatimw) str() string { 165 | return v.s 166 | } 167 | 168 | func (v verbatimw) dump(out io.Writer) { 169 | fmt.Fprintf(out, "verbatim: %s", v.s) 170 | } 171 | 172 | // These words below, as well as any decimal character 173 | var combineExclusion = []string{ 174 | "Mon", 175 | "Monday", 176 | "Jan", 177 | "January", 178 | "MST", 179 | "PM", 180 | "pm", 181 | } 182 | 183 | func canCombine(s string) bool { 184 | if strings.ContainsAny(s, "0123456789") { 185 | return false 186 | } 187 | for _, word := range combineExclusion { 188 | if strings.Contains(s, word) { 189 | return false 190 | } 191 | } 192 | return true 193 | } 194 | 195 | type combiner interface { 196 | canCombine() bool 197 | combine(combiner) Appender 198 | str() string 199 | } 200 | 201 | // this is container for the compiler to keep track of appenders, 202 | // and combine them as we parse and compile the pattern 203 | type combiningAppend struct { 204 | list appenderList 205 | prev Appender 206 | prevCanCombine bool 207 | } 208 | 209 | func (ca *combiningAppend) Append(w Appender) { 210 | if ca.prevCanCombine { 211 | if wc, ok := w.(combiner); ok && wc.canCombine() { 212 | ca.prev = ca.prev.(combiner).combine(wc) 213 | ca.list[len(ca.list)-1] = ca.prev 214 | return 215 | } 216 | } 217 | 218 | ca.list = append(ca.list, w) 219 | ca.prev = w 220 | ca.prevCanCombine = false 221 | if comb, ok := w.(combiner); ok { 222 | if comb.canCombine() { 223 | ca.prevCanCombine = true 224 | } 225 | } 226 | } 227 | 228 | func appendCentury(b []byte, t time.Time) []byte { 229 | n := t.Year() / 100 230 | if n < 10 { 231 | b = append(b, '0') 232 | } 233 | return append(b, strconv.Itoa(n)...) 234 | } 235 | 236 | type weekday int 237 | 238 | func (v weekday) Append(b []byte, t time.Time) []byte { 239 | n := int(t.Weekday()) 240 | if n < int(v) { 241 | n += 7 242 | } 243 | return append(b, byte(n+48)) 244 | } 245 | 246 | type weeknumberOffset bool 247 | 248 | func (v weeknumberOffset) Append(b []byte, t time.Time) []byte { 249 | offset := int(t.Weekday()) 250 | if v { 251 | offset = 6 - offset 252 | } else if offset != 0 { 253 | offset = 7 - offset 254 | } 255 | n := (t.YearDay() + offset) / 7 256 | if n < 10 { 257 | b = append(b, '0') 258 | } 259 | return append(b, strconv.Itoa(n)...) 260 | } 261 | 262 | func appendWeekNumber(b []byte, t time.Time) []byte { 263 | _, n := t.ISOWeek() 264 | if n < 10 { 265 | b = append(b, '0') 266 | } 267 | return append(b, strconv.Itoa(n)...) 268 | } 269 | 270 | func appendDayOfYear(b []byte, t time.Time) []byte { 271 | n := t.YearDay() 272 | if n < 10 { 273 | b = append(b, '0', '0') 274 | } else if n < 100 { 275 | b = append(b, '0') 276 | } 277 | return append(b, strconv.Itoa(n)...) 278 | } 279 | 280 | type hourPadded struct { 281 | pad byte 282 | twelveHour bool 283 | } 284 | 285 | func (v hourPadded) Append(b []byte, t time.Time) []byte { 286 | h := t.Hour() 287 | if v.twelveHour && h > 12 { 288 | h = h - 12 289 | } 290 | if v.twelveHour && h == 0 { 291 | h = 12 292 | } 293 | 294 | if h < 10 { 295 | b = append(b, v.pad) 296 | b = append(b, byte(h+48)) 297 | } else { 298 | b = unrollTwoDigits(b, h) 299 | } 300 | return b 301 | } 302 | 303 | func unrollTwoDigits(b []byte, v int) []byte { 304 | b = append(b, byte((v/10)+48)) 305 | b = append(b, byte((v%10)+48)) 306 | return b 307 | } 308 | 309 | type hmsWAMPM struct{} 310 | 311 | func (v hmsWAMPM) Append(b []byte, t time.Time) []byte { 312 | h := t.Hour() 313 | var am bool 314 | 315 | if h == 0 { 316 | b = append(b, '1') 317 | b = append(b, '2') 318 | am = true 319 | } else { 320 | switch { 321 | case h == 12: 322 | // no op 323 | case h > 12: 324 | h = h - 12 325 | default: 326 | am = true 327 | } 328 | b = unrollTwoDigits(b, h) 329 | } 330 | b = append(b, ':') 331 | b = unrollTwoDigits(b, t.Minute()) 332 | b = append(b, ':') 333 | b = unrollTwoDigits(b, t.Second()) 334 | 335 | b = append(b, ' ') 336 | if am { 337 | b = append(b, 'A') 338 | } else { 339 | b = append(b, 'P') 340 | } 341 | b = append(b, 'M') 342 | 343 | return b 344 | } 345 | -------------------------------------------------------------------------------- /bench/bench_test.go: -------------------------------------------------------------------------------- 1 | package bench_test 2 | 3 | import ( 4 | "bytes" 5 | "log" 6 | "net/http" 7 | _ "net/http/pprof" 8 | "testing" 9 | "time" 10 | 11 | fastly "github.com/fastly/go-utils/strftime" 12 | jehiah "github.com/jehiah/go-strftime" 13 | lestrrat "github.com/lestrrat-go/strftime" 14 | ncruces "github.com/ncruces/go-strftime" 15 | tebeka "github.com/tebeka/strftime" 16 | ) 17 | 18 | func init() { 19 | go func() { 20 | log.Println(http.ListenAndServe("localhost:8080", nil)) 21 | }() 22 | } 23 | 24 | const benchfmt = `%A %a %B %b %d %H %I %M %m %p %S %Y %y %Z` 25 | 26 | func BenchmarkTebeka(b *testing.B) { 27 | var t time.Time 28 | for i := 0; i < b.N; i++ { 29 | tebeka.Format(benchfmt, t) 30 | } 31 | } 32 | 33 | func BenchmarkJehiah(b *testing.B) { 34 | // Grr, uses byte slices, and does it faster, but with more allocs 35 | var t time.Time 36 | for i := 0; i < b.N; i++ { 37 | jehiah.Format(benchfmt, t) 38 | } 39 | } 40 | 41 | func BenchmarkFastly(b *testing.B) { 42 | var t time.Time 43 | for i := 0; i < b.N; i++ { 44 | fastly.Strftime(benchfmt, t) 45 | } 46 | } 47 | 48 | func BenchmarkNcruces(b *testing.B) { 49 | var t time.Time 50 | for i := 0; i < b.N; i++ { 51 | ncruces.Format(benchfmt, t) 52 | } 53 | } 54 | 55 | func BenchmarkNcrucesAppend(b *testing.B) { 56 | var d []byte 57 | var t time.Time 58 | for i := 0; i < b.N; i++ { 59 | d = ncruces.AppendFormat(d[:0], benchfmt, t) 60 | } 61 | } 62 | 63 | func BenchmarkLestrrat(b *testing.B) { 64 | // This is expected to be rather slow, as it compiles the format string 65 | // every time it's called. 66 | var t time.Time 67 | for i := 0; i < b.N; i++ { 68 | lestrrat.Format(benchfmt, t) 69 | } 70 | } 71 | 72 | func BenchmarkLestrratCachedString(b *testing.B) { 73 | var t time.Time 74 | f, _ := lestrrat.New(benchfmt) 75 | // This benchmark does not take into effect the compilation time 76 | for i := 0; i < b.N; i++ { 77 | f.FormatString(t) 78 | } 79 | } 80 | 81 | func BenchmarkLestrratCachedWriter(b *testing.B) { 82 | var t time.Time 83 | f, _ := lestrrat.New(benchfmt) 84 | var buf bytes.Buffer 85 | b.ResetTimer() 86 | 87 | // This benchmark does not take into effect the compilation time 88 | for i := 0; i < b.N; i++ { 89 | buf.Reset() 90 | f.Format(&buf, t) 91 | } 92 | } 93 | 94 | func BenchmarkLestrratCachedFormatBuffer(b *testing.B) { 95 | var t time.Time 96 | f, _ := lestrrat.New(benchfmt) 97 | b.ResetTimer() 98 | 99 | var buf []byte 100 | for i := 0; i < b.N; i++ { 101 | buf = f.FormatBuffer(buf[:0], t) 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /bench/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/lestrrat-go/strftime/bench 2 | 3 | go 1.21 4 | 5 | require ( 6 | github.com/fastly/go-utils v0.0.0-20180712184237-d95a45783239 7 | github.com/jehiah/go-strftime v0.0.0-20171201141054-1d33003b3869 8 | github.com/lestrrat-go/strftime v1.1.0 9 | github.com/ncruces/go-strftime v0.1.9 10 | github.com/tebeka/strftime v0.1.5 11 | ) 12 | -------------------------------------------------------------------------------- /bench/go.sum: -------------------------------------------------------------------------------- 1 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 2 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 3 | github.com/fastly/go-utils v0.0.0-20180712184237-d95a45783239 h1:Ghm4eQYC0nEPnSJdVkTrXpu9KtoVCSo1hg7mtI7G9KU= 4 | github.com/fastly/go-utils v0.0.0-20180712184237-d95a45783239/go.mod h1:Gdwt2ce0yfBxPvZrHkprdPPTTS3N5rwmLE8T22KBXlw= 5 | github.com/jehiah/go-strftime v0.0.0-20171201141054-1d33003b3869 h1:IPJ3dvxmJ4uczJe5YQdrYB16oTJlGSC/OyZDqUk9xX4= 6 | github.com/jehiah/go-strftime v0.0.0-20171201141054-1d33003b3869/go.mod h1:cJ6Cj7dQo+O6GJNiMx+Pa94qKj+TG8ONdKHgMNIyyag= 7 | github.com/lestrrat-go/envload v0.0.0-20180220234015-a3eb8ddeffcc h1:RKf14vYWi2ttpEmkA4aQ3j4u9dStX2t4M8UM6qqNsG8= 8 | github.com/lestrrat-go/envload v0.0.0-20180220234015-a3eb8ddeffcc/go.mod h1:kopuH9ugFRkIXf3YoqHKyrJ9YfUFsckUU9S7B+XP+is= 9 | github.com/lestrrat-go/strftime v1.1.0 h1:gMESpZy44/4pXLO/m+sL0yBd1W6LjgjrrD4a68Gapyg= 10 | github.com/lestrrat-go/strftime v1.1.0/go.mod h1:uzeIB52CeUJenCo1syghlugshMysrqUT51HlxphXVeI= 11 | github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4= 12 | github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls= 13 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 14 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 15 | github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= 16 | github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= 17 | github.com/tebeka/strftime v0.1.5 h1:1NQKN1NiQgkqd/2moD6ySP/5CoZQsKa1d3ZhJ44Jpmg= 18 | github.com/tebeka/strftime v0.1.5/go.mod h1:29/OidkoWHdEKZqzyDLUyC+LmgDgdHo4WAFCDT7D/Ig= 19 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 20 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 21 | -------------------------------------------------------------------------------- /extension.go: -------------------------------------------------------------------------------- 1 | package strftime 2 | 3 | import ( 4 | "strconv" 5 | "time" 6 | ) 7 | 8 | // NOTE: declare private variable and iniitalize once in init(), 9 | // and leave the Milliseconds() function as returning static content. 10 | // This way, `go doc -all` does not show the contents of the 11 | // milliseconds function 12 | var milliseconds Appender 13 | var microseconds Appender 14 | var unixseconds Appender 15 | 16 | func init() { 17 | milliseconds = AppendFunc(func(b []byte, t time.Time) []byte { 18 | millisecond := int(t.Nanosecond()) / int(time.Millisecond) 19 | if millisecond < 100 { 20 | b = append(b, '0') 21 | } 22 | if millisecond < 10 { 23 | b = append(b, '0') 24 | } 25 | return append(b, strconv.Itoa(millisecond)...) 26 | }) 27 | microseconds = AppendFunc(func(b []byte, t time.Time) []byte { 28 | microsecond := int(t.Nanosecond()) / int(time.Microsecond) 29 | if microsecond < 100000 { 30 | b = append(b, '0') 31 | } 32 | if microsecond < 10000 { 33 | b = append(b, '0') 34 | } 35 | if microsecond < 1000 { 36 | b = append(b, '0') 37 | } 38 | if microsecond < 100 { 39 | b = append(b, '0') 40 | } 41 | if microsecond < 10 { 42 | b = append(b, '0') 43 | } 44 | return append(b, strconv.Itoa(microsecond)...) 45 | }) 46 | unixseconds = AppendFunc(func(b []byte, t time.Time) []byte { 47 | return append(b, strconv.FormatInt(t.Unix(), 10)...) 48 | }) 49 | } 50 | 51 | // Milliseconds returns the Appender suitable for creating a zero-padded, 52 | // 3-digit millisecond textual representation. 53 | func Milliseconds() Appender { 54 | return milliseconds 55 | } 56 | 57 | // Microsecond returns the Appender suitable for creating a zero-padded, 58 | // 6-digit microsecond textual representation. 59 | func Microseconds() Appender { 60 | return microseconds 61 | } 62 | 63 | // UnixSeconds returns the Appender suitable for creating 64 | // unix timestamp textual representation. 65 | func UnixSeconds() Appender { 66 | return unixseconds 67 | } 68 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/lestrrat-go/strftime 2 | 3 | go 1.21 4 | 5 | require ( 6 | github.com/lestrrat-go/envload v0.0.0-20180220234015-a3eb8ddeffcc 7 | github.com/stretchr/testify v1.9.0 8 | ) 9 | 10 | require ( 11 | github.com/davecgh/go-spew v1.1.1 // indirect 12 | github.com/pmezard/go-difflib v1.0.0 // indirect 13 | gopkg.in/yaml.v3 v3.0.1 // indirect 14 | ) 15 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 2 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 3 | github.com/lestrrat-go/envload v0.0.0-20180220234015-a3eb8ddeffcc h1:RKf14vYWi2ttpEmkA4aQ3j4u9dStX2t4M8UM6qqNsG8= 4 | github.com/lestrrat-go/envload v0.0.0-20180220234015-a3eb8ddeffcc/go.mod h1:kopuH9ugFRkIXf3YoqHKyrJ9YfUFsckUU9S7B+XP+is= 5 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 6 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 7 | github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= 8 | github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 9 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 10 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 11 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 12 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 13 | -------------------------------------------------------------------------------- /internal_test.go: -------------------------------------------------------------------------------- 1 | package strftime 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | func TestCombine(t *testing.T) { 10 | { 11 | s, err := New(`%A foo`) 12 | if !assert.NoError(t, err, `New should succeed`) { 13 | return 14 | } 15 | 16 | if !assert.Equal(t, 1, len(s.compiled), "there are 1 element") { 17 | return 18 | } 19 | } 20 | { 21 | s, _ := New(`%A 100`) 22 | if !assert.Equal(t, 2, len(s.compiled), "there are two elements") { 23 | return 24 | } 25 | } 26 | { 27 | s, _ := New(`%A Mon`) 28 | if !assert.Equal(t, 2, len(s.compiled), "there are two elements") { 29 | return 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /options.go: -------------------------------------------------------------------------------- 1 | package strftime 2 | 3 | type Option interface { 4 | Name() string 5 | Value() interface{} 6 | } 7 | 8 | type option struct { 9 | name string 10 | value interface{} 11 | } 12 | 13 | func (o *option) Name() string { return o.name } 14 | func (o *option) Value() interface{} { return o.value } 15 | 16 | const optSpecificationSet = `opt-specification-set` 17 | 18 | // WithSpecification allows you to specify a custom specification set 19 | func WithSpecificationSet(ds SpecificationSet) Option { 20 | return &option{ 21 | name: optSpecificationSet, 22 | value: ds, 23 | } 24 | } 25 | 26 | type optSpecificationPair struct { 27 | name byte 28 | appender Appender 29 | } 30 | 31 | const optSpecification = `opt-specification` 32 | 33 | // WithSpecification allows you to create a new specification set on the fly, 34 | // to be used only for that invocation. 35 | func WithSpecification(b byte, a Appender) Option { 36 | return &option{ 37 | name: optSpecification, 38 | value: &optSpecificationPair{ 39 | name: b, 40 | appender: a, 41 | }, 42 | } 43 | } 44 | 45 | // WithMilliseconds is similar to WithSpecification, and specifies that 46 | // the Strftime object should interpret the pattern `%b` (where b 47 | // is the byte that you specify as the argument) 48 | // as the zero-padded, 3 letter milliseconds of the time. 49 | func WithMilliseconds(b byte) Option { 50 | return WithSpecification(b, Milliseconds()) 51 | } 52 | 53 | // WithMicroseconds is similar to WithSpecification, and specifies that 54 | // the Strftime object should interpret the pattern `%b` (where b 55 | // is the byte that you specify as the argument) 56 | // as the zero-padded, 3 letter microseconds of the time. 57 | func WithMicroseconds(b byte) Option { 58 | return WithSpecification(b, Microseconds()) 59 | } 60 | 61 | // WithUnixSeconds is similar to WithSpecification, and specifies that 62 | // the Strftime object should interpret the pattern `%b` (where b 63 | // is the byte that you specify as the argument) 64 | // as the unix timestamp in seconds 65 | func WithUnixSeconds(b byte) Option { 66 | return WithSpecification(b, UnixSeconds()) 67 | } 68 | -------------------------------------------------------------------------------- /specifications.go: -------------------------------------------------------------------------------- 1 | package strftime 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "sync" 7 | ) 8 | 9 | // because there is no such thing was a sync.RWLocker 10 | type rwLocker interface { 11 | RLock() 12 | RUnlock() 13 | sync.Locker 14 | } 15 | 16 | // SpecificationSet is a container for patterns that Strftime uses. 17 | // If you want a custom strftime, you can copy the default 18 | // SpecificationSet and tweak it 19 | type SpecificationSet interface { 20 | Lookup(byte) (Appender, error) 21 | Delete(byte) error 22 | Set(byte, Appender) error 23 | } 24 | 25 | type specificationSet struct { 26 | mutable bool 27 | lock rwLocker 28 | store map[byte]Appender 29 | } 30 | 31 | // The default specification set does not need any locking as it is never 32 | // accessed from the outside, and is never mutated. 33 | var defaultSpecificationSet SpecificationSet 34 | 35 | func init() { 36 | defaultSpecificationSet = newImmutableSpecificationSet() 37 | } 38 | 39 | func newImmutableSpecificationSet() SpecificationSet { 40 | // Create a mutable one so that populateDefaultSpecifications work through 41 | // its magic, then copy the associated map 42 | // (NOTE: this is done this way because there used to be 43 | // two struct types for specification set, united under an interface. 44 | // it can now be removed, but we would need to change the entire 45 | // populateDefaultSpecifications method, and I'm currently too lazy 46 | // PRs welcome) 47 | tmp := NewSpecificationSet() 48 | 49 | ss := &specificationSet{ 50 | mutable: false, 51 | lock: nil, // never used, so intentionally not initialized 52 | store: tmp.(*specificationSet).store, 53 | } 54 | 55 | return ss 56 | } 57 | 58 | // NewSpecificationSet creates a specification set with the default specifications. 59 | func NewSpecificationSet() SpecificationSet { 60 | ds := &specificationSet{ 61 | mutable: true, 62 | lock: &sync.RWMutex{}, 63 | store: make(map[byte]Appender), 64 | } 65 | populateDefaultSpecifications(ds) 66 | 67 | return ds 68 | } 69 | 70 | var defaultSpecifications = map[byte]Appender{ 71 | 'A': fullWeekDayName, 72 | 'a': abbrvWeekDayName, 73 | 'B': fullMonthName, 74 | 'b': abbrvMonthName, 75 | 'C': centuryDecimal, 76 | 'c': timeAndDate, 77 | 'D': mdy, 78 | 'd': dayOfMonthZeroPad, 79 | 'e': dayOfMonthSpacePad, 80 | 'F': ymd, 81 | 'H': twentyFourHourClockZeroPad, 82 | 'h': abbrvMonthName, 83 | 'I': twelveHourClockZeroPad, 84 | 'j': dayOfYear, 85 | 'k': twentyFourHourClockSpacePad, 86 | 'l': twelveHourClockSpacePad, 87 | 'M': minutesZeroPad, 88 | 'm': monthNumberZeroPad, 89 | 'n': newline, 90 | 'p': ampm, 91 | 'R': hm, 92 | 'r': imsp, 93 | 'S': secondsNumberZeroPad, 94 | 'T': hms, 95 | 't': tab, 96 | 'U': weekNumberSundayOrigin, 97 | 'u': weekdayMondayOrigin, 98 | 'V': weekNumberMondayOriginOneOrigin, 99 | 'v': eby, 100 | 'W': weekNumberMondayOrigin, 101 | 'w': weekdaySundayOrigin, 102 | 'X': natReprTime, 103 | 'x': natReprDate, 104 | 'Y': year, 105 | 'y': yearNoCentury, 106 | 'Z': timezone, 107 | 'z': timezoneOffset, 108 | '%': percent, 109 | } 110 | 111 | func populateDefaultSpecifications(ds SpecificationSet) { 112 | for c, handler := range defaultSpecifications { 113 | if err := ds.Set(c, handler); err != nil { 114 | panic(fmt.Sprintf("failed to set default specification for %c: %s", c, err)) 115 | } 116 | } 117 | } 118 | 119 | func (ds *specificationSet) Lookup(b byte) (Appender, error) { 120 | if ds.mutable { 121 | ds.lock.RLock() 122 | defer ds.lock.RLock() 123 | } 124 | v, ok := ds.store[b] 125 | if !ok { 126 | return nil, fmt.Errorf(`lookup failed: '%%%c' was not found in specification set`, b) 127 | } 128 | return v, nil 129 | } 130 | 131 | func (ds *specificationSet) Delete(b byte) error { 132 | if !ds.mutable { 133 | return errors.New(`delete failed: this specification set is marked immutable`) 134 | } 135 | 136 | ds.lock.Lock() 137 | defer ds.lock.Unlock() 138 | delete(ds.store, b) 139 | return nil 140 | } 141 | 142 | func (ds *specificationSet) Set(b byte, a Appender) error { 143 | if !ds.mutable { 144 | return errors.New(`set failed: this specification set is marked immutable`) 145 | } 146 | 147 | ds.lock.Lock() 148 | defer ds.lock.Unlock() 149 | ds.store[b] = a 150 | return nil 151 | } 152 | -------------------------------------------------------------------------------- /strftime.go: -------------------------------------------------------------------------------- 1 | package strftime 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "io" 7 | "strings" 8 | "sync" 9 | "time" 10 | ) 11 | 12 | type compileHandler interface { 13 | handle(Appender) 14 | } 15 | 16 | // compile, and create an appender list 17 | type appenderListBuilder struct { 18 | list *combiningAppend 19 | } 20 | 21 | func (alb *appenderListBuilder) handle(a Appender) { 22 | alb.list.Append(a) 23 | } 24 | 25 | // compile, and execute the appenders on the fly 26 | type appenderExecutor struct { 27 | t time.Time 28 | dst []byte 29 | } 30 | 31 | func (ae *appenderExecutor) handle(a Appender) { 32 | ae.dst = a.Append(ae.dst, ae.t) 33 | } 34 | 35 | func compile(handler compileHandler, p string, ds SpecificationSet) error { 36 | for l := len(p); l > 0; l = len(p) { 37 | // This is a really tight loop, so we don't even calls to 38 | // Verbatim() to cuase extra stuff 39 | var verbatim verbatimw 40 | 41 | i := strings.IndexByte(p, '%') 42 | if i < 0 { 43 | verbatim.s = p 44 | handler.handle(&verbatim) 45 | // this is silly, but I don't trust break keywords when there's a 46 | // possibility of this piece of code being rearranged 47 | p = p[l:] 48 | continue 49 | } 50 | if i == l-1 { 51 | return errors.New(`stray % at the end of pattern`) 52 | } 53 | 54 | // we found a '%'. we need the next byte to decide what to do next 55 | // we already know that i < l - 1 56 | // everything up to the i is verbatim 57 | if i > 0 { 58 | verbatim.s = p[:i] 59 | handler.handle(&verbatim) 60 | p = p[i:] 61 | } 62 | 63 | specification, err := ds.Lookup(p[1]) 64 | if err != nil { 65 | return fmt.Errorf("pattern compilation failed: %w", err) 66 | } 67 | 68 | handler.handle(specification) 69 | p = p[2:] 70 | } 71 | return nil 72 | } 73 | 74 | func getSpecificationSetFor(options ...Option) (SpecificationSet, error) { 75 | var ds SpecificationSet = defaultSpecificationSet 76 | var extraSpecifications []*optSpecificationPair 77 | for _, option := range options { 78 | switch option.Name() { 79 | case optSpecificationSet: 80 | ds = option.Value().(SpecificationSet) 81 | case optSpecification: 82 | extraSpecifications = append(extraSpecifications, option.Value().(*optSpecificationPair)) 83 | } 84 | } 85 | 86 | if len(extraSpecifications) > 0 { 87 | // If ds is immutable, we're going to need to create a new 88 | // one. oh what a waste! 89 | if raw, ok := ds.(*specificationSet); ok && !raw.mutable { 90 | ds = NewSpecificationSet() 91 | } 92 | for _, v := range extraSpecifications { 93 | if err := ds.Set(v.name, v.appender); err != nil { 94 | return nil, err 95 | } 96 | } 97 | } 98 | return ds, nil 99 | } 100 | 101 | var fmtAppendExecutorPool = sync.Pool{ 102 | New: func() interface{} { 103 | var h appenderExecutor 104 | h.dst = make([]byte, 0, 32) 105 | return &h 106 | }, 107 | } 108 | 109 | func getFmtAppendExecutor() *appenderExecutor { 110 | return fmtAppendExecutorPool.Get().(*appenderExecutor) 111 | } 112 | 113 | func releasdeFmtAppendExecutor(v *appenderExecutor) { 114 | // TODO: should we discard the buffer if it's too long? 115 | v.dst = v.dst[:0] 116 | fmtAppendExecutorPool.Put(v) 117 | } 118 | 119 | // Format takes the format `s` and the time `t` to produce the 120 | // format date/time. Note that this function re-compiles the 121 | // pattern every time it is called. 122 | // 123 | // If you know beforehand that you will be reusing the pattern 124 | // within your application, consider creating a `Strftime` object 125 | // and reusing it. 126 | func Format(p string, t time.Time, options ...Option) (string, error) { 127 | // TODO: this may be premature optimization 128 | ds, err := getSpecificationSetFor(options...) 129 | if err != nil { 130 | return "", fmt.Errorf("failed to get specification set: %w", err) 131 | } 132 | h := getFmtAppendExecutor() 133 | defer releasdeFmtAppendExecutor(h) 134 | 135 | h.t = t 136 | if err := compile(h, p, ds); err != nil { 137 | return "", fmt.Errorf("failed to compile format: %w", err) 138 | } 139 | 140 | return string(h.dst), nil 141 | } 142 | 143 | // Strftime is the object that represents a compiled strftime pattern 144 | type Strftime struct { 145 | pattern string 146 | compiled appenderList 147 | } 148 | 149 | // New creates a new Strftime object. If the compilation fails, then 150 | // an error is returned in the second argument. 151 | func New(p string, options ...Option) (*Strftime, error) { 152 | // TODO: this may be premature optimization 153 | ds, err := getSpecificationSetFor(options...) 154 | if err != nil { 155 | return nil, fmt.Errorf("failed to get specification set: %w", err) 156 | } 157 | 158 | var h appenderListBuilder 159 | h.list = &combiningAppend{} 160 | 161 | if err := compile(&h, p, ds); err != nil { 162 | return nil, fmt.Errorf("failed to compile format: %w", err) 163 | } 164 | 165 | return &Strftime{ 166 | pattern: p, 167 | compiled: h.list.list, 168 | }, nil 169 | } 170 | 171 | // Pattern returns the original pattern string 172 | func (f *Strftime) Pattern() string { 173 | return f.pattern 174 | } 175 | 176 | // Format takes the destination `dst` and time `t`. It formats the date/time 177 | // using the pre-compiled pattern, and outputs the results to `dst` 178 | func (f *Strftime) Format(dst io.Writer, t time.Time) error { 179 | const bufSize = 64 180 | var b []byte 181 | max := len(f.pattern) + 10 182 | if max < bufSize { 183 | var buf [bufSize]byte 184 | b = buf[:0] 185 | } else { 186 | b = make([]byte, 0, max) 187 | } 188 | if _, err := dst.Write(f.format(b, t)); err != nil { 189 | return err 190 | } 191 | return nil 192 | } 193 | 194 | // FormatBuffer is equivalent to Format, but appends the result directly to 195 | // supplied slice dst, returning the updated slice. This avoids any internal 196 | // memory allocation. 197 | func (f *Strftime) FormatBuffer(dst []byte, t time.Time) []byte { 198 | return f.format(dst, t) 199 | } 200 | 201 | // Dump outputs the internal structure of the formatter, for debugging purposes. 202 | // Please do NOT assume the output format to be fixed: it is expected to change 203 | // in the future. 204 | func (f *Strftime) Dump(out io.Writer) { 205 | f.compiled.dump(out) 206 | } 207 | 208 | func (f *Strftime) format(b []byte, t time.Time) []byte { 209 | for _, w := range f.compiled { 210 | b = w.Append(b, t) 211 | } 212 | return b 213 | } 214 | 215 | // FormatString takes the time `t` and formats it, returning the 216 | // string containing the formated data. 217 | func (f *Strftime) FormatString(t time.Time) string { 218 | const bufSize = 64 219 | var b []byte 220 | max := len(f.pattern) + 10 221 | if max < bufSize { 222 | var buf [bufSize]byte 223 | b = buf[:0] 224 | } else { 225 | b = make([]byte, 0, max) 226 | } 227 | return string(f.format(b, t)) 228 | } 229 | -------------------------------------------------------------------------------- /strftime_test.go: -------------------------------------------------------------------------------- 1 | package strftime_test 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "os" 7 | "testing" 8 | "time" 9 | 10 | envload "github.com/lestrrat-go/envload" 11 | "github.com/lestrrat-go/strftime" 12 | "github.com/stretchr/testify/assert" 13 | ) 14 | 15 | var ref = time.Unix(1136239445, 123456789).UTC() 16 | 17 | func TestExclusion(t *testing.T) { 18 | s, err := strftime.New("%p PM") 19 | if !assert.NoError(t, err, `strftime.New should succeed`) { 20 | return 21 | } 22 | 23 | var tm time.Time 24 | if !assert.Equal(t, "AM PM", s.FormatString(tm)) { 25 | return 26 | } 27 | } 28 | 29 | func TestInvalid(t *testing.T) { 30 | _, err := strftime.New("%") 31 | if !assert.Error(t, err, `strftime.New should return error`) { 32 | return 33 | } 34 | 35 | _, err = strftime.New(" %") 36 | if !assert.Error(t, err, `strftime.New should return error`) { 37 | return 38 | } 39 | _, err = strftime.New(" % ") 40 | if !assert.Error(t, err, `strftime.New should return error`) { 41 | return 42 | } 43 | } 44 | 45 | func TestFormatMethods(t *testing.T) { 46 | l := envload.New() 47 | defer l.Restore() 48 | 49 | os.Setenv("LC_ALL", "C") 50 | 51 | formatString := `%A %a %B %b %C %c %D %d %e %F %H %h %I %j %k %l %M %m %n %p %R %r %S %T %t %U %u %V %v %W %w %X %x %Y %y %Z %z` 52 | resultString := "Monday Mon January Jan 20 Mon Jan 2 22:04:05 2006 01/02/06 02 2 2006-01-02 22 Jan 10 002 22 10 04 01 \n PM 22:04 10:04:05 PM 05 22:04:05 \t 01 1 01 2-Jan-2006 01 1 22:04:05 01/02/06 2006 06 UTC +0000" 53 | 54 | s, err := strftime.Format(formatString, ref) 55 | if !assert.NoError(t, err, `strftime.Format succeeds`) { 56 | return 57 | } 58 | 59 | if !assert.Equal(t, resultString, s, `formatted result matches`) { 60 | return 61 | } 62 | 63 | formatter, err := strftime.New(formatString) 64 | if !assert.NoError(t, err, `strftime.New succeeds`) { 65 | return 66 | } 67 | 68 | if !assert.Equal(t, resultString, formatter.FormatString(ref), `formatted result matches`) { 69 | return 70 | } 71 | 72 | var buf bytes.Buffer 73 | err = formatter.Format(&buf, ref) 74 | if !assert.NoError(t, err, `Format method succeeds`) { 75 | return 76 | } 77 | if !assert.Equal(t, resultString, buf.String(), `formatted result matches`) { 78 | return 79 | } 80 | 81 | var dst []byte 82 | dst = formatter.FormatBuffer(dst, ref) 83 | if !assert.Equal(t, resultString, string(dst), `formatted result matches`) { 84 | return 85 | } 86 | 87 | dst = []byte("nonsense") 88 | dst = formatter.FormatBuffer(dst[:0], ref) 89 | if !assert.Equal(t, resultString, string(dst), `overwritten result matches`) { 90 | return 91 | } 92 | 93 | dst = []byte("nonsense") 94 | dst = formatter.FormatBuffer(dst, ref) 95 | if !assert.Equal(t, "nonsense"+resultString, string(dst), `appended result matches`) { 96 | return 97 | } 98 | 99 | } 100 | 101 | func TestFormatBlanks(t *testing.T) { 102 | l := envload.New() 103 | defer l.Restore() 104 | 105 | os.Setenv("LC_ALL", "C") 106 | 107 | { 108 | dt := time.Date(1, 1, 1, 18, 0, 0, 0, time.UTC) 109 | s, err := strftime.Format("%l", dt) 110 | if !assert.NoError(t, err, `strftime.Format succeeds`) { 111 | return 112 | } 113 | 114 | if !assert.Equal(t, " 6", s, "leading blank is properly set") { 115 | return 116 | } 117 | } 118 | { 119 | dt := time.Date(1, 1, 1, 6, 0, 0, 0, time.UTC) 120 | s, err := strftime.Format("%k", dt) 121 | if !assert.NoError(t, err, `strftime.Format succeeds`) { 122 | return 123 | } 124 | 125 | if !assert.Equal(t, " 6", s, "leading blank is properly set") { 126 | return 127 | } 128 | } 129 | } 130 | 131 | func TestFormatZeropad(t *testing.T) { 132 | l := envload.New() 133 | defer l.Restore() 134 | 135 | os.Setenv("LC_ALL", "C") 136 | 137 | { 138 | dt := time.Date(1, 1, 1, 1, 0, 0, 0, time.UTC) 139 | s, err := strftime.Format("%j", dt) 140 | if !assert.NoError(t, err, `strftime.Format succeeds`) { 141 | return 142 | } 143 | 144 | if !assert.Equal(t, "001", s, "padding is properly set") { 145 | return 146 | } 147 | } 148 | { 149 | dt := time.Date(1, 1, 10, 6, 0, 0, 0, time.UTC) 150 | s, err := strftime.Format("%j", dt) 151 | if !assert.NoError(t, err, `strftime.Format succeeds`) { 152 | return 153 | } 154 | 155 | if !assert.Equal(t, "010", s, "padding is properly set") { 156 | return 157 | } 158 | } 159 | { 160 | dt := time.Date(1, 6, 1, 6, 0, 0, 0, time.UTC) 161 | s, err := strftime.Format("%j", dt) 162 | if !assert.NoError(t, err, `strftime.Format succeeds`) { 163 | return 164 | } 165 | 166 | if !assert.Equal(t, "152", s, "padding is properly set") { 167 | return 168 | } 169 | } 170 | { 171 | dt := time.Date(100, 1, 1, 1, 0, 0, 0, time.UTC) 172 | s, err := strftime.Format("%C", dt) 173 | if !assert.NoError(t, err, `strftime.Format succeeds`) { 174 | return 175 | } 176 | 177 | if !assert.Equal(t, "01", s, "padding is properly set") { 178 | return 179 | } 180 | } 181 | } 182 | 183 | func TestGHIssue5(t *testing.T) { 184 | const expected = `apm-test/logs/apm.log.01000101` 185 | p, _ := strftime.New("apm-test/logs/apm.log.%Y%m%d") 186 | dt := time.Date(100, 1, 1, 1, 0, 0, 0, time.UTC) 187 | if !assert.Equal(t, expected, p.FormatString(dt), `patterns including 'pm' should be treated as verbatim formatter`) { 188 | return 189 | } 190 | } 191 | 192 | func TestGHPR7(t *testing.T) { 193 | const expected = `123` 194 | 195 | p, _ := strftime.New(`%L`, strftime.WithMilliseconds('L')) 196 | if !assert.Equal(t, expected, p.FormatString(ref), `patterns should match for custom specification`) { 197 | return 198 | } 199 | } 200 | 201 | func TestWithMicroseconds(t *testing.T) { 202 | const expected = `123456` 203 | 204 | p, _ := strftime.New(`%f`, strftime.WithMicroseconds('f')) 205 | if !assert.Equal(t, expected, p.FormatString(ref), `patterns should match for custom specification`) { 206 | return 207 | } 208 | } 209 | 210 | func TestWithUnixSeconds(t *testing.T) { 211 | const expected = `1136239445` 212 | 213 | p, _ := strftime.New(`%s`, strftime.WithUnixSeconds('s')) 214 | if !assert.Equal(t, expected, p.FormatString(ref), `patterns should match for custom specification`) { 215 | return 216 | } 217 | } 218 | 219 | func ExampleSpecificationSet() { 220 | { 221 | // I want %L as milliseconds! 222 | p, err := strftime.New(`%L`, strftime.WithMilliseconds('L')) 223 | if err != nil { 224 | fmt.Println(err) 225 | return 226 | } 227 | p.Format(os.Stdout, ref) 228 | os.Stdout.Write([]byte{'\n'}) 229 | } 230 | 231 | { 232 | // I want %f as milliseconds! 233 | p, err := strftime.New(`%f`, strftime.WithMilliseconds('f')) 234 | if err != nil { 235 | fmt.Println(err) 236 | return 237 | } 238 | p.Format(os.Stdout, ref) 239 | os.Stdout.Write([]byte{'\n'}) 240 | } 241 | 242 | { 243 | // I want %X to print out my name! 244 | a := strftime.Verbatim(`Daisuke Maki`) 245 | p, err := strftime.New(`%X`, strftime.WithSpecification('X', a)) 246 | if err != nil { 247 | fmt.Println(err) 248 | return 249 | } 250 | p.Format(os.Stdout, ref) 251 | os.Stdout.Write([]byte{'\n'}) 252 | } 253 | 254 | { 255 | // I want a completely new specification set, and I want %X to print out my name! 256 | a := strftime.Verbatim(`Daisuke Maki`) 257 | 258 | ds := strftime.NewSpecificationSet() 259 | ds.Set('X', a) 260 | p, err := strftime.New(`%X`, strftime.WithSpecificationSet(ds)) 261 | if err != nil { 262 | fmt.Println(err) 263 | return 264 | } 265 | p.Format(os.Stdout, ref) 266 | os.Stdout.Write([]byte{'\n'}) 267 | } 268 | 269 | { 270 | // I want %s as unix timestamp! 271 | p, err := strftime.New(`%s`, strftime.WithUnixSeconds('s')) 272 | if err != nil { 273 | fmt.Println(err) 274 | return 275 | } 276 | p.Format(os.Stdout, ref) 277 | os.Stdout.Write([]byte{'\n'}) 278 | } 279 | 280 | // OUTPUT: 281 | // 123 282 | // 123 283 | // Daisuke Maki 284 | // Daisuke Maki 285 | // 1136239445 286 | } 287 | 288 | func TestGHIssue9(t *testing.T) { 289 | pattern, _ := strftime.New("/full1/test2/to3/proveIssue9isfixed/11%C22/12345%Y%m%d.%H.log.%C.log") 290 | testTime := time.Date(2020, 1, 1, 1, 1, 1, 1, time.UTC) 291 | correctString := "/full1/test2/to3/proveIssue9isfixed/112022/1234520200101.01.log.20.log" 292 | 293 | var buf bytes.Buffer 294 | pattern.Format(&buf, testTime) 295 | 296 | // Using a fixed time should give us a fixed output. 297 | if !assert.True(t, buf.String() == correctString) { 298 | t.Logf("Buffer [%s] should be [%s]", buf.String(), correctString) 299 | return 300 | } 301 | } 302 | 303 | func TestGHIssue18(t *testing.T) { 304 | testIH := func(twelveHour bool) func(t *testing.T) { 305 | var patternString string 306 | if twelveHour { 307 | patternString = "%I" 308 | } else { 309 | patternString = "%H" 310 | } 311 | return func(t *testing.T) { 312 | t.Helper() 313 | var buf bytes.Buffer 314 | pattern, _ := strftime.New(patternString) 315 | for i := 0; i < 24; i++ { 316 | testTime := time.Date(2020, 1, 1, i, 1, 1, 1, time.UTC) 317 | var correctString string 318 | switch { 319 | case twelveHour && i == 0: 320 | correctString = fmt.Sprintf("%02d", 12) 321 | case twelveHour && i > 12: 322 | correctString = fmt.Sprintf("%02d", i-12) 323 | default: 324 | correctString = fmt.Sprintf("%02d", i) 325 | } 326 | 327 | buf.Reset() 328 | 329 | pattern.Format(&buf, testTime) 330 | if !assert.Equal(t, correctString, buf.String(), "Buffer [%s] should be [%s] for time %s", buf.String(), correctString, testTime) { 331 | return 332 | } 333 | } 334 | } 335 | } 336 | testR := func(t *testing.T) { 337 | t.Helper() 338 | patternString := "%r" 339 | var buf bytes.Buffer 340 | pattern, _ := strftime.New(patternString) 341 | for i := 0; i < 24; i++ { 342 | testTime := time.Date(2020, 1, 1, i, 1, 1, 1, time.UTC) 343 | 344 | var correctString string 345 | switch { 346 | case i == 0: 347 | correctString = fmt.Sprintf("%02d:%02d:%02d AM", 12, testTime.Minute(), testTime.Second()) 348 | case i == 12: 349 | correctString = fmt.Sprintf("%02d:%02d:%02d PM", 12, testTime.Minute(), testTime.Second()) 350 | case i > 12: 351 | correctString = fmt.Sprintf("%02d:%02d:%02d PM", i-12, testTime.Minute(), testTime.Second()) 352 | default: 353 | correctString = fmt.Sprintf("%02d:%02d:%02d AM", i, testTime.Minute(), testTime.Second()) 354 | } 355 | 356 | buf.Reset() 357 | 358 | t.Logf("%s", correctString) 359 | pattern.Format(&buf, testTime) 360 | if !assert.Equal(t, correctString, buf.String(), "Buffer [%s] should be [%s] for time %s", buf.String(), correctString, testTime) { 361 | continue 362 | } 363 | } 364 | } 365 | t.Run("12 hour zero pad %I", testIH(true)) 366 | t.Run("24 hour zero pad %H", testIH(false)) 367 | t.Run("12 hour zero pad %r", testR) 368 | } 369 | 370 | func TestFormat12AM(t *testing.T) { 371 | s, err := strftime.Format(`%H %I %l`, time.Time{}) 372 | if !assert.NoError(t, err, `strftime.Format succeeds`) { 373 | return 374 | } 375 | 376 | if !assert.Equal(t, "00 12 12", s, "correctly format the hour") { 377 | return 378 | } 379 | } 380 | 381 | func TestFormat_WeekNumber(t *testing.T) { 382 | for y := 2000; y < 2020; y++ { 383 | sunday := "00" 384 | monday := "00" 385 | for d := 1; d < 8; d++ { 386 | base := time.Date(y, time.January, d, 0, 0, 0, 0, time.UTC) 387 | 388 | switch base.Weekday() { 389 | case time.Sunday: 390 | sunday = "01" 391 | case time.Monday: 392 | monday = "01" 393 | } 394 | 395 | if got, _ := strftime.Format("%U", base); got != sunday { 396 | t.Errorf("Format(%q, %d) = %q, want %q", "%U", base.Unix(), got, sunday) 397 | } 398 | if got, _ := strftime.Format("%W", base); got != monday { 399 | t.Errorf("Format(%q, %d) = %q, want %q", "%W", base.Unix(), got, monday) 400 | } 401 | } 402 | } 403 | } 404 | --------------------------------------------------------------------------------