Merge pull request #4406 from TD-er/bugfix/cron_match

[Cron] Fix unintended month rollovers, ported from staticlibs#35
This commit is contained in:
TD-er
2023-12-04 16:40:28 +01:00
committed by GitHub
21 changed files with 2500 additions and 1832 deletions
-3
View File
@@ -1,3 +0,0 @@
/build/
/nbproject
/Makefile
-97
View File
@@ -1,97 +0,0 @@
Cron expression parsing in ANSI C
=================================
[![travis](https://travis-ci.org/staticlibs/ccronexpr.svg?branch=master)](https://travis-ci.org/staticlibs/ccronexpr)
[![appveyor](https://ci.appveyor.com/api/projects/status/github/staticlibs/ccronexpr?svg=true)](https://ci.appveyor.com/project/staticlibs/ccronexpr)
Given a cron expression and a date, you can get the next date which satisfies the cron expression.
Supports cron expressions with `seconds` field. Based on implementation of [CronSequenceGenerator](https://github.com/spring-projects/spring-framework/blob/babbf6e8710ab937cd05ece20270f51490299270/spring-context/src/main/java/org/springframework/scheduling/support/CronSequenceGenerator.java) from Spring Framework.
Compiles and should work on Linux (GCC/Clang), Mac OS (Clang), Windows (MSVC), Android NDK, iOS and possibly on other platforms with `time.h` support.
Supports compilation in C (89) and in C++ modes.
Usage example
-------------
#include "ccronexpr.h"
cron_expr expr;
const char* err = NULL;
memset(&expr, 0, sizeof(expr));
cron_parse_expr("0 */2 1-4 * * *", &expr, &err);
if (err) ... /* invalid expression */
time_t cur = time(NULL);
time_t next = cron_next(&expr, cur);
Compilation and tests run examples
----------------------------------
gcc ccronexpr.c ccronexpr_test.c -I. -Wall -Wextra -std=c89 -DCRON_TEST_MALLOC -o a.out && ./a.out
g++ ccronexpr.c ccronexpr_test.c -I. -Wall -Wextra -std=c++11 -DCRON_TEST_MALLOC -o a.out && ./a.out
g++ ccronexpr.c ccronexpr_test.c -I. -Wall -Wextra -std=c++11 -DCRON_TEST_MALLOC -DCRON_COMPILE_AS_CXX -o a.out && ./a.out
clang ccronexpr.c ccronexpr_test.c -I. -Wall -Wextra -std=c89 -DCRON_TEST_MALLOC -o a.out && ./a.out
clang++ ccronexpr.c ccronexpr_test.c -I. -Wall -Wextra -std=c++11 -DCRON_TEST_MALLOC -o a.out && ./a.out
clang++ ccronexpr.c ccronexpr_test.c -I. -Wall -Wextra -std=c++11 -DCRON_TEST_MALLOC -DCRON_COMPILE_AS_CXX -o a.out && ./a.out
cl ccronexpr.c ccronexpr_test.c /W4 /D_CRT_SECURE_NO_WARNINGS && ccronexpr.exe
Examples of supported expressions
---------------------------------
Expression, input date, next date:
"*/15 * 1-4 * * *", "2012-07-01_09:53:50", "2012-07-02_01:00:00"
"0 */2 1-4 * * *", "2012-07-01_09:00:00", "2012-07-02_01:00:00"
"0 0 7 ? * MON-FRI", "2009-09-26_00:42:55", "2009-09-28_07:00:00"
"0 30 23 30 1/3 ?", "2011-04-30_23:30:00", "2011-07-30_23:30:00"
See more examples in [tests](https://github.com/staticlibs/ccronexpr/blob/a1343bc5a546b13430bd4ac72f3b047ac08f8192/ccronexpr_test.c#L251).
Timezones
---------
This implementation does not support explicit timezones handling. By default all dates are
processed as UTC (GMT) dates without timezone infomation.
To use local dates (current system timezone) instead of GMT compile with `-DCRON_USE_LOCAL_TIME`, example:
gcc -DCRON_USE_LOCAL_TIME ccronexpr.c ccronexpr_test.c -I. -Wall -Wextra -std=c89 -DCRON_TEST_MALLOC -o a.out && TZ="America/Toronto" ./a.out
License information
-------------------
This project is released under the [Apache License 2.0](http://www.apache.org/licenses/LICENSE-2.0).
Changelog
---------
**2019-03-27**
* `CRON_USE_LOCAL_TIME` usage fixes
**2018-05-23**
* merged [#8](https://github.com/staticlibs/ccronexpr/pull/8)
* merged [#9](https://github.com/staticlibs/ccronexpr/pull/9)
* minor cleanups
**2018-01-27**
* merged [#6](https://github.com/staticlibs/ccronexpr/pull/6)
* updated license file (to the one parse-able by github)
**2017-09-24**
* merged [#4](https://github.com/staticlibs/ccronexpr/pull/4)
**2016-06-17**
* use thread-safe versions of `gmtime` and `localtime`
**2015-02-28**
* initial public version
File diff suppressed because it is too large Load Diff
-414
View File
@@ -1,414 +0,0 @@
/*
* Copyright 2015, alex at staticlibs.net
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/*
* File: CronExprParser_test.cpp
* Author: alex
*
* Created on February 24, 2015, 9:36 AM
*/
#include <assert.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <limits.h>
#include "ccronexpr.h"
#define MAX_SECONDS 60
#define CRON_MAX_MINUTES 60
#define CRON_MAX_HOURS 24
#define CRON_MAX_DAYS_OF_WEEK 8
#define CRON_MAX_DAYS_OF_MONTH 32
#define CRON_MAX_MONTHS 12
#define INVALID_INSTANT ((time_t) -1)
#define DATE_FORMAT "%Y-%m-%d_%H:%M:%S"
#ifndef ARRAY_LEN
#define ARRAY_LEN(x) sizeof(x)/sizeof(x[0])
#endif
#ifdef CRON_TEST_MALLOC
static int cronAllocations = 0;
static int cronTotalAllocations = 0;
static int maxAlloc = 0;
void* cron_malloc(size_t n) {
cronAllocations++;
cronTotalAllocations++;
if (cronAllocations > maxAlloc) {
maxAlloc = cronAllocations;
}
return malloc(n);
}
void cron_free(void* p) {
cronAllocations--;
free(p);
}
#endif
#ifndef ANDROID
#ifndef _WIN32
time_t timegm(struct tm* __tp);
#else /* _WIN32 */
static time_t timegm(struct tm* tm) {
return _mkgmtime(tm);
}
#endif /* _WIN32 */
#else /* ANDROID */
static time_t timegm(struct tm * const t) {
/* time_t is signed on Android. */
static const time_t kTimeMax = ~(1L << (sizeof (time_t) * CHAR_BIT - 1));
static const time_t kTimeMin = (1L << (sizeof (time_t) * CHAR_BIT - 1));
time64_t result = timegm64(t);
if (result < kTimeMin || result > kTimeMax)
return -1;
return result;
}
#endif
/**
* uint8_t* replace char* for storing hit dates, set_bit and get_bit are used as handlers
*/
uint8_t cron_get_bit(uint8_t* rbyte, int idx);
void cron_set_bit(uint8_t* rbyte, int idx);
void cron_del_bit(uint8_t* rbyte, int idx);
static int crons_equal(cron_expr* cr1, cron_expr* cr2) {
unsigned int i;
for (i = 0; i < ARRAY_LEN(cr1->seconds); i++) {
if (cr1->seconds[i] != cr2->seconds[i]) {
printf("seconds not equal @%d %02x != %02x", i, cr1->seconds[i], cr2->seconds[i]);
return 0;
}
}
for (i = 0; i < ARRAY_LEN(cr1->minutes); i++) {
if (cr1->minutes[i] != cr2->minutes[i]) {
printf("minutes not equal @%d %02x != %02x", i, cr1->minutes[i], cr2->minutes[i]);
return 0;
}
}
for (i = 0; i < ARRAY_LEN(cr1->hours); i++) {
if (cr1->hours[i] != cr2->hours[i]) {
printf("hours not equal @%d %02x != %02x", i, cr1->hours[i], cr2->hours[i]);
return 0;
}
}
for (i = 0; i < ARRAY_LEN(cr1->days_of_week); i++) {
if (cr1->days_of_week[i] != cr2->days_of_week[i]) {
printf("days_of_week not equal @%d %02x != %02x", i, cr1->days_of_week[i], cr2->days_of_week[i]);
return 0;
}
}
for (i = 0; i < ARRAY_LEN(cr1->days_of_month); i++) {
if (cr1->days_of_month[i] != cr2->days_of_month[i]) {
printf("days_of_month not equal @%d %02x != %02x", i, cr1->days_of_month[i], cr2->days_of_month[i]);
return 0;
}
}
for (i = 0; i < ARRAY_LEN(cr1->months); i++) {
if (cr1->months[i] != cr2->months[i]) {
printf("months not equal @%d %02x != %02x", i, cr1->months[i], cr2->months[i]);
return 0;
}
}
return 1;
}
int one_dec_num(const char ch) {
switch (ch) {
case '0':
return 0;
case '1':
return 1;
case '2':
return 2;
case '3':
return 3;
case '4':
return 4;
case '5':
return 5;
case '6':
return 6;
case '7':
return 7;
case '8':
return 8;
case '9':
return 9;
default:
return -1;
}
}
int two_dec_num(const char* first) {
return one_dec_num(first[0]) * 10 + one_dec_num(first[1]);
}
int four_dec_num(const char *first) {
return ((one_dec_num(first[0]) * 1000)
+ (one_dec_num(first[1]) * 100)
+ (one_dec_num(first[2]) * 10)
+ (one_dec_num(first[3]) * 1));
}
/* strptime is not available in msvc */
/* 2012-07-01_09:53:50 */
/* 0123456789012345678 */
struct tm* poors_mans_strptime(const char* str) {
struct tm* cal = (struct tm*) malloc(sizeof(struct tm));
assert(cal != NULL);
memset(cal, 0, sizeof(struct tm));
cal->tm_year = four_dec_num(str) - 1900;
cal->tm_mon = two_dec_num(str + 5) - 1;
cal->tm_mday = two_dec_num(str + 8);
cal->tm_wday = 0;
cal->tm_yday = 0;
cal->tm_hour = two_dec_num(str + 11);
cal->tm_min = two_dec_num(str + 14);
cal->tm_sec = two_dec_num(str + 17);
return cal;
}
void check_next(const char* pattern, const char* initial, const char* expected) {
const char* err = NULL;
cron_expr parsed;
cron_parse_expr(pattern, &parsed, &err);
struct tm* calinit = poors_mans_strptime(initial);
#ifdef CRON_USE_LOCAL_TIME
time_t dateinit = mktime(calinit);
#else
time_t dateinit = timegm(calinit);
#endif
assert(-1 != dateinit);
time_t datenext = cron_next(&parsed, dateinit);
#ifdef CRON_USE_LOCAL_TIME
struct tm* calnext = localtime(&datenext);
#else
struct tm* calnext = gmtime(&datenext);
#endif
assert(calnext);
char* buffer = (char*) malloc(21);
memset(buffer, 0, 21);
strftime(buffer, 20, DATE_FORMAT, calnext);
if (0 != strcmp(expected, buffer)) {
printf("Pattern: %s\n", pattern);
printf("Initial: %s\n", initial);
printf("Expected: %s\n", expected);
printf("Actual: %s\n", buffer);
assert(0);
}
free(buffer);
free(calinit);
}
void check_same(const char* expr1, const char* expr2) {
cron_expr parsed1;
cron_parse_expr(expr1, &parsed1, NULL);
cron_expr parsed2;
cron_parse_expr(expr2, &parsed2, NULL);
int res = crons_equal(&parsed1, &parsed2);
res = res; // to prevent warnings
assert(res);
}
void check_calc_invalid() {
cron_expr parsed;
cron_parse_expr("0 0 0 31 6 *", &parsed, NULL);
struct tm * calinit = poors_mans_strptime("2012-07-01_09:53:50");
time_t dateinit = timegm(calinit);
time_t res = cron_next(&parsed, dateinit);
res = res;; // to prevent warnings
assert(INVALID_INSTANT == res);
free(calinit);
}
void check_expr_invalid(const char* expr) {
const char* err = NULL;
cron_expr test;
cron_parse_expr(expr, &test, &err);
assert(err);
}
void test_expr() {
#ifdef CRON_USE_LOCAL_TIME
check_next("* 15 11 * * *", "2019-03-09_11:43:00", "2019-03-10_11:15:00");
#else
check_next("*/15 * 1-4 * * *", "2012-07-01_09:53:50", "2012-07-02_01:00:00");
check_next("*/15 * 1-4 * * *", "2012-07-01_09:53:00", "2012-07-02_01:00:00");
check_next("0 */2 1-4 * * *", "2012-07-01_09:00:00", "2012-07-02_01:00:00");
check_next("0 */2 * * * *", "2012-07-01_09:00:00", "2012-07-01_09:02:00");
check_next("0 */2 * * * *", "2013-07-01_09:00:00", "2013-07-01_09:02:00");
check_next("0 */2 * * * *", "2018-09-14_14:24:00", "2018-09-14_14:26:00");
check_next("0 */2 * * * *", "2018-09-14_14:25:00", "2018-09-14_14:26:00");
check_next("0 */20 * * * *", "2018-09-14_14:24:00", "2018-09-14_14:40:00");
check_next("* * * * * *", "2012-07-01_09:00:00", "2012-07-01_09:00:01");
check_next("* * * * * *", "2012-12-01_09:00:58", "2012-12-01_09:00:59");
check_next("10 * * * * *", "2012-12-01_09:42:09", "2012-12-01_09:42:10");
check_next("11 * * * * *", "2012-12-01_09:42:10", "2012-12-01_09:42:11");
check_next("10 * * * * *", "2012-12-01_09:42:10", "2012-12-01_09:43:10");
check_next("10-15 * * * * *", "2012-12-01_09:42:09", "2012-12-01_09:42:10");
check_next("10-15 * * * * *", "2012-12-01_21:42:14", "2012-12-01_21:42:15");
check_next("0 * * * * *", "2012-12-01_21:10:42", "2012-12-01_21:11:00");
check_next("0 * * * * *", "2012-12-01_21:11:00", "2012-12-01_21:12:00");
check_next("0 11 * * * *", "2012-12-01_21:10:42", "2012-12-01_21:11:00");
check_next("0 10 * * * *", "2012-12-01_21:11:00", "2012-12-01_22:10:00");
check_next("0 0 * * * *", "2012-09-30_11:01:00", "2012-09-30_12:00:00");
check_next("0 0 * * * *", "2012-09-30_12:00:00", "2012-09-30_13:00:00");
check_next("0 0 * * * *", "2012-09-10_23:01:00", "2012-09-11_00:00:00");
check_next("0 0 * * * *", "2012-09-11_00:00:00", "2012-09-11_01:00:00");
check_next("0 0 0 * * *", "2012-09-01_14:42:43", "2012-09-02_00:00:00");
check_next("0 0 0 * * *", "2012-09-02_00:00:00", "2012-09-03_00:00:00");
check_next("* * * 10 * *", "2012-10-09_15:12:42", "2012-10-10_00:00:00");
check_next("* * * 10 * *", "2012-10-11_15:12:42", "2012-11-10_00:00:00");
check_next("0 0 0 * * *", "2012-09-30_15:12:42", "2012-10-01_00:00:00");
check_next("0 0 0 * * *", "2012-10-01_00:00:00", "2012-10-02_00:00:00");
check_next("0 0 0 * * *", "2012-08-30_15:12:42", "2012-08-31_00:00:00");
check_next("0 0 0 * * *", "2012-08-31_00:00:00", "2012-09-01_00:00:00");
check_next("0 0 0 * * *", "2012-10-30_15:12:42", "2012-10-31_00:00:00");
check_next("0 0 0 * * *", "2012-10-31_00:00:00", "2012-11-01_00:00:00");
check_next("0 0 0 1 * *", "2012-10-30_15:12:42", "2012-11-01_00:00:00");
check_next("0 0 0 1 * *", "2012-11-01_00:00:00", "2012-12-01_00:00:00");
check_next("0 0 0 1 * *", "2010-12-31_15:12:42", "2011-01-01_00:00:00");
check_next("0 0 0 1 * *", "2011-01-01_00:00:00", "2011-02-01_00:00:00");
check_next("0 0 0 31 * *", "2011-10-30_15:12:42", "2011-10-31_00:00:00");
check_next("0 0 0 1 * *", "2011-10-30_15:12:42", "2011-11-01_00:00:00");
check_next("* * * * * 2", "2010-10-25_15:12:42", "2010-10-26_00:00:00");
check_next("* * * * * 2", "2010-10-20_15:12:42", "2010-10-26_00:00:00");
check_next("* * * * * 2", "2010-10-27_15:12:42", "2010-11-02_00:00:00");
check_next("55 5 * * * *", "2010-10-27_15:04:54", "2010-10-27_15:05:55");
check_next("55 5 * * * *", "2010-10-27_15:05:55", "2010-10-27_16:05:55");
check_next("55 * 10 * * *", "2010-10-27_09:04:54", "2010-10-27_10:00:55");
check_next("55 * 10 * * *", "2010-10-27_10:00:55", "2010-10-27_10:01:55");
check_next("* 5 10 * * *", "2010-10-27_09:04:55", "2010-10-27_10:05:00");
check_next("* 5 10 * * *", "2010-10-27_10:05:00", "2010-10-27_10:05:01");
check_next("55 * * 3 * *", "2010-10-02_10:05:54", "2010-10-03_00:00:55");
check_next("55 * * 3 * *", "2010-10-03_00:00:55", "2010-10-03_00:01:55");
check_next("* * * 3 11 *", "2010-10-02_14:42:55", "2010-11-03_00:00:00");
check_next("* * * 3 11 *", "2010-11-03_00:00:00", "2010-11-03_00:00:01");
check_next("0 0 0 29 2 *", "2007-02-10_14:42:55", "2008-02-29_00:00:00");
check_next("0 0 0 29 2 *", "2008-02-29_00:00:00", "2012-02-29_00:00:00");
check_next("0 0 7 ? * MON-FRI", "2009-09-26_00:42:55", "2009-09-28_07:00:00");
check_next("0 0 7 ? * MON-FRI", "2009-09-28_07:00:00", "2009-09-29_07:00:00");
check_next("0 30 23 30 1/3 ?", "2010-12-30_00:00:00", "2011-01-30_23:30:00");
check_next("0 30 23 30 1/3 ?", "2011-01-30_23:30:00", "2011-04-30_23:30:00");
check_next("0 30 23 30 1/3 ?", "2011-04-30_23:30:00", "2011-07-30_23:30:00");
#endif
}
void test_parse() {
check_same("* * * 2 * *", "* * * 2 * ?");
check_same("57,59 * * * * *", "57/2 * * * * *");
check_same("1,3,5 * * * * *", "1-6/2 * * * * *");
check_same("* * 4,8,12,16,20 * * *", "* * 4/4 * * *");
check_same("* * * * * 0-6", "* * * * * TUE,WED,THU,FRI,SAT,SUN,MON");
check_same("* * * * * 0", "* * * * * SUN");
check_same("* * * * * 0", "* * * * * 7");
check_same("* * * * 1-12 *", "* * * * FEB,JAN,MAR,APR,MAY,JUN,JUL,AUG,SEP,OCT,NOV,DEC *");
check_same("* * * * 2 *", "* * * * Feb *");
check_same("* * * * 1 *", "* * * * 1 *");
check_expr_invalid("77 * * * * *");
check_expr_invalid("44-77 * * * * *");
check_expr_invalid("* 77 * * * *");
check_expr_invalid("* 44-77 * * * *");
check_expr_invalid("* * 27 * * *");
check_expr_invalid("* * 23-28 * * *");
check_expr_invalid("* * * 45 * *");
check_expr_invalid("* * * 28-45 * *");
check_expr_invalid("0 0 0 25 13 ?");
check_expr_invalid("0 0 0 25 0 ?");
check_expr_invalid("0 0 0 32 12 ?");
check_expr_invalid("* * * * 11-13 *");
check_expr_invalid("-5 * * * * *");
check_expr_invalid("3-2 */5 * * * *");
check_expr_invalid("/5 * * * * *");
check_expr_invalid("*/0 * * * * *");
check_expr_invalid("*/-0 * * * * *");
check_expr_invalid("* 1 1 0 * *");
}
void test_bits() {
uint8_t testbyte[8];
memset(testbyte, 0, 8);
int err = 0;
err = err; // to prevent warnings
int i;
for (i = 0; i <= 63; i++) {
cron_set_bit(testbyte, i);
if (!cron_get_bit(testbyte, i)) {
printf("Bit set error! Bit: %d!\n", i);
err = 1;
}
cron_del_bit(testbyte, i);
if (cron_get_bit(testbyte, i)) {
printf("Bit clear error! Bit: %d!\n", i);
err = 1;
}
assert(!err);
}
for (i = 0; i < 12; i++) {
cron_set_bit(testbyte, i);
}
if (testbyte[0] != 0xff) {
err = 1;
}
if (testbyte[1] != 0x0f) {
err = 1;
}
assert(!err);
}
/* For this test to work you need to set "-DCRON_TEST_MALLOC=1"*/
#ifdef CRON_TEST_MALLOC
void test_memory() {
cron_expr cron;
const char* err;
cron_parse_expr("* * * * * *", &cron, &err);
if (cronAllocations != 0) {
printf("Allocations != 0 but %d", cronAllocations);
assert(0);
}
printf("Allocations: total: %d, max: %d", cronTotalAllocations, maxAlloc);
}
#endif
int main() {
test_bits();
test_expr();
test_parse();
check_calc_invalid();
#ifdef CRON_TEST_MALLOC
test_memory(); /* For this test to work you need to set "-DCRON_TEST_MALLOC=1"*/
#endif
printf("\nAll OK!");
return 0;
}
-14
View File
@@ -1,14 +0,0 @@
{
"name": "ccronexpr",
"version": "1.0.0",
"description": "Cron expression parsing in ANSI C",
"repository":
{
"type": "git",
"url": "https://github.com/staticlibs/ccronexpr"
},
"frameworks": "arduino",
"platforms": [
"espressif8266","espressif32","espressif32s2"
]
}
-8
View File
@@ -1,8 +0,0 @@
name=ccronexpr
version=1.0.0
sentence=Cron expression parsing in ANSI C
paragraph=
url=https://github.com/staticlibs/ccronexpr
architectures=esp8266,esp32,esp32s2
author=
maintainer=
+60
View File
@@ -0,0 +1,60 @@
name: Build
on:
push:
paths-ignore: [ "*.md" ]
pull_request:
paths-ignore: [ "*.md" ]
env:
BUILD_TYPE: Debug
jobs:
build:
name: Build
strategy:
fail-fast: false
matrix:
os:
- ubuntu-latest
- windows-latest
cron_use_local_time:
- ""
- "-DCRON_USE_LOCAL_TIME=1"
char_c_flag:
- ""
- "-fsigned-char"
- "-funsigned-char"
exclude:
- os: ubuntu-latest
char_c_flag: ""
- os: windows-latest
char_c_flag: "-fsigned-char"
- os: windows-latest
char_c_flag: "-funsigned-char"
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v2
- name: Create Build Environment
run: cmake -E make_directory ${{github.workspace}}/build
- name: Configure CMake
shell: bash
working-directory: ${{github.workspace}}/build
run: cmake $GITHUB_WORKSPACE -DCMAKE_BUILD_TYPE=$BUILD_TYPE ${{ matrix.cron_use_local_time }}
env:
CFLAGS: ${{ matrix.char_c_flag }}
- name: Build
working-directory: ${{github.workspace}}/build
shell: bash
run: cmake --build . --config $BUILD_TYPE
- name: Test
working-directory: ${{github.workspace}}/build
shell: bash
run: ctest --extra-verbose -C $BUILD_TYPE
+8
View File
@@ -0,0 +1,8 @@
build/
cmake-build*/
/nbproject
.idea/
*.o
supertinycron
ccronexpr_test
libccronexpr.so
@@ -25,12 +25,18 @@ compiler:
- clang
script:
- $CC ccronexpr.c ccronexpr_test.c -I. -Wall -Wextra -std=c89 -DCRON_TEST_MALLOC -o a.out && ./a.out
- $CXX ccronexpr.c ccronexpr_test.c -I. -Wall -Wextra -std=c++11 -DCRON_TEST_MALLOC -o a.out && ./a.out
- $CXX ccronexpr.c ccronexpr_test.c -I. -Wall -Wextra -std=c++11 -DCRON_TEST_MALLOC -DCRON_COMPILE_AS_CXX -o a.out && ./a.out
- $CC -DCRON_USE_LOCAL_TIME ccronexpr.c ccronexpr_test.c -I. -Wall -Wextra -std=c89 -DCRON_TEST_MALLOC -o a.out && TZ="America/Toronto" ./a.out
- $CXX -DCRON_USE_LOCAL_TIME ccronexpr.c ccronexpr_test.c -I. -Wall -Wextra -std=c++11 -DCRON_TEST_MALLOC -o a.out && TZ="America/Toronto" ./a.out
- $CXX -DCRON_USE_LOCAL_TIME ccronexpr.c ccronexpr_test.c -I. -Wall -Wextra -std=c++11 -DCRON_TEST_MALLOC -DCRON_COMPILE_AS_CXX -o a.out && TZ="America/Toronto" ./a.out
- $CC ccronexpr.c ccronexpr_test.c -I. -Wall -Wextra -std=c89 -o a.out && ./a.out
- $CXX ccronexpr.c ccronexpr_test.c -I. -Wall -Wextra -std=c++11 -o a.out && ./a.out
- $CXX ccronexpr.c ccronexpr_test.c -I. -Wall -Wextra -std=c++11 -DCRON_COMPILE_AS_CXX -o a.out && ./a.out
- $CC -DCRON_USE_LOCAL_TIME ccronexpr.c ccronexpr_test.c -I. -Wall -Wextra -std=c89 -o a.out && TZ="America/Toronto" ./a.out
- $CXX -DCRON_USE_LOCAL_TIME ccronexpr.c ccronexpr_test.c -I. -Wall -Wextra -std=c++11 -o a.out && TZ="America/Toronto" ./a.out
- $CXX -DCRON_USE_LOCAL_TIME ccronexpr.c ccronexpr_test.c -I. -Wall -Wextra -std=c++11 -DCRON_COMPILE_AS_CXX -o a.out && TZ="America/Toronto" ./a.out
- $CC ccronexpr.c supertinycron.c -I. -Wall -Wextra -std=c89 -o a.out && ./a.out
- $CXX ccronexpr.c supertinycron.c -I. -Wall -Wextra -std=c++11 -o a.out && ./a.out
- $CXX ccronexpr.c supertinycron.c -I. -Wall -Wextra -std=c++11 -DCRON_COMPILE_AS_CXX -o a.out && ./a.out
- $CC -DCRON_USE_LOCAL_TIME ccronexpr.c supertinycron.c -I. -Wall -Wextra -std=c89 -o a.out && TZ="America/Toronto" ./a.out
- $CXX -DCRON_USE_LOCAL_TIME ccronexpr.c supertinycron.c -I. -Wall -Wextra -std=c++11 -o a.out && TZ="America/Toronto" ./a.out
- $CXX -DCRON_USE_LOCAL_TIME ccronexpr.c supertinycron.c -I. -Wall -Wextra -std=c++11 -DCRON_COMPILE_AS_CXX -o a.out && TZ="America/Toronto" ./a.out
notifications:
email:
+39
View File
@@ -0,0 +1,39 @@
cmake_minimum_required(VERSION 3.0)
project(ccronexpr)
# Library
add_library(ccronexpr STATIC ccronexpr.c)
target_include_directories(ccronexpr PUBLIC .)
# Supertinycron binary
#add_executable(supertinycron supertinycron.c)
#target_link_libraries(supertinycron ccronexpr)
if (ESP_PLATFORM)
target_compile_definitions(ccronexpr PRIVATE ESP_PLATFORM=1)
set(CRON_DISABLE_TESTING ON) # disable tests automatically
endif ()
if (CRON_USE_LOCAL_TIME)
target_compile_definitions(ccronexpr PUBLIC CRON_USE_LOCAL_TIME=1)
endif ()
if (CRON_COMPILE_AS_CXX)
target_compile_definitions(ccronexpr PUBLIC CRON_COMPILE_AS_CXX=1)
endif ()
if (MSVC)
# Strict compilation
target_compile_options(ccronexpr PRIVATE /W4 /WX)
# But ignore _s functions
target_compile_definitions(ccronexpr PRIVATE _CRT_SECURE_NO_WARNINGS)
else ()
# Strict compilation
target_compile_options(ccronexpr PRIVATE -ansi -Wall -Wextra -Werror -Wshadow -Wpointer-arith -Wcast-qual -Wconversion -Wno-unused-parameter -pedantic-errors)
endif ()
# Tests
if (NOT CRON_DISABLE_TESTING)
enable_testing()
add_subdirectory(test)
endif ()
+22
View File
@@ -0,0 +1,22 @@
Using library from ESP-IDF
==========================
Note that only local time mode is currently supported on ESP.
Add library as a submodule, but outside components directory:
```shell
git submodule add https://github.com/mdvorak/ccronexpr.git libs/ccronexpr
```
Reference it in the project CMakeLists.txt:
```cmake
set(CRON_USE_LOCAL_TIME ON)
add_subdirectory(libs/ccronexpr)
```
Finally, link it to your component:
```cmake
idf_component_register(...)
target_link_libraries(${COMPONENT_LIB} PUBLIC ccronexpr)
```
+35
View File
@@ -0,0 +1,35 @@
CC = musl-gcc
VERSION := $(shell git rev-parse --short HEAD)-$(shell date +%Y%m%d%H%M%S)
UNCOMMITTED_CHANGES := $(shell git status --porcelain)
ifeq ($(strip $(UNCOMMITTED_CHANGES)),)
VERSION := $(VERSION)
else
VERSION := $(VERSION)-dev-build
endif
CFLAGS = -Wextra -std=c89 -Os -ffunction-sections -fdata-sections -Wl,--gc-sections -Wl,-z,norelro -Wno-unused-parameter -static -DVERSION=\"$(VERSION)\" -DCRON_USE_LOCAL_TIME
CFLAGS = -Wextra -std=c89 -s -Os -ffunction-sections -fdata-sections -Wl,--gc-sections -Wl,-z,norelro -static -DVERSION=\"$(VERSION)\" -DCRON_USE_LOCAL_TIME -fPIC
SOURCES = supertinycron.c ccronexpr.c ccronexpr_test.c
OBJECTS = supertinycron.o ccronexpr.o
OBJECTS_TEST = ccronexpr_test.o ccronexpr.o
OBJECTS_SHARED = ccronexpr.o
EXECUTABLE = supertinycron
EXECUTABLE_TEST = ccronexpr_test
SHARED = libccronexpr.so
LDFLAGS = -shared -Wl,-soname,$(SHARED) -fPIC
all: $(EXECUTABLE) $(EXECUTABLE_TEST)
shared: $(SHARED)
$(SHARED): $(OBJECTS_SHARED)
$(CC) $(LDFLAGS) $^ -o $@
$(EXECUTABLE): $(OBJECTS)
$(CC) $(CFLAGS) $(OBJECTS) -o $@
upx -9 $(EXECUTABLE)
$(EXECUTABLE_TEST): $(OBJECTS_TEST)
$(CC) $(CFLAGS) $(OBJECTS_TEST) -o $@
clean:
rm -f $(OBJECTS) $(OBJECTS_TEST) $(EXECUTABLE) $(EXECUTABLE_TEST) $(SHARED)
+262
View File
@@ -0,0 +1,262 @@
supertinycron
=============
A very small replacement for cron. Particularly useful within containers and for distributing cron tasks alongside a project as a standalone file.
Installing
----------
```bash
make
sudo mv supertinycron /usr/local/bin/
```
Usage
-----
```
supertinycron [expression] [command...]
```
Supertinycron can be conveniently used in your scripts interpreter line:
```bash
#!/usr/local/bin/supertinycron */5 * * * * * * /bin/sh
echo "Current time: $(date)"
```
Or invoked directly via commandline:
```bash
$ supertinycron '*/5 * * * * * *' /bin/echo hello
```
Implementation
--------------
This reference is based on:
- [1] [gorhill's cronexpr on GitHub](https://github.com/gorhill/cronexpr/blob/master/README.md)
- [2] [Quartz Scheduler's CronExpression](https://www.javadoc.io/doc/org.quartz-scheduler/quartz/latest/org/quartz/CronExpression.html)
- [3] [ccronexpr by staticlibs on GitHub](https://github.com/staticlibs/ccronexpr/blob/master/README.md)
- [4] [ccronexpr by mdvorak on GitHub](https://github.com/mdvorak/ccronexpr/blob/main/README.md)
- [5] [Wikipedia on CRON Expression](https://en.wikipedia.org/wiki/Cron#CRON_expression)
```
Field name Mandatory? Allowed values Allowed special characters
---------- ---------- -------------- -------------------------
Second No 0-59 * / , - L
Minute Yes 0-59 * / , -
Hour Yes 0-23 * / , -
Day of month Yes 1-31 * / , - L W
Month Yes 1-12 or JAN-DEC * / , -
Day of week Yes 0-6 or SUN-SAT * / , - L #
Year No 19702199 * / , -
```
**Note:** In the 'Day of week' field, both 0 and 7 represent SAT, as referenced by [crontab's man page](http://linux.die.net/man/5/crontab#). The 'Year' field spans to 2199 as per [2], which differs from [1] where it's up to 2099. When `*` is used for year it should function well above year 2199. Depending on libc, it may function well above year 10000.
### Special Characters
#### Asterisk `*`
The asterisk indicates that the cron expression matches all values of the field. For instance, an asterisk in the 'Month' field matches every month.
#### Hyphen `-`
Hyphens define ranges. For instance, `2000-2010` in the 'Year' field matches every year from 2000 to 2010, inclusive.
#### Slash `/`
Slashes specify increments within ranges. For example, `3-59/15` in the 'Minute' field matches the third minute of the hour and every 15 minutes thereafter. The form `*/...` is equivalent to `first-last/...`, representing an increment over the full range of the field.
#### Comma `,`
Commas separate items in a list. For instance, `MON,WED,FRI` in the 'Day of week' field matches Mondays, Wednesdays, and Fridays.
#### `L`
The character `L` stands for "last". In the 'Day of week' field, `5L` denotes the last Friday of a given month. In the 'Day of month' field, it represents the last day of the month.
- Using `L` alone in the 'Day of week' field is equivalent to `0` or `SAT`. Hence, expressions `* * * * * L *` and `* * * * * 0 *` are the same.
- When followed by another value in the 'Day of week' field, like `6L`, it signifies the last Friday of the month.
- If followed by a negative number in the 'Day of month' field, such as `L-3`, it indicates the third-to-last day of the month.
- If `L` is present in the beginning of 'Second' field, it turns on non standard leap second functionality. Unless timezone specifies leap seconds, it will cycle indefinitely, because it will not be able to find any leap second!
When using 'L', avoid specifying lists or ranges to prevent ambiguous results.
#### `W`
The `W` character is exclusive to the 'Day of month' field. It indicates the closest business day (Monday-Friday) to the given day. For example, `15W` means the nearest business day to the 15th of the month. If you set 1W for the day-of-month and the 1st falls on a Saturday, the trigger activates on Monday the 3rd, since it respects the month's day boundaries and won't skip over them. Similarly, at the end of the month, the behavior ensures it doesn't "jump" over the boundary to the following month.
The `W` character can also pair with `L` (as `LW`), signifying the last business day of the month. Alone, it's equivalent to the range `1-5`, making the expressions `* * * W * * *` and `* * * * * 1-5 *` identical. This interpretation differs from [1,2].
#### Hash `#`
The `#` character is only for the 'Day of week' field and should be followed by a number between one and five, or their negative values. It lets you specify constructs like "the second Friday" of a month.
For example, `6#3` means the third Friday of the month. Note that if you use `#5` and there isn't a fifth occurrence of that weekday in the month, no firing occurs for that month. Using the '#' character requires a single expression in the 'Day of week' field.
Negative nth values are also valid. For instance, `6#-1` is equivalent to `6L`.
#### Known limitation
1. Leap seconds can be prefixed with multiple `L` symbols: `LLLL60` without issuing errors.
2. Ordinals `JAN`...`DEC` and `SUN`...`SAT` are processed in all fields without issuing errors.
3. Errors from lexical analyzers are masked by parser errors.
4. Multiple `#` segments: `1#1,3#3,5#5` are not allowed.
Predefined cron expressions
---------------------------
(Copied from <https://en.wikipedia.org/wiki/Cron#Predefined_scheduling_definitions>, with text modified according to this implementation)
Entry Description Equivalent to
@annually Run once a year at midnight in the morning of January 1 0 0 0 1 1 *
@yearly Run once a year at midnight in the morning of January 1 0 0 0 1 1 *
@monthly Run once a month at midnight in the morning of the first of the month 0 0 0 1 * *
@weekly Run once a week at midnight in the morning of Sunday 0 0 0 * * 0
@daily Run once a day at midnight 0 0 0 * * *
@hourly Run once an hour at the beginning of the hour 0 0 * * * *
@minutely Run once a minute at the beginning of minute 0 * * * * *
@secondly Run once every second * * * * * * *
@reboot Not supported
Note that `@minutely` and `@secondly` are not standard.
Other details
-------------
* If only five fields are present, the Year and Second fields are omitted. The omitted Year and Second are `*` and `0` respectively.
* If only six fields are present, the Year field is omitted. The omitted Year is set to `*`. Note that this is different from [1] which has Second field omitted in this case and [2] which doesn't allow five fields.
* Only proper expressions are guaranteed to work.
* Cron doesn't decide calendar, it follows it. It doesn't and it should not disallow combinations like 31st April or 30rd February. Not only that these dates hisctorically happened, but they may very well happen based on timezone configuration. Within reasonable constrains, it should work under changed conditions.
Config
------
TinyCron can be configured by setting the below environmental variables to a non-empty value:
Variable | Description
--- | ---
TINYCRON_VERBOSE | Enable verbose output
Cron expression parsing in ANSI C
=================================
[![Build](https://github.com/mdvorak/ccronexpr/actions/workflows/build.yml/badge.svg)](https://github.com/mdvorak/ccronexpr/actions/workflows/build.yml)
Given a cron expression and a date, you can get the next date which satisfies the cron expression.
Supports cron expressions with `seconds` field. Based on implementation of [CronSequenceGenerator](https://github.com/spring-projects/spring-framework/blob/babbf6e8710ab937cd05ece20270f51490299270/spring-context/src/main/java/org/springframework/scheduling/support/CronSequenceGenerator.java) from Spring Framework.
Compiles and should work on Linux (GCC/Clang), Mac OS (Clang), Windows (MSVC), Android NDK, iOS and possibly on other platforms with `time.h` support.
Supports compilation in C (89) and in C++ modes.
Usage example
-------------
#include "ccronexpr.h"
cron_expr expr;
const char* err = NULL;
memset(&expr, 0, sizeof(expr));
cron_parse_expr("0 */2 1-4 * * *", &expr, &err);
if (err) ... /* invalid expression */
time_t cur = time(NULL);
time_t next = cron_next(&expr, cur);
Compilation and tests run examples
----------------------------------
gcc ccronexpr.c ccronexpr_test.c -I. -Wall -Wextra -std=c89 -o a.out && ./a.out
g++ ccronexpr.c ccronexpr_test.c -I. -Wall -Wextra -std=c++11 -o a.out && ./a.out
g++ ccronexpr.c ccronexpr_test.c -I. -Wall -Wextra -std=c++11 -DCRON_COMPILE_AS_CXX -o a.out && ./a.out
clang ccronexpr.c ccronexpr_test.c -I. -Wall -Wextra -std=c89 -o a.out && ./a.out
clang++ ccronexpr.c ccronexpr_test.c -I. -Wall -Wextra -std=c++11 -o a.out && ./a.out
clang++ ccronexpr.c ccronexpr_test.c -I. -Wall -Wextra -std=c++11 -DCRON_COMPILE_AS_CXX -o a.out && ./a.out
cl ccronexpr.c ccronexpr_test.c /W4 /D_CRT_SECURE_NO_WARNINGS && ccronexpr.exe
gcc ccronexpr.c supertinycron.c -I. -Wall -Wextra -std=c89 -o a.out && ./a.out
g++ ccronexpr.c supertinycron.c -I. -Wall -Wextra -std=c++11 -o a.out && ./a.out
g++ ccronexpr.c supertinycron.c -I. -Wall -Wextra -std=c++11 -DCRON_COMPILE_AS_CXX -o a.out && ./a.out
clang ccronexpr.c supertinycron.c -I. -Wall -Wextra -std=c89 -o a.out && ./a.out
clang++ ccronexpr.c supertinycron.c -I. -Wall -Wextra -std=c++11 -o a.out && ./a.out
clang++ ccronexpr.c supertinycron.c -I. -Wall -Wextra -std=c++11 -DCRON_COMPILE_AS_CXX -o a.out && ./a.out
cl ccronexpr.c supertinycron.c /W4 /D_CRT_SECURE_NO_WARNINGS && supertinycron.exe
Examples of supported expressions
---------------------------------
Expression, input date, next date:
"*/15 * 1-4 * * *", "2012-07-01_09:53:50", "2012-07-02_01:00:00"
"0 */2 1-4 * * *", "2012-07-01_09:00:00", "2012-07-02_01:00:00"
"0 0 7 ? * MON-FRI", "2009-09-26_00:42:55", "2009-09-28_07:00:00"
"0 30 23 30 1/3 ?", "2011-04-30_23:30:00", "2011-07-30_23:30:00"
See more examples in [tests](https://github.com/staticlibs/ccronexpr/blob/a1343bc5a546b13430bd4ac72f3b047ac08f8192/ccronexpr_test.c#L251).
Timezones
---------
This implementation does not support explicit timezones handling. By default, all dates are
processed as UTC (GMT) dates without timezone information.
To use local dates (current system timezone) instead of GMT compile with `-DCRON_USE_LOCAL_TIME`, example:
gcc -DCRON_USE_LOCAL_TIME ccronexpr.c ccronexpr_test.c -I. -Wall -Wextra -std=c89 -o a.out && TZ="America/Toronto" ./a.out
Years
-----
To disable Year field use `-DCRON_DISABLE_YEARS`. This will lower memory footriprint by 29 bytes for `cron_expr`. It will still accept year field, but field will not be validated and it will be triggered every year.
License information
-------------------
This project is released under the [Apache License 2.0](http://www.apache.org/licenses/LICENSE-2.0).
Changelog
---------
**2023**
* major extension of supported expressions
* command line tool
**2022**
* added CMake build
* added GitHub Workflow for continuous testing
* fixed type casts to support `-Wconvert`
* added tests for cron_prev and leap years
* fixed tests to work with `CRON_USE_LOCAL_TIME`
* added [ESP-IDF](./ESP-IDF.md) usage guide
**2019-03-27**
* `CRON_USE_LOCAL_TIME` usage fixes
**2018-05-23**
* merged [#8](https://github.com/staticlibs/ccronexpr/pull/8)
* merged [#9](https://github.com/staticlibs/ccronexpr/pull/9)
* minor cleanups
**2018-01-27**
* merged [#6](https://github.com/staticlibs/ccronexpr/pull/6)
* updated license file (to the one parse-able by github)
**2017-09-24**
* merged [#4](https://github.com/staticlibs/ccronexpr/pull/4)
**2016-06-17**
* use thread-safe versions of `gmtime` and `localtime`
**2015-02-28**
* initial public version
@@ -23,6 +23,10 @@ build_script:
- cl ccronexpr.c ccronexpr_test.c /W4 /D_CRT_SECURE_NO_WARNINGS /Feccronexpr_32.exe
- call "C:\Program Files (x86)\Microsoft Visual Studio\2017\Community\VC\Auxiliary\Build\vcvars64.bat"
- cl ccronexpr.c ccronexpr_test.c /W4 /D_CRT_SECURE_NO_WARNINGS /Feccronexpr_64.exe
# - call "C:\Program Files (x86)\Microsoft Visual Studio\2017\Community\VC\Auxiliary\Build\vcvars32.bat"
# - cl ccronexpr.c supertinycron.c /W4 /D_CRT_SECURE_NO_WARNINGS /Fesupertinycron_32.exe
# - call "C:\Program Files (x86)\Microsoft Visual Studio\2017\Community\VC\Auxiliary\Build\vcvars64.bat"
# - cl ccronexpr.c supertinycron.c /W4 /D_CRT_SECURE_NO_WARNINGS /Fesupertinycron_64.exe
test_script:
- ccronexpr_32.exe
+883
View File
@@ -0,0 +1,883 @@
/*
* Copyright 2015, alex at staticlibs.net
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/*
* File: ccronexpr.c
* Author: alex
*
* Created on February 24, 2015, 9:35 AM
*/
#include <stdlib.h>
#include <ctype.h>
#include <errno.h>
#include <string.h>
#include <stdio.h>
#include "ccronexpr.h"
#define CRON_MAX_SECONDS 60
#define CRON_MAX_LEAP_SECONDS 2
#define CRON_MAX_MINUTES 60
#define CRON_MAX_HOURS 24
#define CRON_MAX_DAYS_OF_MONTH 32
#define CRON_MAX_DAYS_OF_WEEK 7
#define CRON_MAX_MONTHS 12
#define CRON_MIN_YEARS 1970
#define CRON_MAX_YEARS 2200
#define CRON_MAX_YEARS_DIFF 4
#define YEAR_OFFSET 1900
#define DAY_SECONDS 24 * 60 * 60
#define WEEK_DAYS 7
#define CRON_CF_SECOND 0
#define CRON_CF_MINUTE 1
#define CRON_CF_HOUR_OF_DAY 2
#define CRON_CF_DAY_OF_WEEK 3
#define CRON_CF_DAY_OF_MONTH 4
#define CRON_CF_MONTH 5
#define CRON_CF_YEAR 6
#define CRON_CF_NEXT 7
#define CRON_CF_ARR_LEN 7
static const char* const DAYS_ARR[] = { "SUN", "MON", "TUE", "WED", "THU", "FRI", "SAT" };
#define CRON_DAYS_ARR_LEN 7
static const char* const MONTHS_ARR[] = { "FOO", "JAN", "FEB", "MAR", "APR", "MAY", "JUN", "JUL", "AUG", "SEP", "OCT", "NOV", "DEC" };
#define CRON_MONTHS_ARR_LEN 13
/**
* Time functions from standard library.
* This part defines: cron_mktime: create time_t from tm
* cron_time: create tm from time_t
*/
/* forward declarations for platforms that may need them */
/* can be hidden in time.h */
#if !defined(_WIN32) && !defined(__AVR__) && !defined(ESP8266) && !defined(ESP_PLATFORM) && !defined(ANDROID) && !defined(TARGET_LIKE_MBED)
struct tm *gmtime_r(const time_t *timep, struct tm *result);
time_t timegm(struct tm* __tp);
struct tm *localtime_r(const time_t *timep, struct tm *result);
#endif /* PLEASE CHECK _WIN32 AND ANDROID NEEDS FOR THESE DECLARATIONS */
#ifdef __MINGW32__
/* To avoid warning when building with mingw */
time_t _mkgmtime(struct tm* tm);
#endif /* __MINGW32__ */
/* function definitions */
#ifndef CRON_USE_LOCAL_TIME
static time_t cron_mktime_gm(struct tm* tm) {
#if defined(_WIN32)
/* http://stackoverflow.com/a/22557778 */
return _mkgmtime(tm);
#elif defined(__AVR__)
/* https://www.nongnu.org/avr-libc/user-manual/group__avr__time.html */
return mk_gmtime(tm);
#elif defined(ESP8266) || defined(ESP_PLATFORM) || defined(TARGET_LIKE_MBED)
#error "timegm() is not supported on the ESP platform, please use this library with CRON_USE_LOCAL_TIME"
#elif defined(ANDROID) && !defined(__LP64__)
/* https://github.com/adobe/chromium/blob/cfe5bf0b51b1f6b9fe239c2a3c2f2364da9967d7/base/os_compat_android.cc#L20 */
static const time_t kTimeMax = ~(1L << (sizeof (time_t) * CHAR_BIT - 1));
static const time_t kTimeMin = (1L << (sizeof (time_t) * CHAR_BIT - 1));
time64_t result = timegm64(tm);
if (result < kTimeMin || result > kTimeMax) return -1;
return result;
#else
return timegm(tm);
#endif
}
static struct tm* cron_time_gm(time_t* date, struct tm* out) {
#if defined(__MINGW32__)
(void)(out); /* To avoid unused warning */
return gmtime(date);
#elif defined(_WIN32)
errno_t err = gmtime_s(out, date);
return 0 == err ? out : NULL;
#elif defined(__AVR__)
/* https://www.nongnu.org/avr-libc/user-manual/group__avr__time.html */
gmtime_r(date, out);
return out;
#else
return gmtime_r(date, out);
#endif
}
#else
static time_t cron_mktime_local(struct tm* tm) {
tm->tm_isdst = -1;
return mktime(tm);
}
static struct tm* cron_time_local(time_t* date, struct tm* out) {
#if defined(_WIN32)
errno_t err = localtime_s(out, date);
return 0 == err ? out : NULL;
#elif defined(__AVR__)
/* https://www.nongnu.org/avr-libc/user-manual/group__avr__time.html */
localtime_r(date, out);
return out;
#else
return localtime_r(date, out);
#endif
}
#endif
/* Defining 'cron_' time functions to use use UTC (default) or local time */
#ifndef CRON_USE_LOCAL_TIME
time_t cron_mktime(struct tm* tm) {
return cron_mktime_gm(tm);
}
struct tm* cron_time(time_t* date, struct tm* out) {
return cron_time_gm(date, out);
}
#else /* CRON_USE_LOCAL_TIME */
time_t cron_mktime(struct tm* tm) {
return cron_mktime_local(tm);
}
struct tm* cron_time(time_t* date, struct tm* out) {
return cron_time_local(date, out);
}
#endif /* CRON_USE_LOCAL_TIME */
#define reset_all_min(calendar, fields) reset_all(reset_min, calendar, fields);
#define reset_all_max(calendar, fields) reset_all(reset_max, calendar, fields);
#define PARSE_ERROR(message) { context->err = message; goto error; }
#define CRON_ERROR(message) { *error = message; goto error; }
#define TOKEN_COMPARE(context, token) if (context->err) goto error; if (token == context->type) token_next(context); else goto compare_error;
#define GET_BYTE(idx) (uint8_t) (idx / 8)
#define GET_BIT(idx) (uint8_t) (idx % 8)
void cron_set_bit(uint8_t* rbyte, int idx) {
rbyte[GET_BYTE(idx)] |= (uint8_t)(1 << GET_BIT(idx));
}
void cron_del_bit(uint8_t* rbyte, int idx) {
rbyte[GET_BYTE(idx)] &= (uint8_t)~(1 << GET_BIT(idx));
}
uint8_t cron_get_bit(const uint8_t* rbyte, int idx) {
return (rbyte[GET_BYTE(idx)] & (1 << GET_BIT(idx))) ? 1 : 0;
}
static int next_set_bit(uint8_t* bits, int max, int from_index, int* notfound) {
int i;
if (!bits) goto error;
for (i = from_index; i < max; i++) if (cron_get_bit(bits, i)) return i;
error: *notfound = 1; return 0;
}
static int prev_set_bit(uint8_t* bits, int from_index, int to_index, int* notfound) {
int i;
if (!bits) goto error;
for (i = from_index; i >= to_index; i--) if (cron_get_bit(bits, i)) return i;
error: *notfound = 1; return 0;
}
static int* get_field_ptr(struct tm* calendar, int field) {
switch (field) {
case CRON_CF_SECOND: return &calendar->tm_sec;
case CRON_CF_MINUTE: return &calendar->tm_min;
case CRON_CF_HOUR_OF_DAY: return &calendar->tm_hour;
case CRON_CF_DAY_OF_WEEK: return &calendar->tm_wday;
case CRON_CF_DAY_OF_MONTH: return &calendar->tm_mday;
case CRON_CF_MONTH: return &calendar->tm_mon;
case CRON_CF_YEAR: return &calendar->tm_year;
default: return NULL; /* unknown field */
}
}
static int last_day_of_month(int month, int year) {
struct tm cal;
time_t t;
memset(&cal, 0, sizeof(struct tm));
cal.tm_mon = month + 1;
cal.tm_year = year;
t = cron_mktime(&cal);
return cron_time(&t, &cal)->tm_mday;
}
static int set_field(struct tm* calendar, int field, int val) {
int* field_ptr = get_field_ptr(calendar, field);
int last_mday;
if (!field_ptr || !calendar) return 1;
*field_ptr = val;
if (field == CRON_CF_MONTH) {
last_mday = last_day_of_month(calendar->tm_mon, calendar->tm_year);
if (calendar->tm_mday > last_mday) calendar->tm_mday = last_mday;
}
return CRON_INVALID_INSTANT == cron_mktime(calendar) ? 1 : 0;
}
static int add_to_field(struct tm* calendar, int field, int val) {
int* field_ptr;
if (CRON_CF_DAY_OF_WEEK == field) field = CRON_CF_DAY_OF_MONTH;
field_ptr = get_field_ptr(calendar, field);
if (!field_ptr || !calendar) return 1;
*field_ptr += val;
return CRON_INVALID_INSTANT == cron_mktime(calendar) ? 1 : 0;
}
/**
* Reset the calendar setting all the fields provided to zero.
*/
static int reset_min(struct tm* calendar, int field) {
return set_field(calendar, field, field == CRON_CF_DAY_OF_MONTH);
}
/**
* Reset the calendar setting all the fields provided to zero.
*/
static int reset_max(struct tm* calendar, int field) {
int value;
if (!calendar) return 1;
switch (field) {
case CRON_CF_SECOND: value = CRON_MAX_SECONDS-1; break;
case CRON_CF_MINUTE: value = CRON_MAX_MINUTES-1; break;
case CRON_CF_HOUR_OF_DAY: value = CRON_MAX_HOURS-1; break;
case CRON_CF_DAY_OF_WEEK: value = CRON_MAX_DAYS_OF_WEEK-1; break;
case CRON_CF_DAY_OF_MONTH: value = last_day_of_month(calendar->tm_mon, calendar->tm_year); break;
case CRON_CF_MONTH: value = CRON_MAX_MONTHS-1; break;
/* I don't think this is supposed to happen ... */
/* fprintf(stderr, "reset CRON_CF_YEAR\n"); */
case CRON_CF_YEAR: return 1;
default: return 1; /* unknown field */
}
return set_field(calendar, field, value);
}
static int last_weekday_of_month(int month, int year) {
struct tm cal;
time_t t;
memset(&cal, 0, sizeof(struct tm));
cal.tm_mon = month + 1; /* next month */
cal.tm_year = year; /* years since 1900 */
t = cron_mktime(&cal);
/* If the last day of the month is a Saturday (6) or Sunday (0), decrement the day.
* But it is shifted to (5) and (6). */
while (cron_time(&t, &cal)->tm_wday == 6 || cron_time(&t, &cal)->tm_wday == 0) t -= DAY_SECONDS; /* subtract a day */
return cron_time(&t, &cal)->tm_mday;
}
static int closest_weekday(int day_of_month, int month, int year) {
struct tm cal;
time_t t;
int wday;
memset(&cal, 0, sizeof(struct tm));
cal.tm_mon = month; /* given month */
cal.tm_mday = day_of_month + 1;
cal.tm_year = year; /* years since 1900 */
t = cron_mktime(&cal);
wday = cron_time(&t, &cal)->tm_wday;
/* If it's a Sunday */
if (wday == 0) {
/* If it's the last day of the month, go to the previous Friday */
if (day_of_month + 1 == last_day_of_month(month, year)) t -= 2 * DAY_SECONDS;
else t += DAY_SECONDS; /* go to the next Monday */
/* If it's a Saturday */
} else if (wday == 6) {
/* If it's the first day of the month, go to the next Monday */
if (day_of_month == 0) t += 2 * DAY_SECONDS;
else t -= DAY_SECONDS; /* go to the previous Friday */
}
/* If it's a weekday */
return cron_time(&t, &cal)->tm_mday;
}
static int reset_all(int (*fn)(struct tm* calendar, int field), struct tm* calendar, uint8_t* fields) {
int i;
int res = 0;
if (!calendar || !fields) return 1;
for (i = 0; i < CRON_CF_ARR_LEN; i++) if (cron_get_bit(fields, i)) {
res = fn(calendar, i);
if (0 != res) return res;
}
return 0;
}
typedef enum { T_ASTERISK, T_QUESTION, T_NUMBER, T_COMMA, T_SLASH, T_L, T_W, T_HASH, T_MINUS, T_WS, T_EOF, T_INVALID } TokenType;
typedef struct { const char* input; TokenType type; cron_expr* target; int field_type, value, min, max, offset, fix_dow; uint8_t* field; const char* err; } ParserContext;
static int compare_strings(const char* str1, const char* str2, size_t len) {
size_t i;
for (i = 0; i < len; i++) if (toupper(str1[i]) != str2[i]) return str1[i] - str2[i];
return 0;
}
static int match_ordinals(const char* str, const char* const* arr, size_t arr_len) {
size_t i;
for (i = 0; i < arr_len; i++) if (!compare_strings(str, arr[i], strlen(arr[i]))) return (int)i;
return -1;
}
static int count_fields(const char* str, char del) {
size_t count = 0;
if (!str) return -1;
while ((str = strchr(str, del)) != NULL) {
count++;
do str++; while (del == *str);
}
return (int)count + 1;
}
static void token_next(ParserContext* context) {
const char *input = context->input;
context->type = T_INVALID;
context->value = 0;
if (*context->input == '\0') context->type = T_EOF;
else if (isspace(*context->input)) {
do ++context->input; while (isspace(*context->input));
context->type = T_WS;
} else if (isdigit(*context->input)) {
do {
context->value = context->value * 10 + (*context->input - '0');
++context->input;
} while (isdigit(*context->input));
context->type = T_NUMBER;
} else if (isalpha(*input)) {
do ++input; while (isalpha(*input));
context->value = match_ordinals(context->input, DAYS_ARR, CRON_DAYS_ARR_LEN);
if (context->value < 0) context->value = match_ordinals(context->input, MONTHS_ARR, CRON_MONTHS_ARR_LEN);
if (context->value < 0) goto rest;
context->input = input;
context->type = T_NUMBER;
} else {
rest: switch (*context->input) {
case '*': context->type = T_ASTERISK; break;
case '?': context->type = T_QUESTION; break;
case ',': context->type = T_COMMA; break;
case '/': context->type = T_SLASH; break;
case 'L': context->type = T_L; break;
case 'W': context->type = T_W; break;
case '#': context->type = T_HASH; break;
case '-': context->type = T_MINUS; break;
}
++context->input;
}
if (T_INVALID == context->type) context->err = "Invalid token";
}
static int Number(ParserContext* context) {
int value = 0;
switch (context->type) {
case T_MINUS:
token_next(context);
if (T_NUMBER == context->type) {
value = -context->value;
token_next(context);
} else PARSE_ERROR("Number '-' follows with number");
break;
case T_NUMBER: value = context->value; token_next(context); break;
default: PARSE_ERROR("Number - error");
}
error: return value;
}
static int Frequency(ParserContext* context, int delta, int* to, int range) {
switch (context->type) {
case T_SLASH:
token_next(context);
if (T_NUMBER == context->type) {
delta = context->value;
if (delta < 1) PARSE_ERROR("Frequency - needs to be at least 1");
if (!range) *to = context->max - 1;
token_next(context);
} else PARSE_ERROR("Frequency - '/' follows with number");
break;
case T_COMMA: case T_WS: case T_EOF: break;
default: PARSE_ERROR("Frequency - error");
}
error: return delta;
}
static int Range(ParserContext* context, int* from, int to) {
int i;
switch (context->type) {
case T_HASH:
if (CRON_CF_DAY_OF_WEEK == context->field_type) {
token_next(context);
if (*context->target->day_in_month)
PARSE_ERROR("Nth-day - support for specifying multiple '#' segments is not implemented");
*context->target->day_in_month = (int8_t)Number(context);
if (*context->target->day_in_month > 5 || *context->target->day_in_month < -5)
PARSE_ERROR("Nth-day - '#' can follow only with -5..5");
} else PARSE_ERROR("Nth-day - '#' allowed only for day of week");
break;
case T_MINUS:
token_next(context);
if (T_NUMBER == context->type) {
to = context->value;
token_next(context);
} else PARSE_ERROR("Range '-' follows with number");
break;
case T_W:
*context->target->day_in_month = (int8_t)to;
*from = context->min;
for (i = 1; i <= 5; i++) cron_set_bit(context->target->days_of_week, i);
cron_set_bit(context->target->flags, 2);
to = context->max - 1;
token_next(context);
break;
case T_L:
if (CRON_CF_DAY_OF_WEEK == context->field_type) {
*context->target->day_in_month = -1;
token_next(context);
} else PARSE_ERROR("Range - 'L' allowed only for day of week");
break;
case T_WS: case T_SLASH: case T_COMMA: case T_EOF: break;
default: PARSE_ERROR("Range - error");
}
error: return to;
}
static void Segment(ParserContext* context) {
int i, from = context->min, to = context->max - 1, delta = 1, leap = 0;
start: switch (context->type) {
case T_ASTERISK: token_next(context); delta = Frequency(context, delta, &to, 0); break;
case T_NUMBER:
from = context->value;
token_next(context);
to = Range(context, &from, from);
if (context->err) goto error;
delta = Frequency(context, delta, &to, from != to);
break;
case T_L:
token_next(context);
switch (context->field_type) {
case CRON_CF_SECOND: if (!leap) context->max += CRON_MAX_LEAP_SECONDS; leap = 1; goto start;
case CRON_CF_DAY_OF_MONTH:
*context->target->day_in_month = -1;
switch (context->type) {
case T_MINUS: case T_NUMBER: *context->target->day_in_month += (int8_t)Number(context); break;
case T_W:
if (CRON_CF_DAY_OF_MONTH == context->field_type) {
token_next(context);
for (i = 1; i <= 5; i++) cron_set_bit(context->target->days_of_week, i);
cron_set_bit(context->target->flags, 1);
context->fix_dow = 1;
goto done;
} else PARSE_ERROR("Offset - 'W' allowed only for day of month");
case T_COMMA: case T_WS: case T_EOF: break;
default: PARSE_ERROR("Offset - error");
}
/* Note 0..6 and not 1..7, see end of set_days_of_week. */
for (i = 0; i <= 6; i++) cron_set_bit(context->target->days_of_week, i);
cron_set_bit(context->target->flags, 0);
context->fix_dow = 1;
break;
case CRON_CF_DAY_OF_WEEK: from = to = 0; break;
default: PARSE_ERROR("Segment 'L' allowed only for day of month and leap seconds")
}
break;
case T_W:
for (i = 1; i <= 5; i++) cron_set_bit(context->target->days_of_week, i);
token_next(context);
context->fix_dow = 1;
break;
case T_QUESTION: token_next(context); break;
default: PARSE_ERROR("Segment - error");
}
done: if (context->err) goto error;
if (CRON_CF_DAY_OF_WEEK == context->field_type && context->fix_dow) return;
if (from < context->min || to < context->min) PARSE_ERROR("Range - specified range is less than minimum");
if (from >= context->max || to >= context->max) PARSE_ERROR("Range - specified range exceeds maximum");
if (from > to) PARSE_ERROR("Range - specified range start exceeds range end");
for (; from <= to; from+=delta) cron_set_bit(context->field, from+context->offset);
if (CRON_CF_DAY_OF_WEEK == context->field_type) {
if (cron_get_bit(context->field, 7)) {
/* Sunday can be represented as 0 or 7*/
cron_set_bit(context->field, 0);
cron_del_bit(context->field, 7);
}
}
error: return;
}
static void Field(ParserContext* context) {
Segment(context);
if (context->err) goto error;
switch (context->type) {
case T_COMMA: token_next(context); Field(context); break;
case T_WS: case T_EOF: break;
default: PARSE_ERROR("FieldRest - error");
}
error: return;
}
static void FieldWrapper(ParserContext* context, int field_type, int min, int max, int offset, uint8_t* field) {
context->field_type = field_type;
context->min = min;
context->max = max;
context->offset = offset;
context->field = field;
Field(context);
}
static void Fields(ParserContext* context, int len) {
token_next(context);
if (len < 6) cron_set_bit(context->target->seconds, 0);
else {
FieldWrapper(context, CRON_CF_SECOND, 0, CRON_MAX_SECONDS, 0, context->target->seconds);
TOKEN_COMPARE(context, T_WS);
}
FieldWrapper(context, CRON_CF_MINUTE, 0, CRON_MAX_MINUTES, 0, context->target->minutes);
TOKEN_COMPARE(context, T_WS);
FieldWrapper(context, CRON_CF_HOUR_OF_DAY, 0, CRON_MAX_HOURS, 0, context->target->hours);
TOKEN_COMPARE(context, T_WS);
FieldWrapper(context, CRON_CF_DAY_OF_MONTH, 1, CRON_MAX_DAYS_OF_MONTH, 0, context->target->days_of_month);
TOKEN_COMPARE(context, T_WS);
FieldWrapper(context, CRON_CF_MONTH, 1, CRON_MAX_MONTHS + 1, -1, context->target->months);
TOKEN_COMPARE(context, T_WS);
FieldWrapper(context, CRON_CF_DAY_OF_WEEK, 0, CRON_MAX_DAYS_OF_WEEK + 1, 0, context->target->days_of_week);
#ifndef CRON_DISABLE_YEARS
if (len < 7) cron_set_bit(context->target->years, EXPR_YEARS_LENGTH*8-1);
else {
TOKEN_COMPARE(context, T_WS);
FieldWrapper(context, CRON_CF_YEAR, CRON_MIN_YEARS, CRON_MAX_YEARS, -CRON_MIN_YEARS, context->target->years);
}
#endif
return;
compare_error: PARSE_ERROR("Fields - expected whitespace separator");
error: return;
}
/**
* Search the bits provided for the next set bit after the value provided,
* and reset the calendar.
*/
static int find_next(uint8_t* bits, int max, int value, int offset, struct tm* calendar, int field, int nextField, uint8_t* lower_orders, int* res_out) {
int notfound = 0, err = 0, next_value = next_set_bit(bits, max, value+offset, &notfound)-offset;
/* roll over if needed */
if (notfound) {
err = add_to_field(calendar, nextField, 1);
if (err) goto return_error;
err = reset_min(calendar, field);
if (err) goto return_error;
notfound = 0;
next_value = next_set_bit(bits, max, 0, &notfound);
}
if (notfound || next_value != value) {
err = reset_all_min(calendar, lower_orders);
if (err) goto return_error;
err = set_field(calendar, field, next_value);
if (err) goto return_error;
}
return next_value;
return_error: *res_out = 1; return 0;
}
/**
* Search the bits provided for the next set bit after the value provided,
* and reset the calendar.
*/
static int find_prev(uint8_t* bits, int max, int value, int offset, struct tm* calendar, int field, int nextField, uint8_t* lower_orders, int* res_out) {
int notfound = 0, err = 0, next_value = prev_set_bit(bits, value+offset, 0, &notfound)-offset;
/* roll under if needed */
if (notfound) {
err = add_to_field(calendar, nextField, -1);
if (err) goto return_error;
err = reset_max(calendar, field);
if (err) goto return_error;
notfound = 0;
next_value = prev_set_bit(bits, max - 1, value, &notfound);
}
if (notfound || next_value != value) {
err = reset_all_max(calendar, lower_orders);
if (err) goto return_error;
err = set_field(calendar, field, next_value);
if (err) goto return_error;
}
return next_value;
return_error: *res_out = 1; return 0;
}
static int find_day_condition(struct tm* calendar, uint8_t* days_of_month, int8_t* dim, int dom, uint8_t* days_of_week, int dow, uint8_t* flags, int* day) {
int tmp_day = *day;
if (tmp_day < 0) {
if ((!*flags && *dim < 0) || *flags & 1) tmp_day = last_day_of_month(calendar->tm_mon, calendar->tm_year);
else if (*flags & 2) tmp_day = last_weekday_of_month(calendar->tm_mon, calendar->tm_year);
else if (*flags & 4) tmp_day = closest_weekday(*dim-1, calendar->tm_mon, calendar->tm_year);
*day = tmp_day;
}
if (!cron_get_bit(days_of_month, dom)) return 1;
if (!cron_get_bit(days_of_week, dow)) return 1;
if (*flags) {
if ((*flags & 3) && dom != tmp_day+1+*dim) return 1;
if ((*flags & 4) && dom != tmp_day) return 1;
} else {
if (*dim < 0 && (dom < tmp_day+WEEK_DAYS**dim+1 || dom >= tmp_day+WEEK_DAYS*(*dim+1)+1)) return 1;
if (*dim > 0 && (dom < WEEK_DAYS*(*dim-1)+1 || dom >= WEEK_DAYS**dim+1)) return 1;
}
return 0;
}
static int find_day(struct tm* calendar, uint8_t* days_of_month, int8_t* dim, int dom, uint8_t* days_of_week, int dow, uint8_t* flags, uint8_t* resets, int* res_out, int offset) {
int err, day = -1, year = calendar->tm_year, month = calendar->tm_mon;
unsigned int count = 0, max = 366;
while (find_day_condition(calendar, days_of_month, dim, dom, days_of_week, dow, flags, &day) && count++ < max) {
err = add_to_field(calendar, CRON_CF_DAY_OF_MONTH, offset);
if (err) goto return_error;
dom = calendar->tm_mday;
dow = calendar->tm_wday;
if (year != calendar->tm_year) {
year = calendar->tm_year;
/* This should not be needed unless there is as single day month in libc. */
day = -1;
}
if (month != calendar->tm_mon) {
month = calendar->tm_mon;
day = -1;
}
if (offset > 0) reset_all_min(calendar, resets) else reset_all_max(calendar, resets);
}
return dom;
return_error: *res_out = 1; return 0;
}
static int do_nextprev(
int (*find)(uint8_t* bits, int max, int value, int offset, struct tm* calendar, int field, int nextField, uint8_t* lower_orders, int* res_out),
cron_expr* expr, struct tm* calendar, int dot, int offset) {
int res = 0, value = 0, update_value = 0;
uint8_t resets[1], empty_list[1] = {0};
for(;;) {
*resets = 0;
value = calendar->tm_sec;
update_value = find(expr->seconds, CRON_MAX_SECONDS+CRON_MAX_LEAP_SECONDS, value, 0, calendar, CRON_CF_SECOND, CRON_CF_MINUTE, empty_list, &res);
if (0 != res) break;
if (value == update_value) cron_set_bit(resets, CRON_CF_SECOND);
else if (update_value >= CRON_MAX_SECONDS) continue;
value = calendar->tm_min;
update_value = find(expr->minutes, CRON_MAX_MINUTES, value, 0, calendar, CRON_CF_MINUTE, CRON_CF_HOUR_OF_DAY, resets, &res);
if (0 != res) break;
if (value == update_value) cron_set_bit(resets, CRON_CF_MINUTE); else continue;
value = calendar->tm_hour;
update_value = find(expr->hours, CRON_MAX_HOURS, value, 0, calendar, CRON_CF_HOUR_OF_DAY, CRON_CF_DAY_OF_WEEK, resets, &res);
if (0 != res) break;
if (value == update_value) cron_set_bit(resets, CRON_CF_HOUR_OF_DAY); else continue;
value = calendar->tm_mday;
update_value = find_day(calendar, expr->days_of_month, expr->day_in_month, value, expr->days_of_week, calendar->tm_wday, expr->flags, resets, &res, offset);
if (0 != res) break;
if (value == update_value) cron_set_bit(resets, CRON_CF_DAY_OF_MONTH); else continue;
value = calendar->tm_mon; /*day already adds one if no day in same value is found*/
update_value = find(expr->months, CRON_MAX_MONTHS, value, 0, calendar, CRON_CF_MONTH, CRON_CF_YEAR, resets, &res);
if (0 != res) break;
if (value != update_value) {
if (abs(calendar->tm_year - dot) > CRON_MAX_YEARS_DIFF) {
res = -1;
break;
}
continue;
}
#ifdef CRON_DISABLE_YEARS
else break;
#else
if (cron_get_bit(expr->years, EXPR_YEARS_LENGTH*8-1)) break;
value = calendar->tm_year;
update_value = find(expr->years, CRON_MAX_YEARS-CRON_MIN_YEARS, value, YEAR_OFFSET-CRON_MIN_YEARS, calendar, CRON_CF_YEAR, CRON_CF_NEXT, resets, &res);
if (0 != res || value == update_value) break;
#endif
}
return res;
}
#define STRCATC(dest, buf, inc_len) do { len += inc_len; if (len > buffer_len) return -1; strcat(dest, buf); } while (0)
static int generate_field(char *dest, uint8_t *bits, int min, int max, int offset, int buffer_len) {
char buf[32];
int first = 1, from = -1, i, bit, len = 0;
for (i = min; i < max; i++) {
bit = cron_get_bit(bits, i + offset);
if (bit) {
if (from == -1) from = i;
} else if (from != -1) {
if (first) first = 0; else STRCATC(dest, ",", 1);
if (from == i - 1) len += sprintf(buf, "%d", from);
else len += sprintf(buf, "%d-%d", from, i - 1);
STRCATC(dest, buf, 0);
from = -1;
}
}
if (from == i - 1) {
if (first) first = 0; else STRCATC(dest, ",", 1);
STRCATC(dest, buf, sprintf(buf, "%d", from));
} else if (from == min && i == max) STRCATC(dest, "*", 1);
else if (from != -1) {
if (first) first = 0; else STRCATC(dest, ",", 1);
STRCATC(dest, buf, sprintf(buf, "%d-%d", from, i - 1));
}
return len;
}
#define GFC(dest, bits, min, max, offset, buffer_len) { tmp = generate_field(dest, bits, min, max, offset, buffer_len); if (tmp < 0) return tmp; else len += tmp; }
int cron_generate_expr(cron_expr *source, char *buffer, int buffer_len, int cron_len, const char **error) {
const char* err_local;
char buf[32];
int i, leap = 0, tmp, len = 1;
uint8_t days_of_week = *source->days_of_week;
*buffer = '\0';
if (!error) error = &err_local;
if (cron_len > 5) {
for (i = CRON_MAX_SECONDS; i < CRON_MAX_SECONDS+CRON_MAX_LEAP_SECONDS; i++) {
if (cron_get_bit(source->seconds, i)) {
leap = 1;
STRCATC(buffer, "L", 1);
break;
}
}
GFC(buffer, source->seconds, 0, CRON_MAX_SECONDS + (leap ? CRON_MAX_LEAP_SECONDS : 0), 0, buffer_len - len);
STRCATC(buffer, " ", 1);
}
GFC(buffer, source->minutes, 0, CRON_MAX_MINUTES, 0, buffer_len - len);
STRCATC(buffer, " ", 1);
GFC(buffer, source->hours, 0, CRON_MAX_HOURS, 0, buffer_len - len);
STRCATC(buffer, " ", 1);
if (cron_get_bit(source->flags, 0)) {
STRCATC(buffer, "L", 1);
if (*source->day_in_month < -1) STRCATC(buffer, buf, sprintf(buf, "%d", *source->day_in_month + 1));
} else if (cron_get_bit(source->flags, 1)) STRCATC(buffer, "LW", 2);
else if (cron_get_bit(source->flags, 2)) STRCATC(buffer, buf, sprintf(buf, "%dW", *source->day_in_month));
else if (*source->day_in_month != 0) STRCATC(buffer, "?", 1);
else GFC(buffer, source->days_of_month, 1, CRON_MAX_DAYS_OF_MONTH, 0, buffer_len - len);
STRCATC(buffer, " ", 1);
GFC(buffer, source->months, 1, CRON_MAX_MONTHS + 1, -1, buffer_len - len);
STRCATC(buffer, " ", 1);
if (cron_get_bit(&days_of_week, 0)) {
cron_set_bit(&days_of_week, 7);
cron_del_bit(&days_of_week, 0);
}
if (*source->flags) STRCATC(buffer, "?", 1);
else {
if (*source->day_in_month != 0) {
GFC(buffer, &days_of_week, 1, CRON_MAX_DAYS_OF_WEEK+1, 0, buffer_len - len);
if (*source->day_in_month == -1) STRCATC(buffer, "L", 1);
else STRCATC(buffer, buf, sprintf(buf, "#%d", *source->day_in_month));
} else GFC(buffer, &days_of_week, 1, CRON_MAX_DAYS_OF_WEEK+1, 0, buffer_len - len);
}
#ifndef CRON_DISABLE_YEARS
if (cron_len > 6) {
if (cron_get_bit(source->years, EXPR_YEARS_LENGTH*8-1)) STRCATC(buffer, " *", 2);
else {
STRCATC(buffer, " ", 1);
GFC(buffer, source->years, CRON_MIN_YEARS, CRON_MAX_YEARS, -CRON_MIN_YEARS, buffer_len - len);
}
}
#endif
return len;
}
static time_t cron(
int (*find)(uint8_t* bits, int max, int value, int offset, struct tm* calendar, int field, int nextField, uint8_t* lower_orders, int* res_out),
cron_expr* expr, time_t date, int offset) {
/*
The plan:
1 Round up to the next whole second
2 If seconds match move on, otherwise find the next match:
2.1 If next match is in the next minute then roll forwards
3 If minute matches move on, otherwise find the next match
3.1 If next match is in the next hour then roll forwards
3.2 Reset the seconds and go to 2
4 If hour matches move on, otherwise find the next match
4.1 If next match is in the next day then roll forwards,
4.2 Reset the minutes and seconds and go to 2
...
*/
struct tm calval, *calendar;
int res;
time_t original, calculated;
if (!expr) return CRON_INVALID_INSTANT;
memset(&calval, 0, sizeof(struct tm));
calendar = cron_time(&date, &calval);
if (!calendar) return CRON_INVALID_INSTANT;
original = cron_mktime(calendar);
if (CRON_INVALID_INSTANT == original) return CRON_INVALID_INSTANT;
res = do_nextprev(find, expr, calendar, calendar->tm_year, offset);
if (0 != res) return CRON_INVALID_INSTANT;
calculated = cron_mktime(calendar);
if (CRON_INVALID_INSTANT == calculated) return CRON_INVALID_INSTANT;
if (calculated == original) {
/* We arrived at the original timestamp - round up to the next whole second and try again... */
res = add_to_field(calendar, CRON_CF_SECOND, offset);
if (0 != res) return CRON_INVALID_INSTANT;
res = do_nextprev(find, expr, calendar, calendar->tm_year, offset);
if (0 != res) return CRON_INVALID_INSTANT;
}
return cron_mktime(calendar);
}
void cron_parse_expr(const char* expression, cron_expr* target, const char** error) {
const char* err_local;
int len = 0;
ParserContext context;
if (!error) error = &err_local;
*error = NULL;
if (!expression) CRON_ERROR("Invalid NULL expression");
if (!target) CRON_ERROR("Invalid NULL target");
if ('@' == *expression) {
expression++;
if (!strcmp("annually", expression) || !strcmp("yearly", expression)) expression = "0 0 0 1 1 *";
else if (!strcmp("monthly", expression)) expression = "0 0 0 1 * *";
else if (!strcmp("weekly", expression)) expression = "0 0 0 * * 0";
else if (!strcmp("daily", expression) || !strcmp("midnight", expression)) expression = "0 0 0 * * *";
else if (!strcmp("hourly", expression)) expression = "0 0 * * * *";
else if (!strcmp("minutely", expression)) expression = "0 * * * * *";
else if (!strcmp("secondly", expression)) expression = "* * * * * * *";
else if (!strcmp("reboot", expression)) CRON_ERROR("@reboot not implemented");
}
len = count_fields(expression, ' ');
if (len < 5 || len > 7) CRON_ERROR("Invalid number of fields, expression must consist of 5-7 fields");
memset(target, 0, sizeof(*target));
memset(&context, 0, sizeof(context));
context.input = expression;
context.target = target;
Fields(&context, len);
*error = context.err;
error: return;
}
time_t cron_next(cron_expr* expr, time_t date) { return cron(find_next, expr, date, 1); }
time_t cron_prev(cron_expr* expr, time_t date) { return cron(find_prev, expr, date, -1); }
@@ -14,18 +14,18 @@
* limitations under the License.
*/
/*
/*
* File: ccronexpr.h
* Author: alex
*
* Created on February 24, 2015, 9:35 AM
*
* Cron doesn't decide calendar, it follows it.
*/
#ifndef CCRONEXPR_H
#define CCRONEXPR_H
//-V::795
#if defined(__cplusplus) && !defined(CRON_COMPILE_AS_CXX)
extern "C" {
#endif
@@ -41,12 +41,12 @@ extern "C" {
#define CRON_INVALID_INSTANT ((time_t) -1)
// Define to use local time
#define CRON_USE_LOCAL_TIME
/**
* Parsed cron expression
*/
#define EXPR_YEARS_LENGTH 29
typedef struct {
uint8_t seconds[8];
uint8_t minutes[8];
@@ -54,11 +54,22 @@ typedef struct {
uint8_t days_of_week[1];
uint8_t days_of_month[4];
uint8_t months[2];
int8_t day_in_month[1];
/**
* Flags:
* 0 last day of the month
* 1 last weekday of the month
* 2 closest weekday to day in month
*/
uint8_t flags[1];
#ifndef CRON_DISABLE_YEARS
uint8_t years[EXPR_YEARS_LENGTH];
#endif
} cron_expr;
/**
* Parses specified cron expression.
*
*
* @param expression cron expression as nul-terminated string,
* should be no longer that 256 bytes
* @param pointer to cron expression structure, it's client code responsibility
@@ -71,10 +82,10 @@ void cron_parse_expr(const char* expression, cron_expr* target, const char** err
/**
* Uses the specified expression to calculate the next 'fire' date after
* the specified date. All dates are processed as UTC (GMT) dates
* without timezones information. To use local dates (current system timezone)
* the specified date. All dates are processed as UTC (GMT) dates
* without timezones information. To use local dates (current system timezone)
* instead of GMT compile with '-DCRON_USE_LOCAL_TIME'
*
*
* @param expr parsed cron expression to use in next date calculation
* @param date start date to start calculation from
* @return next 'fire' date in case of success, '((time_t) -1)' in case of error.
@@ -83,21 +94,30 @@ time_t cron_next(cron_expr* expr, time_t date);
/**
* Uses the specified expression to calculate the previous 'fire' date after
* the specified date. All dates are processed as UTC (GMT) dates
* without timezones information. To use local dates (current system timezone)
* the specified date. All dates are processed as UTC (GMT) dates
* without timezones information. To use local dates (current system timezone)
* instead of GMT compile with '-DCRON_USE_LOCAL_TIME'
*
*
* @param expr parsed cron expression to use in previous date calculation
* @param date start date to start calculation from
* @return previous 'fire' date in case of success, '((time_t) -1)' in case of error.
*/
time_t cron_prev(cron_expr* expr, time_t date);
/**
* Generate cron expression from cron_expr structure
*
* @param expr parsed cron expression to use
* @param buffer buffer for the result
* @param buffer_len maximum length of the buffer
* @param expr_len number of cron fields produced
* @param error output error message, will be set to string literal
* @return used length of the buffer or -1 on error
*/
int cron_generate_expr(cron_expr *source, char *buffer, int buffer_len, int expr_len, const char **error);
#if defined(__cplusplus) && !defined(CRON_COMPILE_AS_CXX)
} /* extern "C"*/
#endif
#endif /* CCRONEXPR_H */
+884
View File
@@ -0,0 +1,884 @@
/*
* Copyright 2015, alex at staticlibs.net
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/*
* File: CronExprParser_test.cpp
* Author: alex
*
* Created on February 24, 2015, 9:36 AM
*/
#include <assert.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <limits.h>
#include "ccronexpr.h"
#define MAX_SECONDS 62
#define CRON_MAX_MINUTES 60
#define CRON_MAX_HOURS 24
#define CRON_MAX_DAYS_OF_WEEK 8
#define CRON_MAX_DAYS_OF_MONTH 32
#define CRON_MAX_MONTHS 12
#define DATE_FORMAT "%Y-%m-%d_%H:%M:%S"
#ifndef ARRAY_LEN
#define ARRAY_LEN(x) sizeof(x)/sizeof(x[0])
#endif
/* declared in cronexpr.c */
time_t cron_mktime(struct tm* tm);
/**
* uint8_t* replace char* for storing hit dates, set_bit and get_bit are used as handlers
*/
uint8_t cron_get_bit(const uint8_t* rbyte, int idx);
void cron_set_bit(uint8_t* rbyte, int idx);
void cron_del_bit(uint8_t* rbyte, int idx);
static int crons_equal(cron_expr* cr1, cron_expr* cr2) {
unsigned int i;
for (i = 0; i < ARRAY_LEN(cr1->seconds); i++) {
if (cr1->seconds[i] != cr2->seconds[i]) {
printf("seconds not equal @%d %02x != %02x", i, cr1->seconds[i], cr2->seconds[i]);
return 0;
}
}
for (i = 0; i < ARRAY_LEN(cr1->minutes); i++) {
if (cr1->minutes[i] != cr2->minutes[i]) {
printf("minutes not equal @%d %02x != %02x", i, cr1->minutes[i], cr2->minutes[i]);
return 0;
}
}
for (i = 0; i < ARRAY_LEN(cr1->hours); i++) {
if (cr1->hours[i] != cr2->hours[i]) {
printf("hours not equal @%d %02x != %02x", i, cr1->hours[i], cr2->hours[i]);
return 0;
}
}
for (i = 0; i < ARRAY_LEN(cr1->days_of_week); i++) {
if (cr1->days_of_week[i] != cr2->days_of_week[i]) {
printf("days_of_week not equal @%d %02x != %02x", i, cr1->days_of_week[i], cr2->days_of_week[i]);
return 0;
}
}
for (i = 0; i < ARRAY_LEN(cr1->days_of_month); i++) {
if (cr1->days_of_month[i] != cr2->days_of_month[i]) {
printf("days_of_month not equal @%d %02x != %02x", i, cr1->days_of_month[i], cr2->days_of_month[i]);
return 0;
}
}
for (i = 0; i < ARRAY_LEN(cr1->months); i++) {
if (cr1->months[i] != cr2->months[i]) {
printf("months not equal @%d %02x != %02x", i, cr1->months[i], cr2->months[i]);
return 0;
}
}
return 1;
}
int one_dec_num(const char ch) {
switch (ch) {
case '0':
return 0;
case '1':
return 1;
case '2':
return 2;
case '3':
return 3;
case '4':
return 4;
case '5':
return 5;
case '6':
return 6;
case '7':
return 7;
case '8':
return 8;
case '9':
return 9;
default:
return -1;
}
}
int extract_digits(const char *str, int *digit_count) {
int num = 0;
if (!str || !digit_count) return 0;
while (*str) {
(*digit_count)++;
if (*str >= '0' && *str <= '9') {
num = num * 10 + (*str - '0');
} else break;
str++;
}
return num;
}
/* strptime is not available in msvc */
/* 2012-07-01_09:53:50 */
/* 0123456789012345678 */
void poors_mans_strptime(const char* str, struct tm* cal) {
int count = 0;
assert(cal != NULL);
memset(cal, 0, sizeof(struct tm));
cal->tm_year = extract_digits(str, &count) - 1900;
cal->tm_mon = extract_digits(str + count, &count) - 1;
cal->tm_mday = extract_digits(str + count, &count);
cal->tm_wday = 0;
cal->tm_yday = 0;
cal->tm_hour = extract_digits(str + count, &count);
cal->tm_min = extract_digits(str + count, &count);
cal->tm_sec = extract_digits(str + count, &count);
cal->tm_isdst = -1;
}
typedef time_t (*cron_find_fn)(cron_expr*, time_t);
int count_fields(const char* str, char del) {
size_t count = 0;
if (!str) return -1;
while ((str = strchr(str, del)) != NULL) {
count++;
do str++; while (del == *str);
}
return (int)count + 1;
}
#define check_fn(fn_fn,pattern,initial,expected) check_fn_line(fn_fn, pattern, initial, expected, __LINE__)
void check_fn_line(cron_find_fn fn, const char* pattern, const char* initial, const char* expected, int line) {
const char* err = NULL;
const int len = count_fields(pattern, ' ');
cron_expr parsed1, parsed2;
/*printf("Pattern: %s\n", pattern);**/
cron_parse_expr(pattern, &parsed1, &err);
struct tm calinit;
poors_mans_strptime(initial, &calinit);
time_t dateinit = cron_mktime(&calinit);
assert(-1 != dateinit);
time_t datenext = fn(&parsed1, dateinit);
#ifdef CRON_USE_LOCAL_TIME
struct tm* calnext = localtime(&datenext);
#else
struct tm* calnext = gmtime(&datenext);
#endif
assert(calnext);
char* buffer = (char*) malloc(512);
memset(buffer, 0, 512);
strftime(buffer, 512, DATE_FORMAT, calnext);
printf("parsed: %s\n", pattern);
if (0 != strcmp(expected, buffer+(buffer[0] == '+' ? 1 : 0))) {
printf("Line: %d\n", line);
printf("Pattern: %s\n", pattern);
printf("Initial: %s\n", initial);
printf("Expected: %s\n", expected);
printf("Actual: %s\n", buffer);
assert(0);
}
assert(cron_generate_expr(&parsed1, buffer, 512, len, &err) > 0);
if (0 != strcmp(pattern, buffer)) {
printf("Line: %d\n", line);
printf("Pattern: %s\n", pattern);
printf("Actual: %s\n", buffer);
}
cron_parse_expr(buffer, &parsed2, &err);
assert(crons_equal(&parsed1, &parsed2));
free(buffer);
}
void check_same(const char* expr1, const char* expr2) {
cron_expr parsed1;
cron_parse_expr(expr1, &parsed1, NULL);
cron_expr parsed2;
cron_parse_expr(expr2, &parsed2, NULL);
printf("parsed1: %s\n", expr1);
printf("parsed2: %s\n", expr2);
assert(crons_equal(&parsed1, &parsed2));
}
void check_calc_invalid() {
cron_expr parsed;
cron_parse_expr("0 0 0 31 6 *", &parsed, NULL);
struct tm calinit;
poors_mans_strptime("2012-07-01_09:53:50", &calinit);
time_t dateinit = cron_mktime(&calinit);
time_t res = cron_next(&parsed, dateinit);
assert(CRON_INVALID_INSTANT == res);
}
void check_expr_invalid(const char* pattern) {
const char* err = NULL;
const int len = count_fields(pattern, ' ');
cron_expr parsed1, parsed2;
cron_parse_expr(pattern, &parsed1, &err);
printf("parsed1: %s\n", pattern);
if (err) {
printf("check_expr_invalid: %s\n", err);
}
assert(err);
}
#define check_expr_valid(pattern) check_expr_valid_line(pattern, __LINE__)
void check_expr_valid_line(const char* pattern, int line) {
const char* err = NULL;
const int len = count_fields(pattern, ' ');
cron_expr parsed1, parsed2;
cron_parse_expr(pattern, &parsed1, &err);
printf("parsed1: %s\n", pattern);
if (err) {
printf("check_expr_invalid: %s\n", err);
}
char* buffer = (char*) malloc(512);
memset(buffer, 0, 512);
assert(cron_generate_expr(&parsed1, buffer, 512, len, &err) > 0);
if (0 != strcmp(pattern, buffer)) {
printf("Line: %d\n", line);
printf("Pattern: %s\n", pattern);
printf("Actual: %s\n", buffer);
}
cron_parse_expr(buffer, &parsed2, &err);
assert(crons_equal(&parsed1, &parsed2));
assert(!err);
}
void test_expr() {
char* tz = getenv("TZ");
/*Test leap seconds - nejsou nastavené hodnoty, co se kontrolují */
/*Test leap seconds
check_fn(cron_next, "60 0 0 * * *", "2015-01-01_15:12:42", "2015-06-30_00:00:00");*/
check_fn(cron_next, "* * * * * *", "2100-01-01_15:12:42", "2100-01-01_15:12:43");
check_fn(cron_next, "* * * * * *", "2198-01-01_15:12:42", "2198-01-01_15:12:43");
check_fn(cron_next, "* * * * * *", "2199-01-01_15:12:42", "2199-01-01_15:12:43");
if (tz && !strcmp("right/UTC", tz)) {
check_fn(cron_next, "L59 * * * * *", "2016-12-31_23:50:00", "2016-12-31_23:50:59");
check_fn(cron_next, "L60 * * * * *", "2016-12-31_23:50:00", "2016-12-31_23:59:60");
check_fn(cron_next, "L60 * * * * *", "2016-12-31_23:59:59", "2016-12-31_23:59:60");
}
check_fn(cron_next, "* * * 1 1 * *", "1970-01-01_15:12:42", "1970-01-01_15:12:43");
#ifndef CRON_DISABLE_YEARS
check_fn(cron_next, "* * * 1 1 * 1970,2100,2193,2199", "1970-01-01_15:12:42", "1970-01-01_15:12:43");
/*check_fn(cron_next, "* * * 1 1 * 1969,2100,2193,2199", "1969-01-01_15:12:42", "1969-01-01_15:12:43");*/
check_fn(cron_next, "* * * 1 1 * 1970,2100,2193,2199", "1971-01-01_15:12:42", "2100-01-01_00:00:00");
check_fn(cron_next, "* * * 1 1 * 1970,2100,2193,2199", "2195-01-01_15:12:42", "2199-01-01_00:00:00");
/*check_fn(cron_next, "* * * 1 1 * 1970,2100,2193,2200", "2195-01-01_15:12:42", "2200-01-01_00:00:00");*/
check_fn(cron_next, "* * * 1 1 * 2020", "2011-01-01_15:12:42", "2020-01-01_00:00:00");
#endif
check_fn(cron_next, "* * * 1W * *", "2011-01-01_15:12:42", "2011-01-03_00:00:00");
check_fn(cron_next, "* * * 31W * *", "2010-01-01_15:12:42", "2010-01-29_00:00:00");
check_fn(cron_next, "* * * LW * *", "2010-01-01_15:12:42", "2010-01-29_00:00:00");
check_fn(cron_next, "* * * LW * *", "2010-02-03_15:12:42", "2010-02-26_00:00:00");
check_fn(cron_next, "* * * LW * *", "2010-03-06_15:12:42", "2010-03-31_00:00:00");
check_fn(cron_next, "* * * LW * *", "2010-04-09_15:12:42", "2010-04-30_00:00:00");
check_fn(cron_next, "* * * LW * *", "2010-05-12_15:12:42", "2010-05-31_00:00:00");
check_fn(cron_next, "* * * LW * *", "2010-06-15_15:12:42", "2010-06-30_00:00:00");
check_fn(cron_next, "* * * LW * *", "2010-07-18_15:12:42", "2010-07-30_00:00:00");
check_fn(cron_next, "* * * LW * *", "2010-08-21_15:12:42", "2010-08-31_00:00:00");
check_fn(cron_next, "* * * LW * *", "2010-09-24_15:12:42", "2010-09-30_00:00:00");
check_fn(cron_next, "* * * LW * *", "2010-10-27_15:12:42", "2010-10-29_00:00:00");
check_fn(cron_next, "* * * LW * *", "2010-11-30_15:12:42", "2010-11-30_15:12:43");
check_fn(cron_next, "* * * LW * *", "2010-12-31_15:12:42", "2010-12-31_15:12:43");
check_fn(cron_next, "* * * 15W * *", "2010-01-01_15:12:42", "2010-01-15_00:00:00");
check_fn(cron_next, "* * * 15W * *", "2010-02-03_15:12:42", "2010-02-15_00:00:00");
check_fn(cron_next, "* * * 15W * *", "2010-03-06_15:12:42", "2010-03-15_00:00:00");
check_fn(cron_next, "* * * 15W * *", "2010-04-09_15:12:42", "2010-04-15_00:00:00");
check_fn(cron_next, "* * * 15W * *", "2010-05-12_15:12:42", "2010-05-14_00:00:00");
check_fn(cron_next, "* * * 15W * *", "2010-06-15_15:12:42", "2010-06-15_15:12:43");
check_fn(cron_next, "* * * 15W * *", "2010-07-18_15:12:42", "2010-08-16_00:00:00");
check_fn(cron_next, "* * * 15W * *", "2010-08-21_15:12:42", "2010-09-15_00:00:00");
check_fn(cron_next, "* * * 15W * *", "2010-09-24_15:12:42", "2010-10-15_00:00:00");
check_fn(cron_next, "* * * 15W * *", "2010-10-27_15:12:42", "2010-11-15_00:00:00");
check_fn(cron_next, "* * * 15W * *", "2010-11-30_15:12:42", "2010-12-15_00:00:00");
check_fn(cron_next, "* * * 15W * *", "2010-12-31_15:12:42", "2011-01-14_00:00:00");
check_fn(cron_next, "* * * L-1 * *", "2010-01-01_15:12:42", "2010-01-30_00:00:00");
check_fn(cron_next, "* * * L-1 * *", "2010-02-03_15:12:42", "2010-02-27_00:00:00");
check_fn(cron_next, "* * * L-1 * *", "2010-03-06_15:12:42", "2010-03-30_00:00:00");
check_fn(cron_next, "* * * L-1 * *", "2010-04-09_15:12:42", "2010-04-29_00:00:00");
check_fn(cron_next, "* * * L-1 * *", "2010-05-12_15:12:42", "2010-05-30_00:00:00");
check_fn(cron_next, "* * * L-1 * *", "2010-06-15_15:12:42", "2010-06-29_00:00:00");
check_fn(cron_next, "* * * L-1 * *", "2010-07-18_15:12:42", "2010-07-30_00:00:00");
check_fn(cron_next, "* * * L-1 * *", "2010-08-21_15:12:42", "2010-08-30_00:00:00");
check_fn(cron_next, "* * * L-1 * *", "2010-09-24_15:12:42", "2010-09-29_00:00:00");
check_fn(cron_next, "* * * L-1 * *", "2010-10-27_15:12:42", "2010-10-30_00:00:00");
check_fn(cron_next, "* * * L-1 * *", "2010-11-30_15:12:42", "2010-12-30_00:00:00");
check_fn(cron_next, "* * * L-1 * *", "2010-12-31_15:12:42", "2011-01-30_00:00:00");
check_fn(cron_next, "* * * L * *", "2010-01-01_15:12:42", "2010-01-31_00:00:00");
check_fn(cron_next, "* * * L * *", "2010-02-03_15:12:42", "2010-02-28_00:00:00");
check_fn(cron_next, "* * * L * *", "2010-03-06_15:12:42", "2010-03-31_00:00:00");
check_fn(cron_next, "* * * L * *", "2010-04-09_15:12:42", "2010-04-30_00:00:00");
check_fn(cron_next, "* * * L * *", "2010-05-12_15:12:42", "2010-05-31_00:00:00");
check_fn(cron_next, "* * * L * *", "2010-06-15_15:12:42", "2010-06-30_00:00:00");
check_fn(cron_next, "* * * L * *", "2010-07-18_15:12:42", "2010-07-31_00:00:00");
check_fn(cron_next, "* * * L * *", "2010-08-21_15:12:42", "2010-08-31_00:00:00");
check_fn(cron_next, "* * * L * *", "2010-09-24_15:12:42", "2010-09-30_00:00:00");
check_fn(cron_next, "* * * L * *", "2010-10-27_15:12:42", "2010-10-31_00:00:00");
check_fn(cron_next, "* * * L * *", "2010-11-30_15:12:42", "2010-11-30_15:12:43");
check_fn(cron_next, "* * * L * *", "2010-12-31_15:12:42", "2010-12-31_15:12:43");
check_fn(cron_next, "* * * * * 1#-5", "2010-09-03_15:12:42", "2010-11-01_00:00:00");
check_fn(cron_next, "* * * * * 1#-4", "2010-09-03_15:12:42", "2010-09-06_00:00:00");
check_fn(cron_next, "* * * * * 1#-3", "2010-09-03_15:12:42", "2010-09-13_00:00:00");
check_fn(cron_next, "* * * * * 1#-2", "2010-09-03_15:12:42", "2010-09-20_00:00:00");
check_fn(cron_next, "* * * * * 1#1", "2010-09-03_15:12:42", "2010-09-06_00:00:00");
check_fn(cron_next, "* * * * * 1#2", "2010-09-03_15:12:42", "2010-09-13_00:00:00");
check_fn(cron_next, "* * * * * 1#3", "2010-09-03_15:12:42", "2010-09-20_00:00:00");
check_fn(cron_next, "* * * * * 1#4", "2010-09-03_15:12:42", "2010-09-27_00:00:00");
check_fn(cron_next, "* * * * * 1#5", "2010-09-03_15:12:42", "2010-11-29_00:00:00");
check_fn(cron_next, "* * * * * 2#-5", "2010-09-03_15:12:42", "2010-11-02_00:00:00");
check_fn(cron_next, "* * * * * 2#-4", "2010-09-03_15:12:42", "2010-09-07_00:00:00");
check_fn(cron_next, "* * * * * 2#-3", "2010-09-03_15:12:42", "2010-09-14_00:00:00");
check_fn(cron_next, "* * * * * 2#-2", "2010-09-03_15:12:42", "2010-09-21_00:00:00");
check_fn(cron_next, "* * * * * 2#1", "2010-09-03_15:12:42", "2010-09-07_00:00:00");
check_fn(cron_next, "* * * * * 2#2", "2010-09-03_15:12:42", "2010-09-14_00:00:00");
check_fn(cron_next, "* * * * * 2#3", "2010-09-03_15:12:42", "2010-09-21_00:00:00");
check_fn(cron_next, "* * * * * 2#4", "2010-09-03_15:12:42", "2010-09-28_00:00:00");
check_fn(cron_next, "* * * * * 2#5", "2010-09-03_15:12:42", "2010-11-30_00:00:00");
check_fn(cron_next, "* * * * * 3#-5", "2010-09-03_15:12:42", "2010-12-01_00:00:00");
check_fn(cron_next, "* * * * * 3#-4", "2010-09-03_15:12:42", "2010-09-08_00:00:00");
check_fn(cron_next, "* * * * * 3#-3", "2010-09-03_15:12:42", "2010-09-15_00:00:00");
check_fn(cron_next, "* * * * * 3#-2", "2010-09-03_15:12:42", "2010-09-22_00:00:00");
check_fn(cron_next, "* * * * * 3#1", "2010-09-03_15:12:42", "2010-10-06_00:00:00");
check_fn(cron_next, "* * * * * 3#2", "2010-09-03_15:12:42", "2010-09-08_00:00:00");
check_fn(cron_next, "* * * * * 3#3", "2010-09-03_15:12:42", "2010-09-15_00:00:00");
check_fn(cron_next, "* * * * * 3#4", "2010-09-03_15:12:42", "2010-09-22_00:00:00");
check_fn(cron_next, "* * * * * 3#5", "2010-09-03_15:12:42", "2010-09-29_00:00:00");
check_fn(cron_next, "* * * * * 4#-5", "2010-09-03_15:12:42", "2010-12-02_00:00:00");
check_fn(cron_next, "* * * * * 4#-4", "2010-09-03_15:12:42", "2010-09-09_00:00:00");
check_fn(cron_next, "* * * * * 4#-3", "2010-09-03_15:12:42", "2010-09-16_00:00:00");
check_fn(cron_next, "* * * * * 4#-2", "2010-09-03_15:12:42", "2010-09-23_00:00:00");
check_fn(cron_next, "* * * * * 4#1", "2010-09-03_15:12:42", "2010-10-07_00:00:00");
check_fn(cron_next, "* * * * * 4#2", "2010-09-03_15:12:42", "2010-09-09_00:00:00");
check_fn(cron_next, "* * * * * 4#3", "2010-09-03_15:12:42", "2010-09-16_00:00:00");
check_fn(cron_next, "* * * * * 4#4", "2010-09-03_15:12:42", "2010-09-23_00:00:00");
check_fn(cron_next, "* * * * * 4#5", "2010-09-03_15:12:42", "2010-09-30_00:00:00");
check_fn(cron_next, "* * * * * 5#-5", "2010-09-03_15:12:42", "2010-10-01_00:00:00");
check_fn(cron_next, "* * * * * 5#-4", "2010-09-03_15:12:42", "2010-09-03_15:12:43");
check_fn(cron_next, "* * * * * 5#-3", "2010-09-03_15:12:42", "2010-09-10_00:00:00");
check_fn(cron_next, "* * * * * 5#-2", "2010-09-03_15:12:42", "2010-09-17_00:00:00");
check_fn(cron_next, "* * * * * 5#1", "2010-09-03_15:12:42", "2010-09-03_15:12:43");
check_fn(cron_next, "* * * * * 5#2", "2010-09-03_15:12:42", "2010-09-10_00:00:00");
check_fn(cron_next, "* * * * * 5#3", "2010-09-03_15:12:42", "2010-09-17_00:00:00");
check_fn(cron_next, "* * * * * 5#4", "2010-09-03_15:12:42", "2010-09-24_00:00:00");
check_fn(cron_next, "* * * * * 5#5", "2010-09-03_15:12:42", "2010-10-29_00:00:00");
check_fn(cron_next, "* * * * * 6#-5", "2010-09-03_15:12:42", "2010-10-02_00:00:00");
check_fn(cron_next, "* * * * * 6#-4", "2010-09-03_15:12:42", "2010-09-04_00:00:00");
check_fn(cron_next, "* * * * * 6#-3", "2010-09-03_15:12:42", "2010-09-11_00:00:00");
check_fn(cron_next, "* * * * * 6#-2", "2010-09-03_15:12:42", "2010-09-18_00:00:00");
check_fn(cron_next, "* * * * * 6#1", "2010-09-03_15:12:42", "2010-09-04_00:00:00");
check_fn(cron_next, "* * * * * 6#2", "2010-09-03_15:12:42", "2010-09-11_00:00:00");
check_fn(cron_next, "* * * * * 6#3", "2010-09-03_15:12:42", "2010-09-18_00:00:00");
check_fn(cron_next, "* * * * * 6#4", "2010-09-03_15:12:42", "2010-09-25_00:00:00");
check_fn(cron_next, "* * * * * 6#5", "2010-09-03_15:12:42", "2010-10-30_00:00:00");
check_fn(cron_next, "* * * * * 7#-5", "2010-09-03_15:12:42", "2010-10-03_00:00:00");
check_fn(cron_next, "* * * * * 7#-4", "2010-09-03_15:12:42", "2010-09-05_00:00:00");
check_fn(cron_next, "* * * * * 7#-3", "2010-09-03_15:12:42", "2010-09-12_00:00:00");
check_fn(cron_next, "* * * * * 7#-2", "2010-09-03_15:12:42", "2010-09-19_00:00:00");
check_fn(cron_next, "* * * * * 7#1", "2010-09-03_15:12:42", "2010-09-05_00:00:00");
check_fn(cron_next, "* * * * * 7#2", "2010-09-03_15:12:42", "2010-09-12_00:00:00");
check_fn(cron_next, "* * * * * 7#3", "2010-09-03_15:12:42", "2010-09-19_00:00:00");
check_fn(cron_next, "* * * * * 7#4", "2010-09-03_15:12:42", "2010-09-26_00:00:00");
check_fn(cron_next, "* * * * * 7#5", "2010-09-03_15:12:42", "2010-10-31_00:00:00");
check_fn(cron_next, "* * * * * 1L", "2010-09-30_15:12:42", "2010-10-25_00:00:00");
check_fn(cron_next, "* * * * * 2L", "2010-09-30_15:12:42", "2010-10-26_00:00:00");
check_fn(cron_next, "* * * * * 3L", "2010-09-30_15:12:42", "2010-10-27_00:00:00");
check_fn(cron_next, "* * * * * 4L", "2010-09-30_15:12:42", "2010-09-30_15:12:43");
check_fn(cron_next, "* * * * * 5L", "2010-09-30_15:12:42", "2010-10-29_00:00:00");
check_fn(cron_next, "* * * * * 6L", "2010-09-30_15:12:42", "2010-10-30_00:00:00");
check_fn(cron_next, "* * * * * 7L", "2010-09-30_15:12:42", "2010-10-31_00:00:00");
check_fn(cron_next, "* * * * * 1L", "2010-10-27_15:12:42", "2010-11-29_00:00:00");
check_fn(cron_next, "* * * * * 2L", "2010-10-27_15:12:42", "2010-11-30_00:00:00");
check_fn(cron_next, "* * * * * 3L", "2010-10-27_15:12:42", "2010-10-27_15:12:43");
check_fn(cron_next, "* * * * * 4L", "2010-10-27_15:12:42", "2010-10-28_00:00:00");
check_fn(cron_next, "* * * * * 5L", "2010-10-27_15:12:42", "2010-10-29_00:00:00");
check_fn(cron_next, "* * * * * 6L", "2010-10-27_15:12:42", "2010-10-30_00:00:00");
check_fn(cron_next, "* * * * * 7L", "2010-10-27_15:12:42", "2010-10-31_00:00:00");
check_fn(cron_next, "* * * * * 1L", "2010-10-30_15:12:42", "2010-11-29_00:00:00");
check_fn(cron_next, "* * * * * 2L", "2010-10-30_15:12:42", "2010-11-30_00:00:00");
check_fn(cron_next, "* * * * * 3L", "2010-10-30_15:12:42", "2010-11-24_00:00:00");
check_fn(cron_next, "* * * * * 4L", "2010-10-30_15:12:42", "2010-11-25_00:00:00");
check_fn(cron_next, "* * * * * 5L", "2010-10-30_15:12:42", "2010-11-26_00:00:00");
check_fn(cron_next, "* * * * * 6L", "2010-10-30_15:12:42", "2010-10-30_15:12:43");
check_fn(cron_next, "* * * * * 7L", "2010-10-30_15:12:42", "2010-10-31_00:00:00");
check_fn(cron_next, "* * * * * 1L", "2010-11-27_15:12:42", "2010-11-29_00:00:00");
check_fn(cron_next, "* * * * * 2L", "2010-11-27_15:12:42", "2010-11-30_00:00:00");
check_fn(cron_next, "* * * * * 3L", "2010-11-27_15:12:42", "2010-12-29_00:00:00");
check_fn(cron_next, "* * * * * 4L", "2010-11-27_15:12:42", "2010-12-30_00:00:00");
check_fn(cron_next, "* * * * * 5L", "2010-11-27_15:12:42", "2010-12-31_00:00:00");
check_fn(cron_next, "* * * * * 6L", "2010-11-27_15:12:42", "2010-11-27_15:12:43");
check_fn(cron_next, "* * * * * 7L", "2010-11-27_15:12:42", "2010-11-28_00:00:00");
check_fn(cron_next, "0 0 7 W * *", "2009-09-26_00:42:55", "2009-09-28_07:00:00");
check_fn(cron_next, "0 0 7 W * *", "2009-09-28_07:00:00", "2009-09-29_07:00:00");
check_fn(cron_next, "* * * * * L", "2010-10-25_15:12:42", "2010-10-31_00:00:00");
check_fn(cron_next, "* * * * * L", "2010-10-20_15:12:42", "2010-10-24_00:00:00");
check_fn(cron_next, "* * * * * L", "2010-10-27_15:12:42", "2010-10-31_00:00:00");
check_fn(cron_next, "* 15 11 * * *", "2019-03-09_11:43:00", "2019-03-10_11:15:00");
check_fn(cron_next, "*/15 * 1-4 * * *", "2012-07-01_09:53:50", "2012-07-02_01:00:00");
check_fn(cron_next, "*/15 * 1-4 * * *", "2012-07-01_09:53:00", "2012-07-02_01:00:00");
check_fn(cron_next, "0,15,30,45 * 1,2,3,4 * * *", "2012-07-01_09:53:00", "2012-07-02_01:00:00");
check_fn(cron_next, "0 */2 1-4 * * *", "2012-07-01_09:00:00", "2012-07-02_01:00:00");
check_fn(cron_next, "0 */2 * * * *", "2012-07-01_09:00:00", "2012-07-01_09:02:00");
check_fn(cron_next, "0 */2 * * * *", "2013-07-01_09:00:00", "2013-07-01_09:02:00");
check_fn(cron_next, "0 */2 * * * *", "2018-09-14_14:24:00", "2018-09-14_14:26:00");
check_fn(cron_next, "0 */2 * * * *", "2018-09-14_14:25:00", "2018-09-14_14:26:00");
check_fn(cron_next, "0 */20 * * * *", "2018-09-14_14:24:00", "2018-09-14_14:40:00");
check_fn(cron_next, "* * * * * *", "2012-07-01_09:00:00", "2012-07-01_09:00:01");
check_fn(cron_next, "* * * * * *", "2012-12-01_09:00:58", "2012-12-01_09:00:59");
check_fn(cron_next, "10 * * * * *", "2012-12-01_09:42:09", "2012-12-01_09:42:10");
check_fn(cron_next, "11 * * * * *", "2012-12-01_09:42:10", "2012-12-01_09:42:11");
check_fn(cron_next, "10 * * * * *", "2012-12-01_09:42:10", "2012-12-01_09:43:10");
check_fn(cron_next, "10-15 * * * * *", "2012-12-01_09:42:09", "2012-12-01_09:42:10");
check_fn(cron_next, "10-15 * * * * *", "2012-12-01_21:42:14", "2012-12-01_21:42:15");
check_fn(cron_next, "0 * * * * *", "2012-12-01_21:10:42", "2012-12-01_21:11:00");
check_fn(cron_next, "0 * * * * *", "2012-12-01_21:11:00", "2012-12-01_21:12:00");
check_fn(cron_next, "0 11 * * * *", "2012-12-01_21:10:42", "2012-12-01_21:11:00");
check_fn(cron_next, "0 10 * * * *", "2012-12-01_21:11:00", "2012-12-01_22:10:00");
check_fn(cron_next, "0 0 * * * *", "2012-09-30_11:01:00", "2012-09-30_12:00:00");
check_fn(cron_next, "0 0 * * * *", "2012-09-30_12:00:00", "2012-09-30_13:00:00");
check_fn(cron_next, "0 0 * * * *", "2012-09-10_23:01:00", "2012-09-11_00:00:00");
check_fn(cron_next, "0 0 * * * *", "2012-09-11_00:00:00", "2012-09-11_01:00:00");
check_fn(cron_next, "0 0 0 * * *", "2012-09-01_14:42:43", "2012-09-02_00:00:00");
check_fn(cron_next, "0 0 0 * * *", "2012-09-02_00:00:00", "2012-09-03_00:00:00");
check_fn(cron_next, "* * * 10 * *", "2012-10-09_15:12:42", "2012-10-10_00:00:00");
check_fn(cron_next, "* * * 10 * *", "2012-10-11_15:12:42", "2012-11-10_00:00:00");
check_fn(cron_next, "0 0 0 * * *", "2012-09-30_15:12:42", "2012-10-01_00:00:00");
check_fn(cron_next, "0 0 0 * * *", "2012-10-01_00:00:00", "2012-10-02_00:00:00");
check_fn(cron_next, "0 0 0 * * *", "2012-08-30_15:12:42", "2012-08-31_00:00:00");
check_fn(cron_next, "0 0 0 * * *", "2012-08-31_00:00:00", "2012-09-01_00:00:00");
check_fn(cron_next, "0 0 0 * * *", "2012-10-30_15:12:42", "2012-10-31_00:00:00");
check_fn(cron_next, "0 0 0 * * *", "2012-10-31_00:00:00", "2012-11-01_00:00:00");
check_fn(cron_next, "0 0 0 1 * *", "2012-10-30_15:12:42", "2012-11-01_00:00:00");
check_fn(cron_next, "0 0 0 1 * *", "2012-11-01_00:00:00", "2012-12-01_00:00:00");
check_fn(cron_next, "0 0 0 1 * *", "2010-12-31_15:12:42", "2011-01-01_00:00:00");
check_fn(cron_next, "0 0 0 1 * *", "2011-01-01_00:00:00", "2011-02-01_00:00:00");
check_fn(cron_next, "0 0 0 31 * *", "2011-10-30_15:12:42", "2011-10-31_00:00:00");
check_fn(cron_next, "0 0 0 1 * *", "2011-10-30_15:12:42", "2011-11-01_00:00:00");
check_fn(cron_next, "* * * * * 2", "2010-10-25_15:12:42", "2010-10-26_00:00:00");
check_fn(cron_next, "* * * * * 2", "2010-10-20_15:12:42", "2010-10-26_00:00:00");
check_fn(cron_next, "* * * * * 2", "2010-10-27_15:12:42", "2010-11-02_00:00:00");
check_fn(cron_next, "55 5 * * * *", "2010-10-27_15:04:54", "2010-10-27_15:05:55");
check_fn(cron_next, "55 5 * * * *", "2010-10-27_15:05:55", "2010-10-27_16:05:55");
check_fn(cron_next, "55 * 10 * * *", "2010-10-27_09:04:54", "2010-10-27_10:00:55");
check_fn(cron_next, "55 * 10 * * *", "2010-10-27_10:00:55", "2010-10-27_10:01:55");
check_fn(cron_next, "* 5 10 * * *", "2010-10-27_09:04:55", "2010-10-27_10:05:00");
check_fn(cron_next, "* 5 10 * * *", "2010-10-27_10:05:00", "2010-10-27_10:05:01");
check_fn(cron_next, "55 * * 3 * *", "2010-10-02_10:05:54", "2010-10-03_00:00:55");
check_fn(cron_next, "55 * * 3 * *", "2010-10-03_00:00:55", "2010-10-03_00:01:55");
check_fn(cron_next, "* * * 3 11 *", "2010-10-02_14:42:55", "2010-11-03_00:00:00");
check_fn(cron_next, "* * * 3 11 *", "2010-11-03_00:00:00", "2010-11-03_00:00:01");
check_fn(cron_next, "0 0 0 29 2 *", "2007-02-10_14:42:55", "2008-02-29_00:00:00");
check_fn(cron_next, "0 0 0 29 2 *", "2008-02-29_00:00:00", "2012-02-29_00:00:00");
check_fn(cron_next, "0 0 7 ? * MON-FRI", "2009-09-26_00:42:55", "2009-09-28_07:00:00");
check_fn(cron_next, "0 0 7 ? * MON-FRI", "2009-09-28_07:00:00", "2009-09-29_07:00:00");
check_fn(cron_next, "0 30 23 30 1/3 ?", "2010-12-30_00:00:00", "2011-01-30_23:30:00");
check_fn(cron_next, "0 30 23 30 1/3 ?", "2011-01-30_23:30:00", "2011-04-30_23:30:00");
check_fn(cron_next, "0 30 23 30 1/3 ?", "2011-04-30_23:30:00", "2011-07-30_23:30:00");
check_fn(cron_next, "* * * * * *", "2020-12-31_23:59:59", "2021-01-01_00:00:00");
check_fn(cron_next, "0 0 * * * *", "2020-02-28_23:00:00", "2020-02-29_00:00:00");
check_fn(cron_next, "0 0 0 * * *", "2020-02-29_01:02:03", "2020-03-01_00:00:00");
check_fn(cron_prev, "* 15 11 * * *", "2019-03-09_11:43:00", "2019-03-09_11:15:59");
check_fn(cron_prev, "*/15 * 1-4 * * *", "2012-07-01_09:53:50", "2012-07-01_04:59:45");
check_fn(cron_prev, "*/15 * 1-4 * * *", "2012-07-01_01:00:14", "2012-07-01_01:00:00");
check_fn(cron_prev, "*/15 * 1-4 * * *", "2012-07-01_01:00:00", "2012-06-30_04:59:45");
check_fn(cron_prev, "* * * * * *", "2012-07-01_09:00:00", "2012-07-01_08:59:59");
check_fn(cron_prev, "* * * * * *", "2021-01-01_00:00:00", "2020-12-31_23:59:59");
check_fn(cron_prev, "0 0 * * * *", "2020-02-29_00:00:00", "2020-02-28_23:00:00");
check_fn(cron_prev, "0 0 0 * * *", "2020-03-01_00:00:00", "2020-02-29_00:00:00");
check_fn(cron_prev, "0 0 * * * *", "2020-03-01_00:00:00", "2020-02-29_23:00:00");
check_fn(cron_next, "0 0 0 ? 11-12 *", "2022-05-31_00:00:00", "2022-11-01_00:00:00");
check_fn(cron_next, "0 0 0 ? 11-12 *", "2022-07-31_00:00:00", "2022-11-01_00:00:00");
check_fn(cron_next, "0 0 0 ? 11-12 *", "2022-08-31_00:00:00", "2022-11-01_00:00:00");
check_fn(cron_next, "0 0 0 ? 11-12 *", "2022-10-31_00:00:00", "2022-11-01_00:00:00");
check_fn(cron_next, "0 0 0 ? 6-7 *", "2022-05-31_00:00:00", "2022-06-01_00:00:00");
check_fn(cron_next, "0 0 0 ? 8-9 *", "2022-07-31_00:00:00", "2022-08-01_00:00:00");
check_fn(cron_next, "0 0 0 ? 9-10 *", "2022-08-31_00:00:00", "2022-09-01_00:00:00");
check_fn(cron_next, "0 0 0 ? 2-3 *", "2022-01-31_00:00:00", "2022-02-01_00:00:00");
check_fn(cron_next, "0 0 0 ? 4-5 *", "2022-03-31_00:00:00", "2022-04-01_00:00:00");
check_fn(cron_next, "* * * 29 2 *", "2021-12-07_12:00:00", "2024-02-29_00:00:00");
check_fn(cron_prev, "* * * 29 2 *", "2021-12-07_12:00:00", "2020-02-29_23:59:59");
check_fn(cron_prev, "* * * 1 2 *", "2023-11-01_00:00:00", "2023-02-01_23:59:59");
check_fn(cron_prev, "* * * 2 2 *", "2023-11-01_00:00:00", "2023-02-02_23:59:59");
check_fn(cron_prev, "* * * 3 2 *", "2023-11-01_00:00:00", "2023-02-03_23:59:59");
check_fn(cron_prev, "* * * 4 2 *", "2023-11-01_00:00:00", "2023-02-04_23:59:59");
check_fn(cron_prev, "* * * 1 4 *", "2023-10-01_00:00:00", "2023-04-01_23:59:59");
check_fn(cron_prev, "* * * 2 4 *", "2023-10-01_00:00:00", "2023-04-02_23:59:59");
check_fn(cron_prev, "* * * 3 4 *", "2023-10-01_00:00:00", "2023-04-03_23:59:59");
check_fn(cron_prev, "* * * 4 4 *", "2023-10-01_00:00:00", "2023-04-04_23:59:59");
check_fn(cron_prev, "0 0 20 1 2 *", "2022-12-30_08:10:23", "2022-02-01_20:00:00");
check_fn(cron_prev, "0 0 20 2 2 *", "2022-12-30_08:10:23", "2022-02-02_20:00:00");
check_fn(cron_prev, "0 0 20 3 2 *", "2022-12-30_08:10:23", "2022-02-03_20:00:00");
check_fn(cron_prev, "0 0 20 1 2 *", "2022-12-31_08:10:23", "2022-02-01_20:00:00");
check_fn(cron_prev, "0 0 20 2 2 *", "2022-12-31_08:10:23", "2022-02-02_20:00:00");
check_fn(cron_prev, "0 0 20 3 2 *", "2022-12-31_08:10:23", "2022-02-03_20:00:00");
check_fn(cron_prev, "0 0 20 1 2 *", "2023-01-01_08:10:23", "2022-02-01_20:00:00");
check_fn(cron_prev, "0 0 20 2 2 *", "2023-01-01_08:10:23", "2022-02-02_20:00:00");
check_fn(cron_prev, "0 0 20 3 2 *", "2023-01-01_08:10:23", "2022-02-03_20:00:00");
check_fn(cron_prev, "0 0 17 * 2 2-4", "2023-08-31_18:00:00", "2023-02-28_17:00:00");
check_fn(cron_prev, "0 0 17 * 2 2-4", "2023-09-01_18:00:00", "2023-02-28_17:00:00");
check_fn(cron_prev, "0 0 17 * 2 2-4", "2023-09-02_18:00:00", "2023-02-28_17:00:00");
check_fn(cron_prev, "0 0 17 * 2 2-4", "2023-09-03_18:00:00", "2023-02-28_17:00:00");
check_fn(cron_prev, "0 0 17 * 2 2-4", "2023-09-04_18:00:00", "2023-02-28_17:00:00");
check_fn(cron_prev, "0 0 17 * 2 2-4", "2023-09-05_18:00:00", "2023-02-28_17:00:00");
check_fn(cron_prev, "0 0 17 * 3 1-5", "2023-03-02_17:00:00", "2023-03-01_17:00:00");
check_fn(cron_prev, "0 0 17 * 3 1-5", "2023-03-02_17:00:01", "2023-03-02_17:00:00");
check_fn(cron_prev, "0 0 17 * 3 1-5", "2023-03-02_18:00:00", "2023-03-02_17:00:00");
check_fn(cron_prev, "0 0 17 * 3 1-5", "2023-03-03_18:00:00", "2023-03-03_17:00:00");
check_fn(cron_prev, "0 0 17 * 3 1-5", "2023-03-04_18:00:00", "2023-03-03_17:00:00");
check_fn(cron_prev, "0 0 17 * 3 1-5", "2023-03-05_18:00:00", "2023-03-03_17:00:00");
check_fn(cron_prev, "0 0 17 * 3 1-5", "2023-03-06_18:00:00", "2023-03-06_17:00:00");
check_fn(cron_prev, "0 30 9 * 4 6", "2024-04-05_18:00:00", "2023-04-29_09:30:00");
check_fn(cron_prev, "0 30 9 * 4 6", "2024-04-06_09:29:59", "2023-04-29_09:30:00");
check_fn(cron_prev, "0 30 9 * 4 6", "2024-04-06_09:30:00", "2023-04-29_09:30:00");
check_fn(cron_prev, "0 30 9 * 4 6", "2024-04-06_09:30:01", "2024-04-06_09:30:00");
check_fn(cron_prev, "0 30 9 * 4 6", "2024-04-06_18:00:00", "2024-04-06_09:30:00");
check_fn(cron_prev, "0 30 9 * 4 6", "2024-04-07_18:00:00", "2024-04-06_09:30:00");
check_fn(cron_prev, "0 30 9 * 4 6", "2024-04-26_18:00:00", "2024-04-20_09:30:00");
check_fn(cron_prev, "0 30 9 * 4 6", "2024-04-27_18:00:00", "2024-04-27_09:30:00");
check_fn(cron_prev, "0 30 9 * 4 6", "2024-04-28_18:00:00", "2024-04-27_09:30:00");
check_fn(cron_prev, "0 30 9 * 4 6", "2024-05-01_18:00:00", "2024-04-27_09:30:00");
check_fn(cron_prev, "0 30 11 * * 6", "2020-02-27_10:00:00", "2020-02-22_11:30:00");
check_fn(cron_prev, "0 30 11 * * 6", "2020-02-28_10:00:00", "2020-02-22_11:30:00");
check_fn(cron_prev, "0 30 11 * * 6", "2020-02-29_10:00:00", "2020-02-22_11:30:00");
check_fn(cron_prev, "0 30 11 * * 6", "2020-02-29_11:31:00", "2020-02-29_11:30:00");
check_fn(cron_prev, "0 30 11 * * 6", "2020-03-01_10:00:00", "2020-02-29_11:30:00");
check_fn(cron_prev, "0 30 11 * * 6", "2020-03-01_12:00:00", "2020-02-29_11:30:00");
check_fn(cron_prev, "0 0 10 * * *", "2020-02-29_09:59:59", "2020-02-28_10:00:00");
check_fn(cron_prev, "0 0 10 * * *", "2020-02-29_10:00:00", "2020-02-28_10:00:00");
check_fn(cron_prev, "0 0 10 * * *", "2020-02-29_10:00:01", "2020-02-29_10:00:00");
check_fn(cron_prev, "0 0 10 * * *", "2022-02-28_09:59:59", "2022-02-27_10:00:00");
check_fn(cron_prev, "0 0 10 * * *", "2022-02-28_10:00:00", "2022-02-27_10:00:00");
check_fn(cron_prev, "0 0 10 * * *", "2022-02-28_10:00:01", "2022-02-28_10:00:00");
check_fn(cron_prev, "0 0 10 * * *", "2022-12-31_09:59:59", "2022-12-30_10:00:00");
check_fn(cron_prev, "0 0 10 * * *", "2022-12-31_10:00:00", "2022-12-30_10:00:00");
check_fn(cron_prev, "0 0 10 * * *", "2022-12-31_10:00:01", "2022-12-31_10:00:00");
check_fn(cron_prev, "0 0 10 * * *", "2022-12-31_23:59:59", "2022-12-31_10:00:00");
check_fn(cron_prev, "0 0 10 * * *", "2023-01-01_00:00:00", "2022-12-31_10:00:00");
check_fn(cron_prev, "0 0 10 * * *", "2023-01-01_00:00:01", "2022-12-31_10:00:00");
check_fn(cron_prev, "0 0 10 * * *", "2023-01-01_09:59:59", "2022-12-31_10:00:00");
check_fn(cron_prev, "0 0 10 * * *", "2023-01-01_10:00:00", "2022-12-31_10:00:00");
check_fn(cron_prev, "0 0 10 * * *", "2023-01-01_10:00:01", "2023-01-01_10:00:00");
check_fn(cron_prev, "30 50 23 20,21,22 * *", "2023-07-01_12:34:56", "2023-06-22_23:50:30");
check_fn(cron_prev, "30 50 23 20,21,22 * *", "2023-07-19_12:34:56", "2023-06-22_23:50:30");
check_fn(cron_prev, "30 50 23 20,21,22 * *", "2023-07-20_12:34:56", "2023-06-22_23:50:30");
check_fn(cron_prev, "30 50 23 20,21,22 * *", "2023-07-21_12:34:56", "2023-07-20_23:50:30");
check_fn(cron_prev, "30 50 23 20,21,22 * *", "2023-07-22_12:34:56", "2023-07-21_23:50:30");
check_fn(cron_prev, "30 50 23 20,21,22 * *", "2023-07-23_12:34:56", "2023-07-22_23:50:30");
}
void test_parse() {
check_same("* * * W * *", "* * * * * 1-5");
check_same("* * * * * L", "* * * * * 0");
check_same("* * * * * 6#-1", "* * * * * 6L");
check_same("0 0 0 * * *", "0 0 * * *");
check_same("0 0 0 * * *", "0 0 0 * * * *");
check_same("* * * * * *", "* * * * * * *");
check_same("@annually", "0 0 0 1 1 * *");
check_same("@yearly", "0 0 0 1 1 * *");
check_same("@monthly", "0 0 0 1 * * *");
check_same("@weekly", "0 0 0 * * 0 *");
check_same("@daily", "0 0 0 * * * *");
check_same("@midnight", "0 0 0 * * * *");
check_same("@hourly", "0 0 * * * * *");
check_same("@minutely", "0 * * * * * *");
check_same("@secondly", "* * * * * * *");
check_same("* * * 2 * *", "* * * 2 * ?");
check_same("* * * 2 * *", "* * * 2 * ?");
check_same("57,59 * * * * *", "57/2 * * * * *");
check_same("L57,59,61 * * * * *", "L57/2 * * * * *");
check_same("1,3,5 * * * * *", "1-6/2 * * * * *");
check_same("* * 4,8,12,16,20 * * *", "* * 4/4 * * *");
check_same("* * * * * 0-6", "* * * * * TUE,WED,THU,FRI,SAT,SUN,MON");
check_same("* * * * * 0", "* * * * * SUN");
check_same("* * * * * 0", "* * * * * 7");
check_same("* * * * 1-12 *", "* * * * FEB,JAN,MAR,APR,MAY,JUN,JUL,AUG,SEP,OCT,NOV,DEC *");
check_same("* * * * 2 *", "* * * * Feb *");
check_same("* * * * 1 *", "* * * * 1 *");
check_expr_invalid("Z * * * * *");
check_expr_invalid("* * * * * 1#-6");
check_expr_invalid("* * * * * 1#6");
check_expr_invalid("77 * * * * *");
check_expr_invalid("44-77 * * * * *");
check_expr_invalid("* 77 * * * *");
check_expr_invalid("* 44-77 * * * *");
check_expr_invalid("* * 27 * * *");
check_expr_invalid("* * 23-28 * * *");
check_expr_invalid("* * * 45 * *");
check_expr_invalid("* * * 28-45 * *");
check_expr_invalid("0 0 0 25 13 ?");
check_expr_invalid("0 0 0 25 0 ?");
check_expr_invalid("0 0 0 32 12 ?");
check_expr_invalid("* * * * 11-13 *");
check_expr_invalid("-5 * * * * *");
check_expr_invalid("3-2 */5 * * * *");
check_expr_invalid("/5 * * * * *");
check_expr_invalid("*/0 * * * * *");
check_expr_invalid("*/-0 * * * * *");
check_expr_invalid("* 1 1 0 * *");
/* Source: https://www.freeformatter.com/cron-expression-generator-quartz.html */
check_expr_valid("* * * ? * *"); /* Every second */
check_expr_valid("0 * * ? * *"); /* Every minute */
check_expr_valid("0 */2 * ? * *"); /* Every even minute */
check_expr_valid("0 1/2 * ? * *"); /* Every uneven minute */
check_expr_valid("0 */2 * ? * *"); /* Every 2 minutes */
check_expr_valid("0 */3 * ? * *"); /* Every 3 minutes */
check_expr_valid("0 */4 * ? * *"); /* Every 4 minutes */
check_expr_valid("0 */5 * ? * *"); /* Every 5 minutes */
check_expr_valid("0 */10 * ? * *"); /* Every 10 minutes */
check_expr_valid("0 */15 * ? * *"); /* Every 15 minutes */
check_expr_valid("0 */30 * ? * *"); /* Every 30 minutes */
check_expr_valid("0 15,30,45 * ? * *"); /* Every hour at minutes 15, 30 and 45 */
check_expr_valid("0 0 * ? * *"); /* Every hour */
check_expr_valid("0 0 */2 ? * *"); /* Every hour */
check_expr_valid("0 0 0/2 ? * *"); /* Every even hour */
check_expr_valid("0 0 1/2 ? * *"); /* Every uneven hour */
check_expr_valid("0 0 */3 ? * *"); /* Every three hours */
check_expr_valid("0 0 */4 ? * *"); /* Every four hours */
check_expr_valid("0 0 */6 ? * *"); /* Every six hours */
check_expr_valid("0 0 */8 ? * *"); /* Every eight hours */
check_expr_valid("0 0 */12 ? * *"); /* Every twelve hours */
check_expr_valid("0 0 0 * * ?"); /* Every day at midnight - 12am */
check_expr_valid("0 0 1 * * ?"); /* Every day at 1am */
check_expr_valid("0 0 6 * * ?"); /* Every day at 6am */
check_expr_valid("0 0 12 * * ?"); /* Every day at noon - 12pm */
check_expr_valid("0 0 12 * * ?"); /* Every day at noon - 12pm */
check_expr_valid("0 0 12 ? * SUN"); /* Every Sunday at noon */
check_expr_valid("0 0 12 ? * MON"); /* Every Monday at noon */
check_expr_valid("0 0 12 ? * TUE"); /* Every Tuesday at noon */
check_expr_valid("0 0 12 ? * WED"); /* Every Wednesday at noon */
check_expr_valid("0 0 12 ? * THU"); /* Every Thursday at noon */
check_expr_valid("0 0 12 ? * FRI"); /* Every Friday at noon */
check_expr_valid("0 0 12 ? * SAT"); /* Every Saturday at noon */
check_expr_valid("0 0 12 ? * MON-FRI"); /* Every Weekday at noon */
check_expr_valid("0 0 12 ? * SUN,SAT"); /* Every Saturday and Sunday at noon */
check_expr_valid("0 0 12 */7 * ?"); /* Every 7 days at noon */
check_expr_valid("0 0 12 1 * ?"); /* Every month on the 1st, at noon */
check_expr_valid("0 0 12 2 * ?"); /* Every month on the 2nd, at noon */
check_expr_valid("0 0 12 15 * ?"); /* Every month on the 15th, at noon */
check_expr_valid("0 0 12 1/2 * ?"); /* Every 2 days starting on the 1st of the month, at noon */
check_expr_valid("0 0 12 1/4 * ?"); /* Every 4 days staring on the 1st of the month, at noon */
check_expr_valid("0 0 12 L * ?"); /* Every month on the last day of the month, at noon */
check_expr_valid("0 0 12 L-2 * ?"); /* Every month on the second to last day of the month, at noon */
check_expr_valid("0 0 12 LW * ?"); /* Every month on the last weekday, at noon */
/*check_expr_valid("0 0 12 1L * ?"); */ /* Every month on the last Sunday, at noon */
/*check_expr_valid("0 0 12 2L * ?"); */ /* Every month on the last Monday, at noon */
/*check_expr_valid("0 0 12 6L * ?"); */ /* Every month on the last Friday, at noon */
check_expr_valid("0 0 12 ? * 1L"); /* Every month on the last Sunday, at noon */
check_expr_valid("0 0 12 ? * 2L"); /* Every month on the last Monday, at noon */
check_expr_valid("0 0 12 ? * 6L"); /* Every month on the last Friday, at noon */
check_expr_valid("0 0 12 1W * ?"); /* Every month on the nearest Weekday to the 1st of the month, at noon */
check_expr_valid("0 0 12 15W * ?"); /* Every month on the nearest Weekday to the 15th of the month, at noon */
check_expr_valid("0 0 12 ? * 2#1"); /* Every month on the first Monday of the Month, at noon */
check_expr_valid("0 0 12 ? * 6#1"); /* Every month on the first Friday of the Month, at noon */
check_expr_valid("0 0 12 ? * 2#2"); /* Every month on the second Monday of the Month, at noon */
check_expr_valid("0 0 12 ? * 5#3"); /* Every month on the third Thursday of the Month, at noon - 12pm */
check_expr_valid("0 0 12 ? JAN *"); /* Every day at noon in January only */
check_expr_valid("0 0 12 ? JUN *"); /* Every day at noon in June only */
check_expr_valid("0 0 12 ? JAN,JUN *"); /* Every day at noon in January and June */
check_expr_valid("0 0 12 ? DEC *"); /* Every day at noon in December only */
check_expr_valid("0 0 12 ? JAN,FEB,MAR,APR *"); /* Every day at noon in January, February, March and April */
check_expr_valid("0 0 12 ? 9-12 *"); /* Every day at noon between September and December */
/* ChatGPT generated inputs for further testing. */
check_expr_valid("0 0 12 * * ?"); /* Every day at 12 PM (noon). */
check_expr_valid("0 15 10 ? * *"); /* Every day at 10:15 AM. */
check_expr_valid("0 15 10 * * ?"); /* Every day at 10:15 AM. */
check_expr_valid("0 15 10 * * ? *"); /* Every day at 10:15 AM. */
check_expr_valid("0 15 10 * * ? 2023"); /* Every day at 10:15 AM during the year 2023. */
check_expr_valid("0 * 14 * * ?"); /* Every minute starting at 2 PM and ending at 2:59 PM, every day. */
check_expr_valid("0 0/5 14 * * ?"); /* Every 5 minutes starting at 2 PM and ending at 2:55 PM, every day. */
check_expr_valid("0 0/5 14,18 * * ?"); /* Every 5 minutes starting at 2 PM and ending at 2:55 PM, AND every 5 minutes starting at 6 PM and ending at 6:55 PM, every day. */
check_expr_valid("0 0-5 14 * * ?"); /* Every minute starting at 2 PM and ending at 2:05 PM, every day. */
check_expr_valid("0 10,44 14 ? 3 WED"); /* Every Wednesday in March at 2:10 PM and 2:44 PM. */
check_expr_valid("0 15 10 ? * MON-FRI"); /* Every weekday at 10:15 AM. */
check_expr_valid("0 15 10 15 * ?"); /* Every 15th day of the month at 10:15 AM. */
check_expr_valid("0 15 10 L * ?"); /* Last day of every month at 10:15 AM. */
check_expr_valid("0 15 10 ? * 6L"); /* Last Friday of every month at 10:15 AM. */
check_expr_valid("0 15 10 ? * 6L 2022-2025"); /* Last Friday of every month during the years 2022 through 2025 at 10:15 AM. */
check_expr_valid("0 15 10 ? * 6#3"); /* Third Friday of every month at 10:15 AM. */
check_expr_valid("0 15 10 ? * 2-6"); /* Every weekday (Monday to Friday) at 10:15 AM. */
check_expr_valid("0 0/5 14-18 * * ?"); /* Every 5 minutes from 2 PM to 6:55 PM every day. */
check_expr_valid("0 0 12 1/5 * ?"); /* Every 5 days at 12 PM. */
check_expr_valid("0 11 11 11 11 ?"); /* Every November 11th at 11:11 AM. */
check_expr_valid("0 0 12 ? * SUN"); /* Every Sunday at noon. */
check_expr_valid("0 0 12 ? * MON"); /* Every Monday at noon. */
check_expr_valid("0 0 12 ? * TUE"); /* Every Tuesday at noon. */
check_expr_valid("0 0 12 ? * WED"); /* Every Wednesday at noon. */
check_expr_valid("0 0 12 ? * THU"); /* Every Thursday at noon. */
check_expr_valid("0 0 12 ? * FRI"); /* Every Friday at noon. */
check_expr_valid("0 0 12 ? * SAT"); /* Every Saturday at noon. */
check_expr_valid("0 0/30 8-9 1 * ?"); /* Every 30 minutes between 8-9 AM on the 1st of the month. */
check_expr_valid("0 0 0 1 1 ?"); /* Every New Year's Day at midnight. */
check_expr_valid("0 0 0 25 12 ?"); /* Every Christmas at midnight. */
check_expr_valid("0 0 6,18 * * ?"); /* Every day at 6 AM and 6 PM. */
check_expr_valid("0 0 8-10 ? * 2-6"); /* Every weekday between 8-10 AM. */
check_expr_valid("0 0/15 * ? * *"); /* Every 15 minutes. */
check_expr_valid("0 0 12 LW * ?"); /* Last weekday of the month at noon. */
check_expr_valid("0 0 12 1W * ?"); /* Nearest weekday to the 1st of the month at noon. */
check_expr_valid("0 0 12 W * ?"); /* Every weekday at noon. */
check_expr_valid("0 0 12 ? JAN,DEC *"); /* Every day at noon in January and December. */
check_expr_valid("0 0 12 ? * 1#2"); /* Second Sunday of every month at noon. */
check_expr_valid("0 0 12 ? * 2#2"); /* Second Monday of every month at noon. */
check_expr_valid("0 0 12 ? * 6#1"); /* First Friday of every month at noon. */
check_expr_valid("0 0 8,20 ? * *"); /* Every day at 8 AM and 8 PM. */
check_expr_valid("0 30 10-13 ? * WED,FRI"); /* Every Wednesday and Friday between 10:30 AM and 1:30 PM. */
check_expr_valid("0 0/10 * * * ?"); /* Every 10 minutes. */
check_expr_valid("0 0 12 L-2 * ?"); /* Two days before the end of the month at noon. */
check_expr_valid("0 0 12 15W * ?"); /* Nearest weekday to the 15th of every month at noon. */
check_expr_valid("0 0 0 * * ?"); /* Every day at midnight (beginning of the day). */
check_expr_valid("0 0 23 * * ?"); /* Every day at 11 PM (end of the day). */
check_expr_valid("0 30 10 ? * 2-6"); /* Every weekday at 10:30 AM. */
check_expr_valid("0 0/10 8-17 ? * MON-FRI"); /* Every 10 minutes during working hours (8 AM - 5 PM) on weekdays. */
/*check_expr_valid("0 0 12 1L * ?"); */ /* Last day of the month at noon. */
check_expr_valid("0 0 12 ? * SUN,MON"); /* Every Sunday and Monday at noon. */
check_expr_valid("0 0/5 9-16 * * ?"); /* Every 5 minutes from 9 AM to 4:55 PM every day. */
check_expr_valid("0 0 12 10-15 * ?"); /* Every day from the 10th to the 15th at noon. */
check_expr_valid("0 0 0 1 JAN-JUN ?"); /* First day of the month from January to June at midnight. */
check_expr_valid("0 0 0 ? * 2L"); /* Last Monday of the month at midnight. */
check_expr_valid("0 0 0 ? * 6#4"); /* Fourth Friday of the month at midnight. */
check_expr_valid("0 0 12 1/2 * ?"); /* Every other day at noon. */
check_expr_valid("0 0 12 ? * 2/2"); /* Every other Monday at noon. */
check_expr_valid("0 0 0 29 FEB ?"); /* Every 29th of February at midnight (Leap Year). */
check_expr_valid("0 0 12 1,15 * ?"); /* 1st and 15th of the month at noon. */
check_expr_valid("0 0 0 1 * ? 2024"); /* 1st of every month in 2024 at midnight. */
check_expr_valid("0 30 6 ? * 1-5"); /* Weekdays at 6:30 AM. */
check_expr_valid("0 0 0/2 * * ?"); /* Every 2 hours. */
check_expr_valid("0 0 12/3 ? * *"); /* Every 3 hours starting at noon. */
check_expr_valid("0 15,45 * ? * *"); /* Every hour at 15 and 45 minutes past. */
check_expr_valid("0 0 0 ? * SAT,SUN"); /* Every weekend at midnight. */
check_expr_valid("0 0 8-10,14-16 * * ?"); /* Every day from 8-10 AM and 2-4 PM. */
check_expr_valid("0 0 12 ? 1-6,9-12 *"); /* Every day at noon excluding July and August. */
check_expr_valid("0 0 12/4 * * ?"); /* Every 4 hours starting at noon. */
check_expr_valid("0 0 9-17 * * ?"); /* Every hour from 9 AM to 5 PM. */
check_expr_valid("0 0/20 9-16 * * ?"); /* Every 20 minutes from 9 AM to 4:40 PM. */
check_expr_valid("0 0 12 ? * 2-6"); /* Every weekday at noon. */
check_expr_valid("0 0 8-11,13-16 * * ?"); /* Every day from 8-11 AM and 1-4 PM. */
check_expr_valid("0 0/30 6-18 * * ?"); /* Every 30 minutes from 6 AM to 6:30 PM. */
check_expr_valid("0 0 12 ? JAN,MAR,MAY,JUL,SEP,NOV *"); /* Every alternate month at noon. */
check_expr_valid("0 15 9 ? * MON,TUE,WED,THU,FRI"); /* Every weekday at 9:15 AM. */
check_expr_valid("0 0/40 * ? * *"); /* Every 40 minutes. */
check_expr_valid("0 30 10-14 ? * ?"); /* Every day from 10:30 AM to 2:30 PM. */
check_expr_valid("0 0 12 * JAN-DEC ?"); /* Every day of the year at noon. */
check_expr_valid("0 0 0 25 12 ? *"); /* Every Christmas at midnight. */
check_expr_valid("0 0 0/3 * * ?"); /* Every 3 hours. */
check_expr_valid("0 0 12 * * ? 2025"); /* Every day at noon in the year 2025. */
check_expr_valid("0 0 6 ? * 2-6"); /* Every weekday at 6 AM. */
check_expr_valid("0 0 18 ? * 1-5"); /* Every weekday at 6 PM. */
check_expr_valid("0 0 0 ? * * 2026"); /* Every day at midnight in 2026. */
check_expr_valid("0 0/45 10-14 * * ?"); /* Every 45 minutes between 10 AM and 2:45 PM. */
check_expr_valid("0 0 12 1/3 * ?"); /* Every 3 days at noon. */
check_expr_valid("0 0 0 1 JAN,FEB,MAR,APR,MAY,JUN,JUL,AUG,SEP,OCT,NOV,DEC ?"); /* Start of every month at midnight. */
check_expr_valid("0 0 12 10,20,30 * ?"); /* Every 10th, 20th, and 30th of the month at noon. */
check_expr_valid("0 0/50 8-16 * * ?"); /* Every 50 minutes from 8 AM to 4:50 PM. */
check_expr_invalid("0 0 12 ? * 1#1,3#3,5#5"); /* First Sunday, third Wednesday, and fifth Friday of every month at noon. */
check_expr_valid("0 0 0/4 * * ?"); /* Every 4 hours. */
check_expr_valid("0 15,30,45 * * * ?"); /* Every hour at 15, 30, and 45 minutes past. */
check_expr_valid("0 0 12 ? * 2L"); /* Last Monday of every month at noon. */
check_expr_valid("0 0 12 ? * 5L"); /* Last Thursday of every month at noon. */
check_expr_valid("0 0 0/6 * * ?"); /* Every 6 hours. */
check_expr_valid("0 0 12/2 ? * *"); /* Every 2 hours starting at noon. */
check_expr_valid("0 0 12 ? 1,3,5,7,9,11 *"); /* Every odd month at noon. */
check_expr_valid("0 0 0 ? 2,4,6,8,10,12 *"); /* Every even month at midnight. */
check_expr_valid("0 0 12 * * ? 2027"); /* Every day at noon in the year 2027. */
}
void test_bits() {
uint8_t testbyte[8];
memset(testbyte, 0, 8);
int err = 0;
int i;
for (i = 0; i <= 63; i++) {
cron_set_bit(testbyte, i);
if (!cron_get_bit(testbyte, i)) {
printf("Bit set error! Bit: %d!\n", i);
err = 1;
}
cron_del_bit(testbyte, i);
if (cron_get_bit(testbyte, i)) {
printf("Bit clear error! Bit: %d!\n", i);
err = 1;
}
assert(!err);
}
for (i = 0; i < 12; i++) {
cron_set_bit(testbyte, i);
}
if (testbyte[0] != 0xff) {
err = 1;
}
if (testbyte[1] != 0x0f) {
err = 1;
}
assert(!err);
}
int main() {
test_bits();
test_expr();
test_parse();
check_calc_invalid();
printf("\nAll OK!\n");
return 0;
}
+237
View File
@@ -0,0 +1,237 @@
#define _ISOC99_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/wait.h>
#include <sys/types.h>
#include <signal.h>
#include <ctype.h>
#include "ccronexpr.h"
#ifndef VERSION
#define VERSION "dev-build"
#endif
typedef struct { char *shell, *cmd, *schedule; int verbose; } TinyCronJob;
void output(const char *msg) {
printf("[supertinycron] %s\n", msg);
}
void sigchld_handler(int signo) {
(void) signo;
/*while (waitpid(-1, NULL, WNOHANG) > 0);*/
}
void sig_handler(int signo) {
if (signo == SIGTERM || signo == SIGINT) {
output("terminated");
_exit(0);
}
}
int cron_system(const char *shell, const char *command) {
int stdout_pipe[2], stderr_pipe[2];
pid_t pid;
if (pipe(stdout_pipe) != 0 || pipe(stderr_pipe) != 0) {
perror("pipe");
return -1;
}
pid = fork();
if (pid == 0) {
close(stdout_pipe[0]);
close(stderr_pipe[0]);
if (dup2(stdout_pipe[1], STDOUT_FILENO) == -1) {
perror("dup2 stdout");
return -1;
}
if (dup2(stderr_pipe[1], STDERR_FILENO) == -1) {
perror("dup2 stderr");
return -1;
}
close(stdout_pipe[1]);
close(stderr_pipe[1]);
execl(shell, shell, "-c", command, NULL);
perror("execl");
exit(EXIT_FAILURE);
} else if (pid < 0) {
perror("fork");
return -1;
} else {
char buffer[4096];
int nbytes;
close(stdout_pipe[1]);
close(stderr_pipe[1]);
while ((nbytes = read(stdout_pipe[0], buffer, sizeof(buffer))) > 0) {
write(STDOUT_FILENO, buffer, nbytes);
}
while ((nbytes = read(stderr_pipe[0], buffer, sizeof(buffer))) > 0) {
write(STDERR_FILENO, buffer, nbytes);
}
close(stdout_pipe[0]);
close(stderr_pipe[0]);
int status;
pid_t wpid = waitpid(pid, &status, 0);
if (wpid == -1) {
perror("waitpid");
return -1;
}
if (WIFEXITED(status)) return WEXITSTATUS(status);
else if (WIFSIGNALED(status)) return -WTERMSIG(status);
return -1;
}
}
TinyCronJob optsFromEnv() {
TinyCronJob opts = {0, 0, 0, 0};
if (getenv("TINYCRON_VERBOSE") != NULL) opts.verbose = 1;
opts.shell = getenv("SHELL");
if (!opts.shell) opts.shell = (char *)"/bin/sh";
return opts;
}
void usage() {
printf("Usage: supertinycron [expression] [command]\n");
exit(EXIT_FAILURE);
}
void message(const char *err, const char *msg) {
if (strlen(msg) == 0) output(err);
else {
char errMsg[512];
snprintf(errMsg, sizeof(errMsg), "%s %s", msg, err);
output(errMsg);
}
}
void messageInt(int err, const char *msg) {
if (err) message(strerror(err), msg);
}
void exitOnErr(int err, const char *msg) {
if (err) {
messageInt(err, msg);
exit(EXIT_FAILURE);
}
}
void run(TinyCronJob *job) {
if (job->verbose) message(job->cmd, "running job:");
messageInt(cron_system(job->shell, job->cmd), "job failed:");
}
int nap(TinyCronJob *job) {
time_t current_time = time(NULL), next_run;
cron_expr expr;
const char* err = NULL;
cron_parse_expr(job->schedule, &expr, &err);
if (err) {
message(err, "error parsing cron expression:");
return 1;
}
next_run = cron_next(&expr, current_time);
if (job->verbose) {
char msg[512];
struct tm *time_info = localtime(&next_run);
strftime(msg, sizeof(msg), "%Y-%m-%d %H:%M:%S", time_info);
message(msg, "next job scheduled for");
}
int sleep_duration = next_run - current_time;
sleep(sleep_duration);
return 0;
}
char* find_nth(const char* str, char ch, int n) {
int count = 0;
while (*str) {
if (*str == ch && ++count == n) return (char*)str;
str++;
}
return NULL;
}
void parse_line(char *line, TinyCronJob *job, int count) {
job->schedule = line;
job->cmd = find_nth(line, ' ', line[0] == '@' ? 1 : count);
if (!job->cmd) {
messageInt(1, "incomplete cron expression");
exit(EXIT_FAILURE);
}
*job->cmd = '\0';
++job->cmd;
}
/*
int main(int argc, char *argv[]) {
//signal(SIGCHLD, sigchld_handler);
signal(SIGTERM, sig_handler);
signal(SIGINT, sig_handler);
if (argc < 2 || strcmp(argv[1], "help") == 0) usage();
if (strcmp(argv[1], "version") == 0) {
printf("supertinycron version %s\n", VERSION);
return EXIT_SUCCESS;
}
TinyCronJob job = optsFromEnv();
int i, line_len = 0;
for (i = 1; i < argc; i++) {
line_len += strlen(argv[i]);
}
line_len += argc - 3;
line_len += 1;
char *line = (char *)malloc(line_len);
if (!line) {
perror("malloc");
return EXIT_FAILURE;
}
strcpy(line, argv[1]);
for (i = 2; i < argc; i++) {
strcat(line, " ");
strcat(line, argv[i]);
}
if (job.verbose) message(line, "line");
parse_line(line, &job, 7);
while (1) {
if (nap(&job)) {
perror("error creating job");
break;
}
run(&job);
}
free(line);
return EXIT_SUCCESS;
}
*/
+16
View File
@@ -0,0 +1,16 @@
cmake_minimum_required(VERSION 3.0)
project(ccronexpr_test)
# Test executable
add_executable(ccronexpr_test ../ccronexpr_test.c)
target_compile_features(ccronexpr_test PRIVATE c_std_99)
target_include_directories(ccronexpr_test PRIVATE .)
target_link_libraries(ccronexpr_test ccronexpr)
if (MSVC)
target_compile_definitions(ccronexpr_test PRIVATE _CRT_SECURE_NO_WARNINGS)
endif ()
# Add tests
add_test(NAME ccronexpr_test COMMAND ccronexpr_test)
+3
View File
@@ -37,6 +37,7 @@ build_flags = -D NDEBUG
-DPIO_FRAMEWORK_ARDUINO_LWIP2_LOW_MEMORY
-DVTABLES_IN_FLASH
-DPUYA_SUPPORT=1
-DCRON_USE_LOCAL_TIME
-fno-strict-aliasing
-I$PROJECT_DIR/src/include
-include "ESPEasy_config.h"
@@ -61,6 +62,7 @@ build_flags = -DNDEBUG
-DPUYA_SUPPORT=1
-DCORE_POST_2_5_0
-DDISABLE_SC16IS752_SPI
-DCRON_USE_LOCAL_TIME
-fno-strict-aliasing
-DLIBRARIES_NO_LOG=1
-I$PROJECT_DIR/src/include
@@ -251,6 +253,7 @@ build_flags = -DESP32_STAGE
-DCONFIG_FREERTOS_USE_TICKLESS_IDLE=1
-DCONFIG_FREERTOS_IDLE_TIME_BEFORE_SLEEP=3
-DNEOPIXEL_ESP32_RMT_DEFAULT
-DCRON_USE_LOCAL_TIME
-I$PROJECT_DIR/src/include
-include "sdkconfig.h"
-include "ESPEasy_config.h"