package quando import ( "errors" "testing" "time" ) func TestParse(t *testing.T) { tests := []struct { name string input string expected time.Time }{ // ISO format (YYYY-MM-DD) {"ISO: basic date", "2026-02-09", time.Date(2026, 2, 9, 0, 0, 0, 0, time.UTC)}, {"ISO: year end", "2024-12-31", time.Date(2024, 12, 31, 0, 0, 0, 0, time.UTC)}, {"ISO: year start", "2020-01-01", time.Date(2020, 1, 1, 0, 0, 0, 0, time.UTC)}, {"ISO: leap year", "2024-02-29", time.Date(2024, 2, 29, 0, 0, 0, 0, time.UTC)}, {"ISO: month boundary", "2026-06-30", time.Date(2026, 6, 30, 0, 0, 0, 0, time.UTC)}, // ISO with slash (YYYY/MM/DD) {"ISO slash: basic date", "2026/02/09", time.Date(2026, 2, 9, 0, 0, 0, 0, time.UTC)}, {"ISO slash: year end", "2024/12/31", time.Date(2024, 12, 31, 0, 0, 0, 0, time.UTC)}, {"ISO slash: year start", "2020/01/01", time.Date(2020, 1, 1, 0, 0, 0, 0, time.UTC)}, {"ISO slash: leap year", "2024/02/29", time.Date(2024, 2, 29, 0, 0, 0, 0, time.UTC)}, // EU format (DD.MM.YYYY) {"EU: basic date", "09.02.2026", time.Date(2026, 2, 9, 0, 0, 0, 0, time.UTC)}, {"EU: year end", "31.12.2024", time.Date(2024, 12, 31, 0, 0, 0, 0, time.UTC)}, {"EU: year start", "01.01.2020", time.Date(2020, 1, 1, 0, 0, 0, 0, time.UTC)}, {"EU: leap year", "29.02.2024", time.Date(2024, 2, 29, 0, 0, 0, 0, time.UTC)}, {"EU: month boundary", "30.06.2026", time.Date(2026, 6, 30, 0, 0, 0, 0, time.UTC)}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { result, err := Parse(tt.input) if err != nil { t.Fatalf("Parse(%q) unexpected error: %v", tt.input, err) } // Compare the time values if !result.Time().Equal(tt.expected) { t.Errorf("Parse(%q) = %v, want %v", tt.input, result.Time(), tt.expected) } // Verify default language is EN if result.lang != EN { t.Errorf("Parse(%q) lang = %v, want %v", tt.input, result.lang, EN) } }) } } func TestParseErrors(t *testing.T) { tests := []struct { name string input string wantError bool checkMsg string // substring to check in error message }{ // Ambiguous formats {"ambiguous: 01/02/2026", "01/02/2026", true, "ambiguous"}, {"ambiguous: 31/12/2024", "31/12/2024", true, "ambiguous"}, {"ambiguous: 15/06/2025", "15/06/2025", true, "ambiguous"}, {"ambiguous: 01/01/2020", "01/01/2020", true, "ambiguous"}, // Invalid formats {"invalid: not a date", "not-a-date", true, ""}, {"invalid: empty string", "", true, "empty"}, {"invalid: only whitespace", " ", true, ""}, {"invalid: incomplete date", "2026-02", true, ""}, {"invalid: wrong separator", "2026_02_09", true, ""}, {"invalid: extra characters", "2026-02-09 extra", true, ""}, // Invalid date components {"invalid: month 13", "2026-13-01", true, ""}, {"invalid: month 00", "2026-00-01", true, ""}, {"invalid: day 00", "2026-02-00", true, ""}, {"invalid: day 32", "2026-01-32", true, ""}, {"invalid: Feb 30", "2026-02-30", true, ""}, {"invalid: non-leap year Feb 29", "2023-02-29", true, ""}, {"invalid: April 31", "2026-04-31", true, ""}, // EU format invalid dates {"invalid EU: Feb 30", "30.02.2026", true, ""}, {"invalid EU: month 13", "01.13.2026", true, ""}, // Edge cases {"invalid: just numbers", "20260209", true, ""}, {"invalid: wrong length", "26-02-09", true, ""}, {"invalid: mixed separators", "2026-02/09", true, ""}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { result, err := Parse(tt.input) if tt.wantError { if err == nil { t.Fatalf("Parse(%q) expected error, got nil (result: %v)", tt.input, result) } // Verify it's the right error type if !errors.Is(err, ErrInvalidFormat) { t.Errorf("Parse(%q) error = %v, want error wrapping ErrInvalidFormat", tt.input, err) } // Check for specific message substring if provided if tt.checkMsg != "" && !containsSubstring(err.Error(), tt.checkMsg) { t.Errorf("Parse(%q) error message %q does not contain %q", tt.input, err.Error(), tt.checkMsg) } } else { if err != nil { t.Fatalf("Parse(%q) unexpected error: %v", tt.input, err) } } }) } } func TestParseRFC2822(t *testing.T) { tests := []struct { name string input string expected time.Time }{ { "RFC2822: basic", "Mon, 09 Feb 2026 00:00:00 +0000", time.Date(2026, 2, 9, 0, 0, 0, 0, time.UTC), }, { "RFC2822: with time", "Mon, 09 Feb 2026 15:30:45 +0000", time.Date(2026, 2, 9, 15, 30, 45, 0, time.UTC), }, { "RFC1123: different month", "Fri, 31 Dec 2024 23:59:59 GMT", time.Date(2024, 12, 31, 23, 59, 59, 0, time.UTC), }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { result, err := Parse(tt.input) if err != nil { t.Fatalf("Parse(%q) unexpected error: %v", tt.input, err) } if !result.Time().Equal(tt.expected) { t.Errorf("Parse(%q) = %v, want %v", tt.input, result.Time(), tt.expected) } }) } } func TestParseImmutability(t *testing.T) { input := "2026-02-09" // Parse twice date1, err1 := Parse(input) date2, err2 := Parse(input) if err1 != nil || err2 != nil { t.Fatalf("Parse(%q) unexpected errors: %v, %v", input, err1, err2) } // Verify they're equal if !date1.Time().Equal(date2.Time()) { t.Errorf("Parse(%q) produced different results: %v vs %v", input, date1, date2) } // Modify date1 by adding time modified := date1.Add(1, Days) // Verify original is unchanged if !date1.Time().Equal(date2.Time()) { t.Errorf("Modifying result of Parse affected original date") } // Verify modification worked expected := date1.Time().AddDate(0, 0, 1) if !modified.Time().Equal(expected) { t.Errorf("Add operation failed: got %v, want %v", modified.Time(), expected) } } func TestParseWhitespace(t *testing.T) { tests := []struct { name string input string expected time.Time }{ {"leading whitespace", " 2026-02-09", time.Date(2026, 2, 9, 0, 0, 0, 0, time.UTC)}, {"trailing whitespace", "2026-02-09 ", time.Date(2026, 2, 9, 0, 0, 0, 0, time.UTC)}, {"both whitespace", " 2026-02-09 ", time.Date(2026, 2, 9, 0, 0, 0, 0, time.UTC)}, {"tab whitespace", "\t2026-02-09\t", time.Date(2026, 2, 9, 0, 0, 0, 0, time.UTC)}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { result, err := Parse(tt.input) if err != nil { t.Fatalf("Parse(%q) unexpected error: %v", tt.input, err) } if !result.Time().Equal(tt.expected) { t.Errorf("Parse(%q) = %v, want %v", tt.input, result.Time(), tt.expected) } }) } } // BenchmarkParse benchmarks the Parse function with different formats func BenchmarkParse(b *testing.B) { benchmarks := []struct { name string input string }{ {"ISO format", "2026-02-09"}, {"ISO slash", "2026/02/09"}, {"EU format", "09.02.2026"}, {"RFC2822", "Mon, 09 Feb 2026 00:00:00 +0000"}, } for _, bm := range benchmarks { b.Run(bm.name, func(b *testing.B) { b.ReportAllocs() for i := 0; i < b.N; i++ { _, err := Parse(bm.input) if err != nil { b.Fatalf("Parse failed: %v", err) } } }) } } // BenchmarkParseError benchmarks error case (ambiguous format) func BenchmarkParseError(b *testing.B) { input := "01/02/2026" // Ambiguous format b.ReportAllocs() for i := 0; i < b.N; i++ { _, err := Parse(input) if err == nil { b.Fatal("Expected error for ambiguous format") } } } func BenchmarkParseWithLayout(b *testing.B) { layout := "02/01/2006" input := "09/02/2026" b.ResetTimer() for i := 0; i < b.N; i++ { _, _ = ParseWithLayout(input, layout) } } func BenchmarkParseWithLayoutCustom(b *testing.B) { layout := "2. January 2006" input := "9. February 2026" b.ResetTimer() for i := 0; i < b.N; i++ { _, _ = ParseWithLayout(input, layout) } } // containsSubstring is a helper function to check if a string contains a substring func containsSubstring(s, substr string) bool { return len(substr) == 0 || len(s) >= len(substr) && (s == substr || len(s) > len(substr) && containsSubstringHelper(s, substr)) } func containsSubstringHelper(s, substr string) bool { for i := 0; i <= len(s)-len(substr); i++ { if s[i:i+len(substr)] == substr { return true } } return false } func TestParseWithLayout(t *testing.T) { tests := []struct { name string input string layout string expectYear int expectMonth time.Month expectDay int }{ // Disambiguating US vs EU slash formats { name: "US format 01/02/2026 -> Jan 2", input: "01/02/2026", layout: "01/02/2006", expectYear: 2026, expectMonth: time.January, expectDay: 2, }, { name: "EU format 01/02/2026 -> Feb 1", input: "01/02/2026", layout: "02/01/2006", expectYear: 2026, expectMonth: time.February, expectDay: 1, }, { name: "EU format 31/12/2025", input: "31/12/2025", layout: "02/01/2006", expectYear: 2025, expectMonth: time.December, expectDay: 31, }, // Custom formats { name: "Custom format with English month name", input: "9. February 2026", layout: "2. January 2006", expectYear: 2026, expectMonth: time.February, expectDay: 9, }, { name: "Custom format with short month", input: "15-Mar-2026", layout: "02-Jan-2006", expectYear: 2026, expectMonth: time.March, expectDay: 15, }, // ISO 8601 with time { name: "ISO 8601 with time", input: "2026-02-09T14:30:00", layout: "2006-01-02T15:04:05", expectYear: 2026, expectMonth: time.February, expectDay: 9, }, // Different separators { name: "Dash format MM-DD-YYYY", input: "02-09-2026", layout: "01-02-2006", expectYear: 2026, expectMonth: time.February, expectDay: 9, }, { name: "Space separator", input: "09 02 2026", layout: "02 01 2006", expectYear: 2026, expectMonth: time.February, expectDay: 9, }, // Whitespace handling { name: "Leading whitespace", input: " 09.02.2026", layout: "02.01.2006", expectYear: 2026, expectMonth: time.February, expectDay: 9, }, { name: "Trailing whitespace", input: "09.02.2026 ", layout: "02.01.2006", expectYear: 2026, expectMonth: time.February, expectDay: 9, }, // Edge cases { name: "Leap year Feb 29", input: "29/02/2024", layout: "02/01/2006", expectYear: 2024, expectMonth: time.February, expectDay: 29, }, { name: "Year boundary", input: "01/01/2026", layout: "02/01/2006", expectYear: 2026, expectMonth: time.January, expectDay: 1, }, { name: "Year end", input: "31/12/2026", layout: "02/01/2006", expectYear: 2026, expectMonth: time.December, expectDay: 31, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { date, err := ParseWithLayout(tt.input, tt.layout) if err != nil { t.Fatalf("ParseWithLayout(%q, %q) unexpected error: %v", tt.input, tt.layout, err) } tm := date.Time() if tm.Year() != tt.expectYear { t.Errorf("Year = %d, want %d", tm.Year(), tt.expectYear) } if tm.Month() != tt.expectMonth { t.Errorf("Month = %v, want %v", tm.Month(), tt.expectMonth) } if tm.Day() != tt.expectDay { t.Errorf("Day = %d, want %d", tm.Day(), tt.expectDay) } // Verify default language is set if date.lang != EN { t.Errorf("lang = %v, want EN", date.lang) } }) } } func TestParseWithLayoutErrors(t *testing.T) { tests := []struct { name string input string layout string }{ { name: "Empty input", input: "", layout: "02/01/2006", }, { name: "Whitespace only", input: " ", layout: "02/01/2006", }, { name: "Invalid date for layout", input: "99/99/2026", layout: "02/01/2006", }, { name: "Wrong layout for input", input: "2026-02-09", layout: "02/01/2006", }, { name: "Invalid month", input: "15/13/2026", layout: "02/01/2006", }, { name: "Invalid day", input: "32/01/2026", layout: "02/01/2006", }, { name: "Feb 30 (invalid)", input: "30/02/2026", layout: "02/01/2006", }, { name: "Feb 29 on non-leap year", input: "29/02/2026", layout: "02/01/2006", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { _, err := ParseWithLayout(tt.input, tt.layout) if err == nil { t.Errorf("ParseWithLayout(%q, %q) expected error, got nil", tt.input, tt.layout) } // Verify error wraps ErrInvalidFormat if !errors.Is(err, ErrInvalidFormat) { t.Errorf("error should wrap ErrInvalidFormat, got: %v", err) } }) } } func TestParseWithLayoutImmutability(t *testing.T) { // Parse the same date twice with the same layout date1, err1 := ParseWithLayout("01/02/2026", "01/02/2006") if err1 != nil { t.Fatalf("ParseWithLayout failed: %v", err1) } date2, err2 := ParseWithLayout("01/02/2026", "01/02/2006") if err2 != nil { t.Fatalf("ParseWithLayout failed: %v", err2) } // Modify date1 modified := date1.Add(5, Days) // Verify date2 is unchanged if date2.Unix() != date1.Unix() { t.Error("date2 should not be affected by operations on date1") } if modified.Unix() == date1.Unix() { t.Error("Add should return a new Date instance") } }