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 }