initial commit

This commit is contained in:
2026-02-08 16:10:41 +11:00
commit 635193f381
13 changed files with 3190 additions and 0 deletions

143
converter.go Normal file
View File

@@ -0,0 +1,143 @@
package timefmt
import (
"fmt"
"strings"
)
// ParseGoFormat parses a Go time format string and returns a Format.
// It analyzes the Go reference time format (e.g., "2006-01-02 15:04:05")
// and converts it into a Format with the appropriate fragments.
//
// Example:
//
// format, err := ParseGoFormat("2006-01-02 15:04:05")
// if err != nil {
// log.Fatal(err)
// }
// fmt.Println(format.LDML()) // "yyyy-MM-dd HH:mm:ss"
// fmt.Println(format.Description()) // Full English description
func ParseGoFormat(goFormat string) (*Format, error) {
if goFormat == "" {
return nil, fmt.Errorf("empty format string")
}
// Define token mappings in order of precedence to avoid partial matches
// CRITICAL: Patterns that could conflict must be ordered carefully!
// - Longer patterns before shorter
// - More specific patterns before general
// - Unique identifiers (like "15" for hour, "04" for minute) before ambiguous ones
tokens := []struct {
goToken string
fragment Fragment
}{
// Subseconds (must come before other number patterns due to decimal point)
{".000000000", Nanosecond},
{".999999999", NanosecondTrim},
{".000000", Microsecond},
{".999999", MicrosecondTrim},
{".000", Millisecond},
{".999", MillisecondTrim},
// Timezone (longer patterns first)
{"-07:00:00", TimezoneOffsetColonSeconds},
{"-070000", TimezoneOffsetSeconds},
{"Z07:00", TimezoneISO8601Colon},
{"Z0700", TimezoneISO8601},
{"-07:00", TimezoneOffsetColon},
{"-0700", TimezoneOffset},
{"-07", TimezoneOffsetHourOnly},
{"MST", TimezoneName},
// Year (must come before month numbers)
{"2006", Year4Digit},
{"06", Year2Digit},
// Month names (before numeric months to avoid conflicts)
{"January", MonthFull},
{"Jan", MonthShort},
// Weekday names (before numeric days)
{"Monday", WeekdayFull},
{"Mon", WeekdayShort},
// Day of year (before regular days, longer patterns first)
{"__2", DayOfYearSpacePadded},
{"002", DayOfYearNumeric},
// Time components (MUST come before month/day numbers to avoid conflicts!)
// "15" is unique to 24-hour format, "04" is unique to minutes, "05" to seconds
{"15", Hour24}, // Must come before "1" (month) and "5" (second)
{"04", Minute}, // Must come before "4" (minute unpadded)
{"05", Second}, // Must come before "5" (second unpadded)
{"03", Hour12Padded}, // Must come before "3" (hour)
// Month and day numbers (after time components!)
{"01", MonthNumeric2}, // Padded month
{"02", DayNumeric2}, // Padded day
{"_2", DaySpacePadded}, // Space-padded day
// Single digit patterns (LAST to avoid premature matching!)
{"1", MonthNumeric},
{"2", DayNumeric},
{"3", Hour12},
{"4", MinuteUnpadded},
{"5", SecondUnpadded},
// AM/PM
{"PM", AMPM},
{"pm", AMPMLower},
}
var fragments []interface{}
i := 0
for i < len(goFormat) {
matched := false
// Try to match tokens (longest first)
for _, token := range tokens {
if strings.HasPrefix(goFormat[i:], token.goToken) {
fragments = append(fragments, token.fragment)
i += len(token.goToken)
matched = true
break
}
}
if !matched {
// This is a literal character
// Collect consecutive literal characters
literalStart := i
i++
// Continue collecting until we hit a token
for i < len(goFormat) {
foundToken := false
for _, token := range tokens {
if strings.HasPrefix(goFormat[i:], token.goToken) {
foundToken = true
break
}
}
if foundToken {
break
}
i++
}
literal := goFormat[literalStart:i]
fragments = append(fragments, literal)
}
}
return &Format{fragments: fragments}, nil
}
// MustParseGoFormat is like ParseGoFormat but panics on error.
// It's useful for initialization of package-level variables.
func MustParseGoFormat(goFormat string) *Format {
format, err := ParseGoFormat(goFormat)
if err != nil {
panic(fmt.Sprintf("MustParseGoFormat: %v", err))
}
return format
}