Skip to content

Commit

Permalink
Return better errors for time units
Browse files Browse the repository at this point in the history
  • Loading branch information
Gilthoniel committed Dec 3, 2023
1 parent 1bdfae8 commit 32638ad
Show file tree
Hide file tree
Showing 6 changed files with 173 additions and 9 deletions.
4 changes: 2 additions & 2 deletions cron.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,15 @@ package gocron

import "time"

type timeUnit interface {
type TimeUnit interface {
// Next returns the next iteration of a schedule and `true` when valid,
// otherwise it returns a time after `next` and `false`.
Next(next time.Time) (time.Time, bool)
}

// Schedule is a representation of a Cron expression.
type Schedule struct {
timeUnits []timeUnit
timeUnits []TimeUnit
}

// Parse returns a schedule from the Cron expression and returns an error if the
Expand Down
1 change: 1 addition & 0 deletions cron_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ func TestSchedule_Next(t *testing.T) {
{"* * * * * 5L", time.Date(2000, time.March, 31, 0, 0, 0, 0, time.UTC)},
{"* * * * * 4L", time.Date(2000, time.March, 30, 0, 0, 0, 0, time.UTC)},
{"* * * 31 * L", time.Date(2001, time.March, 31, 0, 0, 0, 0, time.UTC)},
{"* * * ? * 0L", time.Date(2000, time.March, 26, 0, 0, 0, 0, time.UTC)},
}

for _, v := range vectors {
Expand Down
45 changes: 45 additions & 0 deletions error.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
package gocron

import (
"errors"
"fmt"
)

var (
ErrMultipleNotSpecified = errors.New("only one `?` is supported")
)

type TimeUnitError struct {
inner error
kind TimeUnitKind
}

func newTimeUnitErr(kind TimeUnitKind, inner error) TimeUnitError {
return TimeUnitError{inner: inner, kind: kind}
}

func (e TimeUnitError) Error() string {
return fmt.Sprintf("time unit `%s` malformed: %s", e.kind, e.inner)
}

func (e TimeUnitError) Is(err error) bool {
tue, ok := err.(TimeUnitError)
return ok && tue.kind == e.kind && errors.Is(tue.inner, e.inner)
}

type TimeUnitKind int

const (
Seconds TimeUnitKind = iota
Minutes
Hours
Days
Months
WeekDays
)

var kinds = []string{"seconds", "minutes", "hours", "days", "months", "week days"}

func (k TimeUnitKind) String() string {
return kinds[k]
}
44 changes: 37 additions & 7 deletions parser.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,30 +29,34 @@ func (p Parser) Parse(expression string) (schedule Schedule, err error) {

weekdays, err := p.parse(matches[5], convertWeekDay, 0, 6)
if err != nil {
return schedule, err
return schedule, newTimeUnitErr(WeekDays, err)
}
months, err := p.parse(matches[4], convertUnit, 1, 12)
if err != nil {
return schedule, err
return schedule, newTimeUnitErr(Months, err)
}
days, err := p.parse(matches[3], convertWithLastDayOfMonth, 1, 31)
if err != nil {
return schedule, err
return schedule, newTimeUnitErr(Days, err)
}
hours, err := p.parse(matches[2], convertUnit, 0, 23)
if err != nil {
return schedule, err
return schedule, newTimeUnitErr(Hours, err)
}
minutes, err := p.parse(matches[1], convertUnit, 0, 59)
if err != nil {
return schedule, err
return schedule, newTimeUnitErr(Minutes, err)
}
seconds, err := p.parse(matches[0], convertUnit, 0, 59)
if err != nil {
return schedule, err
return schedule, newTimeUnitErr(Seconds, err)
}

schedule.timeUnits = []timeUnit{
if isNotSpecified(weekdays) && isNotSpecified(days) {
err = ErrMultipleNotSpecified
}

schedule.timeUnits = []TimeUnit{
monthTimeUnit{units: months},
dayTimeUnit{units: days},
weekdayTimeUnit{units: weekdays},
Expand All @@ -68,6 +72,10 @@ func (Parser) parse(expr string, convFn ConvertFn, min, max int) (fields []ExprF
if expr == "*" {
return
}
if expr == "?" {
fields = append(fields, notSpecifiedExpr{})
return
}

for _, u := range strings.Split(expr, ",") {
var field ExprField
Expand Down Expand Up @@ -134,6 +142,28 @@ func parseInterval(expr string, convFn ConvertFn, min, max int) (i intervalExpr,
return
}

// isNotSpecified returns true if any of the field is not specified.
func isNotSpecified(fields []ExprField) bool {
for _, field := range fields {
if _, ok := field.(notSpecifiedExpr); ok {
return true
}
}
return false
}

// notSpecifiedExpr is an expression field that represent a question mark `?`,
// or in other words a value not specified.
type notSpecifiedExpr struct{}

func (notSpecifiedExpr) Compare(_ time.Time, other int) Ordering {
return OrderingEqual
}

func (notSpecifiedExpr) Value(_ time.Time, other int) int {
return other
}

// unitExpr is an expression field that represents a single possible value.
type unitExpr int

Expand Down
82 changes: 82 additions & 0 deletions parser_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
package gocron

import (
"errors"
"testing"
)

func TestParser_Parse_refusesTwiceNotSpecified(t *testing.T) {
_, err := Parser{}.Parse("* * * ? * ?")
requireErrorIs(t, err, ErrMultipleNotSpecified)
}

func TestParser_Parse_abortsOnMalformedSeconds(t *testing.T) {
_, err := Parser{}.Parse("a * * * * *")

var e TimeUnitError
requireErrorAs(t, err, &e)
requireSameKind(t, e.kind, Seconds)
}

func TestParser_Parse_abortsOnMalformedMinutes(t *testing.T) {
_, err := Parser{}.Parse("* a * * * *")

var e TimeUnitError
requireErrorAs(t, err, &e)
requireSameKind(t, e.kind, Minutes)
}

func TestParser_Parse_abortsOnMalformedHours(t *testing.T) {
_, err := Parser{}.Parse("* * a * * *")

var e TimeUnitError
requireErrorAs(t, err, &e)
requireSameKind(t, e.kind, Hours)
}

func TestParser_Parse_abortsOnMalformedDays(t *testing.T) {
_, err := Parser{}.Parse("* * * a * *")

var e TimeUnitError
requireErrorAs(t, err, &e)
requireSameKind(t, e.kind, Days)
}

func TestParser_Parse_abortsOnMalformedMonths(t *testing.T) {
_, err := Parser{}.Parse("* * * * a *")

var e TimeUnitError
requireErrorAs(t, err, &e)
requireSameKind(t, e.kind, Months)
}

func TestParser_Parse_abortsOnMalformedWeekDays(t *testing.T) {
_, err := Parser{}.Parse("* * * * * a")

var e TimeUnitError
requireErrorAs(t, err, &e)
requireSameKind(t, e.kind, WeekDays)
}

// --- Utilities

func requireErrorIs(t testing.TB, err, target error) {
t.Helper()
if !errors.Is(err, target) {
t.Fatalf("expected error: %#v, found %#v", target, err)
}
}

func requireErrorAs(t testing.TB, err error, target any) {
t.Helper()
if !errors.As(err, target) {
t.Fatalf("expected error of kind: %T, found %#v", target, err)
}
}

func requireSameKind(t testing.TB, a, b TimeUnitKind) {
t.Helper()
if a != b {
t.Fatalf("%s != %s", a, b)
}
}
6 changes: 6 additions & 0 deletions time-unit.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ type secTimeUnit struct {
units []ExprField
}

// Next implements TimeUnit.
func (s secTimeUnit) Next(next time.Time) (time.Time, bool) {
if len(s.units) == 0 {
// Expression is `*`.
Expand All @@ -32,6 +33,7 @@ type minTimeUnit struct {
fields []ExprField
}

// Next implements TimeUnit.
func (m minTimeUnit) Next(next time.Time) (time.Time, bool) {
if len(m.fields) == 0 {
// Expression is `*`.
Expand All @@ -56,6 +58,7 @@ type hourTimeUnit struct {
fields []ExprField
}

// Next implements TimeUnit.
func (h hourTimeUnit) Next(next time.Time) (time.Time, bool) {
if len(h.fields) == 0 {
// Expression is `*`.
Expand All @@ -80,6 +83,7 @@ type dayTimeUnit struct {
units []ExprField
}

// Next implements TimeUnit.
func (d dayTimeUnit) Next(next time.Time) (time.Time, bool) {
if len(d.units) == 0 {
// Expression is `*`.
Expand Down Expand Up @@ -110,6 +114,7 @@ type monthTimeUnit struct {
units []ExprField
}

// Next implements TimeUnit.
func (m monthTimeUnit) Next(next time.Time) (time.Time, bool) {
if len(m.units) == 0 {
// Expression is `*`.
Expand All @@ -134,6 +139,7 @@ type weekdayTimeUnit struct {
units []ExprField
}

// Next implements TimeUnit.
func (wd weekdayTimeUnit) Next(next time.Time) (time.Time, bool) {
if len(wd.units) == 0 {
return next, true
Expand Down

0 comments on commit 32638ad

Please sign in to comment.