#pragma once #include #include #include #include #include #include #include #include #include #if __cplusplus > 201402L #include #define CRONCPP_IS_CPP17 #endif namespace cron { #ifdef CRONCPP_IS_CPP17 #define CRONCPP_STRING_VIEW std::string_view #define CRONCPP_STRING_VIEW_NPOS std::string_view::npos #define CRONCPP_CONSTEXPTR constexpr #else #define CRONCPP_STRING_VIEW std::string const & #define CRONCPP_STRING_VIEW_NPOS std::string::npos #define CRONCPP_CONSTEXPTR #endif using cron_int = uint8_t; constexpr std::time_t INVALID_TIME = static_cast(-1); constexpr size_t INVALID_INDEX = static_cast(-1); class cronexpr; namespace detail { enum class cron_field { second, minute, hour_of_day, day_of_week, day_of_month, month, year }; template static bool find_next(cronexpr const &cex, std::tm &date, size_t const dot); } struct bad_cronexpr : public std::runtime_error { public: explicit bad_cronexpr(CRONCPP_STRING_VIEW message) : std::runtime_error(message.data()) { } }; struct cron_standard_traits { static const cron_int CRON_MIN_SECONDS = 0; static const cron_int CRON_MAX_SECONDS = 59; static const cron_int CRON_MIN_MINUTES = 0; static const cron_int CRON_MAX_MINUTES = 59; static const cron_int CRON_MIN_HOURS = 0; static const cron_int CRON_MAX_HOURS = 23; static const cron_int CRON_MIN_DAYS_OF_WEEK = 0; static const cron_int CRON_MAX_DAYS_OF_WEEK = 6; static const cron_int CRON_MIN_DAYS_OF_MONTH = 1; static const cron_int CRON_MAX_DAYS_OF_MONTH = 31; static const cron_int CRON_MIN_MONTHS = 1; static const cron_int CRON_MAX_MONTHS = 12; static const cron_int CRON_MAX_YEARS_DIFF = 4; #ifdef CRONCPP_IS_CPP17 static const inline std::vector DAYS = {"SUN", "MON", "TUE", "WED", "THU", "FRI", "SAT"}; static const inline std::vector MONTHS = {"NIL", "JAN", "FEB", "MAR", "APR", "MAY", "JUN", "JUL", "AUG", "SEP", "OCT", "NOV", "DEC"}; #else static std::vector &DAYS() { static std::vector days = {"SUN", "MON", "TUE", "WED", "THU", "FRI", "SAT"}; return days; } static std::vector &MONTHS() { static std::vector months = {"NIL", "JAN", "FEB", "MAR", "APR", "MAY", "JUN", "JUL", "AUG", "SEP", "OCT", "NOV", "DEC"}; return months; } #endif }; struct cron_oracle_traits { static const cron_int CRON_MIN_SECONDS = 0; static const cron_int CRON_MAX_SECONDS = 59; static const cron_int CRON_MIN_MINUTES = 0; static const cron_int CRON_MAX_MINUTES = 59; static const cron_int CRON_MIN_HOURS = 0; static const cron_int CRON_MAX_HOURS = 23; static const cron_int CRON_MIN_DAYS_OF_WEEK = 1; static const cron_int CRON_MAX_DAYS_OF_WEEK = 7; static const cron_int CRON_MIN_DAYS_OF_MONTH = 1; static const cron_int CRON_MAX_DAYS_OF_MONTH = 31; static const cron_int CRON_MIN_MONTHS = 0; static const cron_int CRON_MAX_MONTHS = 11; static const cron_int CRON_MAX_YEARS_DIFF = 4; #ifdef CRONCPP_IS_CPP17 static const inline std::vector DAYS = {"NIL", "SUN", "MON", "TUE", "WED", "THU", "FRI", "SAT"}; static const inline std::vector MONTHS = {"JAN", "FEB", "MAR", "APR", "MAY", "JUN", "JUL", "AUG", "SEP", "OCT", "NOV", "DEC"}; #else static std::vector &DAYS() { static std::vector days = {"NIL", "SUN", "MON", "TUE", "WED", "THU", "FRI", "SAT"}; return days; } static std::vector &MONTHS() { static std::vector months = {"JAN", "FEB", "MAR", "APR", "MAY", "JUN", "JUL", "AUG", "SEP", "OCT", "NOV", "DEC"}; return months; } #endif }; struct cron_quartz_traits { static const cron_int CRON_MIN_SECONDS = 0; static const cron_int CRON_MAX_SECONDS = 59; static const cron_int CRON_MIN_MINUTES = 0; static const cron_int CRON_MAX_MINUTES = 59; static const cron_int CRON_MIN_HOURS = 0; static const cron_int CRON_MAX_HOURS = 23; static const cron_int CRON_MIN_DAYS_OF_WEEK = 1; static const cron_int CRON_MAX_DAYS_OF_WEEK = 7; static const cron_int CRON_MIN_DAYS_OF_MONTH = 1; static const cron_int CRON_MAX_DAYS_OF_MONTH = 31; static const cron_int CRON_MIN_MONTHS = 1; static const cron_int CRON_MAX_MONTHS = 12; static const cron_int CRON_MAX_YEARS_DIFF = 4; #ifdef CRONCPP_IS_CPP17 static const inline std::vector DAYS = {"NIL", "SUN", "MON", "TUE", "WED", "THU", "FRI", "SAT"}; static const inline std::vector MONTHS = {"NIL", "JAN", "FEB", "MAR", "APR", "MAY", "JUN", "JUL", "AUG", "SEP", "OCT", "NOV", "DEC"}; #else static std::vector &DAYS() { static std::vector days = {"NIL", "SUN", "MON", "TUE", "WED", "THU", "FRI", "SAT"}; return days; } static std::vector &MONTHS() { static std::vector months = {"NIL", "JAN", "FEB", "MAR", "APR", "MAY", "JUN", "JUL", "AUG", "SEP", "OCT", "NOV", "DEC"}; return months; } #endif }; class cronexpr; template static cronexpr make_cron(CRONCPP_STRING_VIEW expr); class cronexpr { std::bitset<60> seconds; std::bitset<60> minutes; std::bitset<24> hours; std::bitset<7> days_of_week; std::bitset<31> days_of_month; std::bitset<12> months; std::string expr; friend bool operator==(cronexpr const &e1, cronexpr const &e2); friend bool operator!=(cronexpr const &e1, cronexpr const &e2); template friend bool detail::find_next(cronexpr const &cex, std::tm &date, size_t const dot); friend std::string to_cronstr(cronexpr const &cex); friend std::string to_string(cronexpr const &cex); template friend cronexpr make_cron(CRONCPP_STRING_VIEW expr); }; inline bool operator==(cronexpr const &e1, cronexpr const &e2) { return e1.seconds == e2.seconds && e1.minutes == e2.minutes && e1.hours == e2.hours && e1.days_of_week == e2.days_of_week && e1.days_of_month == e2.days_of_month && e1.months == e2.months; } inline bool operator!=(cronexpr const &e1, cronexpr const &e2) { return !(e1 == e2); } inline std::string to_string(cronexpr const &cex) { return cex.seconds.to_string() + " " + cex.minutes.to_string() + " " + cex.hours.to_string() + " " + cex.days_of_month.to_string() + " " + cex.months.to_string() + " " + cex.days_of_week.to_string(); } inline std::string to_cronstr(cronexpr const &cex) { return cex.expr; } namespace utils { inline std::time_t tm_to_time(std::tm &date) { return std::mktime(&date); } inline std::tm *time_to_tm(std::time_t const *date, std::tm *const out) { #ifdef _WIN32 errno_t err = localtime_s(out, date); return 0 == err ? out : nullptr; #else return localtime_r(date, out); #endif } inline std::tm to_tm(CRONCPP_STRING_VIEW time) { std::tm result; #if __cplusplus > 201103L std::istringstream str(time.data()); str.imbue(std::locale(setlocale(LC_ALL, nullptr))); str >> std::get_time(&result, "%Y-%m-%d %H:%M:%S"); if (str.fail()) throw std::runtime_error("Parsing date failed!"); #else int year = 1900; int month = 1; int day = 1; int hour = 0; int minute = 0; int second = 0; sscanf(time.data(), "%d-%d-%d %d:%d:%d", &year, &month, &day, &hour, &minute, &second); result.tm_year = year - 1900; result.tm_mon = month - 1; result.tm_mday = day; result.tm_hour = hour; result.tm_min = minute; result.tm_sec = second; #endif result.tm_isdst = -1; // DST info not available return result; } inline std::string to_string(std::tm const &tm) { #if __cplusplus > 201103L std::ostringstream str; str.imbue(std::locale(setlocale(LC_ALL, nullptr))); str << std::put_time(&tm, "%Y-%m-%d %H:%M:%S"); if (str.fail()) throw std::runtime_error("Writing date failed!"); return str.str(); #else char buff[70] = {0}; strftime(buff, sizeof(buff), "%Y-%m-%d %H:%M:%S", &tm); return std::string(buff); #endif } inline std::string to_upper(std::string text) { std::transform(std::begin(text), std::end(text), std::begin(text), [](char const c) { return static_cast(std::toupper(c)); }); return text; } static std::vector split(CRONCPP_STRING_VIEW text, char const delimiter) { std::vector tokens; std::string token; std::istringstream tokenStream(text.data()); while (std::getline(tokenStream, token, delimiter)) { tokens.push_back(token); } return tokens; } CRONCPP_CONSTEXPTR inline bool contains(CRONCPP_STRING_VIEW text, char const ch) noexcept { return CRONCPP_STRING_VIEW_NPOS != text.find_first_of(ch); } } namespace detail { inline cron_int to_cron_int(CRONCPP_STRING_VIEW text) { try { return static_cast(std::stoul(text.data())); } catch (std::exception const &ex) { throw bad_cronexpr(ex.what()); } } static std::string replace_ordinals( std::string text, std::vector const &replacement) { for (size_t i = 0; i < replacement.size(); ++i) { auto pos = text.find(replacement[i]); if (std::string::npos != pos) text.replace(pos, 3, std::to_string(i)); } return text; } static std::pair make_range( CRONCPP_STRING_VIEW field, cron_int const minval, cron_int const maxval) { cron_int first = 0; cron_int last = 0; if (field.size() == 1 && field[0] == '*') { first = minval; last = maxval; } else if (!utils::contains(field, '-')) { first = to_cron_int(field); last = first; } else { auto parts = utils::split(field, '-'); if (parts.size() != 2) throw bad_cronexpr("Specified range requires two fields"); first = to_cron_int(parts[0]); last = to_cron_int(parts[1]); } if (first > maxval || last > maxval) { throw bad_cronexpr("Specified range exceeds maximum"); } if (first < minval || last < minval) { throw bad_cronexpr("Specified range is less than minimum"); } if (first > last) { throw bad_cronexpr("Specified range start exceeds range end"); } return {first, last}; } template static void set_cron_field( CRONCPP_STRING_VIEW value, std::bitset &target, cron_int const minval, cron_int const maxval) { if (value.length() > 0 && value[value.length() - 1] == ',') throw bad_cronexpr("Value cannot end with comma"); auto fields = utils::split(value, ','); if (fields.empty()) throw bad_cronexpr("Expression parsing error"); for (auto const &field : fields) { if (!utils::contains(field, '/')) { #ifdef CRONCPP_IS_CPP17 auto [first, last] = detail::make_range(field, minval, maxval); #else auto range = detail::make_range(field, minval, maxval); auto first = range.first; auto last = range.second; #endif for (cron_int i = first - minval; i <= last - minval; ++i) { target.set(i); } } else { auto parts = utils::split(field, '/'); if (parts.size() != 2) throw bad_cronexpr("Incrementer must have two fields"); #ifdef CRONCPP_IS_CPP17 auto [first, last] = detail::make_range(parts[0], minval, maxval); #else auto range = detail::make_range(parts[0], minval, maxval); auto first = range.first; auto last = range.second; #endif if (!utils::contains(parts[0], '-')) { last = maxval; } auto delta = detail::to_cron_int(parts[1]); if (delta <= 0) throw bad_cronexpr("Incrementer must be a positive value"); for (cron_int i = first - minval; i <= last - minval; i += delta) { target.set(i); } } } } template static void set_cron_days_of_week( std::string value, std::bitset<7> &target) { auto days = utils::to_upper(value); auto days_replaced = detail::replace_ordinals( days, #ifdef CRONCPP_IS_CPP17 Traits::DAYS #else Traits::DAYS() #endif ); if (days_replaced.size() == 1 && days_replaced[0] == '?') days_replaced[0] = '*'; set_cron_field( days_replaced, target, Traits::CRON_MIN_DAYS_OF_WEEK, Traits::CRON_MAX_DAYS_OF_WEEK); } template static void set_cron_days_of_month( std::string value, std::bitset<31> &target) { if (value.size() == 1 && value[0] == '?') value[0] = '*'; set_cron_field( value, target, Traits::CRON_MIN_DAYS_OF_MONTH, Traits::CRON_MAX_DAYS_OF_MONTH); } template static void set_cron_month( std::string value, std::bitset<12> &target) { auto month = utils::to_upper(value); auto month_replaced = replace_ordinals( month, #ifdef CRONCPP_IS_CPP17 Traits::MONTHS #else Traits::MONTHS() #endif ); set_cron_field( month_replaced, target, Traits::CRON_MIN_MONTHS, Traits::CRON_MAX_MONTHS); } template inline size_t next_set_bit( std::bitset const &target, size_t /*minimum*/, size_t /*maximum*/, size_t offset) { for (auto i = offset; i < N; ++i) { if (target.test(i)) return i; } return INVALID_INDEX; } inline void add_to_field( std::tm &date, cron_field const field, int const val) { switch (field) { case cron_field::second: date.tm_sec += val; break; case cron_field::minute: date.tm_min += val; break; case cron_field::hour_of_day: date.tm_hour += val; break; case cron_field::day_of_week: case cron_field::day_of_month: date.tm_mday += val; date.tm_isdst = -1; break; case cron_field::month: date.tm_mon += val; date.tm_isdst = -1; break; case cron_field::year: date.tm_year += val; break; } if (INVALID_TIME == utils::tm_to_time(date)) throw bad_cronexpr("Invalid time expression"); } inline void set_field( std::tm &date, cron_field const field, int const val) { switch (field) { case cron_field::second: date.tm_sec = val; break; case cron_field::minute: date.tm_min = val; break; case cron_field::hour_of_day: date.tm_hour = val; break; case cron_field::day_of_week: date.tm_wday = val; break; case cron_field::day_of_month: date.tm_mday = val; date.tm_isdst = -1; break; case cron_field::month: date.tm_mon = val; date.tm_isdst = -1; break; case cron_field::year: date.tm_year = val; break; } if (INVALID_TIME == utils::tm_to_time(date)) throw bad_cronexpr("Invalid time expression"); } inline void reset_field( std::tm &date, cron_field const field) { switch (field) { case cron_field::second: date.tm_sec = 0; break; case cron_field::minute: date.tm_min = 0; break; case cron_field::hour_of_day: date.tm_hour = 0; break; case cron_field::day_of_week: date.tm_wday = 0; break; case cron_field::day_of_month: date.tm_mday = 1; date.tm_isdst = -1; break; case cron_field::month: date.tm_mon = 0; date.tm_isdst = -1; break; case cron_field::year: date.tm_year = 0; break; } if (INVALID_TIME == utils::tm_to_time(date)) throw bad_cronexpr("Invalid time expression"); } inline void reset_all_fields( std::tm &date, std::bitset<7> const &marked_fields) { for (size_t i = 0; i < marked_fields.size(); ++i) { if (marked_fields.test(i)) reset_field(date, static_cast(i)); } } inline void mark_field( std::bitset<7> &orders, cron_field const field) { if (!orders.test(static_cast(field))) orders.set(static_cast(field)); } template static size_t find_next( std::bitset const &target, std::tm &date, unsigned int const minimum, unsigned int const maximum, unsigned int const value, cron_field const field, cron_field const next_field, std::bitset<7> const &marked_fields) { auto next_value = next_set_bit(target, minimum, maximum, value); if (INVALID_INDEX == next_value) { add_to_field(date, next_field, 1); reset_field(date, field); next_value = next_set_bit(target, minimum, maximum, 0); } if (INVALID_INDEX == next_value || next_value != value) { set_field(date, field, static_cast(next_value)); reset_all_fields(date, marked_fields); } return next_value; } template static size_t find_next_day( std::tm &date, std::bitset<31> const &days_of_month, size_t day_of_month, std::bitset<7> const &days_of_week, size_t day_of_week, std::bitset<7> const &marked_fields) { unsigned int count = 0; unsigned int maximum = 366; while ( (!days_of_month.test(day_of_month - Traits::CRON_MIN_DAYS_OF_MONTH) || !days_of_week.test(day_of_week - Traits::CRON_MIN_DAYS_OF_WEEK)) && count++ < maximum) { add_to_field(date, cron_field::day_of_month, 1); day_of_month = date.tm_mday; day_of_week = date.tm_wday; reset_all_fields(date, marked_fields); } return day_of_month; } template static bool find_next(cronexpr const &cex, std::tm &date, size_t const dot) { bool res = true; std::bitset<7> marked_fields{0}; std::bitset<7> empty_list{0}; unsigned int second = date.tm_sec; auto updated_second = find_next( cex.seconds, date, Traits::CRON_MIN_SECONDS, Traits::CRON_MAX_SECONDS, second, cron_field::second, cron_field::minute, empty_list); if (second == updated_second) { mark_field(marked_fields, cron_field::second); } unsigned int minute = date.tm_min; auto update_minute = find_next( cex.minutes, date, Traits::CRON_MIN_MINUTES, Traits::CRON_MAX_MINUTES, minute, cron_field::minute, cron_field::hour_of_day, marked_fields); if (minute == update_minute) { mark_field(marked_fields, cron_field::minute); } else { res = find_next(cex, date, dot); if (!res) return res; } unsigned int hour = date.tm_hour; auto updated_hour = find_next( cex.hours, date, Traits::CRON_MIN_HOURS, Traits::CRON_MAX_HOURS, hour, cron_field::hour_of_day, cron_field::day_of_week, marked_fields); if (hour == updated_hour) { mark_field(marked_fields, cron_field::hour_of_day); } else { res = find_next(cex, date, dot); if (!res) return res; } unsigned int day_of_week = date.tm_wday; unsigned int day_of_month = date.tm_mday; auto updated_day_of_month = find_next_day( date, cex.days_of_month, day_of_month, cex.days_of_week, day_of_week, marked_fields); if (day_of_month == updated_day_of_month) { mark_field(marked_fields, cron_field::day_of_month); } else { res = find_next(cex, date, dot); if (!res) return res; } unsigned int month = date.tm_mon; auto updated_month = find_next( cex.months, date, Traits::CRON_MIN_MONTHS, Traits::CRON_MAX_MONTHS, month, cron_field::month, cron_field::year, marked_fields); if (month != updated_month) { if (date.tm_year - dot > Traits::CRON_MAX_YEARS_DIFF) return false; res = find_next(cex, date, dot); if (!res) return res; } return res; } } template static cronexpr make_cron(CRONCPP_STRING_VIEW expr) { cronexpr cex; if (expr.empty()) throw bad_cronexpr("Invalid empty cron expression"); auto fields = utils::split(expr, ' '); fields.erase( std::remove_if(std::begin(fields), std::end(fields), [](CRONCPP_STRING_VIEW s) { return s.empty(); }), std::end(fields)); if (fields.size() != 6) throw bad_cronexpr("cron expression must have six fields"); detail::set_cron_field(fields[0], cex.seconds, Traits::CRON_MIN_SECONDS, Traits::CRON_MAX_SECONDS); detail::set_cron_field(fields[1], cex.minutes, Traits::CRON_MIN_MINUTES, Traits::CRON_MAX_MINUTES); detail::set_cron_field(fields[2], cex.hours, Traits::CRON_MIN_HOURS, Traits::CRON_MAX_HOURS); detail::set_cron_days_of_week(fields[5], cex.days_of_week); detail::set_cron_days_of_month(fields[3], cex.days_of_month); detail::set_cron_month(fields[4], cex.months); cex.expr = expr; return cex; } template static std::tm cron_next(cronexpr const &cex, std::tm date) { time_t original = utils::tm_to_time(date); if (INVALID_TIME == original) return {}; if (!detail::find_next(cex, date, date.tm_year)) return {}; time_t calculated = utils::tm_to_time(date); if (INVALID_TIME == calculated) return {}; if (calculated == original) { add_to_field(date, detail::cron_field::second, 1); if (!detail::find_next(cex, date, date.tm_year)) return {}; } return date; } template static std::time_t cron_next(cronexpr const &cex, std::time_t const &date) { std::tm val; std::tm *dt = utils::time_to_tm(&date, &val); if (dt == nullptr) return INVALID_TIME; time_t original = utils::tm_to_time(*dt); if (INVALID_TIME == original) return INVALID_TIME; if (!detail::find_next(cex, *dt, dt->tm_year)) return INVALID_TIME; time_t calculated = utils::tm_to_time(*dt); if (INVALID_TIME == calculated) return calculated; if (calculated == original) { add_to_field(*dt, detail::cron_field::second, 1); if (!detail::find_next(cex, *dt, dt->tm_year)) return INVALID_TIME; } return utils::tm_to_time(*dt); } template static std::chrono::system_clock::time_point cron_next(cronexpr const &cex, std::chrono::system_clock::time_point const &time_point) { return std::chrono::system_clock::from_time_t(cron_next(cex, std::chrono::system_clock::to_time_t(time_point))); } }