Files
timefmt/format.go
2026-02-08 16:10:41 +11:00

144 lines
4.0 KiB
Go

package timefmt
import (
"strings"
"time"
)
// Format represents a time format pattern composed of fragments and literals.
// It can convert between Go's reference time format, LDML tokens, and human-readable descriptions.
type Format struct {
fragments []interface{} // Can be Fragment or string (for literals)
}
// NewFormat creates a new Format from a slice of fragments and literals.
// Each element can be either a Fragment or a string literal.
func NewFormat(fragments ...interface{}) *Format {
return &Format{fragments: fragments}
}
// GoFormat returns the Go reference time format string.
// Example: "2006-01-02 15:04:05"
func (f *Format) GoFormat() string {
var sb strings.Builder
for _, frag := range f.fragments {
switch v := frag.(type) {
case Fragment:
sb.WriteString(v.GoFormat)
case string:
sb.WriteString(v)
}
}
return sb.String()
}
// LDML returns the LDML-style human-readable format string.
// Example: "yyyy-MM-dd HH:mm:ss"
func (f *Format) LDML() string {
var sb strings.Builder
for _, frag := range f.fragments {
switch v := frag.(type) {
case Fragment:
sb.WriteString(v.LDML)
case string:
// Escape literals that might conflict with LDML tokens
// by wrapping them in single quotes if they contain letters
if needsEscaping(v) {
sb.WriteRune('\'')
sb.WriteString(v)
sb.WriteRune('\'')
} else {
sb.WriteString(v)
}
}
}
return sb.String()
}
// Description returns a full English description of the format.
// Example: "Year (4-digit), dash, Month (2-digit), dash, Day (2-digit), space, Hour (24-hour, 2-digit), colon, Minute (2-digit), colon, Second (2-digit)"
func (f *Format) Description() string {
var parts []string
for _, frag := range f.fragments {
switch v := frag.(type) {
case Fragment:
parts = append(parts, v.Description)
case string:
parts = append(parts, describeLiteral(v))
}
}
return strings.Join(parts, ", ")
}
// Format formats a time.Time using this format pattern.
// It uses Go's time.Format internally.
func (f *Format) Format(t time.Time) string {
return t.Format(f.GoFormat())
}
// Parse parses a time string using this format pattern.
// It uses Go's time.Parse internally.
func (f *Format) Parse(value string) (time.Time, error) {
return time.Parse(f.GoFormat(), value)
}
// ParseInLocation parses a time string using this format pattern in the specified location.
// It uses Go's time.ParseInLocation internally.
func (f *Format) ParseInLocation(value string, loc *time.Location) (time.Time, error) {
return time.ParseInLocation(f.GoFormat(), value, loc)
}
// Example returns an example of what this format looks like using a reference time.
// The reference time used is: February 8, 2026 at 15:04:05.999999999 in UTC-7
func (f *Format) Example() string {
// Use a specific reference time that shows all components clearly
referenceTime := time.Date(2026, time.February, 8, 15, 4, 5, 999999999, time.FixedZone("MST", -7*3600))
return f.Format(referenceTime)
}
// Fragments returns a copy of the format's fragments slice.
// This is useful for inspecting the format's structure.
func (f *Format) Fragments() []interface{} {
result := make([]interface{}, len(f.fragments))
copy(result, f.fragments)
return result
}
// needsEscaping returns true if a literal string contains letters that might
// be confused with LDML format tokens and should be escaped with quotes.
func needsEscaping(s string) bool {
for _, r := range s {
if (r >= 'a' && r <= 'z') || (r >= 'A' && r <= 'Z') {
return true
}
}
return false
}
// describeLiteral converts a literal string into a human-readable description.
func describeLiteral(s string) string {
switch s {
case "-":
return "dash"
case "/":
return "slash"
case ":":
return "colon"
case " ":
return "space"
case ".":
return "period"
case ",":
return "comma"
case ", ":
return "comma-space"
case "T":
return "literal 'T'"
case "Z":
return "literal 'Z'"
default:
// For other literals, describe as "literal 'X'"
return "literal '" + s + "'"
}
}