package reports import ( "fmt" "regexp" "strconv" "strings" "time" ) type DateRange struct { Start time.Time 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() // Check for predefined ranges (case-insensitive) lowerDateStr := strings.ToLower(dateStr) 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 "since YYYY-MM-DD" format if strings.HasPrefix(lowerDateStr, "since ") { return parseSinceDateRange(dateStr) } // Check for custom date range format: "YYYY-MM-DD to YYYY-MM-DD" if strings.Contains(lowerDateStr, " to ") { return parseCustomDateRange(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', 'since YYYY-MM-DD', or 'YYYY-MM-DD to YYYY-MM-DD')", dateStr) } func parseCustomDateRange(dateStr string) (DateRange, error) { lowerDateStr := strings.ToLower(dateStr) parts := strings.Split(lowerDateStr, " to ") if len(parts) != 2 { return DateRange{}, fmt.Errorf("invalid date range format: expected 'YYYY-MM-DD to YYYY-MM-DD'") } startStr := strings.TrimSpace(parts[0]) endStr := strings.TrimSpace(parts[1]) // Parse start date startDate, err := time.Parse("2006-01-02", startStr) if err != nil { return DateRange{}, fmt.Errorf("invalid start date '%s': expected YYYY-MM-DD format", startStr) } // Parse end date endDate, err := time.Parse("2006-01-02", endStr) if err != nil { return DateRange{}, fmt.Errorf("invalid end date '%s': expected YYYY-MM-DD format", endStr) } // Validate that start date is before or equal to end date if startDate.After(endDate) { return DateRange{}, fmt.Errorf("start date '%s' must be before or equal to end date '%s'", startStr, endStr) } // 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) return DateRange{ Start: startUTC, End: endUTC, }, nil } func getLastWeek(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 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, } } 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, } } 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, } } func parseSinceDateRange(dateStr string) (DateRange, error) { // Remove "since " prefix (case-insensitive) lowerDateStr := strings.ToLower(dateStr) if !strings.HasPrefix(lowerDateStr, "since ") { return DateRange{}, fmt.Errorf("invalid since format: expected 'since YYYY-MM-DD'") } dateOnly := strings.TrimSpace(dateStr[6:]) // Remove "since " prefix // Parse start date startDate, err := time.Parse("2006-01-02", dateOnly) if err != nil { return DateRange{}, fmt.Errorf("invalid start date '%s' in since format: expected YYYY-MM-DD format", dateOnly) } // Convert to UTC and set start time to beginning of day (00:00:00) startUTC := time.Date(startDate.Year(), startDate.Month(), startDate.Day(), 0, 0, 0, 0, time.UTC) // No end date for "since" format - return zero time for End to indicate open-ended return DateRange{ Start: startUTC, End: time.Time{}, // Zero time indicates no end date }, nil }