Skip to content
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
263 changes: 214 additions & 49 deletions cron.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"fmt"
"math"
"math/bits"
"regexp"
"strconv"
"strings"
"time"
Expand All @@ -28,19 +29,28 @@ const (
)

type fieldType struct {
Field cronField
MinValue int
MaxValue int
Field cronField
MinValue int
MaxValue int
SpecialCharacters map[string]struct{}
}

var (
nanoSecond = fieldType{cronFieldNanoSecond, 0, 999999999}
second = fieldType{cronFieldSecond, 0, 59}
minute = fieldType{cronFieldMinute, 0, 59}
hour = fieldType{cronFieldHour, 0, 23}
dayOfMonth = fieldType{cronFieldDayOfMonth, 1, 31}
month = fieldType{cronFieldMonth, 1, 12}
dayOfWeek = fieldType{cronFieldDayOfWeek, 1, 7}
nanoSecond = fieldType{cronFieldNanoSecond, 0, 999999999,
map[string]struct{}{
",": struct{}{}, "*": struct{}{}, "/": struct{}{}, "-": struct{}{}}}
second = fieldType{cronFieldSecond, 0, 59, map[string]struct{}{
",": struct{}{}, "*": struct{}{}, "/": struct{}{}, "-": struct{}{}}}
minute = fieldType{cronFieldMinute, 0, 59, map[string]struct{}{
",": struct{}{}, "*": struct{}{}, "/": struct{}{}, "-": struct{}{}}}
hour = fieldType{cronFieldHour, 0, 23, map[string]struct{}{
",": struct{}{}, "*": struct{}{}, "/": struct{}{}, "-": struct{}{}}}
dayOfMonth = fieldType{cronFieldDayOfMonth, 1, 31, map[string]struct{}{
",": struct{}{}, "*": struct{}{}, "/": struct{}{}, "-": struct{}{}, "L": struct{}{}, "W": struct{}{}, "?": struct{}{}}}
month = fieldType{cronFieldMonth, 1, 12, map[string]struct{}{
",": struct{}{}, "*": struct{}{}, "/": struct{}{}, "-": struct{}{}}}
dayOfWeek = fieldType{cronFieldDayOfWeek, 0, 6, map[string]struct{}{
",": struct{}{}, "*": struct{}{}, "/": struct{}{}, "-": struct{}{}, "L": struct{}{}, "#": struct{}{}, "?": struct{}{}}}
)

var cronFieldTypes = []fieldType{
Expand Down Expand Up @@ -152,16 +162,13 @@ func (expression *CronExpression) nextField(field *cronFieldBits, t time.Time) t
}

func ParseCronExpression(expression string) (*CronExpression, error) {
if len(expression) == 0 {
return nil, errors.New("cron expression must not be empty")
err := IsValid(expression)
if err != nil {
return nil, err
}

fields := strings.Fields(expression)

if len(fields) != 6 {
return nil, fmt.Errorf("cron expression must consist of 6 fields : found %d in \"%s\"", len(fields), expression)
}

cronExpression := newCronExpression()

for index, cronFieldType := range cronFieldTypes {
Expand Down Expand Up @@ -221,15 +228,6 @@ func parseField(value string, fieldType fieldType) (*cronFieldBits, error) {
stepStr := field[slashPos+1:]

step, err = strconv.Atoi(stepStr)

if err != nil {
return nil, fmt.Errorf("step must be number : \"%s\"", stepStr)
}

if step <= 0 {
return nil, fmt.Errorf("step must be 1 or higher in \"%s\"", value)
}

} else {
var err error
valueRange, err = parseRange(field, fieldType)
Expand Down Expand Up @@ -263,8 +261,7 @@ func parseRange(value string, fieldType fieldType) (valueRange, error) {
hyphenPos := strings.Index(value, "-")

if hyphenPos == -1 {
result, err := checkValidValue(value, fieldType)

result, err := strconv.Atoi(value)
if err != nil {
return valueRange{}, err
}
Expand All @@ -274,14 +271,12 @@ func parseRange(value string, fieldType fieldType) (valueRange, error) {
maxStr := value[hyphenPos+1:]
minStr := value[0:hyphenPos]

min, err := checkValidValue(minStr, fieldType)

min, err := strconv.Atoi(minStr)
if err != nil {
return valueRange{}, err
}
var max int
max, err = checkValidValue(maxStr, fieldType)

max, err := strconv.Atoi(maxStr)
if err != nil {
return valueRange{}, err
}
Expand All @@ -306,24 +301,6 @@ func replaceOrdinals(value string, list []string) string {
return value
}

func checkValidValue(value string, fieldType fieldType) (int, error) {
result, err := strconv.Atoi(value)

if err != nil {
return 0, fmt.Errorf("the value in field %s must be number : %s", fieldType.Field, value)
}

if fieldType.Field == cronFieldDayOfWeek && result == 0 {
return result, nil
}

if result >= fieldType.MinValue && result <= fieldType.MaxValue {
return result, nil
}

return 0, fmt.Errorf("the value in field %s must be between %d and %d", fieldType.Field, fieldType.MinValue, fieldType.MaxValue)
}

func getTimeValue(t time.Time, field cronField) int {

switch field {
Expand Down Expand Up @@ -446,3 +423,191 @@ func getFieldMaxValue(t time.Time, fieldType fieldType) int {
func isLeapYear(year int) bool {
return year%400 == 0 || year%100 != 0 && year%4 == 0
}

// IsValid returns nil if the cron expression is valid
func IsValid(expression string) error {
if len(expression) == 0 {
return errors.New("cron expression must not be empty")
}

fields := strings.Fields(expression)

if len(fields) != 6 {
if len(fields) == 7 {
return errors.New("cron expression must consist of 6 fields: Chrono isn't support for 7 fields")
}
return fmt.Errorf("cron expression must consist of 6 fields: found %d fields in '%s'", len(fields), expression)
}

for i, field := range fields {
err := validateSubExpression(field, cronFieldTypes[i], cronFieldTypes[i].SpecialCharacters)
if err != nil {
return err
}
}

return nil
}

func validateSubExpression(subExpression string, fieldType fieldType, specialCharacters map[string]struct{}) error {
specialCharactersTmp := make(map[string]struct{})
for k, v := range specialCharacters {
specialCharactersTmp[k] = v
}

numberMatched, err := regexp.MatchString("^[0-9]+$", subExpression)
if err != nil {
return err
}

stringMatched, err := regexp.MatchString("^[a-z,A-Z]+$", strings.ToUpper(subExpression))
if err != nil {
return err
}

if strings.Contains(subExpression, ",") {
if _, ok := specialCharacters[","]; !ok {
return fmt.Errorf("the character %s is not allowed in field %s", "\",\"", fieldType.Field)
}
subExp := strings.Split(subExpression, ",")

delete(specialCharactersTmp, ",")
delete(specialCharactersTmp, "*")
delete(specialCharactersTmp, "?")
for _, subField := range subExp {
err := validateSubExpression(subField, fieldType, specialCharactersTmp)
if err != nil {
return err
}
}
} else if strings.Contains(subExpression, "/") {
if _, ok := specialCharacters["/"]; !ok {
return fmt.Errorf("the character %s is not allowed in field %s", "\"/\"", fieldType.Field)
}
subExp := strings.Split(subExpression, "/")
if len(subExp) != 2 {
return fmt.Errorf("invalid cron expression: %s", subExpression)
}
delete(specialCharactersTmp, ",")
delete(specialCharactersTmp, "/")
delete(specialCharactersTmp, "#")
delete(specialCharactersTmp, "?")
specialCharactersTmp["*"] = struct{}{}
err := validateSubExpression(subExp[0], fieldType, specialCharactersTmp)
if err != nil {
return err
}

delete(specialCharactersTmp, "*")
delete(specialCharactersTmp, "L")
delete(specialCharactersTmp, "W")
fieldTypeTmp := fieldType
fieldTypeTmp.MinValue = 1
fieldTypeTmp.Field = cronField("step of " + string(fieldType.Field))
err = validateSubExpression(subExp[1], fieldTypeTmp, specialCharactersTmp)
if err != nil {
return err
}
} else if strings.Contains(subExpression, "-") {
if _, ok := specialCharacters["-"]; !ok {
return fmt.Errorf("the character %s is not allowed in field %s", "-", fieldType.Field)
}
subExp := strings.Split(subExpression, "-")
if len(subExp) != 2 {
return fmt.Errorf("invalid cron expression: %s", subExpression)
}
delete(specialCharactersTmp, ",")
delete(specialCharactersTmp, "*")
delete(specialCharactersTmp, "/")
delete(specialCharactersTmp, "-")
delete(specialCharactersTmp, "?")
err := validateSubExpression(subExp[0], fieldType, specialCharactersTmp)
if err != nil {
return err
}
err = validateSubExpression(subExp[1], fieldType, specialCharactersTmp)
if err != nil {
return err
}
} else if strings.Contains(subExpression, "#") {
if _, ok := specialCharacters["#"]; !ok {
return fmt.Errorf("the character %s is not allowed in field %s", "#", fieldType.Field)
}
subExp := strings.Split(subExpression, "#")
if len(subExp) != 2 {
return fmt.Errorf("invalid cron expression: %s", subExpression)
}
err := validateSubExpression(subExp[0], fieldType, map[string]struct{}{})
if err != nil {
return err
}
err = validateSubExpression(subExp[1], fieldType, map[string]struct{}{})
if err != nil {
return err
}
} else if strings.Contains(subExpression, "L") && !strings.Contains(subExpression, "JUL") {
if _, ok := specialCharacters["L"]; !ok {
return fmt.Errorf("the character %s is not allowed in field %s", "L", fieldType.Field)
}

return errors.New("L is not supported")
} else if strings.Contains(subExpression, "W") {
if _, ok := specialCharacters["W"]; !ok {
return fmt.Errorf("the character %s is not allowed in field %s", "\"W\"", fieldType.Field)
}

return errors.New("W is not supported")
} else if strings.Contains(subExpression, "?") {
if _, ok := specialCharacters["?"]; !ok {
return fmt.Errorf("the character %s is not allowed in field %s", "\"?\"", fieldType.Field)
}

return errors.New("? is not supported")
} else if strings.Contains(subExpression, "*") {
if _, ok := specialCharacters["*"]; !ok {
return fmt.Errorf("the character %s is not allowed in field %s", "*", fieldType.Field)
}

return nil
} else if numberMatched {
value, err := strconv.Atoi(subExpression)
if err != nil {
return err
}
if value < fieldType.MinValue || value > fieldType.MaxValue {
return fmt.Errorf("the value %d in %s must be between %d and %d",
value, fieldType.Field, fieldType.MinValue, fieldType.MaxValue)
}
} else if stringMatched {
if fieldType.Field != cronFieldMonth && fieldType.Field != cronFieldDayOfWeek {
return fmt.Errorf("the value in %s must be number: %s", fieldType.Field, subExpression)
}
if fieldType.Field == cronFieldMonth {
find := false
for _, month := range months {
if month == strings.ToUpper(subExpression) {
find = true
break
}
}
if !find {
return fmt.Errorf("invalid cron expression: %s", subExpression)
}
} else if fieldType.Field == cronFieldDayOfWeek {
find := false
for _, day := range days {
if day == strings.ToUpper(subExpression) {
find = true
break
}
}
if !find {
return fmt.Errorf("invalid cron expression: %s", subExpression)
}
}
} else {
return fmt.Errorf("invalid cron expression: %s", subExpression)
}

return nil
}
31 changes: 20 additions & 11 deletions cron_test.go
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
package chrono

import (
"github.com/stretchr/testify/assert"
"testing"
"time"

"github.com/stretchr/testify/assert"
)

const timeLayout = "2006-01-02 15:04:05"
Expand Down Expand Up @@ -631,16 +632,24 @@ func TestParseCronExpression_Errors(t *testing.T) {
errorString string
}{
{expression: "", errorString: "cron expression must not be empty"},
{expression: "test * * * * *", errorString: "the value in field SECOND must be number : test"},
{expression: "5 * * * *", errorString: "cron expression must consist of 6 fields : found 5 in \"5 * * * *\""},
{expression: "61 * * * * *", errorString: "the value in field SECOND must be between 0 and 59"},
{expression: "61 * * * * *", errorString: "the value in field SECOND must be between 0 and 59"},
{expression: "* 65 * * * *", errorString: "the value in field MINUTE must be between 0 and 59"},
{expression: "* * * 0 * *", errorString: "the value in field DAY_OF_MONTH must be between 1 and 31"},
{expression: "* * 1-12/0 * * *", errorString: "step must be 1 or higher in \"1-12/0\""},
{expression: "* * 0-32/5 * * *", errorString: "the value in field HOUR must be between 0 and 23"},
{expression: "* * * * 0-10/2 *", errorString: "the value in field MONTH must be between 1 and 12"},
{expression: "* * 1-12/test * * *", errorString: "step must be number : \"test\""},
{expression: "* *", errorString: "cron expression must consist of 6 fields: found 2 fields in '* *'"},
{expression: "* * * * * * *", errorString: "cron expression must consist of 6 fields: Chrono isn't support for 7 fields"},
{expression: "test * * * * *", errorString: "the value in SECOND must be number: test"},
{expression: "5 * * * *", errorString: "cron expression must consist of 6 fields: found 5 fields in '5 * * * *'"},
{expression: "61 * * * * *", errorString: "the value 61 in SECOND must be between 0 and 59"},
{expression: "* 65 * * * *", errorString: "the value 65 in MINUTE must be between 0 and 59"},
{expression: "* * * 0 * *", errorString: "the value 0 in DAY_OF_MONTH must be between 1 and 31"},
{expression: "* * 1-12/0 * * *", errorString: "the value 0 in step of HOUR must be between 1 and 23"},
{expression: "* * 0-32/5 * * *", errorString: "the value 32 in HOUR must be between 0 and 23"},
{expression: "* * * * 0-10/2 *", errorString: "the value 0 in MONTH must be between 1 and 12"},
{expression: "* * 1-12/test * * *", errorString: "the value in step of HOUR must be number: test"},
{expression: "* * * L * *", errorString: "L is not supported"},
{expression: "* * * 1W * *", errorString: "W is not supported"},
{expression: "* * * ? * *", errorString: "? is not supported"},
{expression: "* * * * * L/2", errorString: "L is not supported"},
{expression: "* * * * * 2/", errorString: "invalid cron expression: "},
{expression: "* * * * * 2/2/2", errorString: "invalid cron expression: 2/2/2"},
{expression: "* 2,3,5,6,* * * * *", errorString: "the character * is not allowed in field MINUTE"},
}

for _, testCase := range testCases {
Expand Down