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 }