package timefmt import ( "testing" "time" ) func TestParseGoFormat(t *testing.T) { tests := []struct { name string goFormat string wantLDML string wantDesc string shouldError bool }{ { name: "ISO 8601 date", goFormat: "2006-01-02", wantLDML: "yyyy-MM-dd", wantDesc: "Year (4-digit), dash, Month (2-digit), dash, Day (2-digit)", }, { name: "24-hour time", goFormat: "15:04:05", wantLDML: "HH:mm:ss", wantDesc: "Hour (24-hour, 2-digit), colon, Minute (2-digit), colon, Second (2-digit)", }, { name: "Full datetime", goFormat: "2006-01-02 15:04:05", wantLDML: "yyyy-MM-dd HH:mm:ss", wantDesc: "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)", }, { name: "12-hour with AM/PM", goFormat: "3:04 PM", wantLDML: "h:mm a", wantDesc: "Hour (12-hour), colon, Minute (2-digit), space, AM/PM (uppercase)", }, { name: "US date format", goFormat: "01/02/2006", wantLDML: "MM/dd/yyyy", wantDesc: "Month (2-digit), slash, Day (2-digit), slash, Year (4-digit)", }, { name: "European date format", goFormat: "02/01/2006", wantLDML: "dd/MM/yyyy", wantDesc: "Day (2-digit), slash, Month (2-digit), slash, Year (4-digit)", }, { name: "RFC3339", goFormat: "2006-01-02T15:04:05Z07:00", wantLDML: "yyyy-MM-dd'T'HH:mm:ssZZZZZ", wantDesc: "Year (4-digit), dash, Month (2-digit), dash, Day (2-digit), literal 'T', Hour (24-hour, 2-digit), colon, Minute (2-digit), colon, Second (2-digit), ISO 8601 timezone (Z or ±HH:MM)", }, { name: "With milliseconds", goFormat: "2006-01-02 15:04:05.000", wantLDML: "yyyy-MM-dd HH:mm:ss.SSS", wantDesc: "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), Millisecond (3-digit)", }, { name: "With nanoseconds", goFormat: "2006-01-02 15:04:05.000000000", wantLDML: "yyyy-MM-dd HH:mm:ss.SSSSSSSSS", wantDesc: "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), Nanosecond (9-digit)", }, { name: "Month names", goFormat: "January 2, 2006", wantLDML: "MMMM d, yyyy", wantDesc: "Month (full name), space, Day (numeric), comma-space, Year (4-digit)", }, { name: "Abbreviated month and weekday", goFormat: "Mon, Jan 2 2006", wantLDML: "EEE, MMM d yyyy", wantDesc: "Weekday (abbreviated), comma-space, Month (abbreviated), space, Day (numeric), space, Year (4-digit)", }, { name: "Full weekday and month", goFormat: "Monday, January 2, 2006", wantLDML: "EEEE, MMMM d, yyyy", wantDesc: "Weekday (full name), comma-space, Month (full name), space, Day (numeric), comma-space, Year (4-digit)", }, { name: "With timezone name", goFormat: "2006-01-02 15:04:05 MST", wantLDML: "yyyy-MM-dd HH:mm:ss zzz", wantDesc: "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), space, Timezone abbreviation", }, { name: "With timezone offset", goFormat: "2006-01-02 15:04:05 -0700", wantLDML: "yyyy-MM-dd HH:mm:ss ZZZ", wantDesc: "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), space, Timezone offset (±HHMM)", }, { name: "Kitchen time", goFormat: "3:04PM", wantLDML: "h:mma", wantDesc: "Hour (12-hour), colon, Minute (2-digit), AM/PM (uppercase)", }, { name: "With literal text", goFormat: "2006-01-02 at 15:04", wantLDML: "yyyy-MM-dd' at 'HH:mm", wantDesc: "Year (4-digit), dash, Month (2-digit), dash, Day (2-digit), literal ' at ', Hour (24-hour, 2-digit), colon, Minute (2-digit)", }, { name: "Space-padded day", goFormat: "Jan _2 15:04:05", wantLDML: "MMM d HH:mm:ss", wantDesc: "Month (abbreviated), space, Day (space-padded), space, Hour (24-hour, 2-digit), colon, Minute (2-digit), colon, Second (2-digit)", }, { name: "Empty string", goFormat: "", shouldError: true, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { format, err := ParseGoFormat(tt.goFormat) if tt.shouldError { if err == nil { t.Error("Expected error but got none") } return } if err != nil { t.Fatalf("ParseGoFormat() error = %v", err) } // Verify the format can reproduce the original Go format gotGoFormat := format.GoFormat() if gotGoFormat != tt.goFormat { t.Errorf("GoFormat() = %q, want %q", gotGoFormat, tt.goFormat) } // Verify LDML conversion gotLDML := format.LDML() if gotLDML != tt.wantLDML { t.Errorf("LDML() = %q, want %q", gotLDML, tt.wantLDML) } // Verify description gotDesc := format.Description() if gotDesc != tt.wantDesc { t.Errorf("Description() = %q, want %q", gotDesc, tt.wantDesc) } }) } } func TestParseGoFormat_StdlibFormats(t *testing.T) { // Test that we can parse all the standard library time format constants tests := []struct { name string goFormat string }{ {"time.ANSIC", "Mon Jan _2 15:04:05 2006"}, {"time.UnixDate", "Mon Jan _2 15:04:05 MST 2006"}, {"time.RubyDate", "Mon Jan 02 15:04:05 -0700 2006"}, {"time.RFC822", "02 Jan 06 15:04 MST"}, {"time.RFC822Z", "02 Jan 06 15:04 -0700"}, {"time.RFC850", "Monday, 02-Jan-06 15:04:05 MST"}, {"time.RFC1123", "Mon, 02 Jan 2006 15:04:05 MST"}, {"time.RFC1123Z", "Mon, 02 Jan 2006 15:04:05 -0700"}, {"time.RFC3339", "2006-01-02T15:04:05Z07:00"}, {"time.RFC3339Nano", "2006-01-02T15:04:05.999999999Z07:00"}, {"time.Kitchen", "3:04PM"}, {"time.Stamp", "Jan _2 15:04:05"}, {"time.StampMilli", "Jan _2 15:04:05.000"}, {"time.StampMicro", "Jan _2 15:04:05.000000"}, {"time.StampNano", "Jan _2 15:04:05.000000000"}, {"time.DateTime", "2006-01-02 15:04:05"}, {"time.DateOnly", "2006-01-02"}, {"time.TimeOnly", "15:04:05"}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { format, err := ParseGoFormat(tt.goFormat) if err != nil { t.Fatalf("ParseGoFormat() error = %v", err) } // Verify roundtrip gotGoFormat := format.GoFormat() if gotGoFormat != tt.goFormat { t.Errorf("GoFormat() = %q, want %q", gotGoFormat, tt.goFormat) } // Verify it can actually format a time testTime := time.Date(2026, time.February, 8, 15, 4, 5, 123456789, time.FixedZone("MST", -7*3600)) formatted := format.Format(testTime) if formatted == "" { t.Error("Format() returned empty string") } // Verify LDML and Description don't panic _ = format.LDML() _ = format.Description() }) } } func TestParseGoFormat_RoundTrip(t *testing.T) { // Test that parsing a format and converting back to Go format is lossless formats := []string{ "2006-01-02", "15:04:05", "2006-01-02 15:04:05", "01/02/2006 3:04:05 PM", "Monday, January 2, 2006", "Jan _2 15:04:05.000", "2006-01-02T15:04:05Z07:00", "02 Jan 06 15:04 MST", } for _, original := range formats { t.Run(original, func(t *testing.T) { format, err := ParseGoFormat(original) if err != nil { t.Fatalf("ParseGoFormat() error = %v", err) } result := format.GoFormat() if result != original { t.Errorf("Round trip failed: got %q, want %q", result, original) } }) } } func TestMustParseGoFormat(t *testing.T) { t.Run("Valid format", func(t *testing.T) { // Should not panic format := MustParseGoFormat("2006-01-02") if format == nil { t.Error("MustParseGoFormat returned nil") } }) t.Run("Invalid format panics", func(t *testing.T) { defer func() { if r := recover(); r == nil { t.Error("MustParseGoFormat should panic on empty string") } }() MustParseGoFormat("") }) } func TestParseGoFormat_EdgeCases(t *testing.T) { tests := []struct { name string goFormat string wantGo string }{ { name: "Consecutive literals", goFormat: "2006-01-02T15:04:05", wantGo: "2006-01-02T15:04:05", }, { name: "Just literals", goFormat: "Hello, World!", wantGo: "Hello, World!", }, { name: "Mixed tokens and literals", goFormat: "Year: 2006, Month: 01", wantGo: "Year: 2006, Month: 01", }, { name: "Special characters", goFormat: "2006/01/02 @ 15:04:05", wantGo: "2006/01/02 @ 15:04:05", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { format, err := ParseGoFormat(tt.goFormat) if err != nil { t.Fatalf("ParseGoFormat() error = %v", err) } gotGo := format.GoFormat() if gotGo != tt.wantGo { t.Errorf("GoFormat() = %q, want %q", gotGo, tt.wantGo) } }) } }