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 + "'" } }