quando/parse_test.go

267 lines
7.7 KiB
Go
Raw Normal View History

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")
}
}
}
// 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
}