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

302
converter_test.go Normal file
View File

@@ -0,0 +1,302 @@
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)
}
})
}
}