package strftime import ( "io" "strings" "time" "github.com/pkg/errors" ) var directives = map[byte]appender{ 'A': timefmt("Monday"), 'a': timefmt("Mon"), 'B': timefmt("January"), 'b': timefmt("Jan"), 'C': ¢ury{}, 'c': timefmt("Mon Jan _2 15:04:05 2006"), 'D': timefmt("01/02/06"), 'd': timefmt("02"), 'e': timefmt("_2"), 'F': timefmt("2006-01-02"), 'H': timefmt("15"), 'h': timefmt("Jan"), // same as 'b' 'I': timefmt("3"), 'j': &dayofyear{}, 'k': hourwblank(false), 'l': hourwblank(true), 'M': timefmt("04"), 'm': timefmt("01"), 'n': verbatim("\n"), 'p': timefmt("PM"), 'R': timefmt("15:04"), 'r': timefmt("3:04:05 PM"), 'S': timefmt("05"), 'T': timefmt("15:04:05"), 't': verbatim("\t"), 'U': weeknumberOffset(0), // week number of the year, Sunday first 'u': weekday(1), 'V': &weeknumber{}, 'v': timefmt("_2-Jan-2006"), 'W': weeknumberOffset(1), // week number of the year, Monday first 'w': weekday(0), 'X': timefmt("15:04:05"), // national representation of the time XXX is this correct? 'x': timefmt("01/02/06"), // national representation of the date XXX is this correct? 'Y': timefmt("2006"), // year with century 'y': timefmt("06"), // year w/o century 'Z': timefmt("MST"), // time zone name 'z': timefmt("-0700"), // time zone ofset from UTC '%': verbatim("%"), } 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 compile(wl *appenderList, p string) error { var ca combiningAppend for l := len(p); l > 0; l = len(p) { i := strings.IndexByte(p, '%') if i < 0 { ca.Append(verbatim(p)) // this is silly, but I don't trust break keywords when there's a // possibility of this piece of code being rearranged p = p[l:] continue } if i == l-1 { return errors.New(`stray % at the end of pattern`) } // we found a '%'. we need the next byte to decide what to do next // we already know that i < l - 1 // everything up to the i is verbatim if i > 0 { ca.Append(verbatim(p[:i])) p = p[i:] } directive, ok := directives[p[1]] if !ok { return errors.Errorf(`unknown time format specification '%c'`, p[1]) } ca.Append(directive) p = p[2:] } *wl = ca.list return nil } // Format takes the format `s` and the time `t` to produce the // format date/time. Note that this function re-compiles the // pattern every time it is called. // // If you know beforehand that you will be reusing the pattern // within your application, consider creating a `Strftime` object // and reusing it. func Format(p string, t time.Time) (string, error) { var dst []byte // TODO: optimize for 64 byte strings dst = make([]byte, 0, len(p)+10) // Compile, but execute as we go for l := len(p); l > 0; l = len(p) { i := strings.IndexByte(p, '%') if i < 0 { dst = append(dst, p...) // this is silly, but I don't trust break keywords when there's a // possibility of this piece of code being rearranged p = p[l:] continue } if i == l-1 { return "", errors.New(`stray % at the end of pattern`) } // we found a '%'. we need the next byte to decide what to do next // we already know that i < l - 1 // everything up to the i is verbatim if i > 0 { dst = append(dst, p[:i]...) p = p[i:] } directive, ok := directives[p[1]] if !ok { return "", errors.Errorf(`unknown time format specification '%c'`, p[1]) } dst = directive.Append(dst, t) p = p[2:] } return string(dst), nil } // Strftime is the object that represents a compiled strftime pattern type Strftime struct { pattern string compiled appenderList } // New creates a new Strftime object. If the compilation fails, then // an error is returned in the second argument. func New(f string) (*Strftime, error) { var wl appenderList if err := compile(&wl, f); err != nil { return nil, errors.Wrap(err, `failed to compile format`) } return &Strftime{ pattern: f, compiled: wl, }, nil } // Pattern returns the original pattern string func (f *Strftime) Pattern() string { return f.pattern } // Format takes the destination `dst` and time `t`. It formats the date/time // using the pre-compiled pattern, and outputs the results to `dst` func (f *Strftime) Format(dst io.Writer, t time.Time) error { const bufSize = 64 var b []byte max := len(f.pattern) + 10 if max < bufSize { var buf [bufSize]byte b = buf[:0] } else { b = make([]byte, 0, max) } if _, err := dst.Write(f.format(b, t)); err != nil { return err } return nil } func (f *Strftime) format(b []byte, t time.Time) []byte { for _, w := range f.compiled { b = w.Append(b, t) } return b } // FormatString takes the time `t` and formats it, returning the // string containing the formated data. func (f *Strftime) FormatString(t time.Time) string { const bufSize = 64 var b []byte max := len(f.pattern) + 10 if max < bufSize { var buf [bufSize]byte b = buf[:0] } else { b = make([]byte, 0, max) } return string(f.format(b, t)) }