You are viewing a plain text version of this content. The canonical link for it is here.
Posted to issues@nifi.apache.org by GitBox <gi...@apache.org> on 2022/05/26 12:42:53 UTC

[GitHub] [nifi-minifi-cpp] fgerlits commented on a diff in pull request #1335: MINIFICPP-1809: custom Cron(quartz syntax) implementation and cron tests

fgerlits commented on code in PR #1335:
URL: https://github.com/apache/nifi-minifi-cpp/pull/1335#discussion_r881753458


##########
libminifi/src/utils/Cron.cpp:
##########
@@ -0,0 +1,483 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include "utils/Cron.h"
+#include "utils/TimeUtil.h"
+#include "utils/StringUtils.h"
+#include "date/date.h"
+
+using namespace std::literals::chrono_literals;
+
+using std::chrono::seconds;
+using std::chrono::minutes;
+using std::chrono::hours;
+using std::chrono::days;
+
+// TODO(C++20): move to std::chrono when calendar is fully supported
+using date::local_seconds;
+using date::day;
+using date::weekday;
+using date::month;
+using date::year;
+using date::year_month_day;
+using date::last;
+using date::local_days;
+using date::from_stream;
+using date::make_time;
+
+using date::Friday; using date::Saturday; using date::Sunday;
+
+namespace org::apache::nifi::minifi::utils {
+namespace {
+
+bool operator<=(const weekday& lhs, const weekday& rhs) {
+  return lhs.c_encoding() <= rhs.c_encoding();

Review Comment:
   Do we need to do something special about Sundays?  In both `cron` and `date`, both 0 and 7 are Sunday, so a day of the week range of "6-7" should probably include 0.
   
   Also, "6-7" may create a range of `weekday{6}`-`weekday{0}`; is that a problem?



##########
libminifi/src/utils/Cron.cpp:
##########
@@ -0,0 +1,483 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include "utils/Cron.h"
+#include "utils/TimeUtil.h"
+#include "utils/StringUtils.h"
+#include "date/date.h"
+
+using namespace std::literals::chrono_literals;
+
+using std::chrono::seconds;
+using std::chrono::minutes;
+using std::chrono::hours;
+using std::chrono::days;
+
+// TODO(C++20): move to std::chrono when calendar is fully supported
+using date::local_seconds;
+using date::day;
+using date::weekday;
+using date::month;
+using date::year;
+using date::year_month_day;
+using date::last;
+using date::local_days;
+using date::from_stream;
+using date::make_time;
+
+using date::Friday; using date::Saturday; using date::Sunday;
+
+namespace org::apache::nifi::minifi::utils {
+namespace {
+
+bool operator<=(const weekday& lhs, const weekday& rhs) {
+  return lhs.c_encoding() <= rhs.c_encoding();
+}
+
+template <typename FieldType>
+FieldType parse(const std::string&);
+
+template <>
+seconds parse<seconds>(const std::string& second_str) {
+  auto sec_int = std::stoul(second_str);
+  if (sec_int <= 59)
+    return seconds(sec_int);
+  throw BadCronExpression("Invalid second " + second_str);
+}

Review Comment:
   Should we do stricter validation?  Here we have the usual banana problem: `parse<seconds>("banana") == seconds{0}` and no warning is given.



##########
libminifi/src/utils/Cron.cpp:
##########
@@ -0,0 +1,483 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include "utils/Cron.h"
+#include "utils/TimeUtil.h"
+#include "utils/StringUtils.h"
+#include "date/date.h"
+
+using namespace std::literals::chrono_literals;
+
+using std::chrono::seconds;
+using std::chrono::minutes;
+using std::chrono::hours;
+using std::chrono::days;
+
+// TODO(C++20): move to std::chrono when calendar is fully supported
+using date::local_seconds;
+using date::day;
+using date::weekday;
+using date::month;
+using date::year;
+using date::year_month_day;
+using date::last;
+using date::local_days;
+using date::from_stream;
+using date::make_time;
+
+using date::Friday; using date::Saturday; using date::Sunday;
+
+namespace org::apache::nifi::minifi::utils {
+namespace {
+
+bool operator<=(const weekday& lhs, const weekday& rhs) {
+  return lhs.c_encoding() <= rhs.c_encoding();
+}
+
+template <typename FieldType>
+FieldType parse(const std::string&);
+
+template <>
+seconds parse<seconds>(const std::string& second_str) {
+  auto sec_int = std::stoul(second_str);
+  if (sec_int <= 59)
+    return seconds(sec_int);
+  throw BadCronExpression("Invalid second " + second_str);
+}
+
+template <>
+minutes parse<minutes>(const std::string& minute_str) {
+  auto min_int = std::stoul(minute_str);
+  if (min_int <= 59)
+    return minutes(min_int);
+  throw BadCronExpression("Invalid minute " + minute_str);
+}
+
+template <>
+hours parse<hours>(const std::string& hour_str) {
+  auto hour_int = std::stoul(hour_str);
+  if (hour_int <= 23)
+    return hours(hour_int);
+  throw BadCronExpression("Invalid hour " + hour_str);
+}
+
+template <>
+days parse<days>(const std::string& days_str) {
+  return days(std::stoul(days_str));
+}
+
+template <>
+day parse<day>(const std::string& day_str) {
+  auto day_int = std::stoul(day_str);
+  if (day_int >= 1 && day_int <= 31)
+    return day(day_int);
+  throw BadCronExpression("Invalid day " + day_str);
+}
+
+template <>
+month parse<month>(const std::string& month_str) {
+// https://github.com/HowardHinnant/date/issues/550
+// TODO(gcc11): Due to https://gcc.gnu.org/bugzilla/show_bug.cgi?id=78714
+// the month parsing with '%b' is case sensitive in gcc11
+// This has been fixed in gcc12
+#if defined(__GNUC__) && __GNUC__ < 12
+  auto patched_month_str = StringUtils::toLower(month_str);
+  if (!patched_month_str.empty())
+    patched_month_str[0] = std::toupper(patched_month_str[0]);
+  std::stringstream stream(patched_month_str);
+#else
+  std::stringstream stream(month_str);
+#endif
+
+  stream.imbue(std::locale("en_US.UTF-8"));
+  month parsed_month{};
+  if (month_str.size() > 2) {
+    from_stream(stream, "%b", parsed_month);
+    if (parsed_month.ok())
+      return parsed_month;
+  } else {
+    from_stream(stream, "%m", parsed_month);
+    if (parsed_month.ok())
+      return parsed_month;
+  }
+
+  throw BadCronExpression("Invalid month " + month_str);
+}
+
+template <>
+weekday parse<weekday>(const std::string& weekday_str) {
+  std::stringstream stream(weekday_str);
+  stream.imbue(std::locale("en_US.UTF-8"));
+
+  if (weekday_str.size() > 2) {
+    weekday parsed_weekday{};
+    from_stream(stream, "%a", parsed_weekday);
+    if (parsed_weekday.ok())
+      return parsed_weekday;
+  } else {
+    unsigned weekday_num;
+    stream >> weekday_num;
+    if (!stream.bad() && weekday_num < 7)
+      return weekday(weekday_num-1);
+  }
+  throw BadCronExpression("Invalid weekday: " + weekday_str);
+}
+
+template <>
+year parse<year>(const std::string& year_str) {
+  auto year_int = std::stoi(year_str);
+  if (year_int >= 1970 && year_int <= 2099)
+    return year(year_int);
+  throw BadCronExpression("Invalid year: " + year_str);
+}
+
+template <typename FieldType>
+FieldType getFieldType(local_seconds time_point);
+
+template <>
+year getFieldType(local_seconds time_point) {
+  year_month_day year_month_day(floor<days>(time_point));
+  return year_month_day.year();
+}
+
+template <>
+month getFieldType(local_seconds time_point) {
+  year_month_day year_month_day(floor<days>(time_point));
+  return year_month_day.month();
+}
+
+template <>
+day getFieldType(local_seconds time_point) {
+  year_month_day year_month_day(floor<days>(time_point));
+  return year_month_day.day();
+}
+
+template <>
+hours getFieldType(local_seconds time_point) {
+  auto dp = floor<days>(time_point);
+  auto time = make_time(time_point-dp);
+  return time.hours();
+}
+
+template <>
+minutes getFieldType(local_seconds time_point) {
+  auto dp = floor<days>(time_point);
+  auto time = make_time(time_point-dp);
+  return time.minutes();
+}
+
+template <>
+seconds getFieldType(local_seconds time_point) {
+  auto dp = floor<days>(time_point);
+  auto time = make_time(time_point-dp);
+  return time.seconds();
+}
+
+template <>
+weekday getFieldType(local_seconds time_point) {
+  auto dp = floor<days>(time_point);
+  return weekday(dp);
+}
+
+bool isWeekday(year_month_day date) {
+  weekday date_weekday = weekday(local_days(date));
+  return date_weekday != Saturday && date_weekday != Sunday;
+}
+
+template <typename FieldType>
+class SingleValueField : public CronField {
+ public:
+  explicit SingleValueField(FieldType value) : value_(value) {}
+
+  [[nodiscard]] bool isValid(local_seconds time_point) const override {
+    return value_ == getFieldType<FieldType>(time_point);
+  }
+
+ private:
+  FieldType value_;
+};
+
+class NotCheckedField : public CronField {
+ public:
+  NotCheckedField() = default;
+
+  [[nodiscard]] bool isValid(local_seconds) const override { return true; }
+};
+
+class AllValuesField : public CronField {
+ public:
+  AllValuesField() = default;
+
+  [[nodiscard]] bool isValid(local_seconds) const override { return true; }
+};
+
+template <typename FieldType>
+class RangeField : public CronField {
+ public:
+  explicit RangeField(FieldType lower_bound, FieldType upper_bound)
+      : lower_bound_(std::move(lower_bound)),
+        upper_bound_(std::move(upper_bound)) {
+  }
+
+  [[nodiscard]] bool isValid(local_seconds value) const override {
+    return lower_bound_ <= getFieldType<FieldType>(value) && getFieldType<FieldType>(value) <= upper_bound_;
+  }
+
+ private:
+  FieldType lower_bound_;
+  FieldType upper_bound_;
+};
+
+template <typename FieldType>
+class ListField : public CronField {
+ public:
+  explicit ListField(std::vector<FieldType> valid_values) : valid_values_(std::move(valid_values)) {}
+
+  [[nodiscard]] bool isValid(local_seconds value) const override {
+    return std::find(valid_values_.begin(), valid_values_.end(), getFieldType<FieldType>(value)) != valid_values_.end();

Review Comment:
   here, too, we may need to do something special with Sundays



##########
libminifi/src/utils/Cron.cpp:
##########
@@ -0,0 +1,483 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include "utils/Cron.h"
+#include "utils/TimeUtil.h"
+#include "utils/StringUtils.h"
+#include "date/date.h"
+
+using namespace std::literals::chrono_literals;
+
+using std::chrono::seconds;
+using std::chrono::minutes;
+using std::chrono::hours;
+using std::chrono::days;
+
+// TODO(C++20): move to std::chrono when calendar is fully supported
+using date::local_seconds;
+using date::day;
+using date::weekday;
+using date::month;
+using date::year;
+using date::year_month_day;
+using date::last;
+using date::local_days;
+using date::from_stream;
+using date::make_time;
+
+using date::Friday; using date::Saturday; using date::Sunday;
+
+namespace org::apache::nifi::minifi::utils {
+namespace {
+
+bool operator<=(const weekday& lhs, const weekday& rhs) {
+  return lhs.c_encoding() <= rhs.c_encoding();
+}
+
+template <typename FieldType>
+FieldType parse(const std::string&);
+
+template <>
+seconds parse<seconds>(const std::string& second_str) {
+  auto sec_int = std::stoul(second_str);
+  if (sec_int <= 59)
+    return seconds(sec_int);
+  throw BadCronExpression("Invalid second " + second_str);
+}
+
+template <>
+minutes parse<minutes>(const std::string& minute_str) {
+  auto min_int = std::stoul(minute_str);
+  if (min_int <= 59)
+    return minutes(min_int);
+  throw BadCronExpression("Invalid minute " + minute_str);
+}
+
+template <>
+hours parse<hours>(const std::string& hour_str) {
+  auto hour_int = std::stoul(hour_str);
+  if (hour_int <= 23)
+    return hours(hour_int);
+  throw BadCronExpression("Invalid hour " + hour_str);
+}
+
+template <>
+days parse<days>(const std::string& days_str) {
+  return days(std::stoul(days_str));
+}
+
+template <>
+day parse<day>(const std::string& day_str) {
+  auto day_int = std::stoul(day_str);
+  if (day_int >= 1 && day_int <= 31)
+    return day(day_int);
+  throw BadCronExpression("Invalid day " + day_str);
+}
+
+template <>
+month parse<month>(const std::string& month_str) {
+// https://github.com/HowardHinnant/date/issues/550
+// TODO(gcc11): Due to https://gcc.gnu.org/bugzilla/show_bug.cgi?id=78714
+// the month parsing with '%b' is case sensitive in gcc11
+// This has been fixed in gcc12
+#if defined(__GNUC__) && __GNUC__ < 12
+  auto patched_month_str = StringUtils::toLower(month_str);
+  if (!patched_month_str.empty())
+    patched_month_str[0] = std::toupper(patched_month_str[0]);
+  std::stringstream stream(patched_month_str);
+#else
+  std::stringstream stream(month_str);
+#endif
+
+  stream.imbue(std::locale("en_US.UTF-8"));
+  month parsed_month{};
+  if (month_str.size() > 2) {
+    from_stream(stream, "%b", parsed_month);
+    if (parsed_month.ok())
+      return parsed_month;
+  } else {
+    from_stream(stream, "%m", parsed_month);
+    if (parsed_month.ok())
+      return parsed_month;
+  }
+
+  throw BadCronExpression("Invalid month " + month_str);
+}
+
+template <>
+weekday parse<weekday>(const std::string& weekday_str) {
+  std::stringstream stream(weekday_str);
+  stream.imbue(std::locale("en_US.UTF-8"));
+
+  if (weekday_str.size() > 2) {
+    weekday parsed_weekday{};
+    from_stream(stream, "%a", parsed_weekday);
+    if (parsed_weekday.ok())
+      return parsed_weekday;
+  } else {
+    unsigned weekday_num;
+    stream >> weekday_num;
+    if (!stream.bad() && weekday_num < 7)
+      return weekday(weekday_num-1);
+  }
+  throw BadCronExpression("Invalid weekday: " + weekday_str);
+}
+
+template <>
+year parse<year>(const std::string& year_str) {
+  auto year_int = std::stoi(year_str);
+  if (year_int >= 1970 && year_int <= 2099)
+    return year(year_int);
+  throw BadCronExpression("Invalid year: " + year_str);
+}
+
+template <typename FieldType>
+FieldType getFieldType(local_seconds time_point);
+
+template <>
+year getFieldType(local_seconds time_point) {
+  year_month_day year_month_day(floor<days>(time_point));
+  return year_month_day.year();
+}
+
+template <>
+month getFieldType(local_seconds time_point) {
+  year_month_day year_month_day(floor<days>(time_point));
+  return year_month_day.month();
+}
+
+template <>
+day getFieldType(local_seconds time_point) {
+  year_month_day year_month_day(floor<days>(time_point));
+  return year_month_day.day();
+}
+
+template <>
+hours getFieldType(local_seconds time_point) {
+  auto dp = floor<days>(time_point);
+  auto time = make_time(time_point-dp);
+  return time.hours();
+}
+
+template <>
+minutes getFieldType(local_seconds time_point) {
+  auto dp = floor<days>(time_point);
+  auto time = make_time(time_point-dp);
+  return time.minutes();
+}
+
+template <>
+seconds getFieldType(local_seconds time_point) {
+  auto dp = floor<days>(time_point);
+  auto time = make_time(time_point-dp);
+  return time.seconds();
+}
+
+template <>
+weekday getFieldType(local_seconds time_point) {
+  auto dp = floor<days>(time_point);
+  return weekday(dp);
+}
+
+bool isWeekday(year_month_day date) {
+  weekday date_weekday = weekday(local_days(date));
+  return date_weekday != Saturday && date_weekday != Sunday;
+}
+
+template <typename FieldType>
+class SingleValueField : public CronField {
+ public:
+  explicit SingleValueField(FieldType value) : value_(value) {}
+
+  [[nodiscard]] bool isValid(local_seconds time_point) const override {
+    return value_ == getFieldType<FieldType>(time_point);
+  }
+
+ private:
+  FieldType value_;
+};
+
+class NotCheckedField : public CronField {
+ public:
+  NotCheckedField() = default;
+
+  [[nodiscard]] bool isValid(local_seconds) const override { return true; }
+};
+
+class AllValuesField : public CronField {
+ public:
+  AllValuesField() = default;
+
+  [[nodiscard]] bool isValid(local_seconds) const override { return true; }
+};
+
+template <typename FieldType>
+class RangeField : public CronField {
+ public:
+  explicit RangeField(FieldType lower_bound, FieldType upper_bound)
+      : lower_bound_(std::move(lower_bound)),
+        upper_bound_(std::move(upper_bound)) {
+  }
+
+  [[nodiscard]] bool isValid(local_seconds value) const override {
+    return lower_bound_ <= getFieldType<FieldType>(value) && getFieldType<FieldType>(value) <= upper_bound_;
+  }
+
+ private:
+  FieldType lower_bound_;
+  FieldType upper_bound_;
+};
+
+template <typename FieldType>
+class ListField : public CronField {
+ public:
+  explicit ListField(std::vector<FieldType> valid_values) : valid_values_(std::move(valid_values)) {}
+
+  [[nodiscard]] bool isValid(local_seconds value) const override {
+    return std::find(valid_values_.begin(), valid_values_.end(), getFieldType<FieldType>(value)) != valid_values_.end();
+  }
+
+ private:
+  std::vector<FieldType> valid_values_;
+};
+
+template <typename FieldType>
+class IncrementField : public CronField {
+ public:
+  IncrementField(FieldType start, int increment) : start_(start), increment_(increment) {}
+
+  [[nodiscard]] bool isValid(local_seconds value) const override {
+    return (getFieldType<FieldType>(value) - start_).count() % increment_ == 0;
+  }
+
+ private:
+  FieldType start_;
+  int increment_;
+};
+
+class LastNthDayInMonthField : public CronField {
+ public:
+  explicit LastNthDayInMonthField(days offset) : offset_(offset) {}
+
+  [[nodiscard]] bool isValid(local_seconds tp) const override {
+    year_month_day date(floor<days>(tp));
+    auto last_day = date.year()/date.month()/last;
+    auto target_date = local_days(last_day)-offset_;
+    return local_days(date) == target_date;
+  }
+
+ private:
+  days offset_;
+};
+
+class NthWeekdayField : public CronField {
+ public:
+  NthWeekdayField(weekday weekday, uint8_t n) : weekday_(weekday), n_(n) {}
+
+  [[nodiscard]] bool isValid(local_seconds tp) const override {
+    year_month_day date(floor<days>(tp));
+    auto target_date = date.year()/date.month()/(weekday_[n_]);
+    return local_days(date) == local_days(target_date);
+  }
+
+ private:
+  weekday weekday_;
+  uint8_t n_;
+};
+
+class LastWeekDayField : public CronField {
+ public:
+  LastWeekDayField() = default;
+
+  [[nodiscard]] bool isValid(local_seconds value) const override {
+    year_month_day date(floor<days>(value));
+    year_month_day last_day_of_the_month_date = year_month_day(local_days(date.year()/date.month()/last));
+    if (isWeekday(last_day_of_the_month_date))
+      return date == last_day_of_the_month_date;
+    year_month_day last_friday_of_the_month_date = year_month_day(local_days(date.year()/date.month()/Friday[last]));
+    return date == last_friday_of_the_month_date;
+  }
+};
+
+class ClosestWeekdayToX : public CronField {
+ public:
+  explicit ClosestWeekdayToX(day x) : x_(x) {}
+
+  [[nodiscard]] bool isValid(local_seconds value) const override {
+    year_month_day date(floor<days>(value));
+    year_month_day target_date = year_month_day(local_days(date.year()/date.month()/x_));
+    if (target_date.ok() && isWeekday(target_date))
+      return target_date == date;
+
+    target_date = year_month_day(local_days(date.year()/date.month()/(x_-days(1))));
+    if (target_date.ok() && isWeekday(target_date))
+      return target_date == date;
+
+    target_date = year_month_day(local_days(date.year()/date.month()/(x_+days(1))));
+    if (target_date.ok() && isWeekday(target_date))
+      return target_date == date;
+
+    target_date = year_month_day(local_days(date.year()/date.month()/(x_+days(2))));
+    if (target_date.ok() && isWeekday(target_date))
+      return target_date == date;

Review Comment:
   This will jump over month boundaries, which (according to https://support.atlassian.com/jira-software-cloud/docs/construct-cron-expressions-for-a-filter-subscription/) is not allowed.
   
   Also, you could do this in a loop to avoid code duplication.
   
   This may work, but please test it:
   ```suggestion
       for (auto diff : {0, -1, 1, -2, 2}) {
         auto target_date = date.year() / date.month() / (x_ + days(diff));
         if (target_date.ok() && isWeekday(target_date))
           return target_date == date;
       }
   ```



##########
libminifi/test/unit/SchedulingAgentTests.cpp:
##########
@@ -0,0 +1,138 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include "../Catch.h"
+#include "../TestBase.h"
+#include "ProvenanceTestHelper.h"
+#include "utils/TestUtils.h"
+
+using namespace std::literals::chrono_literals;
+
+namespace org::apache::nifi::minifi::testing {
+
+class CountOnTriggersProcessor : public minifi::core::Processor {
+ public:
+  using minifi::core::Processor::Processor;
+
+  void onTrigger(core::ProcessContext*, core::ProcessSession*) override {
+    ++number_of_triggers;
+  }
+
+  size_t getNumberOfTriggers() const { return number_of_triggers; }
+
+ private:
+  std::atomic<size_t> number_of_triggers = 0;
+};
+
+
+TEST_CASE("SchedulingAgentTests", "[SchedulingAgent]") {
+  std::shared_ptr<core::Repository> test_repo = std::make_shared<TestRepository>();
+  std::shared_ptr<core::ContentRepository> content_repo = std::make_shared<core::repository::VolatileContentRepository>();
+  std::shared_ptr<TestRepository> repo = std::static_pointer_cast<TestRepository>(test_repo);
+  std::shared_ptr<minifi::FlowController> controller =
+      std::make_shared<TestFlowController>(test_repo, test_repo, content_repo);
+
+  TestController testController;
+  auto test_plan = testController.createPlan();
+  auto controller_services_ = std::make_shared<minifi::core::controller::ControllerServiceMap>();
+  auto configuration = std::make_shared<minifi::Configure>();
+  auto controller_services_provider_ = std::make_shared<minifi::core::controller::StandardControllerServiceProvider>(controller_services_, nullptr, configuration);
+  utils::ThreadPool<utils::TaskRescheduleInfo> thread_pool;
+  auto count_proc = std::make_shared<CountOnTriggersProcessor>("count_proc");
+  count_proc->incrementActiveTasks();
+  count_proc->setScheduledState(core::RUNNING);
+  auto node = std::make_shared<core::ProcessorNode>(count_proc.get());
+  auto context = std::make_shared<core::ProcessContext>(node, nullptr, repo, repo, content_repo);
+  std::shared_ptr<core::ProcessSessionFactory> factory = std::make_shared<core::ProcessSessionFactory>(context);
+  count_proc->setSchedulingPeriodNano(1250ms);
+#ifdef WIN32
+  utils::dateSetInstall(TZ_DATA_DIR);
+#endif
+
+  SECTION("Timer Driven") {
+    auto timer_driven_agent = std::make_shared<TimerDrivenSchedulingAgent>(gsl::make_not_null(controller_services_provider_.get()), test_repo, test_repo, content_repo, configuration, thread_pool);
+    timer_driven_agent->start();
+    auto first_task_reschedule_info = timer_driven_agent->run(count_proc.get(), context, factory);
+    CHECK(!first_task_reschedule_info.finished_);
+    CHECK(first_task_reschedule_info.wait_time_ == 1250ms);
+    CHECK(count_proc->getNumberOfTriggers() == 1);
+
+    auto second_task_reschedule_info = timer_driven_agent->run(count_proc.get(), context, factory);
+
+    CHECK(!second_task_reschedule_info.finished_);
+    CHECK(second_task_reschedule_info.wait_time_ == 1250ms);
+    CHECK(count_proc->getNumberOfTriggers() == 2);
+  }
+
+  SECTION("Event Driven") {
+    auto event_driven_agent = std::make_shared<EventDrivenSchedulingAgent>(gsl::make_not_null(controller_services_provider_.get()), test_repo, test_repo, content_repo, configuration, thread_pool);
+    event_driven_agent->start();
+    auto first_task_reschedule_info = event_driven_agent->run(count_proc.get(), context, factory);
+    CHECK(!first_task_reschedule_info.finished_);
+    CHECK(first_task_reschedule_info.wait_time_ == 0ms);
+    auto count_num_after_one_schedule = count_proc->getNumberOfTriggers();
+    CHECK(count_num_after_one_schedule > 100);
+
+    auto second_task_reschedule_info = event_driven_agent->run(count_proc.get(), context, factory);
+    CHECK(!second_task_reschedule_info.finished_);
+    CHECK(second_task_reschedule_info.wait_time_ == 0ms);
+    auto count_num_after_two_schedule = count_proc->getNumberOfTriggers();
+    CHECK(count_num_after_two_schedule > count_num_after_one_schedule+100);
+  }
+
+  SECTION("Cron Driven every year") {
+    count_proc->setCronPeriod("0 0 0 1 1/12 ?");
+    auto cron_driven_agent = std::make_shared<CronDrivenSchedulingAgent>(gsl::make_not_null(controller_services_provider_.get()), test_repo, test_repo, content_repo, configuration, thread_pool);
+    cron_driven_agent->start();
+    auto first_task_reschedule_info = cron_driven_agent->run(count_proc.get(), context, factory);
+    CHECK(!first_task_reschedule_info.finished_);
+    auto next_run_time_point = std::chrono::round<std::chrono::years>(std::chrono::system_clock::now() + first_task_reschedule_info.wait_time_);
+    CHECK(next_run_time_point == std::chrono::ceil<std::chrono::years>(std::chrono::system_clock::now()));
+    CHECK(count_proc->getNumberOfTriggers() == 0);
+
+    auto second_task_reschedule_info = cron_driven_agent->run(count_proc.get(), context, factory);
+    CHECK(!second_task_reschedule_info.finished_);
+    next_run_time_point = std::chrono::round<std::chrono::years>(std::chrono::system_clock::now() + first_task_reschedule_info.wait_time_);
+    CHECK(next_run_time_point == std::chrono::ceil<std::chrono::years>(std::chrono::system_clock::now()));
+    CHECK(count_proc->getNumberOfTriggers() == 0);
+  }
+
+  SECTION("Cron Driven every sec") {
+    count_proc->setCronPeriod("* * * * * *");
+    auto cron_driven_agent = std::make_shared<CronDrivenSchedulingAgent>(gsl::make_not_null(controller_services_provider_.get()), test_repo, test_repo, content_repo, configuration, thread_pool);
+    cron_driven_agent->start();
+    auto first_task_reschedule_info = cron_driven_agent->run(count_proc.get(), context, factory);
+    CHECK(!first_task_reschedule_info.finished_);
+    CHECK(first_task_reschedule_info.wait_time_ <= 1s);
+    CHECK(count_proc->getNumberOfTriggers() == 0);

Review Comment:
   isn't there some chance that this will get triggered, if the test runs at the top of a second?



##########
libminifi/test/unit/CronTests.cpp:
##########
@@ -0,0 +1,641 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+#include <string>
+
+#include "../Catch.h"
+#include "utils/Cron.h"
+#include "date/date.h"
+#include "date/tz.h"
+
+using std::chrono::system_clock;
+using std::chrono::seconds;
+using org::apache::nifi::minifi::utils::Cron;
+
+
+void checkNext(const std::string& expr, const date::zoned_time<seconds>& from, const date::zoned_time<seconds>& next) {
+  auto cron_expression = Cron(expr);
+  auto next_trigger = cron_expression.calculateNextTrigger(from.get_local_time());
+  CHECK(next_trigger == next.get_local_time());
+}
+
+
+TEST_CASE("Cron expression ctor tests", "[cron]") {
+  REQUIRE_THROWS(Cron("1600 ms"));
+  REQUIRE_THROWS(Cron("foo"));
+  REQUIRE_THROWS(Cron("61 0 0 * * *"));
+  REQUIRE_THROWS(Cron("0 61 0 * * *"));
+  REQUIRE_THROWS(Cron("0 0 24 * * *"));
+  REQUIRE_THROWS(Cron("0 0 0 32 * *"));
+  REQUIRE_THROWS(Cron("0 0 0 32 * *"));
+
+  // Number of fields must be 6 or 7
+  REQUIRE_THROWS(Cron("* * * * *"));
+  REQUIRE_NOTHROW(Cron("* * * * * *"));
+  REQUIRE_NOTHROW(Cron("* * * * * * *"));
+  REQUIRE_THROWS(Cron("* * * * * * * *"));
+
+  // LW can only be used in 4th field
+  REQUIRE_THROWS(Cron("LW * * * * * *"));
+  REQUIRE_THROWS(Cron("* LW * * * * *"));
+  REQUIRE_THROWS(Cron("* * LW * * * *"));
+  REQUIRE_NOTHROW(Cron("* * * LW * * *"));
+  REQUIRE_THROWS(Cron("* * * * LW * *"));
+  REQUIRE_THROWS(Cron("* * * * * LW *"));
+  REQUIRE_THROWS(Cron("* * * * * * LW"));
+
+  // n#m can only be used in 6th field
+  REQUIRE_THROWS(Cron("2#1 * * * * * *"));
+  REQUIRE_THROWS(Cron("* 2#1 * * * * *"));
+  REQUIRE_THROWS(Cron("* * 2#1 * * * *"));
+  REQUIRE_THROWS(Cron("* * * 2#1 * * *"));
+  REQUIRE_THROWS(Cron("* * * * 2#1 * *"));
+  REQUIRE_NOTHROW(Cron("* * * * * 2#1 *"));
+  REQUIRE_THROWS(Cron("* * * * * * 2#1"));
+
+  // L can only be used in 4th, 5th, 6th fields
+  REQUIRE_THROWS(Cron("L * * * * * *"));
+  REQUIRE_THROWS(Cron("* L * * * * *"));
+  REQUIRE_THROWS(Cron("* * L * * * *"));
+  REQUIRE_NOTHROW(Cron("* * * L * * *"));
+  REQUIRE_THROWS(Cron("* * * * L * *"));
+  REQUIRE_NOTHROW(Cron("* * * * * L *"));
+  REQUIRE_THROWS(Cron("* * * * * * L"));
+
+  REQUIRE_NOTHROW(Cron("0 0 12 * * ?"));
+  REQUIRE_NOTHROW(Cron("0 15 10 ? * *"));
+  REQUIRE_NOTHROW(Cron("0 15 10 * * ?"));
+  REQUIRE_NOTHROW(Cron("0 15 10 * * ? *"));
+  REQUIRE_NOTHROW(Cron("0 15 10 * * ? 2005"));
+  REQUIRE_NOTHROW(Cron("0 * 14 * * ?"));
+  REQUIRE_NOTHROW(Cron("0 0/5 14 * * ?"));
+  REQUIRE_NOTHROW(Cron("0 0/5 14,18 * * ?"));
+  REQUIRE_NOTHROW(Cron("0 0-5 14 * * ?"));
+  REQUIRE_NOTHROW(Cron("0 10,44 14 ? 3 WED"));
+  REQUIRE_NOTHROW(Cron("0 15 10 ? * MON-FRI"));
+  REQUIRE_NOTHROW(Cron("0 15 10 15 * ?"));
+  REQUIRE_NOTHROW(Cron("0 15 10 L * ?"));
+  REQUIRE_NOTHROW(Cron("0 15 10 L-2 * ?"));
+  REQUIRE_NOTHROW(Cron("0 15 10 ? * 6L"));
+  REQUIRE_NOTHROW(Cron("0 15 10 ? * 6L"));
+  REQUIRE_NOTHROW(Cron("0 15 10 ? * 6L 2002-2005"));
+  REQUIRE_NOTHROW(Cron("0 15 10 ? * 6#3"));
+  REQUIRE_NOTHROW(Cron("0 0 12 1/5 * ?"));
+  REQUIRE_NOTHROW(Cron("0 11 11 11 11 ?"));
+}
+
+TEST_CASE("Cron allowed nonnumerical inputs", "[cron]") {
+  REQUIRE_NOTHROW(Cron("* * * * Jan,fEb,MAR,Apr,May,jun,Jul,Aug,Sep,Oct,Nov,Dec * *"));
+  REQUIRE_NOTHROW(Cron("* * * * * Mon,tUe,WeD,Thu,Fri,SAT,Sun *"));
+}
+
+TEST_CASE("Cron::calculateNextTrigger", "[cron]") {
+  using date::sys_days;
+  using namespace date::literals;
+  using namespace std::literals::chrono_literals;
+#ifdef WIN32
+  date::set_install(TZ_DATA_DIR);
+#endif
+
+  checkNext("0/15 * 1-4 * * ?",
+            sys_days(2012_y / 07 / 01) + 9h + 53min + 50s,
+            sys_days(2012_y / 07 / 02) + 01h + 00min + 00s);
+  checkNext("0/15 * 1-4 * * ? *",
+            sys_days(2012_y / 07 / 01) + 9h + 53min + 50s,
+            sys_days(2012_y / 07 / 02) + 01h + 00min + 00s);
+  checkNext("0/15 * 1-4 * * ?",
+            sys_days(2012_y / 07 / 01) + 9h + 53min + 00s,
+            sys_days(2012_y / 07 / 02) + 01h + 00min + 00s);
+  checkNext("*/15 * 1-4 * * ?",
+            sys_days(2012_y / 07 / 01) + 9h + 53min + 50s,
+            sys_days(2012_y / 07 / 02) + 01h + 00min + 00s);
+  checkNext("*/15 * 1-4 * * ? *",
+            sys_days(2012_y / 07 / 01) + 9h + 53min + 50s,
+            sys_days(2012_y / 07 / 02) + 01h + 00min + 00s);
+  checkNext("*/15 * 1-4 * * ?",
+            sys_days(2012_y / 07 / 01) + 9h + 53min + 00s,
+            sys_days(2012_y / 07 / 02) + 01h + 00min + 00s);
+  checkNext("0 0/2 1-4 * * ?",
+            sys_days(2012_y / 07 / 01) + 9h + 00min + 00s,
+            sys_days(2012_y / 07 / 02) + 01h + 00min + 00s);
+  checkNext("* * * * * ?",
+            sys_days(2012_y / 07 / 01) + 9h + 00min + 00s,
+            sys_days(2012_y / 07 / 01) + 9h + 00min + 01s);
+  checkNext("* * * * * ?",
+            sys_days(2012_y / 12 / 01) + 9h + 00min + 58s,
+            sys_days(2012_y / 12 / 01) + 9h + 00min + 59s);
+  checkNext("10 * * * * ?",
+            sys_days(2012_y / 12 / 01) + 9h + 42min + 9s,
+            sys_days(2012_y / 12 / 01) + 9h + 42min + 10s);
+  checkNext("11 * * * * ?",
+            sys_days(2012_y / 12 / 01) + 9h + 42min + 10s,
+            sys_days(2012_y / 12 / 01) + 9h + 42min + 11s);
+  checkNext("10 * * * * ?",
+            sys_days(2012_y / 12 / 01) + 9h + 42min + 10s,
+            sys_days(2012_y / 12 / 01) + 9h + 43min + 10s);
+  checkNext("10-15 * * * * ?",
+            sys_days(2012_y / 12 / 01) + 9h + 42min + 9s,
+            sys_days(2012_y / 12 / 01) + 9h + 42min + 10s);
+  checkNext("10-15 * * * * ?",
+            sys_days(2012_y / 12 / 01) + 21h + 42min + 14s,
+            sys_days(2012_y / 12 / 01) + 21h + 42min + 15s);
+  checkNext("0 * * * * ?",
+            sys_days(2012_y / 12 / 01) + 21h + 10min + 42s,
+            sys_days(2012_y / 12 / 01) + 21h + 11min + 00s);
+  checkNext("0 * * * * ?",
+            sys_days(2012_y / 12 / 01) + 21h + 11min + 00s,
+            sys_days(2012_y / 12 / 01) + 21h + 12min + 00s);
+  checkNext("0 11 * * * ?",
+            sys_days(2012_y / 12 / 01) + 21h + 10min + 42s,
+            sys_days(2012_y / 12 / 01) + 21h + 11min + 00s);
+  checkNext("0 10 * * * ?",
+            sys_days(2012_y / 12 / 01) + 21h + 11min + 00s,
+            sys_days(2012_y / 12 / 01) + 22h + 10min + 00s);
+  checkNext("0 0 * * * ?",
+            sys_days(2012_y / 9 / 30) + 11h + 01min + 00s,
+            sys_days(2012_y / 9 / 30) + 12h + 00min + 00s);
+  checkNext("0 0 * * * ?",
+            sys_days(2012_y / 9 / 30) + 12h + 00min + 00s,
+            sys_days(2012_y / 9 / 30) + 13h + 00min + 00s);
+  checkNext("0 0 * * * ?",
+            sys_days(2012_y / 9 / 10) + 23h + 01min + 00s,
+            sys_days(2012_y / 9 / 11) + 00h + 00min + 00s);
+  checkNext("0 0 * * * ?",
+            sys_days(2012_y / 9 / 11) + 00h + 00min + 00s,
+            sys_days(2012_y / 9 / 11) + 01h + 00min + 00s);
+  checkNext("0 0 0 * * ?",
+            sys_days(2012_y / 9 / 01) + 14h + 42min + 43s,
+            sys_days(2012_y / 9 / 02) + 00h + 00min + 00s);
+  checkNext("0 0 0 * * ?",
+            sys_days(2012_y / 9 / 02) + 00h + 00min + 00s,
+            sys_days(2012_y / 9 / 03) + 00h + 00min + 00s);
+  checkNext("* * * 10 * ?",
+            sys_days(2012_y / 10 / 9) + 15h + 12min + 42s,
+            sys_days(2012_y / 10 / 10) + 00h + 00min + 00s);
+  checkNext("* * * 10 * ?",
+            sys_days(2012_y / 10 / 11) + 15h + 12min + 42s,
+            sys_days(2012_y / 11 / 10) + 00h + 00min + 00s);
+  checkNext("0 0 0 * * ? 2020",
+            sys_days(2012_y / 9 / 30) + 15h + 12min + 42s,
+            sys_days(2020_y / 01 / 01) + 00h + 00min + 00s);
+  checkNext("0 0 0 * * ?",
+            sys_days(2012_y / 9 / 30) + 15h + 12min + 42s,
+            sys_days(2012_y / 10 / 01) + 00h + 00min + 00s);
+  checkNext("0 0 0 * * ?",
+            sys_days(2012_y / 10 / 01) + 00h + 00min + 00s,
+            sys_days(2012_y / 10 / 02) + 00h + 00min + 00s);
+  checkNext("0 0 0 * * ?",
+            sys_days(2012_y / 8 / 30) + 15h + 12min + 42s,
+            sys_days(2012_y / 8 / 31) + 00h + 00min + 00s);
+  checkNext("0 0 0 * * ?",
+            sys_days(2012_y / 8 / 31) + 00h + 00min + 00s,
+            sys_days(2012_y / 9 / 01) + 00h + 00min + 00s);
+  checkNext("0 0 0 * * ?",
+            sys_days(2012_y / 10 / 30) + 15h + 12min + 42s,
+            sys_days(2012_y / 10 / 31) + 00h + 00min + 00s);
+  checkNext("0 0 0 * * ?",
+            sys_days(2012_y / 10 / 31) + 00h + 00min + 00s,
+            sys_days(2012_y / 11 / 01) + 00h + 00min + 00s);
+  checkNext("0 0 0 1 * ?",
+            sys_days(2012_y / 10 / 30) + 15h + 12min + 42s,
+            sys_days(2012_y / 11 / 01) + 00h + 00min + 00s);
+  checkNext("0 0 0 1 * ?",
+            sys_days(2012_y / 11 / 01) + 00h + 00min + 00s,
+            sys_days(2012_y / 12 / 01) + 00h + 00min + 00s);
+  checkNext("0 0 0 1 * ?",
+            sys_days(2010_y / 12 / 31) + 15h + 12min + 42s,
+            sys_days(2011_y / 01 / 01) + 00h + 00min + 00s);
+  checkNext("0 0 0 1 * ?",
+            sys_days(2011_y / 01 / 01) + 00h + 00min + 00s,
+            sys_days(2011_y / 02 / 01) + 00h + 00min + 00s);
+  checkNext("0 0 0 31 * ?",
+            sys_days(2011_y / 10 / 30) + 15h + 12min + 42s,
+            sys_days(2011_y / 10 / 31) + 00h + 00min + 00s);
+  checkNext("0 0 0 1 * ?",
+            sys_days(2011_y / 10 / 30) + 15h + 12min + 42s,
+            sys_days(2011_y / 11 / 01) + 00h + 00min + 00s);
+  checkNext("* * * ? * 2",
+            sys_days(2010_y / 10 / 25) + 15h + 12min + 42s,
+            sys_days(2010_y / 10 / 25) + 15h + 12min + 43s);
+  checkNext("* * * ? * 2",
+            sys_days(2010_y / 10 / 20) + 15h + 12min + 42s,
+            sys_days(2010_y / 10 / 25) + 00h + 00min + 00s);
+  checkNext("* * * ? * 2",
+            sys_days(2010_y / 10 / 27) + 15h + 12min + 42s,
+            sys_days(2010_y / 11 / 01) + 00h + 00min + 00s);
+  checkNext("55 5 * * * ?",
+            sys_days(2010_y / 10 / 27) + 15h + 04min + 54s,
+            sys_days(2010_y / 10 / 27) + 15h + 05min + 55s);
+  checkNext("55 5 * * * ?",
+            sys_days(2010_y / 10 / 27) + 15h + 05min + 55s,
+            sys_days(2010_y / 10 / 27) + 16h + 05min + 55s);
+  checkNext("55 * 10 * * ?",
+            sys_days(2010_y / 10 / 27) + 9h + 04min + 54s,
+            sys_days(2010_y / 10 / 27) + 10h + 00min + 55s);
+  checkNext("55 * 10 * * ?",
+            sys_days(2010_y / 10 / 27) + 10h + 00min + 55s,
+            sys_days(2010_y / 10 / 27) + 10h + 01min + 55s);
+  checkNext("* 5 10 * * ?",
+            sys_days(2010_y / 10 / 27) + 9h + 04min + 55s,
+            sys_days(2010_y / 10 / 27) + 10h + 05min + 00s);
+  checkNext("* 5 10 * * ?",
+            sys_days(2010_y / 10 / 27) + 10h + 05min + 00s,
+            sys_days(2010_y / 10 / 27) + 10h + 05min + 01s);
+  checkNext("55 * * 3 * ?",
+            sys_days(2010_y / 10 / 02) + 10h + 05min + 54s,
+            sys_days(2010_y / 10 / 03) + 00h + 00min + 55s);
+  checkNext("55 * * 3 * ?",
+            sys_days(2010_y / 10 / 03) + 00h + 00min + 55s,
+            sys_days(2010_y / 10 / 03) + 00h + 01min + 55s);
+  checkNext("* * * 3 11 ?",
+            sys_days(2010_y / 10 / 02) + 14h + 42min + 55s,
+            sys_days(2010_y / 11 / 03) + 00h + 00min + 00s);
+  checkNext("* * * 3 11 ?",
+            sys_days(2010_y / 11 / 03) + 00h + 00min + 00s,
+            sys_days(2010_y / 11 / 03) + 00h + 00min + 01s);
+  checkNext("0 0 0 29 2 ?",
+            sys_days(2007_y / 02 / 10) + 14h + 42min + 55s,
+            sys_days(2008_y / 02 / 29) + 00h + 00min + 00s);
+  checkNext("0 0 0 29 2 ?",
+            sys_days(2008_y / 02 / 29) + 00h + 00min + 00s,
+            sys_days(2012_y / 02 / 29) + 00h + 00min + 00s);
+  checkNext("0 0 7 ? * Mon-Fri",
+            sys_days(2009_y / 9 / 26) + 00h + 42min + 55s,
+            sys_days(2009_y / 9 / 28) + 07h + 00min + 00s);
+  checkNext("0 0 7 ? * Mon-Fri",
+            sys_days(2009_y / 9 / 26) + 00h + 42min + 55s,
+            sys_days(2009_y / 9 / 28) + 07h + 00min + 00s);
+  checkNext("0 0 7 ? * Mon,Tue,Wed,Thu,Fri",
+            sys_days(2009_y / 9 / 28) + 07h + 00min + 00s,
+            sys_days(2009_y / 9 / 29) + 07h + 00min + 00s);
+  checkNext("0 30 23 30 1/3 ?",
+            sys_days(2010_y / 12 / 30) + 00h + 00min + 00s,
+            sys_days(2011_y / 01 / 30) + 23h + 30min + 00s);
+  checkNext("0 30 23 30 1/3 ?",
+            sys_days(2011_y / 01 / 30) + 23h + 30min + 00s,
+            sys_days(2011_y / 04 / 30) + 23h + 30min + 00s);
+  checkNext("0 30 23 30 1/3 ?",
+            sys_days(2011_y / 04 / 30) + 23h + 30min + 00s,
+            sys_days(2011_y / 07 / 30) + 23h + 30min + 00s);
+
+  checkNext("0 0 0 LW * ? *",
+            sys_days(2022_y / 02 / 27) + 02h + 00min + 00s,
+            sys_days(2022_y / 02 / 28) + 00h + 00min + 00s);
+  checkNext("0 0 0 LW * ? *",
+            sys_days(2024_y / 02 / 27) + 02h + 00min + 00s,
+            sys_days(2024_y / 02 / 29) + 00h + 00min + 00s);
+  checkNext("0 0 0 LW * ? *",
+            sys_days(2027_y / 02 / 27) + 02h + 00min + 00s,
+            sys_days(2027_y / 03 / 31) + 00h + 00min + 00s);
+  checkNext("0 0 0 ? * 3#1 *",
+            sys_days(2022_y / 05 / 04) + 00h + 00min + 00s,
+            sys_days(2022_y / 06 / 07) + 00h + 00min + 00s);
+  checkNext("0 0 0 ? * 3#2 *",
+            sys_days(2022_y / 05 / 04) + 00h + 00min + 00s,
+            sys_days(2022_y / 05 / 10) + 00h + 00min + 00s);
+  checkNext("0 0 0 ? * 3#3 *",
+            sys_days(2022_y / 05 / 04) + 00h + 00min + 00s,
+            sys_days(2022_y / 05 / 17) + 00h + 00min + 00s);
+  checkNext("0 0 0 ? * 3#4 *",
+            sys_days(2022_y / 05 / 04) + 00h + 00min + 00s,
+            sys_days(2022_y / 05 / 24) + 00h + 00min + 00s);
+  checkNext("0 0 0 ? * 3#5 *",
+            sys_days(2022_y / 05 / 04) + 00h + 00min + 00s,
+            sys_days(2022_y / 05 / 31) + 00h + 00min + 00s);
+  checkNext("0 0 0 L * ? *",
+            sys_days(2022_y / 01 / 04) + 00h + 00min + 00s,
+            sys_days(2022_y / 01 / 31) + 00h + 00min + 00s);
+  checkNext("0 0 0 L * ? *",
+            sys_days(2022_y / 02 / 04) + 00h + 00min + 00s,
+            sys_days(2022_y / 02 / 28) + 00h + 00min + 00s);
+  checkNext("0 0 0 L * ? *",
+            sys_days(2024_y / 02 / 04) + 00h + 00min + 00s,
+            sys_days(2024_y / 02 / 29) + 00h + 00min + 00s);
+  checkNext("0 0 0 L * ? *",
+            sys_days(2022_y / 03 / 04) + 00h + 00min + 00s,
+            sys_days(2022_y / 03 / 31) + 00h + 00min + 00s);
+  checkNext("0 0 0 L * ? *",
+            sys_days(2022_y / 04 / 04) + 00h + 00min + 00s,
+            sys_days(2022_y / 04 / 30) + 00h + 00min + 00s);
+  checkNext("0 0 0 L * ? *",
+            sys_days(2022_y / 05 / 31) + 00h + 00min + 00s,
+            sys_days(2022_y / 06 / 30) + 00h + 00min + 00s);
+  checkNext("0 0 0 ? * L *",
+            sys_days(2022_y / 01 / 07) + 00h + 00min + 00s,
+            sys_days(2022_y / 01 / 8) + 00h + 00min + 00s);
+  checkNext("0 0 0 ? * L *",
+            sys_days(2022_y / 02 / 04) + 00h + 00min + 00s,
+            sys_days(2022_y / 02 / 05) + 00h + 00min + 00s);
+  checkNext("0 0 0 ? * L *",
+            sys_days(2024_y / 02 / 04) + 00h + 00min + 00s,
+            sys_days(2024_y / 02 / 10) + 00h + 00min + 00s);
+  checkNext("0 0 0 ? * L *",
+            sys_days(2022_y / 03 / 04) + 00h + 00min + 00s,
+            sys_days(2022_y / 03 / 05) + 00h + 00min + 00s);
+  checkNext("0 0 0 ? * L *",
+            sys_days(2022_y / 04 / 04) + 00h + 00min + 00s,
+            sys_days(2022_y / 04 / 9) + 00h + 00min + 00s);
+  checkNext("0 0 0 ? * L *",
+            sys_days(2022_y / 05 / 28) + 00h + 00min + 00s,
+            sys_days(2022_y / 06 / 04) + 00h + 00min + 00s);
+  checkNext("0 0 0 1W * ? *",
+            sys_days(2022_y / 05 / 01) + 00h + 00min + 00s,
+            sys_days(2022_y / 05 / 02) + 00h + 00min + 00s);
+  checkNext("0 0 0 4W * ? *",
+            sys_days(2022_y / 05 / 01) + 00h + 00min + 00s,
+            sys_days(2022_y / 05 / 04) + 00h + 00min + 00s);
+  checkNext("0 0 0 14W * ? *",
+            sys_days(2022_y / 05 / 01) + 00h + 00min + 00s,
+            sys_days(2022_y / 05 / 13) + 00h + 00min + 00s);
+  checkNext("0 0 0 15W * ? *",
+            sys_days(2022_y / 05 / 01) + 00h + 00min + 00s,
+            sys_days(2022_y / 05 / 16) + 00h + 00min + 00s);
+  checkNext("0 0 0 31W * ? *",
+            sys_days(2022_y / 02 / 01) + 00h + 00min + 00s,
+            sys_days(2022_y / 03 / 31) + 00h + 00min + 00s);
+}

Review Comment:
   Can you add
   ```
     checkNext("0 0 0 1W * ? *",
               sys_days(2021_y / 12 / 15) + 00h + 00min + 00s,
               sys_days(2022_y / 01 / 03) + 00h + 00min + 00s);
     checkNext("0 0 0 31W * ? *",
               sys_days(2022_y / 07 / 15) + 00h + 00min + 00s,
               sys_days(2022_y / 07 / 29) + 00h + 00min + 00s);
   ```
   please?  I think these test cases would fail.



##########
libminifi/src/utils/Cron.cpp:
##########
@@ -0,0 +1,483 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include "utils/Cron.h"
+#include "utils/TimeUtil.h"
+#include "utils/StringUtils.h"
+#include "date/date.h"
+
+using namespace std::literals::chrono_literals;
+
+using std::chrono::seconds;
+using std::chrono::minutes;
+using std::chrono::hours;
+using std::chrono::days;
+
+// TODO(C++20): move to std::chrono when calendar is fully supported
+using date::local_seconds;
+using date::day;
+using date::weekday;
+using date::month;
+using date::year;
+using date::year_month_day;
+using date::last;
+using date::local_days;
+using date::from_stream;
+using date::make_time;
+
+using date::Friday; using date::Saturday; using date::Sunday;
+
+namespace org::apache::nifi::minifi::utils {
+namespace {
+
+bool operator<=(const weekday& lhs, const weekday& rhs) {
+  return lhs.c_encoding() <= rhs.c_encoding();
+}
+
+template <typename FieldType>
+FieldType parse(const std::string&);
+
+template <>
+seconds parse<seconds>(const std::string& second_str) {
+  auto sec_int = std::stoul(second_str);
+  if (sec_int <= 59)
+    return seconds(sec_int);
+  throw BadCronExpression("Invalid second " + second_str);
+}
+
+template <>
+minutes parse<minutes>(const std::string& minute_str) {
+  auto min_int = std::stoul(minute_str);
+  if (min_int <= 59)
+    return minutes(min_int);
+  throw BadCronExpression("Invalid minute " + minute_str);
+}
+
+template <>
+hours parse<hours>(const std::string& hour_str) {
+  auto hour_int = std::stoul(hour_str);
+  if (hour_int <= 23)
+    return hours(hour_int);
+  throw BadCronExpression("Invalid hour " + hour_str);
+}
+
+template <>
+days parse<days>(const std::string& days_str) {
+  return days(std::stoul(days_str));
+}
+
+template <>
+day parse<day>(const std::string& day_str) {
+  auto day_int = std::stoul(day_str);
+  if (day_int >= 1 && day_int <= 31)
+    return day(day_int);
+  throw BadCronExpression("Invalid day " + day_str);
+}
+
+template <>
+month parse<month>(const std::string& month_str) {
+// https://github.com/HowardHinnant/date/issues/550
+// TODO(gcc11): Due to https://gcc.gnu.org/bugzilla/show_bug.cgi?id=78714
+// the month parsing with '%b' is case sensitive in gcc11
+// This has been fixed in gcc12
+#if defined(__GNUC__) && __GNUC__ < 12
+  auto patched_month_str = StringUtils::toLower(month_str);
+  if (!patched_month_str.empty())
+    patched_month_str[0] = std::toupper(patched_month_str[0]);
+  std::stringstream stream(patched_month_str);
+#else
+  std::stringstream stream(month_str);
+#endif
+
+  stream.imbue(std::locale("en_US.UTF-8"));
+  month parsed_month{};
+  if (month_str.size() > 2) {
+    from_stream(stream, "%b", parsed_month);
+    if (parsed_month.ok())
+      return parsed_month;
+  } else {
+    from_stream(stream, "%m", parsed_month);
+    if (parsed_month.ok())
+      return parsed_month;
+  }
+
+  throw BadCronExpression("Invalid month " + month_str);
+}
+
+template <>
+weekday parse<weekday>(const std::string& weekday_str) {
+  std::stringstream stream(weekday_str);
+  stream.imbue(std::locale("en_US.UTF-8"));
+
+  if (weekday_str.size() > 2) {
+    weekday parsed_weekday{};
+    from_stream(stream, "%a", parsed_weekday);
+    if (parsed_weekday.ok())
+      return parsed_weekday;
+  } else {
+    unsigned weekday_num;
+    stream >> weekday_num;
+    if (!stream.bad() && weekday_num < 7)
+      return weekday(weekday_num-1);
+  }
+  throw BadCronExpression("Invalid weekday: " + weekday_str);
+}
+
+template <>
+year parse<year>(const std::string& year_str) {
+  auto year_int = std::stoi(year_str);
+  if (year_int >= 1970 && year_int <= 2099)
+    return year(year_int);
+  throw BadCronExpression("Invalid year: " + year_str);
+}
+
+template <typename FieldType>
+FieldType getFieldType(local_seconds time_point);
+
+template <>
+year getFieldType(local_seconds time_point) {
+  year_month_day year_month_day(floor<days>(time_point));
+  return year_month_day.year();
+}
+
+template <>
+month getFieldType(local_seconds time_point) {
+  year_month_day year_month_day(floor<days>(time_point));
+  return year_month_day.month();
+}
+
+template <>
+day getFieldType(local_seconds time_point) {
+  year_month_day year_month_day(floor<days>(time_point));
+  return year_month_day.day();
+}
+
+template <>
+hours getFieldType(local_seconds time_point) {
+  auto dp = floor<days>(time_point);
+  auto time = make_time(time_point-dp);
+  return time.hours();
+}
+
+template <>
+minutes getFieldType(local_seconds time_point) {
+  auto dp = floor<days>(time_point);
+  auto time = make_time(time_point-dp);
+  return time.minutes();
+}
+
+template <>
+seconds getFieldType(local_seconds time_point) {
+  auto dp = floor<days>(time_point);
+  auto time = make_time(time_point-dp);
+  return time.seconds();
+}
+
+template <>
+weekday getFieldType(local_seconds time_point) {
+  auto dp = floor<days>(time_point);
+  return weekday(dp);
+}
+
+bool isWeekday(year_month_day date) {
+  weekday date_weekday = weekday(local_days(date));
+  return date_weekday != Saturday && date_weekday != Sunday;
+}
+
+template <typename FieldType>
+class SingleValueField : public CronField {
+ public:
+  explicit SingleValueField(FieldType value) : value_(value) {}
+
+  [[nodiscard]] bool isValid(local_seconds time_point) const override {
+    return value_ == getFieldType<FieldType>(time_point);
+  }
+
+ private:
+  FieldType value_;
+};
+
+class NotCheckedField : public CronField {
+ public:
+  NotCheckedField() = default;
+
+  [[nodiscard]] bool isValid(local_seconds) const override { return true; }
+};
+
+class AllValuesField : public CronField {
+ public:
+  AllValuesField() = default;
+
+  [[nodiscard]] bool isValid(local_seconds) const override { return true; }
+};
+
+template <typename FieldType>
+class RangeField : public CronField {
+ public:
+  explicit RangeField(FieldType lower_bound, FieldType upper_bound)
+      : lower_bound_(std::move(lower_bound)),
+        upper_bound_(std::move(upper_bound)) {
+  }
+
+  [[nodiscard]] bool isValid(local_seconds value) const override {
+    return lower_bound_ <= getFieldType<FieldType>(value) && getFieldType<FieldType>(value) <= upper_bound_;
+  }
+
+ private:
+  FieldType lower_bound_;
+  FieldType upper_bound_;
+};
+
+template <typename FieldType>
+class ListField : public CronField {
+ public:
+  explicit ListField(std::vector<FieldType> valid_values) : valid_values_(std::move(valid_values)) {}
+
+  [[nodiscard]] bool isValid(local_seconds value) const override {
+    return std::find(valid_values_.begin(), valid_values_.end(), getFieldType<FieldType>(value)) != valid_values_.end();
+  }
+
+ private:
+  std::vector<FieldType> valid_values_;
+};
+
+template <typename FieldType>
+class IncrementField : public CronField {
+ public:
+  IncrementField(FieldType start, int increment) : start_(start), increment_(increment) {}
+
+  [[nodiscard]] bool isValid(local_seconds value) const override {
+    return (getFieldType<FieldType>(value) - start_).count() % increment_ == 0;
+  }
+
+ private:
+  FieldType start_;
+  int increment_;
+};
+
+class LastNthDayInMonthField : public CronField {
+ public:
+  explicit LastNthDayInMonthField(days offset) : offset_(offset) {}
+
+  [[nodiscard]] bool isValid(local_seconds tp) const override {
+    year_month_day date(floor<days>(tp));
+    auto last_day = date.year()/date.month()/last;
+    auto target_date = local_days(last_day)-offset_;
+    return local_days(date) == target_date;
+  }
+
+ private:
+  days offset_;
+};
+
+class NthWeekdayField : public CronField {
+ public:
+  NthWeekdayField(weekday weekday, uint8_t n) : weekday_(weekday), n_(n) {}
+
+  [[nodiscard]] bool isValid(local_seconds tp) const override {
+    year_month_day date(floor<days>(tp));
+    auto target_date = date.year()/date.month()/(weekday_[n_]);
+    return local_days(date) == local_days(target_date);
+  }
+
+ private:
+  weekday weekday_;
+  uint8_t n_;
+};
+
+class LastWeekDayField : public CronField {
+ public:
+  LastWeekDayField() = default;
+
+  [[nodiscard]] bool isValid(local_seconds value) const override {
+    year_month_day date(floor<days>(value));
+    year_month_day last_day_of_the_month_date = year_month_day(local_days(date.year()/date.month()/last));
+    if (isWeekday(last_day_of_the_month_date))
+      return date == last_day_of_the_month_date;
+    year_month_day last_friday_of_the_month_date = year_month_day(local_days(date.year()/date.month()/Friday[last]));
+    return date == last_friday_of_the_month_date;
+  }
+};
+
+class ClosestWeekdayToX : public CronField {

Review Comment:
   `ClosestWeekdayToTheNthDayOfTheMonth` would be a better name
   
   I would also rename `x_` to `day_number_` or something similar



##########
libminifi/src/CronDrivenSchedulingAgent.cpp:
##########
@@ -20,66 +20,56 @@
 #include "CronDrivenSchedulingAgent.h"
 #include <chrono>
 #include <memory>
-#include <thread>
-#include <iostream>
 #include "core/Processor.h"
 #include "core/ProcessContext.h"
 #include "core/ProcessSessionFactory.h"
-#include "core/Property.h"
 
 using namespace std::literals::chrono_literals;
+using std::chrono::ceil;
+using std::chrono::seconds;
+using std::chrono::milliseconds;
+using std::chrono::time_point_cast;
+using std::chrono::system_clock;
 
-namespace org {
-namespace apache {
-namespace nifi {
-namespace minifi {
+namespace org::apache::nifi::minifi {
 
-utils::TaskRescheduleInfo CronDrivenSchedulingAgent::run(core::Processor* processor, const std::shared_ptr<core::ProcessContext> &processContext,
-                                        const std::shared_ptr<core::ProcessSessionFactory> &sessionFactory) {
+utils::TaskRescheduleInfo CronDrivenSchedulingAgent::run(core::Processor* processor,
+                                                         const std::shared_ptr<core::ProcessContext>& processContext,
+                                                         const std::shared_ptr<core::ProcessSessionFactory>& sessionFactory) {
   if (this->running_ && processor->isRunning()) {
     auto uuid = processor->getUUID();
-    std::chrono::system_clock::time_point result;
-    std::chrono::system_clock::time_point from = std::chrono::system_clock::now();
-    {
-      std::lock_guard<std::mutex> locK(mutex_);
+    auto current_time = date::make_zoned<seconds>(date::current_zone(), time_point_cast<seconds>(system_clock::now()));
+    std::lock_guard<std::mutex> lock(mutex_);
 
-      auto sched_f = schedules_.find(uuid);
-      if (sched_f != std::end(schedules_)) {
-        result = last_exec_[uuid];
-        if (from >= result) {
-          result = sched_f->second.cron_to_next(from);
-          last_exec_[uuid] = result;
-        } else {
-          // we may be woken up a little early so that we can honor our time.
-          // in this case we can return the next time to run with the expectation
-          // that the wakeup mechanism gets more granular.
-          return utils::TaskRescheduleInfo::RetryIn(std::chrono::duration_cast<std::chrono::milliseconds>(result - from));
-        }
-      } else {
-        Bosma::Cron schedule(processor->getCronPeriod());
-        result = schedule.cron_to_next(from);
-        last_exec_[uuid] = result;
-        schedules_.insert(std::make_pair(uuid, schedule));
-      }
-    }
+    if (!schedules_.contains(uuid))
+      schedules_.insert(std::make_pair(uuid, utils::Cron(processor->getCronPeriod())));
+
+    if (!last_exec_.contains(uuid))
+      last_exec_.insert(std::make_pair(uuid, current_time.get_local_time()));

Review Comment:
   minor, but `emplace` is a bit nicer than `insert`:
   ```suggestion
       if (!schedules_.contains(uuid))
         schedules_.emplace(uuid, utils::Cron(processor->getCronPeriod()));
   
       if (!last_exec_.contains(uuid))
         last_exec_.emplace(uuid, current_time.get_local_time());
   ```
   
   also, if computing the values is not much slower than doing an extra lookup, then this is shorter and does the same thing:
   ```suggestion
       schedules_.emplace(uuid, utils::Cron(processor->getCronPeriod()));
       last_exec_.emplace(uuid, current_time.get_local_time());
   ```



##########
libminifi/include/utils/Cron.h:
##########
@@ -0,0 +1,55 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+#pragma once
+
+#include <exception>
+#include <string>
+#include <chrono>
+#include <optional>
+#include <memory>
+#include <utility>
+#include "date/tz.h"
+
+namespace org::apache::nifi::minifi::utils {
+class BadCronExpression : public std::exception {
+ public:
+  explicit BadCronExpression(std::string msg) : msg_(std::move(msg)) {}
+
+  [[nodiscard]] const char* what() const noexcept override { return (msg_.c_str()); }
+
+ private:
+  std::string msg_;
+};
+
+class CronField {
+ public:
+  virtual ~CronField() = default;
+
+  [[nodiscard]] virtual bool isValid(date::local_seconds time_point) const = 0;

Review Comment:
   I think `matches()` would describe better what this does



##########
libminifi/src/utils/Cron.cpp:
##########
@@ -0,0 +1,483 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include "utils/Cron.h"
+#include "utils/TimeUtil.h"
+#include "utils/StringUtils.h"
+#include "date/date.h"
+
+using namespace std::literals::chrono_literals;
+
+using std::chrono::seconds;
+using std::chrono::minutes;
+using std::chrono::hours;
+using std::chrono::days;
+
+// TODO(C++20): move to std::chrono when calendar is fully supported
+using date::local_seconds;
+using date::day;
+using date::weekday;
+using date::month;
+using date::year;
+using date::year_month_day;
+using date::last;
+using date::local_days;
+using date::from_stream;
+using date::make_time;
+
+using date::Friday; using date::Saturday; using date::Sunday;
+
+namespace org::apache::nifi::minifi::utils {
+namespace {
+
+bool operator<=(const weekday& lhs, const weekday& rhs) {
+  return lhs.c_encoding() <= rhs.c_encoding();
+}
+
+template <typename FieldType>
+FieldType parse(const std::string&);
+
+template <>
+seconds parse<seconds>(const std::string& second_str) {
+  auto sec_int = std::stoul(second_str);
+  if (sec_int <= 59)
+    return seconds(sec_int);
+  throw BadCronExpression("Invalid second " + second_str);
+}
+
+template <>
+minutes parse<minutes>(const std::string& minute_str) {
+  auto min_int = std::stoul(minute_str);
+  if (min_int <= 59)
+    return minutes(min_int);
+  throw BadCronExpression("Invalid minute " + minute_str);
+}
+
+template <>
+hours parse<hours>(const std::string& hour_str) {
+  auto hour_int = std::stoul(hour_str);
+  if (hour_int <= 23)
+    return hours(hour_int);
+  throw BadCronExpression("Invalid hour " + hour_str);
+}
+
+template <>
+days parse<days>(const std::string& days_str) {
+  return days(std::stoul(days_str));
+}
+
+template <>
+day parse<day>(const std::string& day_str) {
+  auto day_int = std::stoul(day_str);
+  if (day_int >= 1 && day_int <= 31)
+    return day(day_int);
+  throw BadCronExpression("Invalid day " + day_str);
+}
+
+template <>
+month parse<month>(const std::string& month_str) {
+// https://github.com/HowardHinnant/date/issues/550
+// TODO(gcc11): Due to https://gcc.gnu.org/bugzilla/show_bug.cgi?id=78714
+// the month parsing with '%b' is case sensitive in gcc11
+// This has been fixed in gcc12
+#if defined(__GNUC__) && __GNUC__ < 12
+  auto patched_month_str = StringUtils::toLower(month_str);
+  if (!patched_month_str.empty())
+    patched_month_str[0] = std::toupper(patched_month_str[0]);
+  std::stringstream stream(patched_month_str);
+#else
+  std::stringstream stream(month_str);
+#endif
+
+  stream.imbue(std::locale("en_US.UTF-8"));
+  month parsed_month{};
+  if (month_str.size() > 2) {
+    from_stream(stream, "%b", parsed_month);
+    if (parsed_month.ok())
+      return parsed_month;
+  } else {
+    from_stream(stream, "%m", parsed_month);
+    if (parsed_month.ok())
+      return parsed_month;
+  }
+
+  throw BadCronExpression("Invalid month " + month_str);
+}
+
+template <>
+weekday parse<weekday>(const std::string& weekday_str) {
+  std::stringstream stream(weekday_str);
+  stream.imbue(std::locale("en_US.UTF-8"));
+
+  if (weekday_str.size() > 2) {
+    weekday parsed_weekday{};
+    from_stream(stream, "%a", parsed_weekday);
+    if (parsed_weekday.ok())
+      return parsed_weekday;
+  } else {
+    unsigned weekday_num;
+    stream >> weekday_num;
+    if (!stream.bad() && weekday_num < 7)
+      return weekday(weekday_num-1);
+  }
+  throw BadCronExpression("Invalid weekday: " + weekday_str);
+}
+
+template <>
+year parse<year>(const std::string& year_str) {
+  auto year_int = std::stoi(year_str);
+  if (year_int >= 1970 && year_int <= 2099)
+    return year(year_int);
+  throw BadCronExpression("Invalid year: " + year_str);
+}
+
+template <typename FieldType>
+FieldType getFieldType(local_seconds time_point);
+
+template <>
+year getFieldType(local_seconds time_point) {
+  year_month_day year_month_day(floor<days>(time_point));
+  return year_month_day.year();
+}
+
+template <>
+month getFieldType(local_seconds time_point) {
+  year_month_day year_month_day(floor<days>(time_point));
+  return year_month_day.month();
+}
+
+template <>
+day getFieldType(local_seconds time_point) {
+  year_month_day year_month_day(floor<days>(time_point));
+  return year_month_day.day();
+}
+
+template <>
+hours getFieldType(local_seconds time_point) {
+  auto dp = floor<days>(time_point);
+  auto time = make_time(time_point-dp);
+  return time.hours();
+}
+
+template <>
+minutes getFieldType(local_seconds time_point) {
+  auto dp = floor<days>(time_point);
+  auto time = make_time(time_point-dp);
+  return time.minutes();
+}
+
+template <>
+seconds getFieldType(local_seconds time_point) {
+  auto dp = floor<days>(time_point);
+  auto time = make_time(time_point-dp);
+  return time.seconds();
+}
+
+template <>
+weekday getFieldType(local_seconds time_point) {
+  auto dp = floor<days>(time_point);
+  return weekday(dp);
+}
+
+bool isWeekday(year_month_day date) {
+  weekday date_weekday = weekday(local_days(date));
+  return date_weekday != Saturday && date_weekday != Sunday;
+}
+
+template <typename FieldType>
+class SingleValueField : public CronField {
+ public:
+  explicit SingleValueField(FieldType value) : value_(value) {}
+
+  [[nodiscard]] bool isValid(local_seconds time_point) const override {
+    return value_ == getFieldType<FieldType>(time_point);
+  }
+
+ private:
+  FieldType value_;
+};
+
+class NotCheckedField : public CronField {
+ public:
+  NotCheckedField() = default;
+
+  [[nodiscard]] bool isValid(local_seconds) const override { return true; }
+};
+
+class AllValuesField : public CronField {
+ public:
+  AllValuesField() = default;
+
+  [[nodiscard]] bool isValid(local_seconds) const override { return true; }
+};
+
+template <typename FieldType>
+class RangeField : public CronField {
+ public:
+  explicit RangeField(FieldType lower_bound, FieldType upper_bound)
+      : lower_bound_(std::move(lower_bound)),
+        upper_bound_(std::move(upper_bound)) {
+  }
+
+  [[nodiscard]] bool isValid(local_seconds value) const override {
+    return lower_bound_ <= getFieldType<FieldType>(value) && getFieldType<FieldType>(value) <= upper_bound_;
+  }
+
+ private:
+  FieldType lower_bound_;
+  FieldType upper_bound_;
+};
+
+template <typename FieldType>
+class ListField : public CronField {
+ public:
+  explicit ListField(std::vector<FieldType> valid_values) : valid_values_(std::move(valid_values)) {}
+
+  [[nodiscard]] bool isValid(local_seconds value) const override {
+    return std::find(valid_values_.begin(), valid_values_.end(), getFieldType<FieldType>(value)) != valid_values_.end();
+  }
+
+ private:
+  std::vector<FieldType> valid_values_;
+};
+
+template <typename FieldType>
+class IncrementField : public CronField {
+ public:
+  IncrementField(FieldType start, int increment) : start_(start), increment_(increment) {}
+
+  [[nodiscard]] bool isValid(local_seconds value) const override {
+    return (getFieldType<FieldType>(value) - start_).count() % increment_ == 0;
+  }
+
+ private:
+  FieldType start_;
+  int increment_;
+};
+
+class LastNthDayInMonthField : public CronField {
+ public:
+  explicit LastNthDayInMonthField(days offset) : offset_(offset) {}
+
+  [[nodiscard]] bool isValid(local_seconds tp) const override {
+    year_month_day date(floor<days>(tp));
+    auto last_day = date.year()/date.month()/last;
+    auto target_date = local_days(last_day)-offset_;
+    return local_days(date) == target_date;
+  }
+
+ private:
+  days offset_;
+};
+
+class NthWeekdayField : public CronField {
+ public:
+  NthWeekdayField(weekday weekday, uint8_t n) : weekday_(weekday), n_(n) {}
+
+  [[nodiscard]] bool isValid(local_seconds tp) const override {
+    year_month_day date(floor<days>(tp));
+    auto target_date = date.year()/date.month()/(weekday_[n_]);
+    return local_days(date) == local_days(target_date);
+  }
+
+ private:
+  weekday weekday_;
+  uint8_t n_;
+};
+
+class LastWeekDayField : public CronField {
+ public:
+  LastWeekDayField() = default;
+
+  [[nodiscard]] bool isValid(local_seconds value) const override {
+    year_month_day date(floor<days>(value));
+    year_month_day last_day_of_the_month_date = year_month_day(local_days(date.year()/date.month()/last));
+    if (isWeekday(last_day_of_the_month_date))
+      return date == last_day_of_the_month_date;
+    year_month_day last_friday_of_the_month_date = year_month_day(local_days(date.year()/date.month()/Friday[last]));
+    return date == last_friday_of_the_month_date;
+  }
+};
+
+class ClosestWeekdayToX : public CronField {
+ public:
+  explicit ClosestWeekdayToX(day x) : x_(x) {}
+
+  [[nodiscard]] bool isValid(local_seconds value) const override {
+    year_month_day date(floor<days>(value));
+    year_month_day target_date = year_month_day(local_days(date.year()/date.month()/x_));
+    if (target_date.ok() && isWeekday(target_date))
+      return target_date == date;
+
+    target_date = year_month_day(local_days(date.year()/date.month()/(x_-days(1))));
+    if (target_date.ok() && isWeekday(target_date))
+      return target_date == date;
+
+    target_date = year_month_day(local_days(date.year()/date.month()/(x_+days(1))));
+    if (target_date.ok() && isWeekday(target_date))
+      return target_date == date;
+
+    target_date = year_month_day(local_days(date.year()/date.month()/(x_+days(2))));
+    if (target_date.ok() && isWeekday(target_date))
+      return target_date == date;
+
+    return false;
+  }
+
+ private:
+  day x_;
+};
+
+template <typename FieldType>
+CronField* parseCronField(const std::string& field_str) {
+  try {
+    if (field_str == "*") {
+      return new AllValuesField();

Review Comment:
   this function could return `unique_ptr<CronField>` with minimal code change; I think that would be nicer



##########
libminifi/src/utils/Cron.cpp:
##########
@@ -0,0 +1,483 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include "utils/Cron.h"
+#include "utils/TimeUtil.h"
+#include "utils/StringUtils.h"
+#include "date/date.h"
+
+using namespace std::literals::chrono_literals;
+
+using std::chrono::seconds;
+using std::chrono::minutes;
+using std::chrono::hours;
+using std::chrono::days;
+
+// TODO(C++20): move to std::chrono when calendar is fully supported
+using date::local_seconds;
+using date::day;
+using date::weekday;
+using date::month;
+using date::year;
+using date::year_month_day;
+using date::last;
+using date::local_days;
+using date::from_stream;
+using date::make_time;
+
+using date::Friday; using date::Saturday; using date::Sunday;
+
+namespace org::apache::nifi::minifi::utils {
+namespace {
+
+bool operator<=(const weekday& lhs, const weekday& rhs) {
+  return lhs.c_encoding() <= rhs.c_encoding();
+}
+
+template <typename FieldType>
+FieldType parse(const std::string&);
+
+template <>
+seconds parse<seconds>(const std::string& second_str) {
+  auto sec_int = std::stoul(second_str);
+  if (sec_int <= 59)
+    return seconds(sec_int);
+  throw BadCronExpression("Invalid second " + second_str);
+}
+
+template <>
+minutes parse<minutes>(const std::string& minute_str) {
+  auto min_int = std::stoul(minute_str);
+  if (min_int <= 59)
+    return minutes(min_int);
+  throw BadCronExpression("Invalid minute " + minute_str);
+}
+
+template <>
+hours parse<hours>(const std::string& hour_str) {
+  auto hour_int = std::stoul(hour_str);
+  if (hour_int <= 23)
+    return hours(hour_int);
+  throw BadCronExpression("Invalid hour " + hour_str);
+}
+
+template <>
+days parse<days>(const std::string& days_str) {
+  return days(std::stoul(days_str));
+}
+
+template <>
+day parse<day>(const std::string& day_str) {
+  auto day_int = std::stoul(day_str);
+  if (day_int >= 1 && day_int <= 31)
+    return day(day_int);
+  throw BadCronExpression("Invalid day " + day_str);
+}
+
+template <>
+month parse<month>(const std::string& month_str) {
+// https://github.com/HowardHinnant/date/issues/550
+// TODO(gcc11): Due to https://gcc.gnu.org/bugzilla/show_bug.cgi?id=78714
+// the month parsing with '%b' is case sensitive in gcc11
+// This has been fixed in gcc12
+#if defined(__GNUC__) && __GNUC__ < 12
+  auto patched_month_str = StringUtils::toLower(month_str);
+  if (!patched_month_str.empty())
+    patched_month_str[0] = std::toupper(patched_month_str[0]);
+  std::stringstream stream(patched_month_str);
+#else
+  std::stringstream stream(month_str);
+#endif
+
+  stream.imbue(std::locale("en_US.UTF-8"));
+  month parsed_month{};
+  if (month_str.size() > 2) {
+    from_stream(stream, "%b", parsed_month);
+    if (parsed_month.ok())
+      return parsed_month;
+  } else {
+    from_stream(stream, "%m", parsed_month);
+    if (parsed_month.ok())
+      return parsed_month;
+  }
+
+  throw BadCronExpression("Invalid month " + month_str);
+}
+
+template <>
+weekday parse<weekday>(const std::string& weekday_str) {
+  std::stringstream stream(weekday_str);
+  stream.imbue(std::locale("en_US.UTF-8"));
+
+  if (weekday_str.size() > 2) {
+    weekday parsed_weekday{};
+    from_stream(stream, "%a", parsed_weekday);
+    if (parsed_weekday.ok())
+      return parsed_weekday;
+  } else {
+    unsigned weekday_num;
+    stream >> weekday_num;
+    if (!stream.bad() && weekday_num < 7)
+      return weekday(weekday_num-1);

Review Comment:
   Why `-1`?  As far as I can see, `cron` and `date` represent the day of the week in the same way: 0 = Sun, 1 = Mon, ..., 6 = Sat, 7 = Sun.



##########
libminifi/test/unit/SchedulingAgentTests.cpp:
##########
@@ -0,0 +1,138 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include "../Catch.h"
+#include "../TestBase.h"
+#include "ProvenanceTestHelper.h"
+#include "utils/TestUtils.h"
+
+using namespace std::literals::chrono_literals;
+
+namespace org::apache::nifi::minifi::testing {
+
+class CountOnTriggersProcessor : public minifi::core::Processor {
+ public:
+  using minifi::core::Processor::Processor;
+
+  void onTrigger(core::ProcessContext*, core::ProcessSession*) override {
+    ++number_of_triggers;
+  }
+
+  size_t getNumberOfTriggers() const { return number_of_triggers; }
+
+ private:
+  std::atomic<size_t> number_of_triggers = 0;
+};
+
+
+TEST_CASE("SchedulingAgentTests", "[SchedulingAgent]") {
+  std::shared_ptr<core::Repository> test_repo = std::make_shared<TestRepository>();
+  std::shared_ptr<core::ContentRepository> content_repo = std::make_shared<core::repository::VolatileContentRepository>();
+  std::shared_ptr<TestRepository> repo = std::static_pointer_cast<TestRepository>(test_repo);
+  std::shared_ptr<minifi::FlowController> controller =
+      std::make_shared<TestFlowController>(test_repo, test_repo, content_repo);
+
+  TestController testController;
+  auto test_plan = testController.createPlan();
+  auto controller_services_ = std::make_shared<minifi::core::controller::ControllerServiceMap>();
+  auto configuration = std::make_shared<minifi::Configure>();
+  auto controller_services_provider_ = std::make_shared<minifi::core::controller::StandardControllerServiceProvider>(controller_services_, nullptr, configuration);
+  utils::ThreadPool<utils::TaskRescheduleInfo> thread_pool;
+  auto count_proc = std::make_shared<CountOnTriggersProcessor>("count_proc");
+  count_proc->incrementActiveTasks();
+  count_proc->setScheduledState(core::RUNNING);
+  auto node = std::make_shared<core::ProcessorNode>(count_proc.get());
+  auto context = std::make_shared<core::ProcessContext>(node, nullptr, repo, repo, content_repo);
+  std::shared_ptr<core::ProcessSessionFactory> factory = std::make_shared<core::ProcessSessionFactory>(context);
+  count_proc->setSchedulingPeriodNano(1250ms);
+#ifdef WIN32
+  utils::dateSetInstall(TZ_DATA_DIR);
+#endif
+
+  SECTION("Timer Driven") {
+    auto timer_driven_agent = std::make_shared<TimerDrivenSchedulingAgent>(gsl::make_not_null(controller_services_provider_.get()), test_repo, test_repo, content_repo, configuration, thread_pool);
+    timer_driven_agent->start();
+    auto first_task_reschedule_info = timer_driven_agent->run(count_proc.get(), context, factory);
+    CHECK(!first_task_reschedule_info.finished_);
+    CHECK(first_task_reschedule_info.wait_time_ == 1250ms);
+    CHECK(count_proc->getNumberOfTriggers() == 1);
+
+    auto second_task_reschedule_info = timer_driven_agent->run(count_proc.get(), context, factory);
+
+    CHECK(!second_task_reschedule_info.finished_);
+    CHECK(second_task_reschedule_info.wait_time_ == 1250ms);
+    CHECK(count_proc->getNumberOfTriggers() == 2);
+  }
+
+  SECTION("Event Driven") {
+    auto event_driven_agent = std::make_shared<EventDrivenSchedulingAgent>(gsl::make_not_null(controller_services_provider_.get()), test_repo, test_repo, content_repo, configuration, thread_pool);
+    event_driven_agent->start();
+    auto first_task_reschedule_info = event_driven_agent->run(count_proc.get(), context, factory);
+    CHECK(!first_task_reschedule_info.finished_);
+    CHECK(first_task_reschedule_info.wait_time_ == 0ms);
+    auto count_num_after_one_schedule = count_proc->getNumberOfTriggers();
+    CHECK(count_num_after_one_schedule > 100);
+
+    auto second_task_reschedule_info = event_driven_agent->run(count_proc.get(), context, factory);
+    CHECK(!second_task_reschedule_info.finished_);
+    CHECK(second_task_reschedule_info.wait_time_ == 0ms);
+    auto count_num_after_two_schedule = count_proc->getNumberOfTriggers();
+    CHECK(count_num_after_two_schedule > count_num_after_one_schedule+100);
+  }
+
+  SECTION("Cron Driven every year") {
+    count_proc->setCronPeriod("0 0 0 1 1/12 ?");
+    auto cron_driven_agent = std::make_shared<CronDrivenSchedulingAgent>(gsl::make_not_null(controller_services_provider_.get()), test_repo, test_repo, content_repo, configuration, thread_pool);
+    cron_driven_agent->start();
+    auto first_task_reschedule_info = cron_driven_agent->run(count_proc.get(), context, factory);
+    CHECK(!first_task_reschedule_info.finished_);
+    auto next_run_time_point = std::chrono::round<std::chrono::years>(std::chrono::system_clock::now() + first_task_reschedule_info.wait_time_);
+    CHECK(next_run_time_point == std::chrono::ceil<std::chrono::years>(std::chrono::system_clock::now()));
+    CHECK(count_proc->getNumberOfTriggers() == 0);
+
+    auto second_task_reschedule_info = cron_driven_agent->run(count_proc.get(), context, factory);
+    CHECK(!second_task_reschedule_info.finished_);
+    next_run_time_point = std::chrono::round<std::chrono::years>(std::chrono::system_clock::now() + first_task_reschedule_info.wait_time_);
+    CHECK(next_run_time_point == std::chrono::ceil<std::chrono::years>(std::chrono::system_clock::now()));
+    CHECK(count_proc->getNumberOfTriggers() == 0);

Review Comment:
   I guess there is a tiny chance this will get triggered, if the test is run at 00:00:00 on 1st January -- which is not very likely, of course.



##########
libminifi/test/unit/SchedulingAgentTests.cpp:
##########
@@ -0,0 +1,138 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include "../Catch.h"
+#include "../TestBase.h"
+#include "ProvenanceTestHelper.h"
+#include "utils/TestUtils.h"
+
+using namespace std::literals::chrono_literals;
+
+namespace org::apache::nifi::minifi::testing {
+
+class CountOnTriggersProcessor : public minifi::core::Processor {
+ public:
+  using minifi::core::Processor::Processor;
+
+  void onTrigger(core::ProcessContext*, core::ProcessSession*) override {
+    ++number_of_triggers;
+  }
+
+  size_t getNumberOfTriggers() const { return number_of_triggers; }
+
+ private:
+  std::atomic<size_t> number_of_triggers = 0;
+};
+
+
+TEST_CASE("SchedulingAgentTests", "[SchedulingAgent]") {
+  std::shared_ptr<core::Repository> test_repo = std::make_shared<TestRepository>();
+  std::shared_ptr<core::ContentRepository> content_repo = std::make_shared<core::repository::VolatileContentRepository>();
+  std::shared_ptr<TestRepository> repo = std::static_pointer_cast<TestRepository>(test_repo);
+  std::shared_ptr<minifi::FlowController> controller =
+      std::make_shared<TestFlowController>(test_repo, test_repo, content_repo);
+
+  TestController testController;
+  auto test_plan = testController.createPlan();
+  auto controller_services_ = std::make_shared<minifi::core::controller::ControllerServiceMap>();
+  auto configuration = std::make_shared<minifi::Configure>();
+  auto controller_services_provider_ = std::make_shared<minifi::core::controller::StandardControllerServiceProvider>(controller_services_, nullptr, configuration);
+  utils::ThreadPool<utils::TaskRescheduleInfo> thread_pool;
+  auto count_proc = std::make_shared<CountOnTriggersProcessor>("count_proc");
+  count_proc->incrementActiveTasks();
+  count_proc->setScheduledState(core::RUNNING);
+  auto node = std::make_shared<core::ProcessorNode>(count_proc.get());
+  auto context = std::make_shared<core::ProcessContext>(node, nullptr, repo, repo, content_repo);
+  std::shared_ptr<core::ProcessSessionFactory> factory = std::make_shared<core::ProcessSessionFactory>(context);
+  count_proc->setSchedulingPeriodNano(1250ms);
+#ifdef WIN32
+  utils::dateSetInstall(TZ_DATA_DIR);
+#endif
+
+  SECTION("Timer Driven") {
+    auto timer_driven_agent = std::make_shared<TimerDrivenSchedulingAgent>(gsl::make_not_null(controller_services_provider_.get()), test_repo, test_repo, content_repo, configuration, thread_pool);
+    timer_driven_agent->start();
+    auto first_task_reschedule_info = timer_driven_agent->run(count_proc.get(), context, factory);
+    CHECK(!first_task_reschedule_info.finished_);
+    CHECK(first_task_reschedule_info.wait_time_ == 1250ms);
+    CHECK(count_proc->getNumberOfTriggers() == 1);
+
+    auto second_task_reschedule_info = timer_driven_agent->run(count_proc.get(), context, factory);
+
+    CHECK(!second_task_reschedule_info.finished_);
+    CHECK(second_task_reschedule_info.wait_time_ == 1250ms);
+    CHECK(count_proc->getNumberOfTriggers() == 2);
+  }
+
+  SECTION("Event Driven") {
+    auto event_driven_agent = std::make_shared<EventDrivenSchedulingAgent>(gsl::make_not_null(controller_services_provider_.get()), test_repo, test_repo, content_repo, configuration, thread_pool);
+    event_driven_agent->start();
+    auto first_task_reschedule_info = event_driven_agent->run(count_proc.get(), context, factory);
+    CHECK(!first_task_reschedule_info.finished_);
+    CHECK(first_task_reschedule_info.wait_time_ == 0ms);
+    auto count_num_after_one_schedule = count_proc->getNumberOfTriggers();
+    CHECK(count_num_after_one_schedule > 100);
+
+    auto second_task_reschedule_info = event_driven_agent->run(count_proc.get(), context, factory);
+    CHECK(!second_task_reschedule_info.finished_);
+    CHECK(second_task_reschedule_info.wait_time_ == 0ms);
+    auto count_num_after_two_schedule = count_proc->getNumberOfTriggers();
+    CHECK(count_num_after_two_schedule > count_num_after_one_schedule+100);
+  }
+
+  SECTION("Cron Driven every year") {
+    count_proc->setCronPeriod("0 0 0 1 1/12 ?");

Review Comment:
   "every 12th month starting with January"?  that's a weird way of saying "January"
   ```suggestion
       count_proc->setCronPeriod("0 0 0 1 1 ?");
   ```



##########
libminifi/test/unit/CronTests.cpp:
##########
@@ -0,0 +1,641 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+#include <string>
+
+#include "../Catch.h"
+#include "utils/Cron.h"
+#include "date/date.h"
+#include "date/tz.h"
+
+using std::chrono::system_clock;
+using std::chrono::seconds;
+using org::apache::nifi::minifi::utils::Cron;
+
+
+void checkNext(const std::string& expr, const date::zoned_time<seconds>& from, const date::zoned_time<seconds>& next) {
+  auto cron_expression = Cron(expr);
+  auto next_trigger = cron_expression.calculateNextTrigger(from.get_local_time());
+  CHECK(next_trigger == next.get_local_time());
+}
+
+
+TEST_CASE("Cron expression ctor tests", "[cron]") {
+  REQUIRE_THROWS(Cron("1600 ms"));
+  REQUIRE_THROWS(Cron("foo"));
+  REQUIRE_THROWS(Cron("61 0 0 * * *"));
+  REQUIRE_THROWS(Cron("0 61 0 * * *"));
+  REQUIRE_THROWS(Cron("0 0 24 * * *"));
+  REQUIRE_THROWS(Cron("0 0 0 32 * *"));
+  REQUIRE_THROWS(Cron("0 0 0 32 * *"));

Review Comment:
   duplicate line



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: issues-unsubscribe@nifi.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org