278 lines
5.9 KiB
Go
278 lines
5.9 KiB
Go
// Licensed to Elasticsearch B.V. under one or more contributor
|
|
// license agreements. See the NOTICE file distributed with
|
|
// this work for additional information regarding copyright
|
|
// ownership. Elasticsearch B.V. licenses this file to you under
|
|
// the Apache License, Version 2.0 (the "License"); you may
|
|
// not use this file except in compliance with the License.
|
|
// You may obtain a copy of the License at
|
|
//
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
//
|
|
// Unless required by applicable law or agreed to in writing,
|
|
// software distributed under the License is distributed on an
|
|
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
|
// KIND, either express or implied. See the License for the
|
|
// specific language governing permissions and limitations
|
|
// under the License.
|
|
|
|
package dtfmt
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
"unicode/utf8"
|
|
)
|
|
|
|
// Formatter will format time values into strings, based on pattern used to
|
|
// create the Formatter.
|
|
type Formatter struct {
|
|
prog prog
|
|
sz int
|
|
config ctxConfig
|
|
}
|
|
|
|
var ctxPool = &sync.Pool{
|
|
New: func() interface{} { return &ctx{} },
|
|
}
|
|
|
|
func newCtx() *ctx {
|
|
return ctxPool.Get().(*ctx)
|
|
}
|
|
|
|
func newCtxWithSize(sz int) *ctx {
|
|
ctx := newCtx()
|
|
if ctx.buf == nil || cap(ctx.buf) < sz {
|
|
ctx.buf = make([]byte, 0, sz)
|
|
}
|
|
return ctx
|
|
}
|
|
|
|
func releaseCtx(c *ctx) {
|
|
ctxPool.Put(c)
|
|
}
|
|
|
|
// NewFormatter creates a new time formatter based on provided pattern.
|
|
// If pattern is invalid an error is returned.
|
|
func NewFormatter(pattern string) (*Formatter, error) {
|
|
b := newBuilder()
|
|
|
|
err := parsePatternTo(b, pattern)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
b.optimize()
|
|
|
|
cfg, err := b.createConfig()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
prog, err := b.compile()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
sz := b.estimateSize()
|
|
f := &Formatter{
|
|
prog: prog,
|
|
sz: sz,
|
|
config: cfg,
|
|
}
|
|
return f, nil
|
|
}
|
|
|
|
// EstimateSize estimates the required buffer size required to hold
|
|
// the formatted time string. Estimated size gives no exact guarantees.
|
|
// Estimated size might still be too low or too big.
|
|
func (f *Formatter) EstimateSize() int {
|
|
return f.sz
|
|
}
|
|
|
|
func (f *Formatter) appendTo(ctx *ctx, b []byte, t time.Time) ([]byte, error) {
|
|
ctx.initTime(&f.config, t)
|
|
return f.prog.eval(b, ctx, t)
|
|
}
|
|
|
|
// AppendTo appends the formatted time value to the given byte buffer.
|
|
func (f *Formatter) AppendTo(b []byte, t time.Time) ([]byte, error) {
|
|
ctx := newCtx()
|
|
defer releaseCtx(ctx)
|
|
return f.appendTo(ctx, b, t)
|
|
}
|
|
|
|
// Write writes the formatted time value to the given writer. Returns
|
|
// number of bytes written or error if formatter or writer fails.
|
|
func (f *Formatter) Write(w io.Writer, t time.Time) (int, error) {
|
|
var err error
|
|
|
|
ctx := newCtxWithSize(f.sz)
|
|
defer releaseCtx(ctx)
|
|
|
|
ctx.buf, err = f.appendTo(ctx, ctx.buf[:0], t)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
return w.Write(ctx.buf)
|
|
}
|
|
|
|
// Format formats the given time value into a new string.
|
|
func (f *Formatter) Format(t time.Time) (string, error) {
|
|
var err error
|
|
|
|
ctx := newCtxWithSize(f.sz)
|
|
defer releaseCtx(ctx)
|
|
|
|
ctx.buf, err = f.appendTo(ctx, ctx.buf[:0], t)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
return string(ctx.buf), nil
|
|
}
|
|
|
|
func parsePatternTo(b *builder, pattern string) error {
|
|
for i := 0; i < len(pattern); {
|
|
tok, tokText, err := parseToken(pattern, &i)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
tokLen := len(tokText)
|
|
switch tok {
|
|
case 'x': // weekyear (year)
|
|
if tokLen == 2 {
|
|
b.twoDigitWeekYear()
|
|
} else {
|
|
b.weekyear(tokLen, 4)
|
|
}
|
|
|
|
case 'y', 'Y': // year and year of era (year) == 'y'
|
|
if tokLen == 2 {
|
|
b.twoDigitYear()
|
|
} else {
|
|
b.year(tokLen, 4)
|
|
}
|
|
|
|
case 'w': // week of weekyear (num)
|
|
b.weekOfWeekyear(tokLen)
|
|
|
|
case 'e': // day of week (num)
|
|
b.dayOfWeek(tokLen)
|
|
|
|
case 'E': // day of week (text)
|
|
if tokLen >= 4 {
|
|
b.dayOfWeekText()
|
|
} else {
|
|
b.dayOfWeekShortText()
|
|
}
|
|
|
|
case 'D': // day of year (number)
|
|
b.dayOfYear(tokLen)
|
|
|
|
case 'M': // month of year (month)
|
|
if tokLen >= 3 {
|
|
if tokLen >= 4 {
|
|
b.monthOfYearText()
|
|
} else {
|
|
b.monthOfYearShortText()
|
|
}
|
|
} else {
|
|
b.monthOfYear(tokLen)
|
|
}
|
|
|
|
case 'd': //day of month (number)
|
|
b.dayOfMonth(tokLen)
|
|
|
|
case 'a': // half of day (text) 'AM/PM'
|
|
b.halfdayOfDayText()
|
|
|
|
case 'K': // hour of half day (number) (0 - 11)
|
|
b.hourOfHalfday(tokLen)
|
|
|
|
case 'h': // clock hour of half day (number) (1 - 12)
|
|
b.clockhourOfHalfday(tokLen)
|
|
|
|
case 'H': // hour of day (number) (0 - 23)
|
|
b.hourOfDay(tokLen)
|
|
|
|
case 'k': // clock hour of half day (number) (1 - 24)
|
|
b.clockhourOfDay(tokLen)
|
|
|
|
case 'm': // minute of hour
|
|
b.minuteOfHour(tokLen)
|
|
|
|
case 's': // second of minute
|
|
b.secondOfMinute(tokLen)
|
|
|
|
case 'S': // fraction of second
|
|
b.millisOfSecond(tokLen)
|
|
|
|
case '\'': // literal
|
|
if tokLen == 1 {
|
|
b.appendRune(rune(tokText[0]))
|
|
} else {
|
|
b.appendLiteral(tokText)
|
|
}
|
|
|
|
default:
|
|
return fmt.Errorf("unsupport format '%c'", tok)
|
|
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func parseToken(pattern string, i *int) (rune, string, error) {
|
|
start := *i
|
|
idx := start
|
|
length := len(pattern)
|
|
|
|
r, w := utf8.DecodeRuneInString(pattern[idx:])
|
|
idx += w
|
|
if ('A' <= r && r <= 'Z') || ('a' <= r && r <= 'z') {
|
|
// Scan a run of the same character, which indicates a time pattern.
|
|
|
|
for idx < length {
|
|
peek, w := utf8.DecodeRuneInString(pattern[idx:])
|
|
if peek != r {
|
|
break
|
|
}
|
|
|
|
idx += w
|
|
}
|
|
|
|
*i = idx
|
|
return r, pattern[start:idx], nil
|
|
}
|
|
|
|
if r != '\'' { // single character, no escaped string
|
|
*i = idx
|
|
return '\'', pattern[start:idx], nil
|
|
}
|
|
|
|
start = idx // skip ' character
|
|
iEnd := strings.IndexRune(pattern[start:], '\'')
|
|
if iEnd < 0 {
|
|
return r, "", errors.New("missing closing '")
|
|
}
|
|
|
|
if iEnd == 0 {
|
|
// escape single quote literal
|
|
*i = idx + 1
|
|
return r, pattern[start : idx+1], nil
|
|
}
|
|
|
|
iEnd += start
|
|
|
|
*i = iEnd + 1 // point after '
|
|
if len(pattern) > iEnd+1 && pattern[iEnd+1] == '\'' {
|
|
return r, pattern[start : iEnd+1], nil
|
|
}
|
|
|
|
return r, pattern[start:iEnd], nil
|
|
}
|