Skip to content

Commit

Permalink
Merge pull request #8 from lestrrat-go/topic/configurable-patterns
Browse files Browse the repository at this point in the history
Configurable Specifications
  • Loading branch information
lestrrat authored Nov 8, 2019
2 parents 5c849dd + b7cf3ea commit 76be872
Show file tree
Hide file tree
Showing 11 changed files with 727 additions and 380 deletions.
4 changes: 2 additions & 2 deletions .travis.yml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
language: go
sudo: false
go:
- 1.7.x
- tip
- 1.13.x
- tip
4 changes: 4 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
bench:
go test -tags bench -benchmem -bench .
@git checkout go.mod
@rm go.sum
90 changes: 70 additions & 20 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -88,40 +88,90 @@ Formats the time according to the pre-compiled pattern, and returns the result s
| %z | the time zone offset from UTC |
| %% | a '%' |

# EXTENSIONS / CUSTOM SPECIFICATIONS

This library in general tries to be POSIX compliant, but sometimes you just need that
extra specification or two that is relatively widely used but is not included in the
POSIX specification.

For example, POSIX does not specify how to print out milliseconds,
but popular implementations allow `%f` or `%L` to achieve this.

For those instances, `strftime.Strftime` can be configured to use a custom set of
specifications:

```
ss := strftime.NewSpecificationSet()
ss.Set('L', ...) // provide implementation for `%L`
// pass this new specification set to the strftime instance
p, err := strftime.New(`%L`, strftime.WithSpecificationSet(ss))
p.Format(..., time.Now())
```

The implementation must implement the `Appender` interface, which is

```
type Appender interface {
Append([]byte, time.Time) []byte
}
```

For commonly used extensions such as the millisecond example, we provide a default
implementation so the user can do one of the following:

```
// (1) Pass a speficication byte and the Appender
// This allows you to pass arbitrary Appenders
p, err := strftime.New(
`%L`,
strftime.WithSpecification('L', strftime.Milliseconds),
)
// (2) Pass an option that knows to use strftime.Milliseconds
p, err := strftime.New(
`%L`,
strftime.WithMilliseconds('L'),
)
```

If a common specification is missing, please feel free to submit a PR
(but please be sure to be able to defend how "common" it is)

# PERFORMANCE / OTHER LIBRARIES

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

```
// On my OS X 10.14.5, 2.3 GHz Intel Core i5, 16GB memory.
// go version go1.12.4rc1 darwin/amd64
// On my OS X 10.14.6, 2.3 GHz Intel Core i5, 16GB memory.
// go version go1.13.4 darwin/amd64
hummingbird% go test -tags bench -benchmem -bench .
<snip>
BenchmarkTebeka-4 500000 3894 ns/op 323 B/op 22 allocs/op
BenchmarkJehiah-4 1000000 1503 ns/op 256 B/op 17 allocs/op
BenchmarkFastly-4 3000000 549 ns/op 80 B/op 5 allocs/op
BenchmarkLestrrat-4 2000000 897 ns/op 240 B/op 3 allocs/op
BenchmarkLestrratCachedString-4 3000000 511 ns/op 128 B/op 2 allocs/op
BenchmarkLestrratCachedWriter-4 500000 2020 ns/op 192 B/op 3 allocs/op
BenchmarkTebeka-4 297471 3905 ns/op 257 B/op 20 allocs/op
BenchmarkJehiah-4 818444 1773 ns/op 256 B/op 17 allocs/op
BenchmarkFastly-4 2330794 550 ns/op 80 B/op 5 allocs/op
BenchmarkLestrrat-4 916365 1458 ns/op 80 B/op 2 allocs/op
BenchmarkLestrratCachedString-4 2527428 546 ns/op 128 B/op 2 allocs/op
BenchmarkLestrratCachedWriter-4 537422 2155 ns/op 192 B/op 3 allocs/op
PASS
ok github.com/lestrrat-go/strftime 25.433s
ok github.com/lestrrat-go/strftime 25.618s
```

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

This library is much faster than other libraries *IF* you can reuse the format pattern.
Expand Down
261 changes: 261 additions & 0 deletions appenders.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,261 @@
package strftime

import (
"strconv"
"strings"
"time"
)

// These are all of the standard, POSIX compliant specifications.
// Extensions should be in extensions.go
var (
fullWeekDayName = StdlibFormat("Monday")
abbrvWeekDayName = StdlibFormat("Mon")
fullMonthName = StdlibFormat("January")
abbrvMonthName = StdlibFormat("Jan")
centuryDecimal = AppendFunc(appendCentury)
timeAndDate = StdlibFormat("Mon Jan _2 15:04:05 2006")
mdy = StdlibFormat("01/02/06")
dayOfMonthZeroPad = StdlibFormat("02")
dayOfMonthSpacePad = StdlibFormat("_2")
ymd = StdlibFormat("2006-01-02")
twentyFourHourClockZeroPad = StdlibFormat("15")
twelveHourClockZeroPad = StdlibFormat("3")
dayOfYear = AppendFunc(appendDayOfYear)
twentyFourHourClockSpacePad = hourwblank(false)
twelveHourClockSpacePad = hourwblank(true)
minutesZeroPad = StdlibFormat("04")
monthNumberZeroPad = StdlibFormat("01")
newline = Verbatim("\n")
ampm = StdlibFormat("PM")
hm = StdlibFormat("15:04")
imsp = StdlibFormat("3:04:05 PM")
secondsNumberZeroPad = StdlibFormat("05")
hms = StdlibFormat("15:04:05")
tab = Verbatim("\t")
weekNumberSundayOrigin = weeknumberOffset(0) // week number of the year, Sunday first
weekdayMondayOrigin = weekday(1)
// monday as the first day, and 01 as the first value
weekNumberMondayOriginOneOrigin = AppendFunc(appendWeekNumber)
eby = StdlibFormat("_2-Jan-2006")
// monday as the first day, and 00 as the first value
weekNumberMondayOrigin = weeknumberOffset(1) // week number of the year, Monday first
weekdaySundayOrigin = weekday(0)
natReprTime = StdlibFormat("15:04:05") // national representation of the time XXX is this correct?
natReprDate = StdlibFormat("01/02/06") // national representation of the date XXX is this correct?
year = StdlibFormat("2006") // year with century
yearNoCentury = StdlibFormat("06") // year w/o century
timezone = StdlibFormat("MST") // time zone name
timezoneOffset = StdlibFormat("-0700") // time zone ofset from UTC
percent = Verbatim("%")
)

// Appender is the interface that must be fulfilled by components that
// implement the translation of specifications to actual time value.
//
// The Append method takes the accumulated byte buffer, and the time to
// use to generate the textual representation. The resulting byte
// sequence must be returned by this method, normally by using the
// append() builtin function.
type Appender interface {
Append([]byte, time.Time) []byte
}

// AppendFunc is an utility type to allow users to create a
// function-only version of an Appender
type AppendFunc func([]byte, time.Time) []byte

func (af AppendFunc) Append(b []byte, t time.Time) []byte {
return af(b, t)
}

type appenderList []Appender

// does the time.Format thing
type stdlibFormat struct {
s string
}

// StdlibFormat returns an Appender that simply goes through `time.Format()`
// For example, if you know you want to display the abbreviated month name for %b,
// you can create a StdlibFormat with the pattern `Jan` and register that
// for specification `b`:
//
// a := StdlibFormat(`Jan`)
// ss := NewSpecificationSet()
// ss.Set('b', a) // does %b -> abbreviated month name
func StdlibFormat(s string) Appender {
return &stdlibFormat{s: s}
}

func (v stdlibFormat) Append(b []byte, t time.Time) []byte {
return t.AppendFormat(b, v.s)
}

func (v stdlibFormat) str() string {
return v.s
}

func (v stdlibFormat) canCombine() bool {
return true
}

func (v stdlibFormat) combine(w combiner) Appender {
return StdlibFormat(v.s + w.str())
}

type verbatimw struct {
s string
}

// Verbatim returns an Appender suitable for generating static text.
// For static text, this method is slightly favorable than creating
// your own appender, as adjacent verbatim blocks will be combined
// at compile time to produce more efficient Appenders
func Verbatim(s string) Appender {
return &verbatimw{s: s}
}

func (v verbatimw) Append(b []byte, _ time.Time) []byte {
return append(b, v.s...)
}

func (v verbatimw) canCombine() bool {
return canCombine(v.s)
}

func (v verbatimw) combine(w combiner) Appender {
if _, ok := w.(*stdlibFormat); ok {
return StdlibFormat(v.s + w.str())
}
return Verbatim(v.s + w.str())
}

func (v verbatimw) str() string {
return v.s
}

// These words below, as well as any decimal character
var combineExclusion = []string{
"Mon",
"Monday",
"Jan",
"January",
"MST",
"PM",
"pm",
}

func canCombine(s string) bool {
if strings.ContainsAny(s, "0123456789") {
return false
}
for _, word := range combineExclusion {
if strings.Contains(s, word) {
return false
}
}
return true
}

type combiner interface {
canCombine() bool
combine(combiner) Appender
str() string
}

// this is container for the compiler to keep track of appenders,
// and combine them as we parse and compile the pattern
type combiningAppend struct {
list appenderList
prev Appender
prevCanCombine bool
}

func (ca *combiningAppend) Append(w Appender) {
if ca.prevCanCombine {
if wc, ok := w.(combiner); ok && wc.canCombine() {
ca.prev = ca.prev.(combiner).combine(wc)
ca.list[len(ca.list)-1] = ca.prev
return
}
}

ca.list = append(ca.list, w)
ca.prev = w
ca.prevCanCombine = false
if comb, ok := w.(combiner); ok {
if comb.canCombine() {
ca.prevCanCombine = true
}
}
}

func appendCentury(b []byte, t time.Time) []byte {
n := t.Year() / 100
if n < 10 {
b = append(b, '0')
}
return append(b, strconv.Itoa(n)...)
}

type weekday int

func (v weekday) Append(b []byte, t time.Time) []byte {
n := int(t.Weekday())
if n < int(v) {
n += 7
}
return append(b, byte(n+48))
}

type weeknumberOffset int

func (v weeknumberOffset) Append(b []byte, t time.Time) []byte {
yd := t.YearDay()
offset := int(t.Weekday()) - int(v)
if offset < 0 {
offset += 7
}

if yd < offset {
return append(b, '0', '0')
}

n := ((yd - offset) / 7) + 1
if n < 10 {
b = append(b, '0')
}
return append(b, strconv.Itoa(n)...)
}

func appendWeekNumber(b []byte, t time.Time) []byte {
_, n := t.ISOWeek()
if n < 10 {
b = append(b, '0')
}
return append(b, strconv.Itoa(n)...)
}

func appendDayOfYear(b []byte, t time.Time) []byte {
n := t.YearDay()
if n < 10 {
b = append(b, '0', '0')
} else if n < 100 {
b = append(b, '0')
}
return append(b, strconv.Itoa(n)...)
}

type hourwblank bool

func (v hourwblank) Append(b []byte, t time.Time) []byte {
h := t.Hour()
if bool(v) && h > 12 {
h = h - 12
}
if h < 10 {
b = append(b, ' ')
}
return append(b, strconv.Itoa(h)...)
}
Loading

0 comments on commit 76be872

Please sign in to comment.