├── .idea ├── libcron.iml ├── misc.xml ├── vcs.xml ├── modules.xml └── runConfigurations │ └── All.xml ├── .gitmodules ├── libcron ├── include │ └── libcron │ │ ├── DateTime.h │ │ ├── TimeTypes.h │ │ ├── CronClock.h │ │ ├── CronSchedule.h │ │ ├── Task.h │ │ ├── TaskQueue.h │ │ ├── CronRandomization.h │ │ ├── Cron.h │ │ └── CronData.h ├── src │ ├── CronClock.cpp │ ├── Task.cpp │ ├── CronSchedule.cpp │ ├── CronRandomization.cpp │ └── CronData.cpp └── CMakeLists.txt ├── CMakeLists.txt ├── .github └── workflows │ └── ci.yml ├── .gitignore ├── LICENSE ├── test ├── CMakeLists.txt ├── CronRandomizationTest.cpp ├── CronScheduleTest.cpp ├── CronDataTest.cpp └── CronTest.cpp ├── uncrustify.cfg └── README.md /.idea/libcron.iml: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "libcron/externals/date"] 2 | path = libcron/externals/date 3 | url = https://github.com/HowardHinnant/date.git 4 | [submodule "test/externals/Catch2"] 5 | path = test/externals/Catch2 6 | url = https://github.com/catchorg/Catch2.git 7 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /libcron/include/libcron/DateTime.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | namespace libcron 6 | { 7 | struct DateTime 8 | { 9 | int year = 0; 10 | unsigned month = 0; 11 | unsigned day = 0; 12 | uint8_t hour = 0; 13 | uint8_t min = 0; 14 | uint8_t sec = 0; 15 | }; 16 | } 17 | -------------------------------------------------------------------------------- /CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.6) 2 | 3 | project(top) 4 | add_subdirectory(libcron) 5 | add_subdirectory(test) 6 | 7 | add_dependencies(cron_test libcron) 8 | 9 | install(TARGETS libcron DESTINATION lib) 10 | install(DIRECTORY libcron/include/libcron DESTINATION include) 11 | install(DIRECTORY libcron/externals/date/include/date DESTINATION include) 12 | 13 | -------------------------------------------------------------------------------- /.idea/runConfigurations/All.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: libcron tests 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | Tests: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - uses: actions/checkout@v2 10 | with: 11 | submodules: true 12 | - name: Build 13 | run: | 14 | mkdir build 15 | cd build 16 | cmake .. 17 | make -j4 18 | - name: Test 19 | run: | 20 | cd test/out 21 | ./cron_test -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Prerequisites 2 | *.d 3 | 4 | # Compiled Object files 5 | *.slo 6 | *.lo 7 | *.o 8 | *.obj 9 | 10 | # Precompiled Headers 11 | *.gch 12 | *.pch 13 | 14 | # Compiled Dynamic libraries 15 | *.so 16 | *.dylib 17 | *.dll 18 | 19 | # Fortran module files 20 | *.mod 21 | *.smod 22 | 23 | # Compiled Static libraries 24 | *.lai 25 | *.la 26 | *.a 27 | *.lib 28 | 29 | # Executables 30 | *.exe 31 | *.out 32 | *.app 33 | 34 | cmake-build-* 35 | .idea/workspace.xml 36 | out/* 37 | test/out* 38 | -------------------------------------------------------------------------------- /libcron/src/CronClock.cpp: -------------------------------------------------------------------------------- 1 | #include "libcron/CronClock.h" 2 | 3 | #ifdef WIN32 4 | #ifndef NOMINMAX 5 | #define NOMINMAX 6 | #endif 7 | #define WIN32_LEAN_AND_MEAN 8 | #include 9 | #endif 10 | 11 | using namespace std::chrono; 12 | 13 | namespace libcron 14 | { 15 | 16 | std::chrono::seconds LocalClock::utc_offset(std::chrono::system_clock::time_point now) const 17 | { 18 | #ifdef WIN32 19 | (void)now; 20 | 21 | TIME_ZONE_INFORMATION tz_info{}; 22 | seconds offset{ 0 }; 23 | 24 | auto res = GetTimeZoneInformation(&tz_info); 25 | if (res != TIME_ZONE_ID_INVALID) 26 | { 27 | // https://msdn.microsoft.com/en-us/library/windows/desktop/ms725481(v=vs.85).aspx 28 | // UTC = local time + bias => local_time = utc - bias, so UTC offset is -bias 29 | offset = minutes{ -tz_info.Bias }; 30 | } 31 | #else 32 | auto t = system_clock::to_time_t(now); 33 | tm tm{}; 34 | localtime_r(&t, &tm); 35 | seconds offset{ tm.tm_gmtoff }; 36 | #endif 37 | return offset; 38 | } 39 | } -------------------------------------------------------------------------------- /libcron/include/libcron/TimeTypes.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | namespace libcron 6 | { 7 | enum class Seconds : int8_t 8 | { 9 | First = 0, 10 | Last = 59 11 | }; 12 | 13 | enum class Minutes : int8_t 14 | { 15 | First = 0, 16 | Last = 59 17 | }; 18 | 19 | enum class Hours : int8_t 20 | { 21 | First = 0, 22 | Last = 23 23 | }; 24 | 25 | enum class DayOfMonth : uint8_t 26 | { 27 | First = 1, 28 | Last = 31 29 | }; 30 | 31 | enum class Months : uint8_t 32 | { 33 | First = 1, 34 | January = First, 35 | February, 36 | March, 37 | April, 38 | May, 39 | June, 40 | July, 41 | August, 42 | September, 43 | October, 44 | November, 45 | December = 12, 46 | Last = December 47 | }; 48 | 49 | enum class DayOfWeek : uint8_t 50 | { 51 | // Sunday = 0 ... Saturday = 6 52 | First = 0, 53 | Last = 6, 54 | }; 55 | } 56 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Per Malmberg 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /libcron/include/libcron/CronClock.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | namespace libcron 6 | { 7 | class ICronClock 8 | { 9 | public: 10 | virtual std::chrono::system_clock::time_point now() const = 0; 11 | virtual std::chrono::seconds utc_offset(std::chrono::system_clock::time_point now) const = 0; 12 | }; 13 | 14 | class UTCClock 15 | : public ICronClock 16 | { 17 | public: 18 | std::chrono::system_clock::time_point now() const override 19 | { 20 | return std::chrono::system_clock::now(); 21 | } 22 | 23 | std::chrono::seconds utc_offset(std::chrono::system_clock::time_point) const override 24 | { 25 | using namespace std::chrono; 26 | return 0s; 27 | } 28 | }; 29 | 30 | class LocalClock 31 | : public ICronClock 32 | { 33 | public: 34 | std::chrono::system_clock::time_point now() const override 35 | { 36 | auto now = std::chrono::system_clock::now(); 37 | return now + utc_offset(now); 38 | } 39 | 40 | std::chrono::seconds utc_offset(std::chrono::system_clock::time_point now) const override; 41 | }; 42 | } 43 | -------------------------------------------------------------------------------- /test/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.6) 2 | project(cron_test) 3 | 4 | set(CMAKE_CXX_STANDARD 17) 5 | 6 | # Deactivate Iterator-Debugging on Windows 7 | option(LIBCRON_DEACTIVATE_ITERATOR_DEBUGGING "Build with iterator-debugging (MSVC only)." OFF) 8 | 9 | if( MSVC ) 10 | set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /W4") 11 | 12 | if (LIBCRON_DEACTIVATE_ITERATOR_DEBUGGING) 13 | set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -D_HAS_ITERATOR_DEBUGGING=0") 14 | endif() 15 | else() 16 | set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wall -Wextra -Wpedantic") 17 | endif() 18 | 19 | include_directories( 20 | ${CMAKE_CURRENT_LIST_DIR}/externals/Catch2/single_include/catch2 21 | ${CMAKE_CURRENT_LIST_DIR}/../libcron/externals/date/include 22 | ${CMAKE_CURRENT_LIST_DIR}/.. 23 | ) 24 | 25 | add_executable( 26 | ${PROJECT_NAME} 27 | CronDataTest.cpp 28 | CronRandomizationTest.cpp 29 | CronScheduleTest.cpp 30 | CronTest.cpp) 31 | 32 | if(NOT MSVC) 33 | target_link_libraries(${PROJECT_NAME} libcron pthread) 34 | 35 | # Assume a modern compiler supporting uncaught_exceptions() 36 | target_compile_definitions (${PROJECT_NAME} PRIVATE -DHAS_UNCAUGHT_EXCEPTIONS) 37 | else() 38 | target_link_libraries(${PROJECT_NAME} libcron) 39 | endif() 40 | 41 | set_target_properties(${PROJECT_NAME} PROPERTIES 42 | ARCHIVE_OUTPUT_DIRECTORY "${CMAKE_CURRENT_LIST_DIR}/out" 43 | LIBRARY_OUTPUT_DIRECTORY "${CMAKE_CURRENT_LIST_DIR}/out" 44 | RUNTIME_OUTPUT_DIRECTORY "${CMAKE_CURRENT_LIST_DIR}/out") 45 | -------------------------------------------------------------------------------- /libcron/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.6) 2 | project(libcron) 3 | 4 | set(CMAKE_CXX_STANDARD 17) 5 | 6 | # Deactivate Iterator-Debugging on Windows 7 | option(LIBCRON_DEACTIVATE_ITERATOR_DEBUGGING "Build with iterator-debugging (MSVC only)." OFF) 8 | 9 | if( MSVC ) 10 | set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /W4") 11 | 12 | if (LIBCRON_DEACTIVATE_ITERATOR_DEBUGGING) 13 | set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -D_HAS_ITERATOR_DEBUGGING=0") 14 | endif() 15 | else() 16 | set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wall -Wextra -Wpedantic") 17 | endif() 18 | 19 | add_library(${PROJECT_NAME} 20 | include/libcron/Cron.h 21 | include/libcron/CronClock.h 22 | include/libcron/CronData.h 23 | include/libcron/CronRandomization.h 24 | include/libcron/CronSchedule.h 25 | include/libcron/DateTime.h 26 | include/libcron/Task.h 27 | include/libcron/TimeTypes.h 28 | src/CronClock.cpp 29 | src/CronData.cpp 30 | src/CronRandomization.cpp 31 | src/CronSchedule.cpp 32 | src/Task.cpp) 33 | 34 | target_include_directories(${PROJECT_NAME} 35 | PUBLIC ${CMAKE_CURRENT_LIST_DIR}/externals/date/include 36 | PUBLIC include) 37 | 38 | if(NOT MSVC) 39 | # Assume a modern compiler (gcc 9.3) 40 | target_compile_definitions (${PROJECT_NAME} PRIVATE -DHAS_UNCAUGHT_EXCEPTIONS) 41 | endif() 42 | 43 | set_target_properties(${PROJECT_NAME} PROPERTIES 44 | ARCHIVE_OUTPUT_DIRECTORY "${CMAKE_CURRENT_LIST_DIR}/out/${CMAKE_BUILD_TYPE}" 45 | LIBRARY_OUTPUT_DIRECTORY "${CMAKE_CURRENT_LIST_DIR}/out/${CMAKE_BUILD_TYPE}" 46 | RUNTIME_OUTPUT_DIRECTORY "${CMAKE_CURRENT_LIST_DIR}/out/${CMAKE_BUILD_TYPE}") 47 | -------------------------------------------------------------------------------- /libcron/include/libcron/CronSchedule.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "libcron/CronData.h" 4 | #include 5 | #if defined(_MSC_VER) 6 | #pragma warning(push) 7 | #pragma warning(disable:4244) 8 | #endif 9 | #include 10 | #if defined(_MSC_VER) 11 | #pragma warning(pop) 12 | #endif 13 | 14 | #include "libcron/DateTime.h" 15 | 16 | namespace libcron 17 | { 18 | class CronSchedule 19 | { 20 | public: 21 | explicit CronSchedule(CronData& data) 22 | : data(data) 23 | { 24 | } 25 | 26 | CronSchedule(const CronSchedule&) = default; 27 | 28 | CronSchedule& operator=(const CronSchedule&) = default; 29 | 30 | std::tuple 31 | calculate_from(const std::chrono::system_clock::time_point& from) const; 32 | 33 | // https://github.com/HowardHinnant/date/wiki/Examples-and-Recipes#obtaining-ymd-hms-components-from-a-time_point 34 | static DateTime to_calendar_time(std::chrono::system_clock::time_point time) 35 | { 36 | auto daypoint = date::floor(time); 37 | auto ymd = date::year_month_day(daypoint); // calendar date 38 | auto time_of_day = date::make_time(time - daypoint); // Yields time_of_day type 39 | 40 | // Obtain individual components as integers 41 | DateTime dt{ 42 | int(ymd.year()), 43 | unsigned(ymd.month()), 44 | unsigned(ymd.day()), 45 | static_cast(time_of_day.hours().count()), 46 | static_cast(time_of_day.minutes().count()), 47 | static_cast(time_of_day.seconds().count())}; 48 | 49 | return dt; 50 | } 51 | 52 | private: 53 | CronData data; 54 | }; 55 | 56 | } 57 | -------------------------------------------------------------------------------- /libcron/src/Task.cpp: -------------------------------------------------------------------------------- 1 | #include "libcron/Task.h" 2 | 3 | using namespace std::chrono; 4 | 5 | namespace libcron 6 | { 7 | 8 | bool Task::calculate_next(std::chrono::system_clock::time_point from) 9 | { 10 | auto result = schedule.calculate_from(from); 11 | 12 | // In case the calculation fails, the task will no longer expire. 13 | valid = std::get<0>(result); 14 | if (valid) 15 | { 16 | next_schedule = std::get<1>(result); 17 | 18 | // Make sure that the task is allowed to run. 19 | last_run = next_schedule - 1s; 20 | } 21 | 22 | return valid; 23 | } 24 | 25 | bool Task::is_expired(std::chrono::system_clock::time_point now) const 26 | { 27 | return valid && now >= last_run && time_until_expiry(now) == 0s; 28 | } 29 | 30 | std::chrono::system_clock::duration Task::time_until_expiry(std::chrono::system_clock::time_point now) const 31 | { 32 | system_clock::duration d{}; 33 | 34 | // Explicitly return 0s instead of a possibly negative duration when it has expired. 35 | if (now >= next_schedule) 36 | { 37 | d = 0s; 38 | } 39 | else 40 | { 41 | d = next_schedule - now; 42 | } 43 | 44 | return d; 45 | } 46 | 47 | std::string Task::get_status(std::chrono::system_clock::time_point now) const 48 | { 49 | std::string s = "'"; 50 | s+= get_name(); 51 | s += "' expires in "; 52 | s += std::to_string(duration_cast(time_until_expiry(now)).count()); 53 | s += "ms => "; 54 | 55 | auto dt = CronSchedule::to_calendar_time(next_schedule); 56 | s+= std::to_string(dt.year) + "-"; 57 | s+= std::to_string(dt.month) + "-"; 58 | s+= std::to_string(dt.day) + " "; 59 | s+= std::to_string(dt.hour) + ":"; 60 | s+= std::to_string(dt.min) + ":"; 61 | s+= std::to_string(dt.sec); 62 | return s; 63 | } 64 | } -------------------------------------------------------------------------------- /libcron/include/libcron/Task.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | #include "CronData.h" 7 | #include "CronSchedule.h" 8 | 9 | namespace libcron 10 | { 11 | class TaskInformation 12 | { 13 | public: 14 | virtual ~TaskInformation() = default; 15 | virtual std::chrono::system_clock::duration get_delay() const = 0; 16 | virtual std::string get_name() const = 0; 17 | }; 18 | 19 | class Task : public TaskInformation 20 | { 21 | public: 22 | using TaskFunction = std::function; 23 | 24 | Task(std::string name, const CronSchedule schedule, TaskFunction task) 25 | : name(std::move(name)), schedule(std::move(schedule)), task(std::move(task)) 26 | { 27 | } 28 | 29 | void execute(std::chrono::system_clock::time_point now) 30 | { 31 | // Next Schedule is still the current schedule, calculate delay (actual execution - planned execution) 32 | delay = now - next_schedule; 33 | 34 | last_run = now; 35 | task(*this); 36 | } 37 | 38 | std::chrono::system_clock::duration get_delay() const override 39 | { 40 | return delay; 41 | } 42 | 43 | Task(const Task& other) = default; 44 | 45 | Task& operator=(const Task&) = default; 46 | 47 | bool calculate_next(std::chrono::system_clock::time_point from); 48 | 49 | bool operator>(const Task& other) const 50 | { 51 | return next_schedule > other.next_schedule; 52 | } 53 | 54 | bool operator<(const Task& other) const 55 | { 56 | return next_schedule < other.next_schedule; 57 | } 58 | 59 | bool is_expired(std::chrono::system_clock::time_point now) const; 60 | 61 | std::chrono::system_clock::duration 62 | time_until_expiry(std::chrono::system_clock::time_point now) const; 63 | 64 | std::string get_name() const override 65 | { 66 | return name; 67 | } 68 | 69 | std::string get_status(std::chrono::system_clock::time_point now) const; 70 | 71 | private: 72 | std::string name; 73 | CronSchedule schedule; 74 | std::chrono::system_clock::time_point next_schedule; 75 | std::chrono::system_clock::duration delay = std::chrono::seconds(-1); 76 | TaskFunction task; 77 | bool valid = false; 78 | std::chrono::system_clock::time_point last_run = std::numeric_limits::min(); 79 | }; 80 | } 81 | 82 | inline bool operator==(const std::string &lhs, const libcron::Task &rhs) 83 | { 84 | return lhs == rhs.get_name(); 85 | } 86 | 87 | inline bool operator==(const libcron::Task &lhs, const std::string &rhs) 88 | { 89 | return lhs.get_name() == rhs; 90 | } 91 | 92 | inline bool operator!=(const std::string &lhs, const libcron::Task &rhs) 93 | { 94 | return !(lhs == rhs); 95 | } 96 | 97 | inline bool operator!=(const libcron::Task &lhs, const std::string &rhs) 98 | { 99 | return !(lhs == rhs); 100 | } 101 | -------------------------------------------------------------------------------- /libcron/include/libcron/TaskQueue.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include "Task.h" 8 | 9 | namespace libcron 10 | { 11 | template 12 | class TaskQueue 13 | { 14 | public: 15 | const std::vector& get_tasks() const 16 | { 17 | return c; 18 | } 19 | 20 | std::vector& get_tasks() 21 | { 22 | return c; 23 | } 24 | 25 | size_t size() const noexcept 26 | { 27 | return c.size(); 28 | } 29 | 30 | bool empty() const noexcept 31 | { 32 | return c.empty(); 33 | } 34 | 35 | void push(Task& t) 36 | { 37 | c.push_back(std::move(t)); 38 | } 39 | 40 | void push(Task&& t) 41 | { 42 | c.push_back(std::move(t)); 43 | } 44 | 45 | void push(std::vector& tasks_to_insert) 46 | { 47 | c.reserve(c.size() + tasks_to_insert.size()); 48 | c.insert(c.end(), std::make_move_iterator(tasks_to_insert.begin()), std::make_move_iterator(tasks_to_insert.end())); 49 | } 50 | 51 | const Task& top() const 52 | { 53 | return c[0]; 54 | } 55 | 56 | Task& at(const size_t i) 57 | { 58 | return c[i]; 59 | } 60 | 61 | void sort() 62 | { 63 | std::sort(c.begin(), c.end(), std::less<>()); 64 | } 65 | 66 | void clear() 67 | { 68 | lock.lock(); 69 | c.clear(); 70 | lock.unlock(); 71 | } 72 | 73 | void remove(Task& to_remove) 74 | { 75 | auto it = std::find_if(c.begin(), c.end(), [&to_remove] (const Task& to_compare) { 76 | return to_remove.get_name() == to_compare; 77 | }); 78 | 79 | if (it != c.end()) 80 | { 81 | c.erase(it); 82 | } 83 | } 84 | 85 | void remove(std::string to_remove) 86 | { 87 | lock.lock(); 88 | auto it = std::find_if(c.begin(), c.end(), [&to_remove] (const Task& to_compare) { 89 | return to_remove == to_compare; 90 | }); 91 | if (it != c.end()) 92 | { 93 | c.erase(it); 94 | } 95 | 96 | lock.unlock(); 97 | } 98 | 99 | void lock_queue() 100 | { 101 | /* Do not allow to manipulate the Queue */ 102 | lock.lock(); 103 | } 104 | 105 | void release_queue() 106 | { 107 | /* Allow Access to the Queue Manipulating-Functions */ 108 | lock.unlock(); 109 | } 110 | 111 | private: 112 | LockType lock; 113 | std::vector c; 114 | }; 115 | } 116 | -------------------------------------------------------------------------------- /libcron/include/libcron/CronRandomization.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include "CronData.h" 8 | 9 | namespace libcron 10 | { 11 | class CronRandomization 12 | { 13 | public: 14 | std::tuple parse(const std::string& cron_schedule); 15 | 16 | CronRandomization(); 17 | 18 | CronRandomization(const CronRandomization&) = delete; 19 | 20 | CronRandomization & operator=(const CronRandomization &) = delete; 21 | 22 | private: 23 | template 24 | std::pair get_random_in_range(const std::string& section, 25 | int& selected_value, 26 | std::pair limit = std::make_pair(-1, -1)); 27 | 28 | std::pair day_limiter(const std::set& month); 29 | 30 | int cap(int value, int lower, int upper); 31 | 32 | std::regex const rand_expression{ R"#([rR]\((\d+)\-(\d+)\))#", std::regex_constants::ECMAScript }; 33 | std::random_device rd{}; 34 | std::mt19937 twister; 35 | }; 36 | 37 | template 38 | std::pair CronRandomization::get_random_in_range(const std::string& section, 39 | int& selected_value, 40 | std::pair limit) 41 | { 42 | auto res = std::make_pair(true, std::string{}); 43 | selected_value = -1; 44 | 45 | std::smatch random_match; 46 | 47 | if (std::regex_match(section.cbegin(), section.cend(), random_match, rand_expression)) 48 | { 49 | // Random range, get left and right numbers. 50 | auto left = std::stoi(random_match[1].str()); 51 | auto right = std::stoi(random_match[2].str()); 52 | 53 | if (limit.first != -1 && limit.second != -1) 54 | { 55 | left = cap(left, limit.first, limit.second); 56 | right = cap(right, limit.first, limit.second); 57 | } 58 | 59 | libcron::CronData cd; 60 | std::set numbers; 61 | res.first = cd.convert_from_string_range_to_number_range( 62 | std::to_string(left) + "-" + std::to_string(right), numbers); 63 | 64 | // Remove items outside limits. 65 | if (limit.first != -1 && limit.second != -1) 66 | { 67 | for (auto it = numbers.begin(); it != numbers.end(); ) 68 | { 69 | if (CronData::value_of(*it) < limit.first || CronData::value_of(*it) > limit.second) 70 | { 71 | it = numbers.erase(it); 72 | } 73 | else 74 | { 75 | ++it; 76 | } 77 | } 78 | } 79 | 80 | if (res.first) 81 | { 82 | // Generate random indexes to select one of the numbers in the range. 83 | std::uniform_int_distribution<> dis(0, static_cast(numbers.size() - 1)); 84 | 85 | // Select the random number to use as the schedule 86 | auto it = numbers.begin(); 87 | std::advance(it, dis(twister)); 88 | selected_value = CronData::value_of(*it); 89 | res.second = std::to_string(selected_value); 90 | } 91 | } 92 | else 93 | { 94 | // Not random, just append input to output. 95 | res.second = section; 96 | } 97 | 98 | return res; 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /libcron/src/CronSchedule.cpp: -------------------------------------------------------------------------------- 1 | #include "libcron/CronSchedule.h" 2 | #include 3 | 4 | using namespace std::chrono; 5 | using namespace date; 6 | 7 | namespace libcron 8 | { 9 | 10 | std::tuple 11 | CronSchedule::calculate_from(const std::chrono::system_clock::time_point& from) const 12 | { 13 | auto curr = from; 14 | 15 | bool done = false; 16 | auto max_iterations = std::numeric_limits::max(); 17 | 18 | while (!done && --max_iterations > 0) 19 | { 20 | bool date_changed = false; 21 | year_month_day ymd = date::floor(curr); 22 | 23 | // Add months until one of the allowed days are found, or stay at the current one. 24 | if (data.get_months().find(static_cast(unsigned(ymd.month()))) == data.get_months().end()) 25 | { 26 | auto next_month = ymd + months{1}; 27 | sys_days s = next_month.year() / next_month.month() / 1; 28 | curr = s; 29 | date_changed = true; 30 | } 31 | // If all days are allowed (or the field is ignored via '?'), then the 'day of week' takes precedence. 32 | else if (data.get_day_of_month().size() != CronData::value_of(DayOfMonth::Last)) 33 | { 34 | // Add days until one of the allowed days are found, or stay at the current one. 35 | if (data.get_day_of_month().find(static_cast(unsigned(ymd.day()))) == 36 | data.get_day_of_month().end()) 37 | { 38 | sys_days s = ymd; 39 | curr = s; 40 | curr += days{1}; 41 | date_changed = true; 42 | } 43 | } 44 | else 45 | { 46 | //Add days until the current weekday is one of the allowed weekdays 47 | year_month_weekday ymw = date::floor(curr); 48 | 49 | if (data.get_day_of_week().find(static_cast(ymw.weekday().c_encoding())) == 50 | data.get_day_of_week().end()) 51 | { 52 | sys_days s = ymd; 53 | curr = s; 54 | curr += days{1}; 55 | date_changed = true; 56 | } 57 | } 58 | 59 | if (!date_changed) 60 | { 61 | auto date_time = to_calendar_time(curr); 62 | if (data.get_hours().find(static_cast(date_time.hour)) == data.get_hours().end()) 63 | { 64 | curr += hours{1}; 65 | curr -= minutes{date_time.min}; 66 | curr -= seconds{date_time.sec}; 67 | } 68 | else if (data.get_minutes().find(static_cast(date_time.min)) == data.get_minutes().end()) 69 | { 70 | curr += minutes{1}; 71 | curr -= seconds{date_time.sec}; 72 | } 73 | else if (data.get_seconds().find(static_cast(date_time.sec)) == data.get_seconds().end()) 74 | { 75 | curr += seconds{1}; 76 | } 77 | else 78 | { 79 | done = true; 80 | } 81 | } 82 | } 83 | 84 | // Discard fraction seconds in the calculated schedule time 85 | // that may leftover from the argument `from`, which in turn comes from `now()`. 86 | // Fraction seconds will potentially make the task be triggered more than 1 second late 87 | // if the `tick()` within the same second is earlier than schedule time, 88 | // in that the task will not trigger until the next `tick()` next second. 89 | // By discarding fraction seconds in the scheduled time, 90 | // the `tick()` within the same second will never be earlier than schedule time, 91 | // and the task will trigger in that `tick()`. 92 | curr -= curr.time_since_epoch() % seconds{1}; 93 | 94 | return std::make_tuple(max_iterations > 0, curr); 95 | } 96 | } -------------------------------------------------------------------------------- /test/CronRandomizationTest.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | 9 | using namespace libcron; 10 | const auto EXPECT_FAILURE = true; 11 | 12 | void test(const char* const random_schedule, bool expect_failure = false) 13 | { 14 | libcron::CronRandomization cr; 15 | 16 | for (int i = 0; i < 5000; ++i) 17 | { 18 | auto res = cr.parse(random_schedule); 19 | auto schedule = std::get<1>(res); 20 | 21 | Cron<> cron; 22 | 23 | if(expect_failure) 24 | { 25 | // Parsing of random might succeed, but it yields an invalid schedule. 26 | auto r = std::get<0>(res) && cron.add_schedule("validate schedule", schedule, [](auto&) {}); 27 | REQUIRE_FALSE(r); 28 | } 29 | else 30 | { 31 | REQUIRE(std::get<0>(res)); 32 | REQUIRE(cron.add_schedule("validate schedule", schedule, [](auto&) {})); 33 | 34 | } 35 | } 36 | } 37 | 38 | SCENARIO("Randomize all the things") 39 | { 40 | const char* random_schedule = "R(0-59) R(0-59) R(0-23) R(1-31) R(1-12) ?"; 41 | 42 | GIVEN(random_schedule) 43 | { 44 | THEN("Only valid schedules generated") 45 | { 46 | test(random_schedule); 47 | } 48 | } 49 | } 50 | 51 | SCENARIO("Randomize all the things with reverse ranges") 52 | { 53 | const char* random_schedule = "R(45-15) R(30-0) R(18-2) R(28-15) R(8-3) ?"; 54 | 55 | GIVEN(random_schedule) 56 | { 57 | THEN("Only valid schedules generated") 58 | { 59 | test(random_schedule); 60 | } 61 | } 62 | } 63 | 64 | SCENARIO("Randomize all the things - day of week") 65 | { 66 | const char* random_schedule = "R(0-59) R(0-59) R(0-23) ? R(1-12) R(0-6)"; 67 | 68 | GIVEN(random_schedule) 69 | { 70 | THEN("Only valid schedules generated") 71 | { 72 | test(random_schedule); 73 | } 74 | } 75 | } 76 | 77 | SCENARIO("Randomize all the things with reverse ranges - day of week") 78 | { 79 | const char* random_schedule = "R(45-15) R(30-0) R(18-2) ? R(8-3) R(4-1)"; 80 | 81 | GIVEN(random_schedule) 82 | { 83 | THEN("Only valid schedules generated") 84 | { 85 | test(random_schedule); 86 | } 87 | } 88 | } 89 | 90 | SCENARIO("Test readme examples") 91 | { 92 | GIVEN("0 0 R(13-20) * * ?") 93 | { 94 | THEN("Valid schedule generated") 95 | { 96 | test("0 0 R(13-20) * * ?"); 97 | } 98 | } 99 | 100 | GIVEN("0 0 0 ? * R(0-6)") 101 | { 102 | THEN("Valid schedule generated") 103 | { 104 | test("0 0 0 ? * R(0-6)"); 105 | } 106 | } 107 | 108 | GIVEN("0 R(45-15) */12 ? * *") 109 | { 110 | THEN("Valid schedule generated") 111 | { 112 | test("0 R(45-15) */12 ? * *"); 113 | } 114 | } 115 | } 116 | 117 | SCENARIO("Randomization using text versions of days and months") 118 | { 119 | GIVEN("0 0 0 ? * R(TUE-FRI)") 120 | { 121 | THEN("Valid schedule generated") 122 | { 123 | test("0 0 0 ? * R(TUE-FRI)"); 124 | } 125 | } 126 | 127 | GIVEN("Valid schedule") 128 | { 129 | THEN("Valid schedule generated") 130 | { 131 | test("0 0 0 ? R(JAN-DEC) R(MON-FRI)"); 132 | } 133 | AND_WHEN("Given 0 0 0 ? R(DEC-MAR) R(SAT-SUN)") 134 | { 135 | THEN("Valid schedule generated") 136 | { 137 | test("0 0 0 ? R(DEC-MAR) R(SAT-SUN)"); 138 | } 139 | } 140 | AND_THEN("Given 0 0 0 ? R(JAN-FEB) *") 141 | { 142 | THEN("Valid schedule generated") 143 | { 144 | test("0 0 0 ? R(JAN-FEB) *"); 145 | } 146 | } 147 | AND_THEN("Given 0 0 0 ? R(OCT-OCT) *") 148 | { 149 | THEN("Valid schedule generated") 150 | { 151 | test("0 0 0 ? R(OCT-OCT) *"); 152 | } 153 | } 154 | } 155 | 156 | GIVEN("Invalid schedule") 157 | { 158 | THEN("No schedule generated") 159 | { 160 | // Day of month specified - not allowed with day of week 161 | test("0 0 0 1 R(JAN-DEC) R(MON-SUN)", EXPECT_FAILURE); 162 | } 163 | AND_THEN("No schedule generated") 164 | { 165 | // Invalid range 166 | test("0 0 0 ? R(JAN) *", EXPECT_FAILURE); 167 | } 168 | AND_THEN("No schedule generated") 169 | { 170 | // Days in month field 171 | test("0 0 0 ? R(MON-TUE) *", EXPECT_FAILURE); 172 | } 173 | AND_THEN("No schedule generated") 174 | { 175 | // Month in day field 176 | test("0 0 0 ? * R(JAN-JUN)", EXPECT_FAILURE); 177 | } 178 | 179 | } 180 | } 181 | -------------------------------------------------------------------------------- /libcron/src/CronRandomization.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | 11 | namespace libcron 12 | { 13 | CronRandomization::CronRandomization() 14 | : twister(rd()) 15 | { 16 | } 17 | 18 | std::tuple CronRandomization::parse(const std::string& cron_schedule) 19 | { 20 | // Split on space to get each separate part, six parts expected 21 | const std::regex split{ R"#(^\s*(.*?)\s+(.*?)\s+(.*?)\s+(.*?)\s+(.*?)\s+(.*?)\s*$)#", 22 | std::regex_constants::ECMAScript }; 23 | 24 | std::smatch all_sections; 25 | auto res = std::regex_match(cron_schedule.cbegin(), cron_schedule.cend(), all_sections, split); 26 | 27 | // Replace text with numbers 28 | std::string working_copy{}; 29 | 30 | if (res) 31 | { 32 | // Merge seconds, minutes, hours and day of month back together 33 | working_copy += all_sections[1].str(); 34 | working_copy += " "; 35 | working_copy += all_sections[2].str(); 36 | working_copy += " "; 37 | working_copy += all_sections[3].str(); 38 | working_copy += " "; 39 | working_copy += all_sections[4].str(); 40 | working_copy += " "; 41 | 42 | // Replace month names 43 | auto month = all_sections[5].str(); 44 | CronData::replace_string_name_with_numeric(month); 45 | 46 | working_copy += " "; 47 | working_copy += month; 48 | 49 | // Replace day names 50 | auto dow = all_sections[6].str(); 51 | CronData::replace_string_name_with_numeric(dow); 52 | 53 | working_copy += " "; 54 | working_copy += dow; 55 | } 56 | 57 | std::string final_cron_schedule{}; 58 | 59 | // Split again on space 60 | res = res && std::regex_match(working_copy.cbegin(), working_copy.cend(), all_sections, split); 61 | 62 | if (res) 63 | { 64 | int selected_value = -1; 65 | auto second = get_random_in_range(all_sections[1].str(), selected_value); 66 | res = second.first; 67 | final_cron_schedule = second.second; 68 | 69 | auto minute = get_random_in_range(all_sections[2].str(), selected_value); 70 | res &= minute.first; 71 | final_cron_schedule += " " + minute.second; 72 | 73 | auto hour = get_random_in_range(all_sections[3].str(), selected_value); 74 | res &= hour.first; 75 | final_cron_schedule += " " + hour.second; 76 | 77 | // Do Month before DayOfMonth to allow capping the allowed range. 78 | auto month = get_random_in_range(all_sections[5].str(), selected_value); 79 | res &= month.first; 80 | 81 | std::set month_range{}; 82 | 83 | if (selected_value == -1) 84 | { 85 | // Month is not specific, get the range. 86 | CronData cr; 87 | res &= cr.convert_from_string_range_to_number_range(all_sections[5].str(), month_range); 88 | } 89 | else 90 | { 91 | month_range.emplace(static_cast(selected_value)); 92 | } 93 | 94 | auto limits = day_limiter(month_range); 95 | 96 | auto day_of_month = get_random_in_range(all_sections[4].str(), 97 | selected_value, 98 | limits); 99 | 100 | res &= day_of_month.first; 101 | final_cron_schedule += " " + day_of_month.second + " " + month.second; 102 | 103 | auto day_of_week = get_random_in_range(all_sections[6].str(), selected_value); 104 | res &= day_of_week.first; 105 | final_cron_schedule += " " + day_of_week.second; 106 | } 107 | 108 | return { res, final_cron_schedule }; 109 | } 110 | 111 | std::pair CronRandomization::day_limiter(const std::set& months) 112 | { 113 | int max = CronData::value_of(DayOfMonth::Last); 114 | 115 | for (auto month : months) 116 | { 117 | if (month == Months::February) 118 | { 119 | // Limit to 29 days, possibly causing delaying schedule until next leap year. 120 | max = std::min(max, 29); 121 | } 122 | else if (std::find(std::begin(CronData::months_with_31), 123 | std::end(CronData::months_with_31), 124 | month) == std::end(CronData::months_with_31)) 125 | { 126 | // Not among the months with 31 days 127 | max = std::min(max, 30); 128 | } 129 | } 130 | 131 | auto res = std::pair{ CronData::value_of(DayOfMonth::First), max }; 132 | 133 | return res; 134 | } 135 | 136 | int CronRandomization::cap(int value, int lower, int upper) 137 | { 138 | return std::max(std::min(value, upper), lower); 139 | } 140 | } 141 | -------------------------------------------------------------------------------- /libcron/src/CronData.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include "libcron/CronData.h" 3 | 4 | using namespace date; 5 | 6 | namespace libcron 7 | { 8 | const constexpr Months CronData::months_with_31[NUMBER_OF_LONG_MONTHS] = { Months::January, 9 | Months::March, 10 | Months::May, 11 | Months::July, 12 | Months::August, 13 | Months::October, 14 | Months::December }; 15 | 16 | const std::vector CronData::month_names{ "JAN", "FEB", "MAR", "APR", "MAY", "JUN", "JUL", "AUG", "SEP", "OCT", "NOV", "DEC" }; 17 | const std::vector CronData::day_names{ "SUN", "MON", "TUE", "WED", "THU", "FRI", "SAT" }; 18 | std::unordered_map CronData::cache{}; 19 | 20 | CronData CronData::create(const std::string& cron_expression) 21 | { 22 | CronData c; 23 | auto found = cache.find(cron_expression); 24 | 25 | if (found == cache.end()) 26 | { 27 | c.parse(cron_expression); 28 | cache[cron_expression] = c; 29 | } 30 | else 31 | { 32 | c = found->second; 33 | } 34 | 35 | return c; 36 | } 37 | 38 | void CronData::parse(const std::string& cron_expression) 39 | { 40 | // First, check for "convenience scheduling" using @yearly, @annually, 41 | // @monthly, @weekly, @daily or @hourly. 42 | std::string tmp = std::regex_replace(cron_expression, std::regex("@yearly"), "0 0 0 1 1 *"); 43 | tmp = std::regex_replace(tmp, std::regex("@annually"), "0 0 0 1 1 *"); 44 | tmp = std::regex_replace(tmp, std::regex("@monthly"), "0 0 0 1 * *"); 45 | tmp = std::regex_replace(tmp, std::regex("@weekly"), "0 0 0 * * 0"); 46 | tmp = std::regex_replace(tmp, std::regex("@daily"), "0 0 0 * * ?"); 47 | const std::string expression = std::regex_replace(tmp, std::regex("@hourly"), "0 0 * * * ?"); 48 | 49 | // Second, split on white-space. We expect six parts. 50 | std::regex split{ R"#(^\s*(.*?)\s+(.*?)\s+(.*?)\s+(.*?)\s+(.*?)\s+(.*?)\s*$)#", 51 | std::regex_constants::ECMAScript }; 52 | 53 | std::smatch match; 54 | 55 | if (std::regex_match(expression.begin(), expression.end(), match, split)) 56 | { 57 | valid = validate_numeric(match[1], seconds); 58 | valid &= validate_numeric(match[2], minutes); 59 | valid &= validate_numeric(match[3], hours); 60 | valid &= validate_numeric(match[4], day_of_month); 61 | valid &= validate_literal(match[5], months, month_names); 62 | valid &= validate_literal(match[6], day_of_week, day_names); 63 | valid &= check_dom_vs_dow(match[4], match[6]); 64 | valid &= validate_date_vs_months(); 65 | } 66 | } 67 | 68 | std::vector CronData::split(const std::string& s, char token) 69 | { 70 | std::vector res; 71 | 72 | std::string r = "["; 73 | r += token; 74 | r += "]"; 75 | std::regex splitter{ r, std::regex_constants::ECMAScript }; 76 | 77 | std::copy(std::sregex_token_iterator(s.begin(), s.end(), splitter, -1), 78 | std::sregex_token_iterator(), 79 | std::back_inserter(res)); 80 | 81 | return res; 82 | } 83 | 84 | bool CronData::is_number(const std::string& s) 85 | { 86 | // Find any character that isn't a number. 87 | return !s.empty() 88 | && std::find_if(s.begin(), s.end(), 89 | [](char c) 90 | { 91 | return !std::isdigit(c); 92 | }) == s.end(); 93 | } 94 | 95 | bool CronData::is_between(int32_t value, int32_t low_limit, int32_t high_limt) 96 | { 97 | return value >= low_limit && value <= high_limt; 98 | } 99 | 100 | bool CronData::validate_date_vs_months() const 101 | { 102 | bool res = true; 103 | 104 | // Verify that the available dates are possible based on the given months 105 | if (months.size() == 1 && months.find(static_cast(2)) != months.end()) 106 | { 107 | // Only february allowed, make sure that the allowed date(s) includes 29 and below. 108 | res = has_any_in_range(day_of_month, 1, 29); 109 | } 110 | 111 | if (res) 112 | { 113 | // Make sure that if the days contains only 31, at least one month allows that date. 114 | if (day_of_month.size() == 1 && day_of_month.find(DayOfMonth::Last) != day_of_month.end()) 115 | { 116 | res = false; 117 | 118 | for (size_t i = 0; !res && i < NUMBER_OF_LONG_MONTHS; ++i) 119 | { 120 | res = months.find(months_with_31[i]) != months.end(); 121 | } 122 | } 123 | } 124 | 125 | return res; 126 | } 127 | 128 | bool CronData::check_dom_vs_dow(const std::string& dom, const std::string& dow) const 129 | { 130 | // Day of month and day of week are mutually exclusive so one of them must at always be ignored using 131 | // the '?'-character unless one field already is something other than '*'. 132 | // 133 | // Since we treat an ignored field as allowing the full range, we're OK with both being flagged 134 | // as ignored. To make it explicit to the user of the library, we do however require the use of 135 | // '?' as the ignore flag, although it is functionally equivalent to '*'. 136 | 137 | auto check = [](const std::string& l, std::string r) 138 | { 139 | return l == "*" && (r != "*" || r == "?"); 140 | }; 141 | 142 | return (dom == "?" || dow == "?") 143 | || check(dom, dow) 144 | || check(dow, dom); 145 | } 146 | } 147 | -------------------------------------------------------------------------------- /uncrustify.cfg: -------------------------------------------------------------------------------- 1 | # Uncrustify-0.67-87-d75a44aa9 2 | newlines = lf 3 | input_tab_size = 4 4 | output_tab_size = 4 5 | string_replace_tab_chars = true 6 | utf8_bom = remove 7 | sp_arith = force 8 | sp_arith_additive = force 9 | sp_assign = force 10 | sp_cpp_lambda_assign = remove 11 | sp_cpp_lambda_paren = remove 12 | sp_assign_default = force 13 | sp_after_assign = force 14 | sp_enum_paren = force 15 | sp_enum_assign = force 16 | sp_enum_before_assign = force 17 | sp_enum_after_assign = force 18 | sp_pp_stringify = remove 19 | sp_before_pp_stringify = remove 20 | sp_bool = force 21 | sp_compare = force 22 | sp_inside_paren = remove 23 | sp_paren_paren = remove 24 | sp_before_ptr_star = remove 25 | sp_before_unnamed_ptr_star = remove 26 | sp_between_ptr_star = remove 27 | sp_after_ptr_star = force 28 | sp_after_ptr_block_caret = remove 29 | sp_after_ptr_star_qualifier = force 30 | sp_after_ptr_star_func = force 31 | sp_ptr_star_paren = force 32 | sp_before_ptr_star_func = force 33 | sp_before_byref = remove 34 | sp_before_unnamed_byref = remove 35 | sp_after_byref = force 36 | sp_after_byref_func = force 37 | sp_before_byref_func = force 38 | sp_before_angle = remove 39 | sp_inside_angle = remove 40 | sp_angle_colon = force 41 | sp_after_angle = force 42 | sp_angle_paren_empty = remove 43 | sp_angle_word = force 44 | sp_angle_shift = remove 45 | sp_permit_cpp11_shift = true 46 | sp_before_sparen = force 47 | sp_inside_sparen = remove 48 | sp_inside_sparen_close = remove 49 | sp_inside_sparen_open = remove 50 | sp_after_sparen = force 51 | sp_sparen_brace = force 52 | sp_special_semi = remove 53 | sp_before_semi_for = remove 54 | sp_before_semi_for_empty = remove 55 | sp_after_semi = remove 56 | sp_after_semi_for_empty = force 57 | sp_after_comma = force 58 | sp_before_ellipsis = remove 59 | sp_after_class_colon = force 60 | sp_before_class_colon = force 61 | sp_after_constr_colon = force 62 | sp_before_constr_colon = force 63 | sp_after_operator = remove 64 | sp_after_operator_sym = remove 65 | sp_after_cast = remove 66 | sp_inside_paren_cast = remove 67 | sp_cpp_cast_paren = remove 68 | sp_sizeof_paren = remove 69 | sp_inside_braces_enum = force 70 | sp_inside_braces_struct = force 71 | sp_after_type_brace_init_lst_open = force 72 | sp_before_type_brace_init_lst_close = force 73 | sp_inside_type_brace_init_lst = force 74 | sp_inside_braces_empty = remove 75 | sp_type_func = force 76 | sp_type_brace_init_lst = remove 77 | sp_func_proto_paren = remove 78 | sp_func_proto_paren_empty = remove 79 | sp_func_def_paren = remove 80 | sp_inside_tparen = remove 81 | sp_after_tparen_close = remove 82 | sp_square_fparen = remove 83 | sp_fparen_brace = force 84 | sp_fparen_dbrace = force 85 | sp_func_call_paren = remove 86 | sp_func_class_paren_empty = remove 87 | sp_return_paren = remove 88 | sp_attribute_paren = remove 89 | sp_defined_paren = remove 90 | sp_throw_paren = remove 91 | sp_after_throw = force 92 | sp_catch_paren = force 93 | sp_oc_catch_paren = force 94 | sp_else_brace = force 95 | sp_brace_else = force 96 | sp_before_dc = remove 97 | sp_after_dc = remove 98 | sp_before_nl_cont = force 99 | sp_cond_question = force 100 | sp_after_new = force 101 | sp_between_new_paren = remove 102 | sp_inside_newop_paren = force 103 | sp_inside_newop_paren_open = remove 104 | sp_inside_newop_paren_close = remove 105 | indent_columns = 4 106 | indent_with_tabs = 0 107 | indent_align_string = true 108 | indent_namespace = true 109 | indent_namespace_level = 4 110 | indent_class = true 111 | indent_constr_colon = true 112 | indent_ctor_init = 4 113 | indent_access_spec = 0 114 | indent_access_spec_body = true 115 | indent_cpp_lambda_body = true 116 | indent_cpp_lambda_only_once = true 117 | nl_assign_leave_one_liners = true 118 | nl_class_leave_one_liners = true 119 | nl_enum_leave_one_liners = true 120 | nl_getset_leave_one_liners = true 121 | nl_func_leave_one_liners = true 122 | nl_cpp_lambda_leave_one_liners = true 123 | nl_start_of_file = remove 124 | nl_end_of_file = force 125 | nl_end_of_file_min = 1 126 | nl_enum_brace = force 127 | nl_enum_class = remove 128 | nl_enum_class_identifier = remove 129 | nl_if_brace = force 130 | nl_brace_else = force 131 | nl_elseif_brace = force 132 | nl_else_brace = force 133 | nl_else_if = remove 134 | nl_before_if_closing_paren = remove 135 | nl_brace_finally = force 136 | nl_finally_brace = force 137 | nl_try_brace = force 138 | nl_for_brace = force 139 | nl_catch_brace = force 140 | nl_while_brace = force 141 | nl_do_brace = force 142 | nl_brace_while = force 143 | nl_enum_own_lines = force 144 | nl_func_type_name = remove 145 | nl_func_decl_empty = remove 146 | nl_func_def_empty = remove 147 | nl_func_call_empty = remove 148 | nl_return_expr = remove 149 | nl_after_semicolon = true 150 | nl_after_brace_close = true 151 | nl_before_if = force 152 | nl_after_if = force 153 | nl_before_for = force 154 | nl_after_for = force 155 | nl_before_while = force 156 | nl_after_while = force 157 | nl_before_switch = force 158 | nl_after_switch = force 159 | nl_before_synchronized = force 160 | nl_after_synchronized = force 161 | nl_before_do = force 162 | nl_after_do = force 163 | nl_max = 2 164 | nl_after_func_proto = 2 165 | nl_after_func_proto_group = 2 166 | nl_after_func_class_proto = 2 167 | nl_after_func_class_proto_group = 2 168 | nl_before_func_body_def = 2 169 | nl_before_func_body_proto = 2 170 | nl_after_func_body = 2 171 | nl_after_func_body_class = 2 172 | nl_after_func_body_one_liner = 1 173 | nl_before_block_comment = 2 174 | nl_before_c_comment = 2 175 | nl_before_cpp_comment = 2 176 | nl_after_multiline_comment = true 177 | nl_after_label_colon = true 178 | nl_after_struct = 2 179 | nl_before_class = 2 180 | nl_after_class = 2 181 | nl_before_access_spec = 1 182 | nl_after_access_spec = 1 183 | nl_comment_func_def = 1 184 | nl_after_try_catch_finally = 2 185 | nl_around_cs_property = 2 186 | nl_between_get_set = 2 187 | eat_blanks_after_open_brace = true 188 | eat_blanks_before_close_brace = true 189 | nl_before_return = true 190 | pos_arith = lead 191 | code_width = 120 192 | ls_for_split_full = true 193 | ls_func_split_full = true 194 | cmt_width = 120 195 | cmt_reflow_mode = 2 196 | cmt_indent_multi = false 197 | cmt_c_group = true 198 | cmt_sp_after_star_cont = 1 199 | mod_full_brace_do = force 200 | mod_full_brace_for = force 201 | mod_full_brace_function = force 202 | mod_full_brace_if = force 203 | mod_full_brace_nl_block_rem_mlcond = true 204 | mod_full_brace_while = force 205 | mod_full_brace_using = force 206 | mod_paren_on_return = remove 207 | mod_remove_extra_semicolon = true 208 | mod_sort_using = true 209 | mod_case_brace = force 210 | mod_remove_empty_return = true 211 | pp_ignore_define_body = true 212 | use_indent_func_call_param = false 213 | # option(s) with 'not default' value: 209 214 | # 215 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # libcron 2 | A C++ scheduling library using cron formatting. 3 | 4 | # Using the Scheduler 5 | 6 | Libcron offers an easy to use API to add callbacks with corresponding cron-formatted strings: 7 | 8 | ``` 9 | #include 10 | #include 11 | #include 12 | #include 13 | using namespace std::chrono_literals; 14 | 15 | libcron::Cron cron; 16 | 17 | cron.add_schedule("Hello from Cron", "* * * * * ?", [=](auto&) { 18 | std::cout << "Hello from libcron!" << std::endl; 19 | }); 20 | ``` 21 | 22 | To trigger the execution of callbacks, one must call `libcron::Cron::tick` at least once a second to prevent missing schedules: 23 | 24 | ``` 25 | while(true) 26 | { 27 | cron.tick(); 28 | std::this_thread::sleep_for(500ms); 29 | } 30 | ``` 31 | 32 | In case there is a lot of time between you call `add_schedule` and `tick`, you can call `recalculate_schedule`. 33 | 34 | The callback must have the following signature: 35 | 36 | ``` 37 | std::function 38 | ``` 39 | 40 | `libcron::Taskinformation` offers a convenient API to retrieve further information: 41 | 42 | - `libcron::TaskInformation::get_delay` informs about the delay between planned and actual execution of the callback. Hence, it is possible to ensure that a task was executed within a specific tolerance: 43 | 44 | ``` 45 | libcron::Cron cron; 46 | 47 | cron.add_schedule("Hello from Cron", "* * * * * ?", [=](auto& i) { 48 | using namespace std::chrono_literals; 49 | if (i.get_delay() >= 1s) 50 | { 51 | std::cout << "The Task was executed too late..." << std::endl; 52 | } 53 | }); 54 | ``` 55 | 56 | - `libcron::TaskInformation::get_name` gives you the name of the current Task. This allows to add attach the same callback to multiple schedules: 57 | 58 | ``` 59 | libcron::Cron cron; 60 | 61 | auto f = [](auto& i) { 62 | if (i.get_name() == "Task 1") 63 | { 64 | do_work_task_1(); 65 | } 66 | else if (i.get_name() == "Task 2") 67 | { 68 | do_work_task_2(); 69 | } 70 | }; 71 | 72 | cron.add_schedule("Task 1", "* * * * * ?", f); 73 | cron.add_schedule("Task 2", "* * * * * ?", f); 74 | ``` 75 | 76 | ## Adding multiple tasks with individual schedules at once 77 | 78 | libcron::cron::add_schedule needs to sort the underlying container each time you add a schedule. To improve performance when adding many tasks by only sorting once, there is a convinient way to pass either a `std::map`, a `std::vector>`, a `std::vector>` or a `std::unordered_map` to `add_schedule`, where the first element corresponds to the task name and the second element to the task schedule. Only if all schedules in the container are valid, they will be added to `libcron::Cron`. The return type is a `std::tuple`, where the boolean is `true` if the schedules have been added or false otherwise. If the schedules have not been added, the second element in the tuple corresponds to the task-name with the given invalid schedule. If there are multiple invalid schedules in the container, `add_schedule` will abort at the first invalid element: 79 | 80 | ``` 81 | std::map name_schedule_map; 82 | for(int i = 1; i <= 1000; i++) 83 | { 84 | name_schedule_map["Task-" + std::to_string(i)] = "* * * * * ?"; 85 | } 86 | name_schedule_map["Task-1000"] = "invalid"; 87 | auto res = c1.add_schedule(name_schedule_map, [](auto&) { }); 88 | if (std::get<0>(res) == false) 89 | { 90 | std::cout << "Task " << std::get<1>(res) 91 | << "has an invalid schedule: " 92 | << std::get<2>(res) << std::endl; 93 | } 94 | ``` 95 | 96 | 97 | 98 | ## Removing schedules from `libcron::Cron` 99 | 100 | libcron::Cron offers two convenient functions to remove schedules: 101 | 102 | - `clear_schedules()` will remove all schedules 103 | - `remove_schedule(std::string)` will remove a specific schedule 104 | 105 | For example, `cron.remove_schedule("Hello from Cron")` will remove the previously added task. 106 | 107 | 108 | 109 | ## Removing/Adding tasks at runtime in a multithreaded environment 110 | 111 | When Calling `libcron::Cron::tick` from another thread than `add_schedule`, `clear_schedule` and `remove_schedule`, one must take care to protect the internal resources of `libcron::Cron` so that tasks are not removed or added while `libcron::Cron` is iterating over the schedules. `libcron::Cron` can take care of that, you simply have to define your own aliases: 112 | 113 | ``` 114 | /* The default class uses NullLock, which does not lock the resources at runtime */ 115 | template 116 | class Cron 117 | { 118 | ... 119 | } 120 | 121 | /* Define an alias for a thread-safe Cron scheduler which automatically locks ressources when needed */ 122 | using CronMt = libcron::Cron 123 | 124 | CronMt cron; 125 | cron.add_schedule("Hello from Cron", "* * * * * ?", [=]() { 126 | std::cout << "Hello from CronMt!" std::endl; 127 | }); 128 | 129 | .... 130 | ``` 131 | 132 | However, this comes with costs: Whenever you call `tick`, a `std::mutex` will be locked and unlocked. So only use the `libcron::Locker` to protect resources when you really need too. 133 | 134 | ## Local time vs UTC 135 | 136 | This library uses `std::chrono::system_clock::timepoint` as its time unit. While that is UTC by default, the Cron-class 137 | uses a `LocalClock` by default which offsets `system_clock::now()` by the current UTC-offset. If you wish to work in 138 | UTC, then construct the Cron instance, passing it a `libcron::UTCClock`. 139 | 140 | # Supported formatting 141 | 142 | This implementation supports cron format, as specified below. 143 | 144 | Each schedule expression conststs of 6 parts, all mandatory. However, if 'day of month' specifies specific days, then 'day of week' is ignored. 145 | 146 | ```text 147 | ┌──────────────seconds (0 - 59) 148 | │ ┌───────────── minute (0 - 59) 149 | │ │ ┌───────────── hour (0 - 23) 150 | │ │ │ ┌───────────── day of month (1 - 31) 151 | │ │ │ │ ┌───────────── month (1 - 12) 152 | │ │ │ │ │ ┌───────────── day of week (0 - 6) (Sunday to Saturday) 153 | │ │ │ │ │ │ 154 | │ │ │ │ │ │ 155 | │ │ │ │ │ │ 156 | * * * * * * 157 | ``` 158 | * Allowed formats: 159 | * Special characters: '*', meaning the entire range. 160 | * '?' used to ignore day of month/day of week as noted below. 161 | 162 | * Ranges: 1,2,4-6 163 | * Result: 1,2,4,5,6 164 | * Steps: n/m, where n is the start and m is the step. 165 | * `1/2` yields 1,3,5,7... 166 | * `5/3` yields 5,8,11,14... 167 | * `*/2` yields Result: 1,3,5,7... 168 | * Reversed ranges: 169 | * `0 0 23-2 * * *`, meaning top of each minute and hour, of hours, 23, 0, 1 and 2, every day. 170 | * Compare to `0 0 2-23 * * *` which means top of each minute and hour, of hours, 2,3...21,22,23 every day. 171 | 172 | 173 | 174 | For `month`, these (case insensitive) strings can be used instead of numbers: `JAN, FEB, MAR, APR, MAY, JUN, JUL, AUG, SEP, OCT, NOV, DEC`. 175 | Example: `JAN,MAR,SEP-NOV` 176 | 177 | For `day of week`, these (case insensitive) strings can be used instead of numbers: `SUN, MON, TUE, WED, THU, FRI, SAT`. 178 | Example: `MON-THU,SAT` 179 | 180 | Each part is separated by one or more whitespaces. It is thus important to keep whitespaces out of the respective parts. 181 | 182 | * Valid: 183 | * 0,3,40-50 * * * * ? 184 | 185 | * Invalid: 186 | * 0, 3, 40-50 * * * * ? 187 | 188 | 189 | `Day of month` and `day of week` are mutually exclusive so one of them must at always be ignored using 190 | the '?'-character to ensure that it is not possible to specify a statement which results in an impossible mix of these fields. 191 | 192 | ## Examples 193 | 194 | |Expression | Meaning 195 | | --- | --- | 196 | | * * * * * ? | Every second 197 | | 0 * * * * ? | Every minute 198 | | 0 0 12 * * MON-FRI | Every Weekday at noon 199 | | 0 0 12 1/2 * ? | Every 2 days, starting on the 1st at noon 200 | | 0 0 */12 ? * * | Every twelve hours 201 | | @hourly | Every hour 202 | 203 | Note that the expression formatting has a part for seconds and the day of week. 204 | For the day of week part, a question mark ? is utilized. This format 205 | may not be parsed by all online crontab calculators or expression generators. 206 | 207 | ## Convenience scheduling 208 | 209 | These special time specification tokens which replace the 5 initial time and date fields, and are prefixed with the '@' character, are supported: 210 | 211 | |Token|Meaning 212 | | --- | --- | 213 | | @yearly | Run once a year, ie. "0 0 0 1 1 *". 214 | | @annually | Run once a year, ie. "0 0 0 1 1 *"". 215 | | @monthly | Run once a month, ie. "0 0 0 1 * *". 216 | | @weekly | Run once a week, ie. "0 0 0 * * 0". 217 | | @daily | Run once a day, ie. "0 0 0 * * ?". 218 | | @hourly | Run once an hour, ie. "0 0 * * * ?". 219 | 220 | # Randomization 221 | 222 | The standard cron format does not allow for randomization, but with the use of `CronRandomization` you can generate random 223 | schedules using the following format: `R(range_start-range_end)`, where `range_start` and `range_end` follow the same rules 224 | as for a regular cron range (step-syntax is not supported). All the rules for a regular cron expression still applies 225 | when using randomization, i.e. mutual exclusiveness and no extra spaces. 226 | 227 | ## Examples 228 | |Expression | Meaning 229 | | --- | --- | 230 | | 0 0 R(13-20) * * ? | On the hour, on a random hour 13-20, inclusive. 231 | | 0 0 0 ? * R(0-6) | A random weekday, every week, at midnight. 232 | | 0 R(45-15) */12 ? * * | A random minute between 45-15, inclusive, every 12 hours. 233 | |0 0 0 ? R(DEC-MAR) R(SAT-SUN)| On the hour, on a random month december to march, on a random weekday saturday to sunday. 234 | 235 | 236 | # Used Third party libraries 237 | 238 | Howard Hinnant's [date libraries](https://github.com/HowardHinnant/date/) 239 | -------------------------------------------------------------------------------- /test/CronScheduleTest.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | 7 | using namespace libcron; 8 | using namespace date; 9 | using namespace std::chrono; 10 | 11 | system_clock::time_point DT(year_month_day ymd, hours h = hours{0}, minutes m = minutes{0}, seconds s = seconds{0}) 12 | { 13 | sys_days t = ymd; 14 | auto sum = t + h + m + s; 15 | return sum; 16 | } 17 | 18 | bool test(const std::string& schedule, system_clock::time_point from, 19 | const std::vector& expected_next) 20 | { 21 | auto c = CronData::create(schedule); 22 | bool res = c.is_valid(); 23 | if (res) 24 | { 25 | CronSchedule sched(c); 26 | 27 | auto curr_from = from; 28 | 29 | for (size_t i = 0; res && i < expected_next.size(); ++i) 30 | { 31 | auto result = sched.calculate_from(curr_from); 32 | auto calculated = std::get<1>(result); 33 | 34 | res = std::get<0>(result) && calculated == expected_next[i]; 35 | 36 | if (res) 37 | { 38 | // Add a second to the time so that we move on to the next expected time 39 | // and don't get locked on the current one. 40 | curr_from = expected_next[i] + seconds{1}; 41 | } 42 | else 43 | { 44 | std::cout 45 | << "From: " << curr_from << "\n" 46 | << "Expected: " << expected_next[i] << "\n" 47 | << "Calculated: " << calculated; 48 | } 49 | } 50 | } 51 | 52 | return res; 53 | } 54 | 55 | bool test(const std::string& schedule, system_clock::time_point from, system_clock::time_point expected_next) 56 | { 57 | auto c = CronData::create(schedule); 58 | bool res = c.is_valid(); 59 | if (res) 60 | { 61 | CronSchedule sched(c); 62 | auto result = sched.calculate_from(from); 63 | auto run_time = std::get<1>(result); 64 | res &= std::get<0>(result) && expected_next == run_time; 65 | 66 | if (!res) 67 | { 68 | std::cout 69 | << "From: " << from << "\n" 70 | << "Expected: " << expected_next << "\n" 71 | << "Calculated: " << run_time; 72 | } 73 | } 74 | 75 | return res; 76 | } 77 | 78 | SCENARIO("Calculating next runtime") 79 | { 80 | REQUIRE(test("0 0 * * * ?", DT(2010_y / 1 / 1), DT(2010_y / 1 / 1, hours{0}))); 81 | REQUIRE(test("0 0 * * * ?", DT(2010_y / 1 / 1, hours{0}, minutes{0}, seconds{1}), DT(2010_y / 1 / 1, hours{1}))); 82 | REQUIRE(test("0 0 * * * ?", DT(2010_y / 1 / 1, hours{5}), DT(2010_y / 1 / 1, hours{5}))); 83 | REQUIRE(test("0 0 * * * ?", DT(2010_y / 1 / 1, hours{5}, minutes{1}), DT(2010_y / 1 / 1, hours{6}))); 84 | REQUIRE(test("0 0 * * * ?", DT(2017_y / 12 / 31, hours{23}, minutes{59}, seconds{58}), 85 | DT(2018_y / 1 / 1, hours{0}))); 86 | REQUIRE(test("0 0 10 * * ?", DT(2017_y / 12 / 31, hours{9}, minutes{59}, seconds{58}), 87 | DT(2017_y / 12 / 31, hours{10}))); 88 | REQUIRE(test("0 0 10 * * ?", DT(2017_y / 12 / 31, hours{23}, minutes{59}, seconds{58}), 89 | DT(2018_y / 1 / 1, hours{10}))); 90 | REQUIRE(test("0 0 10 ? FEB *", DT(2017_y / 12 / 31, hours{23}, minutes{59}, seconds{58}), 91 | DT(2018_y / 2 / 1, hours{10}))); 92 | REQUIRE(test("0 0 10 25 FEB ?", DT(2017_y / 12 / 31, hours{23}, minutes{59}, seconds{58}), 93 | DT(2018_y / 2 / 25, hours{10}))); 94 | REQUIRE(test("0 0 10 ? FEB 1", DT(2017_y / 12 / 31, hours{23}, minutes{59}, seconds{58}), 95 | DT(year_month_day{2018_y / 2 / mon[1]}, hours{10}))); 96 | REQUIRE(test("0 0 10 ? FEB 6", DT(2017_y / 12 / 31, hours{23}, minutes{59}, seconds{58}), 97 | DT(year_month_day{2018_y / 2 / sat[1]}, hours{10}))); 98 | REQUIRE(test("* * ? 10-12 NOV ?", DT(2018_y / 11 / 11, hours{10}, minutes{11}, seconds{12}), 99 | DT(year_month_day{2018_y / 11 / 11}, hours{10}, minutes{11}, seconds{12}))); 100 | REQUIRE(test("0 0 * 31 APR,MAY ?", DT(2017_y / 6 / 1), DT(2018_y / may / 31))); 101 | } 102 | 103 | SCENARIO("Leap year") 104 | { 105 | REQUIRE(test("0 0 * 29 FEB *", DT(2015_y / 1 / 1), DT(2016_y / 2 / 29))); 106 | REQUIRE(test("0 0 * 29 FEB ?", DT(2018_y / 1 / 1), DT(2020_y / 2 / 29))); 107 | REQUIRE(test("0 0 * 29 FEB ?", DT(2020_y / 2 / 29, hours{15}, minutes{13}, seconds{13}), 108 | DT(2020_y / 2 / 29, hours{16}))); 109 | } 110 | 111 | SCENARIO("Multiple calculations") 112 | { 113 | WHEN("Every 15 minutes, every 2nd hour") 114 | { 115 | REQUIRE(test("0 0/15 0/2 * * ?", DT(2018_y / 1 / 1, hours{13}, minutes{14}, seconds{59}), 116 | {DT(2018_y / 1 / 1, hours{14}, minutes{00}), 117 | DT(2018_y / 1 / 1, hours{14}, minutes{15}), 118 | DT(2018_y / 1 / 1, hours{14}, minutes{30}), 119 | DT(2018_y / 1 / 1, hours{14}, minutes{45}), 120 | DT(2018_y / 1 / 1, hours{16}, minutes{00}), 121 | DT(2018_y / 1 / 1, hours{16}, minutes{15})})); 122 | } 123 | 124 | WHEN("Every top of the hour, every 12th hour, during 12 and 13:th July") 125 | { 126 | REQUIRE(test("0 0 0/12 12-13 JUL ?", DT(2018_y / 1 / 1), 127 | {DT(2018_y / 7 / 12, hours{0}), 128 | DT(2018_y / 7 / 12, hours{12}), 129 | DT(2018_y / 7 / 13, hours{0}), 130 | DT(2018_y / 7 / 13, hours{12}), 131 | DT(2019_y / 7 / 12, hours{0}), 132 | DT(2019_y / 7 / 12, hours{12})})); 133 | } 134 | 135 | WHEN("Every first of the month, 15h, every second month, 22m") 136 | { 137 | REQUIRE(test("0 22 15 1 * ?", DT(2018_y / 1 / 1), 138 | {DT(2018_y / 1 / 1, hours{15}, minutes{22}), 139 | DT(2018_y / 2 / 1, hours{15}, minutes{22}), 140 | DT(2018_y / 3 / 1, hours{15}, minutes{22}), 141 | DT(2018_y / 4 / 1, hours{15}, minutes{22}), 142 | DT(2018_y / 5 / 1, hours{15}, minutes{22}), 143 | DT(2018_y / 6 / 1, hours{15}, minutes{22}), 144 | DT(2018_y / 7 / 1, hours{15}, minutes{22}), 145 | DT(2018_y / 8 / 1, hours{15}, minutes{22}), 146 | DT(2018_y / 9 / 1, hours{15}, minutes{22}), 147 | DT(2018_y / 10 / 1, hours{15}, minutes{22}), 148 | DT(2018_y / 11 / 1, hours{15}, minutes{22}), 149 | DT(2018_y / 12 / 1, hours{15}, minutes{22}), 150 | DT(2019_y / 1 / 1, hours{15}, minutes{22})})); 151 | } 152 | 153 | WHEN("“At minute 0 past hour 0 and 12 on day-of-month 1 in every 2nd month") 154 | { 155 | REQUIRE(test("0 0 0,12 1 */2 ?", DT(2018_y / 3 / 10, hours{16}, minutes{51}), DT(2018_y / 5 / 1))); 156 | } 157 | 158 | WHEN("“At 00:05 in August") 159 | { 160 | REQUIRE(test("0 5 0 * 8 ?", DT(2018_y / 3 / 10, hours{16}, minutes{51}), 161 | {DT(2018_y / 8 / 1, hours{0}, minutes{5}), 162 | DT(2018_y / 8 / 2, hours{0}, minutes{5})})); 163 | } 164 | 165 | WHEN("At 22:00 on every day-of-week from Monday through Friday") 166 | { 167 | REQUIRE(test("0 0 22 ? * 1-5", DT(2021_y / 12 / 15, hours{16}, minutes{51}), 168 | {DT(2021_y / 12 / 15, hours{22}), 169 | DT(2021_y / 12 / 16, hours{22}), 170 | DT(2021_y / 12 / 17, hours{22}), 171 | // 18-19 are weekend 172 | DT(2021_y / 12 / 20, hours{22}), 173 | DT(2021_y / 12 / 21, hours{22})})); 174 | } 175 | } 176 | 177 | SCENARIO("Examples from README.md") 178 | { 179 | REQUIRE(test("* * * * * ?", DT(2018_y / 03 / 1, hours{12}, minutes{13}, seconds{45}), 180 | { 181 | DT(2018_y / 03 / 1, hours{12}, minutes{13}, seconds{45}), 182 | DT(2018_y / 03 / 1, hours{12}, minutes{13}, seconds{46}), 183 | DT(2018_y / 03 / 1, hours{12}, minutes{13}, seconds{47}), 184 | DT(2018_y / 03 / 1, hours{12}, minutes{13}, seconds{48}) 185 | })); 186 | 187 | REQUIRE(test("0 * * * * ?", DT(2018_y / 03 / 1, hours{ 12 }, minutes{ 0 }, seconds{ 10 }), 188 | { 189 | DT(2018_y / 03 / 1, hours{12}, minutes{1}, seconds{0}), 190 | DT(2018_y / 03 / 1, hours{12}, minutes{2}, seconds{0}), 191 | DT(2018_y / 03 / 1, hours{12}, minutes{3}, seconds{0}), 192 | DT(2018_y / 03 / 1, hours{12}, minutes{4}, seconds{0}) 193 | })); 194 | 195 | 196 | REQUIRE(test("0 0 12 * * MON-FRI", DT(2018_y / 03 / 10, hours{12}, minutes{13}, seconds{45}), 197 | { 198 | DT(2018_y / 03 / 12, hours{12}), 199 | DT(2018_y / 03 / 13, hours{12}), 200 | DT(2018_y / 03 / 14, hours{12}), 201 | DT(2018_y / 03 / 15, hours{12}), 202 | DT(2018_y / 03 / 16, hours{12}), 203 | DT(2018_y / 03 / 19, hours{12}) 204 | })); 205 | 206 | REQUIRE(test("0 0 12 1/2 * ?", DT(2018_y / 01 / 2, hours{12}, minutes{13}, seconds{45}), 207 | { 208 | DT(2018_y / 1 / 3, hours{12}), 209 | DT(2018_y / 1 / 5, hours{12}), 210 | DT(2018_y / 1 / 7, hours{12}) 211 | })); 212 | 213 | REQUIRE(test("0 0 */12 ? * *", DT(2018_y / 8 / 15, hours{13}, minutes{13}, seconds{45}), 214 | { 215 | DT(2018_y / 8 / 16, hours{0}), 216 | DT(2018_y / 8 / 16, hours{12}), 217 | DT(2018_y / 8 / 17, hours{0}) 218 | })); 219 | } 220 | 221 | SCENARIO("Unable to calculate time point") 222 | { 223 | REQUIRE_FALSE(test( "0 0 * 31 FEB *", DT(2021_y / 1 / 1), DT(2022_y / 1 / 1))); 224 | } 225 | -------------------------------------------------------------------------------- /libcron/include/libcron/Cron.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include "Task.h" 11 | #include "CronClock.h" 12 | #include "TaskQueue.h" 13 | 14 | namespace libcron 15 | { 16 | class NullLock 17 | { 18 | public: 19 | void lock() {} 20 | void unlock() {} 21 | }; 22 | 23 | class Locker 24 | { 25 | public: 26 | void lock() { m.lock(); } 27 | void unlock() { m.unlock(); } 28 | private: 29 | std::recursive_mutex m{}; 30 | }; 31 | 32 | template 33 | class Cron; 34 | 35 | template 36 | std::ostream& operator<<(std::ostream& stream, const Cron& c); 37 | 38 | template 40 | class Cron 41 | { 42 | public: 43 | bool add_schedule(std::string name, const std::string& schedule, Task::TaskFunction work); 44 | 45 | template> 46 | std::tuple 47 | add_schedule(const Schedules& name_schedule_map, Task::TaskFunction work); 48 | void clear_schedules(); 49 | void remove_schedule(const std::string& name); 50 | 51 | size_t count() const 52 | { 53 | return tasks.size(); 54 | } 55 | 56 | // Tick is expected to be called at least once a second to prevent missing schedules. 57 | size_t 58 | tick() 59 | { 60 | return tick(clock.now()); 61 | } 62 | 63 | size_t 64 | tick(std::chrono::system_clock::time_point now); 65 | 66 | std::chrono::system_clock::duration 67 | time_until_next() const; 68 | 69 | ClockType& get_clock() 70 | { 71 | return clock; 72 | } 73 | 74 | void recalculate_schedule() 75 | { 76 | for (auto& t : tasks.get_tasks()) 77 | { 78 | using namespace std::chrono_literals; 79 | // Ensure that next schedule is in the future 80 | t.calculate_next(clock.now() + 1s); 81 | } 82 | } 83 | 84 | void get_time_until_expiry_for_tasks( 85 | std::vector>& status) const; 86 | 87 | friend std::ostream& operator<<<>(std::ostream& stream, const Cron& c); 88 | 89 | private: 90 | TaskQueue tasks{}; 91 | ClockType clock{}; 92 | bool first_tick = true; 93 | std::chrono::system_clock::time_point last_tick{}; 94 | }; 95 | 96 | template 97 | bool Cron::add_schedule(std::string name, const std::string& schedule, Task::TaskFunction work) 98 | { 99 | auto cron = CronData::create(schedule); 100 | bool res = cron.is_valid(); 101 | if (res) 102 | { 103 | tasks.lock_queue(); 104 | Task t{std::move(name), CronSchedule{cron}, work }; 105 | if (t.calculate_next(clock.now())) 106 | { 107 | tasks.push(t); 108 | tasks.sort(); 109 | } 110 | tasks.release_queue(); 111 | } 112 | 113 | return res; 114 | } 115 | 116 | template 117 | template 118 | std::tuple 119 | Cron::add_schedule(const Schedules& name_schedule_map, Task::TaskFunction work) 120 | { 121 | bool is_valid = true; 122 | std::tuple res{false, "", ""}; 123 | 124 | std::vector tasks_to_add; 125 | tasks_to_add.reserve(name_schedule_map.size()); 126 | 127 | for (auto it = name_schedule_map.begin(); is_valid && it != name_schedule_map.end(); ++it) 128 | { 129 | const auto& [name, schedule] = *it; 130 | auto cron = CronData::create(schedule); 131 | is_valid = cron.is_valid(); 132 | if (is_valid) 133 | { 134 | Task t{std::move(name), CronSchedule{cron}, work }; 135 | if (t.calculate_next(clock.now())) 136 | { 137 | tasks_to_add.push_back(std::move(t)); 138 | } 139 | } 140 | else 141 | { 142 | std::get<1>(res) = name; 143 | std::get<2>(res) = schedule; 144 | } 145 | } 146 | 147 | // Only add tasks and sort once if all elements in the map where valid 148 | if (is_valid && tasks_to_add.size() > 0) 149 | { 150 | tasks.lock_queue(); 151 | tasks.push(tasks_to_add); 152 | tasks.sort(); 153 | tasks.release_queue(); 154 | } 155 | 156 | std::get<0>(res) = is_valid; 157 | return res; 158 | } 159 | 160 | template 161 | void Cron::clear_schedules() 162 | { 163 | tasks.clear(); 164 | } 165 | 166 | template 167 | void Cron::remove_schedule(const std::string& name) 168 | { 169 | tasks.remove(name); 170 | } 171 | 172 | template 173 | std::chrono::system_clock::duration Cron::time_until_next() const 174 | { 175 | std::chrono::system_clock::duration d{}; 176 | if (tasks.empty()) 177 | { 178 | d = std::numeric_limits::max(); 179 | } 180 | else 181 | { 182 | d = tasks.top().time_until_expiry(clock.now()); 183 | } 184 | 185 | return d; 186 | } 187 | 188 | template 189 | size_t Cron::tick(std::chrono::system_clock::time_point now) 190 | { 191 | tasks.lock_queue(); 192 | size_t res = 0; 193 | 194 | if(!first_tick) 195 | { 196 | // Only allow time to flow if at least one second has passed since the last tick, 197 | // either forward or backward. 198 | auto diff = now - last_tick; 199 | 200 | constexpr auto one_second = std::chrono::seconds{1}; 201 | 202 | if(diff < one_second && diff > -one_second) 203 | { 204 | now = last_tick; 205 | } 206 | } 207 | 208 | 209 | 210 | 211 | if (first_tick) 212 | { 213 | first_tick = false; 214 | } 215 | else 216 | { 217 | // https://linux.die.net/man/8/cron 218 | 219 | constexpr auto three_hours = std::chrono::hours{3}; 220 | auto diff = now - last_tick; 221 | auto absolute_diff = diff > diff.zero() ? diff : -diff; 222 | 223 | if(absolute_diff >= three_hours) 224 | { 225 | // Time changes of more than 3 hours are considered to be corrections to the 226 | // clock or timezone, and the new time is used immediately. 227 | for (auto& t : tasks.get_tasks()) 228 | { 229 | t.calculate_next(now); 230 | } 231 | } 232 | else 233 | { 234 | // Change of less than three hours 235 | 236 | // If time has moved backwards: Since tasks are not rescheduled, they won't run before 237 | // we're back at least the original point in time which prevents running tasks twice. 238 | 239 | // If time has moved forward, tasks that would have run since last tick will be run. 240 | } 241 | } 242 | 243 | last_tick = now; 244 | 245 | if (!tasks.empty()) 246 | { 247 | for (size_t i = 0; i < tasks.size(); i++) 248 | { 249 | if (tasks.at(i).is_expired(now)) 250 | { 251 | auto& t = tasks.at(i); 252 | t.execute(now); 253 | 254 | using namespace std::chrono_literals; 255 | if (!t.calculate_next(now + 1s)) 256 | { 257 | tasks.remove(t); 258 | } 259 | 260 | res++; 261 | } 262 | } 263 | 264 | // Only sort if at least one task was executed 265 | if (res > 0) 266 | { 267 | tasks.sort(); 268 | } 269 | } 270 | 271 | tasks.release_queue(); 272 | return res; 273 | } 274 | 275 | template 276 | void Cron::get_time_until_expiry_for_tasks(std::vector>& status) const 278 | { 279 | auto now = clock.now(); 280 | status.clear(); 281 | 282 | std::for_each(tasks.get_tasks().cbegin(), tasks.get_tasks().cend(), 283 | [&status, &now](const Task& t) 284 | { 285 | status.emplace_back(t.get_name(), t.time_until_expiry(now)); 286 | }); 287 | } 288 | 289 | template 290 | std::ostream& operator<<(std::ostream& stream, const Cron& c) 291 | { 292 | std::for_each(c.tasks.get_tasks().cbegin(), c.tasks.get_tasks().cend(), 293 | [&stream, &c](const Task& t) 294 | { 295 | stream << t.get_status(c.clock.now()) << '\n'; 296 | }); 297 | 298 | return stream; 299 | } 300 | } 301 | -------------------------------------------------------------------------------- /test/CronDataTest.cpp: -------------------------------------------------------------------------------- 1 | #define CATCH_CONFIG_MAIN // This tells Catch to provide a main() - only do this in one cpp file 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | 8 | using namespace libcron; 9 | using namespace date; 10 | using namespace std::chrono; 11 | 12 | template 13 | bool has_value_range(const std::set& set, uint8_t low, uint8_t high) 14 | { 15 | bool found = true; 16 | for (auto i = low; found && i <= high; ++i) 17 | { 18 | found &= set.find(static_cast(i)) != set.end(); 19 | } 20 | 21 | return found; 22 | } 23 | 24 | SCENARIO("Numerical inputs") 25 | { 26 | GIVEN("Valid numerical inputs") 27 | { 28 | WHEN("Creating with all stars") 29 | { 30 | THEN("All parts are filled") 31 | { 32 | auto c = CronData::create("* * * * * ?"); 33 | REQUIRE(c.is_valid()); 34 | REQUIRE(c.get_seconds().size() == 60); 35 | REQUIRE(has_value_range(c.get_seconds(), 0, 59)); 36 | REQUIRE(c.get_minutes().size() == 60); 37 | REQUIRE(has_value_range(c.get_minutes(), 0, 59)); 38 | REQUIRE(c.get_hours().size() == 24); 39 | REQUIRE(has_value_range(c.get_hours(), 0, 23)); 40 | REQUIRE(c.get_day_of_month().size() == 31); 41 | REQUIRE(has_value_range(c.get_day_of_month(), 1, 31)); 42 | REQUIRE(c.get_day_of_week().size() == 7); 43 | REQUIRE(has_value_range(c.get_day_of_week(), 0, 6)); 44 | } 45 | } 46 | AND_WHEN("Using full forward range") 47 | { 48 | THEN("Ranges are correct") 49 | { 50 | auto c = CronData::create("* 0-59 * * * ?"); 51 | REQUIRE(c.is_valid()); 52 | REQUIRE(c.get_seconds().size() == 60); 53 | REQUIRE(c.get_minutes().size() == 60); 54 | REQUIRE(c.get_hours().size() == 24); 55 | REQUIRE(c.get_day_of_month().size() == 31); 56 | REQUIRE(c.get_day_of_week().size() == 7); 57 | REQUIRE(has_value_range(c.get_seconds(), 0, 59)); 58 | } 59 | } 60 | AND_WHEN("Using partial range") 61 | { 62 | THEN("Ranges are correct") 63 | { 64 | auto c = CronData::create("* * * 20-30 * ?"); 65 | REQUIRE(c.is_valid()); 66 | REQUIRE(c.get_seconds().size() == 60); 67 | REQUIRE(c.get_minutes().size() == 60); 68 | REQUIRE(c.get_hours().size() == 24); 69 | REQUIRE(c.get_day_of_month().size() == 11); 70 | REQUIRE(c.get_day_of_week().size() == 7); 71 | REQUIRE(has_value_range(c.get_day_of_month(), 20, 30)); 72 | } 73 | } 74 | AND_WHEN("Using backward range") 75 | { 76 | THEN("Number of hours are correct") 77 | { 78 | auto c = CronData::create("* * 20-5 * * ?"); 79 | REQUIRE(c.is_valid()); 80 | REQUIRE(c.get_hours().size() == 10); 81 | REQUIRE(c.get_hours().find(Hours::First) != c.get_hours().end()); 82 | } 83 | } 84 | AND_WHEN("Using various ranges") 85 | { 86 | THEN("Validation succeeds") 87 | { 88 | REQUIRE(CronData::create("0-59 * * * * ?").is_valid()); 89 | REQUIRE(CronData::create("* 0-59 * * * ?").is_valid()); 90 | REQUIRE(CronData::create("* * 0-23 * * ?").is_valid()); 91 | REQUIRE(CronData::create("* * * 1-31 * ?").is_valid()); 92 | REQUIRE(CronData::create("* * * * 1-12 ?").is_valid()); 93 | REQUIRE(CronData::create("* * * ? * 0-6").is_valid()); 94 | } 95 | } 96 | } 97 | GIVEN("Invalid inputs") 98 | { 99 | WHEN("Creating items") 100 | { 101 | THEN("Validation fails") 102 | { 103 | REQUIRE_FALSE(CronData::create("").is_valid()); 104 | REQUIRE_FALSE(CronData::create("-").is_valid()); 105 | REQUIRE_FALSE(CronData::create("* ").is_valid()); 106 | REQUIRE_FALSE(CronData::create("* 0-60 * * * ?").is_valid()); 107 | REQUIRE_FALSE(CronData::create("* * 0-25 * * ?").is_valid()); 108 | REQUIRE_FALSE(CronData::create("* * * 1-32 * ?").is_valid()); 109 | REQUIRE_FALSE(CronData::create("* * * * 1-13 ?").is_valid()); 110 | REQUIRE_FALSE(CronData::create("* * * * * 0-7").is_valid()); 111 | REQUIRE_FALSE(CronData::create("* * * 0-31 * ?").is_valid()); 112 | REQUIRE_FALSE(CronData::create("* * * * 0-12 ?").is_valid()); 113 | REQUIRE_FALSE(CronData::create("60 * * * * ?").is_valid()); 114 | REQUIRE_FALSE(CronData::create("* 60 * * * ?").is_valid()); 115 | REQUIRE_FALSE(CronData::create("* * 25 * * ?").is_valid()); 116 | REQUIRE_FALSE(CronData::create("* * * 32 * ?").is_valid()); 117 | REQUIRE_FALSE(CronData::create("* * * * 13 ?").is_valid()); 118 | REQUIRE_FALSE(CronData::create("* * * ? * 7").is_valid()); 119 | } 120 | } 121 | } 122 | } 123 | 124 | SCENARIO("Literal input") 125 | { 126 | GIVEN("Literal inputs") 127 | { 128 | WHEN("Using literal ranges") 129 | { 130 | THEN("Range is valid") 131 | { 132 | auto c = CronData::create("* * * * JAN-MAR ?"); 133 | REQUIRE(c.is_valid()); 134 | REQUIRE(has_value_range(c.get_months(), 1, 3)); 135 | } 136 | AND_THEN("Range is valid") 137 | { 138 | auto c = CronData::create("* * * ? * SUN-FRI"); 139 | REQUIRE(c.is_valid()); 140 | REQUIRE(has_value_range(c.get_day_of_week(), 0, 5)); 141 | } 142 | } 143 | AND_WHEN("Using both range and specific month") 144 | { 145 | THEN("Range is valid") 146 | { 147 | auto c = CronData::create("* * * * JAN-MAR,DEC ?"); 148 | REQUIRE(c.is_valid()); 149 | REQUIRE(has_value_range(c.get_months(), 1, 3)); 150 | REQUIRE_FALSE(CronData::has_any_in_range(c.get_months(), 4, 11)); 151 | REQUIRE(has_value_range(c.get_months(), 12, 12)); 152 | } 153 | AND_THEN("Range is valid") 154 | { 155 | auto c = CronData::create("* * * ? JAN-MAR,DEC FRI,MON,THU"); 156 | REQUIRE(c.is_valid()); 157 | REQUIRE(has_value_range(c.get_months(), 1, 3)); 158 | REQUIRE_FALSE(CronData::has_any_in_range(c.get_months(), 4, 11)); 159 | REQUIRE(has_value_range(c.get_months(), 12, 12)); 160 | REQUIRE(has_value_range(c.get_day_of_week(), 5, 5)); 161 | REQUIRE(has_value_range(c.get_day_of_week(), 1, 1)); 162 | REQUIRE(has_value_range(c.get_day_of_week(), 4, 4)); 163 | REQUIRE_FALSE(CronData::has_any_in_range(c.get_day_of_week(), 0, 0)); 164 | REQUIRE_FALSE(CronData::has_any_in_range(c.get_day_of_week(), 2, 3)); 165 | REQUIRE_FALSE(CronData::has_any_in_range(c.get_day_of_week(), 6, 6)); 166 | } 167 | } 168 | AND_WHEN("Using backward range") 169 | { 170 | THEN("Range is valid") 171 | { 172 | auto c = CronData::create("* * * ? APR-JAN *"); 173 | REQUIRE(c.is_valid()); 174 | REQUIRE(has_value_range(c.get_months(), 4, 12)); 175 | REQUIRE(has_value_range(c.get_months(), 1, 1)); 176 | REQUIRE_FALSE(CronData::has_any_in_range(c.get_months(), 2, 3)); 177 | } 178 | AND_THEN("Range is valid") 179 | { 180 | auto c = CronData::create("* * * ? * sat-tue,wed"); 181 | REQUIRE(c.is_valid()); 182 | REQUIRE(has_value_range(c.get_day_of_week(), 6, 6)); // Has saturday 183 | REQUIRE(has_value_range(c.get_day_of_week(), 0, 3)); // Has sun, mon, tue, wed 184 | REQUIRE_FALSE(CronData::has_any_in_range(c.get_day_of_week(), 4, 5)); // Does not have thu or fri. 185 | } 186 | } 187 | } 188 | } 189 | 190 | SCENARIO("Using step syntax") 191 | { 192 | GIVEN("Step inputs") 193 | { 194 | WHEN("Using literal ranges") 195 | { 196 | THEN("Range is valid") 197 | { 198 | auto c = CronData::create("* * * * JAN/2 ?"); 199 | REQUIRE(c.is_valid()); 200 | REQUIRE(has_value_range(c.get_months(), 1, 1)); 201 | REQUIRE(has_value_range(c.get_months(), 3, 3)); 202 | REQUIRE(has_value_range(c.get_months(), 5, 5)); 203 | REQUIRE(has_value_range(c.get_months(), 7, 7)); 204 | REQUIRE(has_value_range(c.get_months(), 9, 9)); 205 | REQUIRE(has_value_range(c.get_months(), 11, 11)); 206 | REQUIRE_FALSE(CronData::has_any_in_range(c.get_months(), 2, 2)); 207 | REQUIRE_FALSE(CronData::has_any_in_range(c.get_months(), 4, 4)); 208 | REQUIRE_FALSE(CronData::has_any_in_range(c.get_months(), 6, 6)); 209 | REQUIRE_FALSE(CronData::has_any_in_range(c.get_months(), 8, 8)); 210 | REQUIRE_FALSE(CronData::has_any_in_range(c.get_months(), 10, 10)); 211 | REQUIRE_FALSE(CronData::has_any_in_range(c.get_months(), 12, 12)); 212 | } 213 | 214 | } 215 | } 216 | } 217 | 218 | SCENARIO("Dates that does not exist") 219 | { 220 | REQUIRE_FALSE(CronData::create("0 0 * 30 FEB *").is_valid()); 221 | REQUIRE_FALSE(CronData::create("0 0 * 31 APR *").is_valid()); 222 | } 223 | 224 | SCENARIO("Date that exist in one of the months") 225 | { 226 | REQUIRE(CronData::create("0 0 * 31 APR,MAY ?").is_valid()); 227 | } 228 | 229 | SCENARIO("Replacing text with numbers") 230 | { 231 | { 232 | std::string s = "SUN-TUE"; 233 | REQUIRE(CronData::replace_string_name_with_numeric(s) == "0-2"); 234 | } 235 | 236 | { 237 | std::string s = "JAN-DEC"; 238 | REQUIRE(CronData::replace_string_name_with_numeric(s) == "1-12"); 239 | } 240 | } 241 | 242 | SCENARIO("Parsing @ expressions works") { 243 | REQUIRE(CronData::create("@yearly").is_valid()); 244 | REQUIRE(CronData::create("@annually").is_valid()); 245 | REQUIRE(CronData::create("@monthly").is_valid()); 246 | REQUIRE(CronData::create("@weekly").is_valid()); 247 | REQUIRE(CronData::create("@daily").is_valid()); 248 | REQUIRE(CronData::create("@hourly").is_valid()); 249 | } -------------------------------------------------------------------------------- /libcron/include/libcron/CronData.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | 10 | namespace libcron 11 | { 12 | class CronData 13 | { 14 | public: 15 | static const int NUMBER_OF_LONG_MONTHS = 7; 16 | static const libcron::Months months_with_31[NUMBER_OF_LONG_MONTHS]; 17 | 18 | static CronData create(const std::string& cron_expression); 19 | 20 | CronData() = default; 21 | 22 | CronData(const CronData&) = default; 23 | 24 | CronData& operator=(const CronData&) = default; 25 | 26 | bool is_valid() const 27 | { 28 | return valid; 29 | } 30 | 31 | const std::set& get_seconds() const 32 | { 33 | return seconds; 34 | } 35 | 36 | const std::set& get_minutes() const 37 | { 38 | return minutes; 39 | } 40 | 41 | const std::set& get_hours() const 42 | { 43 | return hours; 44 | } 45 | 46 | const std::set& get_day_of_month() const 47 | { 48 | return day_of_month; 49 | } 50 | 51 | const std::set& get_months() const 52 | { 53 | return months; 54 | } 55 | 56 | const std::set& get_day_of_week() const 57 | { 58 | return day_of_week; 59 | } 60 | 61 | template 62 | static uint8_t value_of(T t) 63 | { 64 | return static_cast(t); 65 | } 66 | 67 | template 68 | static bool has_any_in_range(const std::set& set, uint8_t low, uint8_t high) 69 | { 70 | bool found = false; 71 | 72 | for (auto i = low; !found && i <= high; ++i) 73 | { 74 | found |= set.find(static_cast(i)) != set.end(); 75 | } 76 | 77 | return found; 78 | } 79 | 80 | template 81 | bool convert_from_string_range_to_number_range(const std::string& range, std::set& numbers); 82 | 83 | template 84 | static std::string& replace_string_name_with_numeric(std::string& s); 85 | 86 | private: 87 | void parse(const std::string& cron_expression); 88 | 89 | template 90 | bool validate_numeric(const std::string& s, std::set& numbers); 91 | 92 | template 93 | bool validate_literal(const std::string& s, 94 | std::set& numbers, 95 | const std::vector& names); 96 | 97 | template 98 | bool process_parts(const std::vector& parts, std::set& numbers); 99 | 100 | template 101 | bool add_number(std::set& set, int32_t number); 102 | 103 | template 104 | bool is_within_limits(int32_t low, int32_t high); 105 | 106 | template 107 | bool get_range(const std::string& s, T& low, T& high); 108 | 109 | template 110 | bool get_step(const std::string& s, uint8_t& start, uint8_t& step); 111 | 112 | std::vector split(const std::string& s, char token); 113 | 114 | bool is_number(const std::string& s); 115 | 116 | bool is_between(int32_t value, int32_t low_limit, int32_t high_limit); 117 | 118 | bool validate_date_vs_months() const; 119 | 120 | bool check_dom_vs_dow(const std::string& dom, const std::string& dow) const; 121 | 122 | std::set seconds{}; 123 | std::set minutes{}; 124 | std::set hours{}; 125 | std::set day_of_month{}; 126 | std::set months{}; 127 | std::set day_of_week{}; 128 | bool valid = false; 129 | 130 | static const std::vector month_names; 131 | static const std::vector day_names; 132 | static std::unordered_map cache; 133 | 134 | template 135 | void add_full_range(std::set& set); 136 | }; 137 | 138 | template 139 | bool CronData::validate_numeric(const std::string& s, std::set& numbers) 140 | { 141 | std::vector parts = split(s, ','); 142 | 143 | return process_parts(parts, numbers); 144 | } 145 | 146 | template 147 | bool CronData::validate_literal(const std::string& s, 148 | std::set& numbers, 149 | const std::vector& names) 150 | { 151 | std::vector parts = split(s, ','); 152 | 153 | auto value_of_first_name = value_of(T::First); 154 | 155 | // Replace each found name with the corresponding value. 156 | for (const auto& name : names) 157 | { 158 | std::regex m(name, std::regex_constants::ECMAScript | std::regex_constants::icase); 159 | 160 | for (auto& part : parts) 161 | { 162 | std::string replaced; 163 | std::regex_replace(std::back_inserter(replaced), part.begin(), part.end(), m, 164 | std::to_string(value_of_first_name)); 165 | 166 | part = replaced; 167 | } 168 | 169 | value_of_first_name++; 170 | } 171 | 172 | return process_parts(parts, numbers); 173 | } 174 | 175 | template 176 | bool CronData::process_parts(const std::vector& parts, std::set& numbers) 177 | { 178 | bool res = true; 179 | 180 | for (const auto& p : parts) 181 | { 182 | res &= convert_from_string_range_to_number_range(p, numbers); 183 | } 184 | 185 | return res; 186 | } 187 | 188 | template 189 | bool CronData::get_range(const std::string& s, T& low, T& high) 190 | { 191 | bool res = false; 192 | 193 | auto value_range = R"#((\d+)-(\d+))#"; 194 | 195 | std::regex range(value_range, std::regex_constants::ECMAScript); 196 | 197 | std::smatch match; 198 | 199 | if (std::regex_match(s.begin(), s.end(), match, range)) 200 | { 201 | auto left = std::stoi(match[1].str()); 202 | auto right = std::stoi(match[2].str()); 203 | 204 | if (is_within_limits(left, right)) 205 | { 206 | low = static_cast(left); 207 | high = static_cast(right); 208 | res = true; 209 | } 210 | } 211 | 212 | return res; 213 | } 214 | 215 | template 216 | bool CronData::get_step(const std::string& s, uint8_t& start, uint8_t& step) 217 | { 218 | bool res = false; 219 | 220 | auto value_range = R"#((\d+|\*)/(\d+))#"; 221 | 222 | std::regex range(value_range, std::regex_constants::ECMAScript); 223 | 224 | std::smatch match; 225 | 226 | if (std::regex_match(s.begin(), s.end(), match, range)) 227 | { 228 | int raw_start; 229 | 230 | if (match[1].str() == "*") 231 | { 232 | raw_start = value_of(T::First); 233 | } 234 | else 235 | { 236 | raw_start = std::stoi(match[1].str()); 237 | } 238 | 239 | auto raw_step = std::stoi(match[2].str()); 240 | 241 | if (is_within_limits(raw_start, raw_start) && raw_step > 0) 242 | { 243 | start = static_cast(raw_start); 244 | step = static_cast(raw_step); 245 | res = true; 246 | } 247 | } 248 | 249 | return res; 250 | } 251 | 252 | template 253 | void CronData::add_full_range(std::set& set) 254 | { 255 | for (auto v = value_of(T::First); v <= value_of(T::Last); ++v) 256 | { 257 | if (set.find(static_cast(v)) == set.end()) 258 | { 259 | set.emplace(static_cast(v)); 260 | } 261 | } 262 | } 263 | 264 | template 265 | bool CronData::add_number(std::set& set, int32_t number) 266 | { 267 | bool res = true; 268 | 269 | // Don't add if already there 270 | if (set.find(static_cast(number)) == set.end()) 271 | { 272 | // Check range 273 | if (is_within_limits(number, number)) 274 | { 275 | set.emplace(static_cast(number)); 276 | } 277 | else 278 | { 279 | res = false; 280 | } 281 | } 282 | 283 | return res; 284 | } 285 | 286 | template 287 | bool CronData::is_within_limits(int32_t low, int32_t high) 288 | { 289 | return is_between(low, value_of(T::First), value_of(T::Last)) 290 | && is_between(high, value_of(T::First), value_of(T::Last)); 291 | } 292 | 293 | template 294 | bool CronData::convert_from_string_range_to_number_range(const std::string& range, std::set& numbers) 295 | { 296 | T left; 297 | T right; 298 | uint8_t step_start; 299 | uint8_t step; 300 | 301 | bool res = true; 302 | 303 | if (range == "*" || range == "?") 304 | { 305 | // We treat the ignore-character '?' the same as the full range being allowed. 306 | add_full_range(numbers); 307 | } 308 | else if (is_number(range)) 309 | { 310 | res = add_number(numbers, std::stoi(range)); 311 | } 312 | else if (get_range(range, left, right)) 313 | { 314 | // A range can be written as both 1-22 or 22-1, meaning totally different ranges. 315 | // First case is 1...22 while 22-1 is only four hours: 22, 23, 0, 1. 316 | if (left <= right) 317 | { 318 | for (auto v = value_of(left); v <= value_of(right); ++v) 319 | { 320 | res &= add_number(numbers, v); 321 | } 322 | } 323 | else 324 | { 325 | // 'left' and 'right' are not in value order. First, get values between 'left' and T::Last, inclusive 326 | for (auto v = value_of(left); v <= value_of(T::Last); ++v) 327 | { 328 | res = add_number(numbers, v); 329 | } 330 | 331 | // Next, get values between T::First and 'right', inclusive. 332 | for (auto v = value_of(T::First); v <= value_of(right); ++v) 333 | { 334 | res = add_number(numbers, v); 335 | } 336 | } 337 | } 338 | else if (get_step(range, step_start, step)) 339 | { 340 | // Add from step_start to T::Last with a step of 'step' 341 | for (auto v = step_start; v <= value_of(T::Last); v += step) 342 | { 343 | res = add_number(numbers, v); 344 | } 345 | } 346 | else 347 | { 348 | res = false; 349 | } 350 | 351 | return res; 352 | } 353 | 354 | template 355 | std::string & CronData::replace_string_name_with_numeric(std::string& s) 356 | { 357 | auto value = static_cast(T::First); 358 | 359 | const std::vector* name_source{}; 360 | 361 | static_assert(std::is_same() 362 | || std::is_same(), 363 | "T must be either Months or DayOfWeek"); 364 | 365 | if constexpr (std::is_same()) 366 | { 367 | name_source = &month_names; 368 | } 369 | else 370 | { 371 | name_source = &day_names; 372 | } 373 | 374 | for (const auto& name : *name_source) 375 | { 376 | std::regex m(name, std::regex_constants::ECMAScript | std::regex_constants::icase); 377 | 378 | std::string replaced; 379 | 380 | std::regex_replace(std::back_inserter(replaced), s.begin(), s.end(), m, std::to_string(value)); 381 | 382 | s = replaced; 383 | 384 | ++value; 385 | } 386 | 387 | return s; 388 | } 389 | } 390 | -------------------------------------------------------------------------------- /test/CronTest.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | 7 | using namespace libcron; 8 | using namespace std::chrono; 9 | using namespace date; 10 | 11 | std::string create_schedule_expiring_in(std::chrono::system_clock::time_point now, hours h, minutes m, seconds s) 12 | { 13 | now = now + h + m + s; 14 | auto dt = CronSchedule::to_calendar_time(now); 15 | 16 | std::string res{}; 17 | res += std::to_string(dt.sec) + " "; 18 | res += std::to_string(dt.min) + " "; 19 | res += std::to_string(dt.hour) + " * * ?"; 20 | 21 | return res; 22 | } 23 | 24 | SCENARIO("Adding a task") 25 | { 26 | GIVEN("A Cron instance with no task") 27 | { 28 | Cron<> c; 29 | auto expired = false; 30 | 31 | THEN("Starts with no task") 32 | { 33 | REQUIRE(c.count() == 0); 34 | } 35 | 36 | WHEN("Adding a task that runs every second") 37 | { 38 | REQUIRE(c.add_schedule("A task", "* * * * * ?", 39 | [&expired](auto&) 40 | { 41 | expired = true; 42 | }) 43 | ); 44 | 45 | THEN("Count is 1 and task was not expired two seconds ago") 46 | { 47 | REQUIRE(c.count() == 1); 48 | c.tick(c.get_clock().now() - 2s); 49 | REQUIRE_FALSE(expired); 50 | } 51 | AND_THEN("Task is expired when calculating based on current time") 52 | { 53 | c.tick(); 54 | THEN("Task is expired") 55 | { 56 | REQUIRE(expired); 57 | } 58 | } 59 | } 60 | } 61 | } 62 | 63 | SCENARIO("Adding a task that expires in the future") 64 | { 65 | GIVEN("A Cron instance with task expiring in 3 seconds") 66 | { 67 | auto expired = false; 68 | 69 | Cron<> c; 70 | REQUIRE(c.add_schedule("A task", 71 | create_schedule_expiring_in(c.get_clock().now(), hours{0}, minutes{0}, seconds{3}), 72 | [&expired](auto&) 73 | { 74 | expired = true; 75 | }) 76 | ); 77 | 78 | THEN("Not yet expired") 79 | { 80 | REQUIRE_FALSE(expired); 81 | } 82 | AND_WHEN("When waiting one second") 83 | { 84 | std::this_thread::sleep_for(1s); 85 | c.tick(); 86 | THEN("Task has not yet expired") 87 | { 88 | REQUIRE_FALSE(expired); 89 | } 90 | } 91 | AND_WHEN("When waiting three seconds") 92 | { 93 | std::this_thread::sleep_for(3s); 94 | c.tick(); 95 | THEN("Task has expired") 96 | { 97 | REQUIRE(expired); 98 | } 99 | } 100 | } 101 | } 102 | 103 | SCENARIO("Get delay using Task-Information") 104 | { 105 | using namespace std::chrono_literals; 106 | 107 | GIVEN("A Cron instance with one task expiring in 2 seconds, but taking 3 seconds to execute") 108 | { 109 | auto _2_second_expired = 0; 110 | auto _delay = std::chrono::system_clock::duration(-1s); 111 | 112 | Cron<> c; 113 | REQUIRE(c.add_schedule("Two", 114 | "*/2 * * * * ?", 115 | [&_2_second_expired, &_delay](auto& i) 116 | { 117 | _2_second_expired++; 118 | _delay = i.get_delay(); 119 | std::this_thread::sleep_for(3s); 120 | }) 121 | ); 122 | THEN("Not yet expired") 123 | { 124 | REQUIRE_FALSE(_2_second_expired); 125 | REQUIRE(_delay <= 0s); 126 | } 127 | WHEN("Exactly schedule task") 128 | { 129 | while (_2_second_expired == 0) 130 | c.tick(); 131 | 132 | THEN("Task should have expired within a valid time") 133 | { 134 | REQUIRE(_2_second_expired == 1); 135 | REQUIRE(_delay <= 1s); 136 | } 137 | AND_THEN("Executing another tick again, leading to execute task again immediatly, but not on time as execution has taken 3 seconds.") 138 | { 139 | c.tick(); 140 | REQUIRE(_2_second_expired == 2); 141 | REQUIRE(_delay >= 1s); 142 | } 143 | } 144 | } 145 | } 146 | 147 | SCENARIO("Task priority") 148 | { 149 | GIVEN("A Cron instance with two tasks expiring in 3 and 5 seconds, added in 'reverse' order") 150 | { 151 | auto _3_second_expired = 0; 152 | auto _5_second_expired = 0; 153 | 154 | 155 | Cron<> c; 156 | REQUIRE(c.add_schedule("Five", 157 | create_schedule_expiring_in(c.get_clock().now(), hours{0}, minutes{0}, seconds{5}), 158 | [&_5_second_expired](auto&) 159 | { 160 | _5_second_expired++; 161 | }) 162 | ); 163 | 164 | REQUIRE(c.add_schedule("Three", 165 | create_schedule_expiring_in(c.get_clock().now(), hours{0}, minutes{0}, seconds{3}), 166 | [&_3_second_expired](auto&) 167 | { 168 | _3_second_expired++; 169 | }) 170 | ); 171 | 172 | THEN("Not yet expired") 173 | { 174 | REQUIRE_FALSE(_3_second_expired); 175 | REQUIRE_FALSE(_5_second_expired); 176 | } 177 | 178 | WHEN("Waiting 1 seconds") 179 | { 180 | std::this_thread::sleep_for(1s); 181 | c.tick(); 182 | 183 | THEN("Task has not yet expired") 184 | { 185 | REQUIRE(_3_second_expired == 0); 186 | REQUIRE(_5_second_expired == 0); 187 | } 188 | } 189 | AND_WHEN("Waiting 3 seconds") 190 | { 191 | std::this_thread::sleep_for(3s); 192 | c.tick(); 193 | 194 | THEN("3 second task has expired") 195 | { 196 | REQUIRE(_3_second_expired == 1); 197 | REQUIRE(_5_second_expired == 0); 198 | } 199 | } 200 | AND_WHEN("Waiting 5 seconds") 201 | { 202 | std::this_thread::sleep_for(5s); 203 | c.tick(); 204 | 205 | THEN("3 and 5 second task has expired") 206 | { 207 | REQUIRE(_3_second_expired == 1); 208 | REQUIRE(_5_second_expired == 1); 209 | } 210 | } 211 | AND_WHEN("Waiting based on the time given by the Cron instance") 212 | { 213 | auto msec = std::chrono::duration_cast(c.time_until_next()); 214 | std::this_thread::sleep_for(c.time_until_next()); 215 | c.tick(); 216 | 217 | THEN("3 second task has expired") 218 | { 219 | REQUIRE(_3_second_expired == 1); 220 | REQUIRE(_5_second_expired == 0); 221 | } 222 | } 223 | AND_WHEN("Waiting based on the time given by the Cron instance") 224 | { 225 | std::this_thread::sleep_for(c.time_until_next()); 226 | REQUIRE(c.tick() == 1); 227 | 228 | std::this_thread::sleep_for(c.time_until_next()); 229 | REQUIRE(c.tick() == 1); 230 | 231 | THEN("3 and 5 second task has each expired once") 232 | { 233 | REQUIRE(_3_second_expired == 1); 234 | REQUIRE(_5_second_expired == 1); 235 | } 236 | } 237 | } 238 | } 239 | 240 | class TestClock 241 | : public ICronClock 242 | { 243 | public: 244 | std::chrono::system_clock::time_point now() const override 245 | { 246 | return current_time; 247 | } 248 | 249 | std::chrono::seconds utc_offset(std::chrono::system_clock::time_point) const override 250 | { 251 | return 0s; 252 | } 253 | 254 | void add(system_clock::duration time) 255 | { 256 | current_time += time; 257 | } 258 | 259 | void set(system_clock::time_point new_time) 260 | { 261 | current_time = new_time; 262 | } 263 | 264 | private: 265 | system_clock::time_point current_time = system_clock::now(); 266 | 267 | }; 268 | 269 | SCENARIO("Clock changes") 270 | { 271 | GIVEN("A Cron instance with a single task expiring every hour") 272 | { 273 | Cron c{}; 274 | auto& clock = c.get_clock(); 275 | 276 | // Midnight 277 | clock.set(sys_days{2018_y / 05 / 05}); 278 | 279 | // Every hour 280 | REQUIRE(c.add_schedule("Clock change task", "0 0 * * * ?", [](auto&) 281 | { 282 | }) 283 | ); 284 | 285 | // https://linux.die.net/man/8/cron 286 | 287 | WHEN("Clock changes <3h forward") 288 | { 289 | THEN("Task expires accordingly") 290 | { 291 | REQUIRE(c.tick() == 1); 292 | clock.add(minutes{30}); // 00:30 293 | REQUIRE(c.tick() == 0); 294 | clock.add(minutes{30}); // 01:00 295 | REQUIRE(c.tick() == 1); 296 | REQUIRE(c.tick() == 0); 297 | REQUIRE(c.tick() == 0); 298 | clock.add(minutes{30}); // 01:30 299 | REQUIRE(c.tick() == 0); 300 | clock.add(minutes{15}); // 01:45 301 | REQUIRE(c.tick() == 0); 302 | clock.add(minutes{15}); // 02:00 303 | REQUIRE(c.tick() == 1); 304 | } 305 | } 306 | AND_WHEN("Clock is moved forward >= 3h") 307 | { 308 | THEN("Task are rescheduled, not run") 309 | { 310 | REQUIRE(c.tick() == 1); 311 | clock.add(hours{3}); // 03:00 312 | REQUIRE(c.tick() == 1); // Rescheduled 313 | clock.add(minutes{15}); // 03:15 314 | REQUIRE(c.tick() == 0); 315 | clock.add(minutes{45}); // 04:00 316 | REQUIRE(c.tick() == 1); 317 | } 318 | } 319 | AND_WHEN("Clock is moved back <3h") 320 | { 321 | THEN("Tasks retain their last scheduled time and are prevented from running twice") 322 | { 323 | REQUIRE(c.tick() == 1); 324 | clock.add(-hours{1}); // 23:00 325 | REQUIRE(c.tick() == 0); 326 | clock.add(-hours{1}); // 22:00 327 | REQUIRE(c.tick() == 0); 328 | clock.add(hours{3}); // 1:00 329 | REQUIRE(c.tick() == 1); 330 | } 331 | } 332 | AND_WHEN("Clock is moved back >3h") 333 | { 334 | THEN("Tasks are rescheduled") 335 | { 336 | REQUIRE(c.tick() == 1); 337 | clock.add(-hours{3}); // 21:00 338 | REQUIRE(c.tick() == 1); 339 | REQUIRE(c.tick() == 0); 340 | clock.add(hours{1}); // 22:00 341 | REQUIRE(c.tick() == 1); 342 | } 343 | } 344 | } 345 | } 346 | 347 | SCENARIO("Multiple ticks per second") 348 | { 349 | Cron c{}; 350 | auto& clock = c.get_clock(); 351 | 352 | auto now = sys_days{2018_y / 05 / 05}; 353 | clock.set(now); 354 | 355 | int run_count = 0; 356 | 357 | // Every 10 seconds 358 | REQUIRE(c.add_schedule("Clock change task", "*/10 0 * * * ?", [&run_count](auto&) 359 | { 360 | run_count++; 361 | }) 362 | ); 363 | 364 | c.tick(now); 365 | 366 | REQUIRE(run_count == 1); 367 | 368 | WHEN("Many ticks during one seconds") 369 | { 370 | for(auto i = 0; i < 10; ++i) 371 | { 372 | clock.add(std::chrono::microseconds{1}); 373 | c.tick(); 374 | } 375 | 376 | THEN("Run count has not increased") 377 | { 378 | REQUIRE(run_count == 1); 379 | } 380 | 381 | } 382 | 383 | } 384 | 385 | SCENARIO("Tasks can be added and removed from the scheduler") 386 | { 387 | GIVEN("A Cron instance with no task") 388 | { 389 | Cron<> c; 390 | auto expired = false; 391 | 392 | WHEN("Adding 5 tasks that runs every second") 393 | { 394 | REQUIRE(c.add_schedule("Task-1", "* * * * * ?", 395 | [&expired](auto&) 396 | { 397 | expired = true; 398 | }) 399 | ); 400 | 401 | REQUIRE(c.add_schedule("Task-2", "* * * * * ?", 402 | [&expired](auto&) 403 | { 404 | expired = true; 405 | }) 406 | ); 407 | 408 | REQUIRE(c.add_schedule("Task-3", "* * * * * ?", 409 | [&expired](auto&) 410 | { 411 | expired = true; 412 | }) 413 | ); 414 | 415 | REQUIRE(c.add_schedule("Task-4", "* * * * * ?", 416 | [&expired](auto&) 417 | { 418 | expired = true; 419 | }) 420 | ); 421 | 422 | REQUIRE(c.add_schedule("Task-5", "* * * * * ?", 423 | [&expired](auto&) 424 | { 425 | expired = true; 426 | }) 427 | ); 428 | 429 | THEN("Count is 5") 430 | { 431 | REQUIRE(c.count() == 5); 432 | } 433 | AND_THEN("Removing all scheduled tasks") 434 | { 435 | c.clear_schedules(); 436 | REQUIRE(c.count() == 0); 437 | } 438 | AND_THEN("Removing a task that does not exist") 439 | { 440 | c.remove_schedule("Task-6"); 441 | REQUIRE(c.count() == 5); 442 | } 443 | AND_THEN("Removing a task that does exist") 444 | { 445 | c.remove_schedule("Task-5"); 446 | REQUIRE(c.count() == 4); 447 | } 448 | } 449 | } 450 | } 451 | --------------------------------------------------------------------------------