├── .github └── workflows │ └── test_and_coverage.yml ├── LICENSE ├── README.md ├── cmd ├── expression.go ├── expression_test.go ├── next.go ├── next_test.go ├── root.go ├── translate.go └── translate_test.go ├── ezcron.go ├── go.mod ├── go.sum └── translator ├── translator.go └── translator_test.go /.github/workflows/test_and_coverage.yml: -------------------------------------------------------------------------------- 1 | name: build 2 | on: 3 | push: 4 | branches: 5 | - master 6 | jobs: 7 | test_and_coverage: 8 | env: 9 | CC_TEST_REPORTER_ID: ${{ secrets.CC_TEST_REPORTER_ID }} 10 | name: go test 11 | runs-on: ubuntu-latest 12 | steps: 13 | - name: Set up Go 1.14 14 | uses: actions/setup-go@v2 15 | with: 16 | go-version: 1.14 17 | - name: Check out source code 18 | uses: actions/checkout@v2 19 | - name: Setup CodeClimate test-report 20 | run: | 21 | curl -L https://codeclimate.com/downloads/test-reporter/test-reporter-latest-linux-amd64 > ./cc-test-reporter 22 | chmod +x ./cc-test-reporter 23 | ./cc-test-reporter before-build 24 | - name: Execute test 25 | run: | 26 | go test ./cmd ./translator -coverprofile c.out 27 | - name: After-build CodeClimate test-report 28 | run: | 29 | ./cc-test-reporter after-build -p "github.com/rueyaa332266/ezcron" --exit-code $? -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2020 JYE RUEY 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Ezcron 2 | 3 | [![Go Report Card](https://goreportcard.com/badge/github.com/rueyaa332266/ezcron)](https://goreportcard.com/report/github.com/rueyaa332266/ezcron) 4 | ![Software License](https://img.shields.io/badge/license-MIT-brightgreen.svg?style=flat-square) 5 | 6 | Ezcron is a CLI tool, helping you deal with cron expression easier. 7 | 8 | ## Feature 9 | 10 | - Creating cron expression with prompts 11 | - Translate cron expression into human-friendly language 12 | - Show the next execute time 13 | - And more (keep working) ... 14 | 15 | ## DEMO 16 | 17 | Create cron expression like a boss 😎 18 | 19 | ![demo](https://github.com/rueyaa332266/assets/raw/master/ezcron/daily_schedule.gif) 20 | 21 | See more DEMO at [example](#Example) 22 | 23 | ## CRON Expression Format 24 | 25 | Only support 5 fields. 26 | ``` 27 | # ┌───────────── minute (0 - 59) 28 | # │ ┌───────────── hour (0 - 23) 29 | # │ │ ┌───────────── day of month (1 - 31) 30 | # │ │ │ ┌───────────── month (1 - 12) 31 | # │ │ │ │ ┌───────────── day of week (0 - 6) (Sunday to Saturday) 32 | # │ │ │ │ │ 33 | # │ │ │ │ │ 34 | # │ │ │ │ │ 35 | # * * * * * 36 | ------------------------------------------------------------------------ 37 | Field name Mandatory? Allowed values Allowed special characters 38 | ---------- ---------- -------------- -------------------------- 39 | Minutes Yes 0-59 * / , - 40 | Hours Yes 0-23 * / , - 41 | Day of month Yes 1-31 * / , - 42 | Month Yes 1-12 or JAN-DEC * / , - 43 | Day of week Yes 0-6 or SUN-SAT * / , - 44 | ``` 45 | 46 | ## Installing 47 | 48 | ``` 49 | go get -u github.com/rueyaa332266/ezcron 50 | ``` 51 | 52 | ## Usage 53 | 54 | ``` 55 | Usage: 56 | ezcron [flags] 57 | ezcron [command] 58 | 59 | Available Commands: 60 | expression Create a cron expression 61 | help Help about any command 62 | next Return next execute time 63 | translate Translate into human-friendly language 64 | 65 | Flags: 66 | -h, --help help for ezcron 67 | 68 | Use "ezcron [command] --help" for more information about a command. 69 | ``` 70 | 71 | ## Example 72 | 73 | ### Create cron expression 74 | 75 | ```shell 76 | $ ezcron expression 77 | ``` 78 | Five types of schedule are available. 79 | 80 | - #### Time schedule: 81 | 82 | Create a schedule in time intervals. 83 | - every_miniute {X_MINUTE} 84 | - every_hour {X_HOUR} 85 | 86 | ![demo](https://github.com/rueyaa332266/assets/raw/master/ezcron/time_schedule.gif) 87 | 88 | - #### Daily schedule: 89 | 90 | Create a daily schedule at specific time. 91 | - every_day 92 | - every_day at {HH:MM} 93 | 94 | ![demo](https://github.com/rueyaa332266/assets/raw/master/ezcron/daily_schedule.gif) 95 | 96 | - #### Weekly schedule: 97 | 98 | Create a weekly schedule on specific weekday at specific time. 99 | - on_every {WEEKDAY} 100 | - on_every {WEEKDAY} at {HH:MM} 101 | 102 | ![demo](https://github.com/rueyaa332266/assets/raw/master/ezcron/weekly_schedule.gif) 103 | 104 | - #### Monthly schedule 105 | 106 | Create a monthly schedule on specific monthday at specific time. 107 | - on {MONTHDAY} of_every_month 108 | - on {MONTHDAY} of_every_month at {HH:MM} 109 | - on {MONTHDAY} of_every {X_MONTH} 110 | - on {MONTHDAY} of_every {X_MONTH} at {HH:MM} 111 | 112 | ![demo](https://github.com/rueyaa332266/assets/raw/master/ezcron/monthly_schedule.gif) 113 | 114 | - #### Yearly schedule 115 | 116 | Create a yearly schedule on specific date at specific time. 117 | - in_every {MONTH} {MONTHDAY} 118 | - in_every {MONTH} {MONTHDAY} at {HH:MM} 119 | 120 | ![demo](https://github.com/rueyaa332266/assets/raw/master/ezcron/yearly_schedule.gif) 121 | 122 | 123 | ### Translate cron expression 124 | 125 | ``` 126 | $ ezcron translate "* * * * *" 127 | At every minute 128 | ``` 129 | 130 | It also works when passing the cron expression by pipe. 131 | ``` 132 | $ echo "* * * * *" | ezcron 133 | At every minute 134 | ``` 135 | 136 | ### Show next execute time 137 | 138 | ``` 139 | $ ezcron next "* * * * *" 140 | Next execute time: 2020-05-10 22:35:00 +0900 JST 141 | ``` -------------------------------------------------------------------------------- /cmd/expression.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "regexp" 7 | "strconv" 8 | "strings" 9 | 10 | "github.com/c-bata/go-prompt" 11 | "github.com/rueyaa332266/ezcron/translator" 12 | "github.com/spf13/cobra" 13 | ) 14 | 15 | var scheduleTypeSuggest = []prompt.Suggest{ 16 | {Text: "Time_schedule:", Description: "Create a schedule in time intervals"}, 17 | {Text: "Daily_schedule:", Description: "Create a daily schedule at specific time"}, 18 | {Text: "Weekly_schedule:", Description: "Create a weekly schedule on specific weekday at specific time"}, 19 | {Text: "Monthly_schedule:", Description: "Create a monthly schedule on specific monthday at specific time"}, 20 | {Text: "Yearly_schedule:", Description: "Create a yearly schedule on specific date at specific time"}, 21 | } 22 | 23 | var dayWList = []string{"Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"} 24 | var monthList = []string{"January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"} 25 | var dayList = []string{"1", "2", "3", "4", "5", "6", "7", "8", "9", "10", "11", "12", "13", "14", "15", "16", "17", "18", "19", "20", "21", "22", "23", "24", "25", "26", "27", "28", "29", "30", "31"} 26 | 27 | func makeTimeSuggest(t string) []prompt.Suggest { 28 | var timeSuggest []prompt.Suggest 29 | var suggest prompt.Suggest 30 | for i := 0; i < 24; i++ { 31 | if t == "minute" && i > 0 { 32 | break 33 | } else if t == "hour" { 34 | hour := strconv.Itoa(i + 1) 35 | suggest = prompt.Suggest{Text: hour + "_hour"} 36 | timeSuggest = append(timeSuggest, suggest) 37 | } 38 | for j := 0; j < 60; j++ { 39 | if t == "hour" { 40 | break 41 | } else if t == "minute" { 42 | minute := strconv.Itoa(j + 1) 43 | suggest = prompt.Suggest{Text: minute + "_minute"} 44 | } else if t == "time" { 45 | hour := translator.AddZeorforTenDigit(strconv.Itoa(i)) 46 | minute := translator.AddZeorforTenDigit(strconv.Itoa(j)) 47 | suggest = prompt.Suggest{Text: hour + ":" + minute} 48 | } 49 | timeSuggest = append(timeSuggest, suggest) 50 | } 51 | } 52 | return timeSuggest 53 | } 54 | 55 | func makeWeekdaySuggest() []prompt.Suggest { 56 | var weekDaySuggest []prompt.Suggest 57 | for _, v := range dayWList { 58 | suggest := prompt.Suggest{Text: v, Description: "default at 00:00"} 59 | weekDaySuggest = append(weekDaySuggest, suggest) 60 | } 61 | return weekDaySuggest 62 | } 63 | 64 | func makeMonthdaySuggest() []prompt.Suggest { 65 | var monthDaySuggest []prompt.Suggest 66 | for i := range dayList { 67 | day := translator.OrdinalDay(dayList[i]) 68 | suggest := prompt.Suggest{Text: day + "_day", Description: "of month"} 69 | monthDaySuggest = append(monthDaySuggest, suggest) 70 | } 71 | return monthDaySuggest 72 | } 73 | 74 | func makeMonthdayNumberSuggest(src []string) []prompt.Suggest { 75 | var monthDayNumbersSuggest []prompt.Suggest 76 | for _, v := range src { 77 | suggest := prompt.Suggest{Text: translator.OrdinalDay(v), Description: "default at 00:00"} 78 | monthDayNumbersSuggest = append(monthDayNumbersSuggest, suggest) 79 | } 80 | return monthDayNumbersSuggest 81 | } 82 | 83 | func makeMonthNumSuggest() []prompt.Suggest { 84 | var monthNumSuggest []prompt.Suggest 85 | for i := 1; i < 13; i++ { 86 | suggest := prompt.Suggest{Text: strconv.Itoa(i) + "_month", Description: "default at 00:00"} 87 | monthNumSuggest = append(monthNumSuggest, suggest) 88 | } 89 | return monthNumSuggest 90 | } 91 | 92 | func makeMonthSuggest() []prompt.Suggest { 93 | var monthSuggest []prompt.Suggest 94 | for _, v := range monthList { 95 | suggest := prompt.Suggest{Text: v} 96 | monthSuggest = append(monthSuggest, suggest) 97 | } 98 | return monthSuggest 99 | } 100 | 101 | func executor(in string) { 102 | if in == "" { 103 | fmt.Println("Empty input") 104 | os.Exit(1) 105 | } 106 | 107 | // split and ignore space 108 | f := func(c rune) bool { 109 | return c == ' ' 110 | } 111 | inputs := strings.FieldsFunc(in, f) 112 | switch inputs[0] { 113 | case "Time_schedule:": 114 | executeTimeSchedule(inputs) 115 | case "Daily_schedule:": 116 | executeDailySchedule(inputs) 117 | case "Weekly_schedule:": 118 | executeWeeklySchedule(inputs) 119 | case "Monthly_schedule:": 120 | executeMonthlySchedule(inputs) 121 | case "Yearly_schedule:": 122 | executeYearlySchedule(inputs) 123 | default: 124 | fmt.Println("invalid input") 125 | } 126 | os.Exit(0) 127 | } 128 | 129 | func executeTimeSchedule(inputs []string) { 130 | if len(inputs) == 3 { 131 | last := inputs[len(inputs)-1] 132 | if strings.Contains(last, "minute") { 133 | fmt.Println("*/" + strings.Split(last, "_")[0] + " * * * *") 134 | } else if strings.Contains(last, "hour") { 135 | fmt.Println("* */" + strings.Split(last, "_")[0] + " * * *") 136 | } else { 137 | fmt.Println("Invalid time schedule") 138 | } 139 | } else { 140 | fmt.Println("Invalid time schedule") 141 | } 142 | } 143 | 144 | func executeDailySchedule(inputs []string) { 145 | if len(inputs) == 2 || len(inputs) == 4 { 146 | last := inputs[len(inputs)-1] 147 | re := regexp.MustCompile(`\d\d:\d\d`) 148 | if re.MatchString(last) { 149 | time := strings.Split(last, ":") 150 | minute := strings.TrimPrefix(time[1], "0") 151 | hour := strings.TrimPrefix(time[0], "0") 152 | fmt.Println(minute + " " + hour + " */1 * *") 153 | } else if last == "every_day" { 154 | fmt.Println("0 0 */1 * *") 155 | } else { 156 | fmt.Println("Invalid daily schedule") 157 | } 158 | } else { 159 | fmt.Println("Invalid daily schedule") 160 | } 161 | } 162 | 163 | func executeWeeklySchedule(inputs []string) { 164 | last := inputs[len(inputs)-1] 165 | if len(inputs) == 5 { 166 | weekDay := inputs[len(inputs)-3] 167 | re := regexp.MustCompile(`\d\d:\d\d`) 168 | if re.MatchString(last) && contains(dayWList, weekDay) { 169 | time := strings.Split(last, ":") 170 | minute := strings.TrimPrefix(time[1], "0") 171 | hour := strings.TrimPrefix(time[0], "0") 172 | fmt.Println(minute + " " + hour + " * * " + translator.WeekDayToNum(weekDay)) 173 | } else { 174 | fmt.Println("Invalid weekly schedule") 175 | } 176 | } else if len(inputs) == 3 { 177 | if contains(dayWList, last) { 178 | fmt.Println("0 0 * * " + translator.WeekDayToNum(last)) 179 | } else { 180 | fmt.Println("Invalid weekly schedule") 181 | } 182 | } else { 183 | fmt.Println("Invalid weekly schedule") 184 | } 185 | } 186 | 187 | func executeMonthlySchedule(inputs []string) { 188 | last := inputs[len(inputs)-1] 189 | if len(inputs) == 7 { 190 | monthDay := inputs[2] 191 | perMonth := inputs[4] 192 | re := regexp.MustCompile(`\d\d:\d\d`) 193 | if re.MatchString(last) && strings.Contains(monthDay, "_day") && strings.Contains(perMonth, "_month") { 194 | time := strings.Split(last, ":") 195 | minute := strings.TrimPrefix(time[1], "0") 196 | hour := strings.TrimPrefix(time[0], "0") 197 | re := regexp.MustCompile(`\d{1,2}`) 198 | monthDay := re.FindAllString(monthDay, 1)[0] 199 | perMonth := strings.TrimRight(perMonth, "_month") 200 | fmt.Println(minute + " " + hour + " " + monthDay + " */" + perMonth + " *") 201 | } else { 202 | fmt.Println("Invalid monthly schedule") 203 | } 204 | } else if len(inputs) == 6 { 205 | monthDay := inputs[2] 206 | re := regexp.MustCompile(`\d\d:\d\d`) 207 | if re.MatchString(last) && strings.Contains(monthDay, "_day") { 208 | time := strings.Split(last, ":") 209 | minute := strings.TrimPrefix(time[1], "0") 210 | hour := strings.TrimPrefix(time[0], "0") 211 | re := regexp.MustCompile(`\d{1,2}`) 212 | monthDay := re.FindAllString(monthDay, 1)[0] 213 | fmt.Println(minute + " " + hour + " " + monthDay + " */1 *") 214 | } else { 215 | fmt.Println("Invalid monthly schedule") 216 | } 217 | } else if len(inputs) == 5 { 218 | monthDay := inputs[2] 219 | perMonth := inputs[4] 220 | if strings.Contains(monthDay, "_day") && strings.Contains(perMonth, "_month") { 221 | re := regexp.MustCompile(`\d{1,2}`) 222 | monthDay := re.FindAllString(monthDay, 1)[0] 223 | perMonth := strings.TrimRight(perMonth, "_month") 224 | fmt.Println("0 0 " + monthDay + " */" + perMonth + " *") 225 | } else { 226 | fmt.Println("Invalid monthly schedule") 227 | } 228 | } else if len(inputs) == 4 { 229 | monthDay := inputs[2] 230 | if strings.Contains(monthDay, "_day") { 231 | re := regexp.MustCompile(`\d{1,2}`) 232 | monthDay := re.FindAllString(monthDay, 1)[0] 233 | fmt.Println("0 0 " + monthDay + " */1 *") 234 | } else { 235 | fmt.Println("Invalid monthly schedule") 236 | } 237 | } else { 238 | fmt.Println("Invalid monthly schedule") 239 | } 240 | } 241 | 242 | func executeYearlySchedule(inputs []string) { 243 | last := inputs[len(inputs)-1] 244 | if len(inputs) == 6 { 245 | month := inputs[2] 246 | monthDay := inputs[3] 247 | reTime := regexp.MustCompile(`\d\d:\d\d`) 248 | reDay := regexp.MustCompile(`^\d{1,2}[a-z]{2}$`) 249 | if reTime.MatchString(last) && contains(monthList, month) && reDay.MatchString(monthDay) { 250 | time := strings.Split(last, ":") 251 | minute := strings.TrimPrefix(time[1], "0") 252 | hour := strings.TrimPrefix(time[0], "0") 253 | re := regexp.MustCompile(`\d{1,2}`) 254 | monthDay := re.FindAllString(monthDay, 1)[0] 255 | month := translator.MonthToNum(month) 256 | fmt.Println(minute + " " + hour + " " + monthDay + " " + month + " *") 257 | } else { 258 | fmt.Println("Invalid yearly schedule") 259 | } 260 | } else if len(inputs) == 4 { 261 | month := inputs[2] 262 | monthDay := inputs[3] 263 | reDay := regexp.MustCompile(`^\d{1,2}[a-z]{2}$`) 264 | if contains(monthList, month) && reDay.MatchString(monthDay) { 265 | re := regexp.MustCompile(`\d{1,2}`) 266 | monthDay := re.FindAllString(monthDay, 1)[0] 267 | month := translator.MonthToNum(month) 268 | fmt.Println("0 0 " + monthDay + " " + month + " *") 269 | } else { 270 | fmt.Println("Invalid yearly schedule") 271 | } 272 | } else { 273 | fmt.Println("Invalid yearly schedule") 274 | } 275 | } 276 | 277 | func completer(in prompt.Document) []prompt.Suggest { 278 | args := strings.Split(in.TextBeforeCursor(), " ") 279 | var suggest []prompt.Suggest 280 | var sub string 281 | if len(args) <= 1 { 282 | return prompt.FilterHasPrefix(scheduleTypeSuggest, args[0], true) 283 | } 284 | switch args[0] { 285 | case "Time_schedule:": 286 | suggest, sub = completeTimeSchedule(args) 287 | case "Daily_schedule:": 288 | suggest, sub = completeDailySchedule(args) 289 | case "Weekly_schedule:": 290 | suggest, sub = completeWeeklySchedule(args) 291 | case "Monthly_schedule:": 292 | suggest, sub = completeMonthlySchedule(args) 293 | case "Yearly_schedule:": 294 | suggest, sub = completeYearlySchedule(args) 295 | } 296 | return prompt.FilterHasPrefix(suggest, sub, true) 297 | } 298 | 299 | func completeTimeSchedule(args []string) ([]prompt.Suggest, string) { 300 | var suggest []prompt.Suggest 301 | sub := args[1] 302 | if len(args) == 2 { 303 | suggest = []prompt.Suggest{{Text: "every_minute", Description: "per minute"}, {Text: "every_hour", Description: "per hour"}} 304 | return suggest, sub 305 | } 306 | sub = args[2] 307 | if len(args) == 3 { 308 | suggest = makeSuggestByPreWord(args[1]) 309 | } 310 | return suggest, sub 311 | } 312 | 313 | func completeDailySchedule(args []string) ([]prompt.Suggest, string) { 314 | var suggest []prompt.Suggest 315 | sub := args[1] 316 | if len(args) == 2 { 317 | suggest = []prompt.Suggest{{Text: "every_day", Description: "default at 00:00"}} 318 | return suggest, sub 319 | } 320 | sub = args[2] 321 | if args[1] == "every_day" { 322 | if len(args) == 3 { 323 | suggest = []prompt.Suggest{{Text: "at", Description: "__:__"}} 324 | return suggest, sub 325 | } 326 | sub = args[3] 327 | if len(args) == 4 { 328 | suggest = makeSuggestByPreWord(args[2]) 329 | } 330 | } 331 | return suggest, sub 332 | } 333 | 334 | func completeWeeklySchedule(args []string) ([]prompt.Suggest, string) { 335 | var suggest []prompt.Suggest 336 | sub := args[1] 337 | if len(args) == 2 { 338 | suggest = []prompt.Suggest{{Text: "on_every", Description: "weekday"}} 339 | return suggest, sub 340 | } 341 | sub = args[2] 342 | if args[1] == "on_every" { 343 | if len(args) == 3 { 344 | suggest = makeWeekdaySuggest() 345 | return suggest, sub 346 | } 347 | sub = args[3] 348 | if contains(dayWList, args[2]) { 349 | if len(args) == 4 { 350 | suggest = []prompt.Suggest{{Text: "at", Description: "__:__"}} 351 | return suggest, sub 352 | } 353 | sub = args[4] 354 | if len(args) == 5 { 355 | suggest = makeSuggestByPreWord(args[3]) 356 | } 357 | } 358 | } 359 | return suggest, sub 360 | } 361 | 362 | func completeMonthlySchedule(args []string) ([]prompt.Suggest, string) { 363 | var suggest []prompt.Suggest 364 | sub := args[1] 365 | if len(args) == 2 { 366 | suggest = []prompt.Suggest{{Text: "on", Description: "monthday"}} 367 | return suggest, sub 368 | } 369 | sub = args[2] 370 | if args[1] == "on" { 371 | if len(args) == 3 { 372 | suggest = makeMonthdaySuggest() 373 | return suggest, sub 374 | } 375 | sub = args[3] 376 | if strings.Contains(args[2], "_day") { 377 | if len(args) == 4 { 378 | suggest = []prompt.Suggest{{Text: "of_every_month", Description: "per month, default at 00:00"}, {Text: "of_every", Description: "period of month"}} 379 | return suggest, sub 380 | } 381 | sub = args[4] 382 | switch args[3] { 383 | case "of_every_month": 384 | if len(args) == 5 { 385 | suggest = []prompt.Suggest{{Text: "at", Description: "__:__"}} 386 | return suggest, sub 387 | } 388 | sub = args[5] 389 | if len(args) == 6 { 390 | suggest = makeSuggestByPreWord(args[4]) 391 | return suggest, sub 392 | } 393 | case "of_every": 394 | if len(args) == 5 { 395 | suggest = makeMonthNumSuggest() 396 | return suggest, sub 397 | } 398 | sub = args[5] 399 | if strings.Contains(args[4], "_month") { 400 | if len(args) == 6 { 401 | suggest = []prompt.Suggest{{Text: "at", Description: "__:__"}} 402 | return suggest, sub 403 | } 404 | sub = args[6] 405 | if len(args) == 7 { 406 | suggest = makeSuggestByPreWord(args[5]) 407 | } 408 | } 409 | } 410 | } 411 | } 412 | return suggest, sub 413 | } 414 | 415 | func completeYearlySchedule(args []string) ([]prompt.Suggest, string) { 416 | var suggest []prompt.Suggest 417 | sub := args[1] 418 | if len(args) == 2 { 419 | suggest = []prompt.Suggest{{Text: "in_every", Description: "month_day"}} 420 | return suggest, sub 421 | } 422 | sub = args[2] 423 | if args[1] == "in_every" { 424 | if len(args) == 3 { 425 | suggest = makeMonthSuggest() 426 | return suggest, sub 427 | } 428 | sub = args[3] 429 | if contains(monthList, args[2]) { 430 | if len(args) == 4 { 431 | switch args[2] { 432 | case "February": 433 | suggest = makeMonthdayNumberSuggest(dayList[:28]) 434 | case "April", "June", "September", "November": 435 | suggest = makeMonthdayNumberSuggest(dayList[:30]) 436 | default: 437 | suggest = makeMonthdayNumberSuggest(dayList) 438 | } 439 | return suggest, sub 440 | } 441 | sub = args[4] 442 | re := regexp.MustCompile(`^\d{1,2}[a-z]{2}$`) 443 | if re.MatchString(args[3]) { 444 | if len(args) == 5 { 445 | suggest = []prompt.Suggest{{Text: "at", Description: "__:__"}} 446 | return suggest, sub 447 | } 448 | sub = args[5] 449 | if len(args) == 6 { 450 | suggest = makeSuggestByPreWord(args[4]) 451 | } 452 | } 453 | } 454 | } 455 | return suggest, sub 456 | } 457 | 458 | func makeSuggestByPreWord(pre string) []prompt.Suggest { 459 | var suggest []prompt.Suggest 460 | switch pre { 461 | case "at": 462 | suggest = makeTimeSuggest("time") 463 | case "every_minute": 464 | suggest = makeTimeSuggest("minute") 465 | case "every_hour": 466 | suggest = makeTimeSuggest("hour") 467 | } 468 | return suggest 469 | } 470 | 471 | func contains(slice []string, str string) bool { 472 | for _, v := range slice { 473 | if v == str { 474 | return true 475 | } 476 | } 477 | return false 478 | } 479 | 480 | var cmdExpression = &cobra.Command{ 481 | Use: "expression", 482 | Short: "Create a cron expression", 483 | Long: `Create a cron expression with prompt`, 484 | Run: func(cmd *cobra.Command, args []string) { 485 | fmt.Println("Follow the prompts and create the cron expression") 486 | p := prompt.New( 487 | executor, 488 | completer, 489 | prompt.OptionPrefix("Press tab for prompts >> "), 490 | prompt.OptionTitle("Create cron expression"), 491 | ) 492 | p.Run() 493 | }, 494 | } 495 | -------------------------------------------------------------------------------- /cmd/expression_test.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "reflect" 5 | "strconv" 6 | "testing" 7 | 8 | "github.com/c-bata/go-prompt" 9 | "github.com/rueyaa332266/ezcron/translator" 10 | ) 11 | 12 | func TestMakeTimeSuggest_Time(t *testing.T) { 13 | var want []prompt.Suggest 14 | got := makeTimeSuggest("time") 15 | for h := 0; h <= 23; h++ { 16 | for m := 0; m <= 59; m++ { 17 | min := translator.AddZeorforTenDigit(strconv.Itoa(m)) 18 | hour := translator.AddZeorforTenDigit(strconv.Itoa(h)) 19 | suggest := prompt.Suggest{Text: hour + ":" + min} 20 | want = append(want, suggest) 21 | } 22 | } 23 | if !reflect.DeepEqual(got, want) { 24 | t.Errorf("Error when making time suggest") 25 | } 26 | } 27 | 28 | func TestMakeTimeSuggest_Minute(t *testing.T) { 29 | var want []prompt.Suggest 30 | got := makeTimeSuggest("minute") 31 | for m := 1; m < 61; m++ { 32 | min := strconv.Itoa(m) 33 | suggest := prompt.Suggest{Text: min + "_minute"} 34 | want = append(want, suggest) 35 | } 36 | if !reflect.DeepEqual(got, want) { 37 | t.Errorf("Error when making minute suggest") 38 | } 39 | } 40 | 41 | func TestMakeTimeSuggest_Hour(t *testing.T) { 42 | var want []prompt.Suggest 43 | got := makeTimeSuggest("hour") 44 | for h := 1; h < 25; h++ { 45 | hour := strconv.Itoa(h) 46 | suggest := prompt.Suggest{Text: hour + "_hour"} 47 | want = append(want, suggest) 48 | } 49 | if !reflect.DeepEqual(got, want) { 50 | t.Errorf("Error when making hour suggest") 51 | } 52 | } 53 | 54 | func TestMakeWeekdaySuggest(t *testing.T) { 55 | want := []prompt.Suggest{ 56 | {Text: "Sunday", Description: "default at 00:00"}, 57 | {Text: "Monday", Description: "default at 00:00"}, 58 | {Text: "Tuesday", Description: "default at 00:00"}, 59 | {Text: "Wednesday", Description: "default at 00:00"}, 60 | {Text: "Thursday", Description: "default at 00:00"}, 61 | {Text: "Friday", Description: "default at 00:00"}, 62 | {Text: "Saturday", Description: "default at 00:00"}, 63 | } 64 | got := makeWeekdaySuggest() 65 | if !reflect.DeepEqual(got, want) { 66 | t.Errorf("Error when making week day suggest") 67 | } 68 | } 69 | 70 | func TestMakeMonthdaySuggest(t *testing.T) { 71 | var want []prompt.Suggest 72 | got := makeMonthdaySuggest() 73 | for d := 1; d < 32; d++ { 74 | day := translator.OrdinalDay(strconv.Itoa(d)) 75 | suggest := prompt.Suggest{Text: day + "_day", Description: "of month"} 76 | want = append(want, suggest) 77 | } 78 | if !reflect.DeepEqual(got, want) { 79 | t.Errorf("Error when making month day suggest") 80 | } 81 | } 82 | 83 | func TestMakeMonthdayNumberSuggest(t *testing.T) { 84 | var want []prompt.Suggest 85 | checkList := [][]string{ 86 | dayList[:28], 87 | dayList[:30], 88 | dayList, 89 | } 90 | for d := 1; d < 32; d++ { 91 | day := translator.OrdinalDay(strconv.Itoa(d)) 92 | suggest := prompt.Suggest{Text: day, Description: "default at 00:00"} 93 | want = append(want, suggest) 94 | } 95 | wantList := [][]prompt.Suggest{ 96 | want[:28], 97 | want[:30], 98 | want, 99 | } 100 | for i := range checkList { 101 | got := makeMonthdayNumberSuggest(checkList[i]) 102 | if !reflect.DeepEqual(got, wantList[i]) { 103 | t.Errorf("Error when making month day suggest") 104 | } 105 | } 106 | } 107 | 108 | func TestMakeMonthNumSuggest(t *testing.T) { 109 | want := []prompt.Suggest{ 110 | {Text: "1_month", Description: "default at 00:00"}, 111 | {Text: "2_month", Description: "default at 00:00"}, 112 | {Text: "3_month", Description: "default at 00:00"}, 113 | {Text: "4_month", Description: "default at 00:00"}, 114 | {Text: "5_month", Description: "default at 00:00"}, 115 | {Text: "6_month", Description: "default at 00:00"}, 116 | {Text: "7_month", Description: "default at 00:00"}, 117 | {Text: "8_month", Description: "default at 00:00"}, 118 | {Text: "9_month", Description: "default at 00:00"}, 119 | {Text: "10_month", Description: "default at 00:00"}, 120 | {Text: "11_month", Description: "default at 00:00"}, 121 | {Text: "12_month", Description: "default at 00:00"}, 122 | } 123 | got := makeMonthNumSuggest() 124 | if !reflect.DeepEqual(got, want) { 125 | t.Errorf("Error when making month number suggest") 126 | } 127 | } 128 | 129 | func TestMakeMonthSuggest(t *testing.T) { 130 | want := []prompt.Suggest{ 131 | {Text: "January"}, 132 | {Text: "February"}, 133 | {Text: "March"}, 134 | {Text: "April"}, 135 | {Text: "May"}, 136 | {Text: "June"}, 137 | {Text: "July"}, 138 | {Text: "August"}, 139 | {Text: "September"}, 140 | {Text: "October"}, 141 | {Text: "November"}, 142 | {Text: "December"}, 143 | } 144 | got := makeMonthSuggest() 145 | if !reflect.DeepEqual(got, want) { 146 | t.Errorf("Error when making month suggest") 147 | } 148 | } 149 | 150 | func Example_executeTimeSchedule() { 151 | checkList := [][]string{ 152 | {"Time_schedule", "every_minute", "1_minute"}, 153 | {"Time_schedule", "every_hour", "1_hour"}, 154 | {"Time_schedule", "test"}, 155 | {"Time_schedule", "every_hour", "test"}, 156 | } 157 | for i := range checkList { 158 | executeTimeSchedule(checkList[i]) 159 | } 160 | 161 | // Output: 162 | // */1 * * * * 163 | // * */1 * * * 164 | // Invalid time schedule 165 | // Invalid time schedule 166 | } 167 | 168 | func Example_executeDailySchedule() { 169 | checkList := [][]string{ 170 | {"Daily_schedule", "every_day"}, 171 | {"Daily_schedule", "every_day", "at", "01:01"}, 172 | {"Daily_schedule"}, 173 | {"Daily_schedule", "test"}, 174 | } 175 | for i := range checkList { 176 | executeDailySchedule(checkList[i]) 177 | } 178 | 179 | // Output: 180 | // 0 0 */1 * * 181 | // 1 1 */1 * * 182 | // Invalid daily schedule 183 | // Invalid daily schedule 184 | } 185 | 186 | func Example_executeWeeklySchedule() { 187 | checkList := [][]string{ 188 | {"Weekly_schedule", "on_every", "Sunday"}, 189 | {"Weekly_schedule", "on_every", "Sunday", "at", "01:01"}, 190 | {"Weekly_schedule", "test"}, 191 | {"Weekly_schedule", "on_every", "test"}, 192 | {"Weekly_schedule", "on_every", "Sunday", "at", "test"}, 193 | } 194 | for i := range checkList { 195 | executeWeeklySchedule(checkList[i]) 196 | } 197 | 198 | // Output: 199 | // 0 0 * * 0 200 | // 1 1 * * 0 201 | // Invalid weekly schedule 202 | // Invalid weekly schedule 203 | // Invalid weekly schedule 204 | } 205 | 206 | func Example_executeMonthlySchedule() { 207 | checkList := [][]string{ 208 | {"Monthly_schedule", "on", "1st_day", "of_every_month"}, 209 | {"Monthly_schedule", "on", "1st_day", "of_every", "2_month"}, 210 | {"Monthly_schedule", "on", "1st_day", "of_every_month", "at", "01:01"}, 211 | {"Monthly_schedule", "on", "1st_day", "of_every", "2_month", "at", "01:01"}, 212 | {"Monthly_schedule", "test"}, 213 | {"Monthly_schedule", "on", "test", "of_every_month"}, 214 | {"Monthly_schedule", "on", "1st_day", "of_every", "test"}, 215 | {"Monthly_schedule", "on", "1st_day", "of_every_month", "at", "test"}, 216 | {"Monthly_schedule", "on", "1st_day", "of_every", "2_month", "at", "test"}, 217 | } 218 | for i := range checkList { 219 | executeMonthlySchedule(checkList[i]) 220 | } 221 | 222 | // Output: 223 | // 0 0 1 */1 * 224 | // 0 0 1 */2 * 225 | // 1 1 1 */1 * 226 | // 1 1 1 */2 * 227 | // Invalid monthly schedule 228 | // Invalid monthly schedule 229 | // Invalid monthly schedule 230 | // Invalid monthly schedule 231 | // Invalid monthly schedule 232 | } 233 | 234 | func Example_executeYearlySchedule() { 235 | checkList := [][]string{ 236 | {"Yearly_schedule", "in_every", "January", "1st"}, 237 | {"Yearly_schedule", "in_every", "January", "1st", "at", "01:01"}, 238 | {"Yearly_schedule", "in_every", "January", "test"}, 239 | {"Yearly_schedule", "in_every", "January", "1st", "at", "test"}, 240 | {"Yearly_schedule", "test"}, 241 | } 242 | for i := range checkList { 243 | executeYearlySchedule(checkList[i]) 244 | } 245 | 246 | // Output: 247 | // 0 0 1 1 * 248 | // 1 1 1 1 * 249 | // Invalid yearly schedule 250 | // Invalid yearly schedule 251 | // Invalid yearly schedule 252 | } 253 | 254 | func TestCompleteTimeSchedule(t *testing.T) { 255 | // var gotSuggest []prompt.Suggest 256 | // var gotSub string 257 | checkList := [][]string{ 258 | {"Time_schedule:", "e"}, 259 | {"Time_schedule:", "every_minute", "1"}, 260 | {"Time_schedule:", "every_hour", "2"}, 261 | } 262 | wantSuggestList := [][]prompt.Suggest{ 263 | {{Text: "every_minute", Description: "per minute"}, {Text: "every_hour", Description: "per hour"}}, 264 | makeTimeSuggest("minute"), 265 | makeTimeSuggest("hour"), 266 | } 267 | wantSubtList := []string{"e", "1", "2"} 268 | for i := range checkList { 269 | gotSuggest, gotSub := completeTimeSchedule(checkList[i]) 270 | wantSuggest := wantSuggestList[i] 271 | wantSub := wantSubtList[i] 272 | if !reflect.DeepEqual(gotSuggest, wantSuggest) { 273 | t.Errorf("Got: %v, but want: %s", gotSuggest, wantSuggest) 274 | } 275 | if !reflect.DeepEqual(gotSub, wantSub) { 276 | t.Errorf("Got: %v, but want: %s", gotSub, wantSub) 277 | } 278 | } 279 | } 280 | 281 | func TestCompleteDailySchedule(t *testing.T) { 282 | // var gotSuggest []prompt.Suggest 283 | // var gotSub string 284 | checkList := [][]string{ 285 | {"Daily_schedule:", "e"}, 286 | {"Daily_schedule:", "every_day", "a"}, 287 | {"Daily_schedule:", "every_day", "at", "1"}, 288 | } 289 | wantSuggestList := [][]prompt.Suggest{ 290 | {{Text: "every_day", Description: "default at 00:00"}}, 291 | {{Text: "at", Description: "__:__"}}, 292 | makeTimeSuggest("time"), 293 | } 294 | wantSubtList := []string{"e", "a", "1"} 295 | for i := range checkList { 296 | gotSuggest, gotSub := completeDailySchedule(checkList[i]) 297 | wantSuggest := wantSuggestList[i] 298 | wantSub := wantSubtList[i] 299 | if !reflect.DeepEqual(gotSuggest, wantSuggest) { 300 | t.Errorf("Got: %v, but want: %s", gotSuggest, wantSuggest) 301 | } 302 | if !reflect.DeepEqual(gotSub, wantSub) { 303 | t.Errorf("Got: %v, but want: %s", gotSub, wantSub) 304 | } 305 | } 306 | } 307 | 308 | func TestCompleteWeeklySchedule(t *testing.T) { 309 | // var gotSuggest []prompt.Suggest 310 | // var gotSub string 311 | checkList := [][]string{ 312 | {"Weekly_schedule:", "o"}, 313 | {"Weekly_schedule:", "on_every", "S"}, 314 | {"Weekly_schedule:", "on_every", "Sunday", "a"}, 315 | {"Weekly_schedule:", "on_every", "Sunday", "at", "1"}, 316 | } 317 | wantSuggestList := [][]prompt.Suggest{ 318 | {{Text: "on_every", Description: "weekday"}}, 319 | makeWeekdaySuggest(), 320 | {{Text: "at", Description: "__:__"}}, 321 | makeTimeSuggest("time"), 322 | } 323 | wantSubtList := []string{"o", "S", "a", "1"} 324 | for i := range checkList { 325 | gotSuggest, gotSub := completeWeeklySchedule(checkList[i]) 326 | wantSuggest := wantSuggestList[i] 327 | wantSub := wantSubtList[i] 328 | if !reflect.DeepEqual(gotSuggest, wantSuggest) { 329 | t.Errorf("Got: %v, but want: %s", gotSuggest, wantSuggest) 330 | } 331 | if !reflect.DeepEqual(gotSub, wantSub) { 332 | t.Errorf("Got: %v, but want: %s", gotSub, wantSub) 333 | } 334 | } 335 | } 336 | func TestCompleteMonthlySchedule(t *testing.T) { 337 | // var gotSuggest []prompt.Suggest 338 | // var gotSub string 339 | checkList := [][]string{ 340 | {"Monthly_schedule:", "o"}, 341 | {"Monthly_schedule:", "on", "1"}, 342 | {"Monthly_schedule:", "on", "1st_day", "o"}, 343 | {"Monthly_schedule:", "on", "1st_day", "of_every_month", "a"}, 344 | {"Monthly_schedule:", "on", "1st_day", "of_every_month", "at", "1"}, 345 | {"Monthly_schedule:", "on", "1st_day", "of_every", "1"}, 346 | {"Monthly_schedule:", "on", "1st_day", "of_every", "1_month", "a"}, 347 | {"Monthly_schedule:", "on", "1st_day", "of_every", "1_month", "at", "1"}, 348 | } 349 | wantSuggestList := [][]prompt.Suggest{ 350 | {{Text: "on", Description: "monthday"}}, 351 | makeMonthdaySuggest(), 352 | {{Text: "of_every_month", Description: "per month, default at 00:00"}, {Text: "of_every", Description: "period of month"}}, 353 | {{Text: "at", Description: "__:__"}}, 354 | makeTimeSuggest("time"), 355 | makeMonthNumSuggest(), 356 | {{Text: "at", Description: "__:__"}}, 357 | makeTimeSuggest("time"), 358 | } 359 | wantSubtList := []string{"o", "1", "o", "a", "1", "1", "a", "1"} 360 | for i := range checkList { 361 | gotSuggest, gotSub := completeMonthlySchedule(checkList[i]) 362 | wantSuggest := wantSuggestList[i] 363 | wantSub := wantSubtList[i] 364 | if !reflect.DeepEqual(gotSuggest, wantSuggest) { 365 | t.Errorf("Got: %v, but want: %s", gotSuggest, wantSuggest) 366 | } 367 | if !reflect.DeepEqual(gotSub, wantSub) { 368 | t.Errorf("Got: %v, but want: %s", gotSub, wantSub) 369 | } 370 | } 371 | } 372 | 373 | func TestCompletYearlySchedule(t *testing.T) { 374 | var day []string 375 | for i := 1; i < 32; i++ { 376 | day = append(day, strconv.Itoa(i)) 377 | } 378 | day28 := day[:28] 379 | day30 := day[:30] 380 | f := func(src []string) []prompt.Suggest { 381 | var suggests []prompt.Suggest 382 | for _, v := range src { 383 | suggest := prompt.Suggest{Text: translator.OrdinalDay(v), Description: "default at 00:00"} 384 | suggests = append(suggests, suggest) 385 | } 386 | return suggests 387 | } 388 | checkList := [][]string{ 389 | {"Yearly_schedule:", "i"}, 390 | {"Yearly_schedule:", "in_every", "J"}, 391 | {"Yearly_schedule:", "in_every", "January", "1"}, 392 | {"Yearly_schedule:", "in_every", "February", "1"}, 393 | {"Yearly_schedule:", "in_every", "April", "1"}, 394 | {"Yearly_schedule:", "in_every", "January", "1st", "a"}, 395 | {"Yearly_schedule:", "in_every", "January", "1st", "at", "1"}, 396 | } 397 | wantSuggestList := [][]prompt.Suggest{ 398 | {{Text: "in_every", Description: "month_day"}}, 399 | makeMonthSuggest(), 400 | f(day), 401 | f(day28), 402 | f(day30), 403 | {{Text: "at", Description: "__:__"}}, 404 | makeTimeSuggest("time"), 405 | } 406 | wantSubtList := []string{"i", "J", "1", "1", "1", "a", "1"} 407 | for i := range checkList { 408 | gotSuggest, gotSub := completeYearlySchedule(checkList[i]) 409 | wantSuggest := wantSuggestList[i] 410 | wantSub := wantSubtList[i] 411 | if !reflect.DeepEqual(gotSuggest, wantSuggest) { 412 | t.Errorf("Got: %v, but want: %s", gotSuggest, wantSuggest) 413 | } 414 | if !reflect.DeepEqual(gotSub, wantSub) { 415 | t.Errorf("Got: %v, but want: %s", gotSub, wantSub) 416 | } 417 | } 418 | } 419 | 420 | func TestMakeSuggestByPreWord(t *testing.T) { 421 | checkList := []string{"at", "every_minute", "every_hour"} 422 | wantList := [][]prompt.Suggest{makeTimeSuggest("time"), makeTimeSuggest("minute"), makeTimeSuggest("hour")} 423 | for i := range checkList { 424 | got := makeSuggestByPreWord(checkList[i]) 425 | want := wantList[i] 426 | if !reflect.DeepEqual(got, want) { 427 | t.Errorf("Got: %v, but want: %s", got, want) 428 | } 429 | } 430 | } 431 | 432 | func TestContains(t *testing.T) { 433 | slice := []string{"foo", "bar"} 434 | checkList := []string{"foo", "buzz"} 435 | wantList := []bool{true, false} 436 | for i := range checkList { 437 | got := contains(slice, checkList[i]) 438 | want := wantList[i] 439 | if got != want { 440 | t.Errorf("got: %t; want: %t", got, want) 441 | } 442 | } 443 | } 444 | -------------------------------------------------------------------------------- /cmd/next.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "os" 7 | "strings" 8 | "time" 9 | 10 | cron "github.com/robfig/cron/v3" 11 | "github.com/rueyaa332266/ezcron/translator" 12 | "github.com/spf13/cobra" 13 | ) 14 | 15 | func nextExecTime(cronExpression string) (time.Time, error) { 16 | valid, _ := translator.MatchCronReg(cronExpression) 17 | if valid { 18 | cronExpression = translator.MonthToNum(cronExpression) 19 | cronExpression = translator.WeekDayToNum(cronExpression) 20 | sched, err := cron.ParseStandard(cronExpression) 21 | if err != nil { 22 | fmt.Println(cronExpression, "Invalid syntax") 23 | return time.Now(), err 24 | } 25 | return sched.Next(time.Now()), nil 26 | } 27 | fmt.Println(cronExpression, "Invalid syntax") 28 | err := errors.New("Invalid syntax") 29 | return time.Now(), err 30 | } 31 | 32 | var cmdNext = &cobra.Command{ 33 | Use: "next [cron expression]", 34 | Short: "Return next execute time", 35 | Long: `Show the next execute time when inputing cron expression`, 36 | Run: getNextTime, 37 | } 38 | 39 | func getNextTime(cmd *cobra.Command, args []string) { 40 | // show help message if got no args 41 | if len(args) < 1 { 42 | err := cmd.Help() 43 | if err != nil { 44 | fmt.Println(err) 45 | } 46 | os.Exit(0) 47 | } 48 | cronExpression := strings.Join(args, " ") 49 | valid, _ := translator.MatchCronReg(cronExpression) 50 | if valid { 51 | next, _ := nextExecTime(cronExpression) 52 | fmt.Println("Next execute time:", next) 53 | } else { 54 | fmt.Println("Invalid syntax") 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /cmd/next_test.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | 7 | "github.com/spf13/cobra" 8 | ) 9 | 10 | func TestNextExecTime(t *testing.T) { 11 | const layout = "2000-01-01 01:01:01" 12 | checkList := []string{"* * * * *", "* * * *"} 13 | for i := range checkList { 14 | got, err := nextExecTime(checkList[i]) 15 | if err != nil { 16 | want := "Invalid syntax" 17 | if err.Error() != want { 18 | t.Errorf("Want: %v", want) 19 | } 20 | } else { 21 | want := time.Now().Add(1 * time.Minute).Format(layout) 22 | if got.Format(layout) != want { 23 | t.Errorf("Got: %v, but want: %s", got, want) 24 | } 25 | } 26 | } 27 | } 28 | 29 | func Example_getNextTime() { 30 | var cmd *cobra.Command 31 | getNextTime(cmd, []string{"*", "*", "*", "*"}) 32 | 33 | // Output: 34 | // Invalid syntax 35 | } 36 | -------------------------------------------------------------------------------- /cmd/root.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "os" 7 | "strings" 8 | 9 | "github.com/rueyaa332266/ezcron/translator" 10 | "github.com/spf13/cobra" 11 | ) 12 | 13 | var rootCmd = &cobra.Command{ 14 | Use: "ezcron", 15 | Run: translateFromPipe, 16 | } 17 | 18 | func translateFromPipe(cmd *cobra.Command, args []string) { 19 | // input from pipe 20 | stat, _ := os.Stdin.Stat() 21 | if (stat.Mode() & os.ModeCharDevice) == 0 { 22 | buf := new(bytes.Buffer) 23 | buf.ReadFrom(os.Stdin) 24 | cronExpression := strings.TrimSuffix(buf.String(), "\n") 25 | valid, result := translator.MatchCronReg(cronExpression) 26 | if valid { 27 | translator.Explain(result) 28 | } else { 29 | fmt.Println("invalid syntax") 30 | os.Exit(1) 31 | } 32 | } else { 33 | cmd.Help() 34 | } 35 | } 36 | 37 | func init() { 38 | rootCmd.AddCommand(cmdNext) 39 | rootCmd.AddCommand(cmdTranslate) 40 | rootCmd.AddCommand(cmdExpression) 41 | } 42 | 43 | // Execute for cobra 44 | func Execute() { 45 | if err := rootCmd.Execute(); err != nil { 46 | fmt.Println(err) 47 | os.Exit(1) 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /cmd/translate.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "strings" 7 | 8 | "github.com/rueyaa332266/ezcron/translator" 9 | "github.com/spf13/cobra" 10 | ) 11 | 12 | var cmdTranslate = &cobra.Command{ 13 | Use: "translate [cron expression]", 14 | Short: "Translate into human-friendly language", 15 | Long: `Translate cron expression into human-friendly language`, 16 | Run: translate, 17 | } 18 | 19 | func translate(cmd *cobra.Command, args []string) { 20 | // show help message if got no args 21 | if len(args) < 1 { 22 | err := cmd.Help() 23 | if err != nil { 24 | fmt.Println(err) 25 | } 26 | os.Exit(0) 27 | } 28 | cronExpression := strings.Join(args, " ") 29 | valid, checkResult := translator.MatchCronReg(cronExpression) 30 | if valid { 31 | translator.Explain(checkResult) 32 | } else { 33 | fmt.Println("Invalid syntax") 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /cmd/translate_test.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "github.com/spf13/cobra" 5 | ) 6 | 7 | func Example_translate() { 8 | var cmd *cobra.Command 9 | checkList := [][]string{ 10 | {"*", "*", "*", "*", "*"}, 11 | {"*", "*", "*", "*"}, 12 | } 13 | for i := range checkList { 14 | translate(cmd, checkList[i]) 15 | } 16 | 17 | // Output: 18 | // At every minute 19 | // Invalid syntax 20 | } 21 | -------------------------------------------------------------------------------- /ezcron.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "github.com/rueyaa332266/ezcron/cmd" 4 | 5 | func main() { 6 | cmd.Execute() 7 | } 8 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/rueyaa332266/ezcron 2 | 3 | go 1.14 4 | 5 | require ( 6 | github.com/c-bata/go-prompt v0.2.3 7 | github.com/dustin/go-humanize v1.0.0 8 | github.com/mattn/go-colorable v0.1.7 // indirect 9 | github.com/mattn/go-runewidth v0.0.9 // indirect 10 | github.com/mattn/go-tty v0.0.3 // indirect 11 | github.com/pkg/term v0.0.0-20200520122047-c3ffed290a03 // indirect 12 | github.com/robfig/cron/v3 v3.0.1 13 | github.com/rueyaa332266/multiregexp v1.0.0 14 | github.com/spf13/cobra v1.0.0 15 | github.com/spf13/pflag v1.0.5 // indirect 16 | golang.org/x/sys v0.0.0-20200724161237-0e2f3a69832c // indirect 17 | ) 18 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= 2 | github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= 3 | github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= 4 | github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= 5 | github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= 6 | github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8= 7 | github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= 8 | github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= 9 | github.com/c-bata/go-prompt v0.2.3 h1:jjCS+QhG/sULBhAaBdjb2PlMRVaKXQgn+4yzaauvs2s= 10 | github.com/c-bata/go-prompt v0.2.3/go.mod h1:VzqtzE2ksDBcdln8G7mk2RX9QyGjH+OVqOCSiVIqS34= 11 | github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= 12 | github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= 13 | github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk= 14 | github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= 15 | github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= 16 | github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= 17 | github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= 18 | github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= 19 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 20 | github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= 21 | github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no= 22 | github.com/dustin/go-humanize v1.0.0 h1:VSnTsYCnlFHaM2/igO1h6X3HA71jcobQuxemgkq4zYo= 23 | github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= 24 | github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= 25 | github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= 26 | github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= 27 | github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= 28 | github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= 29 | github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= 30 | github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= 31 | github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4= 32 | github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= 33 | github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= 34 | github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= 35 | github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 36 | github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 37 | github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= 38 | github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= 39 | github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= 40 | github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= 41 | github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= 42 | github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= 43 | github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= 44 | github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM= 45 | github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= 46 | github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo= 47 | github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= 48 | github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q= 49 | github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= 50 | github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= 51 | github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= 52 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 53 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 54 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 55 | github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= 56 | github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= 57 | github.com/mattn/go-colorable v0.1.6 h1:6Su7aK7lXmJ/U79bYtBjLNaha4Fs1Rg9plHpcH+vvnE= 58 | github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= 59 | github.com/mattn/go-colorable v0.1.7 h1:bQGKb3vps/j0E9GfJQ03JyhRuxsvdAanXlT9BTw3mdw= 60 | github.com/mattn/go-colorable v0.1.7/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= 61 | github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= 62 | github.com/mattn/go-isatty v0.0.10/go.mod h1:qgIWMr58cqv1PHHyhnkY9lrL7etaEgOFcMEpPG5Rm84= 63 | github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY= 64 | github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= 65 | github.com/mattn/go-runewidth v0.0.6/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= 66 | github.com/mattn/go-runewidth v0.0.9 h1:Lm995f3rfxdpd6TSmuVCHVb/QhupuXlYr8sCI/QdE+0= 67 | github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= 68 | github.com/mattn/go-tty v0.0.3 h1:5OfyWorkyO7xP52Mq7tB36ajHDG5OHrmBGIS/DtakQI= 69 | github.com/mattn/go-tty v0.0.3/go.mod h1:ihxohKRERHTVzN+aSVRwACLCeqIoZAWpoICkkvrWyR0= 70 | github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= 71 | github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= 72 | github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= 73 | github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= 74 | github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= 75 | github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= 76 | github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 77 | github.com/pkg/term v0.0.0-20190109203006-aa71e9d9e942 h1:A7GG7zcGjl3jqAqGPmcNjd/D9hzL95SuoOQAaFNdLU0= 78 | github.com/pkg/term v0.0.0-20190109203006-aa71e9d9e942/go.mod h1:eCbImbZ95eXtAUIbLAuAVnBnwf83mjf6QIVH8SHYwqQ= 79 | github.com/pkg/term v0.0.0-20200520122047-c3ffed290a03 h1:pd4YKIqCB0U7O2I4gWHgEUA2mCEOENmco0l/bM957bU= 80 | github.com/pkg/term v0.0.0-20200520122047-c3ffed290a03/go.mod h1:Z9+Ul5bCbBKnbCvdOWbLqTHhJiYV414CURZJba6L8qA= 81 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 82 | github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= 83 | github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso= 84 | github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= 85 | github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= 86 | github.com/prometheus/common v0.0.0-20181113130724-41aa239b4cce/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= 87 | github.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= 88 | github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= 89 | github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= 90 | github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU= 91 | github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs= 92 | github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro= 93 | github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= 94 | github.com/rueyaa332266/multiregexp v0.0.0-20200614090502-8140685dca1e h1:Wgf6fRcCDY/NOdlcceDQKPumDxw0zw6+Y2enG6TTudc= 95 | github.com/rueyaa332266/multiregexp v0.0.0-20200614090502-8140685dca1e/go.mod h1:W34DbkcaCtzDCDBRDTZMZeUPVKTf9d7kQKNIIMtIJZU= 96 | github.com/rueyaa332266/multiregexp v0.0.0-20200727025633-2d4622bb11d5 h1:fYxLoggTRi3uUZKn49FQ3Ir96l39HUJt3hJ108Oa2OU= 97 | github.com/rueyaa332266/multiregexp v0.0.0-20200727025633-2d4622bb11d5/go.mod h1:W34DbkcaCtzDCDBRDTZMZeUPVKTf9d7kQKNIIMtIJZU= 98 | github.com/rueyaa332266/multiregexp v1.0.0 h1:FvIH3wkazkeVsSE7zCG3IhmsK3ZjS4NcAGJrbRfydmI= 99 | github.com/rueyaa332266/multiregexp v1.0.0/go.mod h1:W34DbkcaCtzDCDBRDTZMZeUPVKTf9d7kQKNIIMtIJZU= 100 | github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= 101 | github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= 102 | github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= 103 | github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM= 104 | github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= 105 | github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= 106 | github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= 107 | github.com/spf13/cobra v1.0.0 h1:6m/oheQuQ13N9ks4hubMG6BnvwOeaJrqSPLahSnczz8= 108 | github.com/spf13/cobra v1.0.0/go.mod h1:/6GTrnGXV9HjY+aR4k0oJ5tcvakLuG6EuKReYlHNrgE= 109 | github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= 110 | github.com/spf13/pflag v1.0.3 h1:zPAT6CGy6wXeQ7NtTnaTerfKOsV6V6F8agHXFiazDkg= 111 | github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= 112 | github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= 113 | github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= 114 | github.com/spf13/viper v1.4.0/go.mod h1:PTJ7Z/lr49W6bUbkmS1V3by4uWynFiR9p7+dSq/yZzE= 115 | github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 116 | github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 117 | github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= 118 | github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc= 119 | github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= 120 | github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q= 121 | go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= 122 | go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= 123 | go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= 124 | go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= 125 | golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= 126 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 127 | golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= 128 | golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 129 | golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 130 | golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 131 | golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 132 | golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 133 | golang.org/x/net v0.0.0-20190522155817-f3200d17e092/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= 134 | golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= 135 | golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 136 | golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 137 | golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 138 | golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 139 | golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 140 | golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 141 | golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 142 | golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 143 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a h1:1BGLXjeY4akVXGgbC9HugT3Jv3hCI0z56oJR5vAMgBU= 144 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 145 | golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 146 | golang.org/x/sys v0.0.0-20191008105621-543471e840be/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 147 | golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 148 | golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 149 | golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae h1:/WDfKMnPU+m5M4xB+6x4kaepxRw6jWvR5iDRdvjHgy8= 150 | golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 151 | golang.org/x/sys v0.0.0-20200724161237-0e2f3a69832c h1:UIcGWL6/wpCfyGuJnRFJRurA+yj8RrW7Q6x2YMCXt6c= 152 | golang.org/x/sys v0.0.0-20200724161237-0e2f3a69832c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 153 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 154 | golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= 155 | golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 156 | golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 157 | golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 158 | google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= 159 | google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= 160 | google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= 161 | google.golang.org/grpc v1.21.0/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= 162 | gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= 163 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 164 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 165 | gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo= 166 | gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74= 167 | gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 168 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 169 | honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 170 | -------------------------------------------------------------------------------- /translator/translator.go: -------------------------------------------------------------------------------- 1 | package translator 2 | 3 | import ( 4 | "fmt" 5 | "regexp" 6 | "strconv" 7 | "strings" 8 | 9 | "github.com/dustin/go-humanize" 10 | "github.com/rueyaa332266/multiregexp" 11 | ) 12 | 13 | // CheckResult is the reuslt of cronexpression feild. 14 | // "Valid" will be true when the expression is valid 15 | // "Input" is the value of the field 16 | // "Pattern" shows the feild pattern. Pattern has 5 types as following ["asterisk","number","comma","hyphen","slash"] 17 | type CheckResult struct { 18 | Valid bool 19 | Input string 20 | Pattern string 21 | } 22 | 23 | var dayList = [7]string{"Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"} 24 | var monthList = [12]string{"January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"} 25 | 26 | // MatchCronReg show checkResult of every field 27 | // Field name Mandatory? Allowed values Allowed special characters 28 | // ---------- ---------- -------------- -------------------------- 29 | // Minutes Yes 0-59 * / , - 30 | // Hours Yes 0-23 * / , - 31 | // Day of month Yes 1-31 * / , - 32 | // Month Yes 1-12 or JAN-DEC * / , - 33 | // Day of week Yes 0-6 or SUN-SAT * / , - 34 | func MatchCronReg(cronExpression string) (bool, map[string]*CheckResult) { 35 | var checkResults = make(map[string]*CheckResult) 36 | fieldSlice := strings.Split(cronExpression, " ") 37 | 38 | if len(fieldSlice) != 5 { 39 | return false, checkResults 40 | } 41 | 42 | for index, field := range fieldSlice { 43 | switch index { 44 | case 0: // minute 45 | checkResults["minute"] = checkFieldeReg(field, "minute") 46 | case 1: // hour 47 | checkResults["hour"] = checkFieldeReg(field, "hour") 48 | case 2: // day of the month 49 | checkResults["dayM"] = checkFieldeReg(field, "dayM") 50 | case 3: // month 51 | checkResults["month"] = checkFieldeReg(field, "month") 52 | case 4: // day of the week 53 | checkResults["dayW"] = checkFieldeReg(field, "dayW") 54 | } 55 | } 56 | valid := checkResults["minute"].Valid && checkResults["hour"].Valid && checkResults["dayM"].Valid && checkResults["month"].Valid && checkResults["dayW"].Valid 57 | return valid, checkResults 58 | } 59 | 60 | func checkFieldPattern(str string, patternList map[string]string) (bool, string) { 61 | var pattern string 62 | var valid bool 63 | var regs multiregexp.Regexps 64 | 65 | asteriskReg := regexp.MustCompile(`^` + patternList["asterisk"] + `$`) 66 | numberReg := regexp.MustCompile(`^` + patternList["number"] + `$`) 67 | commaReg := regexp.MustCompile(`^` + patternList["comma"] + `$`) 68 | hyphenReg := regexp.MustCompile(`^` + patternList["hyphen"] + `$`) 69 | slashReg := regexp.MustCompile(`^` + patternList["slash"] + `$`) 70 | 71 | regs = multiregexp.Append(regs, asteriskReg, numberReg, commaReg, hyphenReg, slashReg) 72 | match := regs.MatchStringWhich(str) 73 | 74 | if len(match) == 0 { 75 | valid = false 76 | pattern = "not match" 77 | } else if len(match) == 1 { 78 | valid = true 79 | switch match[0] { 80 | case 0: 81 | pattern = "asterisk" 82 | case 1: 83 | pattern = "number" 84 | case 2: 85 | pattern = "comma" 86 | case 3: 87 | pattern = "hyphen" 88 | case 4: 89 | pattern = "slash" 90 | } 91 | } 92 | 93 | return valid, pattern 94 | } 95 | 96 | // check the logic and format in some pattern 97 | func numLogicFormat(input string, pattern string) (bool, string) { 98 | valid := true 99 | output := input 100 | switch pattern { 101 | case "hyphen": 102 | slice := strings.Split(input, "-") 103 | valid = slice[0] < slice[1] 104 | case "comma": 105 | unique := uniqueSlice(strings.Split(input, ",")) 106 | output = strings.Join(unique, ",") 107 | case "slash": 108 | slice := strings.Split(input, "/") 109 | validLeft, strLeft := numLogicFormat(slice[0], checkFieldeReg(slice[0], "minute").Pattern) 110 | validRight, strRight := numLogicFormat(slice[1], checkFieldeReg(slice[1], "minute").Pattern) 111 | valid = validLeft && validRight 112 | output = strLeft + "/" + strRight 113 | } 114 | return valid, output 115 | } 116 | 117 | func checkFieldeReg(str string, feild string) *CheckResult { 118 | var number, comma string 119 | switch feild { 120 | case "minute": 121 | number = `([0-9]|[1-5][0-9])` 122 | comma = `(` + number + `\,){1,59}` + number 123 | case "hour": 124 | number = `([0-9]|1[0-9]|2[0-3])` 125 | comma = `(` + number + `\,){1,23}` + number 126 | case "dayM": 127 | number = `([1-9]|[1-2][0-9]|3[0-1])` 128 | comma = `(` + number + `\,){1,30}` + number 129 | case "month": 130 | number = `([1-9]|[1][0-2])` 131 | comma = `(` + number + `\,){1,11}` + number 132 | case "dayW": 133 | number = `([0-6])` 134 | comma = `(` + number + `\,){1,5}` + number 135 | } 136 | asterisk := `\*` 137 | hyphen := number + `\-` + number 138 | slash := `(` + asterisk + `|` + number + `|` + comma + `|` + hyphen + `)` + `\/` + `(` + number + `|` + comma + `)` 139 | patternList := map[string]string{"asterisk": asterisk, "number": number, "comma": comma, "hyphen": hyphen, "slash": slash} 140 | 141 | valid, pattern := checkFieldPattern(str, patternList) 142 | if valid { 143 | valid, str = numLogicFormat(str, pattern) 144 | } 145 | return &CheckResult{Valid: valid, Input: str, Pattern: pattern} 146 | } 147 | 148 | // Explain print out the result of the translation 149 | func Explain(cronCheckResults map[string]*CheckResult) { 150 | var explanation string 151 | minuteCheckResult := cronCheckResults["minute"] 152 | hourCheckResult := cronCheckResults["hour"] 153 | dayMonthCheckResult := cronCheckResults["dayM"] 154 | monthCheckResult := cronCheckResults["month"] 155 | dayWeekCheckResult := cronCheckResults["dayW"] 156 | // explain in HH:MM 157 | if minuteCheckResult.Pattern == hourCheckResult.Pattern && minuteCheckResult.Pattern == "number" { 158 | explanation += "At every " + AddZeorforTenDigit(hourCheckResult.Input) + ":" + AddZeorforTenDigit(minuteCheckResult.Input) 159 | } else { 160 | explanation += explainMinute(minuteCheckResult) 161 | explanationHour := explainHour(hourCheckResult) 162 | if explanationHour != "" { 163 | explanation += ", " + explanationHour 164 | } 165 | } 166 | // explain when one of day month and day week use "*/num" 167 | if dayMonthCheckResult.Pattern == "slash" && dayWeekCheckResult.Pattern != "asterisk" { 168 | sliceDayM := strings.Split(dayMonthCheckResult.Input, "/") 169 | slachLeftDayM := checkFieldeReg(sliceDayM[0], "dayM") 170 | explanationDayMonth := explainDayMonth(dayMonthCheckResult) 171 | explanationDayWeek := explainDayWeek(dayWeekCheckResult) 172 | if slachLeftDayM.Pattern == "asterisk" { 173 | explanation += ", " + explanationDayMonth + " if it's " + explanationDayWeek 174 | } else { 175 | explanation += ", " + explanationDayMonth + " and " + explanationDayWeek 176 | } 177 | } else if dayWeekCheckResult.Pattern == "slash" && dayMonthCheckResult.Pattern != "asterisk" { 178 | sliceDayW := strings.Split(dayWeekCheckResult.Input, "/") 179 | slachLeftDayW := checkFieldeReg(sliceDayW[0], "dayM") 180 | explanationDayMonth := explainDayMonth(dayMonthCheckResult) 181 | explanationDayWeek := explainDayWeek(dayWeekCheckResult) 182 | if slachLeftDayW.Pattern == "asterisk" { 183 | explanation += ", " + explanationDayMonth + " if it's " + explanationDayWeek 184 | } else { 185 | explanation += ", " + explanationDayMonth + " and " + explanationDayWeek 186 | } 187 | } else { 188 | explanationDayMonth := explainDayMonth(dayMonthCheckResult) 189 | explanationDayWeek := explainDayWeek(dayWeekCheckResult) 190 | if explanationDayMonth != "" && explanationDayWeek != "" { 191 | explanation += ", " + explanationDayMonth + " and " + explanationDayWeek 192 | } else if explanationDayMonth != "" { 193 | explanation += ", " + explanationDayMonth 194 | } else if explanationDayWeek != "" { 195 | explanation += ", " + explanationDayWeek 196 | } 197 | } 198 | explanationMonth := explainMonth(monthCheckResult) 199 | if explanationMonth != "" { 200 | explanation += ", " + explanationMonth 201 | } 202 | fmt.Println(explanation) 203 | } 204 | 205 | func explainMinute(c *CheckResult) string { 206 | explanation := "At" 207 | switch c.Pattern { 208 | case "asterisk": 209 | explanation += " every minute" 210 | case "number": 211 | minute, _ := strconv.Atoi(c.Input) 212 | explanation += " every " + humanize.Ordinal(minute) + " minute" 213 | case "comma": 214 | minute := c.Input 215 | slice := strings.Split(minute, ",") 216 | for i, v := range slice { 217 | slice[i] = OrdinalDay(v) 218 | } 219 | explanation += " every " + strings.Join(slice, " and ") + " minute" 220 | case "hyphen": 221 | minute := c.Input 222 | explanation += " every " + strings.Replace(minute, "-", " through ", 1) + " minute" 223 | case "slash": 224 | // "asterisk","number","comma","hyphen" / "number","comma" 225 | explanation += " every " + explaiMinuteSlach(c) 226 | } 227 | return explanation 228 | } 229 | 230 | func explaiMinuteSlach(c *CheckResult) string { 231 | minute := c.Input 232 | var output string 233 | slice := strings.Split(minute, "/") 234 | for i, v := range slice { 235 | CheckResult := checkFieldeReg(v, "minute") 236 | // check the left side of slash 237 | if i == 0 { 238 | switch CheckResult.Pattern { 239 | case "asterisk": 240 | output += "" 241 | case "number": 242 | if CheckResult.Input == "59" { 243 | output += " from minute 59" 244 | } else { 245 | output += " from minute " + CheckResult.Input + " through minute 59" 246 | } 247 | case "comma": 248 | minute := CheckResult.Input 249 | slice := strings.Split(minute, ",") 250 | for i, v := range slice { 251 | if v == "59" { 252 | slice[i] = "minute 59" 253 | } else { 254 | slice[i] = "minute " + v + " through minute 59" 255 | } 256 | } 257 | output += " from " + strings.Join(slice, " and ") 258 | case "hyphen": 259 | minute := CheckResult.Input 260 | output += " from minute " + strings.Replace(minute, "-", " through minute ", 1) 261 | } 262 | } else { 263 | // check the right side of slash 264 | // for pattern "number" and "comma" 265 | output = CheckResult.Input + " minute" + output 266 | } 267 | } 268 | return output 269 | } 270 | 271 | func explainHour(c *CheckResult) string { 272 | explanation := "past" 273 | switch c.Pattern { 274 | case "asterisk": 275 | explanation = "" 276 | case "number": 277 | hour := AddZeorforTenDigit(c.Input) 278 | explanation += " " + hour + ":00" 279 | case "comma": 280 | hour := c.Input 281 | slice := strings.Split(hour, ",") 282 | for i, v := range slice { 283 | slice[i] = AddZeorforTenDigit(v) + ":00" 284 | } 285 | explanation += " " + strings.Join(slice, " and ") 286 | case "hyphen": 287 | hour := c.Input 288 | slice := strings.Split(hour, "-") 289 | for i, v := range slice { 290 | slice[i] = AddZeorforTenDigit(v) + ":00" 291 | } 292 | explanation += " from " + strings.Join(slice, "-") 293 | case "slash": 294 | // "asterisk","number","comma","hyphen" / "number","comma" 295 | explanation += " " + explainHourSlach(c) 296 | } 297 | return explanation 298 | } 299 | 300 | func explainHourSlach(c *CheckResult) string { 301 | hour := c.Input 302 | var output string 303 | slice := strings.Split(hour, "/") 304 | for i, v := range slice { 305 | CheckResult := checkFieldeReg(v, "hour") 306 | // check the left side of slash 307 | if i == 0 { 308 | switch CheckResult.Pattern { 309 | case "asterisk": 310 | output += "" 311 | case "number": 312 | output += " from " + CheckResult.Input + ":00-24:00" 313 | case "comma": 314 | hour := CheckResult.Input 315 | slice := strings.Split(hour, ",") 316 | for i, v := range slice { 317 | slice[i] = " " + AddZeorforTenDigit(v) + ":00-24:00" 318 | } 319 | output += " from" + strings.Join(slice, " and") 320 | case "hyphen": 321 | hour := CheckResult.Input 322 | slice := strings.Split(hour, "-") 323 | for i, v := range slice { 324 | slice[i] = AddZeorforTenDigit(v) + ":00" 325 | } 326 | output += " from " + strings.Join(slice, "-") 327 | } 328 | } else { 329 | // check the right side of slash 330 | // for pattern "number" and "comma" 331 | switch CheckResult.Pattern { 332 | case "number": 333 | output = "every " + CheckResult.Input + " hour" + output 334 | case "comma": 335 | hour := CheckResult.Input 336 | slice := strings.Split(hour, ",") 337 | for i, v := range slice { 338 | slice[i] = "every " + v + " hour" 339 | } 340 | output = strings.Join(slice, " and ") + output 341 | } 342 | } 343 | } 344 | return output 345 | } 346 | 347 | func explainDayMonth(c *CheckResult) string { 348 | explanation := "on" 349 | switch c.Pattern { 350 | case "asterisk": 351 | explanation = "" 352 | case "number": 353 | dayM := c.Input 354 | explanation += " day " + dayM + " of the month" 355 | case "comma": 356 | dayM := c.Input 357 | explanation += " day " + strings.Replace(dayM, ",", " and ", -1) + " of the month" 358 | case "hyphen": 359 | dayM := c.Input 360 | explanation = "between day " + strings.Replace(dayM, "-", " and ", -1) + " of the month" 361 | case "slash": 362 | // "asterisk","number","comma","hyphen" / "number","comma" 363 | explanation += " " + explainDayMonthSlach(c) 364 | } 365 | return explanation 366 | } 367 | 368 | func explainDayMonthSlach(c *CheckResult) string { 369 | dayM := c.Input 370 | var output string 371 | slice := strings.Split(dayM, "/") 372 | for i, v := range slice { 373 | CheckResult := checkFieldeReg(v, "dayM") 374 | // check the left side of slash 375 | if i == 0 { 376 | switch CheckResult.Pattern { 377 | case "asterisk": 378 | output += "" 379 | case "number": 380 | output += " from day " + CheckResult.Input + " of the month" 381 | case "comma": 382 | dayM := CheckResult.Input 383 | slice := strings.Split(dayM, ",") 384 | for i, v := range slice { 385 | slice[i] = "day " + v + " of the month" 386 | } 387 | output += " from " + strings.Join(slice, " and ") 388 | case "hyphen": 389 | dayM := CheckResult.Input 390 | slice := strings.Split(dayM, "-") 391 | for i, v := range slice { 392 | slice[i] = "day " + v 393 | } 394 | output += " between " + strings.Join(slice, " and ") + " of the month" 395 | } 396 | } else { 397 | // check the right side of slash 398 | // for pattern "number" and "comma" 399 | output = "every " + CheckResult.Input + " day of month" + output 400 | } 401 | } 402 | return output 403 | } 404 | 405 | func explainMonth(c *CheckResult) string { 406 | explanation := "in" 407 | switch c.Pattern { 408 | case "asterisk": 409 | explanation = "" 410 | case "number": 411 | monthNum, _ := strconv.Atoi(c.Input) 412 | explanation += " " + monthList[monthNum-1] 413 | case "comma": 414 | slice := strings.Split(c.Input, ",") 415 | for i, v := range slice { 416 | monthNum, _ := strconv.Atoi(v) 417 | slice[i] = monthList[monthNum-1] 418 | } 419 | explanation += " " + strings.Join(slice, " and ") 420 | case "hyphen": 421 | slice := strings.Split(c.Input, "-") 422 | for i, v := range slice { 423 | monthNum, _ := strconv.Atoi(v) 424 | slice[i] = monthList[monthNum-1] 425 | } 426 | explanation += " every month from " + strings.Join(slice, " through ") 427 | case "slash": 428 | // "asterisk","number","comma","hyphen" / "number","comma" 429 | explanation += " " + explainMonthSlach(c) 430 | } 431 | return explanation 432 | } 433 | 434 | func explainMonthSlach(c *CheckResult) string { 435 | month := c.Input 436 | var output string 437 | slice := strings.Split(month, "/") 438 | for i, v := range slice { 439 | CheckResult := checkFieldeReg(v, "month") 440 | // check the left side of slash 441 | if i == 0 { 442 | switch CheckResult.Pattern { 443 | case "asterisk": 444 | output += "" 445 | case "number": 446 | monthNum, _ := strconv.Atoi(CheckResult.Input) 447 | output += " from " + monthList[monthNum-1] 448 | case "comma": 449 | slice := strings.Split(CheckResult.Input, ",") 450 | for i, v := range slice { 451 | monthNum, _ := strconv.Atoi(v) 452 | slice[i] = monthList[monthNum-1] 453 | } 454 | output += " from " + strings.Join(slice, " and ") 455 | case "hyphen": 456 | slice := strings.Split(CheckResult.Input, "-") 457 | for i, v := range slice { 458 | monthNum, _ := strconv.Atoi(v) 459 | slice[i] = monthList[monthNum-1] 460 | } 461 | output += " from " + strings.Join(slice, " through ") 462 | } 463 | } else { 464 | // check the right side of slash 465 | // for pattern "number" and "comma" 466 | output = "every " + CheckResult.Input + " month" + output 467 | } 468 | } 469 | return output 470 | } 471 | 472 | func explainDayWeek(c *CheckResult) string { 473 | explanation := "on" 474 | switch c.Pattern { 475 | case "asterisk": 476 | explanation = "" 477 | case "number": 478 | dayNum, _ := strconv.Atoi(c.Input) 479 | explanation += " " + dayList[dayNum] 480 | case "comma": 481 | slice := strings.Split(c.Input, ",") 482 | for i, v := range slice { 483 | dayNum, _ := strconv.Atoi(v) 484 | slice[i] = dayList[dayNum] 485 | } 486 | explanation += " " + strings.Join(slice, " and ") 487 | case "hyphen": 488 | slice := strings.Split(c.Input, "-") 489 | for i, v := range slice { 490 | dayNum, _ := strconv.Atoi(v) 491 | slice[i] = dayList[dayNum] 492 | } 493 | explanation += " every day from " + strings.Join(slice, " through ") 494 | case "slash": 495 | // "asterisk","number","comma","hyphen" / "number","comma" 496 | explanation += " " + explainDayWeekSlach(c) 497 | } 498 | return explanation 499 | } 500 | 501 | func explainDayWeekSlach(c *CheckResult) string { 502 | dayW := c.Input 503 | var output string 504 | slice := strings.Split(dayW, "/") 505 | for i, v := range slice { 506 | CheckResult := checkFieldeReg(v, "dayW") 507 | // check the left side of slash 508 | if i == 0 { 509 | switch CheckResult.Pattern { 510 | case "asterisk": 511 | output += "" 512 | case "number": 513 | dayNum, _ := strconv.Atoi(CheckResult.Input) 514 | output += " from " + dayList[dayNum] 515 | case "comma": 516 | dayW := CheckResult.Input 517 | slice := strings.Split(dayW, ",") 518 | for i, v := range slice { 519 | dayNum, _ := strconv.Atoi(v) 520 | slice[i] = dayList[dayNum] 521 | } 522 | output += " from " + strings.Join(slice, " and ") 523 | case "hyphen": 524 | dayW := CheckResult.Input 525 | slice := strings.Split(dayW, "-") 526 | for i, v := range slice { 527 | dayNum, _ := strconv.Atoi(v) 528 | slice[i] = dayList[dayNum] 529 | } 530 | output += " between " + strings.Join(slice, " and ") 531 | } 532 | } else { 533 | // check the right side of slash 534 | // for pattern "number" and "comma" 535 | output = "every " + CheckResult.Input + " day of week" + output 536 | } 537 | } 538 | return output 539 | } 540 | 541 | // MonthToNum change month into number 542 | func MonthToNum(str string) string { 543 | pattern := [12]string{`(?i)Jan(uary)?`, `(?i)Feb(ruary)?`, `(?i)Mar(ch)?`, `(?i)Apr(il)?`, `(?i)May`, `(?i)June?`, `(?i)July?`, `(?i)Aug(ust)?`, `(?i)Sep(tember)?`, `(?i)Oct(ober)?`, `(?i)Nov(ember)?`, `(?i)Dec(ember)?`} 544 | for i, v := range pattern { 545 | re := regexp.MustCompile(v) 546 | str = re.ReplaceAllString(str, strconv.Itoa(i+1)) 547 | } 548 | return str 549 | } 550 | 551 | // WeekDayToNum change weekday into number 552 | func WeekDayToNum(str string) string { 553 | pattern := [7]string{`(?i)Sun(day)?`, `(?i)Mon(day)?`, `(?i)Tue(sday)?`, `(?i)Wed(nesday)?`, `(?i)Thu(rsday)?`, `(?i)Fri(day)?`, `(?i)Sat(urday)?`} 554 | for i, v := range pattern { 555 | re := regexp.MustCompile(v) 556 | str = re.ReplaceAllString(str, strconv.Itoa(i)) 557 | } 558 | return str 559 | } 560 | 561 | // OrdinalDay add ordinal suffix for number 562 | // ex: 1 -> 1st, 2 -> 2nd, 3 -> 3rd 563 | func OrdinalDay(str string) string { 564 | i, _ := strconv.Atoi(str) 565 | return humanize.Ordinal(i) 566 | } 567 | 568 | // AddZeorforTenDigit add prefix 0 for 0~9 569 | // ex: 0 -> 00, 1 -> 01 570 | func AddZeorforTenDigit(str string) string { 571 | Re := regexp.MustCompile(`^\d{1}$`) 572 | if Re.MatchString(str) { 573 | return "0" + str 574 | } 575 | return str 576 | } 577 | 578 | func uniqueSlice(slice []string) (unique []string) { 579 | m := map[string]bool{} 580 | for _, v := range slice { 581 | if !m[v] { 582 | m[v] = true 583 | unique = append(unique, v) 584 | } 585 | } 586 | return unique 587 | } 588 | -------------------------------------------------------------------------------- /translator/translator_test.go: -------------------------------------------------------------------------------- 1 | package translator 2 | 3 | import ( 4 | "reflect" 5 | "testing" 6 | ) 7 | 8 | func TestMatchCronReg(t *testing.T) { 9 | checkList := []string{"1 1-2 3,4 */1 *", "* * * * * *"} 10 | validList := []bool{true, false} 11 | want := map[string]*CheckResult{ 12 | "minute": {true, "1", "number"}, 13 | "hour": {true, "1-2", "hyphen"}, 14 | "dayM": {true, "3,4", "comma"}, 15 | "month": {true, "*/1", "slash"}, 16 | "dayW": {true, "*", "asterisk"}, 17 | } 18 | for i := range checkList { 19 | gotValid, gotCheckResults := MatchCronReg(checkList[i]) 20 | if gotValid != validList[i] { 21 | t.Errorf("got: %t; want: %t", gotValid, true) 22 | } 23 | // check only in valid pattern 24 | if i == 0 { 25 | for key := range want { 26 | if !reflect.DeepEqual(gotCheckResults[key], want[key]) { 27 | t.Errorf("Error in field: %s", key) 28 | } 29 | } 30 | } 31 | } 32 | } 33 | 34 | func TestCheckFieldPattern(t *testing.T) { 35 | asterisk := `\*` 36 | number := `([0-9]|[1-5][0-9])` 37 | comma := `(` + number + `\,){1,59}` + number 38 | hyphen := number + `\-` + number 39 | slash := `(` + asterisk + `|` + number + `|` + comma + `|` + hyphen + `)` + `\/` + `(` + number + `|` + comma + `)` 40 | patternList := map[string]string{"asterisk": asterisk, "number": number, "comma": comma, "hyphen": hyphen, "slash": slash} 41 | 42 | gotValid, gotPattern := checkFieldPattern("Invalid", patternList) 43 | if gotValid != false || gotPattern != "not match" { 44 | t.Errorf("Error in not match") 45 | } 46 | checkList := []string{"*", "1", "1,2", "1-2", "*/1"} 47 | wantList := []string{"asterisk", "number", "comma", "hyphen", "slash"} 48 | for i := range checkList { 49 | _, got := checkFieldPattern(checkList[i], patternList) 50 | want := wantList[i] 51 | if got != want { 52 | t.Errorf("Got: %s, but want: %s", got, want) 53 | } 54 | } 55 | } 56 | 57 | func TestNumLogicFormat(t *testing.T) { 58 | type ckeck struct { 59 | input string 60 | pattern string 61 | } 62 | type re struct { 63 | valid bool 64 | out string 65 | } 66 | checkList := []ckeck{ 67 | {"1-2", "hyphen"}, 68 | {"2-1", "hyphen"}, 69 | {"1,2,3", "comma"}, 70 | {"1,2,2", "comma"}, 71 | {"*/1,2,2", "slash"}, 72 | {"*/2-1", "slash"}, 73 | } 74 | wantList := []re{ 75 | {true, "1-2"}, 76 | {false, "2-1"}, 77 | {true, "1,2,3"}, 78 | {true, "1,2"}, 79 | {true, "*/1,2"}, 80 | {false, "*/2-1"}, 81 | } 82 | for i := range checkList { 83 | gotValid, gotStr := numLogicFormat(checkList[i].input, checkList[i].pattern) 84 | wantValid := wantList[i].valid 85 | wantStr := wantList[i].out 86 | if gotValid != wantValid { 87 | t.Errorf("Error in pattern: %s", checkList[i].pattern) 88 | } 89 | if gotStr != wantStr { 90 | t.Errorf("Error in pattern: %s", checkList[i].pattern) 91 | } 92 | } 93 | } 94 | 95 | func TestCheckFieldeReg_Minute(t *testing.T) { 96 | checkList := []string{"*", "1", "2,3,4", "1-2", "*/1"} 97 | wantList := []CheckResult{ 98 | {true, "*", "asterisk"}, 99 | {true, "1", "number"}, 100 | {true, "2,3,4", "comma"}, 101 | {true, "1-2", "hyphen"}, 102 | {true, "*/1", "slash"}, 103 | } 104 | for i := range checkList { 105 | got := checkFieldeReg(checkList[i], "minute") 106 | want := &wantList[i] 107 | if !reflect.DeepEqual(got, want) { 108 | t.Errorf("Error in pattern: %s", wantList[i].Pattern) 109 | } 110 | } 111 | } 112 | 113 | func TestCheckFieldeReg_Hour(t *testing.T) { 114 | checkList := []string{"*", "1", "2,3,4", "1-2", "*/1"} 115 | wantList := []CheckResult{ 116 | {true, "*", "asterisk"}, 117 | {true, "1", "number"}, 118 | {true, "2,3,4", "comma"}, 119 | {true, "1-2", "hyphen"}, 120 | {true, "*/1", "slash"}, 121 | } 122 | for i := range checkList { 123 | got := checkFieldeReg(checkList[i], "hour") 124 | want := &wantList[i] 125 | if !reflect.DeepEqual(got, want) { 126 | t.Errorf("Error in pattern: %s", wantList[i].Pattern) 127 | } 128 | } 129 | } 130 | 131 | func TestCheckFieldeReg_DayMonth(t *testing.T) { 132 | checkList := []string{"*", "1", "2,3,4", "1-2", "*/1"} 133 | wantList := []CheckResult{ 134 | {true, "*", "asterisk"}, 135 | {true, "1", "number"}, 136 | {true, "2,3,4", "comma"}, 137 | {true, "1-2", "hyphen"}, 138 | {true, "*/1", "slash"}, 139 | } 140 | for i := range checkList { 141 | got := checkFieldeReg(checkList[i], "dayM") 142 | want := &wantList[i] 143 | if !reflect.DeepEqual(got, want) { 144 | t.Errorf("Error in pattern: %s", wantList[i].Pattern) 145 | } 146 | } 147 | } 148 | 149 | func TestCheckFieldeReg_Month(t *testing.T) { 150 | checkList := []string{"*", "1", "2,3,4", "1-2", "*/1"} 151 | wantList := []CheckResult{ 152 | {true, "*", "asterisk"}, 153 | {true, "1", "number"}, 154 | {true, "2,3,4", "comma"}, 155 | {true, "1-2", "hyphen"}, 156 | {true, "*/1", "slash"}, 157 | } 158 | for i := range checkList { 159 | got := checkFieldeReg(checkList[i], "month") 160 | want := &wantList[i] 161 | if !reflect.DeepEqual(got, want) { 162 | t.Errorf("Error in pattern: %s", wantList[i].Pattern) 163 | } 164 | } 165 | } 166 | 167 | func TestCheckFieldeReg_DayWeek(t *testing.T) { 168 | checkList := []string{"*", "1", "2,3,4", "1-2", "*/1"} 169 | wantList := []CheckResult{ 170 | {true, "*", "asterisk"}, 171 | {true, "1", "number"}, 172 | {true, "2,3,4", "comma"}, 173 | {true, "1-2", "hyphen"}, 174 | {true, "*/1", "slash"}, 175 | } 176 | for i := range checkList { 177 | got := checkFieldeReg(checkList[i], "dayW") 178 | want := &wantList[i] 179 | if !reflect.DeepEqual(got, want) { 180 | t.Errorf("Error in pattern: %s", wantList[i].Pattern) 181 | } 182 | } 183 | } 184 | 185 | func ExampleExplain() { 186 | checkList := []string{ 187 | "* * * * *", 188 | "30 12 * * *", 189 | "* * */1 * *", 190 | "* * * * */1", 191 | "* * 1 * 1", 192 | } 193 | for i := range checkList { 194 | _, checkResult := MatchCronReg(checkList[i]) 195 | Explain(checkResult) 196 | } 197 | 198 | // Output: 199 | // At every minute 200 | // At every 12:30 201 | // At every minute, on every 1 day of month 202 | // At every minute, on every 1 day of week 203 | // At every minute, on day 1 of the month and on Monday 204 | } 205 | 206 | func TestExplainMinute(t *testing.T) { 207 | checkList := []string{"*", "1", "2,3,4", "1-2", "*/1", "1/1,2", "1,2/1", "1-2/1"} 208 | wantList := []string{ 209 | "At every minute", 210 | "At every 1st minute", 211 | "At every 2nd and 3rd and 4th minute", 212 | "At every 1 through 2 minute", 213 | "At every 1 minute", 214 | "At every 1,2 minute from minute 1 through minute 59", 215 | "At every 1 minute from minute 1 through minute 59 and minute 2 through minute 59", 216 | "At every 1 minute from minute 1 through minute 2", 217 | } 218 | for i := range checkList { 219 | got := explainMinute(checkFieldeReg(checkList[i], "minute")) 220 | want := wantList[i] 221 | if got != want { 222 | t.Errorf("Got: %s, but want: %s", got, want) 223 | } 224 | } 225 | } 226 | 227 | func TestExplainHour(t *testing.T) { 228 | checkList := []string{"*", "1", "2,3,4", "1-2", "*/1", "1/1,2", "1,2/1", "1-2/1"} 229 | wantList := []string{ 230 | "", 231 | "past 01:00", 232 | "past 02:00 and 03:00 and 04:00", 233 | "past from 01:00-02:00", 234 | "past every 1 hour", 235 | "past every 1 hour and every 2 hour from 1:00-24:00", 236 | "past every 1 hour from 01:00-24:00 and 02:00-24:00", 237 | "past every 1 hour from 01:00-02:00", 238 | } 239 | for i := range checkList { 240 | got := explainHour(checkFieldeReg(checkList[i], "hour")) 241 | want := wantList[i] 242 | if got != want { 243 | t.Errorf("Got: %s, but want: %s", got, want) 244 | } 245 | } 246 | } 247 | 248 | func TestExplainDayMonth(t *testing.T) { 249 | checkList := []string{"*", "1", "2,3,4", "1-2", "*/1", "1/1,2", "1,2/1", "1-2/1"} 250 | wantList := []string{ 251 | "", 252 | "on day 1 of the month", 253 | "on day 2 and 3 and 4 of the month", 254 | "between day 1 and 2 of the month", 255 | "on every 1 day of month", 256 | "on every 1,2 day of month from day 1 of the month", 257 | "on every 1 day of month from day 1 of the month and day 2 of the month", 258 | "on every 1 day of month between day 1 and day 2 of the month", 259 | } 260 | for i := range checkList { 261 | got := explainDayMonth(checkFieldeReg(checkList[i], "dayM")) 262 | want := wantList[i] 263 | if got != want { 264 | t.Errorf("Got: %s, but want: %s", got, want) 265 | } 266 | } 267 | } 268 | 269 | func TestExplainMonth(t *testing.T) { 270 | checkList := []string{"*", "1", "2,3,4", "1-2", "*/1", "1/1,2", "1,2/1", "1-2/1"} 271 | wantList := []string{ 272 | "", 273 | "in January", 274 | "in February and March and April", 275 | "in every month from January through February", 276 | "in every 1 month", 277 | "in every 1,2 month from January", 278 | "in every 1 month from January and February", 279 | "in every 1 month from January through February", 280 | } 281 | for i := range checkList { 282 | got := explainMonth(checkFieldeReg(checkList[i], "month")) 283 | want := wantList[i] 284 | if got != want { 285 | t.Errorf("Got: %s, but want: %s", got, want) 286 | } 287 | } 288 | } 289 | func TestExplainDayWeek(t *testing.T) { 290 | checkList := []string{"*", "1", "2,3,4", "1-2", "*/1", "1/1,2", "1,2/1", "1-2/1"} 291 | wantList := []string{ 292 | "", 293 | "on Monday", 294 | "on Tuesday and Wednesday and Thursday", 295 | "on every day from Monday through Tuesday", 296 | "on every 1 day of week", 297 | "on every 1,2 day of week from Monday", 298 | "on every 1 day of week from Monday and Tuesday", 299 | "on every 1 day of week between Monday and Tuesday", 300 | } 301 | for i := range checkList { 302 | got := explainDayWeek(checkFieldeReg(checkList[i], "dayW")) 303 | want := wantList[i] 304 | if got != want { 305 | t.Errorf("Got: %s, but want: %s", got, want) 306 | } 307 | } 308 | } 309 | func TestMonthToNum(t *testing.T) { 310 | checkList := []string{"Jan", "FEB", "March", "APr", "MaY", "june", "July", "Aug", "Sep", "Oct", "Nov", "Dec"} 311 | wantList := []string{"1", "2", "3", "4", "5", "6", "7", "8", "9", "10", "11", "12"} 312 | for i := range checkList { 313 | got := MonthToNum(checkList[i]) 314 | want := wantList[i] 315 | if got != want { 316 | t.Errorf("Got: %s, but want: %s", got, want) 317 | } 318 | } 319 | } 320 | 321 | func TestWeekDayToNum(t *testing.T) { 322 | checkList := []string{"Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"} 323 | wantList := []string{"0", "1", "2", "3", "4", "5", "6"} 324 | for i := range checkList { 325 | got := WeekDayToNum(checkList[i]) 326 | want := wantList[i] 327 | if got != want { 328 | t.Errorf("Got: %s, but want: %s", got, want) 329 | } 330 | } 331 | } 332 | 333 | func TestOrdinalDay(t *testing.T) { 334 | checkList := []string{"1", "2", "3", "4"} 335 | wantList := []string{"1st", "2nd", "3rd", "4th"} 336 | for i := range checkList { 337 | got := OrdinalDay(checkList[i]) 338 | want := wantList[i] 339 | if got != want { 340 | t.Errorf("Got: %s, but want: %s", got, want) 341 | } 342 | } 343 | } 344 | 345 | func TestAddZeorforTenDigit(t *testing.T) { 346 | checkList := []string{"0", "1", "2", "3", "4", "5", "6", "7", "8", "9"} 347 | wantList := []string{"00", "01", "02", "03", "04", "05", "06", "07", "08", "09"} 348 | for i := range checkList { 349 | got := AddZeorforTenDigit(checkList[i]) 350 | want := wantList[i] 351 | if got != want { 352 | t.Errorf("Got: %s, but want: %s", got, want) 353 | } 354 | } 355 | } 356 | 357 | func TestUniqueSlice(t *testing.T) { 358 | slice := []string{"foo", "bar", "fizz", "foo", "fizz"} 359 | got := uniqueSlice(slice) 360 | want := []string{"foo", "bar", "fizz"} 361 | if !reflect.DeepEqual(got, want) { 362 | t.Errorf("Got: %s, but want: %s", got, want) 363 | } 364 | } 365 | --------------------------------------------------------------------------------