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  }