diff --git a/.idea/codeStyles/Project.xml b/.idea/codeStyles/Project.xml
deleted file mode 100644
index 30aa626..0000000
--- a/.idea/codeStyles/Project.xml
+++ /dev/null
@@ -1,29 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/.idea/vcs.xml b/.idea/vcs.xml
index 94a25f7..7d34eb9 100644
--- a/.idea/vcs.xml
+++ b/.idea/vcs.xml
@@ -2,5 +2,6 @@
+
\ No newline at end of file
diff --git a/CMakeLists.txt b/CMakeLists.txt
index 1c6e4df..051ba98 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -1,7 +1,5 @@
cmake_minimum_required(VERSION 3.6)
-set(OUTPUT_LOCATION ${CMAKE_CURRENT_LIST_DIR}/out/)
-
add_subdirectory(libcron)
add_subdirectory(test)
diff --git a/README.md b/README.md
index 24f4f1b..8efbbde 100644
--- a/README.md
+++ b/README.md
@@ -56,18 +56,33 @@ Each part is separated by one or more whitespaces. It is thus important to keep
* 0, 3, 40-50 * * * * ?
`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 '*'.
+the '?'-character to ensure that it is not possible to specify a statement which results in an impossible mix of these fields.
-# Examples
+## Examples
|Expression | Meaning
| --- | --- |
| * * * * * ? | Every second
-|0 0 12 * * MON-FRI | Every Weekday at noon
-|0 0 12 1/2 * ? | Every 2 days, starting on the 1st at noon
+| 0 0 12 * * MON-FRI | Every Weekday at noon
+| 0 0 12 1/2 * ? | Every 2 days, starting on the 1st at noon
| 0 0 */12 ? * * | Every twelve hours
-# Third party libraries
+# Randomization
+
+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.
+
+## Examples
+|Expression | Meaning
+| --- | --- |
+| 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.
+
+
+# Used Third party libraries
Howard Hinnant's [date libraries](https://github.com/HowardHinnant/date/)
diff --git a/libcron/CMakeLists.txt b/libcron/CMakeLists.txt
index 7623cf1..51992ba 100644
--- a/libcron/CMakeLists.txt
+++ b/libcron/CMakeLists.txt
@@ -9,22 +9,26 @@ else()
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wall -Wextra -Wpedantic")
endif()
-include_directories(${CMAKE_CURRENT_LIST_DIR}/externals/date/include)
-
add_library(${PROJECT_NAME}
- Cron.h
- Task.h
- CronData.h
- TimeTypes.h
- CronData.cpp
- CronSchedule.cpp
- CronSchedule.h
- DateTime.h
- Task.cpp
- CronClock.h
- CronClock.cpp)
+ include/libcron/Cron.h
+ include/libcron/CronClock.h
+ include/libcron/CronData.h
+ include/libcron/CronRandomization.h
+ include/libcron/CronSchedule.h
+ include/libcron/DateTime.h
+ include/libcron/Task.h
+ include/libcron/TimeTypes.h
+ src/CronClock.cpp
+ src/CronData.cpp
+ src/CronRandomization.cpp
+ src/CronSchedule.cpp
+ src/Task.cpp)
+
+target_include_directories(${PROJECT_NAME}
+ PRIVATE ${CMAKE_CURRENT_LIST_DIR}/externals/date/include
+ PUBLIC include)
set_target_properties(${PROJECT_NAME} PROPERTIES
- ARCHIVE_OUTPUT_DIRECTORY "${OUTPUT_LOCATION}"
- LIBRARY_OUTPUT_DIRECTORY "${OUTPUT_LOCATION}"
- RUNTIME_OUTPUT_DIRECTORY "${OUTPUT_LOCATION}")
\ No newline at end of file
+ ARCHIVE_OUTPUT_DIRECTORY "${CMAKE_CURRENT_LIST_DIR}/out"
+ LIBRARY_OUTPUT_DIRECTORY "${CMAKE_CURRENT_LIST_DIR}/out"
+ RUNTIME_OUTPUT_DIRECTORY "${CMAKE_CURRENT_LIST_DIR}/out")
\ No newline at end of file
diff --git a/libcron/TimeTypes.h b/libcron/TimeTypes.h
deleted file mode 100644
index a3238d3..0000000
--- a/libcron/TimeTypes.h
+++ /dev/null
@@ -1,43 +0,0 @@
-#pragma once
-
-#include
-
-namespace libcron
-{
- enum class Seconds : int8_t
- {
- First = 0,
- Last = 59
- };
-
- enum class Minutes : int8_t
- {
- First = 0,
- Last = 59
- };
-
- enum class Hours : int8_t
- {
- First = 0,
- Last = 23
- };
-
- enum class DayOfMonth : uint8_t
- {
- First = 1,
- Last = 31
- };
-
- enum class Months : uint8_t
- {
- First = 1,
- Last = 12
- };
-
- enum class DayOfWeek : uint8_t
- {
- // Sunday = 0 ... Saturday = 6
- First = 0,
- Last = 6,
- };
-}
diff --git a/libcron/Cron.h b/libcron/include/libcron/Cron.h
similarity index 100%
rename from libcron/Cron.h
rename to libcron/include/libcron/Cron.h
diff --git a/libcron/CronClock.h b/libcron/include/libcron/CronClock.h
similarity index 100%
rename from libcron/CronClock.h
rename to libcron/include/libcron/CronClock.h
diff --git a/libcron/CronData.h b/libcron/include/libcron/CronData.h
similarity index 76%
rename from libcron/CronData.h
rename to libcron/include/libcron/CronData.h
index 4f83c65..5437b87 100644
--- a/libcron/CronData.h
+++ b/libcron/include/libcron/CronData.h
@@ -4,16 +4,20 @@
#include
#include
#include
-#include "TimeTypes.h"
+#include
namespace libcron
{
class CronData
{
public:
+ static const int NUMBER_OF_LONG_MONTHS = 7;
+ static const libcron::Months months_with_31[NUMBER_OF_LONG_MONTHS];
+
static CronData create(const std::string& cron_expression);
CronData();
+
CronData(const CronData&) = default;
bool is_valid() const
@@ -61,6 +65,7 @@ namespace libcron
static bool has_any_in_range(const std::set& set, uint8_t low, uint8_t high)
{
bool found = false;
+
for (auto i = low; !found && i <= high; ++i)
{
found |= set.find(static_cast(i)) != set.end();
@@ -69,8 +74,10 @@ namespace libcron
return found;
}
- private:
+ template
+ bool convert_from_string_range_to_number_range(const std::string& range, std::set& numbers);
+ private:
void parse(const std::string& cron_expression);
template
@@ -142,20 +149,20 @@ namespace libcron
for (const auto& name : names)
{
std::regex m(name, std::regex_constants::ECMAScript | std::regex_constants::icase);
- for (size_t i = 0; i < parts.size(); ++i)
+
+ for (auto& part : parts)
{
std::string replaced;
- std::regex_replace(std::back_inserter(replaced), parts[i].begin(), parts[i].end(), m,
+ std::regex_replace(std::back_inserter(replaced), part.begin(), part.end(), m,
std::to_string(value_of_first_name));
- parts[i] = replaced;
+ part = replaced;
}
value_of_first_name++;
}
return process_parts(parts, numbers);
-
}
template
@@ -163,60 +170,9 @@ namespace libcron
{
bool res = true;
- T left;
- T right;
- uint8_t step_start;
- uint8_t step;
-
for (const auto& p : parts)
{
- if (p == "*" || p == "?")
- {
- // We treat the ignore-character '?' the same as the full range being allowed.
- add_full_range(numbers);
- }
- else if (is_number(p))
- {
- res &= add_number(numbers, std::stoi(p));
- }
- else if (get_range(p, left, right))
- {
- // A range can be written as both 1-22 or 22-1, meaning totally different ranges.
- // First case is 1...22 while 22-1 is only four hours: 22, 23, 0, 1.
- if (left <= right)
- {
- for (auto v = value_of(left); v <= value_of(right); ++v)
- {
- res &= add_number(numbers, v);
- }
- }
- else
- {
- // 'left' and 'right' are not in value order. First, get values between 'left' and T::Last, inclusive
- for (auto v = value_of(left); v <= value_of(T::Last); ++v)
- {
- res &= add_number(numbers, v);
- }
-
- // Next, get values between T::First and 'right', inclusive.
- for (auto v = value_of(T::First); v <= value_of(right); ++v)
- {
- res &= add_number(numbers, v);
- }
- }
- }
- else if (get_step(p, step_start, step))
- {
- // Add from step_start to T::Last with a step of 'step'
- for (auto v = step_start; v <= value_of(T::Last); v += step)
- {
- res &= add_number(numbers, v);
- }
- }
- else
- {
- res = false;
- }
+ res &= convert_from_string_range_to_number_range(p, numbers);
}
return res;
@@ -235,8 +191,8 @@ namespace libcron
if (std::regex_match(s.begin(), s.end(), match, range))
{
- auto left = std::stoi(match[1].str().c_str());
- auto right = std::stoi(match[2].str().c_str());
+ auto left = std::stoi(match[1].str());
+ auto right = std::stoi(match[2].str());
if (is_within_limits(left, right))
{
@@ -263,16 +219,17 @@ namespace libcron
if (std::regex_match(s.begin(), s.end(), match, range))
{
int raw_start;
- if(match[1].str() == "*")
+
+ if (match[1].str() == "*")
{
raw_start = value_of(T::First);
}
else
{
- raw_start = std::stoi(match[1].str().c_str());
+ raw_start = std::stoi(match[1].str());
}
- auto raw_step = std::stoi(match[2].str().c_str());
+ auto raw_step = std::stoi(match[2].str());
if (is_within_limits(raw_start, raw_start) && raw_step > 0)
{
@@ -326,5 +283,64 @@ namespace libcron
&& is_between(high, value_of(T::First), value_of(T::Last));
}
+ template
+ bool CronData::convert_from_string_range_to_number_range(const std::string& range, std::set& numbers)
+ {
+ T left;
+ T right;
+ uint8_t step_start;
+ uint8_t step;
+ bool res = true;
+
+ if (range == "*" || range == "?")
+ {
+ // We treat the ignore-character '?' the same as the full range being allowed.
+ add_full_range(numbers);
+ }
+ else if (is_number(range))
+ {
+ res = add_number(numbers, std::stoi(range));
+ }
+ else if (get_range(range, left, right))
+ {
+ // A range can be written as both 1-22 or 22-1, meaning totally different ranges.
+ // First case is 1...22 while 22-1 is only four hours: 22, 23, 0, 1.
+ if (left <= right)
+ {
+ for (auto v = value_of(left); v <= value_of(right); ++v)
+ {
+ res &= add_number(numbers, v);
+ }
+ }
+ else
+ {
+ // 'left' and 'right' are not in value order. First, get values between 'left' and T::Last, inclusive
+ for (auto v = value_of(left); v <= value_of(T::Last); ++v)
+ {
+ res = add_number(numbers, v);
+ }
+
+ // Next, get values between T::First and 'right', inclusive.
+ for (auto v = value_of(T::First); v <= value_of(right); ++v)
+ {
+ res = add_number(numbers, v);
+ }
+ }
+ }
+ else if (get_step(range, step_start, step))
+ {
+ // Add from step_start to T::Last with a step of 'step'
+ for (auto v = step_start; v <= value_of(T::Last); v += step)
+ {
+ res = add_number(numbers, v);
+ }
+ }
+ else
+ {
+ res = false;
+ }
+
+ return res;
+ }
}
diff --git a/libcron/include/libcron/CronRandomization.h b/libcron/include/libcron/CronRandomization.h
new file mode 100644
index 0000000..759995e
--- /dev/null
+++ b/libcron/include/libcron/CronRandomization.h
@@ -0,0 +1,100 @@
+#pragma once
+
+#include
+#include
+#include
+#include
+#include "CronData.h"
+
+namespace libcron
+{
+ class CronRandomization
+ {
+ public:
+ std::tuple parse(const std::string& cron_schedule);
+
+ CronRandomization();
+
+ CronRandomization(const CronRandomization&) = delete;
+
+ CronRandomization & operator=(const CronRandomization &) = delete;
+
+ private:
+ template
+ std::pair get_random_in_range(const std::string& section,
+ int& selected_value,
+ std::pair limit = std::make_pair(-1, -1));
+
+ std::pair day_limiter(const std::set& month);
+
+ int cap(int value, int lower, int upper);
+
+ std::regex const rand_expression{ R"#([rR]\((\d+)\-(\d+)\))#", std::regex_constants::ECMAScript };
+ std::random_device rd{};
+ std::mt19937 twister;
+ };
+
+ template
+ std::pair CronRandomization::get_random_in_range(const std::string& section,
+ int& selected_value,
+ std::pair limit)
+ {
+ auto res = std::make_pair(true, std::string{});
+ selected_value = -1;
+
+ std::smatch random_match;
+
+ if (std::regex_match(section.begin(), section.end(), random_match, rand_expression))
+ {
+ // Random range, get left and right numbers.
+ auto left = std::stoi(random_match[1].str());
+ auto right = std::stoi(random_match[2].str());
+
+ if (limit.first != -1 && limit.second != -1)
+ {
+ left = cap(left, limit.first, limit.second);
+ right = cap(right, limit.first, limit.second);
+ }
+
+ libcron::CronData cd;
+ std::set numbers;
+ res.first = cd.convert_from_string_range_to_number_range(
+ std::to_string(left) + "-" + std::to_string(right), numbers);
+
+ // Remove items outside limits.
+ if (limit.first != -1 && limit.second != -1)
+ {
+ for (auto it = numbers.begin(); it != numbers.end(); )
+ {
+ if (CronData::value_of(*it) < limit.first || CronData::value_of(*it) > limit.second)
+ {
+ it = numbers.erase(it);
+ }
+ else
+ {
+ ++it;
+ }
+ }
+ }
+
+ if (res.first)
+ {
+ // Generate random indexes to select one of the numbers in the range.
+ std::uniform_int_distribution<> dis(0, static_cast(numbers.size() - 1));
+
+ // Select the random number to use as the schedule
+ auto it = numbers.begin();
+ std::advance(it, dis(twister));
+ selected_value = CronData::value_of(*it);
+ res.second = std::to_string(selected_value);
+ }
+ }
+ else
+ {
+ // Not random, just append input to output.
+ res.second = section;
+ }
+
+ return res;
+ }
+}
diff --git a/libcron/CronSchedule.h b/libcron/include/libcron/CronSchedule.h
similarity index 96%
rename from libcron/CronSchedule.h
rename to libcron/include/libcron/CronSchedule.h
index e619167..1c6fe97 100644
--- a/libcron/CronSchedule.h
+++ b/libcron/include/libcron/CronSchedule.h
@@ -1,17 +1,17 @@
#pragma once
-#include "CronData.h"
+#include "libcron/CronData.h"
#include
-#ifdef _WIN32
-#pragma warning(push)
-#pragma warning(disable:4244)
-#endif
-#include
-#ifdef _WIN32
-#pragma warning(pop)
+#ifdef _WIN32
+#pragma warning(push)
+#pragma warning(disable:4244)
+#endif
+#include
+#ifdef _WIN32
+#pragma warning(pop)
#endif
-#include "DateTime.h"
+#include "libcron/DateTime.h"
namespace libcron
{
diff --git a/libcron/DateTime.h b/libcron/include/libcron/DateTime.h
similarity index 100%
rename from libcron/DateTime.h
rename to libcron/include/libcron/DateTime.h
diff --git a/libcron/Task.h b/libcron/include/libcron/Task.h
similarity index 100%
rename from libcron/Task.h
rename to libcron/include/libcron/Task.h
diff --git a/libcron/include/libcron/TimeTypes.h b/libcron/include/libcron/TimeTypes.h
new file mode 100644
index 0000000..18aa133
--- /dev/null
+++ b/libcron/include/libcron/TimeTypes.h
@@ -0,0 +1,55 @@
+#pragma once
+
+#include
+
+namespace libcron
+{
+ enum class Seconds : int8_t
+ {
+ First = 0,
+ Last = 59
+ };
+
+ enum class Minutes : int8_t
+ {
+ First = 0,
+ Last = 59
+ };
+
+ enum class Hours : int8_t
+ {
+ First = 0,
+ Last = 23
+ };
+
+ enum class DayOfMonth : uint8_t
+ {
+ First = 1,
+ Last = 31
+ };
+
+ enum class Months : uint8_t
+ {
+ First = 1,
+ January = First,
+ February,
+ March,
+ April,
+ May,
+ June,
+ July,
+ August,
+ September,
+ October,
+ November,
+ December = 12,
+ Last = December
+ };
+
+ enum class DayOfWeek : uint8_t
+ {
+ // Sunday = 0 ... Saturday = 6
+ First = 0,
+ Last = 6,
+ };
+}
diff --git a/libcron/CronClock.cpp b/libcron/src/CronClock.cpp
similarity index 96%
rename from libcron/CronClock.cpp
rename to libcron/src/CronClock.cpp
index 2933777..9aeb00f 100644
--- a/libcron/CronClock.cpp
+++ b/libcron/src/CronClock.cpp
@@ -1,8 +1,8 @@
-#include "CronClock.h"
+#include "libcron/CronClock.h"
#ifdef WIN32
-#ifndef NOMINMAX
-#define NOMINMAX
+#ifndef NOMINMAX
+#define NOMINMAX
#endif
#define WIN32_LEAN_AND_MEAN
#include
diff --git a/libcron/CronData.cpp b/libcron/src/CronData.cpp
similarity index 72%
rename from libcron/CronData.cpp
rename to libcron/src/CronData.cpp
index e7f4082..6ee9dbc 100644
--- a/libcron/CronData.cpp
+++ b/libcron/src/CronData.cpp
@@ -1,10 +1,17 @@
#include
-#include "CronData.h"
+#include "libcron/CronData.h"
using namespace date;
namespace libcron
{
+ const constexpr Months CronData::months_with_31[NUMBER_OF_LONG_MONTHS] = { Months::January,
+ Months::March,
+ Months::May,
+ Months::July,
+ Months::August,
+ Months::October,
+ Months::December };
CronData CronData::create(const std::string& cron_expression)
{
@@ -15,16 +22,16 @@ namespace libcron
}
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"})
+ : 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::regex split{ R"#(^\s*(.*?)\s+(.*?)\s+(.*?)\s+(.*?)\s+(.*?)\s+(.*?)\s*$)#",
+ std::regex_constants::ECMAScript };
std::smatch match;
@@ -48,13 +55,12 @@ namespace libcron
std::string r = "[";
r += token;
r += "]";
- std::regex splitter{r, std::regex_constants::ECMAScript};
+ 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;
}
@@ -90,25 +96,15 @@ namespace libcron
// 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)
+
+ for (size_t i = 0; !res && i < NUMBER_OF_LONG_MONTHS; ++i)
{
- res = months.find(static_cast(months_with_31[i])) != months.end();
+ res = months.find(months_with_31[i]) != months.end();
}
}
}
-
return res;
}
@@ -122,12 +118,12 @@ namespace libcron
// '?' as the ignore flag, although it is functionally equivalent to '*'.
auto check = [](const std::string& l, std::string r)
- {
- return l == "*" && (r != "*" || r == "?");
- };
+ {
+ return l == "*" && (r != "*" || r == "?");
+ };
return (dom == "?" || dow == "?")
|| check(dom, dow)
|| check(dow, dom);
}
-}
\ No newline at end of file
+}
diff --git a/libcron/src/CronRandomization.cpp b/libcron/src/CronRandomization.cpp
new file mode 100644
index 0000000..144fbcb
--- /dev/null
+++ b/libcron/src/CronRandomization.cpp
@@ -0,0 +1,108 @@
+#include
+
+#include
+#include