diff --git a/libcron/CronData.cpp b/libcron/CronData.cpp index 9496829..56552ff 100644 --- a/libcron/CronData.cpp +++ b/libcron/CronData.cpp @@ -78,9 +78,9 @@ namespace libcron bool res = true; // Verify that the available dates are possible based on the given months - if (months.find(static_cast(2)) != months.end()) + if (months.size() == 1 && months.find(static_cast(2)) != months.end()) { - // February allowed, make sure that the allowed date(s) includes 29 and below. + // Only february allowed, make sure that the allowed date(s) includes 29 and below. res = has_any_in_range(day_of_month, 1, 29); } @@ -102,8 +102,7 @@ namespace libcron res = false; for(size_t i = 0; !res && i < months_with_31.size(); ++i) { - auto month = months_with_31[i]; - res = months.find(static_cast(month)) != months.end(); + res = months.find(static_cast(months_with_31[i])) != months.end(); } } } diff --git a/libcron/CronData.h b/libcron/CronData.h index 60c6832..110bb5e 100644 --- a/libcron/CronData.h +++ b/libcron/CronData.h @@ -295,7 +295,7 @@ namespace libcron { bool res = false; - auto value_range = R"#((\d+)/(\d+))#"; + auto value_range = R"#((\d+|\*)/(\d+))#"; std::regex range(value_range, std::regex_constants::ECMAScript); @@ -303,7 +303,16 @@ namespace libcron if (std::regex_match(s.begin(), s.end(), match, range)) { - auto raw_start = std::stoi(match[1].str().c_str()); + int raw_start; + if(match[1].str() == "*") + { + raw_start = value_of(T::First); + } + else + { + raw_start = std::stoi(match[1].str().c_str()); + } + auto raw_step = std::stoi(match[2].str().c_str()); if (is_within_limits(raw_start, raw_start) && raw_step > 0) diff --git a/libcron/CronSchedule.cpp b/libcron/CronSchedule.cpp index a91f1b1..7d2b66d 100644 --- a/libcron/CronSchedule.cpp +++ b/libcron/CronSchedule.cpp @@ -6,17 +6,17 @@ using namespace date; namespace libcron { - std::chrono::system_clock::time_point - CronSchedule::calculate_from(const std::chrono::system_clock::time_point& from) + std::tuple + CronSchedule::calculate_from(const std::chrono::system_clock::time_point& from) const { - //auto time_part = from - date::floor(from); - auto curr = from;// - time_part; - //auto dt = to_calendar_time(curr); + auto curr = from; bool done = false; + auto max_iterations = std::numeric_limits::max(); - while (!done) + while (!done && --max_iterations > 0) { + bool date_changed = false; year_month_day ymd = date::floor(curr); // Add months until one of the allowed days are found, or stay at the current one. @@ -25,7 +25,7 @@ namespace libcron auto next_month = ymd + months{1}; sys_days s = next_month.year() / next_month.month() / 1; curr = s; - continue; + date_changed = true; } // If all days are allowed, then the 'day of week' takes precedence, which also means that // day of week only is ignored when specific days of months are specified. @@ -38,7 +38,7 @@ namespace libcron sys_days s = ymd; curr = s; curr += days{1}; - continue; + date_changed = true; } } else @@ -52,33 +52,35 @@ namespace libcron sys_days s = ymd; curr = s; curr += days{1}; - continue; + date_changed = true; } } - //curr += time_part; - auto date_time = to_calendar_time(curr); - if (data.get_hours().find(static_cast(date_time.hour)) == data.get_hours().end()) + if(!date_changed) { - curr += hours{1}; - curr -= minutes{date_time.min}; - curr -= seconds{date_time.sec}; - } - else if (data.get_minutes().find(static_cast(date_time.min)) == data.get_minutes().end()) - { - curr += minutes{1}; - curr -= seconds{date_time.sec}; - } - else if (data.get_seconds().find(static_cast(date_time.sec)) == data.get_seconds().end()) - { - curr += seconds{1}; - } - else - { - done = true; + auto date_time = to_calendar_time(curr); + if (data.get_hours().find(static_cast(date_time.hour)) == data.get_hours().end()) + { + curr += hours{1}; + curr -= minutes{date_time.min}; + curr -= seconds{date_time.sec}; + } + else if (data.get_minutes().find(static_cast(date_time.min)) == data.get_minutes().end()) + { + curr += minutes{1}; + curr -= seconds{date_time.sec}; + } + else if (data.get_seconds().find(static_cast(date_time.sec)) == data.get_seconds().end()) + { + curr += seconds{1}; + } + else + { + done = true; + } } } - return curr; + return std::make_tuple(max_iterations > 0, max_iterations > 0 ? curr : system_clock::now()); } } \ No newline at end of file diff --git a/libcron/CronSchedule.h b/libcron/CronSchedule.h index f78c909..3d38bc2 100644 --- a/libcron/CronSchedule.h +++ b/libcron/CronSchedule.h @@ -15,23 +15,24 @@ namespace libcron { } - std::chrono::system_clock::time_point calculate_from(const std::chrono::system_clock::time_point& from); + std::tuple + calculate_from(const std::chrono::system_clock::time_point& from) const; // https://github.com/HowardHinnant/date/wiki/Examples-and-Recipes#obtaining-ymd-hms-components-from-a-time_point static DateTime to_calendar_time(std::chrono::system_clock::time_point time) { auto daypoint = date::floor(time); auto ymd = date::year_month_day(daypoint); // calendar date - auto tod = date::make_time(time - daypoint); // Yields time_of_day type + auto time_of_day = date::make_time(time - daypoint); // Yields time_of_day type // Obtain individual components as integers DateTime dt{ int(ymd.year()), unsigned(ymd.month()), unsigned(ymd.day()), - static_cast(tod.hours().count()), - static_cast(tod.minutes().count()), - static_cast(tod.seconds().count())}; + static_cast(time_of_day.hours().count()), + static_cast(time_of_day.minutes().count()), + static_cast(time_of_day.seconds().count())}; return dt; } diff --git a/test/CronScheduleTest.cpp b/test/CronScheduleTest.cpp index caeb199..99d44f9 100644 --- a/test/CronScheduleTest.cpp +++ b/test/CronScheduleTest.cpp @@ -15,6 +15,43 @@ system_clock::time_point DT(year_month_day ymd, hours h = hours{0}, minutes m = return sum; } +bool test(const std::string& schedule, system_clock::time_point from, + std::vector expected_next) +{ + auto c = CronData::create(schedule); + bool res = c.is_valid(); + if (res) + { + CronSchedule sched(c); + + auto curr_from = from; + + for (size_t i = 0; res && i < expected_next.size(); ++i) + { + auto result = sched.calculate_from(curr_from); + auto calculated = std::get<1>(result); + + res = std::get<0>(result) && calculated == expected_next[i]; + + if (res) + { + // Add a second to the time so that we move on to the next expected time + // and don't get locked on the current one. + curr_from = expected_next[i] + seconds{1}; + } + else + { + std::cout + << "From: " << curr_from << "\n" + << "Expected: " << expected_next[i] << "\n" + << "Calculated: " << calculated; + } + } + } + + return res; +} + bool test(const std::string& schedule, system_clock::time_point from, system_clock::time_point expected_next) { auto c = CronData::create(schedule); @@ -22,8 +59,9 @@ bool test(const std::string& schedule, system_clock::time_point from, system_clo if (res) { CronSchedule sched(c); - auto run_time = sched.calculate_from(from); - res &= expected_next == run_time; + auto result = sched.calculate_from(from); + auto run_time = std::get<1>(result); + res &= std::get<0>(result) && expected_next == run_time; if (!res) { @@ -31,32 +69,115 @@ bool test(const std::string& schedule, system_clock::time_point from, system_clo << "From: " << from << "\n" << "Expected: " << expected_next << "\n" << "Calculated: " << run_time; - } } return res; } - SCENARIO("Calculating next runtime") { REQUIRE(test("0 0 * * * *", DT(2010_y / 1 / 1), DT(2010_y / 1 / 1, hours{0}))); REQUIRE(test("0 0 * * * *", DT(2010_y / 1 / 1, hours{0}, minutes{0}, seconds{1}), DT(2010_y / 1 / 1, hours{1}))); REQUIRE(test("0 0 * * * *", DT(2010_y / 1 / 1, hours{5}), DT(2010_y / 1 / 1, hours{5}))); REQUIRE(test("0 0 * * * *", DT(2010_y / 1 / 1, hours{5}, minutes{1}), DT(2010_y / 1 / 1, hours{6}))); - REQUIRE(test("0 0 * * * *", DT(2017_y / 12 / 31, hours{23}, minutes{59}, seconds{58}), DT(2018_y / 1 / 1, hours{0}))); - REQUIRE(test("0 0 10 * * *", DT(2017_y / 12 / 31, hours{9}, minutes{59}, seconds{58}), DT(2017_y / 12 / 31, hours{10}))); - REQUIRE(test("0 0 10 * * *", DT(2017_y / 12 / 31, hours{23}, minutes{59}, seconds{58}), DT(2018_y / 1 / 1, hours{10}))); - REQUIRE(test("0 0 10 * FEB *", DT(2017_y / 12 / 31, hours{23}, minutes{59}, seconds{58}), DT(2018_y / 2 / 1, hours{10}))); - REQUIRE(test("0 0 10 25 FEB *", DT(2017_y / 12 / 31, hours{23}, minutes{59}, seconds{58}), DT(2018_y / 2 / 25, hours{10}))); - REQUIRE(test("0 0 10 * FEB 1", DT(2017_y / 12 / 31, hours{23}, minutes{59}, seconds{58}), DT(year_month_day{2018_y/2/mon[1]}, hours{10}))); - REQUIRE(test("0 0 10 * FEB 6", DT(2017_y / 12 / 31, hours{23}, minutes{59}, seconds{58}), DT(year_month_day{2018_y/2/sat[1]}, hours{10}))); - REQUIRE(test("* * * 10-12 NOV *", DT(2018_y / 11 / 11, hours{10}, minutes{11}, seconds{12}), DT(year_month_day{2018_y/11/11}, hours{10}, minutes{11}, seconds{12}))); + REQUIRE(test("0 0 * * * *", DT(2017_y / 12 / 31, hours{23}, minutes{59}, seconds{58}), + DT(2018_y / 1 / 1, hours{0}))); + REQUIRE(test("0 0 10 * * *", DT(2017_y / 12 / 31, hours{9}, minutes{59}, seconds{58}), + DT(2017_y / 12 / 31, hours{10}))); + REQUIRE(test("0 0 10 * * *", DT(2017_y / 12 / 31, hours{23}, minutes{59}, seconds{58}), + DT(2018_y / 1 / 1, hours{10}))); + REQUIRE(test("0 0 10 * FEB *", DT(2017_y / 12 / 31, hours{23}, minutes{59}, seconds{58}), + DT(2018_y / 2 / 1, hours{10}))); + REQUIRE(test("0 0 10 25 FEB *", DT(2017_y / 12 / 31, hours{23}, minutes{59}, seconds{58}), + DT(2018_y / 2 / 25, hours{10}))); + REQUIRE(test("0 0 10 * FEB 1", DT(2017_y / 12 / 31, hours{23}, minutes{59}, seconds{58}), + DT(year_month_day{2018_y / 2 / mon[1]}, hours{10}))); + REQUIRE(test("0 0 10 * FEB 6", DT(2017_y / 12 / 31, hours{23}, minutes{59}, seconds{58}), + DT(year_month_day{2018_y / 2 / sat[1]}, hours{10}))); + REQUIRE(test("* * * 10-12 NOV *", DT(2018_y / 11 / 11, hours{10}, minutes{11}, seconds{12}), + DT(year_month_day{2018_y / 11 / 11}, hours{10}, minutes{11}, seconds{12}))); REQUIRE(test("0 0 * 31 APR,MAY *", DT(2017_y / 6 / 1), DT(2018_y / may / 31))); } SCENARIO("Leap year") { + REQUIRE(test("0 0 * 29 FEB *", DT(2015_y / 1 / 1), DT(2016_y / 2 / 29))); REQUIRE(test("0 0 * 29 FEB *", DT(2018_y / 1 / 1), DT(2020_y / 2 / 29))); + REQUIRE(test("0 0 * 29 FEB *", DT(2020_y / 2 / 29, hours{15}, minutes{13}, seconds{13}), + DT(2020_y / 2 / 29, hours{16}))); +} + +SCENARIO("Multiple calculations") +{ + WHEN("Every 15 minutes, every 2nd hour") + { + REQUIRE(test("0 0/15 0/2 * * *", DT(2018_y / 1 / 1, hours{13}, minutes{14}, seconds{59}), + {DT(2018_y / 1 / 1, hours{14}, minutes{00}), + DT(2018_y / 1 / 1, hours{14}, minutes{15}), + DT(2018_y / 1 / 1, hours{14}, minutes{30}), + DT(2018_y / 1 / 1, hours{14}, minutes{45}), + DT(2018_y / 1 / 1, hours{16}, minutes{00}), + DT(2018_y / 1 / 1, hours{16}, minutes{15})})); + } + + WHEN("Every top of the hour, every 12th hour, during 12 and 13:th July") + { + REQUIRE(test("0 0 0/12 12-13 JUL *", DT(2018_y / 1 / 1), + {DT(2018_y / 7 / 12, hours{0}), + DT(2018_y / 7 / 12, hours{12}), + DT(2018_y / 7 / 13, hours{0}), + DT(2018_y / 7 / 13, hours{12}), + DT(2019_y / 7 / 12, hours{0}), + DT(2019_y / 7 / 12, hours{12})})); + } + + WHEN("Every first of the month, 15h, every second month, 22m") + { + REQUIRE(test("0 22 15 1 * *", DT(2018_y / 1 / 1), + {DT(2018_y / 1 / 1, hours{15}, minutes{22}), + DT(2018_y / 2 / 1, hours{15}, minutes{22}), + DT(2018_y / 3 / 1, hours{15}, minutes{22}), + DT(2018_y / 4 / 1, hours{15}, minutes{22}), + DT(2018_y / 5 / 1, hours{15}, minutes{22}), + DT(2018_y / 6 / 1, hours{15}, minutes{22}), + DT(2018_y / 7 / 1, hours{15}, minutes{22}), + DT(2018_y / 8 / 1, hours{15}, minutes{22}), + DT(2018_y / 9 / 1, hours{15}, minutes{22}), + DT(2018_y / 10 / 1, hours{15}, minutes{22}), + DT(2018_y / 11 / 1, hours{15}, minutes{22}), + DT(2018_y / 12 / 1, hours{15}, minutes{22}), + DT(2019_y / 1 / 1, hours{15}, minutes{22})})); + } + + WHEN("“At minute 0 past hour 0 and 12 on day-of-month 1 in every 2nd month") + { + // Note that day-of-week, 5, is not in effect in this schedule. + REQUIRE(test("0 0 0,12 1 */2 5", DT(2018_y / 3 / 10, hours{16}, minutes{51}), DT(2018_y / 5 / 1))); + } + + WHEN("“At 00:05 in August") + { + // Note that day-of-week, 5, is not in effect in this schedule. + REQUIRE(test("0 5 0 * 8 *", DT(2018_y / 3 / 10, hours{16}, minutes{51}), + DT(2018_y / 8 / 1, hours{0}, minutes{5}))); + } + + WHEN("At 22:00 on every day-of-week from Monday through Friday") + { + // Note that day-of-week, 5, is not in effect in this schedule. + REQUIRE(test("0 0 22 * * 1-5", DT(2021_y / 12 / 15, hours{16}, minutes{51}), + {DT(2021_y / 12 / 15, hours{22}), + DT(2021_y / 12 / 16, hours{22}), + DT(2021_y / 12 / 17, hours{22}), + // 18-19 are weekend + DT(2021_y / 12 / 20, hours{22}), + DT(2021_y / 12 / 21, hours{22})})); + } +} + +SCENARIO("Unable to calculate time point") +{ + // TODO: Find a + //REQUIRE_FALSE(test("0 0 0 1 1 0", DT(2021_y / 12 / 15), DT(2022_y / 1 / 1))); } \ No newline at end of file