From ae82c88e1ea4da6f25a741d96246ffad52eb36df Mon Sep 17 00:00:00 2001 From: Marius Bancila Date: Wed, 12 Aug 2026 16:26:42 +0300 Subject: [PATCH] complete the chrono overloads --- README.md | 24 ++++++++++ include/croncpp.h | 74 +++++++++++++++++++++++-------- test/CMakeLists.txt | 2 +- test/test_chrono.cpp | 103 +++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 184 insertions(+), 19 deletions(-) create mode 100644 test/test_chrono.cpp diff --git a/README.md b/README.md index f794d43..0f8f610 100644 --- a/README.md +++ b/README.md @@ -150,6 +150,8 @@ catch (cron::bad_cronexpr const & ex) A `std::chrono::system_clock::time_point` overload is also available, but because `cron_next()` converts the time point to `time_t` using `to_time_t()`, fractional seconds are truncated. If you are working from a known trigger instant and a sub-second drift could move the time point below the exact second, use `cron_next_ceil()` to round up to the next whole second before computing the next occurrence. +Both take a `std::chrono::time_point` of any duration on the system clock and return the same type, so a caller working in milliseconds gets milliseconds back. + ``` try { @@ -165,6 +167,28 @@ catch (cron::bad_cronexpr const & ex) } ``` +croncpp deliberately stays within C++11, so it does not use the calendar and time zone facilities added to `` in C++20. Time is handled through `std::mktime` and `localtime`, in local time, as described under [Time zones and daylight saving time](#time-zones-and-daylight-saving-time). + +### When there is no next occurrence + +An expression can name a schedule that never comes round again, most obviously one whose years have all gone by. Each overload reports that in the way that suits its return type: + +| Overload | Reports failure as | +| --- | --- | +| `std::time_t` | `INVALID_TIME`, that is `static_cast(-1)` | +| `std::tm` | a zeroed `std::tm` | +| `std::chrono::time_point` | `time_point::min()` | + +``` +auto cron = cron::make_cron("0 15 10 * * ? 2005"); // a year long past + +auto next = cron::cron_next(cron, std::chrono::system_clock::now()); +if (next == std::chrono::system_clock::time_point::min()) +{ + // nothing further is scheduled +} +``` + When you use these functions as shown above you implicitly use the standard supported values for the fields, as described in the first section. However, you can use any other settings. The ones provided with the library are called `cron_standard_traits`, `cron_oracle_traits` and `cron_quartz_traits` (coresponding to the aforementioned settings). ``` diff --git a/include/croncpp.h b/include/croncpp.h index 837bf98..705231d 100644 --- a/include/croncpp.h +++ b/include/croncpp.h @@ -1510,22 +1510,60 @@ namespace cron return detail::find_next_after(cex, date, result); } - template - static std::chrono::system_clock::time_point cron_next(cronexpr const & cex, std::chrono::system_clock::time_point const & time_point) { - return std::chrono::system_clock::from_time_t(cron_next(cex, std::chrono::system_clock::to_time_t(time_point))); - } - - template - static std::chrono::system_clock::time_point cron_next_ceil(cronexpr const & cex, std::chrono::system_clock::time_point const & time_point) { - // std::chrono::system_clock::to_time_t truncates fractional seconds. - // If the input time_point represents a known cron trigger time but is - // slightly below that exact second, truncation can move it back one - // second and cause cron_next to return the current trigger instead of - // the next one. - auto tt = std::chrono::system_clock::to_time_t(time_point); - if (std::chrono::system_clock::from_time_t(tt) < time_point) { - ++tt; - } - return std::chrono::system_clock::from_time_t(cron_next(cex, tt)); - } + // The time_point overloads keep the duration they were given, so a caller + // working in milliseconds gets milliseconds back rather than the default + // duration of the clock. + // + // When there is no next occurrence they return time_point::min(), which is + // the counterpart of the INVALID_TIME the std::time_t overload returns. + // Turning that sentinel into a time_point through from_time_t would name a + // real instant just before the epoch, which reads as an ordinary answer. + template + static std::chrono::time_point cron_next( + cronexpr const & cex, + std::chrono::time_point const & time_point) + { + using result_type = + std::chrono::time_point; + + auto const from = std::chrono::time_point_cast< + std::chrono::system_clock::duration>(time_point); + + auto const next = cron_next( + cex, std::chrono::system_clock::to_time_t(from)); + + if (INVALID_TIME == next) return (result_type::min)(); + + return std::chrono::time_point_cast( + std::chrono::system_clock::from_time_t(next)); + } + + template + static std::chrono::time_point cron_next_ceil( + cronexpr const & cex, + std::chrono::time_point const & time_point) + { + using result_type = + std::chrono::time_point; + + auto const from = std::chrono::time_point_cast< + std::chrono::system_clock::duration>(time_point); + + // std::chrono::system_clock::to_time_t truncates fractional seconds. + // If the input time_point represents a known cron trigger time but is + // slightly below that exact second, truncation can move it back one + // second and cause cron_next to return the current trigger instead of + // the next one. + auto tt = std::chrono::system_clock::to_time_t(from); + if (std::chrono::system_clock::from_time_t(tt) < from) { + ++tt; + } + + auto const next = cron_next(cex, tt); + + if (INVALID_TIME == next) return (result_type::min)(); + + return std::chrono::time_point_cast( + std::chrono::system_clock::from_time_t(next)); + } } diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index 03443fd..e826f65 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -1,4 +1,4 @@ -set(SOURCES main.cpp test_day_fields.cpp test_dst.cpp test_oracle.cpp test_quartz.cpp test_special.cpp test_standard.cpp test_years.cpp) +set(SOURCES main.cpp test_chrono.cpp test_day_fields.cpp test_dst.cpp test_oracle.cpp test_quartz.cpp test_special.cpp test_standard.cpp test_years.cpp) add_executable(test_croncpp ${SOURCES}) diff --git a/test/test_chrono.cpp b/test/test_chrono.cpp new file mode 100644 index 0000000..7d3e341 --- /dev/null +++ b/test/test_chrono.cpp @@ -0,0 +1,103 @@ +#include "catch.hpp" +#include "croncpp.h" + +#include +#include +#include + +using namespace cron; +using namespace std::chrono; + +namespace +{ + system_clock::time_point point_at(std::string const & text) + { + auto date = utils::to_tm(text); + + return system_clock::from_time_t(utils::tm_to_time(date)); + } + + std::string text_of(system_clock::time_point const & tp) + { + auto const t = system_clock::to_time_t(tp); + + std::tm tm; + if (utils::time_to_tm(&t, &tm) == nullptr) return "invalid"; + + return utils::to_string(tm); + } +} + +TEST_CASE("chrono: the time_point overload agrees with the others", "[chrono]") +{ + auto cex = make_cron("0 15 10 * * ?"); + auto const from = point_at("2026-06-01 00:00:00"); + + REQUIRE(text_of(cron_next(cex, from)) == "2026-06-01 10:15:00"); + + // the three overloads name the same instant + auto const from_time = system_clock::to_time_t(from); + auto date = utils::to_tm("2026-06-01 00:00:00"); + + auto from_date = cron_next(cex, date); + + REQUIRE(system_clock::to_time_t(cron_next(cex, from)) == cron_next(cex, from_time)); + REQUIRE(system_clock::to_time_t(cron_next(cex, from)) == utils::tm_to_time(from_date)); +} + +TEST_CASE("chrono: an expression with no next occurrence", "[chrono]") +{ + // the year has gone by, so there is nothing to return + auto cex = make_cron("0 15 10 * * ? 2005"); + auto const from = point_at("2026-06-01 00:00:00"); + + REQUIRE(cron_next(cex, from) == (system_clock::time_point::min)()); + REQUIRE(cron_next_ceil(cex, from) == (system_clock::time_point::min)()); + + // and the time_t overload still reports it the way it always has + REQUIRE(cron_next(cex, system_clock::to_time_t(from)) == INVALID_TIME); +} + +TEST_CASE("chrono: the duration of the argument is kept", "[chrono]") +{ + auto cex = make_cron("0 15 10 * * ?"); + + auto const in_ms = time_point_cast(point_at("2026-06-01 00:00:00")); + auto const out_ms = cron_next(cex, in_ms); + + static_assert(std::is_same const>::value, + "milliseconds in, milliseconds out"); + REQUIRE(text_of(time_point_cast(out_ms)) == "2026-06-01 10:15:00"); + + auto const in_min = time_point_cast(point_at("2026-06-01 00:00:00")); + auto const out_min = cron_next(cex, in_min); + + static_assert(std::is_same const>::value, + "minutes in, minutes out"); + REQUIRE(text_of(time_point_cast(out_min)) == "2026-06-01 10:15:00"); +} + +TEST_CASE("chrono: cron_next_ceil rounds up before searching", "[chrono]") +{ + auto cex = make_cron("0 0 12 * * *"); + auto const trigger = point_at("2026-06-01 12:00:00"); + + // a shade below a trigger instant: truncation would put us back on it, so + // cron_next answers with that trigger and cron_next_ceil with the next one + auto const just_before = trigger - milliseconds(50); + + REQUIRE(text_of(cron_next(cex, just_before)) == "2026-06-01 12:00:00"); + REQUIRE(text_of(cron_next_ceil(cex, just_before)) == "2026-06-02 12:00:00"); + + // exactly on the trigger, both move to the next one + REQUIRE(text_of(cron_next(cex, trigger)) == "2026-06-02 12:00:00"); + REQUIRE(text_of(cron_next_ceil(cex, trigger)) == "2026-06-02 12:00:00"); +} + +TEST_CASE("chrono: the traits can still be given explicitly", "[chrono]") +{ + auto cex = make_cron("0 15 10 ? * 6#2"); + auto const from = point_at("2011-04-30 23:30:00"); + + REQUIRE(text_of(cron_next(cex, from)) == "2011-05-13 10:15:00"); +}