#include #include "CronData.h" using namespace date; namespace libcron { CronData CronData::create(const std::string& cron_expression) { CronData c; c.parse(cron_expression); return std::move(c); } CronData::CronData() : month_names({"JAN", "FEB", "MAR", "APR", "MAY", "JUN", "JUL", "AUG", "SEP", "OCT", "NOV", "DEC"}), day_names({"SUN", "MON", "TUE", "WED", "THU", "FRI", "SAT"}) { } void CronData::parse(const std::string& cron_expression) { // First, split on white-space. We expect six parts. std::regex split{R"#(^\s*(.*?)\s+(.*?)\s+(.*?)\s+(.*?)\s+(.*?)\s+(.*?)\s*$)#", std::regex_constants::ECMAScript}; std::smatch match; if (std::regex_match(cron_expression.begin(), cron_expression.end(), match, split)) { valid = validate_numeric(match[1], seconds); valid &= validate_numeric(match[2], minutes); valid &= validate_numeric(match[3], hours); valid &= validate_numeric(match[4], day_of_month); valid &= validate_literal(match[5], months, month_names); valid &= validate_literal(match[6], day_of_week, day_names); valid &= check_dom_vs_dow(match[4], match[6]); valid &= validate_date_vs_months(); } } std::vector CronData::split(const std::string& s, char token) { std::vector res; std::string r = "["; r += token; r += "]"; std::regex splitter{r, std::regex_constants::ECMAScript}; std::copy(std::sregex_token_iterator(s.begin(), s.end(), splitter, -1), std::sregex_token_iterator(), std::back_inserter(res)); return res; } bool CronData::is_number(const std::string& s) { // Find any character that isn't a number. return !s.empty() && std::find_if(s.begin(), s.end(), [](char c) { return !std::isdigit(c); }) == s.end(); } bool CronData::is_between(int32_t value, int32_t low_limit, int32_t high_limt) { return value >= low_limit && value <= high_limt; } bool CronData::validate_date_vs_months() const { bool res = true; // Verify that the available dates are possible based on the given months if (months.size() == 1 && months.find(static_cast(2)) != months.end()) { // Only february allowed, make sure that the allowed date(s) includes 29 and below. res = has_any_in_range(day_of_month, 1, 29); } if (res) { // Make sure that if the days contains only 31, at least one month allows that date. if (day_of_month.size() == 1 && day_of_month.find(DayOfMonth::Last) != day_of_month.end()) { std::vector months_with_31; for (int32_t i = 1; i <= 12; ++i) { auto ymd = 2018_y / i / date::last; if (unsigned(ymd.day()) == 31) { months_with_31.push_back(i); } } res = false; for (size_t i = 0; !res && i < months_with_31.size(); ++i) { res = months.find(static_cast(months_with_31[i])) != months.end(); } } } return res; } bool CronData::check_dom_vs_dow(const std::string& dom, const std::string& dow) const { // Day of month and day of week are mutually exclusive so one of them must at always be ignored using // the '?'-character unless one field already is something other than '*'. // // Since we treat an ignored field as allowing the full range, we're OK with both being flagged // as ignored. To make it explicit to the user of the library, we do however require the use of // '?' as the ignore flag, although it is functionally equivalent to '*'. auto check = [](const std::string& l, std::string r) { return l == "*" && (r != "*" || r == "?"); }; return (dom == "?" || dow == "?") || check(dom, dow) || check(dow, dom); } }