github.com/sharovik/devbot@v1.0.1-0.20240308094637-4a0387c40516/internal/service/schedule/execute_at.go (about) 1 package schedule 2 3 import ( 4 "fmt" 5 "strconv" 6 "strings" 7 "time" 8 9 _time "github.com/sharovik/devbot/internal/service/time" 10 11 "github.com/sharovik/devbot/internal/helper" 12 ) 13 14 const ( 15 //DatetimeRegexp regexp for datetime parsing 16 DatetimeRegexp = `(?im)(\d+-\d+-\d+ \d+:\d+)` 17 18 //MinuteRegexp regexp for minutes parsing 19 MinuteRegexp = `(?im)((\d+) minute|minutes)` 20 21 //HourRegexp regexp for hours parsing 22 HourRegexp = `(?im)((\d+) hour|hours)` 23 24 //DayRegexp regexp for hours parsing 25 DayRegexp = `(?im)((\d+) day|days)` 26 27 repeatableRegexp = `(?im)(?:^|\s)(repeat|every)\s` 28 delayedTimeRegexp = `(?im)(?:^|\s)(in|after)\s` 29 exactTimeRegexp = `(?im)(\d+):(\d+)` 30 31 timeFormat = "2006-01-02 15:04" 32 ) 33 34 var daysOfWeek = map[string]time.Weekday{ 35 "sunday": time.Sunday, 36 "monday": time.Monday, 37 "tuesday": time.Tuesday, 38 "wednesday": time.Wednesday, 39 "thursday": time.Thursday, 40 "friday": time.Friday, 41 "saturday": time.Saturday, 42 } 43 44 type ExecuteAt struct { 45 Days int64 46 Minutes int64 47 Hours int64 48 Weekday interface{} 49 IsRepeatable bool 50 IsDelayed bool 51 ExactDatetime time.Time 52 IsExactHours bool 53 } 54 55 func (e *ExecuteAt) parseExactTime(text string) error { 56 res := helper.FindMatches(exactTimeRegexp, text) 57 if len(res) == 0 { 58 return nil 59 } 60 61 hour, err := strconv.Atoi(res["1"]) 62 if err != nil { 63 return err 64 } 65 66 minute, err := strconv.Atoi(res["2"]) 67 if err != nil { 68 return err 69 } 70 71 e.Hours = int64(hour) 72 e.Minutes = int64(minute) 73 74 e.IsExactHours = true 75 76 return nil 77 } 78 79 func (e *ExecuteAt) getDatetime() time.Time { 80 t := _time.Service.Now() 81 82 if e.Days != 0 || e.Minutes != 0 || e.Hours != 0 { 83 hours := t.Hour() 84 if e.Hours != 0 { 85 hours = int(e.Hours) 86 } 87 88 minutes := int(e.Minutes) 89 if !e.IsExactHours { 90 minutes = t.Minute() + minutes 91 } 92 93 if e.IsRepeatable || e.IsDelayed { 94 e.generateDelayedDate() 95 return e.ExactDatetime 96 } 97 98 return time.Date(t.Year(), t.Month(), e.generateDays(t), hours, minutes, 0, 0, t.Location()) 99 } 100 101 return e.ExactDatetime 102 } 103 104 func (e *ExecuteAt) generateDays(now time.Time) int { 105 if e.Weekday == nil { 106 days := now.Day() 107 if e.Days != 0 { 108 days += int(e.Days) 109 } 110 111 return days 112 } 113 114 if e.Weekday.(time.Weekday) == now.Weekday() { 115 return now.Day() 116 } 117 118 days := int((7 + (e.Weekday.(time.Weekday) - now.Weekday())) % 7) 119 _, _, d := now.AddDate(0, 0, days).Date() 120 return d 121 } 122 123 func (e *ExecuteAt) IsEmpty() bool { 124 return e.Days == 0 && e.Hours == 0 && e.Minutes == 0 && e.ExactDatetime.IsZero() 125 } 126 127 func (e *ExecuteAt) toString() string { 128 if e.IsEmpty() { 129 return "" 130 } 131 132 if !e.ExactDatetime.IsZero() && !e.IsRepeatable { 133 return e.ExactDatetime.Format(timeFormat) 134 } 135 136 var res []string 137 if e.Days != 0 { 138 res = append(res, fmt.Sprintf("%d days", e.Days)) 139 } 140 141 if e.Weekday != nil { 142 res = append(res, e.Weekday.(time.Weekday).String()) 143 } 144 145 if e.IsExactHours { 146 res = append(res, fmt.Sprintf("at %d:%d", e.Hours, e.Minutes)) 147 } else { 148 if e.Hours != 0 { 149 res = append(res, fmt.Sprintf("%d hours", e.Hours)) 150 } 151 152 if e.Minutes != 0 { 153 res = append(res, fmt.Sprintf("%d minutes", e.Minutes)) 154 } 155 } 156 157 if len(res) == 0 { 158 return "" 159 } 160 161 result := "" 162 if e.IsRepeatable { 163 result = "repeat " 164 } 165 166 return fmt.Sprintf("%s%s", result, strings.Join(res, " and ")) 167 } 168 169 func (e *ExecuteAt) parseDateTime(text string) error { 170 res := helper.FindMatches(DatetimeRegexp, text) 171 if res["1"] == "" { 172 return nil 173 } 174 175 result, err := time.ParseInLocation(timeFormat, text, _time.Service.TimeZone) 176 if err != nil { 177 return err 178 } 179 180 e.ExactDatetime = result 181 return nil 182 } 183 184 func (e *ExecuteAt) parse(text string, regex string) (result interface{}, err error) { 185 res := helper.FindMatches(regex, text) 186 if res["2"] == "" { 187 return nil, nil 188 } 189 190 return strconv.Atoi(res["2"]) 191 } 192 193 func (e *ExecuteAt) parseDays(text string) error { 194 res := helper.FindMatches(DayRegexp, text) 195 if res["2"] == "" { 196 return nil 197 } 198 199 days, err := strconv.Atoi(res["2"]) 200 if err != nil { 201 return err 202 } 203 204 e.Days = int64(days) 205 206 return nil 207 } 208 209 func (e *ExecuteAt) parseWeekday(text string) error { 210 var days []string 211 e.Weekday = nil 212 for dayName := range daysOfWeek { 213 days = append(days, dayName) 214 } 215 216 regexStr := fmt.Sprintf("(?i)(%s)", strings.Join(days, "|")) 217 res := helper.FindMatches(regexStr, text) 218 if res["1"] == "" { 219 return nil 220 } 221 222 dayName := strings.ToLower(res["1"]) 223 224 e.Weekday = daysOfWeek[dayName] 225 226 return nil 227 } 228 229 func (e *ExecuteAt) parseHoursAndMinutes(text string) error { 230 var ( 231 hours interface{} 232 minutes interface{} 233 err error 234 ) 235 236 if hours, err = e.parse(text, HourRegexp); err != nil { 237 return err 238 } 239 240 if minutes, err = e.parse(text, MinuteRegexp); err != nil { 241 return err 242 } 243 244 if hours == nil && minutes == nil { 245 return nil 246 } 247 248 //When we receive only hours but not minutes, we convert hours as minutes 249 if hours != nil && minutes == nil { 250 e.Minutes = int64(hours.(int) * 60) 251 252 return nil 253 } 254 255 if hours == nil { 256 e.Hours = 0 257 } else { 258 e.Hours = int64(hours.(int)) 259 } 260 261 e.Minutes = int64(minutes.(int)) 262 263 return nil 264 } 265 266 func (e *ExecuteAt) isRepeatable(text string) bool { 267 res := helper.FindMatches(repeatableRegexp, text) 268 269 return res["1"] != "" 270 } 271 272 func (e *ExecuteAt) isDelayed(text string) bool { 273 res := helper.FindMatches(delayedTimeRegexp, text) 274 275 return res["1"] != "" 276 } 277 278 func (e *ExecuteAt) FromString(text string) (ExecuteAt, error) { 279 if err := e.parseDateTime(text); err != nil { 280 return ExecuteAt{}, err 281 } 282 283 if !e.IsEmpty() { 284 return *e, nil 285 } 286 287 if e.isRepeatable(text) { 288 e.IsRepeatable = true 289 } 290 291 if e.isDelayed(text) { 292 e.IsDelayed = true 293 } 294 295 if err := e.parseHoursAndMinutes(text); err != nil { 296 return ExecuteAt{}, err 297 } 298 299 if err := e.parseWeekday(text); err != nil { 300 return ExecuteAt{}, err 301 } 302 303 if err := e.parseDays(text); err != nil { 304 return ExecuteAt{}, err 305 } 306 307 if err := e.parseExactTime(text); err != nil { 308 return ExecuteAt{}, err 309 } 310 311 if e.IsDelayed || e.IsRepeatable { 312 e.generateDelayedDate() 313 } 314 315 return *e, nil 316 } 317 318 func (e *ExecuteAt) generateDelayedDate() { 319 t := _time.Service.Now() 320 days := t.Day() 321 if e.Days != 0 { 322 days += int(e.Days) 323 } 324 325 hours := t.Hour() 326 minutes := t.Minute() 327 328 if !e.IsExactHours { 329 if e.Hours != 0 { 330 hours += int(e.Hours) 331 } 332 333 if e.Minutes != 0 { 334 minutes += int(e.Minutes) 335 } 336 } else { 337 hours = int(e.Hours) 338 minutes = int(e.Minutes) 339 } 340 341 e.ExactDatetime = time.Date(t.Year(), t.Month(), e.generateDays(t), hours, minutes, 0, 0, t.Location()) 342 }