Skip to content

Commit 76be872

Browse files
authored
Merge pull request #8 from lestrrat-go/topic/configurable-patterns
Configurable Specifications
2 parents 5c849dd + b7cf3ea commit 76be872

File tree

11 files changed

+727
-380
lines changed

11 files changed

+727
-380
lines changed

.travis.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
language: go
22
sudo: false
33
go:
4-
- 1.7.x
5-
- tip
4+
- 1.13.x
5+
- tip

Makefile

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
bench:
2+
go test -tags bench -benchmem -bench .
3+
@git checkout go.mod
4+
@rm go.sum

README.md

Lines changed: 70 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -88,40 +88,90 @@ Formats the time according to the pre-compiled pattern, and returns the result s
8888
| %z | the time zone offset from UTC |
8989
| %% | a '%' |
9090

91+
# EXTENSIONS / CUSTOM SPECIFICATIONS
92+
93+
This library in general tries to be POSIX compliant, but sometimes you just need that
94+
extra specification or two that is relatively widely used but is not included in the
95+
POSIX specification.
96+
97+
For example, POSIX does not specify how to print out milliseconds,
98+
but popular implementations allow `%f` or `%L` to achieve this.
99+
100+
For those instances, `strftime.Strftime` can be configured to use a custom set of
101+
specifications:
102+
103+
```
104+
ss := strftime.NewSpecificationSet()
105+
ss.Set('L', ...) // provide implementation for `%L`
106+
107+
// pass this new specification set to the strftime instance
108+
p, err := strftime.New(`%L`, strftime.WithSpecificationSet(ss))
109+
p.Format(..., time.Now())
110+
```
111+
112+
The implementation must implement the `Appender` interface, which is
113+
114+
```
115+
type Appender interface {
116+
Append([]byte, time.Time) []byte
117+
}
118+
```
119+
120+
For commonly used extensions such as the millisecond example, we provide a default
121+
implementation so the user can do one of the following:
122+
123+
```
124+
// (1) Pass a speficication byte and the Appender
125+
// This allows you to pass arbitrary Appenders
126+
p, err := strftime.New(
127+
`%L`,
128+
strftime.WithSpecification('L', strftime.Milliseconds),
129+
)
130+
131+
// (2) Pass an option that knows to use strftime.Milliseconds
132+
p, err := strftime.New(
133+
`%L`,
134+
strftime.WithMilliseconds('L'),
135+
)
136+
```
137+
138+
If a common specification is missing, please feel free to submit a PR
139+
(but please be sure to be able to defend how "common" it is)
140+
91141
# PERFORMANCE / OTHER LIBRARIES
92142

93143
The following benchmarks were run separately because some libraries were using cgo on specific platforms (notabley, the fastly version)
94144

95145
```
96-
// On my OS X 10.14.5, 2.3 GHz Intel Core i5, 16GB memory.
97-
// go version go1.12.4rc1 darwin/amd64
146+
// On my OS X 10.14.6, 2.3 GHz Intel Core i5, 16GB memory.
147+
// go version go1.13.4 darwin/amd64
98148
hummingbird% go test -tags bench -benchmem -bench .
99149
<snip>
100-
BenchmarkTebeka-4 500000 3894 ns/op 323 B/op 22 allocs/op
101-
BenchmarkJehiah-4 1000000 1503 ns/op 256 B/op 17 allocs/op
102-
BenchmarkFastly-4 3000000 549 ns/op 80 B/op 5 allocs/op
103-
BenchmarkLestrrat-4 2000000 897 ns/op 240 B/op 3 allocs/op
104-
BenchmarkLestrratCachedString-4 3000000 511 ns/op 128 B/op 2 allocs/op
105-
BenchmarkLestrratCachedWriter-4 500000 2020 ns/op 192 B/op 3 allocs/op
150+
BenchmarkTebeka-4 297471 3905 ns/op 257 B/op 20 allocs/op
151+
BenchmarkJehiah-4 818444 1773 ns/op 256 B/op 17 allocs/op
152+
BenchmarkFastly-4 2330794 550 ns/op 80 B/op 5 allocs/op
153+
BenchmarkLestrrat-4 916365 1458 ns/op 80 B/op 2 allocs/op
154+
BenchmarkLestrratCachedString-4 2527428 546 ns/op 128 B/op 2 allocs/op
155+
BenchmarkLestrratCachedWriter-4 537422 2155 ns/op 192 B/op 3 allocs/op
106156
PASS
107-
ok github.com/lestrrat-go/strftime 25.433s
157+
ok github.com/lestrrat-go/strftime 25.618s
108158
```
109159

110160
```
111-
// (NOTE: This benchmark is outdated, and needs to be ru-run)
112-
// On a host on Google Cloud Platform, machine-type: n1-standard-4 (vCPU x 4, memory: 15GB)
113-
// Linux <snip> 3.16.0-4-amd64 #1 SMP Debian 3.16.36-1+deb8u2 (2016-10-19) x86_64 GNU/Linux
114-
// go version go1.8rc1 linux/amd64
161+
// On a host on Google Cloud Platform, machine-type: f1-micro (vCPU x 1, memory: 0.6GB)
162+
// (Yes, I was being skimpy)
163+
// Linux <snip> 4.9.0-11-amd64 #1 SMP Debian 4.9.189-3+deb9u1 (2019-09-20) x86_64 GNU/Linux
164+
// go version go1.13.4 linux/amd64
115165
hummingbird% go test -tags bench -benchmem -bench .
116166
<snip>
117-
BenchmarkTebeka-4 500000 3904 ns/op 288 B/op 21 allocs/op
118-
BenchmarkJehiah-4 1000000 1665 ns/op 256 B/op 17 allocs/op
119-
BenchmarkFastly-4 1000000 2134 ns/op 192 B/op 13 allocs/op
120-
BenchmarkLestrrat-4 1000000 1327 ns/op 240 B/op 3 allocs/op
121-
BenchmarkLestrratCachedString-4 3000000 498 ns/op 128 B/op 2 allocs/op
122-
BenchmarkLestrratCachedWriter-4 1000000 3390 ns/op 192 B/op 3 allocs/op
167+
BenchmarkTebeka 254997 4726 ns/op 256 B/op 20 allocs/op
168+
BenchmarkJehiah 659289 1882 ns/op 256 B/op 17 allocs/op
169+
BenchmarkFastly 389150 3044 ns/op 224 B/op 13 allocs/op
170+
BenchmarkLestrrat 699069 1780 ns/op 80 B/op 2 allocs/op
171+
BenchmarkLestrratCachedString 2081594 589 ns/op 128 B/op 2 allocs/op
172+
BenchmarkLestrratCachedWriter 825763 1480 ns/op 192 B/op 3 allocs/op
123173
PASS
124-
ok github.com/lestrrat-go/strftime 44.854s
174+
ok github.com/lestrrat-go/strftime 11.355s
125175
```
126176

127177
This library is much faster than other libraries *IF* you can reuse the format pattern.

appenders.go

Lines changed: 261 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,261 @@
1+
package strftime
2+
3+
import (
4+
"strconv"
5+
"strings"
6+
"time"
7+
)
8+
9+
// These are all of the standard, POSIX compliant specifications.
10+
// Extensions should be in extensions.go
11+
var (
12+
fullWeekDayName = StdlibFormat("Monday")
13+
abbrvWeekDayName = StdlibFormat("Mon")
14+
fullMonthName = StdlibFormat("January")
15+
abbrvMonthName = StdlibFormat("Jan")
16+
centuryDecimal = AppendFunc(appendCentury)
17+
timeAndDate = StdlibFormat("Mon Jan _2 15:04:05 2006")
18+
mdy = StdlibFormat("01/02/06")
19+
dayOfMonthZeroPad = StdlibFormat("02")
20+
dayOfMonthSpacePad = StdlibFormat("_2")
21+
ymd = StdlibFormat("2006-01-02")
22+
twentyFourHourClockZeroPad = StdlibFormat("15")
23+
twelveHourClockZeroPad = StdlibFormat("3")
24+
dayOfYear = AppendFunc(appendDayOfYear)
25+
twentyFourHourClockSpacePad = hourwblank(false)
26+
twelveHourClockSpacePad = hourwblank(true)
27+
minutesZeroPad = StdlibFormat("04")
28+
monthNumberZeroPad = StdlibFormat("01")
29+
newline = Verbatim("\n")
30+
ampm = StdlibFormat("PM")
31+
hm = StdlibFormat("15:04")
32+
imsp = StdlibFormat("3:04:05 PM")
33+
secondsNumberZeroPad = StdlibFormat("05")
34+
hms = StdlibFormat("15:04:05")
35+
tab = Verbatim("\t")
36+
weekNumberSundayOrigin = weeknumberOffset(0) // week number of the year, Sunday first
37+
weekdayMondayOrigin = weekday(1)
38+
// monday as the first day, and 01 as the first value
39+
weekNumberMondayOriginOneOrigin = AppendFunc(appendWeekNumber)
40+
eby = StdlibFormat("_2-Jan-2006")
41+
// monday as the first day, and 00 as the first value
42+
weekNumberMondayOrigin = weeknumberOffset(1) // week number of the year, Monday first
43+
weekdaySundayOrigin = weekday(0)
44+
natReprTime = StdlibFormat("15:04:05") // national representation of the time XXX is this correct?
45+
natReprDate = StdlibFormat("01/02/06") // national representation of the date XXX is this correct?
46+
year = StdlibFormat("2006") // year with century
47+
yearNoCentury = StdlibFormat("06") // year w/o century
48+
timezone = StdlibFormat("MST") // time zone name
49+
timezoneOffset = StdlibFormat("-0700") // time zone ofset from UTC
50+
percent = Verbatim("%")
51+
)
52+
53+
// Appender is the interface that must be fulfilled by components that
54+
// implement the translation of specifications to actual time value.
55+
//
56+
// The Append method takes the accumulated byte buffer, and the time to
57+
// use to generate the textual representation. The resulting byte
58+
// sequence must be returned by this method, normally by using the
59+
// append() builtin function.
60+
type Appender interface {
61+
Append([]byte, time.Time) []byte
62+
}
63+
64+
// AppendFunc is an utility type to allow users to create a
65+
// function-only version of an Appender
66+
type AppendFunc func([]byte, time.Time) []byte
67+
68+
func (af AppendFunc) Append(b []byte, t time.Time) []byte {
69+
return af(b, t)
70+
}
71+
72+
type appenderList []Appender
73+
74+
// does the time.Format thing
75+
type stdlibFormat struct {
76+
s string
77+
}
78+
79+
// StdlibFormat returns an Appender that simply goes through `time.Format()`
80+
// For example, if you know you want to display the abbreviated month name for %b,
81+
// you can create a StdlibFormat with the pattern `Jan` and register that
82+
// for specification `b`:
83+
//
84+
// a := StdlibFormat(`Jan`)
85+
// ss := NewSpecificationSet()
86+
// ss.Set('b', a) // does %b -> abbreviated month name
87+
func StdlibFormat(s string) Appender {
88+
return &stdlibFormat{s: s}
89+
}
90+
91+
func (v stdlibFormat) Append(b []byte, t time.Time) []byte {
92+
return t.AppendFormat(b, v.s)
93+
}
94+
95+
func (v stdlibFormat) str() string {
96+
return v.s
97+
}
98+
99+
func (v stdlibFormat) canCombine() bool {
100+
return true
101+
}
102+
103+
func (v stdlibFormat) combine(w combiner) Appender {
104+
return StdlibFormat(v.s + w.str())
105+
}
106+
107+
type verbatimw struct {
108+
s string
109+
}
110+
111+
// Verbatim returns an Appender suitable for generating static text.
112+
// For static text, this method is slightly favorable than creating
113+
// your own appender, as adjacent verbatim blocks will be combined
114+
// at compile time to produce more efficient Appenders
115+
func Verbatim(s string) Appender {
116+
return &verbatimw{s: s}
117+
}
118+
119+
func (v verbatimw) Append(b []byte, _ time.Time) []byte {
120+
return append(b, v.s...)
121+
}
122+
123+
func (v verbatimw) canCombine() bool {
124+
return canCombine(v.s)
125+
}
126+
127+
func (v verbatimw) combine(w combiner) Appender {
128+
if _, ok := w.(*stdlibFormat); ok {
129+
return StdlibFormat(v.s + w.str())
130+
}
131+
return Verbatim(v.s + w.str())
132+
}
133+
134+
func (v verbatimw) str() string {
135+
return v.s
136+
}
137+
138+
// These words below, as well as any decimal character
139+
var combineExclusion = []string{
140+
"Mon",
141+
"Monday",
142+
"Jan",
143+
"January",
144+
"MST",
145+
"PM",
146+
"pm",
147+
}
148+
149+
func canCombine(s string) bool {
150+
if strings.ContainsAny(s, "0123456789") {
151+
return false
152+
}
153+
for _, word := range combineExclusion {
154+
if strings.Contains(s, word) {
155+
return false
156+
}
157+
}
158+
return true
159+
}
160+
161+
type combiner interface {
162+
canCombine() bool
163+
combine(combiner) Appender
164+
str() string
165+
}
166+
167+
// this is container for the compiler to keep track of appenders,
168+
// and combine them as we parse and compile the pattern
169+
type combiningAppend struct {
170+
list appenderList
171+
prev Appender
172+
prevCanCombine bool
173+
}
174+
175+
func (ca *combiningAppend) Append(w Appender) {
176+
if ca.prevCanCombine {
177+
if wc, ok := w.(combiner); ok && wc.canCombine() {
178+
ca.prev = ca.prev.(combiner).combine(wc)
179+
ca.list[len(ca.list)-1] = ca.prev
180+
return
181+
}
182+
}
183+
184+
ca.list = append(ca.list, w)
185+
ca.prev = w
186+
ca.prevCanCombine = false
187+
if comb, ok := w.(combiner); ok {
188+
if comb.canCombine() {
189+
ca.prevCanCombine = true
190+
}
191+
}
192+
}
193+
194+
func appendCentury(b []byte, t time.Time) []byte {
195+
n := t.Year() / 100
196+
if n < 10 {
197+
b = append(b, '0')
198+
}
199+
return append(b, strconv.Itoa(n)...)
200+
}
201+
202+
type weekday int
203+
204+
func (v weekday) Append(b []byte, t time.Time) []byte {
205+
n := int(t.Weekday())
206+
if n < int(v) {
207+
n += 7
208+
}
209+
return append(b, byte(n+48))
210+
}
211+
212+
type weeknumberOffset int
213+
214+
func (v weeknumberOffset) Append(b []byte, t time.Time) []byte {
215+
yd := t.YearDay()
216+
offset := int(t.Weekday()) - int(v)
217+
if offset < 0 {
218+
offset += 7
219+
}
220+
221+
if yd < offset {
222+
return append(b, '0', '0')
223+
}
224+
225+
n := ((yd - offset) / 7) + 1
226+
if n < 10 {
227+
b = append(b, '0')
228+
}
229+
return append(b, strconv.Itoa(n)...)
230+
}
231+
232+
func appendWeekNumber(b []byte, t time.Time) []byte {
233+
_, n := t.ISOWeek()
234+
if n < 10 {
235+
b = append(b, '0')
236+
}
237+
return append(b, strconv.Itoa(n)...)
238+
}
239+
240+
func appendDayOfYear(b []byte, t time.Time) []byte {
241+
n := t.YearDay()
242+
if n < 10 {
243+
b = append(b, '0', '0')
244+
} else if n < 100 {
245+
b = append(b, '0')
246+
}
247+
return append(b, strconv.Itoa(n)...)
248+
}
249+
250+
type hourwblank bool
251+
252+
func (v hourwblank) Append(b []byte, t time.Time) []byte {
253+
h := t.Hour()
254+
if bool(v) && h > 12 {
255+
h = h - 12
256+
}
257+
if h < 10 {
258+
b = append(b, ' ')
259+
}
260+
return append(b, strconv.Itoa(h)...)
261+
}

0 commit comments

Comments
 (0)