Skip to content

Commit b82267a

Browse files
authored
Merge pull request #3 from PerMalmberg/feature/random-via-textual-names
Implemented support for using textual names in randomization.
2 parents a918f3d + d61086f commit b82267a

File tree

9 files changed

+180
-23
lines changed

9 files changed

+180
-23
lines changed

README.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -71,15 +71,16 @@ the '?'-character to ensure that it is not possible to specify a statement which
7171

7272
The standard cron format does not allow for randomization, but with the use of `CronRandomization` you can generate random
7373
schedules using the following format: `R(range_start-range_end)`, where `range_start` and `range_end` follow the same rules
74-
as for a regular cron range with the addition that only numbers are allowed. All the rules for a regular cron expression
75-
still applies when using randomization, i.e. mutual exclusiveness and not extra spaces.
74+
as for a regular cron range (step-syntax is not supported). All the rules for a regular cron expression still applies
75+
when using randomization, i.e. mutual exclusiveness and no extra spaces.
7676

7777
## Examples
7878
|Expression | Meaning
7979
| --- | --- |
8080
| 0 0 R(13-20) * * ? | On the hour, on a random hour 13-20, inclusive.
8181
| 0 0 0 ? * R(0-6) | A random weekday, every week, at midnight.
8282
| 0 R(45-15) */12 ? * * | A random minute between 45-15, inclusive, every 12 hours.
83+
|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.
8384

8485

8586
# Used Third party libraries

libcron/CMakeLists.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
cmake_minimum_required(VERSION 3.6)
22
project(libcron)
33

4-
set(CMAKE_CXX_STANDARD 14)
4+
set(CMAKE_CXX_STANDARD 17)
55

66
if( MSVC )
77
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /W4")

libcron/include/libcron/CronData.h

Lines changed: 42 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ namespace libcron
1616

1717
static CronData create(const std::string& cron_expression);
1818

19-
CronData();
19+
CronData() = default;
2020

2121
CronData(const CronData&) = default;
2222

@@ -77,6 +77,9 @@ namespace libcron
7777
template<typename T>
7878
bool convert_from_string_range_to_number_range(const std::string& range, std::set<T>& numbers);
7979

80+
template<typename T>
81+
static std::string& replace_string_name_with_numeric(std::string& s);
82+
8083
private:
8184
void parse(const std::string& cron_expression);
8285

@@ -121,8 +124,8 @@ namespace libcron
121124
std::set<DayOfWeek> day_of_week{};
122125
bool valid = false;
123126

124-
std::vector<std::string> month_names;
125-
std::vector<std::string> day_names;
127+
static const std::vector<std::string> month_names;
128+
static const std::vector<std::string> day_names;
126129

127130
template<typename T>
128131
void add_full_range(std::set<T>& set);
@@ -343,4 +346,40 @@ namespace libcron
343346

344347
return res;
345348
}
349+
350+
template<typename T>
351+
std::string & CronData::replace_string_name_with_numeric(std::string& s)
352+
{
353+
auto value = static_cast<int>(T::First);
354+
355+
const std::vector<std::string>* name_source{};
356+
357+
static_assert(std::is_same<T, libcron::Months>()
358+
|| std::is_same<T, libcron::DayOfWeek>(),
359+
"T must be either Months or DayOfWeek");
360+
361+
if constexpr (std::is_same<T, libcron::Months>())
362+
{
363+
name_source = &month_names;
364+
}
365+
else
366+
{
367+
name_source = &day_names;
368+
}
369+
370+
for (const auto& name : *name_source)
371+
{
372+
std::regex m(name, std::regex_constants::ECMAScript | std::regex_constants::icase);
373+
374+
std::string replaced;
375+
376+
std::regex_replace(std::back_inserter(replaced), s.begin(), s.end(), m, std::to_string(value));
377+
378+
s = replaced;
379+
380+
++value;
381+
}
382+
383+
return s;
384+
}
346385
}

libcron/include/libcron/CronRandomization.h

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ namespace libcron
4444

4545
std::smatch random_match;
4646

47-
if (std::regex_match(section.begin(), section.end(), random_match, rand_expression))
47+
if (std::regex_match(section.cbegin(), section.cend(), random_match, rand_expression))
4848
{
4949
// Random range, get left and right numbers.
5050
auto left = std::stoi(random_match[1].str());

libcron/src/CronData.cpp

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,9 @@ namespace libcron
1313
Months::October,
1414
Months::December };
1515

16+
const std::vector<std::string> CronData::month_names{ "JAN", "FEB", "MAR", "APR", "MAY", "JUN", "JUL", "AUG", "SEP", "OCT", "NOV", "DEC" };
17+
const std::vector<std::string> CronData::day_names{ "SUN", "MON", "TUE", "WED", "THU", "FRI", "SAT" };
18+
1619
CronData CronData::create(const std::string& cron_expression)
1720
{
1821
CronData c;
@@ -21,12 +24,6 @@ namespace libcron
2124
return c;
2225
}
2326

24-
CronData::CronData()
25-
: month_names({ "JAN", "FEB", "MAR", "APR", "MAY", "JUN", "JUL", "AUG", "SEP", "OCT", "NOV", "DEC" }),
26-
day_names({ "SUN", "MON", "TUE", "WED", "THU", "FRI", "SAT" })
27-
{
28-
}
29-
3027
void CronData::parse(const std::string& cron_expression)
3128
{
3229
// First, split on white-space. We expect six parts.

libcron/src/CronRandomization.cpp

Lines changed: 36 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,14 +18,46 @@ namespace libcron
1818
std::tuple<bool, std::string> CronRandomization::parse(const std::string& cron_schedule)
1919
{
2020
// Split on space to get each separate part, six parts expected
21-
std::regex split{ R"#(^\s*(.*?)\s+(.*?)\s+(.*?)\s+(.*?)\s+(.*?)\s+(.*?)\s*$)#",
22-
std::regex_constants::ECMAScript };
21+
const std::regex split{ R"#(^\s*(.*?)\s+(.*?)\s+(.*?)\s+(.*?)\s+(.*?)\s+(.*?)\s*$)#",
22+
std::regex_constants::ECMAScript };
2323

2424
std::smatch all_sections;
25+
auto res = std::regex_match(cron_schedule.cbegin(), cron_schedule.cend(), all_sections, split);
2526

26-
std::string final_cron_schedule;
27+
// Replace text with numbers
28+
std::string working_copy{};
2729

28-
auto res = std::regex_match(cron_schedule.begin(), cron_schedule.end(), all_sections, split);
30+
if (res)
31+
{
32+
// Merge seconds, minutes, hours and day of month back together
33+
working_copy += all_sections[1].str();
34+
working_copy += " ";
35+
working_copy += all_sections[2].str();
36+
working_copy += " ";
37+
working_copy += all_sections[3].str();
38+
working_copy += " ";
39+
working_copy += all_sections[4].str();
40+
working_copy += " ";
41+
42+
// Replace month names
43+
auto month = all_sections[5].str();
44+
CronData::replace_string_name_with_numeric<libcron::Months>(month);
45+
46+
working_copy += " ";
47+
working_copy += month;
48+
49+
// Replace day names
50+
auto dow = all_sections[6].str();
51+
CronData::replace_string_name_with_numeric<libcron::DayOfWeek>(dow);
52+
53+
working_copy += " ";
54+
working_copy += dow;
55+
}
56+
57+
std::string final_cron_schedule{};
58+
59+
// Split again on space
60+
res = res && std::regex_match(working_copy.cbegin(), working_copy.cend(), all_sections, split);
2961

3062
if (res)
3163
{

test/CMakeLists.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
cmake_minimum_required(VERSION 3.6)
22
project(cron_test)
33

4-
set(CMAKE_CXX_STANDARD 14)
4+
set(CMAKE_CXX_STANDARD 17)
55

66
if( MSVC )
77
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /W4")

test/CronDataTest.cpp

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -224,4 +224,17 @@ SCENARIO("Dates that does not exist")
224224
SCENARIO("Date that exist in one of the months")
225225
{
226226
REQUIRE(CronData::create("0 0 * 31 APR,MAY ?").is_valid());
227+
}
228+
229+
SCENARIO("Replacing text with numbers")
230+
{
231+
{
232+
std::string s = "SUN-TUE";
233+
REQUIRE(CronData::replace_string_name_with_numeric<libcron::DayOfWeek>(s) == "0-2");
234+
}
235+
236+
{
237+
std::string s = "JAN-DEC";
238+
REQUIRE(CronData::replace_string_name_with_numeric<libcron::Months>(s) == "1-12");
239+
}
227240
}

test/CronRandomizationTest.cpp

Lines changed: 80 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,21 +7,31 @@
77
#include <iostream>
88

99
using namespace libcron;
10+
const auto EXPECT_FAILURE = true;
1011

11-
void test(const char* const random_schedule)
12+
void test(const char* const random_schedule, bool expect_failure = false)
1213
{
1314
libcron::CronRandomization cr;
14-
std::unordered_map<int, std::unordered_map<int, int>> results{};
1515

1616
for (int i = 0; i < 5000; ++i)
1717
{
1818
auto res = cr.parse(random_schedule);
19-
REQUIRE(std::get<0>(res));
2019
auto schedule = std::get<1>(res);
2120

22-
INFO("schedule:" << schedule);
2321
Cron<> cron;
24-
REQUIRE(cron.add_schedule("validate schedule", schedule, []() {}));
22+
23+
if(expect_failure)
24+
{
25+
// Parsing of random might succeed, but it yields an invalid schedule.
26+
auto r = std::get<0>(res) && cron.add_schedule("validate schedule", schedule, []() {});
27+
REQUIRE_FALSE(r);
28+
}
29+
else
30+
{
31+
REQUIRE(std::get<0>(res));
32+
REQUIRE(cron.add_schedule("validate schedule", schedule, []() {}));
33+
34+
}
2535
}
2636
}
2737

@@ -103,3 +113,68 @@ SCENARIO("Test readme examples")
103113
}
104114
}
105115
}
116+
117+
SCENARIO("Randomization using text versions of days and months")
118+
{
119+
GIVEN("0 0 0 ? * R(TUE-FRI)")
120+
{
121+
THEN("Valid schedule generated")
122+
{
123+
test("0 0 0 ? * R(TUE-FRI)");
124+
}
125+
}
126+
127+
GIVEN("Valid schedule")
128+
{
129+
THEN("Valid schedule generated")
130+
{
131+
test("0 0 0 ? R(JAN-DEC) R(MON-FRI)");
132+
}
133+
AND_WHEN("Given 0 0 0 ? R(DEC-MAR) R(SAT-SUN)")
134+
{
135+
THEN("Valid schedule generated")
136+
{
137+
test("0 0 0 ? R(DEC-MAR) R(SAT-SUN)");
138+
}
139+
}
140+
AND_THEN("Given 0 0 0 ? R(JAN-FEB) *")
141+
{
142+
THEN("Valid schedule generated")
143+
{
144+
test("0 0 0 ? R(JAN-FEB) *");
145+
}
146+
}
147+
AND_THEN("Given 0 0 0 ? R(OCT-OCT) *")
148+
{
149+
THEN("Valid schedule generated")
150+
{
151+
test("0 0 0 ? R(OCT-OCT) *");
152+
}
153+
}
154+
}
155+
156+
GIVEN("Invalid schedule")
157+
{
158+
THEN("No schedule generated")
159+
{
160+
// Day of month specified - not allowed with day of week
161+
test("0 0 0 1 R(JAN-DEC) R(MON-SUN)", EXPECT_FAILURE);
162+
}
163+
AND_THEN("No schedule generated")
164+
{
165+
// Invalid range
166+
test("0 0 0 ? R(JAN) *", EXPECT_FAILURE);
167+
}
168+
AND_THEN("No schedule generated")
169+
{
170+
// Days in month field
171+
test("0 0 0 ? R(MON-TUE) *", EXPECT_FAILURE);
172+
}
173+
AND_THEN("No schedule generated")
174+
{
175+
// Month in day field
176+
test("0 0 0 ? * R(JAN-JUN)", EXPECT_FAILURE);
177+
}
178+
179+
}
180+
}

0 commit comments

Comments
 (0)