Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
{
Expand All @@ -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 `<chrono>` 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<std::time_t>(-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).

```
Expand Down
74 changes: 56 additions & 18 deletions include/croncpp.h
Original file line number Diff line number Diff line change
Expand Up @@ -1510,22 +1510,60 @@ namespace cron
return detail::find_next_after<Traits>(cex, date, result);
}

template <typename Traits = cron_standard_traits>
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<Traits>(cex, std::chrono::system_clock::to_time_t(time_point)));
}

template <typename Traits = cron_standard_traits>
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<Traits>(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 <typename Traits = cron_standard_traits, typename Duration>
static std::chrono::time_point<std::chrono::system_clock, Duration> cron_next(
cronexpr const & cex,
std::chrono::time_point<std::chrono::system_clock, Duration> const & time_point)
{
using result_type =
std::chrono::time_point<std::chrono::system_clock, Duration>;

auto const from = std::chrono::time_point_cast<
std::chrono::system_clock::duration>(time_point);

auto const next = cron_next<Traits>(
cex, std::chrono::system_clock::to_time_t(from));

if (INVALID_TIME == next) return (result_type::min)();

return std::chrono::time_point_cast<Duration>(
std::chrono::system_clock::from_time_t(next));
}

template <typename Traits = cron_standard_traits, typename Duration>
static std::chrono::time_point<std::chrono::system_clock, Duration> cron_next_ceil(
cronexpr const & cex,
std::chrono::time_point<std::chrono::system_clock, Duration> const & time_point)
{
using result_type =
std::chrono::time_point<std::chrono::system_clock, Duration>;

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<Traits>(cex, tt);

if (INVALID_TIME == next) return (result_type::min)();

return std::chrono::time_point_cast<Duration>(
std::chrono::system_clock::from_time_t(next));
}
}
2 changes: 1 addition & 1 deletion test/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -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})

Expand Down
103 changes: 103 additions & 0 deletions test/test_chrono.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
#include "catch.hpp"
#include "croncpp.h"

#include <chrono>
#include <string>
#include <type_traits>

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<milliseconds>(point_at("2026-06-01 00:00:00"));
auto const out_ms = cron_next(cex, in_ms);

static_assert(std::is_same<decltype(out_ms), time_point<system_clock, milliseconds> const>::value,
"milliseconds in, milliseconds out");
REQUIRE(text_of(time_point_cast<system_clock::duration>(out_ms)) == "2026-06-01 10:15:00");

auto const in_min = time_point_cast<minutes>(point_at("2026-06-01 00:00:00"));
auto const out_min = cron_next(cex, in_min);

static_assert(std::is_same<decltype(out_min), time_point<system_clock, minutes> const>::value,
"minutes in, minutes out");
REQUIRE(text_of(time_point_cast<system_clock::duration>(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<cron_quartz_traits>("0 15 10 ? * 6#2");
auto const from = point_at("2011-04-30 23:30:00");

REQUIRE(text_of(cron_next<cron_quartz_traits>(cex, from)) == "2011-05-13 10:15:00");
}
Loading