| #include <chrono> |
| #include <string> |
| #include <sstream> |
| #include <vector> |
| #include <iterator> |
| |
| namespace Bosma { |
| using Clock = std::chrono::system_clock; |
| |
| inline void add(std::tm &tm, Clock::duration time) { |
| auto tp = Clock::from_time_t(std::mktime(&tm)); |
| auto tp_adjusted = tp + time; |
| auto tm_adjusted = Clock::to_time_t(tp_adjusted); |
| tm = *std::localtime(&tm_adjusted); |
| } |
| |
| class BadCronExpression : public std::exception { |
| public: |
| explicit BadCronExpression(std::string msg) : msg_(std::move(msg)) {} |
| |
| const char *what() const noexcept override { return (msg_.c_str()); } |
| |
| private: |
| std::string msg_; |
| }; |
| |
| inline void |
| verify_and_set(const std::string &token, const std::string &expression, int &field, const int lower_bound, |
| const int upper_bound, const bool adjust = false) { |
| if (token == "*") |
| field = -1; |
| else { |
| try { |
| field = std::stoi(token); |
| } catch (const std::invalid_argument &) { |
| throw BadCronExpression("malformed cron string (`" + token + "` not an integer or *): " + expression); |
| } catch (const std::out_of_range &) { |
| throw BadCronExpression("malformed cron string (`" + token + "` not convertable to int): " + expression); |
| } |
| if (field < lower_bound || field > upper_bound) { |
| std::ostringstream oss; |
| oss << "malformed cron string ('" << token << "' must be <= " << upper_bound << " and >= " << lower_bound |
| << "): " << expression; |
| throw BadCronExpression(oss.str()); |
| } |
| if (adjust) |
| field--; |
| } |
| } |
| |
| class Cron { |
| public: |
| explicit Cron(const std::string &expression) { |
| std::istringstream iss(expression); |
| std::vector<std::string> tokens{std::istream_iterator<std::string>{iss}, |
| std::istream_iterator<std::string>{}}; |
| |
| if (tokens.size() != 5) throw BadCronExpression("malformed cron string (must be 5 fields): " + expression); |
| |
| verify_and_set(tokens[0], expression, minute, 0, 59); |
| verify_and_set(tokens[1], expression, hour, 0, 23); |
| verify_and_set(tokens[2], expression, day, 1, 31); |
| verify_and_set(tokens[3], expression, month, 1, 12, true); |
| verify_and_set(tokens[4], expression, day_of_week, 0, 6); |
| } |
| |
| // http://stackoverflow.com/a/322058/1284550 |
| Clock::time_point cron_to_next(const Clock::time_point from = Clock::now()) const { |
| // get current time as a tm object |
| auto now = Clock::to_time_t(from); |
| std::tm next(*std::localtime(&now)); |
| // it will always at least run the next minute |
| next.tm_sec = 0; |
| add(next, std::chrono::minutes(1)); |
| while (true) { |
| if (month != -1 && next.tm_mon != month) { |
| // add a month |
| // if this will bring us over a year, increment the year instead and reset the month |
| if (next.tm_mon + 1 > 11) { |
| next.tm_mon = 0; |
| next.tm_year++; |
| } else |
| next.tm_mon++; |
| |
| next.tm_mday = 1; |
| next.tm_hour = 0; |
| next.tm_min = 0; |
| continue; |
| } |
| if (day != -1 && next.tm_mday != day) { |
| add(next, std::chrono::hours(24)); |
| next.tm_hour = 0; |
| next.tm_min = 0; |
| continue; |
| } |
| if (day_of_week != -1 && next.tm_wday != day_of_week) { |
| add(next, std::chrono::hours(24)); |
| next.tm_hour = 0; |
| next.tm_min = 0; |
| continue; |
| } |
| if (hour != -1 && next.tm_hour != hour) { |
| add(next, std::chrono::hours(1)); |
| next.tm_min = 0; |
| continue; |
| } |
| if (minute != -1 && next.tm_min != minute) { |
| add(next, std::chrono::minutes(1)); |
| continue; |
| } |
| break; |
| } |
| |
| // telling mktime to figure out dst |
| next.tm_isdst = -1; |
| return Clock::from_time_t(std::mktime(&next)); |
| } |
| |
| int minute, hour, day, month, day_of_week; |
| }; |
| } |