package timeparsing import ( "testing" "time" ) func TestParseCompactDuration(t *testing.T) { // Fixed reference time for deterministic tests now := time.Date(2025, 6, 15, 12, 0, 0, 0, time.UTC) tests := []struct { name string input string want time.Time wantErr bool }{ // Valid positive durations { name: "+6h adds 6 hours", input: "+6h", want: time.Date(2025, 6, 15, 18, 0, 0, 0, time.UTC), }, { name: "+1d adds 1 day", input: "+1d", want: time.Date(2025, 6, 16, 12, 0, 0, 0, time.UTC), }, { name: "+2w adds 2 weeks", input: "+2w", want: time.Date(2025, 6, 29, 12, 0, 0, 0, time.UTC), }, { name: "+3m adds 3 months", input: "+3m", want: time.Date(2025, 9, 15, 12, 0, 0, 0, time.UTC), }, { name: "+1y adds 1 year", input: "+1y", want: time.Date(2026, 6, 15, 12, 0, 0, 0, time.UTC), }, // Valid negative durations (past) { name: "-1d subtracts 1 day", input: "-1d", want: time.Date(2025, 6, 14, 12, 0, 0, 0, time.UTC), }, { name: "-2w subtracts 2 weeks", input: "-2w", want: time.Date(2025, 6, 1, 12, 0, 0, 0, time.UTC), }, { name: "-6h subtracts 6 hours", input: "-6h", want: time.Date(2025, 6, 15, 6, 0, 0, 0, time.UTC), }, // No sign means positive { name: "3m without sign adds 3 months", input: "3m", want: time.Date(2025, 9, 15, 12, 0, 0, 0, time.UTC), }, { name: "1y without sign adds 1 year", input: "1y", want: time.Date(2026, 6, 15, 12, 0, 0, 0, time.UTC), }, { name: "6h without sign adds 6 hours", input: "6h", want: time.Date(2025, 6, 15, 18, 0, 0, 0, time.UTC), }, // Multi-digit amounts { name: "+24h adds 24 hours", input: "+24h", want: time.Date(2025, 6, 16, 12, 0, 0, 0, time.UTC), }, { name: "+365d adds 365 days", input: "+365d", want: time.Date(2026, 6, 15, 12, 0, 0, 0, time.UTC), }, // Invalid inputs { name: "6h+ (sign at end) is invalid", input: "6h+", wantErr: true, }, { name: "++1d (double sign) is invalid", input: "++1d", wantErr: true, }, { name: "1x (unknown unit) is invalid", input: "1x", wantErr: true, }, { name: "empty string is invalid", input: "", wantErr: true, }, { name: "just a number is invalid", input: "6", wantErr: true, }, { name: "just a unit is invalid", input: "h", wantErr: true, }, { name: "spaces are invalid", input: "+ 6h", wantErr: true, }, { name: "ISO date is not compact duration", input: "2025-01-15", wantErr: true, }, { name: "natural language is not compact duration", input: "tomorrow", wantErr: true, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { got, err := ParseCompactDuration(tt.input, now) if (err != nil) != tt.wantErr { t.Errorf("ParseCompactDuration(%q) error = %v, wantErr %v", tt.input, err, tt.wantErr) return } if !tt.wantErr && !got.Equal(tt.want) { t.Errorf("ParseCompactDuration(%q) = %v, want %v", tt.input, got, tt.want) } }) } } func TestIsCompactDuration(t *testing.T) { tests := []struct { input string want bool }{ {"+6h", true}, {"-1d", true}, {"+2w", true}, {"3m", true}, {"1y", true}, {"+24h", true}, {"", false}, {"tomorrow", false}, {"2025-01-15", false}, {"6h+", false}, {"++1d", false}, {"1x", false}, } for _, tt := range tests { t.Run(tt.input, func(t *testing.T) { got := IsCompactDuration(tt.input) if got != tt.want { t.Errorf("IsCompactDuration(%q) = %v, want %v", tt.input, got, tt.want) } }) } } // TestParseCompactDuration_MonthBoundary tests month arithmetic edge cases. func TestParseCompactDuration_MonthBoundary(t *testing.T) { // Jan 31 + 1 month = Feb 28 (or 29 in leap year) jan31 := time.Date(2025, 1, 31, 12, 0, 0, 0, time.UTC) got, err := ParseCompactDuration("+1m", jan31) if err != nil { t.Fatalf("unexpected error: %v", err) } // Go's AddDate normalizes: Jan 31 + 1 month = March 3 (31 days into Feb) // This is Go's default behavior, which we preserve if got.Month() != time.March { t.Logf("Note: Jan 31 + 1m = %v (Go's AddDate overflow behavior)", got) } } // TestParseCompactDuration_LeapYear tests leap year handling. func TestParseCompactDuration_LeapYear(t *testing.T) { // Feb 28, 2024 (leap year) + 1d = Feb 29 feb28_2024 := time.Date(2024, 2, 28, 12, 0, 0, 0, time.UTC) got, err := ParseCompactDuration("+1d", feb28_2024) if err != nil { t.Fatalf("unexpected error: %v", err) } want := time.Date(2024, 2, 29, 12, 0, 0, 0, time.UTC) if !got.Equal(want) { t.Errorf("Feb 28, 2024 + 1d = %v, want %v", got, want) } } // TestParseCompactDuration_PreservesTimezone tests that local timezone is preserved. func TestParseCompactDuration_PreservesTimezone(t *testing.T) { loc, err := time.LoadLocation("America/New_York") if err != nil { t.Skip("timezone America/New_York not available") } now := time.Date(2025, 6, 15, 12, 0, 0, 0, loc) got, err := ParseCompactDuration("+1d", now) if err != nil { t.Fatalf("unexpected error: %v", err) } if got.Location() != loc { t.Errorf("timezone not preserved: got %v, want %v", got.Location(), loc) } }