package strftime import "unicode/utf8" type parser struct { format func(spec, flag byte) error literal func(byte) error } func (p *parser) parse(fmt string) error { const ( initial = iota percent flagged modified ) var flag, modifier byte var err error state := initial start := 0 for i, b := range []byte(fmt) { switch state { default: if b == '%' { state = percent start = i continue } err = p.literal(b) case percent: if b == '-' || b == ':' { state = flagged flag = b continue } if b == 'E' || b == 'O' { state = modified modifier = b flag = 0 continue } err = p.format(b, 0) state = initial case flagged: if b == 'E' || b == 'O' { state = modified modifier = b continue } err = p.format(b, flag) state = initial case modified: if okModifier(modifier, b) { err = p.format(b, flag) } else { err = p.literals(fmt[start : i+1]) } state = initial } if err != nil { if err, ok := err.(formatError); ok { err.setDirective(fmt, start, i) return err } return err } } if state != initial { return p.literals(fmt[start:]) } return nil } func (p *parser) literals(literal string) error { for _, b := range []byte(literal) { if err := p.literal(b); err != nil { return err } } return nil } type literalErr string func (e literalErr) Error() string { return "strftime: unsupported literal: " + string(e) } type formatError struct { message string directive string } func (e formatError) Error() string { return "strftime: unsupported directive: " + e.directive + " " + e.message } func (e *formatError) setDirective(str string, i, j int) { _, n := utf8.DecodeRuneInString(str[j:]) e.directive = str[i : j+n] }
package strftime import "strings" // https://strftime.org/ func goLayout(spec, flag byte, parsing bool) string { switch spec { default: return "" case 'B': return "January" case 'b', 'h': return "Jan" case 'm': if flag == '-' || parsing { return "1" } return "01" case 'A': return "Monday" case 'a': return "Mon" case 'e': return "_2" case 'd': if flag == '-' || parsing { return "2" } return "02" case 'j': if flag == '-' { if parsing { return "__2" } return "" } return "002" case 'I': if flag == '-' || parsing { return "3" } return "03" case 'H': if flag == '-' && !parsing { return "" } return "15" case 'M': if flag == '-' || parsing { return "4" } return "04" case 'S': if flag == '-' || parsing { return "5" } return "05" case 'y': return "06" case 'Y': return "2006" case 'p': return "PM" case 'P': return "pm" case 'Z': return "MST" case 'z': if flag == ':' { if parsing { return "Z07:00" } return "-07:00" } if parsing { return "Z0700" } return "-0700" case '+': if parsing { return "Mon Jan _2 15:4:5 MST 2006" } return "Mon Jan _2 15:04:05 MST 2006" case 'c': if parsing { return "Mon Jan _2 15:4:5 2006" } return "Mon Jan _2 15:04:05 2006" case 'v': return "_2-Jan-2006" case 'F': if parsing { return "2006-1-2" } return "2006-01-02" case 'D', 'x': if parsing { return "1/2/06" } return "01/02/06" case 'r': if parsing { return "3:4:5 PM" } return "03:04:05 PM" case 'T', 'X': if parsing { return "15:4:5" } return "15:04:05" case 'R': if parsing { return "15:4" } return "15:04" case '%': return "%" case 't': return "\t" case 'n': return "\n" } } // https://nsdateformatter.com/ func uts35Pattern(spec, flag byte) string { switch spec { default: return "" case 'B': return "MMMM" case 'b', 'h': return "MMM" case 'm': if flag == '-' { return "M" } return "MM" case 'A': return "EEEE" case 'a': return "E" case 'd': if flag == '-' { return "d" } return "dd" case 'j': if flag == '-' { return "D" } return "DDD" case 'I': if flag == '-' { return "h" } return "hh" case 'H': if flag == '-' { return "H" } return "HH" case 'M': if flag == '-' { return "m" } return "mm" case 'S': if flag == '-' { return "s" } return "ss" case 'y': return "yy" case 'Y': return "yyyy" case 'g': return "YY" case 'G': return "YYYY" case 'V': if flag == '-' { return "w" } return "ww" case 'p': return "a" case 'Z': return "zzz" case 'z': if flag == ':' { return "xxx" } return "xx" case 'L': return "SSS" case 'f': return "SSSSSS" case 'N': return "SSSSSSSSS" case '+': return "E MMM d HH:mm:ss zzz yyyy" case 'c': return "E MMM d HH:mm:ss yyyy" case 'v': return "d-MMM-yyyy" case 'F': return "yyyy-MM-dd" case 'D', 'x': return "MM/dd/yy" case 'r': return "hh:mm:ss a" case 'T', 'X': return "HH:mm:ss" case 'R': return "HH:mm" case '%': return "%" case 't': return "\t" case 'n': return "\n" } } // http://man.he.net/man3/strftime func okModifier(mod, spec byte) bool { if mod == 'E' { return strings.Contains("cCxXyY", string(spec)) } if mod == 'O' { return strings.Contains("deHImMSuUVwWy", string(spec)) } return false }
package strftime import ( "bytes" "strconv" "time" ) // Format returns a textual representation of the time value // formatted according to the strftime format specification. func Format(fmt string, t time.Time) string { buf := buffer(fmt) return string(AppendFormat(buf, fmt, t)) } // AppendFormat is like Format, but appends the textual representation // to dst and returns the extended buffer. func AppendFormat(dst []byte, fmt string, t time.Time) []byte { var parser parser parser.literal = func(b byte) error { dst = append(dst, b) return nil } parser.format = func(spec, flag byte) error { switch spec { case 'A': dst = append(dst, t.Weekday().String()...) return nil case 'a': dst = append(dst, t.Weekday().String()[:3]...) return nil case 'B': dst = append(dst, t.Month().String()...) return nil case 'b', 'h': dst = append(dst, t.Month().String()[:3]...) return nil case 'm': dst = appendInt2(dst, int(t.Month()), flag) return nil case 'd': dst = appendInt2(dst, int(t.Day()), flag) return nil case 'e': dst = appendInt2(dst, int(t.Day()), ' ') return nil case 'I': dst = append12Hour(dst, t, flag) return nil case 'l': dst = append12Hour(dst, t, ' ') return nil case 'H': dst = appendInt2(dst, t.Hour(), flag) return nil case 'k': dst = appendInt2(dst, t.Hour(), ' ') return nil case 'M': dst = appendInt2(dst, t.Minute(), flag) return nil case 'S': dst = appendInt2(dst, t.Second(), flag) return nil case 'L': dst = append(dst, t.Format(".000")[1:]...) return nil case 'f': dst = append(dst, t.Format(".000000")[1:]...) return nil case 'N': dst = append(dst, t.Format(".000000000")[1:]...) return nil case 'y': dst = t.AppendFormat(dst, "06") return nil case 'Y': dst = t.AppendFormat(dst, "2006") return nil case 'C': dst = t.AppendFormat(dst, "2006") dst = dst[:len(dst)-2] return nil case 'U': dst = appendWeekNumber(dst, t, flag, true) return nil case 'W': dst = appendWeekNumber(dst, t, flag, false) return nil case 'V': _, w := t.ISOWeek() dst = appendInt2(dst, w, flag) return nil case 'g': y, _ := t.ISOWeek() dst = year(y).AppendFormat(dst, "06") return nil case 'G': y, _ := t.ISOWeek() dst = year(y).AppendFormat(dst, "2006") return nil case 's': dst = strconv.AppendInt(dst, t.Unix(), 10) return nil case 'Q': dst = strconv.AppendInt(dst, t.UnixMilli(), 10) return nil case 'w': w := t.Weekday() dst = appendInt1(dst, int(w)) return nil case 'u': if w := t.Weekday(); w == 0 { dst = append(dst, '7') } else { dst = appendInt1(dst, int(w)) } return nil case 'j': if flag == '-' { dst = strconv.AppendInt(dst, int64(t.YearDay()), 10) } else { dst = t.AppendFormat(dst, "002") } return nil } if layout := goLayout(spec, flag, false); layout != "" { dst = t.AppendFormat(dst, layout) return nil } dst = append(dst, '%') if flag != 0 { dst = append(dst, flag) } dst = append(dst, spec) return nil } parser.parse(fmt) return dst } // Parse converts a textual representation of time to the time value it represents // according to the strptime format specification. func Parse(fmt, value string) (time.Time, error) { pattern, err := layout(fmt, true) if err != nil { return time.Time{}, err } return time.Parse(pattern, value) } // Layout converts a strftime format specification // to a Go time pattern specification. func Layout(fmt string) (string, error) { return layout(fmt, false) } func layout(fmt string, parsing bool) (string, error) { dst := buffer(fmt) var parser parser parser.literal = func(b byte) error { if '0' <= b && b <= '9' { return literalErr(b) } dst = append(dst, b) if b == 'M' || b == 'T' || b == 'm' || b == 'n' { switch { case bytes.HasSuffix(dst, []byte("Jan")): return literalErr("Jan") case bytes.HasSuffix(dst, []byte("Mon")): return literalErr("Mon") case bytes.HasSuffix(dst, []byte("MST")): return literalErr("MST") case bytes.HasSuffix(dst, []byte("PM")): return literalErr("PM") case bytes.HasSuffix(dst, []byte("pm")): return literalErr("pm") } } return nil } parser.format = func(spec, flag byte) error { if layout := goLayout(spec, flag, parsing); layout != "" { dst = append(dst, layout...) return nil } switch spec { default: return formatError{} case 'L', 'f', 'N': if bytes.HasSuffix(dst, []byte(".")) || bytes.HasSuffix(dst, []byte(",")) { switch spec { default: dst = append(dst, "000"...) case 'f': dst = append(dst, "000000"...) case 'N': dst = append(dst, "000000000"...) } return nil } return formatError{message: "must follow '.' or ','"} } } if err := parser.parse(fmt); err != nil { return "", err } return string(dst), nil } // UTS35 converts a strftime format specification // to a Unicode Technical Standard #35 Date Format Pattern. func UTS35(fmt string) (string, error) { const quote = '\'' var quoted bool dst := buffer(fmt) var parser parser parser.literal = func(b byte) error { if b == quote { dst = append(dst, quote, quote) return nil } if !quoted && ('a' <= b && b <= 'z' || 'A' <= b && b <= 'Z') { dst = append(dst, quote) quoted = true } dst = append(dst, b) return nil } parser.format = func(spec, flag byte) error { if quoted { dst = append(dst, quote) quoted = false } if pattern := uts35Pattern(spec, flag); pattern != "" { dst = append(dst, pattern...) return nil } return formatError{} } if err := parser.parse(fmt); err != nil { return "", err } if quoted { dst = append(dst, quote) } return string(dst), nil } func buffer(format string) (buf []byte) { const bufSize = 64 max := len(format) + 10 if max < bufSize { var b [bufSize]byte buf = b[:0] } else { buf = make([]byte, 0, max) } return } func year(y int) time.Time { return time.Date(y, time.January, 1, 0, 0, 0, 0, time.UTC) } func appendWeekNumber(dst []byte, t time.Time, flag byte, sunday bool) []byte { offset := int(t.Weekday()) if sunday { offset = 6 - offset } else if offset != 0 { offset = 7 - offset } return appendInt2(dst, (t.YearDay()+offset)/7, flag) } func append12Hour(dst []byte, t time.Time, flag byte) []byte { h := t.Hour() if h == 0 { h = 12 } else if h > 12 { h -= 12 } return appendInt2(dst, h, flag) } func appendInt1(dst []byte, i int) []byte { return append(dst, byte('0'+i)) } func appendInt2(dst []byte, i int, flag byte) []byte { if flag == 0 || i >= 10 { return append(dst, smallsString[i*2:i*2+2]...) } if flag == ' ' { dst = append(dst, flag) } return appendInt1(dst, i) } const smallsString = "" + "00010203040506070809" + "10111213141516171819" + "20212223242526272829" + "30313233343536373839" + "40414243444546474849" + "50515253545556575859" + "60616263646566676869" + "70717273747576777879" + "80818283848586878889" + "90919293949596979899"