time_parser.go 5.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199
  1. package parser
  2. import (
  3. "fmt"
  4. "regexp"
  5. "strconv"
  6. "strings"
  7. "time"
  8. )
  9. // ParseTime converts natural language or ISO 8601 timestamps to ISO 8601 format
  10. func ParseTime(input string) (string, error) {
  11. input = strings.TrimSpace(strings.ToLower(input))
  12. // Try parsing as ISO 8601 first
  13. formats := []string{
  14. time.RFC3339,
  15. "2006-01-02T15:04:05Z",
  16. "2006-01-02 15:04:05",
  17. "2006-01-02 15:04",
  18. "2006-01-02T15:04",
  19. }
  20. for _, format := range formats {
  21. if t, err := time.Parse(format, input); err == nil {
  22. return t.UTC().Format(time.RFC3339), nil
  23. }
  24. }
  25. now := time.Now().UTC()
  26. // "in X minutes/hours/days"
  27. if strings.HasPrefix(input, "in ") {
  28. return parseRelativeTime(input, now)
  29. }
  30. // "tomorrow at HH:MM"
  31. if strings.HasPrefix(input, "tomorrow") {
  32. return parseTomorrow(input, now)
  33. }
  34. // "next monday/tuesday/etc at HH:MM"
  35. if strings.HasPrefix(input, "next ") {
  36. return parseNextDay(input, now)
  37. }
  38. // "now"
  39. if input == "now" {
  40. return now.Format(time.RFC3339), nil
  41. }
  42. return "", fmt.Errorf("unable to parse time: %s\n\nSupported formats:\n - ISO 8601: 2025-11-12T19:30:00Z\n - Relative: in 5 minutes, in 2 hours, in 3 days\n - Tomorrow: tomorrow at 9am, tomorrow at 14:30\n - Next day: next monday at 3pm, next friday at 10:00\n - Now: now", input)
  43. }
  44. func parseRelativeTime(input string, now time.Time) (string, error) {
  45. // "in 5 minutes", "in 2 hours", "in 3 days"
  46. re := regexp.MustCompile(`^in (\d+)\s*(minute|minutes|min|hour|hours|hr|hrs|h|day|days|d)s?$`)
  47. matches := re.FindStringSubmatch(input)
  48. if len(matches) != 3 {
  49. return "", fmt.Errorf("invalid relative time format: %s", input)
  50. }
  51. value, _ := strconv.Atoi(matches[1])
  52. unit := matches[2]
  53. var t time.Time
  54. switch {
  55. case strings.HasPrefix(unit, "min"):
  56. t = now.Add(time.Duration(value) * time.Minute)
  57. case strings.HasPrefix(unit, "h"):
  58. t = now.Add(time.Duration(value) * time.Hour)
  59. case strings.HasPrefix(unit, "d"):
  60. t = now.AddDate(0, 0, value)
  61. default:
  62. return "", fmt.Errorf("unknown time unit: %s", unit)
  63. }
  64. return t.Format(time.RFC3339), nil
  65. }
  66. func parseTomorrow(input string, now time.Time) (string, error) {
  67. // "tomorrow" or "tomorrow at 9am" or "tomorrow at 14:30"
  68. tomorrow := now.AddDate(0, 0, 1)
  69. if input == "tomorrow" {
  70. // Default to 9am tomorrow
  71. tomorrow = time.Date(tomorrow.Year(), tomorrow.Month(), tomorrow.Day(), 9, 0, 0, 0, time.UTC)
  72. return tomorrow.Format(time.RFC3339), nil
  73. }
  74. // Parse "tomorrow at HH:MM" or "tomorrow at 9am"
  75. atIndex := strings.Index(input, " at ")
  76. if atIndex == -1 {
  77. return "", fmt.Errorf("expected 'at' in: %s", input)
  78. }
  79. timeStr := strings.TrimSpace(input[atIndex+4:])
  80. hour, minute, err := parseTimeOfDay(timeStr)
  81. if err != nil {
  82. return "", err
  83. }
  84. t := time.Date(tomorrow.Year(), tomorrow.Month(), tomorrow.Day(), hour, minute, 0, 0, time.UTC)
  85. return t.Format(time.RFC3339), nil
  86. }
  87. func parseNextDay(input string, now time.Time) (string, error) {
  88. // "next monday at 3pm", "next friday at 10:00"
  89. re := regexp.MustCompile(`^next\s+(monday|tuesday|wednesday|thursday|friday|saturday|sunday)\s+at\s+(.+)$`)
  90. matches := re.FindStringSubmatch(input)
  91. if len(matches) != 3 {
  92. return "", fmt.Errorf("expected format 'next DAY at TIME': %s", input)
  93. }
  94. dayName := matches[1]
  95. timeStr := matches[2]
  96. // Find target weekday
  97. targetWeekday := parseWeekday(dayName)
  98. currentWeekday := now.Weekday()
  99. daysUntil := int(targetWeekday - currentWeekday)
  100. if daysUntil <= 0 {
  101. daysUntil += 7
  102. }
  103. targetDate := now.AddDate(0, 0, daysUntil)
  104. hour, minute, err := parseTimeOfDay(timeStr)
  105. if err != nil {
  106. return "", err
  107. }
  108. t := time.Date(targetDate.Year(), targetDate.Month(), targetDate.Day(), hour, minute, 0, 0, time.UTC)
  109. return t.Format(time.RFC3339), nil
  110. }
  111. func parseTimeOfDay(input string) (hour int, minute int, err error) {
  112. input = strings.TrimSpace(strings.ToLower(input))
  113. // Handle special cases
  114. switch input {
  115. case "noon", "12pm":
  116. return 12, 0, nil
  117. case "midnight", "12am":
  118. return 0, 0, nil
  119. }
  120. // Parse "3pm", "9am"
  121. re := regexp.MustCompile(`^(\d+)(am|pm)$`)
  122. matches := re.FindStringSubmatch(input)
  123. if len(matches) == 3 {
  124. h, _ := strconv.Atoi(matches[1])
  125. if matches[2] == "pm" && h != 12 {
  126. h += 12
  127. }
  128. if matches[2] == "am" && h == 12 {
  129. h = 0
  130. }
  131. return h, 0, nil
  132. }
  133. // Parse "14:30", "9:15"
  134. re = regexp.MustCompile(`^(\d+):(\d+)$`)
  135. matches = re.FindStringSubmatch(input)
  136. if len(matches) == 3 {
  137. h, _ := strconv.Atoi(matches[1])
  138. m, _ := strconv.Atoi(matches[2])
  139. if h > 23 || m > 59 {
  140. return 0, 0, fmt.Errorf("invalid time: %s", input)
  141. }
  142. return h, m, nil
  143. }
  144. return 0, 0, fmt.Errorf("unable to parse time of day: %s", input)
  145. }
  146. func parseWeekday(day string) time.Weekday {
  147. switch strings.ToLower(day) {
  148. case "sunday":
  149. return time.Sunday
  150. case "monday":
  151. return time.Monday
  152. case "tuesday":
  153. return time.Tuesday
  154. case "wednesday":
  155. return time.Wednesday
  156. case "thursday":
  157. return time.Thursday
  158. case "friday":
  159. return time.Friday
  160. case "saturday":
  161. return time.Saturday
  162. default:
  163. return time.Monday
  164. }
  165. }