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
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,8 @@ The special characters have the following meaning:

**Note:** the `L`, `W` and `#` special characters are described here for completeness but are **not implemented** by croncpp. An expression that uses one of them is rejected by `make_cron()` with a `bad_cronexpr` exception.

**Note:** an expression describing a date that never occurs, such as `0 0 5 31 2 ?` for the 31st of February, is also rejected by `make_cron()` with a `bad_cronexpr` exception, because it has no next occurrence to compute. February is measured as a leap year, so the 29th is accepted and the 30th is not.

Examples:

| CRON | Description |
Expand Down
69 changes: 66 additions & 3 deletions include/croncpp.h
Original file line number Diff line number Diff line change
Expand Up @@ -386,15 +386,43 @@ namespace cron
}
}

inline bool is_field_separator(char const ch)
{
return ch == ',' || ch == '-' || ch == '/';
}

static std::string replace_ordinals(
std::string text,
std::vector<std::string> const & replacement)
{
for (size_t i = 0; i < replacement.size(); ++i)
{
auto pos = text.find(replacement[i]);
if (std::string::npos != pos)
text.replace(pos, 3 ,std::to_string(i));
std::string const & name = replacement[i];
if (name.empty()) continue;

std::string const value = std::to_string(i);

size_t pos = text.find(name);
while (std::string::npos != pos)
{
// Only a name standing on its own is a name. Replacing it
// wherever it appears turns "JAN1" into "11", which is a
// perfectly good month number and quietly means November.
size_t const end = pos + name.size();

bool const at_start = pos == 0 || is_field_separator(text[pos - 1]);
bool const at_end = end == text.size() || is_field_separator(text[end]);

if (at_start && at_end)
{
text.replace(pos, name.size(), value);
pos = text.find(name, pos + value.size());
}
else
{
pos = text.find(name, pos + 1);
}
}
}

return text;
Expand Down Expand Up @@ -581,6 +609,35 @@ namespace cron
return INVALID_INDEX;
}

// Reports whether any day the expression asks for can occur in any month
// it asks for. February is measured as a leap year, so the 29th counts
// as reachable and the 30th does not.
//
// Both bitsets are indexed from the traits minimum, and every traits
// type numbers January and the first of the month as that minimum, so
// bit 0 is January and day 1 whatever the traits are.
inline bool has_reachable_date(
std::bitset<31> const & days_of_month,
std::bitset<12> const & months)
{
static int const last_day_of[12] =
{ 31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 };

for (size_t month = 0; month < months.size(); ++month)
{
if (!months.test(month)) continue;

for (size_t day = 0; day < days_of_month.size(); ++day)
{
if (days_of_month.test(day) &&
static_cast<int>(day) + 1 <= last_day_of[month])
return true;
}
}

return false;
}

inline int field_value(
std::tm const & date,
cron_field const field)
Expand Down Expand Up @@ -986,6 +1043,12 @@ namespace cron

detail::set_cron_month<Traits>(fields[4], cex.months);

// A date such as the 31st of February never arrives, so there is no
// next occurrence to compute and the expression is rejected here rather
// than leaving the caller to make sense of a search that never succeeds.
if (!detail::has_reachable_date(cex.days_of_month, cex.months))
throw bad_cronexpr("Date specified by the expression is invalid");

cex.expr = expr;

return cex;
Expand Down
23 changes: 23 additions & 0 deletions test/test_oracle.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,29 @@ TEST_CASE("oracle: invalid days of week", "[oracle]")
CRON_EXPECT_EXCEPT("* * * * * 0-8");
}

TEST_CASE("oracle: month and day names have to stand on their own", "[oracle]")
{
CRON_EXPECT_EXCEPT("0 0 0 1 JAN1 *");
CRON_EXPECT_EXCEPT("* * * * * MON1");

// oracle numbers the months from 0, so JAN is 0 and DEC is 11
CRON_ORCL_EQUAL("* * * * JAN *", "* * * * 0 *");
CRON_ORCL_EQUAL("* * * * JAN,DEC *", "* * * * 0,11 *");
CRON_ORCL_EQUAL("* * * * * MON-FRI", "* * * * * 2-6");
}

TEST_CASE("oracle: dates that never occur", "[oracle]")
{
// oracle numbers the months 0 (January) to 11 (December), so February is 1
CRON_EXPECT_EXCEPT("0 0 5 31 1 ?");
CRON_EXPECT_EXCEPT("0 0 0 30 1 *");
CRON_EXPECT_EXCEPT("0 0 0 31 3 *"); // April

REQUIRE_NOTHROW(make_cron<cron_oracle_traits>("0 0 0 29 1 *")); // leap day
REQUIRE_NOTHROW(make_cron<cron_oracle_traits>("0 0 0 31 0 *")); // January
REQUIRE_NOTHROW(make_cron<cron_oracle_traits>("0 0 0 31 2 *")); // March
}

TEST_CASE("oracle: every day of the week is reachable", "[oracle]")
{
// 2023-09-03 is a Sunday; oracle numbers the days 1 (SUN) to 7 (SAT)
Expand Down
51 changes: 51 additions & 0 deletions test/test_standard.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -376,6 +376,57 @@ TEST_CASE("standard: invalid days of week", "[std]")
CRON_EXPECT_EXCEPT("* * * * * /2*-");
}

TEST_CASE("standard: month and day names have to stand on their own", "[std]")
{
// "JAN1" used to be read as "11", which is a valid month number, so the
// expression was accepted and quietly meant November
CRON_EXPECT_EXCEPT("0 0 0 1 JAN1 *");
CRON_EXPECT_EXCEPT("0 0 0 1 1JAN *");
CRON_EXPECT_EXCEPT("0 0 0 1 JANJAN *");
CRON_EXPECT_EXCEPT("0 0 0 1 JANUARY *");
CRON_EXPECT_EXCEPT("0 0 0 1 XJANX *");
CRON_EXPECT_EXCEPT("* * * * * MON1");
CRON_EXPECT_EXCEPT("* * * * * 1MON");
CRON_EXPECT_EXCEPT("* * * * * SUNDAY");
}

TEST_CASE("standard: names next to a separator are still names", "[std]")
{
CRON_STD_EQUAL("* * * * JAN *", "* * * * 1 *");
CRON_STD_EQUAL("* * * * JAN,FEB *", "* * * * 1,2 *");
CRON_STD_EQUAL("* * * * JAN-MAR *", "* * * * 1-3 *");
CRON_STD_EQUAL("* * * * JAN/2 *", "* * * * 1/2 *");
CRON_STD_EQUAL("* * * * FEB-DEC/3 *", "* * * * 2-12/3 *");
CRON_STD_EQUAL("* * * * * MON-FRI", "* * * * * 1-5");
CRON_STD_EQUAL("* * * * * SUN,SAT", "* * * * * 0,6");

// a name repeated in a list is replaced every time, not just the first
CRON_STD_EQUAL("* * * * JAN,JAN *", "* * * * 1 *");
CRON_STD_EQUAL("* * * * * MON,MON,FRI", "* * * * * 1,5");
}

TEST_CASE("standard: issue 7, dates that never occur", "[std]")
{
CRON_EXPECT_EXCEPT("0 0 5 31 2 ?"); // February 31st
CRON_EXPECT_EXCEPT("0 0 0 30 2 *"); // February 30th
CRON_EXPECT_EXCEPT("0 0 0 31 4 *"); // April 31st
CRON_EXPECT_EXCEPT("0 0 0 31 6 *"); // June 31st
CRON_EXPECT_EXCEPT("0 0 0 31 9 *"); // September 31st
CRON_EXPECT_EXCEPT("0 0 0 31 11 *"); // November 31st
CRON_EXPECT_EXCEPT("0 0 0 30,31 2 *");
CRON_EXPECT_EXCEPT("0 0 0 31 2,4,6 *");
}

TEST_CASE("standard: dates that do occur are still accepted", "[std]")
{
REQUIRE_NOTHROW(make_cron("0 0 0 29 2 *")); // leap day
REQUIRE_NOTHROW(make_cron("0 0 0 31 1 *")); // January has 31 days
REQUIRE_NOTHROW(make_cron("0 0 0 31 2,3 *")); // March does too
REQUIRE_NOTHROW(make_cron("0 0 0 30 2,3 *"));
REQUIRE_NOTHROW(make_cron("0 0 0 31 * *"));
REQUIRE_NOTHROW(make_cron("0 0 0 * 2 *"));
}

TEST_CASE("standard: unsupported special characters", "[std]")
{
CRON_EXPECT_EXCEPT("0 0 12 L * ?");
Expand Down
Loading