summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorPavel Bashkirov <[email protected]>2025-06-26 12:44:04 +0300
committerrobot-piglet <[email protected]>2025-06-26 12:57:52 +0300
commit0b47b15628153922f04000ad1219c3e0df829385 (patch)
tree1b6dc9a984a2d1cc9dfc2513a7e9c6721a137b38
parenteb4cc660850b42db63b573cb25a6fdd36d4d508d (diff)
Use CRON to set the schedule of queues export
# Description Currently the interval of queues export can only be set as "Every N units". This sometimes does not work well as it always starts the count from the time `0` (January 1st, 1970). It means that, for instance, if the customer wants to set an export to "Every week", it will happen every Thursday. This PR fixes the problem by providing a CRON way to set the schedule which is proven to be very flexible and meeting (almost) any requirement the user may have. * Changelog entry Type: feature Component: queue-agent Support CRON schedules for queue exports. --- Pull Request resolved: https://github.com/ytsaurus/ytsaurus/pull/1239 Co-authored-by: apachee <[email protected]> Co-authored-by: apachee <[email protected]> Co-authored-by: apachee <[email protected]> Co-authored-by: apachee <[email protected]> Co-authored-by: apachee <[email protected]> commit_hash:6a536f5edc17b3ad8d2243d55d876994141d38b0
-rw-r--r--library/cpp/cron_expression/cron_expression.cpp1199
-rw-r--r--library/cpp/cron_expression/cron_expression.h16
-rw-r--r--library/cpp/cron_expression/readme.md98
-rw-r--r--library/cpp/cron_expression/ut/cron_expression_ut.cpp775
-rw-r--r--library/cpp/cron_expression/ut/ya.make11
-rw-r--r--library/cpp/cron_expression/ya.make15
-rw-r--r--yt/yt/client/queue_client/config.cpp30
-rw-r--r--yt/yt/client/queue_client/config.h15
-rw-r--r--yt/yt/client/unittests/queue_static_export_config_ut.cpp55
-rw-r--r--yt/yt/client/unittests/ya.make1
-rw-r--r--yt/yt/client/ya.make1
11 files changed, 2210 insertions, 6 deletions
diff --git a/library/cpp/cron_expression/cron_expression.cpp b/library/cpp/cron_expression/cron_expression.cpp
new file mode 100644
index 00000000000..55b64ee6d24
--- /dev/null
+++ b/library/cpp/cron_expression/cron_expression.cpp
@@ -0,0 +1,1199 @@
+#include "cron_expression.h"
+
+#include <util/generic/bitmap.h>
+#include <util/string/ascii.h>
+#include <util/string/join.h>
+#include <util/string/split.h>
+
+class TCronExpression::TImpl {
+private:
+ static constexpr uint32_t CRON_MAX_SECONDS = 60;
+ static constexpr uint32_t CRON_MAX_MINUTES = 60;
+ static constexpr uint32_t CRON_MAX_HOURS = 24;
+ static constexpr uint32_t CRON_MAX_DAYS_OF_MONTH = 31;
+ static constexpr uint32_t CRON_MAX_DAYS_OF_WEEK = 7;
+ static constexpr uint32_t CRON_MAX_MONTHS = 12;
+ static constexpr uint32_t CRON_MIN_YEARS = 1970;
+ static constexpr uint32_t CRON_MAX_YEARS = 2200;
+ static constexpr uint32_t CRON_MAX_YEARS_DIFF = 4;
+
+ static constexpr uint32_t WEEK_DAYS = 7;
+ static constexpr uint32_t YEARS_GAP_LENGTH = 231;
+
+ enum class ECronField {
+ CF_SECOND,
+ CF_MINUTE,
+ CF_HOUR_OF_DAY,
+ CF_DAY_OF_WEEK,
+ CF_DAY_OF_MONTH,
+ CF_MONTH,
+ CF_YEAR,
+ CF_NEXT
+ };
+
+ enum class ETokenType {
+ TT_ASTERISK,
+ TT_QUESTION,
+ TT_NUMBER,
+ TT_COMMA,
+ TT_SLASH,
+ TT_L,
+ TT_W,
+ TT_HASH,
+ TT_MINUS,
+ TT_WS,
+ TT_EOF,
+ TT_INVALID
+ };
+
+ static constexpr std::array<TStringBuf, 7> DAYS_ARR = {"SUN"sv, "MON"sv, "TUE"sv, "WED"sv, "THU"sv, "FRI"sv, "SAT"sv};
+ static constexpr std::array<TStringBuf, 12> CF_MONTHS_ARR = {"JAN"sv, "FEB"sv, "MAR"sv, "APR"sv, "MAY"sv, "JUN"sv, "JUL"sv, "AUG"sv, "SEP"sv, "OCT"sv, "NOV"sv, "DEC"sv};
+
+ static constexpr TStringBuf ErrorWS = "Fields - expected whitespace separator"sv;
+ static constexpr TStringBuf ErrorOutOfRange = "Idx out of range"sv;
+ static constexpr TStringBuf ErrorUnknownField = "Unknown field"sv;
+ static constexpr TStringBuf ErrorDateNotExists = "Requested date does not exist"sv;
+
+ class TCronExpr {
+ private:
+ TBitMap<60, uint8_t> Seconds;
+ TBitMap<60, uint8_t> Minutes;
+ TBitMap<24, uint8_t> Hours;
+
+ // Sunday can be represented both as 0 and 7
+ TBitMap<8, uint8_t> DaysOfWeek;
+ TBitMap<31, uint8_t> DaysOfMonth;
+ TBitMap<12, uint8_t> Months;
+ int8_t DayInMonth = 0;
+
+ // 0 last day of the month
+ // 1 last weekday of the month
+ // 2 closest weekday to day in month
+ uint8_t Flags = 0;
+ TBitMap<YEARS_GAP_LENGTH, uint8_t> Years;
+
+ private:
+
+ template <size_t N>
+ static TMaybe<uint32_t> NextSetBit(const TBitMap<N, uint8_t>& bits, uint32_t max, uint32_t fromIndex, uint32_t offset) {
+ if (fromIndex < offset || max < offset) {
+ ythrow yexception() << ErrorOutOfRange;
+ }
+ fromIndex -= offset;
+ max -= offset;
+ if (bits.Get(fromIndex)) {
+ return fromIndex + offset;
+ }
+ uint8_t nextBit = bits.NextNonZeroBit(fromIndex);
+ if (nextBit < max) {
+ return nextBit + offset;
+ }
+ return Nothing();
+ }
+
+ template <size_t N>
+ static TMaybe<uint32_t> PrevSetBit(const TBitMap<N, uint8_t>& bits, uint32_t fromIndex, uint32_t toIndex, uint32_t offset) {
+ if (fromIndex < offset || toIndex < offset) {
+ ythrow yexception() << ErrorOutOfRange;
+ }
+ fromIndex -= offset;
+ toIndex -= offset;
+ for (; fromIndex + 1 > toIndex; --fromIndex) {
+ if (bits.Get(fromIndex)) {
+ return fromIndex + offset;
+ }
+ if (fromIndex == 0) {
+ return Nothing();
+ }
+ }
+ return Nothing();
+ }
+
+ public:
+ void SetBit(ECronField field, uint32_t idx) {
+ switch(field) {
+ case ECronField::CF_SECOND: {
+ Seconds.Set(idx);
+ break;
+ }
+ case ECronField::CF_MINUTE: {
+ Minutes.Set(idx);
+ break;
+ }
+ case ECronField::CF_HOUR_OF_DAY: {
+ Hours.Set(idx);
+ break;
+ }
+ case ECronField::CF_DAY_OF_WEEK: {
+ DaysOfWeek.Set(idx);
+ break;
+ }
+ case ECronField::CF_DAY_OF_MONTH: {
+ if (idx < 1) {
+ ythrow yexception() << ErrorOutOfRange;
+ }
+ DaysOfMonth.Set(idx - 1);
+ break;
+ }
+ case ECronField::CF_MONTH: {
+ if (idx < 1) {
+ ythrow yexception() << ErrorOutOfRange;
+ }
+ Months.Set(idx - 1);
+ break;
+ }
+ case ECronField::CF_YEAR: {
+ if (idx < 1970) {
+ ythrow yexception() << ErrorOutOfRange;
+ }
+ Years.Set(idx - 1970);
+ break;
+ }
+ default:
+ ythrow yexception() << ErrorUnknownField;
+ }
+ }
+
+ void DelBit(ECronField field, uint32_t idx) {
+ switch(field) {
+ case ECronField::CF_SECOND: {
+ Seconds.Reset(idx);
+ break;
+ }
+ case ECronField::CF_MINUTE: {
+ Minutes.Reset(idx);
+ break;
+ }
+ case ECronField::CF_HOUR_OF_DAY: {
+ Hours.Reset(idx);
+ break;
+ }
+ case ECronField::CF_DAY_OF_WEEK: {
+ DaysOfWeek.Reset(idx);
+ break;
+ }
+ case ECronField::CF_DAY_OF_MONTH: {
+ if (idx < 1) {
+ ythrow yexception() << ErrorOutOfRange;
+ }
+ DaysOfMonth.Reset(idx - 1);
+ break;
+ }
+ case ECronField::CF_MONTH: {
+ if (idx < 1) {
+ ythrow yexception() << ErrorOutOfRange;
+ }
+ Months.Reset(idx - 1);
+ break;
+ }
+ case ECronField::CF_YEAR: {
+ if (idx < 1970) {
+ ythrow yexception() << ErrorOutOfRange;
+ }
+ Years.Reset(idx - 1970);
+ break;
+ }
+ default:
+ ythrow yexception() << ErrorUnknownField;
+ }
+ }
+
+ uint8_t GetBit(ECronField field, uint32_t idx) {
+ switch(field) {
+ case ECronField::CF_SECOND: {
+ return Seconds.Get(idx);
+ }
+ case ECronField::CF_MINUTE: {
+ return Minutes.Get(idx);
+ }
+ case ECronField::CF_HOUR_OF_DAY: {
+ return Hours.Get(idx);
+ }
+ case ECronField::CF_DAY_OF_WEEK: {
+ return DaysOfWeek.Get(idx);
+ }
+ case ECronField::CF_DAY_OF_MONTH: {
+ if (idx < 1) {
+ ythrow yexception() << ErrorOutOfRange;
+ }
+ return DaysOfMonth.Get(idx - 1);
+ }
+ case ECronField::CF_MONTH: {
+ if (idx < 1) {
+ ythrow yexception() << ErrorOutOfRange;
+ }
+ return Months.Get(idx - 1);
+ }
+ case ECronField::CF_YEAR: {
+ if (idx < 1970) {
+ ythrow yexception() << ErrorOutOfRange;
+ }
+ return Years.Get(idx - 1970);
+ }
+ default: {
+ ythrow yexception() << ErrorUnknownField;
+ }
+ }
+ }
+
+ TMaybe<uint32_t> FindNextSetBit(ECronField field, uint32_t max, uint32_t fromIndex) {
+ switch(field) {
+ case ECronField::CF_SECOND: {
+ return NextSetBit(Seconds, max, fromIndex, 0);
+ }
+ case ECronField::CF_MINUTE: {
+ return NextSetBit(Minutes, max, fromIndex, 0);
+ }
+ case ECronField::CF_HOUR_OF_DAY: {
+ return NextSetBit(Hours, max, fromIndex, 0);
+ }
+ case ECronField::CF_DAY_OF_WEEK: {
+ return NextSetBit(DaysOfWeek, max, fromIndex, 0);
+ }
+ case ECronField::CF_DAY_OF_MONTH: {
+ return NextSetBit(DaysOfMonth, max, fromIndex, 1);
+ }
+ case ECronField::CF_MONTH: {
+ return NextSetBit(Months, max, fromIndex, 1);
+ }
+ case ECronField::CF_YEAR: {
+ return NextSetBit(Years, max, fromIndex, 1970);
+ }
+ default:
+ ythrow yexception() << ErrorUnknownField;
+ }
+ }
+
+ TMaybe<uint32_t> FindPrevSetBit(ECronField field, uint32_t fromIndex, uint32_t toIndex) {
+ switch(field) {
+ case ECronField::CF_SECOND: {
+ return PrevSetBit(Seconds, fromIndex, toIndex, 0);
+ }
+ case ECronField::CF_MINUTE: {
+ return PrevSetBit(Minutes, fromIndex, toIndex, 0);
+ }
+ case ECronField::CF_HOUR_OF_DAY: {
+ return PrevSetBit(Hours, fromIndex, toIndex, 0);
+ }
+ case ECronField::CF_DAY_OF_WEEK: {
+ return PrevSetBit(DaysOfWeek, fromIndex, toIndex, 0);
+ }
+ case ECronField::CF_DAY_OF_MONTH: {
+ return PrevSetBit(DaysOfMonth, fromIndex, toIndex, 1);
+ }
+ case ECronField::CF_MONTH: {
+ return PrevSetBit(Months, fromIndex, toIndex, 1);
+ }
+ case ECronField::CF_YEAR: {
+ return PrevSetBit(Years, fromIndex, toIndex, 1970);
+ }
+ default:
+ ythrow yexception() << ErrorUnknownField;
+ }
+ }
+
+ uint8_t GetFlags() {
+ return Flags;
+ }
+
+ void SetFlag(uint32_t idx) {
+ Y_ENSURE(8 > idx, ErrorOutOfRange);
+ Flags |= static_cast<uint8_t>(1 << idx);
+ }
+
+ int8_t GetDayInMonth() {
+ return DayInMonth;
+ }
+
+ void SetDayInMonth(int8_t dim) {
+ DayInMonth = dim;
+ }
+
+ void AddToDayInMonth(int8_t dim) {
+ DayInMonth += dim;
+ }
+ };
+
+ struct TParserContext {
+ TStringBuf input;
+ ETokenType Type = ETokenType::TT_INVALID;
+ ECronField FieldType = ECronField::CF_SECOND;
+ int32_t Value = 0;
+ uint32_t Min = 0;
+ uint32_t Max = 0;
+ bool FixDow = false;
+ bool LDow = false;
+ };
+
+private:
+ TCronExpr Target_;
+
+ static uint32_t GetWeekday(const NDatetime::TCivilSecond& calendar) {
+ return (static_cast<int32_t>(NDatetime::GetWeekday(calendar)) + 1) % 7;
+ }
+
+ static uint32_t GetField(const NDatetime::TCivilSecond& date, ECronField field) {
+ switch (field) {
+ case ECronField::CF_SECOND:
+ return date.second();
+ case ECronField::CF_MINUTE:
+ return date.minute();
+ case ECronField::CF_HOUR_OF_DAY:
+ return date.hour();
+ case ECronField::CF_DAY_OF_WEEK:
+ return GetWeekday(date);
+ case ECronField::CF_DAY_OF_MONTH:
+ return date.day();
+ case ECronField::CF_MONTH:
+ return date.month();
+ case ECronField::CF_YEAR:
+ return date.year();
+ default:
+ ythrow yexception() << ErrorUnknownField;
+ }
+ }
+
+ static uint32_t LastDayOfMonth(uint32_t month, uint32_t year, bool isWeekday) {
+ NDatetime::TCivilSecond calendar(year, month + 1, 0);
+ uint32_t day = calendar.day();
+
+ if (isWeekday) {
+ uint32_t weekday = GetWeekday(calendar);
+ if (weekday == 0) {
+ if (day < 2) {
+ ythrow yexception() << ErrorOutOfRange;
+ }
+ day -= 2;
+ } else if (weekday == 6) {
+ if (day < 1) {
+ ythrow yexception() << ErrorOutOfRange;
+ }
+ day -= 1;
+ }
+ }
+ return day;
+ }
+
+ static uint32_t ClosestWeekday(uint32_t dayOfMonth, uint32_t month, uint32_t year) {
+ NDatetime::TCivilSecond calendar;
+
+ if (dayOfMonth > LastDayOfMonth(month, year, false)) {
+ ythrow yexception() << "Day of month out of range";
+ }
+
+ calendar = NDatetime::TCivilSecond(year, month, dayOfMonth);
+
+ uint32_t wday = GetWeekday(calendar);
+
+ if (wday == 0) {
+ if (dayOfMonth == LastDayOfMonth(month, year, 0)) {
+ dayOfMonth -= 2;
+ } else {
+ dayOfMonth += 1;
+ }
+
+ } else if (wday == 6) {
+ if (dayOfMonth == 1) {
+ dayOfMonth += 2;
+ } else {
+ dayOfMonth -= 1;
+ }
+ }
+
+ return dayOfMonth;
+ }
+
+ static void SetField(NDatetime::TCivilSecond& calendar, ECronField field, int32_t value) {
+ switch (field) {
+ case ECronField::CF_SECOND:
+ calendar = NDatetime::TCivilSecond(calendar.year(), calendar.month(), calendar.day(), calendar.hour(), calendar.minute(), value);
+ return;
+ case ECronField::CF_MINUTE:
+ calendar = NDatetime::TCivilSecond(calendar.year(), calendar.month(), calendar.day(), calendar.hour(), value, calendar.second());
+ return;
+ case ECronField::CF_HOUR_OF_DAY:
+ calendar = NDatetime::TCivilSecond(calendar.year(), calendar.month(), calendar.day(), value, calendar.minute(), calendar.second());
+ return;
+ case ECronField::CF_DAY_OF_MONTH:
+ calendar = NDatetime::TCivilSecond(calendar.year(), calendar.month(), value, calendar.hour(), calendar.minute(), calendar.second());
+ return;
+ case ECronField::CF_MONTH: {
+ int32_t maxDays = LastDayOfMonth(value, calendar.year(), false);
+ if (calendar.day() > maxDays) {
+ calendar = NDatetime::TCivilSecond(calendar.year(), calendar.month(), maxDays, calendar.hour(), calendar.minute(), calendar.second());
+ }
+ calendar = NDatetime::TCivilSecond(calendar.year(), value, calendar.day(), calendar.hour(), calendar.minute(), calendar.second());
+ return;
+ }
+ case ECronField::CF_YEAR:
+ calendar = NDatetime::TCivilSecond(value, calendar.month(), calendar.day(), calendar.hour(), calendar.minute(), calendar.second());
+ return;
+ default:
+ ythrow yexception() << ErrorUnknownField;
+ }
+ }
+
+ static void AddToField(NDatetime::TCivilSecond& calendar, ECronField field, int32_t value) {
+ SetField(calendar, field, static_cast<int32_t>(GetField(calendar, field)) + value);
+ }
+
+ static void ResetMin(NDatetime::TCivilSecond& calendar, ECronField field) {
+ if (field == ECronField::CF_DAY_OF_MONTH || field == ECronField::CF_MONTH) {
+ SetField(calendar, field, 1);
+ } else {
+ SetField(calendar, field, 0);
+ }
+ }
+
+ static void ResetMax(NDatetime::TCivilSecond& calendar, ECronField field) {
+ switch (field) {
+ case ECronField::CF_SECOND:
+ SetField(calendar, field, CRON_MAX_SECONDS - 1);
+ break;
+ case ECronField::CF_MINUTE:
+ SetField(calendar, field, CRON_MAX_MINUTES - 1);
+ break;
+ case ECronField::CF_HOUR_OF_DAY:
+ SetField(calendar, field, CRON_MAX_HOURS - 1);
+ break;
+ case ECronField::CF_DAY_OF_MONTH:
+ SetField(calendar, field, LastDayOfMonth(calendar.month(), calendar.year(), 0));
+ break;
+ default:
+ break;
+ }
+ }
+
+ static void ResetAllMin(NDatetime::TCivilSecond& calendar, TBitMap<7, uint8_t>& fields) {
+ for (int32_t i = 0; i < 7; ++i) {
+
+ if (fields.Get(i)) {
+ ResetMin(calendar, ECronField(i));
+ }
+ }
+ }
+
+ static void ResetAllMax(NDatetime::TCivilSecond& calendar, TBitMap<7, uint8_t>& fields) {
+ for (int32_t i = 0; i < 7; ++i) {
+ if (fields.Get(i)) {
+ ResetMax(calendar, ECronField(i));
+ }
+ }
+ }
+
+ template <size_t N>
+ static TMaybe<uint32_t> MatchOrdinals(TStringBuf str, std::array<TStringBuf, N> arr) {
+ size_t arrLen = arr.size();
+
+ for (size_t i = 0; i < arrLen; ++i) {
+ if (AsciiHasPrefixIgnoreCase(str, arr[i])) {
+ return i;
+ }
+ }
+ return Nothing();
+ }
+
+ static void TokenNext(TParserContext& context) {
+ auto input = context.input;
+ context.Type = ETokenType::TT_INVALID;
+ context.Value = 0;
+
+ if (context.input.empty() || context.input.front() == '\0') {
+ context.Type = ETokenType::TT_EOF;
+ } else if (IsAsciiSpace(context.input.front())) {
+
+ while (!context.input.empty() && IsAsciiSpace(context.input.front())) {
+ context.input.Skip(1);
+ }
+ context.Type = ETokenType::TT_WS;
+
+ } else if (IsAsciiDigit(context.input.front())) {
+
+ while (!context.input.empty() && IsAsciiDigit(context.input.front())) {
+ context.Value = context.Value * 10 + (context.input.front() - '0');
+ context.input.Skip(1);
+ }
+ context.Type = ETokenType::TT_NUMBER;
+
+ } else {
+ bool isAlpha = IsAsciiAlpha(input.front());
+
+ if (isAlpha) {
+ while (!input.empty() && IsAsciiAlpha(input.front())) {
+ input.Skip(1);
+ }
+ context.Value = MatchOrdinals(context.input, DAYS_ARR).GetOrElse(-1);
+
+ if (context.Value < 0) {
+ context.Value = MatchOrdinals(context.input, CF_MONTHS_ARR).GetOrElse(-1) + 1;
+ if (context.Value == 0) {
+ context.Value = -1;
+ }
+ }
+
+ if (context.Value >= 0) {
+ context.input = input;
+ context.Type = ETokenType::TT_NUMBER;
+ }
+ }
+
+ if (!isAlpha || context.Value < 0) {
+ switch (context.input.front()) {
+ case '*':
+ context.Type = ETokenType::TT_ASTERISK;
+ break;
+ case '?':
+ context.Type = ETokenType::TT_QUESTION;
+ break;
+ case ',':
+ context.Type = ETokenType::TT_COMMA;
+ break;
+ case '/':
+ context.Type = ETokenType::TT_SLASH;
+ break;
+ case 'L':
+ context.Type = ETokenType::TT_L;
+ break;
+ case 'W':
+ context.Type = ETokenType::TT_W;
+ break;
+ case '#':
+ context.Type = ETokenType::TT_HASH;
+ break;
+ case '-':
+ context.Type = ETokenType::TT_MINUS;
+ break;
+ }
+ context.input.Skip(1);
+ }
+ }
+
+ if (ETokenType::TT_INVALID == context.Type) {
+ ythrow yexception() << ErrorUnknownField;
+ }
+ }
+
+ static int32_t Number(TParserContext& context) {
+ int32_t value = 0;
+
+ switch (context.Type) {
+ case ETokenType::TT_MINUS:
+ TokenNext(context);
+
+ if (ETokenType::TT_NUMBER == context.Type) {
+ value = -context.Value;
+ TokenNext(context);
+ } else {
+ ythrow yexception() << "Number '-' follows with number";
+ }
+ break;
+ case ETokenType::TT_NUMBER:
+ value = context.Value;
+ TokenNext(context);
+ break;
+ default:
+ ythrow yexception() << "Number - error";
+ }
+
+ return value;
+ }
+
+ static uint32_t Frequency(TParserContext& context, uint32_t delta, uint32_t& to, bool range) {
+ switch (context.Type) {
+ case ETokenType::TT_SLASH: {
+ TokenNext(context);
+ if (ETokenType::TT_NUMBER == context.Type) {
+ delta = context.Value;
+ if (delta < 1) {
+ ythrow yexception() << "Frequency - needs to be at least 1";
+ }
+ if (!range) {
+ to = context.Max - 1;
+ }
+ TokenNext(context);
+ } else {
+ ythrow yexception() << "Frequency - '/' follows with number";
+ }
+ break;
+ }
+ case ETokenType::TT_COMMA:
+ case ETokenType::TT_WS:
+ case ETokenType::TT_EOF:
+ break;
+ default:
+ ythrow yexception() << "Frequency - error";
+ }
+ return delta;
+ }
+
+ uint32_t Range(TParserContext& context, uint32_t& from, uint32_t to) {
+ switch (context.Type) {
+ case ETokenType::TT_HASH: {
+
+ if (ECronField::CF_DAY_OF_WEEK == context.FieldType) {
+ TokenNext(context);
+
+ if (Target_.GetDayInMonth()) {
+ ythrow yexception() << "Nth-day - support for specifying multiple '#' segments is not implemented";
+ }
+
+ Target_.SetDayInMonth(Number(context));
+
+ if (Target_.GetDayInMonth() > 5 || Target_.GetDayInMonth() < -5) {
+ ythrow yexception() << "Nth-day - '#' can follow only with -5..5";
+ }
+ } else {
+ ythrow yexception() << "Nth-day - '#' allowed only for day of week";
+ }
+ break;
+ }
+ case ETokenType::TT_MINUS: {
+ TokenNext(context);
+
+ if (ETokenType::TT_NUMBER == context.Type) {
+ to = context.Value;
+ TokenNext(context);
+ } else {
+ ythrow yexception() << "Range '-' follows with number";
+ }
+ break;
+ }
+ case ETokenType::TT_W: {
+ Target_.SetDayInMonth(static_cast<int8_t>(to));
+ from = context.Min;
+
+ for (uint32_t i = 1; i <= 5; ++i) {
+ Target_.SetBit(ECronField::CF_DAY_OF_WEEK, i);
+ }
+ Target_.SetFlag(2);
+ to = context.Max - 1;
+ TokenNext(context);
+ break;
+ }
+ case ETokenType::TT_L: {
+ if (ECronField::CF_DAY_OF_WEEK == context.FieldType) {
+ Target_.SetDayInMonth(-1);
+ TokenNext(context);
+ } else {
+ ythrow yexception() << "Range - 'L' allowed only for day of week";
+ }
+ break;
+ }
+ case ETokenType::TT_WS:
+ case ETokenType::TT_SLASH:
+ case ETokenType::TT_COMMA:
+ case ETokenType::TT_EOF:
+ break;
+ default:
+ ythrow yexception() << "Range - error";
+ }
+
+ return to;
+ }
+
+ void Segment(TParserContext& context) {
+ uint32_t from = context.Min;
+ Y_ENSURE(context.Max > 0);
+ uint32_t to = context.Max - 1;
+ uint32_t delta = 1;
+ bool isLW = false;
+
+ switch (context.Type) {
+ case ETokenType::TT_ASTERISK: {
+ TokenNext(context);
+ delta = Frequency(context, delta, to, false);
+ break;
+ }
+ case ETokenType::TT_NUMBER: {
+ from = context.Value;
+ TokenNext(context);
+ to = Range(context, from, from);
+ delta = Frequency(context, delta, to, from != to);
+ break;
+ }
+ case ETokenType::TT_L: {
+ TokenNext(context);
+
+ switch (context.FieldType) {
+ case ECronField::CF_DAY_OF_MONTH: {
+ Target_.SetDayInMonth(-1);
+
+ switch (context.Type) {
+ case ETokenType::TT_MINUS:
+ case ETokenType::TT_NUMBER: {
+ Target_.AddToDayInMonth(Number(context));
+ break;
+ }
+ case ETokenType::TT_W: {
+ if (ECronField::CF_DAY_OF_MONTH == context.FieldType) {
+ TokenNext(context);
+
+ for (uint32_t i = 1; i <= 5; ++i) {
+ Target_.SetBit(ECronField::CF_DAY_OF_WEEK, i);
+ }
+
+ Target_.SetFlag(1);
+ context.FixDow = true;
+ isLW = true;
+ } else {
+ ythrow yexception() << "Offset - 'W' allowed only for day of month";
+ }
+ break;
+ }
+ case ETokenType::TT_COMMA:
+ case ETokenType::TT_WS:
+ case ETokenType::TT_EOF:
+ break;
+ default:
+ ythrow yexception() << "Offset - error";
+ }
+ /* Note 0..6 and not 1..7*/
+ if (!isLW) {
+ Target_.SetFlag(0);
+ context.FixDow = true;
+ context.LDow = true;
+ }
+ break;
+ }
+ case ECronField::CF_DAY_OF_WEEK:
+ from = to = 0;
+ break;
+ default:
+ ythrow yexception() << "Segment 'L' allowed only for day of month and day of week";
+ }
+ break;
+ }
+ case ETokenType::TT_W: {
+ if (ECronField::CF_DAY_OF_MONTH != context.FieldType) {
+ ythrow yexception() << "Segment 'W' allowed only for day of month";
+ }
+ for (uint32_t i = 1; i <= 5; ++i) {
+ Target_.SetBit(ECronField::CF_DAY_OF_WEEK, i);
+ }
+ TokenNext(context);
+ context.FixDow = true;
+ break;
+ }
+ case ETokenType::TT_QUESTION: {
+ TokenNext(context);
+ break;
+ }
+ default:
+ ythrow yexception() << "Segment - error";
+ }
+
+ if (ECronField::CF_DAY_OF_WEEK == context.FieldType && context.FixDow && !context.LDow) {
+ return;
+ }
+
+ if (from < context.Min || to < context.Min) {
+ ythrow yexception() << "Range - specified range is less than minimum";
+ }
+
+ if (from >= context.Max || to >= context.Max) {
+ ythrow yexception() << "Range - specified range exceeds maximum";
+ }
+
+ if (from > to && ECronField::CF_DAY_OF_WEEK != context.FieldType) {
+ ythrow yexception() << "Range - specified range start exceeds range end";
+ }
+
+ if (from > to && ECronField::CF_DAY_OF_WEEK == context.FieldType) {
+ for (; from < 7; from += delta) {
+ Target_.SetBit(context.FieldType, from);
+ }
+ for (from %= 7; from <= to; from += delta) {
+ Target_.SetBit(context.FieldType, from);
+ }
+ }
+
+ for (; from <= to; from += delta) {
+ Target_.SetBit(context.FieldType, from);
+ }
+
+ if (ECronField::CF_DAY_OF_WEEK == context.FieldType && Target_.GetBit(ECronField::CF_DAY_OF_WEEK, 7)) {
+
+ // Sunday can be represented as 0 or 7
+ Target_.SetBit(ECronField::CF_DAY_OF_WEEK, 0);
+ Target_.DelBit(ECronField::CF_DAY_OF_WEEK, 7);
+ }
+ return;
+ }
+
+ void Field(TParserContext& context) {
+ Segment(context);
+ switch (context.Type) {
+ case ETokenType::TT_COMMA:
+ TokenNext(context);
+ Field(context);
+ break;
+ case ETokenType::TT_WS:
+ case ETokenType::TT_EOF:
+ break;
+ default:
+ ythrow yexception() << "FieldRest - error";
+ }
+ return;
+ }
+
+ void FieldWrapper(TParserContext& context, ECronField FieldType, uint32_t min, uint32_t max) {
+ context.FieldType = FieldType;
+ context.Min = min;
+ context.Max = max;
+
+ Field(context);
+ }
+
+ void Fields(TParserContext& context, uint32_t len) {
+ TokenNext(context);
+
+ if (len < 6) {
+ Target_.SetBit(ECronField::CF_SECOND, 0);
+ } else {
+ FieldWrapper(context, ECronField::CF_SECOND, 0, CRON_MAX_SECONDS);
+
+ if (ETokenType::TT_WS == context.Type) {
+ TokenNext(context);
+ } else {
+ ythrow yexception() << ErrorWS;
+ }
+ }
+
+ FieldWrapper(context, ECronField::CF_MINUTE, 0, CRON_MAX_MINUTES);
+ if (ETokenType::TT_WS == context.Type) {
+ TokenNext(context);
+ } else {
+ ythrow yexception() << ErrorWS;
+ }
+
+ FieldWrapper(context, ECronField::CF_HOUR_OF_DAY, 0, CRON_MAX_HOURS);
+ if (ETokenType::TT_WS == context.Type) {
+ TokenNext(context);
+ } else {
+ ythrow yexception() << ErrorWS;
+ }
+
+ FieldWrapper(context, ECronField::CF_DAY_OF_MONTH, 1, CRON_MAX_DAYS_OF_MONTH + 1);
+ if (ETokenType::TT_WS == context.Type) {
+ TokenNext(context);
+ } else {
+ ythrow yexception() << ErrorWS;
+ }
+
+ FieldWrapper(context, ECronField::CF_MONTH, 1, CRON_MAX_MONTHS + 1);
+ if (ETokenType::TT_WS == context.Type) {
+ TokenNext(context);
+ } else {
+ ythrow yexception() << ErrorWS;
+ }
+
+ FieldWrapper(context, ECronField::CF_DAY_OF_WEEK, 0, CRON_MAX_DAYS_OF_WEEK + 1);
+ if (len < 7) {
+ Target_.SetBit(ECronField::CF_YEAR, 1970 + YEARS_GAP_LENGTH - 1);
+ } else {
+ if (ETokenType::TT_WS == context.Type) {
+ TokenNext(context);
+ } else {
+ ythrow yexception() << ErrorWS;
+ }
+
+ FieldWrapper(context, ECronField::CF_YEAR, CRON_MIN_YEARS, CRON_MAX_YEARS);
+ }
+ return;
+ }
+
+ /////////////////////////////////////////////////
+
+ TMaybe<uint32_t> FindNextPrev(uint32_t max, uint32_t value, uint32_t min, NDatetime::TCivilSecond& calendar, ECronField field, ECronField nextField, TBitMap<7, uint8_t>& lowerOrders, int32_t offset) {
+ TMaybe<uint32_t> nextValue = (offset > 0 ? Target_.FindNextSetBit(field, max, value) : Target_.FindPrevSetBit(field, value, min));
+
+ // roll under if needed
+ if (nextValue.Empty()) {
+ if (nextField == ECronField::CF_NEXT) {
+ ythrow yexception() << ErrorDateNotExists;
+ }
+ if (offset > 0) {
+ ResetMax(calendar, field);
+ } else {
+ ResetMin(calendar, field);
+ }
+ AddToField(calendar, nextField, offset);
+
+ nextValue = offset > 0 ? Target_.FindNextSetBit(field, max, min) : Target_.FindPrevSetBit(field, max - 1, value);
+ }
+
+ if (nextValue.Empty() || nextValue != value) {
+ if (offset > 0) {
+ ResetAllMin(calendar, lowerOrders);
+ } else {
+ ResetAllMax(calendar, lowerOrders);
+ }
+ if (nextValue.Empty()) {
+ SetField(calendar, field, 0);
+ return 0;
+ } else {
+ SetField(calendar, field, nextValue.GetRef());
+ }
+
+ }
+ return nextValue;
+ }
+
+ bool FindDayCondition(const NDatetime::TCivilSecond& calendar, int8_t dim, uint32_t dom, uint32_t dow, uint8_t flags, TMaybe<uint32_t> day) {
+ if (day.Empty()) {
+ if ((!flags && dim < 0) || flags & 1) {
+ day = LastDayOfMonth(calendar.month(), calendar.year(), 0);
+ } else if (flags & 2) {
+ day = LastDayOfMonth(calendar.month(), calendar.year(), 1);
+ } else if (flags & 4) {
+ Y_ENSURE(dim >= 0);
+ day = ClosestWeekday(dim, calendar.month(), calendar.year());
+ }
+ }
+
+ if (!Target_.GetBit(ECronField::CF_DAY_OF_MONTH, dom) || !Target_.GetBit(ECronField::CF_DAY_OF_WEEK, dow)) {
+ return true;
+ }
+
+ int64_t dayValue = day.Empty() ? 0 : (day.GetRef() + 1);
+ int64_t domValue = static_cast<int64_t>(dom);
+
+ if (flags) {
+ if ((flags & 3) && domValue != dayValue + dim) {
+ return true;
+ }
+
+ if ((flags & 4) && domValue != dayValue - 1) {
+ return true;
+ }
+ } else {
+ if (dim < 0 && (domValue < dayValue + static_cast<int64_t>(WEEK_DAYS) * dim || domValue >= dayValue + static_cast<int64_t>(WEEK_DAYS) * (dim + 1))) {
+ return true;
+ }
+
+ if (dim > 0 && (domValue < static_cast<int64_t>(WEEK_DAYS) * (dim - 1) + 1 || domValue >= static_cast<int64_t>(WEEK_DAYS) * dim + 1)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ TMaybe<int32_t> FindDay(NDatetime::TCivilSecond& calendar, int8_t dim, uint32_t dom, uint32_t dow, uint8_t flags, TBitMap<7, uint8_t>& resets, int32_t offset) {
+ TMaybe<uint32_t> day = Nothing();
+ uint32_t year = calendar.year();
+ uint32_t month = calendar.month();
+ uint32_t count = 0;
+ uint32_t max = 366;
+
+ while (FindDayCondition(calendar, dim, dom, dow, flags, day) && count++ < max) {
+ if (offset > 0) {
+ ResetAllMin(calendar, resets);
+ } else {
+ ResetAllMax(calendar, resets);
+ }
+
+ AddToField(calendar, ECronField::CF_DAY_OF_MONTH, offset);
+
+ dom = calendar.day();
+ dow = GetWeekday(calendar);
+
+ if (year != calendar.year()) {
+ year = calendar.year();
+ day = Nothing(); /* This should not be needed unless there is as single day month in libc. */
+ }
+
+ if (month != static_cast<uint32_t>(calendar.month())) {
+ month = calendar.month();
+ day = Nothing();
+ Y_ENSURE(day.Empty());
+ }
+ }
+ return dom;
+ }
+
+ void DoNextPrev(NDatetime::TCivilSecond& calendar, int32_t offset) {
+ uint32_t baseYear = calendar.year();
+ uint32_t value = 0;
+ TMaybe<uint32_t> updateValue = Nothing();
+ TBitMap<7, uint8_t> resets(0);
+
+ for (;;) {
+ resets.Clear();
+
+ value = GetField(calendar, ECronField::CF_SECOND);
+ updateValue = FindNextPrev(CRON_MAX_SECONDS, value, 0, calendar, ECronField::CF_SECOND, ECronField::CF_MINUTE, resets, offset);
+
+ if (updateValue.Empty()) {
+ ythrow yexception() << ErrorDateNotExists;
+ }
+
+ if (value == updateValue) {
+ resets.Set(static_cast<uint32_t>(ECronField::CF_SECOND));
+ } else {
+ continue;
+ }
+
+ value = GetField(calendar, ECronField::CF_MINUTE);
+ updateValue = FindNextPrev(CRON_MAX_MINUTES, value, 0, calendar, ECronField::CF_MINUTE, ECronField::CF_HOUR_OF_DAY, resets, offset);
+
+ if (updateValue.Empty()) {
+ ythrow yexception() << ErrorDateNotExists;
+ }
+
+ if (value == updateValue) {
+ resets.Set(static_cast<uint32_t>(ECronField::CF_MINUTE));
+ } else {
+ continue;
+ }
+
+ value = GetField(calendar, ECronField::CF_HOUR_OF_DAY);
+ updateValue = FindNextPrev(CRON_MAX_HOURS, value, 0, calendar, ECronField::CF_HOUR_OF_DAY, ECronField::CF_DAY_OF_MONTH, resets, offset);
+
+ if (updateValue.Empty()) {
+ ythrow yexception() << ErrorDateNotExists;
+ }
+
+ if (value == updateValue) {
+ resets.Set(static_cast<uint32_t>(ECronField::CF_HOUR_OF_DAY));
+ } else {
+ continue;
+ }
+
+ value = GetField(calendar, ECronField::CF_DAY_OF_MONTH);
+ updateValue = FindDay(calendar, Target_.GetDayInMonth(), value, GetWeekday(calendar), Target_.GetFlags(), resets, offset);
+ if (updateValue.Empty()) {
+ ythrow yexception() << ErrorDateNotExists;
+ }
+
+ if (value == updateValue) {
+ resets.Set(static_cast<uint32_t>(ECronField::CF_DAY_OF_MONTH));
+ } else {
+ continue;
+ }
+
+ value = GetField(calendar, ECronField::CF_MONTH);
+ updateValue = FindNextPrev(CRON_MAX_MONTHS, value, 1, calendar, ECronField::CF_MONTH, ECronField::CF_YEAR, resets, offset);
+
+ if (updateValue.Empty()) {
+ ythrow yexception() << ErrorDateNotExists;
+ }
+
+ if (value != updateValue) {
+ if (static_cast<uint32_t>(std::abs(calendar.year() - baseYear)) > CRON_MAX_YEARS_DIFF) {
+ break;
+ }
+ continue;
+ }
+
+ if (Target_.GetBit(ECronField::CF_YEAR, 1970 + YEARS_GAP_LENGTH - 1)) {
+ break;
+ } else {
+ resets.Set(static_cast<uint32_t>(ECronField::CF_MONTH));
+ }
+
+ value = GetField(calendar, ECronField::CF_YEAR);
+ updateValue = FindNextPrev(CRON_MAX_YEARS, value, CRON_MIN_YEARS, calendar, ECronField::CF_YEAR, ECronField::CF_NEXT, resets, offset);
+
+ if (updateValue.Empty()) {
+ ythrow yexception() << ErrorDateNotExists;
+ }
+
+ if (value == updateValue) {
+ break;
+ }
+ }
+ }
+
+ NDatetime::TCivilSecond Cron(NDatetime::TCivilSecond original, int32_t offset) {
+ NDatetime::TCivilSecond calendar;
+ calendar = original;
+
+ DoNextPrev(calendar, offset);
+
+ if (calendar == original) {
+ /* We arrived at the original timestamp - round up to the next whole second and try again... */
+ AddToField(calendar, ECronField::CF_SECOND, offset);
+ DoNextPrev(calendar, offset);
+ }
+
+ if (
+ !Target_.GetBit(ECronField::CF_SECOND, calendar.second()) ||
+ !Target_.GetBit(ECronField::CF_MINUTE, calendar.minute()) ||
+ !Target_.GetBit(ECronField::CF_HOUR_OF_DAY, calendar.hour()) ||
+ !Target_.GetBit(ECronField::CF_DAY_OF_MONTH, calendar.day()) ||
+ !Target_.GetBit(ECronField::CF_MONTH, calendar.month()) ||
+ !(Target_.GetBit(ECronField::CF_YEAR, 1970 + YEARS_GAP_LENGTH - 1) || Target_.GetBit(ECronField::CF_YEAR, calendar.year())) ||
+ !Target_.GetBit(ECronField::CF_DAY_OF_WEEK, GetWeekday(calendar))
+ ) {
+ ythrow yexception() << ErrorDateNotExists;
+ }
+
+ return calendar;
+ }
+
+ void CronParseExpr(TStringBuf expression) {
+ TParserContext context;
+ if (expression.empty()) {
+ ythrow yexception() << "Invalid empty expression";
+ }
+
+ if ('@' == expression.front()) {
+ if (expression == "@annually"sv || expression == "@yearly"sv) {
+ expression = "0 0 0 1 1 *"sv;
+ } else if (expression == "@monthly"sv) {
+ expression = "0 0 0 1 * *"sv;
+ } else if (expression == "@weekly"sv) {
+ expression = "0 0 0 * * 0"sv;
+ } else if (expression == "@daily"sv || expression == "@midnight"sv) {
+ expression = "0 0 0 * * *"sv;
+ } else if (expression == "@hourly"sv) {
+ expression = "0 0 * * * *"sv;
+ } else if (expression == "@minutely"sv) {
+ expression = "0 * * * * *"sv;
+ } else if (expression == "@secondly"sv) {
+ expression = "* * * * * * *"sv;
+ } else if (expression == "@reboot"sv) {
+ ythrow yexception() << "@reboot not implemented";
+ } else {
+ ythrow yexception() << "Unknown typed cron";
+ }
+ }
+ uint32_t len = StringSplitter(expression).Split(' ').SkipEmpty().Count();
+ if (len < 5 || len > 7) {
+ ythrow yexception() << Join(" ", "Invalid number of fields, expression must consist of 5-7 fields but has:"sv, len);
+ }
+
+ context.input = expression;
+ Fields(context, len);
+ return;
+ }
+
+public:
+
+ NDatetime::TCivilSecond FindCronNext(NDatetime::TCivilSecond date) {
+ return Cron(date, +1);
+ }
+
+ NDatetime::TCivilSecond FindCronPrev(NDatetime::TCivilSecond date) {
+ return Cron(date, -1);
+ }
+
+ TImpl(const TStringBuf cronUnparsedExpr) {
+ CronParseExpr(cronUnparsedExpr);
+ }
+};
+
+TCronExpression::TCronExpression(const TStringBuf cronUnparsedExpr)
+ : Impl(MakeHolder<TImpl>(cronUnparsedExpr))
+{
+}
+
+TCronExpression::~TCronExpression() = default;
+
+NDatetime::TCivilSecond TCronExpression::CronNext(NDatetime::TCivilSecond date) {
+ return Impl->FindCronNext(date);
+}
+
+NDatetime::TCivilSecond TCronExpression::CronPrev(NDatetime::TCivilSecond date) {
+ return Impl->FindCronPrev(date);
+}
diff --git a/library/cpp/cron_expression/cron_expression.h b/library/cpp/cron_expression/cron_expression.h
new file mode 100644
index 00000000000..2b51a72957d
--- /dev/null
+++ b/library/cpp/cron_expression/cron_expression.h
@@ -0,0 +1,16 @@
+#pragma once
+
+#include <library/cpp/timezone_conversion/civil.h>
+
+class TCronExpression {
+public:
+ TCronExpression(const TStringBuf cronUnparsedExpr);
+ ~TCronExpression();
+
+ NDatetime::TCivilSecond CronNext(NDatetime::TCivilSecond date);
+ NDatetime::TCivilSecond CronPrev(NDatetime::TCivilSecond date);
+
+private:
+ class TImpl;
+ THolder<TImpl> Impl;
+};
diff --git a/library/cpp/cron_expression/readme.md b/library/cpp/cron_expression/readme.md
new file mode 100644
index 00000000000..a247aa69237
--- /dev/null
+++ b/library/cpp/cron_expression/readme.md
@@ -0,0 +1,98 @@
+
+CronExpression
+=============
+
+Implementation
+--------------
+This reference is based on:
+- [exander77's supertinycron on GitHub](https://github.com/exander77/supertinycron/blob/master/README.md#supertinycron)
+
+Overview
+--------------
+
+```
+Field name Mandatory? Allowed values Allowed special characters
+---------- ---------- -------------- -------------------------
+Second No 0-59 * / , - L
+Minute Yes 0-59 * / , -
+Hour Yes 0-23 * / , -
+Day of month Yes 1-31 * / , - L W
+Month Yes 1-12 or JAN-DEC * / , -
+Day of week Yes 0-6 or SUN-SAT * / , - L #
+Year No 1970–2199 * / , -
+```
+
+### Special Characters
+
+#### Asterisk `*`
+The asterisk indicates that the cron expression matches all values of the field. For instance, an asterisk in the 'Month' field matches every month.
+
+#### Hyphen `-`
+Hyphens define ranges. For instance, `2000-2010` in the 'Year' field matches every year from 2000 to 2010, inclusive.
+
+#### Slash `/`
+Slashes specify increments within ranges. For example, `3-59/15` in the 'Minute' field matches the third minute of the hour and every 15 minutes thereafter. The form `*/...` is equivalent to `first-last/...`, representing an increment over the full range of the field. (Example: `*/40` in minute field means repeating every time when minutes amount matches `0` or `40`, but not every `40` minutes!)
+
+#### Comma `,`
+Commas separate items in a list. For instance, `MON,WED,FRI` in the 'Day of week' field matches Mondays, Wednesdays, and Fridays.
+
+#### `L`
+The character `L` stands for "last". In the 'Day of week' field, `5L` denotes the last Friday of a given month. In the 'Day of month' field, it represents the last day of the month.
+
+- Using `L` alone in the 'Day of week' field is equivalent to `0` or `SAT`. Hence, expressions `* * * * * L *` and `* * * * * 0 *` are the same.
+
+- When followed by another value in the 'Day of week' field, like `5L`, it signifies the last Friday of the month.
+
+- If followed by a negative number in the 'Day of month' field, such as `L-3`, it indicates the third-to-last day of the month.
+
+Using 'L' with other specifying lists or ranges is allowed, in this case different requirements must be satisfied at the sane time (Example: `* * * L 1 L` indicates dates, when `31JAN` is `SAT`).
+
+#### `W`
+The `W` character is exclusive to the 'Day of month' field. It indicates the closest business day (Monday-Friday) to the given day. For example, `15W` means the nearest business day to the 15th of the month. If you set 1W for the day-of-month and the 1st falls on a Saturday, the trigger activates on Monday the 3rd, since it respects the month's day boundaries and won't skip over them. Similarly, at the end of the month, the behavior ensures it doesn't "jump" over the boundary to the following month.
+
+The `W` character can also pair with `L` (as `LW`), signifying the last business day of the month. Alone, it's equivalent to the range `1-5`, making the expressions `* * * W * * *` and `* * * * * 1-5 *` identical.
+
+#### Hash `#`
+The `#` character is only for the 'Day of week' field and should be followed by a number between one and five, or their negative values. It lets you specify constructs like "the second Friday" of a month.
+
+For example, `6#3` means the third Friday of the month. Note that if you use `#5` and there isn't a fifth occurrence of that weekday in the month, no firing occurs for that month. Using the '#' character requires a single expression in the 'Day of week' field.
+
+Negative nth values are also valid. For instance, `6#-1` is equivalent to `6L`.
+
+Predefined cron expressions
+---------------------------
+(Copied from <https://en.wikipedia.org/wiki/Cron#Predefined_scheduling_definitions>, with text modified according to this implementation)
+
+ Entry Description Equivalent to
+ @annually Run once a year at midnight in the morning of January 1 0 0 0 1 1 *
+ @yearly Run once a year at midnight in the morning of January 1 0 0 0 1 1 *
+ @monthly Run once a month at midnight in the morning of the first of the month 0 0 0 1 * *
+ @weekly Run once a week at midnight in the morning of Sunday 0 0 0 * * 0
+ @daily Run once a day at midnight 0 0 0 * * *
+ @hourly Run once an hour at the beginning of the hour 0 0 * * * *
+ @minutely Run once a minute at the beginning of minute 0 * * * * *
+ @secondly Run once every second * * * * * * *
+ @reboot Not supported
+
+Other details
+-------------
+* If only five fields are present, the Year and Second fields are omitted. The omitted Year and Second are `*` and `0` respectively.
+* If only six fields are present, the Year field is omitted. The omitted Year is set to `*`.
+
+Usage
+-----
+
+TCronExpression has a constuctor from cron expression. Then has two methods: CronNext(TInstant), CronPrev(TInstant), which returnes next (previous) appropriate date from given.
+
+Examples of supported expressions
+---------------------------------
+
+Expression, input date, next date:
+
+ "*/15 * 1-4 * * *", "2012-07-01_09:53:50", "2012-07-02_01:00:00"
+ "0 */2 1-4 * * *", "2012-07-01_09:00:00", "2012-07-02_01:00:00"
+ "0 0 7 ? * MON-FRI", "2009-09-26_00:42:55", "2009-09-28_07:00:00"
+ "0 */40 * * * *", "2004-09-01_23:46:00", "2004-09-02_00:00:00"
+ "0 30 23 30 1/3 ?", "2011-04-30_23:30:00", "2011-07-30_23:30:00"
+
+See more examples in /ut.
diff --git a/library/cpp/cron_expression/ut/cron_expression_ut.cpp b/library/cpp/cron_expression/ut/cron_expression_ut.cpp
new file mode 100644
index 00000000000..999f1e00260
--- /dev/null
+++ b/library/cpp/cron_expression/ut/cron_expression_ut.cpp
@@ -0,0 +1,775 @@
+#include <library/cpp/cron_expression/cron_expression.h>
+
+#include <util/datetime/base.h>
+#include <library/cpp/testing/unittest/registar.h>
+
+namespace {
+
+void TestNext(const TStringBuf cron, const TStringBuf date, const TStringBuf ans) {
+ TInstant ansTI = TInstant::ParseIso8601(ans);
+ TInstant dateTI = TInstant::ParseIso8601(date);
+ NDatetime::TCivilSecond ansTZ = NDatetime::Convert(ansTI, "UTC");
+ NDatetime::TCivilSecond dateTZ = NDatetime::Convert(dateTI, "UTC");
+ TCronExpression cronConverter(cron);
+ UNIT_ASSERT_VALUES_EQUAL(cronConverter.CronNext(dateTZ), ansTZ);
+}
+
+void TestPrev(const TStringBuf cron, const TStringBuf date, const TStringBuf ans) {
+ TInstant ansTI = TInstant::ParseIso8601(ans);
+ TInstant dateTI = TInstant::ParseIso8601(date);
+ NDatetime::TCivilSecond ansTZ = NDatetime::Convert(ansTI, "UTC");
+ NDatetime::TCivilSecond dateTZ = NDatetime::Convert(dateTI, "UTC");
+ TCronExpression cronConverter(cron);
+ UNIT_ASSERT_VALUES_EQUAL(cronConverter.CronPrev(dateTZ), ansTZ);
+}
+
+void TestNextCron(TCronExpression* cron, const TStringBuf date, const TStringBuf ans) {
+ TInstant ansTI = TInstant::ParseIso8601(ans);
+ TInstant dateTI = TInstant::ParseIso8601(date);
+ NDatetime::TCivilSecond ansTZ = NDatetime::Convert(ansTI, "UTC");
+ NDatetime::TCivilSecond dateTZ = NDatetime::Convert(dateTI, "UTC");
+ UNIT_ASSERT_VALUES_EQUAL(cron->CronNext(dateTZ), ansTZ);
+}
+
+void TestPrevCron(TCronExpression* cron, const TStringBuf date, const TStringBuf ans) {
+ TInstant ansTI = TInstant::ParseIso8601(ans);
+ TInstant dateTI = TInstant::ParseIso8601(date);
+ NDatetime::TCivilSecond ansTZ = NDatetime::Convert(ansTI, "UTC");
+ NDatetime::TCivilSecond dateTZ = NDatetime::Convert(dateTI, "UTC");
+ UNIT_ASSERT_VALUES_EQUAL(cron->CronPrev(dateTZ), ansTZ);
+}
+
+void TestNextCronBadRequest(TCronExpression* cron, const TStringBuf date) {
+ TInstant dateTI = TInstant::ParseIso8601(date);
+ NDatetime::TCivilSecond dateTZ = NDatetime::Convert(dateTI, "UTC");
+ UNIT_ASSERT_EXCEPTION(cron->CronNext(dateTZ), yexception);
+}
+
+void TestPrevCronBadRequest(TCronExpression* cron, const TStringBuf date) {
+ TInstant dateTI = TInstant::ParseIso8601(date);
+ NDatetime::TCivilSecond dateTZ = NDatetime::Convert(dateTI, "UTC");
+ UNIT_ASSERT_EXCEPTION(cron->CronPrev(dateTZ), yexception);
+}
+
+Y_UNIT_TEST_SUITE(TestNext) {
+ Y_UNIT_TEST(Basic) {
+ TestNext("* * * * * *", "2022-12-29T23:59:59Z", "2022-12-30T00:00:00Z");
+ TestNext("* * * * * *", "2022-12-30T08:10:23Z", "2022-12-30T08:10:24Z");
+ TestNext("* * * * * *", "2100-01-01T15:12:42Z", "2100-01-01T15:12:43Z");
+ TestNext("* * * * * *", "2198-01-01T15:12:42Z", "2198-01-01T15:12:43Z");
+ TestNext("* * * * * *", "2199-01-01T15:12:42Z", "2199-01-01T15:12:43Z");
+ TestNext("* * * 1 * *", "2024-02-03T15:12:42Z", "2024-03-01T00:00:00Z");
+ TestNext("* * * 1 1 * *", "1970-01-01T15:12:42Z", "1970-01-01T15:12:43Z");
+ TestNext("* * * 1 1 * 1970,2100,2193,2199", "1970-01-01T15:12:42Z", "1970-01-01T15:12:43Z");
+ TestNext("* * * 1 1 * 1970,2100,2193,2199", "1971-01-01T15:12:42Z", "2100-01-01T00:00:00Z");
+ TestNext("* * * 1 1 * 1970,2100,2193,2199", "2195-01-01T15:12:42Z", "2199-01-01T00:00:00Z");
+ TestNext("* * * 1 1 * 2020", "2011-02-02T15:12:42Z", "2020-01-01T00:00:00Z");
+ }
+
+ Y_UNIT_TEST(WorkingDays) {
+ TestNext("* * * 1W * *", "2011-01-01T15:12:42Z", "2011-01-03T00:00:00Z");
+ TestNext("* * * 31W * *", "2010-01-01T15:12:42Z", "2010-01-29T00:00:00Z");
+ TestNext("* * * LW * *", "2010-01-01T15:12:42Z", "2010-01-29T00:00:00Z");
+ TestNext("* * * LW * *", "2010-02-03T15:12:42Z", "2010-02-26T00:00:00Z");
+ TestNext("* * * LW * *", "2010-03-06T15:12:42Z", "2010-03-31T00:00:00Z");
+ TestNext("* * * LW * *", "2010-04-09T15:12:42Z", "2010-04-30T00:00:00Z");
+ TestNext("* * * LW * *", "2010-05-12T15:12:42Z", "2010-05-31T00:00:00Z");
+ TestNext("* * * LW * *", "2010-06-15T15:12:42Z", "2010-06-30T00:00:00Z");
+ TestNext("* * * LW * *", "2010-07-18T15:12:42Z", "2010-07-30T00:00:00Z");
+ TestNext("* * * LW * *", "2010-08-21T15:12:42Z", "2010-08-31T00:00:00Z");
+ TestNext("* * * LW * *", "2010-09-24T15:12:42Z", "2010-09-30T00:00:00Z");
+ TestNext("* * * LW * *", "2010-10-27T15:12:42Z", "2010-10-29T00:00:00Z");
+ TestNext("* * * LW * *", "2010-11-30T15:12:42Z", "2010-11-30T15:12:43Z");
+ TestNext("* * * LW * *", "2010-12-31T15:12:42Z", "2010-12-31T15:12:43Z");
+ TestNext("* * * 15W * *", "2010-01-01T15:12:42Z", "2010-01-15T00:00:00Z");
+ TestNext("* * * 15W * *", "2010-02-03T15:12:42Z", "2010-02-15T00:00:00Z");
+ TestNext("* * * 15W * *", "2010-03-06T15:12:42Z", "2010-03-15T00:00:00Z");
+ TestNext("* * * 15W * *", "2010-04-09T15:12:42Z", "2010-04-15T00:00:00Z");
+ TestNext("* * * 15W * *", "2010-05-12T15:12:42Z", "2010-05-14T00:00:00Z");
+ TestNext("* * * 15W * *", "2010-06-15T15:12:42Z", "2010-06-15T15:12:43Z");
+ TestNext("* * * 15W * *", "2010-07-18T15:12:42Z", "2010-08-16T00:00:00Z");
+ TestNext("* * * 15W * *", "2010-08-21T15:12:42Z", "2010-09-15T00:00:00Z");
+ TestNext("* * * 15W * *", "2010-09-24T15:12:42Z", "2010-10-15T00:00:00Z");
+ TestNext("* * * 15W * *", "2010-10-27T15:12:42Z", "2010-11-15T00:00:00Z");
+ TestNext("* * * 15W * *", "2010-11-30T15:12:42Z", "2010-12-15T00:00:00Z");
+ TestNext("* * * 15W * *", "2010-12-31T15:12:42Z", "2011-01-14T00:00:00Z");
+ }
+
+ Y_UNIT_TEST(LastDaysOfMonth) {
+ TestNext("* * * L-3 * *", "2010-01-01T15:12:42Z", "2010-01-28T00:00:00Z");
+ TestNext("* * * L-1 * *", "2010-01-01T15:12:42Z", "2010-01-30T00:00:00Z");
+ TestNext("* * * L-1 * *", "2010-02-03T15:12:42Z", "2010-02-27T00:00:00Z");
+ TestNext("* * * L-1 * *", "2010-03-06T15:12:42Z", "2010-03-30T00:00:00Z");
+ TestNext("* * * L-1 * *", "2010-04-09T15:12:42Z", "2010-04-29T00:00:00Z");
+ TestNext("* * * L-1 * *", "2010-05-12T15:12:42Z", "2010-05-30T00:00:00Z");
+ TestNext("* * * L-1 * *", "2010-06-15T15:12:42Z", "2010-06-29T00:00:00Z");
+ TestNext("* * * L-1 * *", "2010-07-18T15:12:42Z", "2010-07-30T00:00:00Z");
+ TestNext("* * * L-1 * *", "2010-08-21T15:12:42Z", "2010-08-30T00:00:00Z");
+ TestNext("* * * L-1 * *", "2010-09-24T15:12:42Z", "2010-09-29T00:00:00Z");
+ TestNext("* * * L-1 * *", "2010-10-27T15:12:42Z", "2010-10-30T00:00:00Z");
+ TestNext("* * * L-1 * *", "2010-11-30T15:12:42Z", "2010-12-30T00:00:00Z");
+ TestNext("* * * L-1 * *", "2010-12-31T15:12:42Z", "2011-01-30T00:00:00Z");
+ TestNext("* * * L * *", "2010-01-01T15:12:42Z", "2010-01-31T00:00:00Z");
+ TestNext("* * * L * *", "2010-02-03T15:12:42Z", "2010-02-28T00:00:00Z");
+ TestNext("* * * L * *", "2010-03-06T15:12:42Z", "2010-03-31T00:00:00Z");
+ TestNext("* * * L * *", "2010-04-09T15:12:42Z", "2010-04-30T00:00:00Z");
+ TestNext("* * * L * *", "2010-05-12T15:12:42Z", "2010-05-31T00:00:00Z");
+ TestNext("* * * L * *", "2010-06-15T15:12:42Z", "2010-06-30T00:00:00Z");
+ TestNext("* * * L * *", "2010-07-18T15:12:42Z", "2010-07-31T00:00:00Z");
+ TestNext("* * * L * *", "2010-08-21T15:12:42Z", "2010-08-31T00:00:00Z");
+ TestNext("* * * L * *", "2010-09-24T15:12:42Z", "2010-09-30T00:00:00Z");
+ TestNext("* * * L * *", "2010-10-27T15:12:42Z", "2010-10-31T00:00:00Z");
+ TestNext("* * * L * *", "2010-11-30T15:12:42Z", "2010-11-30T15:12:43Z");
+ TestNext("* * * L * *", "2010-12-31T15:12:42Z", "2010-12-31T15:12:43Z");
+ }
+
+ Y_UNIT_TEST(Everys) {
+ TestNext("* * * * * 1#-5", "2010-09-03T15:12:42Z", "2010-11-01T00:00:00Z");
+ TestNext("* * * * * 1#-4", "2010-09-03T15:12:42Z", "2010-09-06T00:00:00Z");
+ TestNext("* * * * * 1#-3", "2010-09-03T15:12:42Z", "2010-09-13T00:00:00Z");
+ TestNext("* * * * * 1#-2", "2010-09-03T15:12:42Z", "2010-09-20T00:00:00Z");
+ TestNext("* * * * * 1#1", "2010-09-03T15:12:42Z", "2010-09-06T00:00:00Z");
+ TestNext("* * * * * 1#2", "2010-09-03T15:12:42Z", "2010-09-13T00:00:00Z");
+ TestNext("* * * * * 1#3", "2010-09-03T15:12:42Z", "2010-09-20T00:00:00Z");
+ TestNext("* * * * * 1#4", "2010-09-03T15:12:42Z", "2010-09-27T00:00:00Z");
+ TestNext("* * * * * 1#5", "2010-09-03T15:12:42Z", "2010-11-29T00:00:00Z");
+
+ TestNext("* * * * * 2#-5", "2010-09-03T15:12:42Z", "2010-11-02T00:00:00Z");
+ TestNext("* * * * * 2#-4", "2010-09-03T15:12:42Z", "2010-09-07T00:00:00Z");
+ TestNext("* * * * * 2#-3", "2010-09-03T15:12:42Z", "2010-09-14T00:00:00Z");
+ TestNext("* * * * * 2#-2", "2010-09-03T15:12:42Z", "2010-09-21T00:00:00Z");
+ TestNext("* * * * * 2#1", "2010-09-03T15:12:42Z", "2010-09-07T00:00:00Z");
+ TestNext("* * * * * 2#2", "2010-09-03T15:12:42Z", "2010-09-14T00:00:00Z");
+ TestNext("* * * * * 2#3", "2010-09-03T15:12:42Z", "2010-09-21T00:00:00Z");
+ TestNext("* * * * * 2#4", "2010-09-03T15:12:42Z", "2010-09-28T00:00:00Z");
+ TestNext("* * * * * 2#5", "2010-09-03T15:12:42Z", "2010-11-30T00:00:00Z");
+
+ TestNext("* * * * * 3#-5", "2010-09-03T15:12:42Z", "2010-12-01T00:00:00Z");
+ TestNext("* * * * * 3#-4", "2010-09-03T15:12:42Z", "2010-09-08T00:00:00Z");
+ TestNext("* * * * * 3#-3", "2010-09-03T15:12:42Z", "2010-09-15T00:00:00Z");
+ TestNext("* * * * * 3#-2", "2010-09-03T15:12:42Z", "2010-09-22T00:00:00Z");
+ TestNext("* * * * * 3#1", "2010-09-03T15:12:42Z", "2010-10-06T00:00:00Z");
+ TestNext("* * * * * 3#2", "2010-09-03T15:12:42Z", "2010-09-08T00:00:00Z");
+ TestNext("* * * * * 3#3", "2010-09-03T15:12:42Z", "2010-09-15T00:00:00Z");
+ TestNext("* * * * * 3#4", "2010-09-03T15:12:42Z", "2010-09-22T00:00:00Z");
+ TestNext("* * * * * 3#5", "2010-09-03T15:12:42Z", "2010-09-29T00:00:00Z");
+
+ TestNext("* * * * * 4#-5", "2010-09-03T15:12:42Z", "2010-12-02T00:00:00Z");
+ TestNext("* * * * * 4#-4", "2010-09-03T15:12:42Z", "2010-09-09T00:00:00Z");
+ TestNext("* * * * * 4#-3", "2010-09-03T15:12:42Z", "2010-09-16T00:00:00Z");
+ TestNext("* * * * * 4#-2", "2010-09-03T15:12:42Z", "2010-09-23T00:00:00Z");
+ TestNext("* * * * * 4#1", "2010-09-03T15:12:42Z", "2010-10-07T00:00:00Z");
+ TestNext("* * * * * 4#2", "2010-09-03T15:12:42Z", "2010-09-09T00:00:00Z");
+ TestNext("* * * * * 4#3", "2010-09-03T15:12:42Z", "2010-09-16T00:00:00Z");
+ TestNext("* * * * * 4#4", "2010-09-03T15:12:42Z", "2010-09-23T00:00:00Z");
+ TestNext("* * * * * 4#5", "2010-09-03T15:12:42Z", "2010-09-30T00:00:00Z");
+
+ TestNext("* * * * * 5#-5", "2010-09-03T15:12:42Z", "2010-10-01T00:00:00Z");
+ TestNext("* * * * * 5#-4", "2010-09-03T15:12:42Z", "2010-09-03T15:12:43Z");
+ TestNext("* * * * * 5#-3", "2010-09-03T15:12:42Z", "2010-09-10T00:00:00Z");
+ TestNext("* * * * * 5#-2", "2010-09-03T15:12:42Z", "2010-09-17T00:00:00Z");
+ TestNext("* * * * * 5#1", "2010-09-03T15:12:42Z", "2010-09-03T15:12:43Z");
+ TestNext("* * * * * 5#2", "2010-09-03T15:12:42Z", "2010-09-10T00:00:00Z");
+ TestNext("* * * * * 5#3", "2010-09-03T15:12:42Z", "2010-09-17T00:00:00Z");
+ TestNext("* * * * * 5#4", "2010-09-03T15:12:42Z", "2010-09-24T00:00:00Z");
+ TestNext("* * * * * 5#5", "2010-09-03T15:12:42Z", "2010-10-29T00:00:00Z");
+
+ TestNext("* * * * * 6#-5", "2010-09-03T15:12:42Z", "2010-10-02T00:00:00Z");
+ TestNext("* * * * * 6#-4", "2010-09-03T15:12:42Z", "2010-09-04T00:00:00Z");
+ TestNext("* * * * * 6#-3", "2010-09-03T15:12:42Z", "2010-09-11T00:00:00Z");
+ TestNext("* * * * * 6#-2", "2010-09-03T15:12:42Z", "2010-09-18T00:00:00Z");
+ TestNext("* * * * * 6#1", "2010-09-03T15:12:42Z", "2010-09-04T00:00:00Z");
+ TestNext("* * * * * 6#2", "2010-09-03T15:12:42Z", "2010-09-11T00:00:00Z");
+ TestNext("* * * * * 6#3", "2010-09-03T15:12:42Z", "2010-09-18T00:00:00Z");
+ TestNext("* * * * * 6#4", "2010-09-03T15:12:42Z", "2010-09-25T00:00:00Z");
+ TestNext("* * * * * 6#5", "2010-09-03T15:12:42Z", "2010-10-30T00:00:00Z");
+
+ TestNext("* * * * * 7#-5", "2010-09-03T15:12:42Z", "2010-10-03T00:00:00Z");
+ TestNext("* * * * * 7#-4", "2010-09-03T15:12:42Z", "2010-09-05T00:00:00Z");
+ TestNext("* * * * * 7#-3", "2010-09-03T15:12:42Z", "2010-09-12T00:00:00Z");
+ TestNext("* * * * * 7#-2", "2010-09-03T15:12:42Z", "2010-09-19T00:00:00Z");
+ TestNext("* * * * * 7#1", "2010-09-03T15:12:42Z", "2010-09-05T00:00:00Z");
+ TestNext("* * * * * 7#2", "2010-09-03T15:12:42Z", "2010-09-12T00:00:00Z");
+ TestNext("* * * * * 7#3", "2010-09-03T15:12:42Z", "2010-09-19T00:00:00Z");
+ TestNext("* * * * * 7#4", "2010-09-03T15:12:42Z", "2010-09-26T00:00:00Z");
+ TestNext("* * * * * 7#5", "2010-09-03T15:12:42Z", "2010-10-31T00:00:00Z");
+ }
+
+ Y_UNIT_TEST(LastWeekdays) {
+ TestNext("* * * * * 1L", "2010-09-30T15:12:42Z", "2010-10-25T00:00:00Z");
+ TestNext("* * * * * 2L", "2010-09-30T15:12:42Z", "2010-10-26T00:00:00Z");
+ TestNext("* * * * * 3L", "2010-09-30T15:12:42Z", "2010-10-27T00:00:00Z");
+ TestNext("* * * * * 4L", "2010-09-30T15:12:42Z", "2010-09-30T15:12:43Z");
+ TestNext("* * * * * 5L", "2010-09-30T15:12:42Z", "2010-10-29T00:00:00Z");
+ TestNext("* * * * * 6L", "2010-09-30T15:12:42Z", "2010-10-30T00:00:00Z");
+ TestNext("* * * * * 7L", "2010-09-30T15:12:42Z", "2010-10-31T00:00:00Z");
+ TestNext("* * * * * 1L", "2010-10-27T15:12:42Z", "2010-11-29T00:00:00Z");
+ TestNext("* * * * * 2L", "2010-10-27T15:12:42Z", "2010-11-30T00:00:00Z");
+ TestNext("* * * * * 3L", "2010-10-27T15:12:42Z", "2010-10-27T15:12:43Z");
+ TestNext("* * * * * 4L", "2010-10-27T15:12:42Z", "2010-10-28T00:00:00Z");
+ TestNext("* * * * * 5L", "2010-10-27T15:12:42Z", "2010-10-29T00:00:00Z");
+ TestNext("* * * * * 6L", "2010-10-27T15:12:42Z", "2010-10-30T00:00:00Z");
+ TestNext("* * * * * 7L", "2010-10-27T15:12:42Z", "2010-10-31T00:00:00Z");
+
+ TestNext("* * * * * 1L", "2010-10-30T15:12:42Z", "2010-11-29T00:00:00Z");
+ TestNext("* * * * * 2L", "2010-10-30T15:12:42Z", "2010-11-30T00:00:00Z");
+ TestNext("* * * * * 3L", "2010-10-30T15:12:42Z", "2010-11-24T00:00:00Z");
+ TestNext("* * * * * 4L", "2010-10-30T15:12:42Z", "2010-11-25T00:00:00Z");
+ TestNext("* * * * * 5L", "2010-10-30T15:12:42Z", "2010-11-26T00:00:00Z");
+ TestNext("* * * * * 6L", "2010-10-30T15:12:42Z", "2010-10-30T15:12:43Z");
+ TestNext("* * * * * 7L", "2010-10-30T15:12:42Z", "2010-10-31T00:00:00Z");
+ TestNext("* * * * * 1L", "2010-11-27T15:12:42Z", "2010-11-29T00:00:00Z");
+ TestNext("* * * * * 2L", "2010-11-27T15:12:42Z", "2010-11-30T00:00:00Z");
+ TestNext("* * * * * 3L", "2010-11-27T15:12:42Z", "2010-12-29T00:00:00Z");
+ TestNext("* * * * * 4L", "2010-11-27T15:12:42Z", "2010-12-30T00:00:00Z");
+ TestNext("* * * * * 5L", "2010-11-27T15:12:42Z", "2010-12-31T00:00:00Z");
+ TestNext("* * * * * 6L", "2010-11-27T15:12:42Z", "2010-11-27T15:12:43Z");
+ TestNext("* * * * * 7L", "2010-11-27T15:12:42Z", "2010-11-28T00:00:00Z");
+ }
+
+ Y_UNIT_TEST(ComplicatedCases) {
+ TestNext("10-45/3 30-50/7 10,12,13,14,22,23 */2 * 1-5", "2024-04-23T09:42:45Z", "2024-04-23T10:30:10Z");
+ TestNext("10-45/3 30-50/7 10,12,13,14,22,23 */2 * 1-5", "2024-04-22T09:42:45Z", "2024-04-23T10:30:10Z");
+ TestNext("10-45/3 30-50/7 10,12,13,14,22,23 */2 * 1-5", "2024-04-20T04:30:13Z", "2024-04-23T10:30:10Z");
+ TestNext("10-45/3 30-50/7 10,12,13,14,22,23 */2 * 1-5", "2024-04-23T11:32:00Z", "2024-04-23T12:30:10Z");
+ TestNext("10-45/3 30-50/7 10,12,13,14,22,23 */2 * 1-5", "2024-04-23T12:32:17Z", "2024-04-23T12:37:10Z");
+ TestNext("10-45/3 30-50/7 10,12,13,14,22,23 30 * 1#5", "2024-04-23T12:32:17Z", "2024-09-30T10:30:10Z");
+ TestNext("10-45/3 30-50/7 10,12,13,14,22,23 L * 1", "2024-04-23T10:31:11Z", "2024-09-30T10:30:10Z");
+ TestNext("* * 10 L * 5", "2024-04-23T10:31:11Z", "2024-05-31T10:00:00Z");
+ TestNext("* * 10 L-1 * 4", "2024-04-23T10:31:11Z", "2024-05-30T10:00:00Z");
+ TestNext("* * 10 L-2 * 3", "2024-04-23T10:31:11Z", "2024-05-29T10:00:00Z");
+ TestNext("* * 10 L-3 * 2", "2024-04-23T10:31:11Z", "2024-05-28T10:00:00Z");
+ TestNext("* * 10 L-4 * 1", "2024-04-23T10:31:11Z", "2024-05-27T10:00:00Z");
+ TestNext("* * 10 L-5 * 0", "2024-04-23T10:31:11Z", "2024-05-26T10:00:00Z");
+ TestNext("* * 10 L-6 * 6", "2024-04-23T10:31:11Z", "2024-05-25T10:00:00Z");
+ }
+
+ Y_UNIT_TEST(DifferentCases) {
+ TestNext("10-45/3 * * * * *", "2012-12-01T09:42:19Z", "2012-12-01T09:42:22Z");
+ TestNext("10-45/3 * * * * *", "2012-12-01T09:42:45Z", "2012-12-01T09:43:10Z");
+ TestNext("* * 19/2 * * *", "2012-12-01T09:42:45Z", "2012-12-01T19:00:00Z");
+ TestNext("* * 19/2 * * *", "2012-12-01T19:42:45Z", "2012-12-01T19:42:46Z");
+ TestNext("37 * 19/2 * * *", "2012-12-01T19:42:45Z", "2012-12-01T19:43:37Z");
+ TestNext("37 * 19/2 * * *", "2012-12-01T19:59:45Z", "2012-12-01T21:00:37Z");
+ TestNext("37 * 19/2 * * *", "2012-12-01T19:59:37Z", "2012-12-01T21:00:37Z");
+ TestNext("37 * 18/2 * * *", "2012-12-01T23:00:00Z", "2012-12-02T18:00:37Z");
+ TestNext("37 * 18/2 * * *", "2012-12-01T22:59:37Z", "2012-12-02T18:00:37Z");
+ TestNext("0 0 7 W * *", "2009-09-26T00:42:55Z", "2009-09-28T07:00:00Z");
+ TestNext("0 0 7 W * *", "2009-09-28T07:00:00Z", "2009-09-29T07:00:00Z");
+ TestNext("* * * * * L", "2010-10-25T15:12:42Z", "2010-10-31T00:00:00Z");
+ TestNext("* * * * * L", "2010-10-20T15:12:42Z", "2010-10-24T00:00:00Z");
+ TestNext("* * * * * L", "2010-10-27T15:12:42Z", "2010-10-31T00:00:00Z");
+ TestNext("* 15 11 * * *", "2019-03-09T11:43:00Z", "2019-03-10T11:15:00Z");
+ TestNext("*/15 * 1-4 * * *", "2012-07-01T09:53:50Z", "2012-07-02T01:00:00Z");
+ TestNext("*/15 * 1-4 * * *", "2012-07-01T09:53:00Z", "2012-07-02T01:00:00Z");
+ TestNext("3,15,30,45 * 1,2,3,4 * * *", "2012-07-01T01:53:00Z", "2012-07-01T01:53:03Z");
+ TestNext("3,15,30,45 10 * * * *", "2012-07-01T09:53:10Z", "2012-07-01T10:10:03Z");
+ TestNext("3,15,30,45 * 1,2,3,4 * * *", "2012-07-01T09:53:10Z", "2012-07-02T01:00:03Z");
+ TestNext("10,20 1,2,3,4 1,2,3,6 * *", "2012-07-01T09:15:00Z", "2012-07-02T01:10:00Z");
+ TestNext("0 */2 1-4 * * *", "2012-07-01T09:00:00Z", "2012-07-02T01:00:00Z");
+ TestNext("0 */2 * * * *", "2012-07-01T09:00:00Z", "2012-07-01T09:02:00Z");
+ TestNext("0 */2 * * * *", "2013-07-01T09:00:00Z", "2013-07-01T09:02:00Z");
+ TestNext("0 */2 * * * *", "2018-09-14T14:24:00Z", "2018-09-14T14:26:00Z");
+ TestNext("0 */2 * * * *", "2018-09-14T14:25:00Z", "2018-09-14T14:26:00Z");
+ TestNext("0 */20 * * * *", "2018-09-14T14:24:00Z", "2018-09-14T14:40:00Z");
+ TestNext("* * * * * *", "2012-07-01T09:00:00Z", "2012-07-01T09:00:01Z");
+ TestNext("* * * * * *", "2012-12-01T09:00:58Z", "2012-12-01T09:00:59Z");
+ TestNext("10 * * * * *", "2012-12-01T09:42:09Z", "2012-12-01T09:42:10Z");
+ TestNext("11 * * * * *", "2012-12-01T09:42:10Z", "2012-12-01T09:42:11Z");
+ TestNext("10 * * * * *", "2012-12-01T09:42:10Z", "2012-12-01T09:43:10Z");
+ TestNext("10-15 * * * * *", "2012-12-01T09:42:09Z", "2012-12-01T09:42:10Z");
+ TestNext("10-15 * * * * *", "2012-12-01T21:42:14Z", "2012-12-01T21:42:15Z");
+ TestNext("0 * * * * *", "2012-12-01T21:10:42Z", "2012-12-01T21:11:00Z");
+ TestNext("0 * * * * *", "2012-12-01T21:11:00Z", "2012-12-01T21:12:00Z");
+ TestNext("0 11 * * * *", "2012-12-01T21:10:42Z", "2012-12-01T21:11:00Z");
+ TestNext("0 10 * * * *", "2012-12-01T21:11:00Z", "2012-12-01T22:10:00Z");
+ TestNext("0 0 * * * *", "2012-09-30T11:01:00Z", "2012-09-30T12:00:00Z");
+ TestNext("0 0 * * * *", "2012-09-30T12:00:00Z", "2012-09-30T13:00:00Z");
+ TestNext("0 0 * * * *", "2012-09-10T23:01:00Z", "2012-09-11T00:00:00Z");
+ TestNext("0 0 * * * *", "2012-09-11T00:00:00Z", "2012-09-11T01:00:00Z");
+ TestNext("0 0 0 * * *", "2012-09-01T14:42:43Z", "2012-09-02T00:00:00Z");
+ TestNext("0 0 0 * * *", "2012-09-02T00:00:00Z", "2012-09-03T00:00:00Z");
+ TestNext("* * * 10 * *", "2012-10-09T15:12:42Z", "2012-10-10T00:00:00Z");
+ TestNext("* * * 10 * *", "2012-10-11T15:12:42Z", "2012-11-10T00:00:00Z");
+ TestNext("0 0 0 * * *", "2012-09-30T15:12:42Z", "2012-10-01T00:00:00Z");
+ TestNext("0 0 0 * * *", "2012-10-01T00:00:00Z", "2012-10-02T00:00:00Z");
+ TestNext("0 0 0 * * *", "2012-08-30T15:12:42Z", "2012-08-31T00:00:00Z");
+ TestNext("0 0 0 * * *", "2012-08-31T00:00:00Z", "2012-09-01T00:00:00Z");
+ TestNext("0 0 0 * * *", "2012-10-30T15:12:42Z", "2012-10-31T00:00:00Z");
+ TestNext("0 0 0 * * *", "2012-10-31T00:00:00Z", "2012-11-01T00:00:00Z");
+ TestNext("0 0 0 1 * *", "2012-10-30T15:12:42Z", "2012-11-01T00:00:00Z");
+ TestNext("0 0 0 1 * *", "2012-11-01T00:00:00Z", "2012-12-01T00:00:00Z");
+ TestNext("0 0 0 1 * *", "2010-12-31T15:12:42Z", "2011-01-01T00:00:00Z");
+ TestNext("0 0 0 1 * *", "2011-01-01T00:00:00Z", "2011-02-01T00:00:00Z");
+ TestNext("0 0 0 31 * *", "2011-10-30T15:12:42Z", "2011-10-31T00:00:00Z");
+ TestNext("0 0 0 1 * *", "2011-10-30T15:12:42Z", "2011-11-01T00:00:00Z");
+ TestNext("* * * * * 2", "2010-10-25T15:12:42Z", "2010-10-26T00:00:00Z");
+ TestNext("* * * * * 2", "2010-10-20T15:12:42Z", "2010-10-26T00:00:00Z");
+ TestNext("* * * * * 2", "2010-10-27T15:12:42Z", "2010-11-02T00:00:00Z");
+ TestNext("55 5 * * * *", "2010-10-27T15:04:54Z", "2010-10-27T15:05:55Z");
+ TestNext("55 5 * * * *", "2010-10-27T15:05:55Z", "2010-10-27T16:05:55Z");
+ TestNext("55 * 10 * * *", "2010-10-27T09:04:54Z", "2010-10-27T10:00:55Z");
+ TestNext("55 * 10 * * *", "2010-10-27T10:00:55Z", "2010-10-27T10:01:55Z");
+ TestNext("* 5 10 * * *", "2010-10-27T09:04:55Z", "2010-10-27T10:05:00Z");
+ TestNext("* 5 10 * * *", "2010-10-27T10:05:00Z", "2010-10-27T10:05:01Z");
+ TestNext("55 * * 3 * *", "2010-10-02T10:05:54Z", "2010-10-03T00:00:55Z");
+ TestNext("55 * * 3 * *", "2010-10-03T00:00:55Z", "2010-10-03T00:01:55Z");
+ TestNext("* * * 3 11 *", "2010-10-02T14:42:55Z", "2010-11-03T00:00:00Z");
+ TestNext("* * * 3 11 *", "2010-11-03T00:00:00Z", "2010-11-03T00:00:01Z");
+ TestNext("0 0 0 29 2 *", "2007-02-10T14:42:55Z", "2008-02-29T00:00:00Z");
+ TestNext("0 0 0 29 2 *", "2008-02-29T00:00:00Z", "2012-02-29T00:00:00Z");
+ TestNext("0 0 7 ? * MON-FRI", "2009-09-26T00:42:55Z", "2009-09-28T07:00:00Z");
+ TestNext("0 0 7 ? * MON-FRI", "2009-09-28T07:00:00Z", "2009-09-29T07:00:00Z");
+ TestNext("0 0 7 ? * 6-0", "2024-05-02T15:37:04Z", "2024-05-04T07:00:00Z");
+ TestNext("0 30 23 30 1/3 ?", "2010-12-30T00:00:00Z", "2011-01-30T23:30:00Z");
+ TestNext("0 30 23 30 1/3 ?", "2011-01-30T23:30:00Z", "2011-04-30T23:30:00Z");
+ TestNext("0 30 23 30 1/3 ?", "2011-04-30T23:30:00Z", "2011-07-30T23:30:00Z");
+ TestNext("* * * * * *", "2020-12-31T23:59:59Z", "2021-01-01T00:00:00Z");
+ TestNext("0 0 * * * *", "2020-02-28T23:00:00Z", "2020-02-29T00:00:00Z");
+ TestNext("0 0 0 * * *", "2020-02-29T01:02:03Z", "2020-03-01T00:00:00Z");
+ TestNext("0 0 0 ? 11-12 *", "2022-05-31T00:00:00Z", "2022-11-01T00:00:00Z");
+ TestNext("0 0 0 ? 11-12 *", "2022-07-31T00:00:00Z", "2022-11-01T00:00:00Z");
+ TestNext("0 0 0 ? 11-12 *", "2022-08-31T00:00:00Z", "2022-11-01T00:00:00Z");
+ TestNext("0 0 0 ? 11-12 *", "2022-10-31T00:00:00Z", "2022-11-01T00:00:00Z");
+ TestNext("0 0 0 ? 6-7 *", "2022-05-31T00:00:00Z", "2022-06-01T00:00:00Z");
+ TestNext("0 0 0 ? 8-9 *", "2022-07-31T00:00:00Z", "2022-08-01T00:00:00Z");
+ TestNext("0 0 0 ? 9-10 *", "2022-08-31T00:00:00Z", "2022-09-01T00:00:00Z");
+ TestNext("0 0 0 ? 2-3 *", "2022-01-31T00:00:00Z", "2022-02-01T00:00:00Z");
+ TestNext("0 0 0 ? 4-5 *", "2022-03-31T00:00:00Z", "2022-04-01T00:00:00Z");
+ TestNext("* * * 29 2 *", "2021-12-07T12:00:00Z", "2024-02-29T00:00:00Z");
+ }
+
+ Y_UNIT_TEST(TypedDates) {
+ TestNext("@annually", "2021-12-07T12:00:00Z", "2022-01-01T00:00:00Z");
+ TestNext("@yearly", "2021-12-07T12:00:00Z", "2022-01-01T00:00:00Z");
+ TestNext("@monthly", "2021-11-07T12:23:00Z", "2021-12-01T00:00:00Z");
+ TestNext("@weekly", "2024-04-22T16:05:32Z", "2024-04-28T00:00:00Z");
+ TestNext("@daily", "2024-04-22T16:05:32Z", "2024-04-23T00:00:00Z");
+ TestNext("@hourly", "2021-12-07T12:20:07Z", "2021-12-07T13:00:00Z");
+ TestNext("@minutely", "2024-04-22T16:05:32Z", "2024-04-22T16:06:00Z");
+ TestNext("@secondly", "2021-12-07T12:00:00Z", "2021-12-07T12:00:01Z");
+ }
+}
+
+Y_UNIT_TEST_SUITE(TestPrev) {
+ Y_UNIT_TEST(DifferentCases) {
+ TestPrev("* 15 11 * * *", "2019-03-09T11:43:00Z", "2019-03-09T11:15:59Z");
+ TestPrev("*/15 * 1-4 * * *", "2012-07-01T09:53:50Z", "2012-07-01T04:59:45Z");
+ TestPrev("*/15 * 1-4 * * *", "2012-07-01T01:00:14Z", "2012-07-01T01:00:00Z");
+ TestPrev("*/15 * 1-4 * * *", "2012-07-01T01:00:00Z", "2012-06-30T04:59:45Z");
+ TestPrev("* * * * * *", "2012-07-01T09:00:00Z", "2012-07-01T08:59:59Z");
+ TestPrev("* * * * * *", "2021-01-01T00:00:00Z", "2020-12-31T23:59:59Z");
+ TestPrev("0 0 * * * *", "2020-02-29T00:00:00Z", "2020-02-28T23:00:00Z");
+ TestPrev("0 0 0 * * *", "2020-03-01T00:00:00Z", "2020-02-29T00:00:00Z");
+ TestPrev("0 0 * * * *", "2020-03-01T00:00:00Z", "2020-02-29T23:00:00Z");
+
+ TestPrev("* * * 29 2 *", "2021-12-07T12:00:00Z", "2020-02-29T23:59:59Z");
+
+ TestPrev("* * * 1 2 *", "2023-11-01T00:00:00Z", "2023-02-01T23:59:59Z");
+ TestPrev("* * * 2 2 *", "2023-11-01T00:00:00Z", "2023-02-02T23:59:59Z");
+ TestPrev("* * * 3 2 *", "2023-11-01T00:00:00Z", "2023-02-03T23:59:59Z");
+ TestPrev("* * * 4 2 *", "2023-11-01T00:00:00Z", "2023-02-04T23:59:59Z");
+
+ TestPrev("* * * 1 4 *", "2023-10-01T00:00:00Z", "2023-04-01T23:59:59Z");
+ TestPrev("* * * 2 4 *", "2023-10-01T00:00:00Z", "2023-04-02T23:59:59Z");
+ TestPrev("* * * 3 4 *", "2023-10-01T00:00:00Z", "2023-04-03T23:59:59Z");
+ TestPrev("* * * 4 4 *", "2023-10-01T00:00:00Z", "2023-04-04T23:59:59Z");
+
+ TestPrev("0 0 20 1 2 *", "2022-12-30T08:10:23Z", "2022-02-01T20:00:00Z");
+ TestPrev("0 0 20 2 2 *", "2022-12-30T08:10:23Z", "2022-02-02T20:00:00Z");
+ TestPrev("0 0 20 3 2 *", "2022-12-30T08:10:23Z", "2022-02-03T20:00:00Z");
+ TestPrev("0 0 20 1 2 *", "2022-12-31T08:10:23Z", "2022-02-01T20:00:00Z");
+ TestPrev("0 0 20 2 2 *", "2022-12-31T08:10:23Z", "2022-02-02T20:00:00Z");
+ TestPrev("0 0 20 3 2 *", "2022-12-31T08:10:23Z", "2022-02-03T20:00:00Z");
+ TestPrev("0 0 20 1 2 *", "2023-01-01T08:10:23Z", "2022-02-01T20:00:00Z");
+ TestPrev("0 0 20 2 2 *", "2023-01-01T08:10:23Z", "2022-02-02T20:00:00Z");
+ TestPrev("0 0 20 3 2 *", "2023-01-01T08:10:23Z", "2022-02-03T20:00:00Z");
+
+ TestPrev("0 0 17 * 2 2-4", "2023-08-31T18:00:00Z", "2023-02-28T17:00:00Z");
+ TestPrev("0 0 17 * 2 2-4", "2023-09-01T18:00:00Z", "2023-02-28T17:00:00Z");
+ TestPrev("0 0 17 * 2 2-4", "2023-09-02T18:00:00Z", "2023-02-28T17:00:00Z");
+ TestPrev("0 0 17 * 2 2-4", "2023-09-03T18:00:00Z", "2023-02-28T17:00:00Z");
+ TestPrev("0 0 17 * 2 2-4", "2023-09-04T18:00:00Z", "2023-02-28T17:00:00Z");
+ TestPrev("0 0 17 * 2 2-4", "2023-09-05T18:00:00Z", "2023-02-28T17:00:00Z");
+
+ TestPrev("0 0 17 * 3 1-5", "2023-03-02T17:00:00Z", "2023-03-01T17:00:00Z");
+ TestPrev("0 0 17 * 3 1-5", "2023-03-02T17:00:01Z", "2023-03-02T17:00:00Z");
+ TestPrev("0 0 17 * 3 1-5", "2023-03-02T18:00:00Z", "2023-03-02T17:00:00Z");
+ TestPrev("0 0 17 * 3 1-5", "2023-03-03T18:00:00Z", "2023-03-03T17:00:00Z");
+ TestPrev("0 0 17 * 3 1-5", "2023-03-04T18:00:00Z", "2023-03-03T17:00:00Z");
+ TestPrev("0 0 17 * 3 1-5", "2023-03-05T18:00:00Z", "2023-03-03T17:00:00Z");
+ TestPrev("0 0 17 * 3 1-5", "2023-03-06T18:00:00Z", "2023-03-06T17:00:00Z");
+
+ TestPrev("0 30 9 * 4 6", "2024-04-05T18:00:00Z", "2023-04-29T09:30:00Z");
+ TestPrev("0 30 9 * 4 6", "2024-04-06T09:29:59Z", "2023-04-29T09:30:00Z");
+ TestPrev("0 30 9 * 4 6", "2024-04-06T09:30:00Z", "2023-04-29T09:30:00Z");
+ TestPrev("0 30 9 * 4 6", "2024-04-06T09:30:01Z", "2024-04-06T09:30:00Z");
+ TestPrev("0 30 9 * 4 6", "2024-04-06T18:00:00Z", "2024-04-06T09:30:00Z");
+ TestPrev("0 30 9 * 4 6", "2024-04-07T18:00:00Z", "2024-04-06T09:30:00Z");
+ TestPrev("0 30 9 * 4 6", "2024-04-26T18:00:00Z", "2024-04-20T09:30:00Z");
+ TestPrev("0 30 9 * 4 6", "2024-04-27T18:00:00Z", "2024-04-27T09:30:00Z");
+ TestPrev("0 30 9 * 4 6", "2024-04-28T18:00:00Z", "2024-04-27T09:30:00Z");
+ TestPrev("0 30 9 * 4 6", "2024-05-01T18:00:00Z", "2024-04-27T09:30:00Z");
+
+ TestPrev("0 30 11 * * 6", "2020-02-27T10:00:00Z", "2020-02-22T11:30:00Z");
+ TestPrev("0 30 11 * * 6", "2020-02-28T10:00:00Z", "2020-02-22T11:30:00Z");
+ TestPrev("0 30 11 * * 6", "2020-02-29T10:00:00Z", "2020-02-22T11:30:00Z");
+ TestPrev("0 30 11 * * 6", "2020-02-29T11:31:00Z", "2020-02-29T11:30:00Z");
+ TestPrev("0 30 11 * * 6", "2020-03-01T10:00:00Z", "2020-02-29T11:30:00Z");
+ TestPrev("0 30 11 * * 6", "2020-03-01T12:00:00Z", "2020-02-29T11:30:00Z");
+
+ TestPrev("0 0 10 * * *", "2020-02-29T09:59:59Z", "2020-02-28T10:00:00Z");
+ TestPrev("0 0 10 * * *", "2020-02-29T10:00:00Z", "2020-02-28T10:00:00Z");
+ TestPrev("0 0 10 * * *", "2020-02-29T10:00:01Z", "2020-02-29T10:00:00Z");
+ TestPrev("0 0 10 * * *", "2022-02-28T09:59:59Z", "2022-02-27T10:00:00Z");
+ TestPrev("0 0 10 * * *", "2022-02-28T10:00:00Z", "2022-02-27T10:00:00Z");
+ TestPrev("0 0 10 * * *", "2022-02-28T10:00:01Z", "2022-02-28T10:00:00Z");
+ TestPrev("0 0 10 * * *", "2022-12-31T09:59:59Z", "2022-12-30T10:00:00Z");
+ TestPrev("0 0 10 * * *", "2022-12-31T10:00:00Z", "2022-12-30T10:00:00Z");
+ TestPrev("0 0 10 * * *", "2022-12-31T10:00:01Z", "2022-12-31T10:00:00Z");
+ TestPrev("0 0 10 * * *", "2022-12-31T23:59:59Z", "2022-12-31T10:00:00Z");
+ TestPrev("0 0 10 * * *", "2023-01-01T00:00:00Z", "2022-12-31T10:00:00Z");
+ TestPrev("0 0 10 * * *", "2023-01-01T00:00:01Z", "2022-12-31T10:00:00Z");
+ TestPrev("0 0 10 * * *", "2023-01-01T09:59:59Z", "2022-12-31T10:00:00Z");
+ TestPrev("0 0 10 * * *", "2023-01-01T10:00:00Z", "2022-12-31T10:00:00Z");
+ TestPrev("0 0 10 * * *", "2023-01-01T10:00:01Z", "2023-01-01T10:00:00Z");
+
+ TestPrev("30 50 23 20,21,22 * *", "2023-07-01T12:34:56Z", "2023-06-22T23:50:30Z");
+ TestPrev("30 50 23 20,21,22 * *", "2023-07-19T12:34:56Z", "2023-06-22T23:50:30Z");
+ TestPrev("30 50 23 20,21,22 * *", "2023-07-20T12:34:56Z", "2023-06-22T23:50:30Z");
+ TestPrev("30 50 23 20,21,22 * *", "2023-07-21T12:34:56Z", "2023-07-20T23:50:30Z");
+ TestPrev("30 50 23 20,21,22 * *", "2023-07-22T12:34:56Z", "2023-07-21T23:50:30Z");
+ TestPrev("30 50 23 20,21,22 * *", "2023-07-23T12:34:56Z", "2023-07-22T23:50:30Z");
+ }
+}
+
+Y_UNIT_TEST_SUITE(AliceBasicUsageExamples) {
+ Y_UNIT_TEST(First) {
+ TCronExpression cronConverter("*/10 * * * *");
+
+ TestNextCron(&cronConverter, "2023-07-20T12:34:56Z", "2023-07-20T12:40:00Z");
+ TestNextCron(&cronConverter, "2023-07-20T12:40:00Z", "2023-07-20T12:50:00Z");
+ TestNextCron(&cronConverter, "2023-07-20T23:50:00Z", "2023-07-21T00:00:00Z");
+
+ TestPrevCron(&cronConverter, "2023-07-20T23:50:00Z", "2023-07-20T23:40:00Z");
+ TestPrevCron(&cronConverter, "2023-07-20T00:00:01Z", "2023-07-20T00:00:00Z");
+ TestPrevCron(&cronConverter, "2023-07-20T00:00:00Z", "2023-07-19T23:50:00Z");
+ }
+
+ Y_UNIT_TEST(Second) {
+ TCronExpression cronConverter("0 6 * * 7");
+
+ TestNextCron(&cronConverter, "2024-04-22T16:05:32Z", "2024-04-28T06:00:00Z");
+ TestNextCron(&cronConverter, "2024-04-21T16:05:32Z", "2024-04-28T06:00:00Z");
+ TestNextCron(&cronConverter, "2024-04-21T01:01:32Z", "2024-04-21T06:00:00Z");
+
+ TestPrevCron(&cronConverter, "2024-04-30T23:50:00Z", "2024-04-28T06:00:00Z");
+ TestPrevCron(&cronConverter, "2024-04-28T06:00:00Z", "2024-04-21T06:00:00Z");
+ TestPrevCron(&cronConverter, "2024-04-21T06:00:01Z", "2024-04-21T06:00:00Z");
+ }
+
+ Y_UNIT_TEST(Third) {
+ TCronExpression cronConverter("0 0 1 1 *");
+
+ TestNextCron(&cronConverter, "2024-04-22T16:05:32Z", "2025-01-01T00:00:00Z");
+ TestNextCron(&cronConverter, "2024-01-01T00:00:00Z", "2025-01-01T00:00:00Z");
+ TestNextCron(&cronConverter, "2024-12-31T23:59:59Z", "2025-01-01T00:00:00Z");
+
+ TestPrevCron(&cronConverter, "2025-04-22T16:05:32Z", "2025-01-01T00:00:00Z");
+ TestPrevCron(&cronConverter, "2025-01-01T00:00:01Z", "2025-01-01T00:00:00Z");
+ TestPrevCron(&cronConverter, "2026-01-01T00:00:00Z", "2025-01-01T00:00:00Z");
+ }
+
+ Y_UNIT_TEST(Fourth) {
+ TCronExpression cronConverter("30 15 * * 1-5");
+
+ TestNextCron(&cronConverter, "2024-04-22T16:05:32Z", "2024-04-23T15:30:00Z");
+ TestNextCron(&cronConverter, "2024-04-20T16:05:32Z", "2024-04-22T15:30:00Z");
+ TestNextCron(&cronConverter, "2024-03-30T11:05:32Z", "2024-04-01T15:30:00Z");
+
+ TestPrevCron(&cronConverter, "2024-04-23T16:05:32Z", "2024-04-23T15:30:00Z");
+ TestPrevCron(&cronConverter, "2024-04-28T16:05:32Z", "2024-04-26T15:30:00Z");
+ TestPrevCron(&cronConverter, "2024-04-02T11:05:32Z", "2024-04-01T15:30:00Z");
+ }
+
+ Y_UNIT_TEST(Fifth) {
+ TCronExpression cronConverter("0 22 * * *");
+
+ TestNextCron(&cronConverter, "2024-04-20T16:05:32Z", "2024-04-20T22:00:00Z");
+ TestNextCron(&cronConverter, "2024-12-31T22:00:00Z", "2025-01-01T22:00:00Z");
+ TestNextCron(&cronConverter, "2024-04-20T00:05:32Z", "2024-04-20T22:00:00Z");
+
+ TestPrevCron(&cronConverter, "2024-04-21T16:05:32Z", "2024-04-20T22:00:00Z");
+ TestPrevCron(&cronConverter, "2025-01-01T22:00:00Z", "2024-12-31T22:00:00Z");
+ TestPrevCron(&cronConverter, "2024-04-20T23:05:32Z", "2024-04-20T22:00:00Z");
+ }
+
+ Y_UNIT_TEST(Sixth) {
+ TCronExpression cronConverter("0 12 L * *");
+
+ TestNextCron(&cronConverter, "2024-04-20T16:05:32Z", "2024-04-30T12:00:00Z");
+ TestNextCron(&cronConverter, "2024-02-20T12:05:43Z", "2024-02-29T12:00:00Z");
+ TestNextCron(&cronConverter, "2021-02-01T22:05:32Z", "2021-02-28T12:00:00Z");
+
+ TestPrevCron(&cronConverter, "2024-05-20T16:05:32Z", "2024-04-30T12:00:00Z");
+ TestPrevCron(&cronConverter, "2024-03-20T12:05:43Z", "2024-02-29T12:00:00Z");
+ TestPrevCron(&cronConverter, "2021-03-01T22:05:32Z", "2021-02-28T12:00:00Z");
+ }
+
+ Y_UNIT_TEST(Seventh) {
+ TCronExpression cronConverter("0 8 * * 1-5");
+
+ TestNextCron(&cronConverter, "2024-04-22T07:59:59Z", "2024-04-22T08:00:00Z");
+ TestNextCron(&cronConverter, "2024-04-20T07:59:59Z", "2024-04-22T08:00:00Z");
+ TestNextCron(&cronConverter, "2024-04-22T14:29:54Z", "2024-04-23T08:00:00Z");
+
+ TestPrevCron(&cronConverter, "2024-04-22T08:00:01Z", "2024-04-22T08:00:00Z");
+ TestPrevCron(&cronConverter, "2024-04-23T07:59:59Z", "2024-04-22T08:00:00Z");
+ TestPrevCron(&cronConverter, "2024-04-22T06:29:54Z", "2024-04-19T08:00:00Z");
+ }
+
+ Y_UNIT_TEST(Eigths) {
+ TCronExpression cronConverter("0 */2 * * 7");
+
+ TestNextCron(&cronConverter, "2024-04-21T07:59:59Z", "2024-04-21T08:00:00Z");
+ TestNextCron(&cronConverter, "2024-04-21T14:59:59Z", "2024-04-21T16:00:00Z");
+ TestNextCron(&cronConverter, "2024-04-16T21:03:25Z", "2024-04-21T00:00:00Z");
+ }
+
+ Y_UNIT_TEST(Ninth) {
+ TCronExpression cronConverter("45 21 5 * *");
+
+ TestNextCron(&cronConverter, "2024-04-22T16:05:32Z", "2024-05-05T21:45:00Z");
+ TestNextCron(&cronConverter, "2024-04-05T22:05:32Z", "2024-05-05T21:45:00Z");
+ TestNextCron(&cronConverter, "2024-04-05T16:05:32Z", "2024-04-05T21:45:00Z");
+
+ TestPrevCron(&cronConverter, "2024-05-22T16:05:32Z", "2024-05-05T21:45:00Z");
+ TestPrevCron(&cronConverter, "2024-05-05T22:05:32Z", "2024-05-05T21:45:00Z");
+ TestPrevCron(&cronConverter, "2024-05-05T16:05:32Z", "2024-04-05T21:45:00Z");
+ }
+
+ Y_UNIT_TEST(Tenth) {
+ TCronExpression cronConverter("0 3 * * 1-6");
+
+ TestNextCron(&cronConverter, "2024-04-22T16:05:32Z", "2024-04-23T03:00:00Z");
+ TestNextCron(&cronConverter, "2024-04-23T02:05:32Z", "2024-04-23T03:00:00Z");
+ TestNextCron(&cronConverter, "2024-04-21T02:05:32Z", "2024-04-22T03:00:00Z");
+ TestNextCron(&cronConverter, "2024-04-20T12:05:32Z", "2024-04-22T03:00:00Z");
+
+ TestPrevCron(&cronConverter, "2024-04-23T16:05:32Z", "2024-04-23T03:00:00Z");
+ TestPrevCron(&cronConverter, "2024-04-23T02:05:32Z", "2024-04-22T03:00:00Z");
+ TestPrevCron(&cronConverter, "2024-04-22T02:05:32Z", "2024-04-20T03:00:00Z");
+ TestPrevCron(&cronConverter, "2024-04-21T12:05:32Z", "2024-04-20T03:00:00Z");
+ }
+
+ Y_UNIT_TEST(Eleventh) {
+ TCronExpression cronConverter("0 17 * * 3");
+
+ TestNextCron(&cronConverter, "2024-04-22T16:05:32Z", "2024-04-24T17:00:00Z");
+ TestNextCron(&cronConverter, "2024-04-27T16:05:32Z", "2024-05-01T17:00:00Z");
+ TestNextCron(&cronConverter, "2024-04-24T17:00:00Z", "2024-05-01T17:00:00Z");
+ TestNextCron(&cronConverter, "2023-12-28T19:45:32Z", "2024-01-03T17:00:00Z");
+
+ TestPrevCron(&cronConverter, "2024-05-01T16:05:32Z", "2024-04-24T17:00:00Z");
+ TestPrevCron(&cronConverter, "2024-05-01T18:05:32Z", "2024-05-01T17:00:00Z");
+ TestPrevCron(&cronConverter, "2024-05-01T17:00:00Z", "2024-04-24T17:00:00Z");
+ TestPrevCron(&cronConverter, "2024-01-02T19:45:32Z", "2023-12-27T17:00:00Z");
+ }
+
+ Y_UNIT_TEST(Twelvth) {
+ TCronExpression cronConverter("0 10,22 * * *");
+
+ TestNextCron(&cronConverter, "2024-04-22T16:05:32Z", "2024-04-22T22:00:00Z");
+ TestNextCron(&cronConverter, "2024-04-22T22:05:32Z", "2024-04-23T10:00:00Z");
+ TestNextCron(&cronConverter, "2024-04-30T23:05:32Z", "2024-05-01T10:00:00Z");
+
+ TestPrevCron(&cronConverter, "2024-04-22T23:05:32Z", "2024-04-22T22:00:00Z");
+ TestPrevCron(&cronConverter, "2024-04-22T21:05:32Z", "2024-04-22T10:00:00Z");
+ TestPrevCron(&cronConverter, "2024-05-01T06:05:32Z", "2024-04-30T22:00:00Z");
+ }
+
+ Y_UNIT_TEST(Thirteenth) {
+ TCronExpression cronConverter("* * 1-5 * *");
+
+ TestNextCron(&cronConverter, "2024-04-22T16:05:32Z", "2024-05-01T00:00:00Z");
+ TestNextCron(&cronConverter, "2024-04-04T16:05:32Z", "2024-04-04T16:06:00Z");
+ TestNextCron(&cronConverter, "2024-04-04T16:05:00Z", "2024-04-04T16:06:00Z");
+ TestNextCron(&cronConverter, "2024-04-05T23:59:21Z", "2024-05-01T00:00:00Z");
+
+ TestPrevCron(&cronConverter, "2024-04-22T16:05:32Z", "2024-04-05T23:59:00Z");
+ TestPrevCron(&cronConverter, "2024-04-04T16:05:32Z", "2024-04-04T16:05:00Z");
+ TestPrevCron(&cronConverter, "2024-04-04T16:05:00Z", "2024-04-04T16:04:00Z");
+ TestPrevCron(&cronConverter, "2024-04-01T00:00:00Z", "2024-03-05T23:59:00Z");
+ }
+
+ Y_UNIT_TEST(Fourteenth) {
+ TCronExpression cronConverter("*/15 * * * 1-5");
+
+ TestNextCron(&cronConverter, "2024-04-22T16:05:32Z", "2024-04-22T16:15:00Z");
+ TestNextCron(&cronConverter, "2024-04-22T16:15:00Z", "2024-04-22T16:30:00Z");
+ TestNextCron(&cronConverter, "2024-04-20T16:05:32Z", "2024-04-22T00:00:00Z");
+
+ TestPrevCron(&cronConverter, "2024-04-22T16:05:32Z", "2024-04-22T16:00:00Z");
+ TestPrevCron(&cronConverter, "2024-04-22T16:15:00Z", "2024-04-22T16:00:00Z");
+ TestPrevCron(&cronConverter, "2024-04-20T16:05:32Z", "2024-04-19T23:45:00Z");
+ }
+
+ Y_UNIT_TEST(Fifteenth) {
+ TCronExpression cronConverter("0 0 * * 6");
+
+ TestNextCron(&cronConverter, "2024-04-22T16:05:32Z", "2024-04-27T00:00:00Z");
+ TestNextCron(&cronConverter, "2024-04-27T00:00:00Z", "2024-05-04T00:00:00Z");
+ TestNextCron(&cronConverter, "2024-04-27T16:05:32Z", "2024-05-04T00:00:00Z");
+
+ TestPrevCron(&cronConverter, "2024-05-02T16:05:32Z", "2024-04-27T00:00:00Z");
+ TestPrevCron(&cronConverter, "2024-05-04T00:00:00Z", "2024-04-27T00:00:00Z");
+ TestPrevCron(&cronConverter, "2024-04-27T16:05:32Z", "2024-04-27T00:00:00Z");
+ }
+
+ Y_UNIT_TEST(Sixteenth) {
+ TCronExpression cronConverter("20 14 * * 0-5");
+
+ TestNextCron(&cronConverter, "2024-04-22T16:05:32Z", "2024-04-23T14:20:00Z");
+ TestNextCron(&cronConverter, "2024-04-22T11:05:32Z", "2024-04-22T14:20:00Z");
+ TestNextCron(&cronConverter, "2024-04-20T02:05:32Z", "2024-04-21T14:20:00Z");
+ TestNextCron(&cronConverter, "2024-04-19T14:20:00Z", "2024-04-21T14:20:00Z");
+
+ TestPrevCron(&cronConverter, "2024-04-24T11:05:32Z", "2024-04-23T14:20:00Z");
+ TestPrevCron(&cronConverter, "2024-04-22T16:05:32Z", "2024-04-22T14:20:00Z");
+ TestPrevCron(&cronConverter, "2024-04-21T02:05:32Z", "2024-04-19T14:20:00Z");
+ TestPrevCron(&cronConverter, "2024-04-21T14:20:00Z", "2024-04-19T14:20:00Z");
+ }
+
+ Y_UNIT_TEST(Seventeenth) {
+ TCronExpression cronConverter("0 9,18 * * *");
+
+ TestNextCron(&cronConverter, "2024-04-22T16:05:32Z", "2024-04-22T18:00:00Z");
+ TestNextCron(&cronConverter, "2024-04-22T22:05:32Z", "2024-04-23T09:00:00Z");
+ TestNextCron(&cronConverter, "2024-04-30T23:05:32Z", "2024-05-01T09:00:00Z");
+
+ TestPrevCron(&cronConverter, "2024-04-22T18:00:00Z", "2024-04-22T09:00:00Z");
+ TestPrevCron(&cronConverter, "2024-04-22T08:05:32Z", "2024-04-21T18:00:00Z");
+ TestPrevCron(&cronConverter, "2024-05-01T06:05:32Z", "2024-04-30T18:00:00Z");
+ }
+
+ Y_UNIT_TEST(Eighteenth) {
+ TCronExpression cronConverter("*/5 10 * * *");
+
+ TestNextCron(&cronConverter, "2024-04-22T16:05:32Z", "2024-04-23T10:00:00Z");
+ TestNextCron(&cronConverter, "2024-04-22T10:05:32Z", "2024-04-22T10:10:00Z");
+ TestNextCron(&cronConverter, "2024-04-22T10:15:00Z", "2024-04-22T10:20:00Z");
+ TestNextCron(&cronConverter, "2024-04-22T10:55:23Z", "2024-04-23T10:00:00Z");
+
+ TestPrevCron(&cronConverter, "2024-04-22T16:05:32Z", "2024-04-22T10:55:00Z");
+ TestPrevCron(&cronConverter, "2024-04-22T10:10:32Z", "2024-04-22T10:10:00Z");
+ TestPrevCron(&cronConverter, "2024-04-22T10:25:00Z", "2024-04-22T10:20:00Z");
+ TestPrevCron(&cronConverter, "2024-04-23T08:55:23Z", "2024-04-22T10:55:00Z");
+ }
+
+ Y_UNIT_TEST(Nineteenth) {
+ TCronExpression cronConverter("30 7 * * 2,4");
+
+ TestNextCron(&cronConverter, "2024-04-22T16:05:32Z", "2024-04-23T07:30:00Z");
+ TestNextCron(&cronConverter, "2024-04-23T16:05:32Z", "2024-04-25T07:30:00Z");
+ TestNextCron(&cronConverter, "2024-04-25T16:05:32Z", "2024-04-30T07:30:00Z");
+ TestNextCron(&cronConverter, "2024-04-23T04:05:32Z", "2024-04-23T07:30:00Z");
+
+ TestPrevCron(&cronConverter, "2024-04-24T16:05:32Z", "2024-04-23T07:30:00Z");
+ TestPrevCron(&cronConverter, "2024-04-29T16:05:32Z", "2024-04-25T07:30:00Z");
+ TestPrevCron(&cronConverter, "2024-04-30T16:05:32Z", "2024-04-30T07:30:00Z");
+ TestPrevCron(&cronConverter, "2024-04-23T04:05:32Z", "2024-04-18T07:30:00Z");
+ }
+
+ Y_UNIT_TEST(Twentieth) {
+ TCronExpression cronConverter("0 0 1,15 * *");
+
+ TestNextCron(&cronConverter, "2024-04-22T16:05:32Z", "2024-05-01T00:00:00Z");
+ TestNextCron(&cronConverter, "2024-04-15T00:00:00Z", "2024-05-01T00:00:00Z");
+ TestNextCron(&cronConverter, "2024-04-30T23:59:59Z", "2024-05-01T00:00:00Z");
+
+ TestPrevCron(&cronConverter, "2024-04-22T16:05:32Z", "2024-04-15T00:00:00Z");
+ TestPrevCron(&cronConverter, "2024-04-15T00:00:00Z", "2024-04-01T00:00:00Z");
+ TestPrevCron(&cronConverter, "2024-04-02T00:00:00Z", "2024-04-01T00:00:00Z");
+ }
+}
+
+Y_UNIT_TEST_SUITE(InvalidTests) {
+ Y_UNIT_TEST(InvalidFieldsAmount) {
+ UNIT_ASSERT_EXCEPTION_CONTAINS(TCronExpression(""), yexception, "Invalid empty expression");
+ UNIT_ASSERT_EXCEPTION_CONTAINS(TCronExpression(" "), yexception, "Invalid number of fields, expression must consist of 5-7 fields but has: 0");
+ UNIT_ASSERT_EXCEPTION_CONTAINS(TCronExpression("* * * * * * * *"), yexception, "Invalid number of fields, expression must consist of 5-7 fields but has: 8");
+ UNIT_ASSERT_EXCEPTION_CONTAINS(TCronExpression("1 1 1 "), yexception, "Invalid number of fields, expression must consist of 5-7 fields but has: 3");
+ UNIT_ASSERT_EXCEPTION_CONTAINS(TCronExpression(" C S K A "), yexception, "Invalid number of fields, expression must consist of 5-7 fields but has: 4");
+ UNIT_ASSERT_EXCEPTION_CONTAINS(TCronExpression(" chemp i on "), yexception, "Invalid number of fields, expression must consist of 5-7 fields but has: 3");
+ }
+
+ Y_UNIT_TEST(InvalidTypedCrons) {
+ UNIT_ASSERT_EXCEPTION_CONTAINS(TCronExpression("@@@@"), yexception, "Unknown typed cron");
+ UNIT_ASSERT_EXCEPTION_CONTAINS(TCronExpression("@ */2 * * 3 *"), yexception, "Unknown typed cron");
+ UNIT_ASSERT_EXCEPTION_CONTAINS(TCronExpression("@everyfriday13"), yexception, "Unknown typed cron");
+ UNIT_ASSERT_EXCEPTION_CONTAINS(TCronExpression("@milisecondly"), yexception, "Unknown typed cron");
+ UNIT_ASSERT_EXCEPTION_CONTAINS(TCronExpression("@reboot"), yexception, "@reboot not implemented");
+ }
+
+ Y_UNIT_TEST(InvalidCrons) {
+ UNIT_ASSERT_EXCEPTION(TCronExpression("1- * * * * *"), yexception);
+ UNIT_ASSERT_EXCEPTION(TCronExpression("-4/3 * * * * *"), yexception);
+ UNIT_ASSERT_EXCEPTION(TCronExpression("1-100 * * * * *"), yexception);
+ UNIT_ASSERT_EXCEPTION(TCronExpression("-5-30/4 * * * * *"), yexception);
+ UNIT_ASSERT_EXCEPTION(TCronExpression("* * * * * fRi-MO"), yexception);
+ UNIT_ASSERT_EXCEPTION(TCronExpression("* * * * * FR"), yexception);
+ UNIT_ASSERT_EXCEPTION(TCronExpression("0 0 1,15 * W"), yexception);
+ UNIT_ASSERT_EXCEPTION(TCronExpression("0 W 1,15 * W"), yexception);
+ UNIT_ASSERT_EXCEPTION(TCronExpression("W * * * * *"), yexception);
+ UNIT_ASSERT_EXCEPTION(TCronExpression("* W * * * *"), yexception);
+ UNIT_ASSERT_EXCEPTION(TCronExpression("* * W * * *"), yexception);
+ UNIT_ASSERT_EXCEPTION(TCronExpression("* * * * W *"), yexception);
+ UNIT_ASSERT_EXCEPTION(TCronExpression("* * * * * W"), yexception);
+ UNIT_ASSERT_EXCEPTION(TCronExpression("L * * * * *"), yexception);
+ UNIT_ASSERT_EXCEPTION(TCronExpression("* L * * * *"), yexception);
+ UNIT_ASSERT_EXCEPTION(TCronExpression("* * L * * *"), yexception);
+ UNIT_ASSERT_EXCEPTION(TCronExpression("* * * * L *"), yexception);
+ UNIT_ASSERT_EXCEPTION(TCronExpression("* * * * 13 *"), yexception);
+ UNIT_ASSERT_EXCEPTION(TCronExpression("77 * * * * *"), yexception);
+ UNIT_ASSERT_EXCEPTION(TCronExpression("* * -1 * * *"), yexception);
+ UNIT_ASSERT_EXCEPTION(TCronExpression("* * * 32 2 * 2020"), yexception);
+ UNIT_ASSERT_EXCEPTION(TCronExpression("* * * * * 8"), yexception);
+ UNIT_ASSERT_EXCEPTION(TCronExpression("* * * * * * 1969"), yexception);
+ UNIT_ASSERT_EXCEPTION(TCronExpression("0 0 1,15 * 20492049"), yexception);
+ UNIT_ASSERT_EXCEPTION(TCronExpression("5-2 0 1,15 * W"), yexception);
+ UNIT_ASSERT_EXCEPTION(TCronExpression("* * * * * * 2021-2019"), yexception);
+ UNIT_ASSERT_EXCEPTION(TCronExpression("* * * * -4 *"), yexception);
+ UNIT_ASSERT_EXCEPTION(TCronExpression("* */-2 * * W *"), yexception);
+ UNIT_ASSERT_EXCEPTION(TCronExpression("*/0.5 * * * W *"), yexception);
+ UNIT_ASSERT_EXCEPTION(TCronExpression("* * * LW-1 * *"), yexception);
+ UNIT_ASSERT_EXCEPTION(TCronExpression("radio goo goo radio gaa gaa"), yexception);
+ UNIT_ASSERT_EXCEPTION(TCronExpression("* * 23-28 * * *"), yexception);
+ UNIT_ASSERT_EXCEPTION(TCronExpression("*/0 1 * * * *"), yexception);
+ UNIT_ASSERT_EXCEPTION(TCronExpression("/5 * * * * *"), yexception);
+ UNIT_ASSERT_EXCEPTION(TCronExpression("* 1 1 0 * *"), yexception);
+ UNIT_ASSERT_EXCEPTION(TCronExpression("* * * * * * 3050"), yexception);
+ UNIT_ASSERT_EXCEPTION(TCronExpression("60 * * * * * *"), yexception);
+ UNIT_ASSERT_EXCEPTION(TCronExpression("* 60 * * * * *"), yexception);
+ UNIT_ASSERT_EXCEPTION(TCronExpression("* * 24 * * * *"), yexception);
+ UNIT_ASSERT_EXCEPTION(TCronExpression("* * * 32 * * *"), yexception);
+ UNIT_ASSERT_EXCEPTION(TCronExpression("* * * * 13 * *"), yexception);
+ }
+
+ Y_UNIT_TEST(InvalidRequestInPastOrFuture) {
+ TCronExpression cronConverter("0 0 1,15 * * * 2020");
+
+ TestNextCronBadRequest(&cronConverter, "2030-02-22T16:05:32Z");
+ TestNextCronBadRequest(&cronConverter, "2020-12-31T15:00:00Z");
+ TestPrevCronBadRequest(&cronConverter, "2020-01-01T01:00:00Z");
+ TestPrevCronBadRequest(&cronConverter, "1970-02-22T16:05:32Z");
+ }
+
+ Y_UNIT_TEST(InvalidRequestDoesNotExist) {
+ TCronExpression cronConverter("0 0 7 30 2 * *");
+
+ TestNextCronBadRequest(&cronConverter, "2030-02-22T16:05:32Z");
+ }
+}
+
+} // namespace
+
diff --git a/library/cpp/cron_expression/ut/ya.make b/library/cpp/cron_expression/ut/ya.make
new file mode 100644
index 00000000000..b61ef6379ee
--- /dev/null
+++ b/library/cpp/cron_expression/ut/ya.make
@@ -0,0 +1,11 @@
+UNITTEST()
+
+SRCS(
+ cron_expression_ut.cpp
+)
+
+PEERDIR(
+ library/cpp/cron_expression
+)
+
+END()
diff --git a/library/cpp/cron_expression/ya.make b/library/cpp/cron_expression/ya.make
new file mode 100644
index 00000000000..6caebaadf7e
--- /dev/null
+++ b/library/cpp/cron_expression/ya.make
@@ -0,0 +1,15 @@
+LIBRARY()
+
+SRC(
+ cron_expression.cpp
+)
+
+PEERDIR(
+ library/cpp/timezone_conversion
+)
+
+END()
+
+RECURSE_FOR_TESTS (
+ ut
+)
diff --git a/yt/yt/client/queue_client/config.cpp b/yt/yt/client/queue_client/config.cpp
index cba7329eba0..e62d04a7f3c 100644
--- a/yt/yt/client/queue_client/config.cpp
+++ b/yt/yt/client/queue_client/config.cpp
@@ -1,5 +1,7 @@
#include "config.h"
+#include <library/cpp/cron_expression/cron_expression.h>
+
namespace NYT::NQueueClient {
////////////////////////////////////////////////////////////////////////////////
@@ -57,7 +59,11 @@ bool operator==(const TQueueAutoTrimConfig& lhs, const TQueueAutoTrimConfig& rhs
void TQueueStaticExportConfig::Register(TRegistrar registrar)
{
registrar.Parameter("export_period", &TThis::ExportPeriod)
- .GreaterThan(TDuration::Zero());
+ .GreaterThan(TDuration::Zero())
+ .Optional();
+ registrar.Parameter("export_cron_schedule", &TThis::ExportCronSchedule)
+ .NonEmpty()
+ .Optional();
registrar.Parameter("export_directory", &TThis::ExportDirectory);
registrar.Parameter("export_ttl", &TThis::ExportTtl)
.Default(TDuration::Zero());
@@ -67,15 +73,31 @@ void TQueueStaticExportConfig::Register(TRegistrar registrar)
.Default(false);
registrar.Postprocessor([] (TThis* config) {
- if (config->ExportPeriod.GetValue() % TDuration::Seconds(1).GetValue() != 0) {
- THROW_ERROR_EXCEPTION("The value of \"export_period\" must be a multiple of 1000 (1 second)");
+ if (config->ExportPeriod && config->ExportCronSchedule) {
+ THROW_ERROR_EXCEPTION("Both \"export_period\" and \"export_cron_schedule\" cannot be set at the same time");
+ }
+
+ if (config->ExportPeriod) {
+ if (config->ExportPeriod->GetValue() % TDuration::Seconds(1).GetValue() != 0) {
+ THROW_ERROR_EXCEPTION("The value of \"export_period\" must be a multiple of 1000 (1 second)");
+ }
+ } else if (config->ExportCronSchedule) {
+ try {
+ TCronExpression{*config->ExportCronSchedule};
+ } catch (const std::exception& ex) {
+ THROW_ERROR_EXCEPTION("Export CRON schedule %Qv is not well-formed", *config->ExportCronSchedule)
+ << ex;
+ }
+ } else {
+ THROW_ERROR_EXCEPTION("One of \"export_period\", \"export_cron_schedule\" must be specified");
}
});
}
bool operator==(const TQueueStaticExportConfig& lhs, const TQueueStaticExportConfig& rhs)
{
- return std::tie(lhs.ExportPeriod, lhs.ExportDirectory) == std::tie(rhs.ExportPeriod, rhs.ExportDirectory);
+ return std::tie(lhs.ExportPeriod, lhs.ExportCronSchedule, lhs.ExportDirectory) ==
+ std::tie(rhs.ExportPeriod, lhs.ExportCronSchedule, rhs.ExportDirectory);
}
////////////////////////////////////////////////////////////////////////////////
diff --git a/yt/yt/client/queue_client/config.h b/yt/yt/client/queue_client/config.h
index b4fe0f671d3..1bfdc84d06c 100644
--- a/yt/yt/client/queue_client/config.h
+++ b/yt/yt/client/queue_client/config.h
@@ -64,8 +64,19 @@ bool operator==(const TQueueAutoTrimConfig& lhs, const TQueueAutoTrimConfig& rhs
struct TQueueStaticExportConfig
: public NYTree::TYsonStruct
{
- //! Export will be performed at times that are multiple of this period.
- TDuration ExportPeriod;
+ //! Export will be performed at times that are multiple of this period. Mutually exclusive
+ //! with ExportCronSchedule parameter.
+ std::optional<TDuration> ExportPeriod;
+
+ //! Export will be performed at schedule that is defined by this CRON expression. Mutually exclusive
+ //! with ExportPeriod parameter.
+ //! This CRON format supports features beyond the standard ones, allowing from 5 to 7 fields to be specified:
+ //! - with 5 fields, it's a standard CRON
+ //! - with 6 fields, the first field is interpreted as SECONDS
+ //! - with 7 fields, the last field is interpreted as YEARS
+ //!
+ //! \note See library/cpp/cron_expression/readme.md.
+ std::optional<TString> ExportCronSchedule;
//! Path to directory that will contain resulting static tables with exported data.
NYPath::TYPath ExportDirectory;
diff --git a/yt/yt/client/unittests/queue_static_export_config_ut.cpp b/yt/yt/client/unittests/queue_static_export_config_ut.cpp
new file mode 100644
index 00000000000..41a198c86d5
--- /dev/null
+++ b/yt/yt/client/unittests/queue_static_export_config_ut.cpp
@@ -0,0 +1,55 @@
+#include <yt/yt/core/test_framework/framework.h>
+
+#include <yt/yt/client/queue_client/config.h>
+
+namespace NYT::NQueueClient {
+namespace {
+
+using namespace NYTree;
+using namespace NYson;
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TQueueStaticExportConfigTest
+ : public ::testing::TestWithParam<std::tuple<
+ TYsonString,
+ bool>>
+{ };
+
+TEST_P(TQueueStaticExportConfigTest, ScheduleInvariants)
+{
+ const auto& [ysonConfig, shouldThrow] = GetParam();
+
+ if (shouldThrow) {
+ ASSERT_ANY_THROW(ConvertTo<TQueueStaticExportConfigPtr>(ysonConfig));
+ } else {
+ ASSERT_NO_THROW(ConvertTo<TQueueStaticExportConfigPtr>(ysonConfig));
+ }
+}
+
+INSTANTIATE_TEST_SUITE_P(
+ TQueueStaticExportConfigTest,
+ TQueueStaticExportConfigTest,
+ ::testing::Values(
+ std::tuple(
+ R"({"export_directory"="/tmp/foo"; "export_period"=1000;})"_sb,
+ false
+ ),
+ std::tuple(
+ R"({"export_directory"="/tmp/foo"; "export_cron_schedule"="* * * * *";})"_sb,
+ false
+ ),
+ std::tuple(
+ R"({"export_directory"="/tmp/foo";})"_sb,
+ true
+ ),
+ std::tuple(
+ R"({"export_directory"="/tmp/foo"; "export_period"=1000; "export_cron_schedule"="* * * * *";})"_sb,
+ true
+ )
+));
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace
+} // namespace NYT::NQueryClient
diff --git a/yt/yt/client/unittests/ya.make b/yt/yt/client/unittests/ya.make
index bbde30040b2..72886d467f2 100644
--- a/yt/yt/client/unittests/ya.make
+++ b/yt/yt/client/unittests/ya.make
@@ -23,6 +23,7 @@ SRCS(
time_text_ut.cpp
node_directory_ut.cpp
query_builder_ut.cpp
+ queue_static_export_config_ut.cpp
read_limit_ut.cpp
replication_card_ut.cpp
replication_progress_ut.cpp
diff --git a/yt/yt/client/ya.make b/yt/yt/client/ya.make
index 9a00da36c67..115413e7c42 100644
--- a/yt/yt/client/ya.make
+++ b/yt/yt/client/ya.make
@@ -230,6 +230,7 @@ PEERDIR(
library/cpp/digest/crc32c
library/cpp/json
library/cpp/string_utils/base64
+ library/cpp/cron_expression
contrib/libs/pfr
)