youtubebeat/vendor/github.com/elastic/beats/libbeat/common/dtfmt/fmt.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
}