#1 - Randomization WiP.

This commit is contained in:
Per Malmberg 2019-03-14 17:09:25 +01:00
parent 6ed4bc3b2e
commit 70f55b8ce6
10 changed files with 416 additions and 89 deletions

View File

@ -1,7 +1,5 @@
cmake_minimum_required(VERSION 3.6) cmake_minimum_required(VERSION 3.6)
set(OUTPUT_LOCATION ${CMAKE_CURRENT_LIST_DIR}/out/)
add_subdirectory(libcron) add_subdirectory(libcron)
add_subdirectory(test) add_subdirectory(test)

View File

@ -13,18 +13,20 @@ add_library(${PROJECT_NAME}
include/libcron/Cron.h include/libcron/Cron.h
include/libcron/CronClock.h include/libcron/CronClock.h
include/libcron/CronData.h include/libcron/CronData.h
include/libcron/CronRandomization.h
include/libcron/CronSchedule.h include/libcron/CronSchedule.h
include/libcron/DateTime.h include/libcron/DateTime.h
include/libcron/Task.h include/libcron/Task.h
include/libcron/TimeTypes.h include/libcron/TimeTypes.h
src/CronClock.cpp src/CronClock.cpp
src/CronData.cpp src/CronData.cpp
src/CronRandomization.cpp
src/CronSchedule.cpp src/CronSchedule.cpp
src/Task.cpp) src/Task.cpp)
target_include_directories(${PROJECT_NAME} target_include_directories(${PROJECT_NAME}
PRIVATE ${CMAKE_CURRENT_LIST_DIR}/externals/date/include PRIVATE ${CMAKE_CURRENT_LIST_DIR}/externals/date/include
PUBLIC ${CMAKE_CURRENT_LIST_DIR}/include) PUBLIC include)
set_target_properties(${PROJECT_NAME} PROPERTIES set_target_properties(${PROJECT_NAME} PROPERTIES
ARCHIVE_OUTPUT_DIRECTORY "${CMAKE_CURRENT_LIST_DIR}/out" ARCHIVE_OUTPUT_DIRECTORY "${CMAKE_CURRENT_LIST_DIR}/out"

View File

@ -11,9 +11,12 @@ namespace libcron
class CronData class CronData
{ {
public: public:
static const std::array<Months, 7> months_with_31;
static CronData create(const std::string& cron_expression); static CronData create(const std::string& cron_expression);
CronData(); CronData();
CronData(const CronData&) = default; CronData(const CronData&) = default;
bool is_valid() const bool is_valid() const
@ -61,6 +64,7 @@ namespace libcron
static bool has_any_in_range(const std::set<T>& set, uint8_t low, uint8_t high) static bool has_any_in_range(const std::set<T>& set, uint8_t low, uint8_t high)
{ {
bool found = false; bool found = false;
for (auto i = low; !found && i <= high; ++i) for (auto i = low; !found && i <= high; ++i)
{ {
found |= set.find(static_cast<T>(i)) != set.end(); found |= set.find(static_cast<T>(i)) != set.end();
@ -69,8 +73,10 @@ namespace libcron
return found; return found;
} }
private: template<typename T>
bool convert_from_string_range_to_number_range(const std::string& range, std::set<T>& numbers);
private:
void parse(const std::string& cron_expression); void parse(const std::string& cron_expression);
template<typename T> template<typename T>
@ -142,6 +148,7 @@ namespace libcron
for (const auto& name : names) for (const auto& name : names)
{ {
std::regex m(name, std::regex_constants::ECMAScript | std::regex_constants::icase); std::regex m(name, std::regex_constants::ECMAScript | std::regex_constants::icase);
for (auto& part : parts) for (auto& part : parts)
{ {
std::string replaced; std::string replaced;
@ -155,7 +162,6 @@ namespace libcron
} }
return process_parts(parts, numbers); return process_parts(parts, numbers);
} }
template<typename T> template<typename T>
@ -163,60 +169,9 @@ namespace libcron
{ {
bool res = true; bool res = true;
T left;
T right;
uint8_t step_start;
uint8_t step;
for (const auto& p : parts) for (const auto& p : parts)
{ {
if (p == "*" || p == "?") res &= convert_from_string_range_to_number_range(p, numbers);
{
// We treat the ignore-character '?' the same as the full range being allowed.
add_full_range<T>(numbers);
}
else if (is_number(p))
{
res &= add_number<T>(numbers, std::stoi(p));
}
else if (get_range<T>(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<T>(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;
}
} }
return res; return res;
@ -263,6 +218,7 @@ namespace libcron
if (std::regex_match(s.begin(), s.end(), match, range)) if (std::regex_match(s.begin(), s.end(), match, range))
{ {
int raw_start; int raw_start;
if (match[1].str() == "*") if (match[1].str() == "*")
{ {
raw_start = value_of(T::First); raw_start = value_of(T::First);
@ -326,5 +282,64 @@ namespace libcron
&& is_between(high, value_of(T::First), value_of(T::Last)); && is_between(high, value_of(T::First), value_of(T::Last));
} }
template<typename T>
bool CronData::convert_from_string_range_to_number_range(const std::string& range, std::set<T>& 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<T>(numbers);
}
else if (is_number(range))
{
res = add_number<T>(numbers, std::stoi(range));
}
else if (get_range<T>(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<T>(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;
}
} }

View File

@ -0,0 +1,96 @@
#pragma once
#include <tuple>
#include <random>
#include <regex>
#include <functional>
#include "CronData.h"
namespace libcron
{
class CronRandomization
{
public:
std::tuple<bool, std::string> parse(const std::string& cron_schedule);
CronRandomization();
CronRandomization(const CronRandomization&) = delete;
CronRandomization & operator=(const CronRandomization &) = delete;
private:
template<typename T>
std::pair<bool, std::string> get_random_in_range(const std::string& section,
int& selected_value,
std::pair<int, int> limit = std::make_pair(-1, -1));
std::pair<int, int> day_limiter(const std::set<Months>& 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<typename T>
std::pair<bool, std::string> CronRandomization::get_random_in_range(const std::string& section,
int& selected_value,
std::pair<int, int> 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<T> numbers;
res.first = cd.convert_from_string_range_to_number_range<T>(
std::to_string(left) + "-" + std::to_string(right), numbers);
// Remove items outside limits.
// 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<int>(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;
}
}

View File

@ -31,7 +31,19 @@ namespace libcron
enum class Months : uint8_t enum class Months : uint8_t
{ {
First = 1, First = 1,
Last = 12 January = First,
February,
March,
April,
May,
June,
July,
August,
September,
October,
November,
December = 12,
Last = December
}; };
enum class DayOfWeek : uint8_t enum class DayOfWeek : uint8_t

View File

@ -5,6 +5,13 @@ using namespace date;
namespace libcron namespace libcron
{ {
const constexpr std::array<Months, 7> CronData::months_with_31{ Months::January,
Months::March,
Months::May,
Months::July,
Months::August,
Months::October,
Months::December };
CronData CronData::create(const std::string& cron_expression) CronData CronData::create(const std::string& cron_expression)
{ {
@ -54,7 +61,6 @@ namespace libcron
std::sregex_token_iterator(), std::sregex_token_iterator(),
std::back_inserter(res)); std::back_inserter(res));
return res; return res;
} }
@ -90,17 +96,15 @@ namespace libcron
// Make sure that if the days contains only 31, at least one month allows that date. // 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()) if (day_of_month.size() == 1 && day_of_month.find(DayOfMonth::Last) != day_of_month.end())
{ {
constexpr std::array<uint32_t, 7> months_with_31{1, 3, 5, 7, 8, 10, 12};
res = false; res = false;
for (size_t i = 0; !res && i < months_with_31.size(); ++i) for (size_t i = 0; !res && i < months_with_31.size(); ++i)
{ {
res = months.find(static_cast<Months>(months_with_31[i])) != months.end(); res = months.find(months_with_31[i]) != months.end();
} }
} }
} }
return res; return res;
} }

View File

@ -0,0 +1,110 @@
#include <libcron/CronRandomization.h>
#include <regex>
#include <map>
#include <array>
#include <algorithm>
#include <iterator>
#include <libcron/TimeTypes.h>
#include <libcron/CronData.h>
namespace libcron
{
CronRandomization::CronRandomization()
: twister(rd())
{
}
std::tuple<bool, std::string> 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 };
std::smatch all_sections;
std::string final_cron_schedule;
auto res = std::regex_match(cron_schedule.begin(), cron_schedule.end(), all_sections, split);
if (res)
{
int selected_value = -1;
auto second = get_random_in_range<Seconds>(all_sections[1].str(), selected_value);
res = second.first;
final_cron_schedule = second.second;
auto minute = get_random_in_range<Minutes>(all_sections[2].str(), selected_value);
res &= minute.first;
final_cron_schedule += " " + minute.second;
auto hour = get_random_in_range<Hours>(all_sections[3].str(), selected_value);
res &= hour.first;
final_cron_schedule += " " + hour.second;
// Do Month before DayOfMonth to allow capping the allowed range.
auto month = get_random_in_range<Months>(all_sections[5].str(), selected_value);
res &= month.first;
std::set<Months> month_range{};
if (selected_value == -1)
{
// Month is not specific, we need to get the 'max minimum' day of month to use.
CronData cr;
res &= cr.convert_from_string_range_to_number_range<Months>(all_sections[5].str(), month_range);
}
else
{
month_range.emplace(static_cast<Months>(selected_value));
}
auto limits = day_limiter(month_range);
auto day_of_month = get_random_in_range<DayOfMonth>(all_sections[4].str(),
selected_value,
limits);
res &= day_of_month.first;
final_cron_schedule += " " + day_of_month.second + " " + month.second;
auto day_of_week = get_random_in_range<DayOfWeek>(all_sections[6].str(), selected_value);
res &= day_of_week.first;
final_cron_schedule += " " + day_of_week.second;
}
return { res, final_cron_schedule };
}
std::pair<int, int> CronRandomization::day_limiter(const std::set<Months>& months)
{
auto max = 31;
for (auto month : months)
{
if (month == Months::February)
{
// Limit to 29 days, possibly causing delaying schedule until next leap year.
max = std::min(max, 29);
}
else if (std::find(CronData::months_with_31.begin(),
CronData::months_with_31.end(),
month) == CronData::months_with_31.end())
{
max = std::min(max, 30);
}
else
{
max = std::min(max, 31);
}
}
auto res = std::pair<int, int>{ CronData::value_of(DayOfMonth::First), max };
return res;
}
int CronRandomization::cap(int value, int lower, int upper)
{
return std::max(std::min(value, upper), lower);
}
}

View File

@ -18,11 +18,13 @@ include_directories(
add_executable( add_executable(
${PROJECT_NAME} ${PROJECT_NAME}
CronDataTest.cpp CronDataTest.cpp
CronScheduleTest.cpp CronTest.cpp) CronRandomizationTest.cpp
CronScheduleTest.cpp
CronTest.cpp)
target_link_libraries(${PROJECT_NAME} libcron) target_link_libraries(${PROJECT_NAME} libcron)
set_target_properties(${PROJECT_NAME} PROPERTIES set_target_properties(${PROJECT_NAME} PROPERTIES
ARCHIVE_OUTPUT_DIRECTORY "${OUTPUT_LOCATION}" ARCHIVE_OUTPUT_DIRECTORY "${CMAKE_CURRENT_LIST_DIR}/out"
LIBRARY_OUTPUT_DIRECTORY "${OUTPUT_LOCATION}" LIBRARY_OUTPUT_DIRECTORY "${CMAKE_CURRENT_LIST_DIR}/out"
RUNTIME_OUTPUT_DIRECTORY "${OUTPUT_LOCATION}") RUNTIME_OUTPUT_DIRECTORY "${CMAKE_CURRENT_LIST_DIR}/out")

View File

@ -0,0 +1,88 @@
#include <catch.hpp>
#include <string>
#include <unordered_map>
#include <algorithm>
#include <libcron/CronRandomization.h>
#include <libcron/Cron.h>
#include <iostream>
using namespace libcron;
SCENARIO("Randomize all the things")
{
const char* full_random = "R(0-59) R(0-59) R(0-23) R(1-31) R(1-12) ?";
Cron<> cron;
GIVEN(full_random)
{
THEN("Only valid schedules generated")
{
libcron::CronRandomization cr;
std::unordered_map<int, std::unordered_map<int, int>> results{};
for (int i = 0; i < 1000; ++i)
{
auto res = cr.parse(full_random);
REQUIRE(std::get<0>(res));
auto schedule = std::get<1>(res);
auto start = schedule.begin();
auto space = std::find(schedule.begin(), schedule.end(), ' ');
for (int section = 0; start != schedule.end() && section < 5; ++section)
{
auto& map = results[section];
auto s = std::string{start, space};
auto value = std::stoi(s);
map[value]++;
start = space + 1;
space = std::find(start, schedule.end(), ' ');
}
REQUIRE(results.size() == 5);
INFO("schedule:" << schedule);
REQUIRE(cron.add_schedule("validate schedule", schedule, []() {}));
}
}
}
}
SCENARIO("Randomize all the things with reverse ranges")
{
// Only generate DayOfMonth up to 28 to prevent failing tests where the month doesn't have more days.
const char* full_random = "R(45-15) R(30-0) R(18-2) R(28-15) 2 ?";
Cron<> cron;
GIVEN(full_random)
{
THEN("Only valid schedules generated")
{
libcron::CronRandomization cr;
std::unordered_map<int, std::unordered_map<int, int>> results{};
for (int i = 0; i < 1000; ++i)
{
auto res = cr.parse(full_random);
REQUIRE(std::get<0>(res));
auto schedule = std::get<1>(res);
auto start = schedule.begin();
auto space = std::find(schedule.begin(), schedule.end(), ' ');
for (int section = 0; start != schedule.end() && section < 5; ++section)
{
auto& map = results[section];
auto s = std::string{start, space};
auto value = std::stoi(s);
map[value]++;
start = space + 1;
space = std::find(start, schedule.end(), ' ');
}
REQUIRE(results.size() == 5);
INFO("schedule:" << schedule);
REQUIRE(cron.add_schedule("validate schedule", schedule, []() {}));
}
}
}
}

BIN
test/out/cron_test Executable file

Binary file not shown.