1111#include < oryx/chron/details/string_cast.hpp>
1212#include < oryx/chron/details/to_underlying.hpp>
1313#include < oryx/chron/details/parser.hpp>
14+ #include " oryx/chron/details/in_range.hpp"
1415
1516namespace oryx ::chron {
1617namespace {
1718
19+ constexpr int kSelectedValueNone = -1 ;
20+
1821template <typename T>
1922auto GetRandomInRange (std::string_view section,
2023 int & selected_value,
2124 std::mt19937& twister,
22- std::pair<int , int > limit = std::make_pair(-1 , -1 )) -> std::pair<bool, std::string> {
23- auto result = std::make_pair (true , std::string{});
24- selected_value = -1 ;
25-
26- if (auto match = ctre::match<R"#( [rR]\((\d+)\-(\d+)\))#" >(section)) {
27- // Random range, parse left and right numbers
28- auto left = details::StringCast<int >(match.get <1 >().to_view ());
29- auto right = details::StringCast<int >(match.get <2 >().to_view ());
30-
31- // Apply limit if provided
32- if (limit.first != -1 && limit.second != -1 ) {
33- left = std::clamp (left, limit.first , limit.second );
34- right = std::clamp (right, limit.first , limit.second );
35- }
25+ std::pair<int , int > limit = std::make_pair(-1 , -1 )) -> std::optional<std::string> {
26+ selected_value = kSelectedValueNone ;
3627
37- std::set<T> numbers;
38- result.first = details::Parser::ConvertFromStringRangeToNumberRange (std::format (" {}-{}" , left, right), numbers);
39-
40- // Remove items outside the limit
41- if (limit.first != -1 && limit.second != -1 ) {
42- for (auto it = numbers.begin (); it != numbers.end ();) {
43- if (details::to_underlying (*it) < limit.first || details::to_underlying (*it) > limit.second ) {
44- it = numbers.erase (it);
45- } else {
46- ++it;
47- }
48- }
49- }
28+ auto match = ctre::match<R"#( [rR]\((\d+)\-(\d+)\))#" >(section);
29+ if (!match) {
30+ return std::string (section);
31+ }
5032
51- if (result.first && !numbers.empty ()) {
52- // Select a random value from the valid numbers
53- std::uniform_int_distribution<> distribution (0 , static_cast <int >(numbers.size () - 1 ));
54- auto it = numbers.begin ();
55- std::advance (it, distribution (twister));
56- selected_value = details::to_underlying (*it);
57- result.second = std::to_string (selected_value);
58- }
59- } else {
60- // Not a random section, return as-is
61- result.second = section;
33+ // Random range, parse left and right numbers
34+ auto left = details::StringCast<int >(match.get <1 >().to_view ());
35+ auto right = details::StringCast<int >(match.get <2 >().to_view ());
36+
37+ // Apply limit if provided
38+ if (limit.first != -1 && limit.second != -1 ) {
39+ left = std::clamp (left, limit.first , limit.second );
40+ right = std::clamp (right, limit.first , limit.second );
41+ }
42+
43+ std::set<T> numbers;
44+ bool success = details::Parser::ConvertFromStringRangeToNumberRange (std::format (" {}-{}" , left, right), numbers);
45+
46+ // Remove items outside the limit
47+ if (limit.first != -1 && limit.second != -1 ) {
48+ std::erase_if (numbers, [&limit](T val) -> bool {
49+ return !details::InRange<int >(details::to_underlying (val), limit.first , limit.second );
50+ });
51+ }
52+
53+ if (!success || numbers.empty ()) {
54+ return std::nullopt ;
6255 }
6356
64- return result;
57+ std::uniform_int_distribution distribution (0 , static_cast <int >(numbers.size () - 1 ));
58+ auto it = numbers.begin ();
59+ std::advance (it, distribution (twister));
60+ selected_value = details::to_underlying (*it);
61+ return std::to_string (selected_value);
6562}
6663
6764auto DayLimiter (const std::set<Months>& months) -> std::pair<int, int> {
@@ -86,11 +83,9 @@ Randomization::Randomization()
8683 : twister_(random_device_()) {}
8784
8885auto Randomization::Parse (std::string_view cron_schedule) -> std::optional<std::string> {
89- // Split on space to get each separate part, six parts expected
90- auto matcher = ctre::match<R"#( ^\s*(.*?)\s+(.*?)\s+(.*?)\s+(.*?)\s+(.*?)\s+(.*?)\s*$)#" >;
91- // Replace text with numbers
92- std::string working_copy{};
86+ const auto matcher = ctre::match<R"#( ^\s*(.*?)\s+(.*?)\s+(.*?)\s+(.*?)\s+(.*?)\s+(.*?)\s*$)#" >;
9387
88+ std::string preprocessed_schedule{};
9489 if (auto match = matcher (cron_schedule)) {
9590 // Replace month and day names first
9691 auto month = match.get <5 >().to_string ();
@@ -100,53 +95,58 @@ auto Randomization::Parse(std::string_view cron_schedule) -> std::optional<std::
10095 details::ReplaceDayNameWithNumeric (dow);
10196
10297 // Merge all sections into one string
103- working_copy = std::format (" {} {} {} {} {} {}" , match.get <1 >().to_view (), match.get <2 >().to_view (),
104- match.get <3 >().to_view (), match.get <4 >().to_view (), month, dow);
98+ preprocessed_schedule = std::format (" {} {} {} {} {} {}" , match.get <1 >().to_view (), match.get <2 >().to_view (),
99+ match.get <3 >().to_view (), match.get <4 >().to_view (), month, dow);
105100 }
106101
107- std::string final_cron_schedule{};
108- bool success{};
109- if (auto match = matcher (working_copy)) {
110- int selected_value = -1 ;
111- auto second = GetRandomInRange<Seconds>(match.get <1 >().to_view (), selected_value, twister_);
112- success = second.first ;
113- final_cron_schedule = second.second ;
114-
115- auto minute = GetRandomInRange<Minutes>(match.get <2 >().to_view (), selected_value, twister_);
116- success &= minute.first ;
117- final_cron_schedule += " " + minute.second ;
118-
119- auto hour = GetRandomInRange<Hours>(match.get <3 >().to_view (), selected_value, twister_);
120- success &= hour.first ;
121- final_cron_schedule += " " + hour.second ;
122-
123- // Do Month before MonthDays to allow capping the allowed range.
124- auto month = GetRandomInRange<Months>(match.get <5 >().to_view (), selected_value, twister_);
125- success &= month.first ;
126-
127- std::set<Months> month_range{};
128- if (selected_value == -1 ) {
129- // Month is not specific, get the range.
130- success &= details::Parser::ConvertFromStringRangeToNumberRange (match.get <5 >().to_string (), month_range);
131- } else {
132- month_range.emplace (static_cast <Months>(selected_value));
133- }
102+ auto match = matcher (preprocessed_schedule);
103+ if (!match) [[unlikely]] {
104+ return std::nullopt ;
105+ }
106+
107+ int selected_value = kSelectedValueNone ;
108+ auto second = GetRandomInRange<Seconds>(match.get <1 >().to_view (), selected_value, twister_);
109+ if (!second) [[unlikely]] {
110+ return std::nullopt ;
111+ }
112+
113+ auto minute = GetRandomInRange<Minutes>(match.get <2 >().to_view (), selected_value, twister_);
114+ if (!minute) [[unlikely]] {
115+ return std::nullopt ;
116+ }
134117
135- auto limits = DayLimiter (month_range);
118+ auto hour = GetRandomInRange<Hours>(match.get <3 >().to_view (), selected_value, twister_);
119+ if (!hour) [[unlikely]] {
120+ return std::nullopt ;
121+ }
136122
137- auto day_of_month = GetRandomInRange<MonthDays>(match.get <4 >().to_view (), selected_value, twister_, limits);
123+ // Do Month before MonthDays to allow capping the allowed range.
124+ auto month = GetRandomInRange<Months>(match.get <5 >().to_view (), selected_value, twister_);
125+ if (!month) [[unlikely]] {
126+ return std::nullopt ;
127+ }
138128
139- success &= day_of_month.first ;
140- final_cron_schedule += " " + day_of_month.second + " " + month.second ;
129+ std::set<Months> month_range{};
130+ if (selected_value == kSelectedValueNone ) {
131+ // Month is not specific, get the range.
132+ bool success = details::Parser::ConvertFromStringRangeToNumberRange (match.get <5 >().to_view (), month_range);
133+ if (!success) return std::nullopt ;
134+ } else {
135+ month_range.emplace (static_cast <Months>(selected_value));
136+ }
141137
142- auto day_of_week = GetRandomInRange<Weekdays>(match.get <6 >().to_view (), selected_value, twister_);
143- success &= day_of_week.first ;
144- final_cron_schedule += " " + day_of_week.second ;
138+ auto limits = DayLimiter (month_range);
139+ auto day_of_month = GetRandomInRange<MonthDays>(match.get <4 >().to_view (), selected_value, twister_, limits);
140+ if (!day_of_month) [[unlikely]] {
141+ return std::nullopt ;
145142 }
146- if (success) {
147- return final_cron_schedule;
143+
144+ auto day_of_week = GetRandomInRange<Weekdays>(match.get <6 >().to_view (), selected_value, twister_);
145+ if (!day_of_week) [[unlikely]] {
146+ return std::nullopt ;
148147 }
149- return std::nullopt ;
148+
149+ return std::format (" {} {} {} {} {} {}" , *second, *minute, *hour, *day_of_month, *month, *day_of_week);
150150}
151151
152152} // namespace oryx::chron
0 commit comments