diff --git a/README.md b/README.md index 8efbbde..cfa3fac 100644 --- a/README.md +++ b/README.md @@ -71,8 +71,8 @@ the '?'-character to ensure that it is not possible to specify a statement which The standard cron format does not allow for randomization, but with the use of `CronRandomization` you can generate random schedules using the following format: `R(range_start-range_end)`, where `range_start` and `range_end` follow the same rules -as for a regular cron range with the addition that only numbers are allowed. All the rules for a regular cron expression -still applies when using randomization, i.e. mutual exclusiveness and not extra spaces. +as for a regular cron range (step-syntax is not supported). All the rules for a regular cron expression still applies +when using randomization, i.e. mutual exclusiveness and no extra spaces. ## Examples |Expression | Meaning @@ -80,6 +80,7 @@ still applies when using randomization, i.e. mutual exclusiveness and not extra | 0 0 R(13-20) * * ? | On the hour, on a random hour 13-20, inclusive. | 0 0 0 ? * R(0-6) | A random weekday, every week, at midnight. | 0 R(45-15) */12 ? * * | A random minute between 45-15, inclusive, every 12 hours. +|0 0 0 ? R(DEC-MAR) R(SAT-SUN)| On the hour, on a random month december to march, on a random weekday saturday to sunday. # Used Third party libraries diff --git a/libcron/CMakeLists.txt b/libcron/CMakeLists.txt index cab5763..c57b73a 100644 --- a/libcron/CMakeLists.txt +++ b/libcron/CMakeLists.txt @@ -1,7 +1,7 @@ cmake_minimum_required(VERSION 3.6) project(libcron) -set(CMAKE_CXX_STANDARD 14) +set(CMAKE_CXX_STANDARD 17) if( MSVC ) set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /W4") diff --git a/libcron/include/libcron/CronData.h b/libcron/include/libcron/CronData.h index 5437b87..6412988 100644 --- a/libcron/include/libcron/CronData.h +++ b/libcron/include/libcron/CronData.h @@ -16,7 +16,7 @@ namespace libcron static CronData create(const std::string& cron_expression); - CronData(); + CronData() = default; CronData(const CronData&) = default; @@ -77,6 +77,9 @@ namespace libcron template bool convert_from_string_range_to_number_range(const std::string& range, std::set& numbers); + template + static std::string& replace_string_name_with_numeric(std::string& s); + private: void parse(const std::string& cron_expression); @@ -121,8 +124,8 @@ namespace libcron std::set day_of_week{}; bool valid = false; - std::vector month_names; - std::vector day_names; + static const std::vector month_names; + static const std::vector day_names; template void add_full_range(std::set& set); @@ -343,4 +346,40 @@ namespace libcron return res; } + + template + std::string & CronData::replace_string_name_with_numeric(std::string& s) + { + auto value = static_cast(T::First); + + const std::vector* name_source{}; + + static_assert(std::is_same() + || std::is_same(), + "T must be either Months or DayOfWeek"); + + if constexpr (std::is_same()) + { + name_source = &month_names; + } + else + { + name_source = &day_names; + } + + for (const auto& name : *name_source) + { + std::regex m(name, std::regex_constants::ECMAScript | std::regex_constants::icase); + + std::string replaced; + + std::regex_replace(std::back_inserter(replaced), s.begin(), s.end(), m, std::to_string(value)); + + s = replaced; + + ++value; + } + + return s; + } } diff --git a/libcron/include/libcron/CronRandomization.h b/libcron/include/libcron/CronRandomization.h index 759995e..0c039d8 100644 --- a/libcron/include/libcron/CronRandomization.h +++ b/libcron/include/libcron/CronRandomization.h @@ -44,7 +44,7 @@ namespace libcron std::smatch random_match; - if (std::regex_match(section.begin(), section.end(), random_match, rand_expression)) + if (std::regex_match(section.cbegin(), section.cend(), random_match, rand_expression)) { // Random range, get left and right numbers. auto left = std::stoi(random_match[1].str()); diff --git a/libcron/src/CronData.cpp b/libcron/src/CronData.cpp index 6ee9dbc..e28896b 100644 --- a/libcron/src/CronData.cpp +++ b/libcron/src/CronData.cpp @@ -13,6 +13,9 @@ namespace libcron Months::October, Months::December }; + const std::vector CronData::month_names{ "JAN", "FEB", "MAR", "APR", "MAY", "JUN", "JUL", "AUG", "SEP", "OCT", "NOV", "DEC" }; + const std::vector CronData::day_names{ "SUN", "MON", "TUE", "WED", "THU", "FRI", "SAT" }; + CronData CronData::create(const std::string& cron_expression) { CronData c; @@ -21,12 +24,6 @@ namespace libcron return 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. diff --git a/libcron/src/CronRandomization.cpp b/libcron/src/CronRandomization.cpp index 144fbcb..2aadb29 100644 --- a/libcron/src/CronRandomization.cpp +++ b/libcron/src/CronRandomization.cpp @@ -18,14 +18,46 @@ namespace libcron std::tuple CronRandomization::parse(const std::string& cron_schedule) { // Split on space to get each separate part, six parts expected - std::regex split{ R"#(^\s*(.*?)\s+(.*?)\s+(.*?)\s+(.*?)\s+(.*?)\s+(.*?)\s*$)#", - std::regex_constants::ECMAScript }; + const std::regex split{ R"#(^\s*(.*?)\s+(.*?)\s+(.*?)\s+(.*?)\s+(.*?)\s+(.*?)\s*$)#", + std::regex_constants::ECMAScript }; std::smatch all_sections; + auto res = std::regex_match(cron_schedule.cbegin(), cron_schedule.cend(), all_sections, split); - std::string final_cron_schedule; + // Replace text with numbers + std::string working_copy{}; - auto res = std::regex_match(cron_schedule.begin(), cron_schedule.end(), all_sections, split); + if (res) + { + // Merge seconds, minutes, hours and day of month back together + working_copy += all_sections[1].str(); + working_copy += " "; + working_copy += all_sections[2].str(); + working_copy += " "; + working_copy += all_sections[3].str(); + working_copy += " "; + working_copy += all_sections[4].str(); + working_copy += " "; + + // Replace month names + auto month = all_sections[5].str(); + CronData::replace_string_name_with_numeric(month); + + working_copy += " "; + working_copy += month; + + // Replace day names + auto dow = all_sections[6].str(); + CronData::replace_string_name_with_numeric(dow); + + working_copy += " "; + working_copy += dow; + } + + std::string final_cron_schedule{}; + + // Split again on space + res = res && std::regex_match(working_copy.cbegin(), working_copy.cend(), all_sections, split); if (res) { diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index da2349b..25e7727 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -1,7 +1,7 @@ cmake_minimum_required(VERSION 3.6) project(cron_test) -set(CMAKE_CXX_STANDARD 14) +set(CMAKE_CXX_STANDARD 17) if( MSVC ) set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /W4") diff --git a/test/CronDataTest.cpp b/test/CronDataTest.cpp index 74d7b83..faaa5dc 100644 --- a/test/CronDataTest.cpp +++ b/test/CronDataTest.cpp @@ -224,4 +224,17 @@ SCENARIO("Dates that does not exist") SCENARIO("Date that exist in one of the months") { REQUIRE(CronData::create("0 0 * 31 APR,MAY ?").is_valid()); +} + +SCENARIO("Replacing text with numbers") +{ + { + std::string s = "SUN-TUE"; + REQUIRE(CronData::replace_string_name_with_numeric(s) == "0-2"); + } + + { + std::string s = "JAN-DEC"; + REQUIRE(CronData::replace_string_name_with_numeric(s) == "1-12"); + } } \ No newline at end of file diff --git a/test/CronRandomizationTest.cpp b/test/CronRandomizationTest.cpp index 7f9b8d1..0ecef3c 100644 --- a/test/CronRandomizationTest.cpp +++ b/test/CronRandomizationTest.cpp @@ -7,21 +7,31 @@ #include using namespace libcron; +const auto EXPECT_FAILURE = true; -void test(const char* const random_schedule) +void test(const char* const random_schedule, bool expect_failure = false) { libcron::CronRandomization cr; - std::unordered_map> results{}; for (int i = 0; i < 5000; ++i) { auto res = cr.parse(random_schedule); - REQUIRE(std::get<0>(res)); auto schedule = std::get<1>(res); - INFO("schedule:" << schedule); Cron<> cron; - REQUIRE(cron.add_schedule("validate schedule", schedule, []() {})); + + if(expect_failure) + { + // Parsing of random might succeed, but it yields an invalid schedule. + auto r = std::get<0>(res) && cron.add_schedule("validate schedule", schedule, []() {}); + REQUIRE_FALSE(r); + } + else + { + REQUIRE(std::get<0>(res)); + REQUIRE(cron.add_schedule("validate schedule", schedule, []() {})); + + } } } @@ -103,3 +113,68 @@ SCENARIO("Test readme examples") } } } + +SCENARIO("Randomization using text versions of days and months") +{ + GIVEN("0 0 0 ? * R(TUE-FRI)") + { + THEN("Valid schedule generated") + { + test("0 0 0 ? * R(TUE-FRI)"); + } + } + + GIVEN("Valid schedule") + { + THEN("Valid schedule generated") + { + test("0 0 0 ? R(JAN-DEC) R(MON-FRI)"); + } + AND_WHEN("Given 0 0 0 ? R(DEC-MAR) R(SAT-SUN)") + { + THEN("Valid schedule generated") + { + test("0 0 0 ? R(DEC-MAR) R(SAT-SUN)"); + } + } + AND_THEN("Given 0 0 0 ? R(JAN-FEB) *") + { + THEN("Valid schedule generated") + { + test("0 0 0 ? R(JAN-FEB) *"); + } + } + AND_THEN("Given 0 0 0 ? R(OCT-OCT) *") + { + THEN("Valid schedule generated") + { + test("0 0 0 ? R(OCT-OCT) *"); + } + } + } + + GIVEN("Invalid schedule") + { + THEN("No schedule generated") + { + // Day of month specified - not allowed with day of week + test("0 0 0 1 R(JAN-DEC) R(MON-SUN)", EXPECT_FAILURE); + } + AND_THEN("No schedule generated") + { + // Invalid range + test("0 0 0 ? R(JAN) *", EXPECT_FAILURE); + } + AND_THEN("No schedule generated") + { + // Days in month field + test("0 0 0 ? R(MON-TUE) *", EXPECT_FAILURE); + } + AND_THEN("No schedule generated") + { + // Month in day field + test("0 0 0 ? * R(JAN-JUN)", EXPECT_FAILURE); + } + + } +}