},
}
-// scheduleParseCron parses a cron expression of five fields: minute, hour, day
-// of month, month, and day of week, where 0 and 7 are Sunday. A field is a
-// comma list of items, and an item is *, N, or N-M, where * and N-M can have a
-// step /S. Names, macros, the characters ? L W #, and N/S are errors.
-//
-// It refuses an expression whose runs can be less than scheduleCronGap minutes
-// apart. The check takes the wall times of one day from the hour and the minute
-// fields, and the gap across midnight as if the next day matches too. It also
-// refuses an expression that matches no day in scheduleCronYears years, such as
-// one on February 30.
-func scheduleParseCron(expression string) (ScheduleCron, error) {
- fields := strings.Fields(expression)
- if len(fields) != len(scheduleCronFields) {
- return ScheduleCron{}, fmt.Errorf("the cron expression %q has %d fields, not 5: minute, hour, day of month, month, day of week", expression, len(fields))
- }
-
- var sets [5]uint64
- for index, field := range fields {
- set, err := scheduleCronField(field, scheduleCronFields[index].low, scheduleCronFields[index].high)
- if err != nil {
- return ScheduleCron{}, fmt.Errorf("the %s field of %q: %w", scheduleCronFields[index].name, expression, err)
- }
-
- sets[index] = set
- }
-
- c := ScheduleCron{
- minutes: sets[0],
- hours: sets[1],
- days: sets[2],
- months: sets[3],
- weekdays: sets[4],
- anyDay: strings.HasPrefix(fields[2], "*"),
- anyWeekday: strings.HasPrefix(fields[4], "*"),
- description: strings.Join(fields, " "),
- }
-
- if c.weekdays&(1<<7) != 0 {
- c.weekdays |= 1
- }
-
- if c.gap() < scheduleCronGap {
- return ScheduleCron{}, fmt.Errorf("the runs of %q can be less than %d minutes apart: the runs of a schedule must be at least %d minutes apart", expression, scheduleCronGap, scheduleCronGap)
- }
-
- start := time.Date(2024, time.January, 1, 0, 0, 0, 0, time.UTC)
- _, found := c.Next(start)
- if !found {
- return ScheduleCron{}, fmt.Errorf("the cron expression %q matches no day in %d years", expression, scheduleCronYears)
- }
-
- return c, nil
-}
-
// ScheduleCron is a parsed cron expression. Each set holds a bit for each value
// of its field. anyDay and anyWeekday tell that the day field or the weekday
// field starts with *. When neither does, a day matches when either field
// one day are not always in order. The first time is the
// smallest one.
var next time.Time
+
for hour := range 24 {
if c.hours&(1<<hour) == 0 {
continue
// and the minute fields, also across midnight.
func (c ScheduleCron) gap() int {
var times []int
+
for hour := range 24 {
for minute := range 60 {
if c.hours&(1<<hour) != 0 && c.minutes&(1<<minute) != 0 {
return gap
}
+// scheduleParseCron parses a cron expression of five fields: minute, hour, day
+// of month, month, and day of week, where 0 and 7 are Sunday. A field is a
+// comma list of items, and an item is *, N, or N-M, where * and N-M can have a
+// step /S. Names, macros, the characters ? L W #, and N/S are errors.
+//
+// It refuses an expression whose runs can be less than scheduleCronGap minutes
+// apart. The check takes the wall times of one day from the hour and the minute
+// fields, and the gap across midnight as if the next day matches too. It also
+// refuses an expression that matches no day in scheduleCronYears years, such as
+// one on February 30.
+func scheduleParseCron(expression string) (ScheduleCron, error) {
+ fields := strings.Fields(expression)
+ if len(fields) != len(scheduleCronFields) {
+ return ScheduleCron{}, fmt.Errorf("the cron expression %q has %d fields, not 5: minute, hour, day of month, month, day of week", expression, len(fields))
+ }
+
+ var sets [5]uint64
+
+ for index, field := range fields {
+ set, err := scheduleCronField(field, scheduleCronFields[index].low, scheduleCronFields[index].high)
+ if err != nil {
+ return ScheduleCron{}, fmt.Errorf("the %s field of %q: %w", scheduleCronFields[index].name, expression, err)
+ }
+
+ sets[index] = set
+ }
+
+ c := ScheduleCron{
+ minutes: sets[0],
+ hours: sets[1],
+ days: sets[2],
+ months: sets[3],
+ weekdays: sets[4],
+ anyDay: strings.HasPrefix(fields[2], "*"),
+ anyWeekday: strings.HasPrefix(fields[4], "*"),
+ description: strings.Join(fields, " "),
+ }
+
+ if c.weekdays&(1<<7) != 0 {
+ c.weekdays |= 1
+ }
+
+ if c.gap() < scheduleCronGap {
+ return ScheduleCron{}, fmt.Errorf("the runs of %q can be less than %d minutes apart: the runs of a schedule must be at least %d minutes apart", expression, scheduleCronGap, scheduleCronGap)
+ }
+
+ start := time.Date(2024, time.January, 1, 0, 0, 0, 0, time.UTC)
+ _, found := c.Next(start)
+ if !found {
+ return ScheduleCron{}, fmt.Errorf("the cron expression %q matches no day in %d years", expression, scheduleCronYears)
+ }
+
+ return c, nil
+}
+
// scheduleWallTime returns the time of a wall time in location. A wall time
// that the clock skips, at the start of daylight saving time, is later by the
// length of the skip: 02:30 becomes 03:30 when the clock goes from 02:00 to
// high.
func scheduleCronField(field string, low, high int) (uint64, error) {
var set uint64
+
for item := range strings.SplitSeq(field, ",") {
base, stepText, stepped := strings.Cut(item, "/")
step := 1
if stepped {
var err error
+
step, err = scheduleCronNumber(stepText, 1, high)
if err != nil {
return 0, fmt.Errorf("the step of %q: %w", item, err)
case base == "*":
case ranged:
var err error
+
first, err = scheduleCronNumber(startText, low, high)
if err == nil {
last, err = scheduleCronNumber(endText, low, high)
return 0, fmt.Errorf("%q has a step after a single value: write a range N-M/S or */S", item)
default:
var err error
+
first, err = scheduleCronNumber(base, low, high)
if err != nil {
return 0, err