Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add L support with no changes on the SpecSchedule structure or whatsoever #325

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions cron.go
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,7 @@ func New(opts ...Option) *Cron {
// FuncJob is a wrapper that turns a func() into a cron.Job
type FuncJob func()

// Run will satisfies interface declaration
func (f FuncJob) Run() { f() }

// AddFunc adds a func to the Cron to be run on the given schedule.
Expand Down
7 changes: 5 additions & 2 deletions doc.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,13 +44,16 @@ A cron expression represents a set of times, using 5 space-separated fields.
---------- | ---------- | -------------- | --------------------------
Minutes | Yes | 0-59 | * / , -
Hours | Yes | 0-23 | * / , -
Day of month | Yes | 1-31 | * / , - ?
Day of month | Yes | 1-31 | * / , - ? L 1L 2L 3L 4L 5L 6L 7L
Month | Yes | 1-12 or JAN-DEC | * / , -
Day of week | Yes | 0-6 or SUN-SAT | * / , - ?
Day of week | Yes | 0-6 or SUN-SAT | * / , - ? 0L to 6L SUNL to SATL

Month and Day-of-week field values are case insensitive. "SUN", "Sun", and
"sun" are equally accepted.

L in day of month indicates last day in the month (eom), 1L means eom - 1 , etc...
Additional L in day of week indicates last occurance of the day in the month

The specific interpretation of the format is based on the Cron Wikipedia page:
https://en.wikipedia.org/wiki/Cron

Expand Down
4 changes: 2 additions & 2 deletions logger.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,10 @@ import (
)

// DefaultLogger is used by Cron if none is specified.
var DefaultLogger Logger = PrintfLogger(log.New(os.Stdout, "cron: ", log.LstdFlags))
var DefaultLogger = PrintfLogger(log.New(os.Stdout, "cron: ", log.LstdFlags))

// DiscardLogger can be used by callers to discard all log messages.
var DiscardLogger Logger = PrintfLogger(log.New(ioutil.Discard, "", 0))
var DiscardLogger = PrintfLogger(log.New(ioutil.Discard, "", 0))

// Logger is the interface used in this package for logging, so that any backend
// can be plugged in. It is a subset of the github.com/go-logr/logr interface.
Expand Down
21 changes: 18 additions & 3 deletions parser.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,21 @@ import (
"time"
)

// Configuration options for creating a parser. Most options specify which
// ParseOption is the configuration options for creating a parser. Most options specify which
// fields should be included, while others enable features. If a field is not
// included the parser will assume a default value. These options do not change
// the order fields are parse in.
type ParseOption int

// Second field of parse option, default 0
// Optional seconds field, default 0
// Minutes field, default 0
// Hours field, default 0
// Day of month field, default *
// Month field, default *
// Day of week field, default *
// Optional day of week field, default *
// Allow descriptors such as @monthly, @weekly, etc.
const (
Second ParseOption = 1 << iota // Seconds field, default 0
SecondOptional // Optional seconds field, default 0
Expand Down Expand Up @@ -44,7 +53,7 @@ var defaults = []string{
"*",
}

// A custom Parser that can be configured.
//Parser is a custom parser that can be configured.
type Parser struct {
options ParseOption
}
Expand Down Expand Up @@ -305,7 +314,13 @@ func getRange(expr string, r bounds) (uint64, error) {
return 0, fmt.Errorf("beginning of range (%d) below minimum (%d): %s", start, r.min, expr)
}
if end > r.max {
return 0, fmt.Errorf("end of range (%d) above maximum (%d): %s", end, r.max, expr)
if r.max != 31 && r.max != 6 { // not dom and not dow
return 0, fmt.Errorf("end of range (%d) above maximum (%d): %s", end, r.max, expr)
} else if r.max == 31 && (end > 55 || end < 48) {
return 0, fmt.Errorf("end of range (%d) above maximum (%d): %s", end, r.max, expr)
} else if r.max == 6 && (end > 55 || end < 49) {
return 0, fmt.Errorf("end of range (%d) above maximum (%d): %s", end, r.max, expr)
}
}
if start > end {
return 0, fmt.Errorf("beginning of range (%d) beyond end of range (%d): %s", start, end, expr)
Expand Down
97 changes: 87 additions & 10 deletions spec.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package cron

import "time"
import (
"time"
)

// SpecSchedule specifies a duty cycle (to the second granularity), based on a
// traditional crontab specification. It is computed initially and stored as bit sets.
Expand All @@ -22,8 +24,17 @@ var (
seconds = bounds{0, 59, nil}
minutes = bounds{0, 59, nil}
hours = bounds{0, 23, nil}
dom = bounds{1, 31, nil}
months = bounds{1, 12, map[string]uint{
dom = bounds{1, 31, map[string]uint{
"l": 55,
"1l": 54,
"2l": 53,
"3l": 52,
"4l": 51,
"5l": 50,
"6l": 49,
"7l": 48,
}}
months = bounds{1, 12, map[string]uint{
"jan": 1,
"feb": 2,
"mar": 3,
Expand All @@ -38,13 +49,27 @@ var (
"dec": 12,
}}
dow = bounds{0, 6, map[string]uint{
"sun": 0,
"mon": 1,
"tue": 2,
"wed": 3,
"thu": 4,
"fri": 5,
"sat": 6,
"sun": 0,
"mon": 1,
"tue": 2,
"wed": 3,
"thu": 4,
"fri": 5,
"sat": 6,
"sunl": 49,
"monl": 50,
"tuel": 51,
"wedl": 52,
"thul": 53,
"fril": 54,
"satl": 55,
"0l": 49,
"1l": 50,
"2l": 51,
"3l": 52,
"4l": 53,
"5l": 54,
"6l": 55,
}}
)

Expand Down Expand Up @@ -177,12 +202,64 @@ WRAP:
// dayMatches returns true if the schedule's day-of-week and day-of-month
// restrictions are satisfied by the given time.
func dayMatches(s *SpecSchedule, t time.Time) bool {
eom, eowd := eomBits(s, t)
var (
domMatch bool = 1<<uint(t.Day())&s.Dom > 0
dowMatch bool = 1<<uint(t.Weekday())&s.Dow > 0
)
if eom > 0 {
domMatch = domMatch || (1<<uint(t.Day())&eom > 0)
}
if eowd > 0 {
dowMatch = dowMatch || (1<<uint(t.Day())&eowd > 0)
}
if s.Dom&starBit > 0 || s.Dow&starBit > 0 {
return domMatch && dowMatch
}
return domMatch || dowMatch
}

// basically EOM(to EOM - 7) flag is stored in bits 55 - 48 of SpecSchedule's Dom
// you just need to know what date of t's eom, and shift bits 55 - 48 (0x00FF_0000_0000_0000) to that position
func eomBits(s *SpecSchedule, t time.Time) (uint64, uint64) {
bDow := byte(s.Dow & 0x00FE000000000000 >> (6 * 8))
if s.Dom&0x00FF000000000000 == 0 && bDow == 0 {
return 0, 0
}
eom := byte(30)
year := t.Year()
leapYear := byte(0)
if (year%4 == 0 && year%100 != 0) || year%400 == 0 {
leapYear = 1
}

switch t.Month() {
case time.April, time.June, time.September, time.November:
case time.February:
eom = 28 + leapYear
default:
eom = 31
}

dowBits := uint64(0)
if bDow > 0 {
lastDayOfWeek := time.Date(t.Year(), t.Month(), int(eom), 0, 0, 0, 0, t.Location()).Weekday()
switch lastDayOfWeek {
case 0:
bDow = (0xFC&bDow)>>1 | (bDow << 6)
case 1:
bDow = (0xF8&bDow)>>2 | (bDow << 5)
case 2:
bDow = (0xF0&bDow)>>3 | (bDow << 4)
case 3:
bDow = (0xE0&bDow)>>4 | (bDow << 3)
case 4:
bDow = (0xC0&bDow)>>5 | (bDow << 2)
case 5:
bDow = (0x80&bDow)>>6 | (bDow << 1)
default: // actaually case 6 // which is doing nothing
}
dowBits = uint64(bDow) << (6 * 8)
}
return (s.Dom & 0x00FF000000000000) >> (55 - eom), dowBits >> (55 - eom)
}
20 changes: 20 additions & 0 deletions spec_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,18 @@ func TestActivation(t *testing.T) {
{"Mon Jul 9 00:00 2012", "* * 1,15 * *", false},
{"Sun Jul 15 00:00 2012", "* * 1,15 * *", true},
{"Sun Jul 15 00:00 2012", "* * */2 * Sun", true},

// test for EOM
{"Wed Jul 15 00:00 2020", "* * L * *", false},
{"Fri Jul 31 00:00 2020", "* * L * *", true},
{"Thu Jul 30 00:00 2020", "* * 1L * *", true},
{"Sat Feb 22 00:00 2020", "* * 7L * *", true},

// test for EOWD
{"Wed Jul 15 00:00 2020", "* * L * 0L", false},
{"Fri Jul 31 00:00 2020", "* * * * 5L", true},
{"Thu Jul 30 00:00 2020", "* * 1L * 3L", true},
{"Sat Feb 22 00:00 2020", "* * * * 1L", false},
}

for _, test := range tests {
Expand Down Expand Up @@ -183,6 +195,14 @@ func TestNext(t *testing.T) {
// https://github.com/robfig/cron/issues/157
{"2018-10-17T05:00:00-0400", "TZ=America/Sao_Paulo 0 0 9 10 * ?", "2018-11-10T06:00:00-0500"},
{"2018-02-14T05:00:00-0500", "TZ=America/Sao_Paulo 0 0 9 22 * ?", "2018-02-22T07:00:00-0500"},

// EOM test
{"TZ=America/New_York 2012-11-04T03:00:00-0500", "0 0 0 L * ?", "2012-11-30T00:00:00-0500"},
{"TZ=America/New_York 2012-11-04T03:00:00-0500", "0 0 3 3L * ?", "2012-11-27T03:00:00-0500"},

// Last weekday of the month
{"TZ=Asia/Jakarta 2020-07-27T03:00:00+0700", "0 0 0 * * 4L", "2020-07-30T00:00:00+0700"},
{"TZ=Asia/Jakarta 2020-07-04T03:00:00+0700", "0 0 3 * * MonL", "2020-07-27T03:00:00+0700"},
}

for _, c := range runs {
Expand Down