diff options
Diffstat (limited to 'internal/reports')
-rw-r--r-- | internal/reports/daterange.go | 162 | ||||
-rw-r--r-- | internal/reports/daterange_test.go | 246 |
2 files changed, 398 insertions, 10 deletions
diff --git a/internal/reports/daterange.go b/internal/reports/daterange.go index 3478615..adeb48b 100644 --- a/internal/reports/daterange.go +++ b/internal/reports/daterange.go @@ -2,6 +2,8 @@ package reports import ( "fmt" + "regexp" + "strconv" "strings" "time" ) @@ -11,6 +13,33 @@ type DateRange struct { End time.Time } +var monthNames = map[string]time.Month{ + "january": time.January, + "february": time.February, + "march": time.March, + "april": time.April, + "may": time.May, + "june": time.June, + "july": time.July, + "august": time.August, + "september": time.September, + "october": time.October, + "november": time.November, + "december": time.December, + "jan": time.January, + "feb": time.February, + "mar": time.March, + "apr": time.April, + "jun": time.June, + "jul": time.July, + "aug": time.August, + "sep": time.September, + "sept": time.September, + "oct": time.October, + "nov": time.November, + "dec": time.December, +} + func ParseDateRange(dateStr string) (DateRange, error) { dateStr = strings.TrimSpace(dateStr) now := time.Now().UTC() @@ -20,8 +49,22 @@ func ParseDateRange(dateStr string) (DateRange, error) { switch lowerDateStr { case "last week": return getLastWeek(now), nil + case "this week": + return getThisWeek(now), nil case "last month": return getLastMonth(now), nil + case "this month": + return getThisMonth(now), nil + } + + // Check for month name patterns (case-insensitive) + if dateRange, err := parseMonthName(dateStr, now); err == nil { + return dateRange, nil + } + + // Check for "month year" format + if dateRange, err := parseMonthYear(dateStr); err == nil { + return dateRange, nil } // Check for custom date range format: "YYYY-MM-DD to YYYY-MM-DD" @@ -29,7 +72,7 @@ func ParseDateRange(dateStr string) (DateRange, error) { return parseCustomDateRange(dateStr) } - return DateRange{}, fmt.Errorf("unsupported date range: %s (supported: 'last week', 'last month', or 'YYYY-MM-DD to YYYY-MM-DD')", dateStr) + return DateRange{}, fmt.Errorf("unsupported date range: %s (supported: 'this week', 'this month', 'last week', 'last month', month names like 'february', 'month year' like 'july 2023', or 'YYYY-MM-DD to YYYY-MM-DD')", dateStr) } func parseCustomDateRange(dateStr string) (DateRange, error) { @@ -61,7 +104,7 @@ func parseCustomDateRange(dateStr string) (DateRange, error) { // Convert to UTC and set times appropriately // Start date: beginning of day (00:00:00) startUTC := time.Date(startDate.Year(), startDate.Month(), startDate.Day(), 0, 0, 0, 0, time.UTC) - + // End date: end of day (23:59:59.999999999) endUTC := time.Date(endDate.Year(), endDate.Month(), endDate.Day(), 23, 59, 59, 999999999, time.UTC) @@ -77,14 +120,14 @@ func getLastWeek(now time.Time) DateRange { if weekday == 0 { // Sunday weekday = 7 } - + // Start of current week - currentWeekStart := now.AddDate(0, 0, -(weekday-1)).Truncate(24 * time.Hour) - + currentWeekStart := now.AddDate(0, 0, -(weekday - 1)).Truncate(24 * time.Hour) + // Last week is the week before current week lastWeekStart := currentWeekStart.AddDate(0, 0, -7) lastWeekEnd := currentWeekStart.Add(-time.Nanosecond) - + return DateRange{ Start: lastWeekStart, End: lastWeekEnd, @@ -94,15 +137,114 @@ func getLastWeek(now time.Time) DateRange { func getLastMonth(now time.Time) DateRange { // Start of current month currentMonthStart := time.Date(now.Year(), now.Month(), 1, 0, 0, 0, 0, time.UTC) - + // Last month start lastMonthStart := currentMonthStart.AddDate(0, -1, 0) - + // Last month end (last nanosecond of last month) lastMonthEnd := currentMonthStart.Add(-time.Nanosecond) - + return DateRange{ Start: lastMonthStart, End: lastMonthEnd, } -}
\ No newline at end of file +} + +func getThisWeek(now time.Time) DateRange { + // Find the start of current week (Monday) + weekday := int(now.Weekday()) + if weekday == 0 { // Sunday + weekday = 7 + } + + // Start of current week + weekStart := now.AddDate(0, 0, -(weekday - 1)).Truncate(24 * time.Hour) + + // End of current week (Sunday 23:59:59.999999999) + weekEnd := weekStart.AddDate(0, 0, 7).Add(-time.Nanosecond) + + return DateRange{ + Start: weekStart, + End: weekEnd, + } +} + +func getThisMonth(now time.Time) DateRange { + // Start of current month + monthStart := time.Date(now.Year(), now.Month(), 1, 0, 0, 0, 0, time.UTC) + + // End of current month (last nanosecond) + nextMonthStart := monthStart.AddDate(0, 1, 0) + monthEnd := nextMonthStart.Add(-time.Nanosecond) + + return DateRange{ + Start: monthStart, + End: monthEnd, + } +} + +func parseMonthName(dateStr string, now time.Time) (DateRange, error) { + lowerDateStr := strings.ToLower(dateStr) + month, exists := monthNames[lowerDateStr] + if !exists { + return DateRange{}, fmt.Errorf("not a valid month name") + } + + // Find the most recent occurrence of this month + var targetYear int + currentMonth := now.Month() + + if month == currentMonth { + // If it's the same month as now, use this year + targetYear = now.Year() + } else if month < currentMonth { + // If the month has already passed this year, use this year + targetYear = now.Year() + } else { + // If the month hasn't come yet this year, use last year + targetYear = now.Year() - 1 + } + + return getMonthRange(targetYear, month), nil +} + +func parseMonthYear(dateStr string) (DateRange, error) { + // Match patterns like "july 2023", "feb 2022", etc. + re := regexp.MustCompile(`^(\w+)\s+(\d{4})$`) + matches := re.FindStringSubmatch(strings.ToLower(dateStr)) + if len(matches) != 3 { + return DateRange{}, fmt.Errorf("invalid month year format") + } + + monthStr := matches[1] + yearStr := matches[2] + + // Parse year + year, err := strconv.Atoi(yearStr) + if err != nil { + return DateRange{}, fmt.Errorf("invalid year: %s", yearStr) + } + + // Parse month + month, exists := monthNames[monthStr] + if !exists { + return DateRange{}, fmt.Errorf("invalid month name: %s", monthStr) + } + + return getMonthRange(year, month), nil +} + +func getMonthRange(year int, month time.Month) DateRange { + // Start of the specified month + monthStart := time.Date(year, month, 1, 0, 0, 0, 0, time.UTC) + + // End of the specified month (last nanosecond) + nextMonthStart := monthStart.AddDate(0, 1, 0) + monthEnd := nextMonthStart.Add(-time.Nanosecond) + + return DateRange{ + Start: monthStart, + End: monthEnd, + } +} + diff --git a/internal/reports/daterange_test.go b/internal/reports/daterange_test.go new file mode 100644 index 0000000..69b0e4a --- /dev/null +++ b/internal/reports/daterange_test.go @@ -0,0 +1,246 @@ +package reports + +import ( + "fmt" + "strings" + "testing" + "time" +) + +func TestParseDateRange(t *testing.T) { + // Use a fixed time for testing + testTime := time.Date(2024, time.August, 15, 10, 30, 0, 0, time.UTC) // Thursday, August 15, 2024 + + tests := []struct { + name string + input string + wantErr bool + validate func(t *testing.T, dr DateRange) + }{ + { + name: "this week", + input: "this week", + wantErr: false, + validate: func(t *testing.T, dr DateRange) { + // Should be Monday Aug 12 to Sunday Aug 18 + expectedStart := time.Date(2024, time.August, 12, 0, 0, 0, 0, time.UTC) + expectedEnd := time.Date(2024, time.August, 18, 23, 59, 59, 999999999, time.UTC) + if !dr.Start.Equal(expectedStart) { + t.Errorf("Start = %v, want %v", dr.Start, expectedStart) + } + if !dr.End.Equal(expectedEnd) { + t.Errorf("End = %v, want %v", dr.End, expectedEnd) + } + }, + }, + { + name: "this month", + input: "this month", + wantErr: false, + validate: func(t *testing.T, dr DateRange) { + // Should be Aug 1 to Aug 31 + expectedStart := time.Date(2024, time.August, 1, 0, 0, 0, 0, time.UTC) + expectedEnd := time.Date(2024, time.August, 31, 23, 59, 59, 999999999, time.UTC) + if !dr.Start.Equal(expectedStart) { + t.Errorf("Start = %v, want %v", dr.Start, expectedStart) + } + if !dr.End.Equal(expectedEnd) { + t.Errorf("End = %v, want %v", dr.End, expectedEnd) + } + }, + }, + { + name: "month name - current month", + input: "august", + wantErr: false, + validate: func(t *testing.T, dr DateRange) { + // Should be August 2024 (current year since we're in August) + expectedStart := time.Date(2024, time.August, 1, 0, 0, 0, 0, time.UTC) + expectedEnd := time.Date(2024, time.August, 31, 23, 59, 59, 999999999, time.UTC) + if !dr.Start.Equal(expectedStart) { + t.Errorf("Start = %v, want %v", dr.Start, expectedStart) + } + if !dr.End.Equal(expectedEnd) { + t.Errorf("End = %v, want %v", dr.End, expectedEnd) + } + }, + }, + { + name: "month name - past month this year", + input: "july", + wantErr: false, + validate: func(t *testing.T, dr DateRange) { + // Should be July 2024 (current year since July already passed) + expectedStart := time.Date(2024, time.July, 1, 0, 0, 0, 0, time.UTC) + expectedEnd := time.Date(2024, time.July, 31, 23, 59, 59, 999999999, time.UTC) + if !dr.Start.Equal(expectedStart) { + t.Errorf("Start = %v, want %v", dr.Start, expectedStart) + } + if !dr.End.Equal(expectedEnd) { + t.Errorf("End = %v, want %v", dr.End, expectedEnd) + } + }, + }, + { + name: "month name - future month last year", + input: "december", + wantErr: false, + validate: func(t *testing.T, dr DateRange) { + // Should be December 2023 (last year since December hasn't come yet) + expectedStart := time.Date(2023, time.December, 1, 0, 0, 0, 0, time.UTC) + expectedEnd := time.Date(2023, time.December, 31, 23, 59, 59, 999999999, time.UTC) + if !dr.Start.Equal(expectedStart) { + t.Errorf("Start = %v, want %v", dr.Start, expectedStart) + } + if !dr.End.Equal(expectedEnd) { + t.Errorf("End = %v, want %v", dr.End, expectedEnd) + } + }, + }, + { + name: "case insensitive month name", + input: "FEBRUARY", + wantErr: false, + validate: func(t *testing.T, dr DateRange) { + // Should be February 2024 (current year since February already passed) + expectedStart := time.Date(2024, time.February, 1, 0, 0, 0, 0, time.UTC) + expectedEnd := time.Date(2024, time.February, 29, 23, 59, 59, 999999999, time.UTC) // 2024 is a leap year + if !dr.Start.Equal(expectedStart) { + t.Errorf("Start = %v, want %v", dr.Start, expectedStart) + } + if !dr.End.Equal(expectedEnd) { + t.Errorf("End = %v, want %v", dr.End, expectedEnd) + } + }, + }, + { + name: "short month name", + input: "feb", + wantErr: false, + validate: func(t *testing.T, dr DateRange) { + // Should be February 2024 + expectedStart := time.Date(2024, time.February, 1, 0, 0, 0, 0, time.UTC) + expectedEnd := time.Date(2024, time.February, 29, 23, 59, 59, 999999999, time.UTC) // 2024 is a leap year + if !dr.Start.Equal(expectedStart) { + t.Errorf("Start = %v, want %v", dr.Start, expectedStart) + } + if !dr.End.Equal(expectedEnd) { + t.Errorf("End = %v, want %v", dr.End, expectedEnd) + } + }, + }, + { + name: "month year format", + input: "july 2023", + wantErr: false, + validate: func(t *testing.T, dr DateRange) { + // Should be July 2023 + expectedStart := time.Date(2023, time.July, 1, 0, 0, 0, 0, time.UTC) + expectedEnd := time.Date(2023, time.July, 31, 23, 59, 59, 999999999, time.UTC) + if !dr.Start.Equal(expectedStart) { + t.Errorf("Start = %v, want %v", dr.Start, expectedStart) + } + if !dr.End.Equal(expectedEnd) { + t.Errorf("End = %v, want %v", dr.End, expectedEnd) + } + }, + }, + { + name: "short month year format", + input: "feb 2022", + wantErr: false, + validate: func(t *testing.T, dr DateRange) { + // Should be February 2022 + expectedStart := time.Date(2022, time.February, 1, 0, 0, 0, 0, time.UTC) + expectedEnd := time.Date(2022, time.February, 28, 23, 59, 59, 999999999, time.UTC) // 2022 is not a leap year + if !dr.Start.Equal(expectedStart) { + t.Errorf("Start = %v, want %v", dr.Start, expectedStart) + } + if !dr.End.Equal(expectedEnd) { + t.Errorf("End = %v, want %v", dr.End, expectedEnd) + } + }, + }, + { + name: "case insensitive month year", + input: "MARCH 2023", + wantErr: false, + validate: func(t *testing.T, dr DateRange) { + // Should be March 2023 + expectedStart := time.Date(2023, time.March, 1, 0, 0, 0, 0, time.UTC) + expectedEnd := time.Date(2023, time.March, 31, 23, 59, 59, 999999999, time.UTC) + if !dr.Start.Equal(expectedStart) { + t.Errorf("Start = %v, want %v", dr.Start, expectedStart) + } + if !dr.End.Equal(expectedEnd) { + t.Errorf("End = %v, want %v", dr.End, expectedEnd) + } + }, + }, + { + name: "invalid month name", + input: "invalid", + wantErr: true, + }, + { + name: "invalid month year format", + input: "july 23", + wantErr: true, + }, + { + name: "invalid year", + input: "july abcd", + wantErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Create a custom ParseDateRange that uses our test time + testParseFunc := func(dateStr string) (DateRange, error) { + dateStr = strings.TrimSpace(dateStr) + + // Check for predefined ranges (case-insensitive) + lowerDateStr := strings.ToLower(dateStr) + switch lowerDateStr { + case "last week": + return getLastWeek(testTime), nil + case "this week": + return getThisWeek(testTime), nil + case "last month": + return getLastMonth(testTime), nil + case "this month": + return getThisMonth(testTime), nil + } + + // Check for month name patterns (case-insensitive) + if dateRange, err := parseMonthName(dateStr, testTime); err == nil { + return dateRange, nil + } + + // Check for "month year" format + if dateRange, err := parseMonthYear(dateStr); err == nil { + return dateRange, nil + } + + // Check for custom date range format: "YYYY-MM-DD to YYYY-MM-DD" + if strings.Contains(dateStr, " to ") { + return parseCustomDateRange(dateStr) + } + + return DateRange{}, fmt.Errorf("unsupported date range: %s", dateStr) + } + + got, err := testParseFunc(tt.input) + if (err != nil) != tt.wantErr { + t.Errorf("ParseDateRange() error = %v, wantErr %v", err, tt.wantErr) + return + } + + if !tt.wantErr && tt.validate != nil { + tt.validate(t, got) + } + }) + } +} + |