Skip to content

Commit

Permalink
fix: next, prev updated
Browse files Browse the repository at this point in the history
Signed-off-by: Eray Ates <eray.ates@worldline.com>
  • Loading branch information
rytsh committed Aug 5, 2023
1 parent 8885436 commit 4634e95
Show file tree
Hide file tree
Showing 4 changed files with 419 additions and 78 deletions.
22 changes: 22 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
.DEFAULT_GOAL := help

.PHONY: test coverage html html-gen html-wsl help

test: ## Run unit tests
@go test -timeout 30s -v -race ./...

coverage: ## Run unit tests with coverage
@go test -timeout 30s -v -race -cover -coverpkg=./... -coverprofile=coverage.out -covermode=atomic ./...
@go tool cover -func=coverage.out

html: ## Show html coverage result
@go tool cover -html=./coverage.out

html-gen: ## Export html coverage result
@go tool cover -html=./coverage.out -o ./coverage.html

html-wsl: html-gen ## Open html coverage result in wsl
@explorer.exe `wslpath -w ./coverage.html` || true

help: ## Display this help screen
@grep -h -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}'
7 changes: 3 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ go get github.com/worldline-go/hardloop

Check the https://crontab.guru/ to explain about cron specs.

> Hardloop different works than _crontab.guru_ in weekdays and day of month selection. We use __and__ operation but that site use __or__ operation when used both of them.
You can give as much as you want start, stop times.

If stop time is not given, it will run forever.
Expand Down Expand Up @@ -64,8 +66,5 @@ myFunctionLoop.SetLogger(myLog{})
Test code

```sh
go test -cover -coverprofile=coverage.out ./...
go tool cover -func=coverage.out
go tool cover -html=coverage.out
# go tool cover -html coverage.out -o coverage.html
make coverage html-wsl
```
192 changes: 176 additions & 16 deletions cron.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,127 @@ const (
starBit = 1 << 63
)

// Next returns the next time this schedule is activated, greater than the given
// time. If no time can be found to satisfy the schedule, return the zero time.
func (s *SpecSchedule) Next(t time.Time) time.Time {
// General approach
//
// For Month, Day, Hour, Minute, Second:
// Check if the time value matches. If yes, continue to the next field.
// If the field doesn't match the schedule, then increment the field until it matches.
// While incrementing the field, a wrap-around brings it back to the beginning
// of the field list (since it is necessary to re-verify previous field
// values)

// Convert the given time into the schedule's timezone, if one is specified.
// Save the original timezone so we can convert back after we find a time.
// Note that schedules without a time zone specified (time.Local) are treated
// as local to the time provided.
origLocation := t.Location()
loc := s.Location
if loc == time.Local {
loc = t.Location()
}
if s.Location != time.Local {
t = t.In(s.Location)
}

// Start at the earliest possible time (the upcoming second).
t = t.Add(1*time.Second - time.Duration(t.Nanosecond())*time.Nanosecond)

// This flag indicates whether a field has been incremented.
added := false

// If no time is found within five years, return zero.
yearLimit := t.Year() + 5

WRAP:
if t.Year() > yearLimit {
return time.Time{}
}

// Find the first applicable month.
// If it's this month, then do nothing.
for 1<<uint(t.Month())&s.Month == 0 {
// If we have to add a month, reset the other parts to 0.
if !added {
added = true
// Otherwise, set the date at the beginning (since the current time is irrelevant).
t = time.Date(t.Year(), t.Month(), 1, 0, 0, 0, 0, loc)
}
t = t.AddDate(0, 1, 0)

// Wrapped around.
if t.Month() == time.January {
goto WRAP
}
}

// Now get a day in that month.
//
// NOTE: This causes issues for daylight savings regimes where midnight does
// not exist. For example: Sao Paulo has DST that transforms midnight on
// 11/3 into 1am. Handle that by noticing when the Hour ends up != 0.
for !dayMatches(s, t) {
if !added {
added = true
t = time.Date(t.Year(), t.Month(), t.Day(), 0, 0, 0, 0, loc)
}
t = t.AddDate(0, 0, 1)
// Notice if the hour is no longer midnight due to DST.
// Add an hour if it's 23, subtract an hour if it's 1.
if t.Hour() != 0 {
if t.Hour() > 12 {
t = t.Add(time.Duration(24-t.Hour()) * time.Hour)
} else {
t = t.Add(time.Duration(-t.Hour()) * time.Hour)
}
}

if t.Day() == 1 {
goto WRAP
}
}

for 1<<uint(t.Hour())&s.Hour == 0 {
if !added {
added = true
t = time.Date(t.Year(), t.Month(), t.Day(), t.Hour(), 0, 0, 0, loc)
}
t = t.Add(1 * time.Hour)

if t.Hour() == 0 {
goto WRAP
}
}

for 1<<uint(t.Minute())&s.Minute == 0 {
if !added {
added = true
t = t.Truncate(time.Minute)
}
t = t.Add(1 * time.Minute)

if t.Minute() == 0 {
goto WRAP
}
}

for 1<<uint(t.Second())&s.Second == 0 {
if !added {
added = true
t = t.Truncate(time.Second)
}
t = t.Add(1 * time.Second)

if t.Second() == 0 {
goto WRAP
}
}

return t.In(origLocation)
}

// Prev returns the prev time this schedule is activated, less than the given
// time. If no time can be found to satisfy the schedule, return the zero time.
func (s *SpecSchedule) Prev(t time.Time) time.Time {
Expand Down Expand Up @@ -97,23 +218,19 @@ WRAP:
}

// Now get a day in that month.
//
// NOTE: This causes issues for daylight savings regimes where midnight does
// not exist. For example: Sao Paulo has DST that transforms midnight on
// 11/3 into 1am. Handle that by noticing when the Hour ends up != 0.
for !dayMatches(s, t) {
if !added {
added = true
t = time.Date(t.Year(), t.Month(), t.Day(), 0, 0, 0, 0, loc)
}
currentDay := t.Day()
t = t.AddDate(0, 0, -1)

// Add hour to fix previous hour check.
if t.Hour() == 0 {
if !dayMatches(s, t) && t.Hour() == 0 {
t = t.Add(time.Hour * 23)
}

if t.Day() == 1 {
if t.Day()-currentDay != 1 {
goto WRAP
}
}
Expand All @@ -125,8 +242,7 @@ WRAP:
}
t = t.Add(-1 * time.Hour)

// Add minutes to fix previous minute check.
if t.Minute() == 0 {
if 1<<uint(t.Hour())&s.Hour != 0 && t.Minute() == 0 {
t = t.Add(time.Minute * 59)
}

Expand All @@ -142,8 +258,7 @@ WRAP:
}
t = t.Add(-1 * time.Minute)

// Add minutes to fix previous minute check.
if t.Second() == 0 {
if 1<<uint(t.Minute())&s.Minute != 0 && t.Second() == 0 {
t = t.Add(time.Second * 59)
}

Expand All @@ -169,23 +284,37 @@ WRAP:
return t.In(origLocation)
}

// same function of github.com/robfig/cron/v3 dayMatches
// edited function of github.com/robfig/cron/v3 to changed day of week and day of month both usage
func dayMatches(s *SpecSchedule, t time.Time) bool {
var (
domMatch bool = 1<<uint(t.Day())&s.Dom > 0
dowMatch bool = 1<<uint(t.Weekday())&s.Dow > 0
)
if s.Dom&starBit > 0 || s.Dow&starBit > 0 {

if s.Dom&starBit > 0 && s.Dow&starBit > 0 {
return domMatch || dowMatch
}

if !(s.Dom&starBit > 0) && !(s.Dow&starBit > 0) {
return domMatch && dowMatch
}
return domMatch || dowMatch

if !(s.Dow&starBit > 0) && dowMatch {
return dowMatch
}

if !(s.Dom&starBit > 0) && domMatch {
return domMatch
}

return domMatch && dowMatch
}

// Parse returns a new cron schedule for the given spec.
//
// It accepts
// - Standard crontab specs, e.g. "* * * * ?"
// - Descriptors, e.g. "@midnight", "@every 1h30m"
// - Standard crontab specs, e.g. "* * * * ?"
// - Descriptors, e.g. "@midnight", "@every 1h30m"
func ParseStandard(spec string) (*SpecSchedule, error) {
specSchedule, err := cron.ParseStandard(spec)
if err != nil {
Expand All @@ -194,3 +323,34 @@ func ParseStandard(spec string) (*SpecSchedule, error) {

return &SpecSchedule{SpecSchedule: specSchedule.(*cron.SpecSchedule)}, nil
}

// Parser is default parser for cron to replacing the functions.
type Parser struct {
ParseFn func(standardSpec string) (cron.Schedule, error)
}

func (p Parser) Parse(spec string) (cron.Schedule, error) {
parseFn := p.ParseFn
if parseFn == nil {
parseFn = cron.ParseStandard
}

specSchedule, err := parseFn(spec)
if err != nil {
return nil, err
}

return &SpecSchedule{SpecSchedule: specSchedule.(*cron.SpecSchedule)}, nil
}

// Parse2 is a helper function for parsing the spec and returning the SpecSchedule.
func (p Parser) Parse2(spec string) (Schedule, error) {
v, err := p.Parse(spec)
if err != nil {
return nil, err
}

return v.(*SpecSchedule), nil
}

var _ cron.ScheduleParser = &Parser{}
Loading

0 comments on commit 4634e95

Please sign in to comment.