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
17 changes: 17 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,23 @@ 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).

### Looking backwards

`cron_prev()` is the counterpart of `cron_next()`: it returns the last time matching the expression that is strictly earlier than the one given. It takes the same three argument types and reports failure the same way.

```
auto cron = cron::make_cron("0 15 10 * * ?");

std::time_t now = std::time(0);
std::time_t previous = cron::cron_prev(cron, now);
```

It is answered by calling `cron_next()` repeatedly rather than by searching backwards, which keeps one implementation of the calendar rules instead of two. A single call costs at most **34 calls to `cron_next()`** — six to bracket the answer, twenty seven to halve the interval and one to read it off — and about **8** for an expression that fires every second. That cost follows the width of the interval searched, not how often the expression fires, so an expression firing every second of one day costs no more than one firing once a year.

For a single lookup that is not worth thinking about. For walking a long stretch of history, call `cron_next()` forward from a starting point instead of calling `cron_prev()` in a loop.

`cron_prev()` reaches back at most four years, the same horizon `cron_next()` searches forward. An occurrence older than that is reported as no occurrence at all.

### 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:
Expand Down
116 changes: 116 additions & 0 deletions include/croncpp.h
Original file line number Diff line number Diff line change
Expand Up @@ -1566,4 +1566,120 @@ namespace cron
return std::chrono::time_point_cast<Duration>(
std::chrono::system_clock::from_time_t(next));
}

// The last time matching the expression that is strictly earlier than the
// one given, the counterpart of cron_next.
//
// It is answered with repeated cron_next calls rather than a search running
// backwards, so that it inherits the behaviour of the forward search rather
// than restating it in mirror image. cron_next is monotonic, so "the next
// occurrence is still earlier than the time asked about" holds for every
// instant up to the answer and for none after it, and the boundary between
// the two can be found by halving an interval.
//
// The interval starts as the smallest of a minute, an hour, a day, a month,
// a year and the four year search horizon that contains an occurrence at
// all. That costs one call per size tried, and bounds what the halving then
// has to cover.
//
// The total is at most six probes, twenty seven halvings of the four year
// interval and one final lookup, so thirty four calls; an expression firing
// every second is answered in eight. The count follows the width of the
// interval rather than the number of occurrences inside it, so an
// expression firing every second of a single day costs no more than one
// firing once a year.
template <typename Traits = cron_standard_traits>
static std::time_t cron_prev(cronexpr const & cex, std::time_t const & date)
{
if (cex.empty())
throw bad_cronexpr("Invalid empty cron expression");

static std::time_t const windows[] =
{
60, // a minute
60 * 60, // an hour
24 * 60 * 60, // a day
31 * 24 * 60 * 60, // a month
366 * 24 * 60 * 60, // a year
4 * 366 * 24 * 60 * 60
};

std::time_t low = 0;
bool bracketed = false;

for (size_t i = 0; i < sizeof(windows) / sizeof(windows[0]); ++i)
{
// do not reach back past what a std::time_t can hold
if (date < (std::numeric_limits<std::time_t>::min)() + windows[i]) continue;

std::time_t const start = date - windows[i];
std::time_t const first = cron_next<Traits>(cex, start);

if (INVALID_TIME != first && first < date)
{
low = start;
bracketed = true;
break;
}
}

if (!bracketed) return INVALID_TIME;

// Narrow to the last instant whose next occurrence is still earlier than
// the time asked about. The occurrence after that instant is the answer.
std::time_t high = date;
while (high - low > 1)
{
std::time_t const middle = low + (high - low) / 2;
std::time_t const next = cron_next<Traits>(cex, middle);

if (INVALID_TIME != next && next < date) low = middle;
else high = middle;
}

std::time_t const result = cron_next<Traits>(cex, low);

return (INVALID_TIME != result && result < date) ? result : INVALID_TIME;
}

template <typename Traits = cron_standard_traits>
static std::tm cron_prev(cronexpr const & cex, std::tm date)
{
std::time_t const original = utils::tm_to_time(date);
if (INVALID_TIME == original) return {};

std::time_t const result = cron_prev<Traits>(cex, original);
if (INVALID_TIME == result) return {};

std::tm out;
if (utils::time_to_tm(&result, &out) == nullptr) return {};

return out;
}

template <typename Traits = cron_standard_traits, typename Duration>
static std::chrono::time_point<std::chrono::system_clock, Duration> cron_prev(
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);

// An occurrence at the truncated second is still earlier than a time
// point carrying a fraction of one, so round the input up before asking.
auto tt = std::chrono::system_clock::to_time_t(from);
if (std::chrono::system_clock::from_time_t(tt) < from) {
++tt;
}

auto const previous = cron_prev<Traits>(cex, tt);

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

return std::chrono::time_point_cast<Duration>(
std::chrono::system_clock::from_time_t(previous));
}
}
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_chrono.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_prev.cpp test_quartz.cpp test_special.cpp test_standard.cpp test_years.cpp)

add_executable(test_croncpp ${SOURCES})

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

#include <chrono>
#include <string>

using namespace cron;

namespace
{
std::time_t time_at(std::string const & text)
{
auto date = utils::to_tm(text);

return utils::tm_to_time(date);
}

std::string text_at(std::time_t const t)
{
std::tm tm;
if (utils::time_to_tm(&t, &tm) == nullptr) return "invalid";

return utils::to_string(tm);
}

template <typename Traits>
std::string prev_str(std::string const & expr, std::string const & from)
{
auto cex = make_cron<Traits>(expr);
auto const result = cron_prev<Traits>(cex, time_at(from));

return INVALID_TIME == result ? "none" : text_at(result);
}

std::string prev(std::string const & expr, std::string const & from)
{
return prev_str<cron_standard_traits>(expr, from);
}
}

TEST_CASE("prev: the occurrence before a given time", "[prev]")
{
REQUIRE(prev("0 15 10 * * ?", "2026-06-01 12:00:00") == "2026-06-01 10:15:00");
REQUIRE(prev("0 15 10 * * ?", "2026-06-01 09:00:00") == "2026-05-31 10:15:00");
REQUIRE(prev("0 0 12 * * *", "2026-06-01 12:00:01") == "2026-06-01 12:00:00");
}

TEST_CASE("prev: strictly earlier, as cron_next is strictly later", "[prev]")
{
// asked at an occurrence, the answer is the one before it rather than itself
REQUIRE(prev("0 15 10 * * ?", "2026-06-01 10:15:00") == "2026-05-31 10:15:00");
REQUIRE(prev("* * * * * *", "2026-06-01 10:15:00") == "2026-06-01 10:14:59");
}

TEST_CASE("prev: sparse expressions", "[prev]")
{
REQUIRE(prev("0 0 0 29 2 *", "2026-06-01 00:00:00") == "2024-02-29 00:00:00");
REQUIRE(prev("0 0 0 1 1 ?", "2026-06-01 00:00:00") == "2026-01-01 00:00:00");
REQUIRE(prev("0 15 10 ? * 5L", "2026-06-01 00:00:00") == "2026-05-29 10:15:00");
REQUIRE(prev("0 15 10 L * ?", "2026-06-01 00:00:00") == "2026-05-31 10:15:00");
}

TEST_CASE("prev: an expression with a great many occurrences in the interval", "[prev]")
{
// every second of the 1st of January. Walking the year's occurrences one at
// a time would be 86400 steps; halving the interval is a few dozen.
REQUIRE(prev("* * * 1 1 ?", "2026-06-01 00:00:00") == "2026-01-01 23:59:59");
REQUIRE(prev("* * * 1 1 ?", "2026-01-01 12:00:00") == "2026-01-01 11:59:59");
}

TEST_CASE("prev: expressions with a year", "[prev]")
{
REQUIRE(prev("0 15 10 * * ? 2005", "2006-01-01 00:00:00") == "2005-12-31 10:15:00");
REQUIRE(prev("0 15 10 * * ? 2005", "2005-06-01 00:00:00") == "2005-05-31 10:15:00");

// nothing has happened yet, so there is nothing before it
REQUIRE(prev("0 15 10 * * ? 2050", "2026-06-01 00:00:00") == "none");

// beyond the four year horizon the search reaches back to
REQUIRE(prev("0 15 10 * * ? 2005", "2026-06-01 00:00:00") == "none");
}

TEST_CASE("prev: it agrees with cron_next", "[prev]")
{
// whatever the expression, the occurrence before a time is earlier than it,
// and the occurrence after that one is not
char const * const exprs[] = {
"* * * * * *",
"0 * * * * *",
"0 0/15 * * * *",
"0 15 10 * * ?",
"0 0 0 1 * ?",
"0 15 10 ? * 5#2",
"0 0 0 29 2 *",
};

std::time_t const from = time_at("2026-06-15 13:47:11");

for (auto const & expr : exprs)
{
auto cex = make_cron(expr);

std::time_t at = from;
for (int step = 0; step < 5; ++step)
{
auto const previous = cron_prev(cex, at);
REQUIRE(previous != INVALID_TIME);
REQUIRE(previous < at);

// nothing matches between the two
REQUIRE(cron_next(cex, previous) >= at);

at = previous;
}
}
}

TEST_CASE("prev: the std::tm overload", "[prev]")
{
auto cex = make_cron("0 15 10 * * ?");
auto date = utils::to_tm("2026-06-01 12:00:00");

auto result = cron_prev(cex, date);
REQUIRE(utils::to_string(result) == "2026-06-01 10:15:00");

// a zeroed tm when there is nothing before the time given
auto future = make_cron("0 15 10 * * ? 2050");
auto none = cron_prev(future, date);
REQUIRE(none.tm_year == 0);
REQUIRE(none.tm_mday == 0);
}

TEST_CASE("prev: the time_point overload", "[prev]")
{
using namespace std::chrono;

auto cex = make_cron("0 0 12 * * *");
auto const noon = system_clock::from_time_t(time_at("2026-06-01 12:00:00"));

REQUIRE(text_at(system_clock::to_time_t(cron_prev(cex, noon))) == "2026-05-31 12:00:00");

// a time point just past an occurrence still has that occurrence behind it,
// which truncation to whole seconds would otherwise hide
auto const just_after = noon + milliseconds(50);
REQUIRE(text_at(system_clock::to_time_t(cron_prev(cex, just_after))) == "2026-06-01 12:00:00");

// the duration of the argument is kept, and failure is reported the same
// way as for cron_next
auto const in_ms = time_point_cast<milliseconds>(noon);
auto const out_ms = cron_prev(cex, in_ms);
static_assert(std::is_same<decltype(out_ms), time_point<system_clock, milliseconds> const>::value,
"milliseconds in, milliseconds out");

auto future = make_cron("0 15 10 * * ? 2050");
REQUIRE(cron_prev(future, noon) == (system_clock::time_point::min)());
}

TEST_CASE("prev: an empty expression is refused", "[prev]")
{
cronexpr cex;

REQUIRE_THROWS_AS(cron_prev(cex, time_at("2026-06-01 12:00:00")), bad_cronexpr);
}
Loading