github.com/argoproj/argo-events@v1.9.1/common/cronutil.go (about)

     1  package common
     2  
     3  import (
     4  	"fmt"
     5  	"time"
     6  
     7  	cronlib "github.com/robfig/cron/v3"
     8  )
     9  
    10  const (
    11  	// Set the top bit if a star was included in the expression.
    12  	starBit = 1 << 63
    13  )
    14  
    15  // For a given cron specification, return the previous activation time
    16  // If no time can be found to satisfy the schedule, return the zero time.
    17  func PrevCronTime(cronSpec string, parser cronlib.Parser, t time.Time) (time.Time, error) {
    18  	var tm time.Time
    19  	sched, err := parser.Parse(cronSpec)
    20  	if err != nil {
    21  		return tm, fmt.Errorf("can't derive previous Cron time for cron spec %s; couldn't parse; err=%v", cronSpec, err)
    22  	}
    23  	s, castOk := sched.(*cronlib.SpecSchedule)
    24  	if !castOk {
    25  		return tm, fmt.Errorf("can't derive previous Cron time for cron spec %s: unexpected type for %v", cronSpec, sched)
    26  	}
    27  
    28  	// General approach is based on approach to SpecSchedule.Next() implementation
    29  
    30  	origLocation := t.Location()
    31  	loc := s.Location
    32  	if loc == time.Local {
    33  		loc = t.Location()
    34  	}
    35  	if s.Location != time.Local {
    36  		t = t.In(s.Location)
    37  	}
    38  
    39  	// Start at the previous second
    40  	t = t.Add(-1*time.Second - time.Duration(t.Nanosecond())*time.Nanosecond)
    41  
    42  	// If no time is found within five years, return zero.
    43  	yearLimit := t.Year() - 5
    44  
    45  WRAP:
    46  	if t.Year() < yearLimit {
    47  		return tm, fmt.Errorf("can't derive previous Cron time for cron spec %s: no time found within %d years", cronSpec, yearLimit)
    48  	}
    49  
    50  	// Find the first applicable month.
    51  	// If it's this month, then do nothing.
    52  	for 1<<uint(t.Month())&s.Month == 0 {
    53  		// set t to the last second of the previous month
    54  		t = time.Date(t.Year(), t.Month(), 1, 0, 0, 0, 0, loc)
    55  		t = t.Add(-1 * time.Second)
    56  
    57  		// Wrapped around.
    58  		if t.Month() == time.December {
    59  			goto WRAP
    60  		}
    61  	}
    62  
    63  	// Now get a day in that month.
    64  	for !dayMatches(s, t) {
    65  		// set t to the last second of the previous day
    66  
    67  		saveMonth := t.Month()
    68  		t = time.Date(t.Year(), t.Month(), t.Day(), 0, 0, 0, 0, loc)
    69  
    70  		// NOTE: This causes issues for daylight savings regimes where midnight does
    71  		// not exist.  For example: Sao Paulo has DST that transforms midnight on
    72  		// 11/3 into 1am. Handle that by noticing when the Hour ends up != 0.
    73  
    74  		// Notice if the hour is no longer midnight due to DST.
    75  		// Add an hour if it's 23, subtract an hour if it's 1.
    76  		if t.Hour() != 0 {
    77  			if t.Hour() > 12 {
    78  				t = t.Add(time.Duration(24-t.Hour()) * time.Hour)
    79  			} else {
    80  				t = t.Add(time.Duration(-t.Hour()) * time.Hour)
    81  			}
    82  		}
    83  
    84  		t = t.Add(-1 * time.Second)
    85  
    86  		if saveMonth != t.Month() {
    87  			goto WRAP
    88  		}
    89  	}
    90  
    91  	for 1<<uint(t.Hour())&s.Hour == 0 {
    92  		// set t to the last second of the previous hour
    93  		t = time.Date(t.Year(), t.Month(), t.Day(), t.Hour(), 0, 0, 0, loc)
    94  		t = t.Add(-1 * time.Second)
    95  
    96  		if t.Hour() == 23 {
    97  			goto WRAP
    98  		}
    99  	}
   100  
   101  	for 1<<uint(t.Minute())&s.Minute == 0 {
   102  		// set t to the last second of the previous minute
   103  		t = time.Date(t.Year(), t.Month(), t.Day(), t.Hour(), t.Minute(), 0, 0, loc)
   104  		t = t.Add(-1 * time.Second)
   105  
   106  		if t.Minute() == 59 {
   107  			goto WRAP
   108  		}
   109  	}
   110  
   111  	for 1<<uint(t.Second())&s.Second == 0 {
   112  		// set t to the previous second
   113  		t = t.Add(-1 * time.Second)
   114  
   115  		if t.Second() == 59 {
   116  			goto WRAP
   117  		}
   118  	}
   119  
   120  	return t.In(origLocation), nil
   121  }
   122  
   123  // dayMatches returns true if the schedule's day-of-week and day-of-month
   124  // restrictions are satisfied by the given time.
   125  func dayMatches(s *cronlib.SpecSchedule, t time.Time) bool {
   126  	var (
   127  		domMatch bool = 1<<uint(t.Day())&s.Dom > 0
   128  		dowMatch bool = 1<<uint(t.Weekday())&s.Dow > 0
   129  	)
   130  	if s.Dom&starBit > 0 || s.Dow&starBit > 0 {
   131  		return domMatch && dowMatch
   132  	}
   133  	return domMatch || dowMatch
   134  }