cd476cfc99
- Add month name parsing (jan, january, feb, etc.) with year logic - Add day+month parsing (21jan, Jan21, 21January, etc.) in all case variations - Add period boundaries (sod, eod, sow, eow, som, eom, soy, eoy) - Add special keywords (later, someday -> 2150-01-01) - Add duration-as-date offset (2d, 3w, etc. means X from now) - Add named duration aliases (daily, weekly, monthly, yearly) - Enhance ParseRecurrencePattern to support min, sec, hrs and word forms - Implement time-of-day parsing (mon:15:35, tomorrow:0800, 15:35) - Add comprehensive test suite (50+ tests total) - All tests passing
261 lines
7.8 KiB
Go
261 lines
7.8 KiB
Go
package engine
|
|
|
|
import (
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
func TestParseMonthName(t *testing.T) {
|
|
// Base: Jan 5, 2026
|
|
base := time.Date(2026, 1, 5, 12, 0, 0, 0, time.UTC)
|
|
parser := NewDateParser(base, time.Monday)
|
|
|
|
tests := []struct {
|
|
name string
|
|
input string
|
|
expected time.Time
|
|
}{
|
|
{"jan (passed)", "jan", time.Date(2027, 1, 1, 0, 0, 0, 0, time.UTC)},
|
|
{"january (passed)", "january", time.Date(2027, 1, 1, 0, 0, 0, 0, time.UTC)},
|
|
{"feb (future)", "feb", time.Date(2026, 2, 1, 0, 0, 0, 0, time.UTC)},
|
|
{"february (future)", "february", time.Date(2026, 2, 1, 0, 0, 0, 0, time.UTC)},
|
|
{"mar", "mar", time.Date(2026, 3, 1, 0, 0, 0, 0, time.UTC)},
|
|
{"march", "march", time.Date(2026, 3, 1, 0, 0, 0, 0, time.UTC)},
|
|
{"dec", "dec", time.Date(2026, 12, 1, 0, 0, 0, 0, time.UTC)},
|
|
{"december", "december", time.Date(2026, 12, 1, 0, 0, 0, 0, time.UTC)},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
result, ok := parser.parseMonthName(tt.input)
|
|
if !ok {
|
|
t.Fatalf("Failed to parse month name: %s", tt.input)
|
|
}
|
|
if !result.Equal(tt.expected) {
|
|
t.Errorf("Expected %v, got %v", tt.expected, result)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestParseDayMonth(t *testing.T) {
|
|
// Base: Jan 5, 2026
|
|
base := time.Date(2026, 1, 5, 12, 0, 0, 0, time.UTC)
|
|
parser := NewDateParser(base, time.Monday)
|
|
|
|
tests := []struct {
|
|
name string
|
|
input string
|
|
expected time.Time
|
|
}{
|
|
{"21jan (future this year)", "21jan", time.Date(2026, 1, 21, 0, 0, 0, 0, time.UTC)},
|
|
{"21Jan", "21Jan", time.Date(2026, 1, 21, 0, 0, 0, 0, time.UTC)},
|
|
{"Jan21", "Jan21", time.Date(2026, 1, 21, 0, 0, 0, 0, time.UTC)},
|
|
{"jan21", "jan21", time.Date(2026, 1, 21, 0, 0, 0, 0, time.UTC)},
|
|
{"1jan (passed)", "1jan", time.Date(2027, 1, 1, 0, 0, 0, 0, time.UTC)},
|
|
{"30dec", "30dec", time.Date(2026, 12, 30, 0, 0, 0, 0, time.UTC)},
|
|
{"15feb", "15feb", time.Date(2026, 2, 15, 0, 0, 0, 0, time.UTC)},
|
|
{"15February", "15February", time.Date(2026, 2, 15, 0, 0, 0, 0, time.UTC)},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
result, ok := parser.parseDayMonth(tt.input)
|
|
if !ok {
|
|
t.Fatalf("Failed to parse day+month: %s", tt.input)
|
|
}
|
|
if !result.Equal(tt.expected) {
|
|
t.Errorf("Expected %v, got %v", tt.expected, result)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestParsePeriodBoundary(t *testing.T) {
|
|
// Base: Monday, Jan 5, 2026, 14:30:00
|
|
base := time.Date(2026, 1, 5, 14, 30, 0, 0, time.UTC)
|
|
parser := NewDateParser(base, time.Monday)
|
|
|
|
tests := []struct {
|
|
name string
|
|
input string
|
|
expected time.Time
|
|
}{
|
|
{"sod", "sod", time.Date(2026, 1, 5, 0, 0, 0, 0, time.UTC)},
|
|
{"eod", "eod", time.Date(2026, 1, 5, 23, 59, 59, 0, time.UTC)},
|
|
{"sow", "sow", time.Date(2026, 1, 12, 0, 0, 0, 0, time.UTC)}, // Next Monday
|
|
{"eow", "eow", time.Date(2026, 1, 11, 23, 59, 59, 0, time.UTC)}, // Next Sunday
|
|
{"som", "som", time.Date(2026, 2, 1, 0, 0, 0, 0, time.UTC)}, // Next month (Jan 1 passed)
|
|
{"eom", "eom", time.Date(2026, 1, 31, 23, 59, 59, 0, time.UTC)}, // End of Jan
|
|
{"soy", "soy", time.Date(2027, 1, 1, 0, 0, 0, 0, time.UTC)}, // Next year (Jan 1 passed)
|
|
{"eoy", "eoy", time.Date(2026, 12, 31, 23, 59, 59, 0, time.UTC)}, // End of 2026
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
result, ok := parser.parsePeriodBoundary(tt.input)
|
|
if !ok {
|
|
t.Fatalf("Failed to parse period boundary: %s", tt.input)
|
|
}
|
|
if !result.Equal(tt.expected) {
|
|
t.Errorf("Expected %v, got %v", tt.expected, result)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestParseSpecialKeyword(t *testing.T) {
|
|
base := time.Date(2026, 1, 5, 12, 0, 0, 0, time.UTC)
|
|
parser := NewDateParser(base, time.Monday)
|
|
|
|
tests := []struct {
|
|
name string
|
|
input string
|
|
expected time.Time
|
|
}{
|
|
{"later", "later", time.Date(2150, 1, 1, 0, 0, 0, 0, time.UTC)},
|
|
{"someday", "someday", time.Date(2150, 1, 1, 0, 0, 0, 0, time.UTC)},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
result, ok := parser.parseSpecialKeyword(tt.input)
|
|
if !ok {
|
|
t.Fatalf("Failed to parse special keyword: %s", tt.input)
|
|
}
|
|
if !result.Equal(tt.expected) {
|
|
t.Errorf("Expected %v, got %v", tt.expected, result)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestParseDurationAsDate(t *testing.T) {
|
|
base := time.Date(2026, 1, 5, 12, 0, 0, 0, time.UTC)
|
|
parser := NewDateParser(base, time.Monday)
|
|
|
|
tests := []struct {
|
|
name string
|
|
input string
|
|
expected time.Time
|
|
}{
|
|
{"2d", "2d", base.AddDate(0, 0, 2)},
|
|
{"3w", "3w", base.AddDate(0, 0, 21)},
|
|
{"1m", "1m", base.AddDate(0, 0, 30)},
|
|
{"1y", "1y", base.AddDate(0, 0, 365)},
|
|
{"daily", "daily", base.AddDate(0, 0, 1)},
|
|
{"weekly", "weekly", base.AddDate(0, 0, 7)},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
result, ok := parser.parseDurationAsDate(tt.input)
|
|
if !ok {
|
|
t.Fatalf("Failed to parse duration as date: %s", tt.input)
|
|
}
|
|
if !result.Equal(tt.expected) {
|
|
t.Errorf("Expected %v, got %v", tt.expected, result)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestParseTimeOfDay(t *testing.T) {
|
|
base := time.Date(2026, 1, 5, 12, 0, 0, 0, time.UTC)
|
|
parser := NewDateParser(base, time.Monday)
|
|
|
|
tests := []struct {
|
|
name string
|
|
date time.Time
|
|
timeStr string
|
|
expected time.Time
|
|
wantErr bool
|
|
}{
|
|
{"HH:MM format", base, "15:35", time.Date(2026, 1, 5, 15, 35, 0, 0, time.UTC), false},
|
|
{"HHMM format", base, "0800", time.Date(2026, 1, 5, 8, 0, 0, 0, time.UTC), false},
|
|
{"midnight", base, "00:00", time.Date(2026, 1, 5, 0, 0, 0, 0, time.UTC), false},
|
|
{"end of day", base, "23:59", time.Date(2026, 1, 5, 23, 59, 0, 0, time.UTC), false},
|
|
{"invalid hour", base, "25:00", time.Time{}, true},
|
|
{"invalid minute", base, "12:65", time.Time{}, true},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
result, err := parser.parseTimeOfDay(tt.date, tt.timeStr)
|
|
if tt.wantErr {
|
|
if err == nil {
|
|
t.Error("Expected error but got none")
|
|
}
|
|
return
|
|
}
|
|
if err != nil {
|
|
t.Fatalf("Unexpected error: %v", err)
|
|
}
|
|
if !result.Equal(tt.expected) {
|
|
t.Errorf("Expected %v, got %v", tt.expected, result)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestParseDateWithTime(t *testing.T) {
|
|
base := time.Date(2026, 1, 5, 12, 0, 0, 0, time.UTC) // Monday
|
|
parser := NewDateParser(base, time.Monday)
|
|
|
|
tests := []struct {
|
|
name string
|
|
input string
|
|
expected time.Time
|
|
}{
|
|
{"mon:15:35", "mon:15:35", time.Date(2026, 1, 12, 15, 35, 0, 0, time.UTC)}, // Next Monday at 15:35
|
|
{"tomorrow:0800", "tomorrow:0800", time.Date(2026, 1, 6, 8, 0, 0, 0, time.UTC)},
|
|
{"21jan:1430", "21jan:1430", time.Date(2026, 1, 21, 14, 30, 0, 0, time.UTC)},
|
|
{"just time 15:35", "15:35", time.Date(2026, 1, 5, 15, 35, 0, 0, time.UTC)}, // Today at 15:35
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
result, err := parser.ParseDate(tt.input)
|
|
if err != nil {
|
|
t.Fatalf("Failed to parse date with time: %v", err)
|
|
}
|
|
if !result.Equal(tt.expected) {
|
|
t.Errorf("Expected %v, got %v", tt.expected, result)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestExpandedDurationFormats(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
input string
|
|
expected time.Duration
|
|
}{
|
|
{"5sec", "5sec", 5 * time.Second},
|
|
{"5seconds", "5seconds", 5 * time.Second},
|
|
{"10min", "10min", 10 * time.Minute},
|
|
{"10minutes", "10minutes", 10 * time.Minute},
|
|
{"2hrs", "2hrs", 2 * time.Hour},
|
|
{"2hours", "2hours", 2 * time.Hour},
|
|
{"3days", "3days", 3 * 24 * time.Hour},
|
|
{"2weeks", "2weeks", 2 * 7 * 24 * time.Hour},
|
|
{"1month", "1month", 30 * 24 * time.Hour},
|
|
{"1year", "1year", 365 * 24 * time.Hour},
|
|
{"hour (no number)", "hour", time.Hour},
|
|
{"day (no number)", "day", 24 * time.Hour},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
result, err := ParseRecurrencePattern(tt.input)
|
|
if err != nil {
|
|
t.Fatalf("Failed to parse duration: %v", err)
|
|
}
|
|
if result != tt.expected {
|
|
t.Errorf("Expected %v, got %v", tt.expected, result)
|
|
}
|
|
})
|
|
}
|
|
}
|