This is an automated email from the ASF dual-hosted git repository.

szaszm pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/nifi-minifi-cpp.git


The following commit(s) were added to refs/heads/main by this push:
     new ad886ae9e MINIFICPP-1809 custom Cron (quartz syntax) implementation 
and cron tests
ad886ae9e is described below

commit ad886ae9ef840d619a6b85351d8466448af23b3a
Author: Martin Zink <[email protected]>
AuthorDate: Wed Jun 8 18:16:16 2022 +0200

    MINIFICPP-1809 custom Cron (quartz syntax) implementation and cron tests
    
    Closes #1335
    Signed-off-by: Marton Szasz <[email protected]>
    Co-authored-by: Ferenc Gerlits <[email protected]>
---
 CMakeLists.txt                                     |   4 -
 LICENSE                                            |  23 -
 cmake/BuildTests.cmake                             |   1 +
 cmake/Date.cmake                                   |   2 +-
 extensions/expression-language/CMakeLists.txt      |   2 +-
 .../standard-processors/tests/CMakeLists.txt       |   1 +
 .../tests/integration/TailFileTest.cpp             |   6 +-
 libminifi/CMakeLists.txt                           |   2 +-
 libminifi/include/CronDrivenSchedulingAgent.h      |  52 +-
 libminifi/include/utils/Cron.h                     |  56 ++
 libminifi/include/utils/TestUtils.h                |  11 +
 libminifi/include/utils/TimeUtil.h                 |  37 +-
 libminifi/src/CronDrivenSchedulingAgent.cpp        |  81 +--
 libminifi/src/controllers/SSLContextService.cpp    |   2 +-
 libminifi/src/utils/Cron.cpp                       | 511 +++++++++++++++
 .../src/utils/TestUtils.cpp                        |  24 +-
 libminifi/test/resources/TestTailFileCron.yml      |   6 +-
 libminifi/test/unit/CronTests.cpp                  | 702 +++++++++++++++++++++
 libminifi/test/unit/SchedulingAgentTests.cpp       | 140 ++++
 libminifi/test/unit/TimeUtilTests.cpp              |  91 +++
 thirdparty/cron/Cron.h                             | 121 ----
 thirdparty/cron/LICENSE.TXT                        |  21 -
 22 files changed, 1615 insertions(+), 281 deletions(-)

diff --git a/CMakeLists.txt b/CMakeLists.txt
index d7492af33..ee6ca4575 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -351,10 +351,6 @@ target_include_directories(concurrentqueue SYSTEM 
INTERFACE "${CMAKE_CURRENT_SOU
 add_library(RapidJSON INTERFACE)
 target_include_directories(RapidJSON SYSTEM INTERFACE 
"${CMAKE_CURRENT_SOURCE_DIR}/thirdparty/rapidjson-48fbd8cd202ca54031fe799db2ad44ffa8e77c13/include")
 
-# Cron
-add_library(cron INTERFACE)
-target_include_directories(cron SYSTEM INTERFACE 
"${CMAKE_CURRENT_SOURCE_DIR}/thirdparty/cron")
-
 # cxxopts
 include(CxxOpts)
 
diff --git a/LICENSE b/LICENSE
index 836615ad9..4dba5c977 100644
--- a/LICENSE
+++ b/LICENSE
@@ -1697,29 +1697,6 @@ These files are available under a 3-Clause BSD license:
  implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
  See the License for more information.
 
-This project bundles the Bosma Scheduler library, which is available under an 
MIT Licenses:
-MIT License
-
-Copyright (c) 2017 Bosma
-
-Permission is hereby granted, free of charge, to any person obtaining a copy
-of this software and associated documentation files (the "Software"), to deal
-in the Software without restriction, including without limitation the rights
-to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
-copies of the Software, and to permit persons to whom the Software is
-furnished to do so, subject to the following conditions:
-
-The above copyright notice and this permission notice shall be included in all
-copies or substantial portions of the Software.
-
-THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
-AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
-OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
-SOFTWARE.
-
 This product bundles 'libssh2' which is available under a "3-clause BSD" 
license.
 
 /* Copyright (c) 2004-2007 Sara Golemon <[email protected]>
diff --git a/cmake/BuildTests.cmake b/cmake/BuildTests.cmake
index 043fc50a3..d5014b679 100644
--- a/cmake/BuildTests.cmake
+++ b/cmake/BuildTests.cmake
@@ -111,6 +111,7 @@ SET(UNIT_TEST_COUNT 0)
 FOREACH(testfile ${UNIT_TESTS})
   get_filename_component(testfilename "${testfile}" NAME_WE)
   add_executable("${testfilename}" "${TEST_DIR}/unit/${testfile}")
+  target_compile_definitions("${testfilename}" PRIVATE 
TZ_DATA_DIR="${CMAKE_BINARY_DIR}/tzdata")
   createTests("${testfilename}")
   target_link_libraries(${testfilename} ${CATCH_MAIN_LIB})
   MATH(EXPR UNIT_TEST_COUNT "${UNIT_TEST_COUNT}+1")
diff --git a/cmake/Date.cmake b/cmake/Date.cmake
index 9b6e24948..2e52f6ee5 100644
--- a/cmake/Date.cmake
+++ b/cmake/Date.cmake
@@ -47,7 +47,7 @@ endif()
 
 FetchContent_Declare(date_src
     GIT_REPOSITORY https://github.com/HowardHinnant/date.git
-    GIT_TAG        v3.0.0  # adjust tag/branch/commit as needed
+    GIT_TAG        v3.0.1  # adjust tag/branch/commit as needed
 )
 FetchContent_GetProperties(date_src)
 if (NOT date_src_POPULATED)
diff --git a/extensions/expression-language/CMakeLists.txt 
b/extensions/expression-language/CMakeLists.txt
index d8f9b5c80..bca4816eb 100644
--- a/extensions/expression-language/CMakeLists.txt
+++ b/extensions/expression-language/CMakeLists.txt
@@ -106,7 +106,7 @@ endif()
 add_library(minifi-expression-language-extensions SHARED ${SOURCES} 
${BISON_el-parser_OUTPUTS} ${FLEX_el-scanner_OUTPUTS})
 
 target_link_libraries(minifi-expression-language-extensions ${LIBMINIFI})
-target_link_libraries(minifi-expression-language-extensions date::tz RapidJSON 
CURL::libcurl)
+target_link_libraries(minifi-expression-language-extensions RapidJSON 
CURL::libcurl)
 
 register_extension(minifi-expression-language-extensions "EXPRESSION LANGUAGE 
EXTENSIONS" EXPRESSION-LANGUAGE-EXTENSIONS "This enables NiFi expression 
language" "extensions/expression-language/tests")
 register_extension_linter(minifi-expression-language-extensions-linter)
diff --git a/extensions/standard-processors/tests/CMakeLists.txt 
b/extensions/standard-processors/tests/CMakeLists.txt
index 6ae351367..d0ffa6dc3 100644
--- a/extensions/standard-processors/tests/CMakeLists.txt
+++ b/extensions/standard-processors/tests/CMakeLists.txt
@@ -65,6 +65,7 @@ FOREACH(testfile ${PROCESSOR_INTEGRATION_TESTS})
        target_link_libraries(${testfilename})
        target_link_libraries(${testfilename} minifi-standard-processors)
        target_link_libraries(${testfilename} minifi-civet-extensions)
+       target_compile_definitions("${testfilename}" PRIVATE 
TZ_DATA_DIR="${CMAKE_BINARY_DIR}/tzdata")
        if (NOT DISABLE_ROCKSDB)
                target_link_libraries(${testfilename} minifi-rocksdb-repos)
        endif()
diff --git a/extensions/standard-processors/tests/integration/TailFileTest.cpp 
b/extensions/standard-processors/tests/integration/TailFileTest.cpp
index 42e1a7f4e..0114693ab 100644
--- a/extensions/standard-processors/tests/integration/TailFileTest.cpp
+++ b/extensions/standard-processors/tests/integration/TailFileTest.cpp
@@ -32,12 +32,13 @@
 #include "state/ProcessorController.h"
 #include "integration/IntegrationBase.h"
 #include "utils/IntegrationTestUtils.h"
+#include "utils/TestUtils.h"
 
 using std::literals::chrono_literals::operator""s;
 
 class TailFileTestHarness : public IntegrationBase {
  public:
-  TailFileTestHarness() : IntegrationBase(1s) {
+  TailFileTestHarness() : IntegrationBase(2s) {
     dir = testController.createTempDirectory();
 
     statefile = dir + utils::file::get_separator();
@@ -53,6 +54,9 @@ class TailFileTestHarness : public IntegrationBase {
     
LogTestController::getInstance().setInfo<minifi::processors::LogAttribute>();
     LogTestController::getInstance().setTrace<minifi::processors::TailFile>();
     LogTestController::getInstance().setTrace<minifi::FlowController>();
+#ifdef WIN32
+    utils::dateSetInstall(TZ_DATA_DIR);
+#endif
   }
 
   void cleanup() override {
diff --git a/libminifi/CMakeLists.txt b/libminifi/CMakeLists.txt
index 367da27a7..0bf8b8824 100644
--- a/libminifi/CMakeLists.txt
+++ b/libminifi/CMakeLists.txt
@@ -89,7 +89,7 @@ else()
 endif()
 
 include(RangeV3)
-list(APPEND LIBMINIFI_LIBRARIES yaml-cpp ZLIB::ZLIB concurrentqueue RapidJSON 
spdlog cron Threads::Threads gsl-lite libsodium range-v3 expected-lite 
date::date)
+list(APPEND LIBMINIFI_LIBRARIES yaml-cpp ZLIB::ZLIB concurrentqueue RapidJSON 
spdlog Threads::Threads gsl-lite libsodium range-v3 expected-lite date::date 
date::tz)
 if(NOT WIN32)
        list(APPEND LIBMINIFI_LIBRARIES OSSP::libuuid++)
 endif()
diff --git a/libminifi/include/CronDrivenSchedulingAgent.h 
b/libminifi/include/CronDrivenSchedulingAgent.h
index 23a2c6c0a..3595f302f 100644
--- a/libminifi/include/CronDrivenSchedulingAgent.h
+++ b/libminifi/include/CronDrivenSchedulingAgent.h
@@ -17,43 +17,41 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-#ifndef LIBMINIFI_INCLUDE_CRONDRIVENSCHEDULINGAGENT_H_
-#define LIBMINIFI_INCLUDE_CRONDRIVENSCHEDULINGAGENT_H_
+#pragma once
 
 #include <chrono>
 #include <map>
 #include <memory>
 #include <string>
+#include <utility>
 
 #include "core/logging/Logger.h"
 #include "core/ProcessContext.h"
 #include "core/Processor.h"
 #include "core/ProcessSessionFactory.h"
-#include "Cron.h"
+#include "utils/Cron.h"
 #include "ThreadedSchedulingAgent.h"
 
-namespace org {
-namespace apache {
-namespace nifi {
-namespace minifi {
+namespace org::apache::nifi::minifi {
 
-// CronDrivenSchedulingAgent Class
 class CronDrivenSchedulingAgent : public ThreadedSchedulingAgent {
  public:
-  // Constructor
-  /*!
-   * Create a new event driven scheduling agent.
-   */
-  CronDrivenSchedulingAgent(const 
gsl::not_null<core::controller::ControllerServiceProvider*> 
controller_service_provider, std::shared_ptr<core::Repository> repo,
-                            std::shared_ptr<core::Repository> flow_repo, 
std::shared_ptr<core::ContentRepository> content_repo, 
std::shared_ptr<Configure> configuration,
-                            utils::ThreadPool<utils::TaskRescheduleInfo> 
&thread_pool)
-      : ThreadedSchedulingAgent(controller_service_provider, repo, flow_repo, 
content_repo, configuration, thread_pool) {
+  CronDrivenSchedulingAgent(const 
gsl::not_null<core::controller::ControllerServiceProvider*> 
controller_service_provider,
+                            std::shared_ptr<core::Repository> repo,
+                            std::shared_ptr<core::Repository> flow_repo,
+                            std::shared_ptr<core::ContentRepository> 
content_repo,
+                            std::shared_ptr<Configure> configuration,
+                            utils::ThreadPool<utils::TaskRescheduleInfo>& 
thread_pool)
+      : ThreadedSchedulingAgent(controller_service_provider, std::move(repo), 
std::move(flow_repo), std::move(content_repo), std::move(configuration), 
thread_pool) {
   }
-  // Destructor
+
+  CronDrivenSchedulingAgent(const CronDrivenSchedulingAgent& parent) = delete;
+  CronDrivenSchedulingAgent& operator=(const CronDrivenSchedulingAgent& 
parent) = delete;
   ~CronDrivenSchedulingAgent() override = default;
-  // Run function for the thread
-  utils::TaskRescheduleInfo run(core::Processor* processor, const 
std::shared_ptr<core::ProcessContext> &processContext,
-      const std::shared_ptr<core::ProcessSessionFactory> &sessionFactory) 
override;
+
+  utils::TaskRescheduleInfo run(core::Processor *processor,
+                                const std::shared_ptr<core::ProcessContext>& 
processContext,
+                                const 
std::shared_ptr<core::ProcessSessionFactory>& sessionFactory) override;
 
   void stop() override {
     std::lock_guard<std::mutex> locK(mutex_);
@@ -63,16 +61,8 @@ class CronDrivenSchedulingAgent : public 
ThreadedSchedulingAgent {
 
  private:
   std::mutex mutex_;
-  std::map<utils::Identifier, Bosma::Cron> schedules_;
-  std::map<utils::Identifier, std::chrono::system_clock::time_point> 
last_exec_;
-  // Prevent default copy constructor and assignment operation
-  // Only support pass by reference or pointer
-  CronDrivenSchedulingAgent(const CronDrivenSchedulingAgent &parent);
-  CronDrivenSchedulingAgent &operator=(const CronDrivenSchedulingAgent 
&parent);
+  std::map<utils::Identifier, utils::Cron> schedules_;
+  std::map<utils::Identifier, date::local_time<std::chrono::seconds>> 
last_exec_;
 };
 
-}  // namespace minifi
-}  // namespace nifi
-}  // namespace apache
-}  // namespace org
-#endif  // LIBMINIFI_INCLUDE_CRONDRIVENSCHEDULINGAGENT_H_
+}  // namespace org::apache::nifi::minifi
diff --git a/libminifi/include/utils/Cron.h b/libminifi/include/utils/Cron.h
new file mode 100644
index 000000000..ad8b6f605
--- /dev/null
+++ b/libminifi/include/utils/Cron.h
@@ -0,0 +1,56 @@
+/**
+ * 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"
+#include "Exception.h"
+
+namespace org::apache::nifi::minifi::utils {
+class BadCronExpression : public minifi::Exception {
+ public:
+  explicit BadCronExpression(const std::string& errmsg) : 
minifi::Exception(errmsg) {}
+};
+
+class CronField {
+ public:
+  virtual ~CronField() = default;
+
+  [[nodiscard]] virtual bool matches(date::local_seconds time_point) const = 0;
+};
+
+class Cron {
+ public:
+  explicit Cron(const std::string& expression);
+
+  [[nodiscard]] std::optional<date::local_seconds> 
calculateNextTrigger(date::local_seconds start) const;
+
+  std::unique_ptr<CronField> second_;
+  std::unique_ptr<CronField> minute_;
+  std::unique_ptr<CronField> hour_;
+  std::unique_ptr<CronField> day_;
+  std::unique_ptr<CronField> month_;
+  std::unique_ptr<CronField> day_of_week_;
+  std::unique_ptr<CronField> year_;
+};
+
+}  // namespace org::apache::nifi::minifi::utils
diff --git a/libminifi/include/utils/TestUtils.h 
b/libminifi/include/utils/TestUtils.h
index b31a27a0d..c125ad72c 100644
--- a/libminifi/include/utils/TestUtils.h
+++ b/libminifi/include/utils/TestUtils.h
@@ -28,6 +28,10 @@
 #include "utils/Id.h"
 #include "utils/TimeUtil.h"
 
+#ifdef WIN32
+#include "date/tz.h"
+#endif
+
 namespace org {
 namespace apache {
 namespace nifi {
@@ -65,6 +69,13 @@ class ManualClock : public timeutils::Clock {
   std::chrono::milliseconds time_{0};
 };
 
+
+#ifdef WIN32
+// The tzdata location is set as a global variable in date-tz library
+// We need to set it from from libminifi to effect calls made from libminifi 
(on Windows)
+void dateSetInstall(const std::string& install);
+#endif
+
 }  // namespace utils
 }  // namespace minifi
 }  // namespace nifi
diff --git a/libminifi/include/utils/TimeUtil.h 
b/libminifi/include/utils/TimeUtil.h
index 68c7c8b7d..5a352947c 100644
--- a/libminifi/include/utils/TimeUtil.h
+++ b/libminifi/include/utils/TimeUtil.h
@@ -14,8 +14,7 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-#ifndef LIBMINIFI_INCLUDE_UTILS_TIMEUTIL_H_
-#define LIBMINIFI_INCLUDE_UTILS_TIMEUTIL_H_
+#pragma once
 
 #include <cstring>
 #include <ctime>
@@ -308,12 +307,32 @@ std::optional<TargetDuration> StringToDuration(const 
std::string& input) {
     std::chrono::days>(unit, value);
 }
 
-}  // namespace org::apache::nifi::minifi::utils::timeutils
+inline date::local_seconds roundToNextYear(date::local_seconds tp) {
+  date::year_month_day date(std::chrono::floor<std::chrono::days>(tp));
+  auto start_of_year = date.year()/1/1;
+  return date::local_days(start_of_year + std::chrono::years(1));
+}
+
+inline date::local_seconds roundToNextMonth(date::local_seconds tp) {
+  date::year_month_day date(std::chrono::floor<std::chrono::days>(tp));
+  auto start_of_month = date.year()/date.month()/1;
+  return date::local_days(start_of_month + std::chrono::months(1));
+}
+
+inline date::local_seconds roundToNextDay(date::local_seconds tp) {
+  return std::chrono::floor<std::chrono::days>(tp) + std::chrono::days(1);
+}
 
-// for backwards compatibility, to be removed after 0.8
-using org::apache::nifi::minifi::utils::timeutils::getTimeNano;
-using org::apache::nifi::minifi::utils::timeutils::getTimeStr;
-using org::apache::nifi::minifi::utils::timeutils::parseDateTimeStr;
-using org::apache::nifi::minifi::utils::timeutils::getDateTimeStr;
+inline date::local_seconds roundToNextHour(date::local_seconds tp) {
+  return std::chrono::floor<std::chrono::hours>(tp) + std::chrono::hours(1);
+}
+
+inline date::local_seconds roundToNextMinute(date::local_seconds tp) {
+  return std::chrono::floor<std::chrono::minutes>(tp) + 
std::chrono::minutes(1);
+}
 
-#endif  // LIBMINIFI_INCLUDE_UTILS_TIMEUTIL_H_
+inline date::local_seconds roundToNextSecond(date::local_seconds tp) {
+  return std::chrono::floor<std::chrono::seconds>(tp) + 
std::chrono::seconds(1);
+}
+
+}  // namespace org::apache::nifi::minifi::utils::timeutils
diff --git a/libminifi/src/CronDrivenSchedulingAgent.cpp 
b/libminifi/src/CronDrivenSchedulingAgent.cpp
index 27fddc552..3ad94f1ea 100644
--- a/libminifi/src/CronDrivenSchedulingAgent.cpp
+++ b/libminifi/src/CronDrivenSchedulingAgent.cpp
@@ -20,66 +20,53 @@
 #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;
+namespace org::apache::nifi::minifi {
 
-namespace org {
-namespace apache {
-namespace nifi {
-namespace minifi {
+utils::TaskRescheduleInfo CronDrivenSchedulingAgent::run(core::Processor* 
processor,
+                                                         const 
std::shared_ptr<core::ProcessContext>& processContext,
+                                                         const 
std::shared_ptr<core::ProcessSessionFactory>& sessionFactory) {
+  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;
 
-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));
-      }
-    }
+    schedules_.emplace(uuid, utils::Cron(processor->getCronPeriod()));
+    last_exec_.emplace(uuid, current_time.get_local_time());
+
+    auto last_trigger = last_exec_[uuid];
+    auto next_to_last_trigger = 
schedules_.at(uuid).calculateNextTrigger(last_trigger);
+    if (!next_to_last_trigger)
+      return utils::TaskRescheduleInfo::Done();
 
-    if (result > from) {
-      bool shouldYield = this->onTrigger(processor, processContext, 
sessionFactory);
+    if (*next_to_last_trigger > current_time.get_local_time())
+      return 
utils::TaskRescheduleInfo::RetryIn(ceil<milliseconds>(*next_to_last_trigger-current_time.get_local_time()));
 
-      if (processor->isYield()) {
-        // Honor the yield
-        return utils::TaskRescheduleInfo::RetryIn(processor->getYieldTime());
-      } else if (shouldYield && this->bored_yield_duration_ > 0ms) {
-        // No work to do or need to apply back pressure
-        return utils::TaskRescheduleInfo::RetryIn(this->bored_yield_duration_);
-      }
+    last_exec_[uuid] = current_time.get_local_time();
+    bool shouldYield = this->onTrigger(processor, processContext, 
sessionFactory);
+
+    if (processor->isYield()) {
+      return utils::TaskRescheduleInfo::RetryIn(processor->getYieldTime());
+    } else if (shouldYield && this->bored_yield_duration_ > 0ms) {
+      return utils::TaskRescheduleInfo::RetryIn(this->bored_yield_duration_);
     }
-    return 
utils::TaskRescheduleInfo::RetryIn(std::chrono::duration_cast<std::chrono::milliseconds>(result
 - from));
+
+    auto next_trigger = 
schedules_.at(uuid).calculateNextTrigger(current_time.get_local_time());
+    if (!next_trigger)
+      return utils::TaskRescheduleInfo::Done();
+    return 
utils::TaskRescheduleInfo::RetryIn(ceil<milliseconds>(*next_trigger-current_time.get_local_time()));
   }
   return utils::TaskRescheduleInfo::Done();
 }
 
-} /* namespace minifi */
-} /* namespace nifi */
-} /* namespace apache */
-} /* namespace org */
+}  // namespace org::apache::nifi::minifi
diff --git a/libminifi/src/controllers/SSLContextService.cpp 
b/libminifi/src/controllers/SSLContextService.cpp
index 3dd2f8127..f765cbb50 100644
--- a/libminifi/src/controllers/SSLContextService.cpp
+++ b/libminifi/src/controllers/SSLContextService.cpp
@@ -563,7 +563,7 @@ void SSLContextService::initializeProperties() {
 void SSLContextService::verifyCertificateExpiration() {
   auto verify = [&] (const std::string& cert_file, const 
utils::tls::X509_unique_ptr& cert) {
     if (auto end_date = utils::tls::getCertificateExpiration(cert)) {
-      std::string end_date_str = 
getTimeStr(std::chrono::duration_cast<std::chrono::milliseconds>(end_date->time_since_epoch()).count());
+      std::string end_date_str = 
utils::timeutils::getTimeStr(std::chrono::duration_cast<std::chrono::milliseconds>(end_date->time_since_epoch()).count());
       if (end_date.value() < std::chrono::system_clock::now()) {
         core::logging::LOG_ERROR(logger_) << "Certificate in '" << cert_file 
<< "' expired at " << end_date_str;
       } else if (auto diff = end_date.value() - 
std::chrono::system_clock::now(); diff < std::chrono::weeks{2}) {
diff --git a/libminifi/src/utils/Cron.cpp b/libminifi/src/utils/Cron.cpp
new file mode 100644
index 000000000..5409765f9
--- /dev/null
+++ b/libminifi/src/utils/Cron.cpp
@@ -0,0 +1,511 @@
+/**
+ * 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 <charconv>
+#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;
+
+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 {
+
+template<class T>
+std::optional<T> fromChars(const std::string& input) {
+  T t{};
+  const auto result = std::from_chars(input.data(), input.data() + 
input.size(), t);
+  if (result.ptr != input.data() + input.size())
+    return std::nullopt;
+  return t;
+}
+
+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) {
+  if (auto sec_int = fromChars<uint64_t>(second_str); sec_int && *sec_int <= 
59)
+    return seconds(*sec_int);
+  throw BadCronExpression("Invalid second " + second_str);
+}
+
+template <>
+minutes parse<minutes>(const std::string& minute_str) {
+  if (auto min_int = fromChars<uint64_t>(minute_str); min_int && *min_int <= 
59)
+    return minutes(*min_int);
+  throw BadCronExpression("Invalid minute " + minute_str);
+}
+
+template <>
+hours parse<hours>(const std::string& hour_str) {
+  if (auto hour_int = fromChars<uint64_t>(hour_str); hour_int && *hour_int <= 
23)
+    return hours(*hour_int);
+  throw BadCronExpression("Invalid hour " + hour_str);
+}
+
+template <>
+days parse<days>(const std::string& days_str) {
+  if (auto days_int = fromChars<uint64_t>(days_str))
+    return days(*days_int);
+  throw BadCronExpression("Invalid days " + days_str);
+}
+
+template <>
+day parse<day>(const std::string& day_str) {
+  if (auto day_int = fromChars<uint64_t>(day_str); day_int && 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
+// 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] = static_cast<char>(std::toupper(static_cast<unsigned 
char>(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 (!stream.fail() && parsed_month.ok() && stream.peek() == EOF)
+      return parsed_month;
+  } else {
+    from_stream(stream, "%m", parsed_month);
+    if (!stream.fail() && parsed_month.ok() && stream.peek() == EOF)
+      return parsed_month;
+  }
+
+  throw BadCronExpression("Invalid month " + month_str);
+}
+
+template <>
+weekday parse<weekday>(const std::string& weekday_str) {
+// https://github.com/HowardHinnant/date/issues/550
+// Due to https://gcc.gnu.org/bugzilla/show_bug.cgi?id=78714
+// the weekday parsing with '%a' is case sensitive in gcc11
+// This has been fixed in gcc12
+#if defined(__GNUC__) && __GNUC__ < 12
+  auto patched_weekday_str = StringUtils::toLower(weekday_str);
+  if (!patched_weekday_str.empty())
+    patched_weekday_str[0] = 
static_cast<char>(std::toupper(static_cast<unsigned 
char>(patched_weekday_str[0])));
+  std::stringstream stream(patched_weekday_str);
+#else
+  std::stringstream stream(weekday_str);
+#endif
+  stream.imbue(std::locale("en_US.UTF-8"));
+
+  if (weekday_str.size() > 2) {
+    weekday parsed_weekday{};
+    from_stream(stream, "%a", parsed_weekday);
+    if (!stream.fail() && parsed_weekday.ok() && stream.peek() == EOF)
+      return parsed_weekday;
+  } else {
+    unsigned weekday_num;
+    stream >> weekday_num;
+    if (!stream.fail() && weekday_num < 7 && stream.peek() == EOF)
+      return weekday(weekday_num-1);
+  }
+  throw BadCronExpression("Invalid weekday: " + weekday_str);
+}
+
+template <>
+year parse<year>(const std::string& year_str) {
+  if (auto year_int = fromChars<uint64_t>(year_str); year_int && *year_int >= 
1970 && *year_int <= 2999)
+    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 matches(local_seconds time_point) const override {
+    return value_ == getFieldType<FieldType>(time_point);
+  }
+
+ private:
+  FieldType value_;
+};
+
+class NotCheckedField : public CronField {
+ public:
+  NotCheckedField() = default;
+
+  [[nodiscard]] bool matches(local_seconds) const override { return true; }
+};
+
+class AllValuesField : public CronField {
+ public:
+  AllValuesField() = default;
+
+  [[nodiscard]] bool matches(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)) {
+    if (!(lower_bound_ <= upper_bound_))
+      throw std::out_of_range("lower bound must be smaller or equal to upper 
bound");
+  }
+
+  [[nodiscard]] bool matches(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 matches(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 matches(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) {
+    if (!(offset_ <= std::chrono::days(30)))
+      throw BadCronExpression("Offset from last day must be <= 30");
+  }
+
+  [[nodiscard]] bool matches(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 wday, uint8_t n) : weekday_(wday), n_(n) {}
+
+  [[nodiscard]] bool matches(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 matches(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 LastSpecificDayOfTheWeekOfTheMonth : public CronField {
+ public:
+  explicit LastSpecificDayOfTheWeekOfTheMonth(weekday wday) : weekday_(wday) {}
+
+  [[nodiscard]] bool matches(local_seconds value) const override {
+    year_month_day date(floor<days>(value));
+    year_month_day last_weekday_of_month_date = 
year_month_day(local_days(date.year()/date.month()/weekday_[last]));
+    return date == last_weekday_of_month_date;
+  }
+ private:
+  weekday weekday_;
+};
+
+class ClosestWeekdayToTheNthDayOfTheMonth : public CronField {
+ public:
+  explicit ClosestWeekdayToTheNthDayOfTheMonth(day day_number) : 
day_number_(day_number) {}
+
+  [[nodiscard]] bool matches(local_seconds value) const override {
+    year_month_day date(floor<days>(value));
+    for (auto diff : {0, -1, 1, -2, 2}) {
+      auto target_date = date.year() / date.month() / (day_number_ + 
days(diff));
+      if (target_date.ok() && isWeekday(target_date))
+        return target_date == date;
+    }
+
+    return false;
+  }
+
+ private:
+  day day_number_;
+};
+
+template <typename FieldType>
+std::unique_ptr<CronField> parseCronField(const std::string& field_str) {
+  try {
+    if (field_str == "*") {
+      return std::make_unique<AllValuesField>();
+    }
+
+    if (field_str == "?") {
+      return std::make_unique<NotCheckedField>();
+    }
+
+    if (field_str == "L") {
+      if constexpr (std::is_same<day, FieldType>())
+        return std::make_unique<LastNthDayInMonthField>(days(0));
+      if constexpr (std::is_same<weekday, FieldType>())
+        return std::make_unique<SingleValueField<weekday>>(Saturday);
+      throw BadCronExpression("L can only be used in the Day of month/Day of 
week fields");
+    }
+
+    if (field_str == "LW") {
+      if constexpr (!std::is_same<day, FieldType>())
+        throw BadCronExpression("LW can only be used in the Day of month 
field");
+      return std::make_unique<LastWeekDayField>();
+    }
+
+    if (field_str.find('#') != std::string::npos) {
+      if constexpr (!std::is_same<weekday, FieldType>())
+        throw BadCronExpression("# can only be used in the Day of week field");
+      auto operands = StringUtils::split(field_str, "#");
+      if (operands.size() != 2)
+        throw BadCronExpression("Invalid field " + field_str);
+
+      if (auto second_operand = fromChars<uint64_t>(operands[1]))
+        return std::make_unique<NthWeekdayField>(parse<weekday>(operands[0]), 
*second_operand);
+    }
+
+    if (field_str.find('-') != std::string::npos) {
+      auto operands = StringUtils::split(field_str, "-");
+      if (operands.size() != 2)
+        throw BadCronExpression("Invalid field " + field_str);
+      if (operands[0] == "L") {
+        if constexpr (std::is_same<day, FieldType>())
+          return 
std::make_unique<LastNthDayInMonthField>(parse<days>(operands[1]));
+      }
+      return 
std::make_unique<RangeField<FieldType>>(parse<FieldType>(operands[0]), 
parse<FieldType>(operands[1]));
+    }
+
+    if (field_str.ends_with('L')) {
+      if constexpr (!std::is_same<weekday, FieldType>())
+        throw BadCronExpression("<X>L can only be used in the Day of week 
field");
+      auto prefix = field_str.substr(0, field_str.size()-1);
+      return 
std::make_unique<LastSpecificDayOfTheWeekOfTheMonth>(parse<weekday>(prefix));
+    }
+
+    if (field_str.find('/') != std::string::npos) {
+      auto operands = StringUtils::split(field_str, "/");
+      if (operands.size() != 2)
+        throw BadCronExpression("Invalid field " + field_str);
+      if (operands[0] == "*")
+        operands[0] = "0";
+      if (auto second_operand = fromChars<int>(operands[1]))
+        return 
std::make_unique<IncrementField<FieldType>>(parse<FieldType>(operands[0]), 
*second_operand);
+    }
+
+    if (field_str.find(',') != std::string::npos) {
+      auto operands_str = StringUtils::split(field_str, ",");
+      std::vector<FieldType> operands;
+      std::transform(operands_str.begin(), operands_str.end(), 
std::back_inserter(operands), parse<FieldType>);
+      return std::make_unique<ListField<FieldType>>(std::move(operands));
+    }
+
+    if (field_str.ends_with('W')) {
+      if constexpr (!std::is_same<day, FieldType>())
+        throw BadCronExpression("W can only be used in the Day of month 
field");
+      auto operands_str = StringUtils::split(field_str, "W");
+      if (operands_str.size() != 2)
+        throw BadCronExpression("Invalid field " + field_str);
+      return 
std::make_unique<ClosestWeekdayToTheNthDayOfTheMonth>(parse<day>(operands_str[0]));
+    }
+
+    return 
std::make_unique<SingleValueField<FieldType>>(parse<FieldType>(field_str));
+  } catch (const std::exception& e) {
+    throw BadCronExpression("Couldn't parse cron field: " + field_str + " " + 
e.what());
+  }
+}
+}  // namespace
+
+Cron::Cron(const std::string& expression) {
+  auto tokens = StringUtils::split(expression, " ");
+
+  if (tokens.size() != 6 && tokens.size() != 7)
+    throw BadCronExpression("malformed cron string (must be 6 or 7 fields): " 
+ expression);
+
+  second_ = parseCronField<seconds>(tokens[0]);
+  minute_ = parseCronField<minutes>(tokens[1]);
+  hour_ = parseCronField<hours>(tokens[2]);
+  day_ = parseCronField<day>(tokens[3]);
+  month_ = parseCronField<month>(tokens[4]);
+  day_of_week_ = parseCronField<weekday>(tokens[5]);
+  if (tokens.size() == 7)
+    year_ = parseCronField<year>(tokens[6]);
+}
+
+std::optional<local_seconds> Cron::calculateNextTrigger(const local_seconds 
start) const {
+  gsl_Expects(second_ && minute_ && hour_ && day_ && month_ && day_of_week_);
+  auto next = timeutils::roundToNextSecond(start);
+  while (next < local_days((year(2999)/1/1))) {
+    if (year_ && !year_->matches(next)) {
+      next = timeutils::roundToNextYear(next);
+      continue;
+    }
+    if (!month_->matches(next)) {
+      next = timeutils::roundToNextMonth(next);
+      continue;
+    }
+    if (!day_->matches(next)) {
+      next = timeutils::roundToNextDay(next);
+      continue;
+    }
+    if (!day_of_week_->matches(next)) {
+      next = timeutils::roundToNextDay(next);
+      continue;
+    }
+    if (!hour_->matches(next)) {
+      next = timeutils::roundToNextHour(next);
+      continue;
+    }
+    if (!minute_->matches(next)) {
+      next = timeutils::roundToNextMinute(next);
+      continue;
+    }
+    if (!second_->matches(next)) {
+      next = timeutils::roundToNextSecond(next);
+      continue;
+    }
+    return next;
+  }
+  return std::nullopt;
+}
+
+}  // namespace org::apache::nifi::minifi::utils
diff --git a/extensions/standard-processors/tests/unit/SchedulingAgentTests.cpp 
b/libminifi/src/utils/TestUtils.cpp
similarity index 60%
rename from extensions/standard-processors/tests/unit/SchedulingAgentTests.cpp
rename to libminifi/src/utils/TestUtils.cpp
index a65516e1f..8d46e8efd 100644
--- a/extensions/standard-processors/tests/unit/SchedulingAgentTests.cpp
+++ b/libminifi/src/utils/TestUtils.cpp
@@ -1,5 +1,4 @@
 /**
- *
  * 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.
@@ -16,21 +15,12 @@
  * limitations under the License.
  */
 
-#include <memory>
-#include <string>
-#include <vector>
-#include "io/CRCStream.h"
-#include "io/BufferStream.h"
-#include "TestBase.h"
-#include "Catch.h"
-#include "GetFile.h"
-#include "LogAttribute.h"
-#include "SchedulingAgent.h"
-#include "TimerDrivenSchedulingAgent.h"
-
+#include "utils/TestUtils.h"
 
-TEST_CASE("TestTDAgent", "[test1]") {
-  std::shared_ptr<core::Processor> procA = 
std::make_shared<minifi::processors::GetFile>("getFile");
-  std::shared_ptr<core::Processor> procB = 
std::make_shared<minifi::processors::LogAttribute>("logAttribute");
-  // agent.run()
+namespace org::apache::nifi::minifi::utils {
+#ifdef WIN32
+void dateSetInstall(const std::string& install) {
+  date::set_install(install);
 }
+#endif
+}  // namespace org::apache::nifi::minifi::utils
diff --git a/libminifi/test/resources/TestTailFileCron.yml 
b/libminifi/test/resources/TestTailFileCron.yml
index 5e6d0f248..afd5d41f8 100644
--- a/libminifi/test/resources/TestTailFileCron.yml
+++ b/libminifi/test/resources/TestTailFileCron.yml
@@ -25,7 +25,7 @@ Processors:
       class: org.apache.nifi.processors.standard.TailFile
       max concurrent tasks: 1
       scheduling strategy: CRON_DRIVEN
-      scheduling period: "* * * * *"
+      scheduling period: "* * * * * *"
       penalization period: 30 sec
       yield period: 1 sec
       run duration nanos: 0
@@ -58,7 +58,7 @@ Connections:
       source relationship name: success
       max work queue size: 0
       max work queue data size: 1 MB
-      flowfile expiration: 60 sec      
+      flowfile expiration: 60 sec
 
 Remote Processing Groups:
-    
+
diff --git a/libminifi/test/unit/CronTests.cpp 
b/libminifi/test/unit/CronTests.cpp
new file mode 100644
index 000000000..4d8fa5669
--- /dev/null
+++ b/libminifi/test/unit/CronTests.cpp
@@ -0,0 +1,702 @@
+/**
+ * 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("1banana * * * * * *"));
+  REQUIRE_THROWS(Cron("* 1banana * * * * *"));
+  REQUIRE_THROWS(Cron("* * 1banana * * * *"));
+  REQUIRE_THROWS(Cron("* * * 1banana * * *"));
+  REQUIRE_THROWS(Cron("* * * * 1banana * *"));
+  REQUIRE_THROWS(Cron("* * * * DECbanana * *"));
+  REQUIRE_THROWS(Cron("* * * * * WEDbanana *"));
+
+  REQUIRE_THROWS(Cron("* * * * * * 1banana"));
+  REQUIRE_THROWS(Cron("* * * * * * 2000banana"));
+
+  REQUIRE_THROWS(Cron("1G * * * * * *"));
+  REQUIRE_THROWS(Cron("* 1G * * * * *"));
+  REQUIRE_THROWS(Cron("* * 1G * * * *"));
+  REQUIRE_THROWS(Cron("* * * 1G * * *"));
+  REQUIRE_THROWS(Cron("* * * * 1G * *"));
+  REQUIRE_THROWS(Cron("* * * * * 1G *"));
+  REQUIRE_THROWS(Cron("* * * * * * 1G"));
+
+  // 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, 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 ?"));
+
+  REQUIRE_THROWS(Cron("0 15 10 L-32 * ?"));
+  REQUIRE_THROWS(Cron("15-10 * * * * * *"));
+  REQUIRE_THROWS(Cron("* 4-3 * * * * *"));
+  REQUIRE_THROWS(Cron("* * 4-3 * * * *"));
+  REQUIRE_THROWS(Cron("* * * 31-29 * * *"));
+  REQUIRE_THROWS(Cron("0 0 0 ? * MON-SUN"));
+  REQUIRE_NOTHROW(Cron("0 0 0 ? * SUN-MON"));
+}
+
+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);
+  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);
+
+  checkNext("0 15 10 ? * 6L",
+            sys_days(2022_y / 07 / 15) + 00h + 00min + 00s,
+            sys_days(2022_y / 07 / 29) + 10h + 15min + 00s);
+
+  checkNext("0 0 0 L-3 * ?",
+            sys_days(2022_y / 01 / 10) + 00h + 00min + 00s,
+            sys_days(2022_y / 01 / 28) + 00h + 00min + 00s);
+
+  checkNext("0 0 0 L-30 * ?",
+            sys_days(2022_y / 01 / 10) + 00h + 00min + 00s,
+            sys_days(2022_y / 03 / 01) + 00h + 00min + 00s);
+}
+
+TEST_CASE("Cron::calculateNextTrigger with timezones", "[cron]") {
+  using date::local_days;
+  using date::locate_zone;
+  using date::zoned_time;
+  using namespace date::literals;
+  using namespace std::literals::chrono_literals;
+#ifdef WIN32
+  date::set_install(TZ_DATA_DIR);
+#endif
+
+  const std::vector<std::string> time_zones{ "Europe/Berlin", "Asia/Seoul", 
"America/Los_Angeles", "Asia/Singapore", "UCT" };
+
+  for (const auto& time_zone: time_zones) {
+    checkNext("0/15 * 1-4 * * ?",
+              make_zoned(locate_zone(time_zone), local_days(2012_y / 07 / 01) 
+ 9h + 53min + 50s),
+              make_zoned(locate_zone(time_zone), local_days(2012_y / 07 / 02) 
+ 01h + 00min + 00s));
+    checkNext("0/15 * 1-4 * * ? *",
+              make_zoned(locate_zone(time_zone), local_days(2012_y / 07 / 01) 
+ 9h + 53min + 50s),
+              make_zoned(locate_zone(time_zone), local_days(2012_y / 07 / 02) 
+ 01h + 00min + 00s));
+    checkNext("0/15 * 1-4 * * ?",
+              make_zoned(locate_zone(time_zone), local_days(2012_y / 07 / 01) 
+ 9h + 53min + 00s),
+              make_zoned(locate_zone(time_zone), local_days(2012_y / 07 / 02) 
+ 01h + 00min + 00s));
+    checkNext("0/15 * 1-4 * * ?",
+              make_zoned(locate_zone(time_zone), local_days(2012_y / 07 / 01) 
+ 9h + 53min + 50s),
+              make_zoned(locate_zone(time_zone), local_days(2012_y / 07 / 02) 
+ 01h + 00min + 00s));
+    checkNext("0/15 * 1-4 * * ? *",
+              make_zoned(locate_zone(time_zone), local_days(2012_y / 07 / 01) 
+ 9h + 53min + 50s),
+              make_zoned(locate_zone(time_zone), local_days(2012_y / 07 / 02) 
+ 01h + 00min + 00s));
+    checkNext("0/15 * 1-4 * * ?",
+              make_zoned(locate_zone(time_zone), local_days(2012_y / 07 / 01) 
+ 9h + 53min + 00s),
+              make_zoned(locate_zone(time_zone), local_days(2012_y / 07 / 02) 
+ 01h + 00min + 00s));
+    checkNext("0 0/2 1-4 * * ?",
+              make_zoned(locate_zone(time_zone), local_days(2012_y / 07 / 01) 
+ 9h + 00min + 00s),
+              make_zoned(locate_zone(time_zone), local_days(2012_y / 07 / 02) 
+ 01h + 00min + 00s));
+    checkNext("* * * * * ?",
+              make_zoned(locate_zone(time_zone), local_days(2012_y / 07 / 01) 
+ 9h + 00min + 00s),
+              make_zoned(locate_zone(time_zone), local_days(2012_y / 07 / 01) 
+ 9h + 00min + 01s));
+    checkNext("* * * * * ?",
+              make_zoned(locate_zone(time_zone), local_days(2016_y / 12 / 01) 
+ 9h + 00min + 58s),
+              make_zoned(locate_zone(time_zone), local_days(2016_y / 12 / 01) 
+ 9h + 00min + 59s));
+    checkNext("10 * * * * ?",
+              make_zoned(locate_zone(time_zone), local_days(2016_y / 12 / 01) 
+ 9h + 42min + 9s),
+              make_zoned(locate_zone(time_zone), local_days(2016_y / 12 / 01) 
+ 9h + 42min + 10s));
+    checkNext("11 * * * * ?",
+              make_zoned(locate_zone(time_zone), local_days(2016_y / 12 / 01) 
+ 9h + 42min + 10s),
+              make_zoned(locate_zone(time_zone), local_days(2016_y / 12 / 01) 
+ 9h + 42min + 11s));
+    checkNext("10 * * * * ?",
+              make_zoned(locate_zone(time_zone), local_days(2016_y / 12 / 01) 
+ 9h + 42min + 10s),
+              make_zoned(locate_zone(time_zone), local_days(2016_y / 12 / 01) 
+ 9h + 43min + 10s));
+    checkNext("10-15 * * * * ?",
+              make_zoned(locate_zone(time_zone), local_days(2016_y / 12 / 01) 
+ 9h + 42min + 9s),
+              make_zoned(locate_zone(time_zone), local_days(2016_y / 12 / 01) 
+ 9h + 42min + 10s));
+    checkNext("10-15 * * * * ?",
+              make_zoned(locate_zone(time_zone), local_days(2016_y / 12 / 01) 
+ 21h + 42min + 14s),
+              make_zoned(locate_zone(time_zone), local_days(2016_y / 12 / 01) 
+ 21h + 42min + 15s));
+    checkNext("0 * * * * ?",
+              make_zoned(locate_zone(time_zone), local_days(2016_y / 12 / 01) 
+ 21h + 10min + 42s),
+              make_zoned(locate_zone(time_zone), local_days(2016_y / 12 / 01) 
+ 21h + 11min + 00s));
+    checkNext("0 * * * * ?",
+              make_zoned(locate_zone(time_zone), local_days(2016_y / 12 / 01) 
+ 21h + 11min + 00s),
+              make_zoned(locate_zone(time_zone), local_days(2016_y / 12 / 01) 
+ 21h + 12min + 00s));
+    checkNext("0 11 * * * ?",
+              make_zoned(locate_zone(time_zone), local_days(2016_y / 12 / 01) 
+ 21h + 10min + 42s),
+              make_zoned(locate_zone(time_zone), local_days(2016_y / 12 / 01) 
+ 21h + 11min + 00s));
+    checkNext("0 10 * * * ?",
+              make_zoned(locate_zone(time_zone), local_days(2016_y / 12 / 01) 
+ 21h + 11min + 00s),
+              make_zoned(locate_zone(time_zone), local_days(2016_y / 12 / 01) 
+ 22h + 10min + 00s));
+    checkNext("0 0 * * * ?",
+              make_zoned(locate_zone(time_zone), local_days(2012_y / 9 / 30) + 
11h + 01min + 00s),
+              make_zoned(locate_zone(time_zone), local_days(2012_y / 9 / 30) + 
12h + 00min + 00s));
+    checkNext("0 0 * * * ?",
+              make_zoned(locate_zone(time_zone), local_days(2012_y / 9 / 30) + 
12h + 00min + 00s),
+              make_zoned(locate_zone(time_zone), local_days(2012_y / 9 / 30) + 
13h + 00min + 00s));
+    checkNext("0 0 * * * ?",
+              make_zoned(locate_zone(time_zone), local_days(2012_y / 9 / 10) + 
23h + 01min + 00s),
+              make_zoned(locate_zone(time_zone), local_days(2012_y / 9 / 11) + 
00h + 00min + 00s));
+    checkNext("0 0 * * * ?",
+              make_zoned(locate_zone(time_zone), local_days(2012_y / 9 / 11) + 
00h + 00min + 00s),
+              make_zoned(locate_zone(time_zone), local_days(2012_y / 9 / 11) + 
01h + 00min + 00s));
+    checkNext("0 0 0 * * ?",
+              make_zoned(locate_zone(time_zone), local_days(2012_y / 9 / 01) + 
14h + 42min + 43s),
+              make_zoned(locate_zone(time_zone), local_days(2012_y / 9 / 02) + 
00h + 00min + 00s));
+    checkNext("0 0 0 * * ?",
+              make_zoned(locate_zone(time_zone), local_days(2012_y / 9 / 02) + 
00h + 00min + 00s),
+              make_zoned(locate_zone(time_zone), local_days(2012_y / 9 / 03) + 
00h + 00min + 00s));
+    checkNext("* * * 10 * ?",
+              make_zoned(locate_zone(time_zone), local_days(2012_y / 10 / 9) + 
15h + 12min + 42s),
+              make_zoned(locate_zone(time_zone), local_days(2012_y / 10 / 10) 
+ 00h + 00min + 00s));
+    checkNext("* * * 10 * ?",
+              make_zoned(locate_zone(time_zone), local_days(2012_y / 10 / 11) 
+ 15h + 12min + 42s),
+              make_zoned(locate_zone(time_zone), local_days(2012_y / 11 / 10) 
+ 00h + 00min + 00s));
+    checkNext("0 0 0 * * ? 2020",
+              make_zoned(locate_zone(time_zone), local_days(2012_y / 9 / 30) + 
15h + 12min + 42s),
+              make_zoned(locate_zone(time_zone), local_days(2020_y / 01 / 01) 
+ 00h + 00min + 00s));
+    checkNext("0 0 0 * * ?",
+              make_zoned(locate_zone(time_zone), local_days(2012_y / 9 / 30) + 
15h + 12min + 42s),
+              make_zoned(locate_zone(time_zone), local_days(2012_y / 10 / 01) 
+ 00h + 00min + 00s));
+    checkNext("0 0 0 * * ?",
+              make_zoned(locate_zone(time_zone), local_days(2012_y / 10 / 01) 
+ 00h + 00min + 00s),
+              make_zoned(locate_zone(time_zone), local_days(2012_y / 10 / 02) 
+ 00h + 00min + 00s));
+    checkNext("0 0 0 * * ?",
+              make_zoned(locate_zone(time_zone), local_days(2012_y / 8 / 30) + 
15h + 12min + 42s),
+              make_zoned(locate_zone(time_zone), local_days(2012_y / 8 / 31) + 
00h + 00min + 00s));
+    checkNext("0 0 0 * * ?",
+              make_zoned(locate_zone(time_zone), local_days(2012_y / 8 / 31) + 
00h + 00min + 00s),
+              make_zoned(locate_zone(time_zone), local_days(2012_y / 9 / 01) + 
00h + 00min + 00s));
+    checkNext("0 0 0 * * ?",
+              make_zoned(locate_zone(time_zone), local_days(2012_y / 10 / 30) 
+ 15h + 12min + 42s),
+              make_zoned(locate_zone(time_zone), local_days(2012_y / 10 / 31) 
+ 00h + 00min + 00s));
+    checkNext("0 0 0 * * ?",
+              make_zoned(locate_zone(time_zone), local_days(2012_y / 10 / 31) 
+ 00h + 00min + 00s),
+              make_zoned(locate_zone(time_zone), local_days(2012_y / 11 / 01) 
+ 00h + 00min + 00s));
+    checkNext("0 0 0 1 * ?",
+              make_zoned(locate_zone(time_zone), local_days(2012_y / 10 / 30) 
+ 15h + 12min + 42s),
+              make_zoned(locate_zone(time_zone), local_days(2012_y / 11 / 01) 
+ 00h + 00min + 00s));
+    checkNext("0 0 0 1 * ?",
+              make_zoned(locate_zone(time_zone), local_days(2012_y / 11 / 01) 
+ 00h + 00min + 00s),
+              make_zoned(locate_zone(time_zone), local_days(2012_y / 12 / 01) 
+ 00h + 00min + 00s));
+    checkNext("0 0 0 1 * ?",
+              make_zoned(locate_zone(time_zone), local_days(2010_y / 12 / 31) 
+ 15h + 12min + 42s),
+              make_zoned(locate_zone(time_zone), local_days(2011_y / 01 / 01) 
+ 00h + 00min + 00s));
+    checkNext("0 0 0 1 * ?",
+              make_zoned(locate_zone(time_zone), local_days(2011_y / 01 / 01) 
+ 00h + 00min + 00s),
+              make_zoned(locate_zone(time_zone), local_days(2011_y / 02 / 01) 
+ 00h + 00min + 00s));
+    checkNext("0 0 0 31 * ?",
+              make_zoned(locate_zone(time_zone), local_days(2011_y / 10 / 30) 
+ 15h + 12min + 42s),
+              make_zoned(locate_zone(time_zone), local_days(2011_y / 10 / 31) 
+ 00h + 00min + 00s));
+    checkNext("0 0 0 1 * ?",
+              make_zoned(locate_zone(time_zone), local_days(2011_y / 10 / 30) 
+ 15h + 12min + 42s),
+              make_zoned(locate_zone(time_zone), local_days(2011_y / 11 / 01) 
+ 00h + 00min + 00s));
+    checkNext("* * * ? * 2",
+              make_zoned(locate_zone(time_zone), local_days(2010_y / 10 / 25) 
+ 15h + 12min + 42s),
+              make_zoned(locate_zone(time_zone), local_days(2010_y / 10 / 25) 
+ 15h + 12min + 43s));
+    checkNext("* * * ? * 2",
+              make_zoned(locate_zone(time_zone), local_days(2010_y / 10 / 20) 
+ 15h + 12min + 42s),
+              make_zoned(locate_zone(time_zone), local_days(2010_y / 10 / 25) 
+ 00h + 00min + 00s));
+    checkNext("* * * ? * 2",
+              make_zoned(locate_zone(time_zone), local_days(2010_y / 10 / 27) 
+ 15h + 12min + 42s),
+              make_zoned(locate_zone(time_zone), local_days(2010_y / 11 / 01) 
+ 00h + 00min + 00s));
+    checkNext("55 5 * * * ?",
+              make_zoned(locate_zone(time_zone), local_days(2010_y / 10 / 27) 
+ 15h + 04min + 54s),
+              make_zoned(locate_zone(time_zone), local_days(2010_y / 10 / 27) 
+ 15h + 05min + 55s));
+    checkNext("55 5 * * * ?",
+              make_zoned(locate_zone(time_zone), local_days(2010_y / 10 / 27) 
+ 15h + 05min + 55s),
+              make_zoned(locate_zone(time_zone), local_days(2010_y / 10 / 27) 
+ 16h + 05min + 55s));
+    checkNext("55 * 10 * * ?",
+              make_zoned(locate_zone(time_zone), local_days(2010_y / 10 / 27) 
+ 9h + 04min + 54s),
+              make_zoned(locate_zone(time_zone), local_days(2010_y / 10 / 27) 
+ 10h + 00min + 55s));
+    checkNext("55 * 10 * * ?",
+              make_zoned(locate_zone(time_zone), local_days(2010_y / 10 / 27) 
+ 10h + 00min + 55s),
+              make_zoned(locate_zone(time_zone), local_days(2010_y / 10 / 27) 
+ 10h + 01min + 55s));
+    checkNext("* 5 10 * * ?",
+              make_zoned(locate_zone(time_zone), local_days(2010_y / 10 / 27) 
+ 9h + 04min + 55s),
+              make_zoned(locate_zone(time_zone), local_days(2010_y / 10 / 27) 
+ 10h + 05min + 00s));
+    checkNext("* 5 10 * * ?",
+              make_zoned(locate_zone(time_zone), local_days(2010_y / 10 / 27) 
+ 10h + 05min + 00s),
+              make_zoned(locate_zone(time_zone), local_days(2010_y / 10 / 27) 
+ 10h + 05min + 01s));
+    checkNext("55 * * 3 * ?",
+              make_zoned(locate_zone(time_zone), local_days(2010_y / 10 / 02) 
+ 10h + 05min + 54s),
+              make_zoned(locate_zone(time_zone), local_days(2010_y / 10 / 03) 
+ 00h + 00min + 55s));
+    checkNext("55 * * 3 * ?",
+              make_zoned(locate_zone(time_zone), local_days(2010_y / 10 / 03) 
+ 00h + 00min + 55s),
+              make_zoned(locate_zone(time_zone), local_days(2010_y / 10 / 03) 
+ 00h + 01min + 55s));
+    checkNext("* * * 3 11 ?",
+              make_zoned(locate_zone(time_zone), local_days(2010_y / 10 / 02) 
+ 14h + 42min + 55s),
+              make_zoned(locate_zone(time_zone), local_days(2010_y / 11 / 03) 
+ 00h + 00min + 00s));
+    checkNext("* * * 3 11 ?",
+              make_zoned(locate_zone(time_zone), local_days(2010_y / 11 / 03) 
+ 00h + 00min + 00s),
+              make_zoned(locate_zone(time_zone), local_days(2010_y / 11 / 03) 
+ 00h + 00min + 01s));
+    checkNext("0 0 0 29 2 ?",
+              make_zoned(locate_zone(time_zone), local_days(2007_y / 02 / 10) 
+ 14h + 42min + 55s),
+              make_zoned(locate_zone(time_zone), local_days(2008_y / 02 / 29) 
+ 00h + 00min + 00s));
+    checkNext("0 0 0 29 2 ?",
+              make_zoned(locate_zone(time_zone), local_days(2008_y / 02 / 29) 
+ 00h + 00min + 00s),
+              make_zoned(locate_zone(time_zone), local_days(2012_y / 02 / 29) 
+ 00h + 00min + 00s));
+    checkNext("0 0 7 ? * Mon-Fri",
+              make_zoned(locate_zone(time_zone), local_days(2009_y / 9 / 26) + 
00h + 42min + 55s),
+              make_zoned(locate_zone(time_zone), local_days(2009_y / 9 / 28) + 
07h + 00min + 00s));
+    checkNext("0 0 7 ? * Mon-Fri",
+              make_zoned(locate_zone(time_zone), local_days(2009_y / 9 / 26) + 
00h + 42min + 55s),
+              make_zoned(locate_zone(time_zone), local_days(2009_y / 9 / 28) + 
07h + 00min + 00s));
+    checkNext("0 0 7 ? * Mon,Tue,Wed,Thu,Fri",
+              make_zoned(locate_zone(time_zone), local_days(2009_y / 9 / 28) + 
07h + 00min + 00s),
+              make_zoned(locate_zone(time_zone), local_days(2009_y / 9 / 29) + 
07h + 00min + 00s));
+    checkNext("0 30 23 30 1/3 ?",
+              make_zoned(locate_zone(time_zone), local_days(2010_y / 12 / 30) 
+ 00h + 00min + 00s),
+              make_zoned(locate_zone(time_zone), local_days(2011_y / 01 / 30) 
+ 23h + 30min + 00s));
+    checkNext("0 30 23 30 1/3 ?",
+              make_zoned(locate_zone(time_zone), local_days(2011_y / 01 / 30) 
+ 23h + 30min + 00s),
+              make_zoned(locate_zone(time_zone), local_days(2011_y / 04 / 30) 
+ 23h + 30min + 00s));
+    checkNext("0 30 23 30 1/3 ?",
+              make_zoned(locate_zone(time_zone), local_days(2011_y / 04 / 30) 
+ 23h + 30min + 00s),
+              make_zoned(locate_zone(time_zone), local_days(2011_y / 07 / 30) 
+ 23h + 30min + 00s));
+
+    checkNext("0 0 0 LW * ? *",
+              make_zoned(locate_zone(time_zone), local_days(2022_y / 02 / 27) 
+ 02h + 00min + 00s),
+              make_zoned(locate_zone(time_zone), local_days(2022_y / 02 / 28) 
+ 00h + 00min + 00s));
+    checkNext("0 0 0 LW * ? *",
+              make_zoned(locate_zone(time_zone), local_days(2024_y / 02 / 27) 
+ 02h + 00min + 00s),
+              make_zoned(locate_zone(time_zone), local_days(2024_y / 02 / 29) 
+ 00h + 00min + 00s));
+    checkNext("0 0 0 LW * ? *",
+              make_zoned(locate_zone(time_zone), local_days(2027_y / 02 / 27) 
+ 02h + 00min + 00s),
+              make_zoned(locate_zone(time_zone), local_days(2027_y / 03 / 31) 
+ 00h + 00min + 00s));
+    checkNext("0 0 0 ? * 3#1 *",
+              make_zoned(locate_zone(time_zone), local_days(2022_y / 05 / 04) 
+ 00h + 00min + 00s),
+              make_zoned(locate_zone(time_zone), local_days(2022_y / 06 / 07) 
+ 00h + 00min + 00s));
+    checkNext("0 0 0 ? * 3#2 *",
+              make_zoned(locate_zone(time_zone), local_days(2022_y / 05 / 04) 
+ 00h + 00min + 00s),
+              make_zoned(locate_zone(time_zone), local_days(2022_y / 05 / 10) 
+ 00h + 00min + 00s));
+    checkNext("0 0 0 ? * 3#3 *",
+              make_zoned(locate_zone(time_zone), local_days(2022_y / 05 / 04) 
+ 00h + 00min + 00s),
+              make_zoned(locate_zone(time_zone), local_days(2022_y / 05 / 17) 
+ 00h + 00min + 00s));
+    checkNext("0 0 0 ? * 3#4 *",
+              make_zoned(locate_zone(time_zone), local_days(2022_y / 05 / 04) 
+ 00h + 00min + 00s),
+              make_zoned(locate_zone(time_zone), local_days(2022_y / 05 / 24) 
+ 00h + 00min + 00s));
+    checkNext("0 0 0 ? * 3#5 *",
+              make_zoned(locate_zone(time_zone), local_days(2022_y / 05 / 04) 
+ 00h + 00min + 00s),
+              make_zoned(locate_zone(time_zone), local_days(2022_y / 05 / 31) 
+ 00h + 00min + 00s));
+    checkNext("0 0 0 L * ? *",
+              make_zoned(locate_zone(time_zone), local_days(2022_y / 01 / 04) 
+ 00h + 00min + 00s),
+              make_zoned(locate_zone(time_zone), local_days(2022_y / 01 / 31) 
+ 00h + 00min + 00s));
+    checkNext("0 0 0 L * ? *",
+              make_zoned(locate_zone(time_zone), local_days(2022_y / 02 / 04) 
+ 00h + 00min + 00s),
+              make_zoned(locate_zone(time_zone), local_days(2022_y / 02 / 28) 
+ 00h + 00min + 00s));
+    checkNext("0 0 0 L * ? *",
+              make_zoned(locate_zone(time_zone), local_days(2024_y / 02 / 04) 
+ 00h + 00min + 00s),
+              make_zoned(locate_zone(time_zone), local_days(2024_y / 02 / 29) 
+ 00h + 00min + 00s));
+    checkNext("0 0 0 L * ? *",
+              make_zoned(locate_zone(time_zone), local_days(2022_y / 03 / 04) 
+ 00h + 00min + 00s),
+              make_zoned(locate_zone(time_zone), local_days(2022_y / 03 / 31) 
+ 00h + 00min + 00s));
+    checkNext("0 0 0 L * ? *",
+              make_zoned(locate_zone(time_zone), local_days(2022_y / 04 / 04) 
+ 00h + 00min + 00s),
+              make_zoned(locate_zone(time_zone), local_days(2022_y / 04 / 30) 
+ 00h + 00min + 00s));
+    checkNext("0 0 0 L * ? *",
+              make_zoned(locate_zone(time_zone), local_days(2022_y / 05 / 31) 
+ 00h + 00min + 00s),
+              make_zoned(locate_zone(time_zone), local_days(2022_y / 06 / 30) 
+ 00h + 00min + 00s));
+    checkNext("0 0 0 ? * L *",
+              make_zoned(locate_zone(time_zone), local_days(2022_y / 01 / 07) 
+ 00h + 00min + 00s),
+              make_zoned(locate_zone(time_zone), local_days(2022_y / 01 / 8) + 
00h + 00min + 00s));
+    checkNext("0 0 0 ? * L *",
+              make_zoned(locate_zone(time_zone), local_days(2022_y / 02 / 04) 
+ 00h + 00min + 00s),
+              make_zoned(locate_zone(time_zone), local_days(2022_y / 02 / 05) 
+ 00h + 00min + 00s));
+    checkNext("0 0 0 ? * L *",
+              make_zoned(locate_zone(time_zone), local_days(2024_y / 02 / 04) 
+ 00h + 00min + 00s),
+              make_zoned(locate_zone(time_zone), local_days(2024_y / 02 / 10) 
+ 00h + 00min + 00s));
+    checkNext("0 0 0 ? * L *",
+              make_zoned(locate_zone(time_zone), local_days(2022_y / 03 / 04) 
+ 00h + 00min + 00s),
+              make_zoned(locate_zone(time_zone), local_days(2022_y / 03 / 05) 
+ 00h + 00min + 00s));
+    checkNext("0 0 0 ? * L *",
+              make_zoned(locate_zone(time_zone), local_days(2022_y / 04 / 04) 
+ 00h + 00min + 00s),
+              make_zoned(locate_zone(time_zone), local_days(2022_y / 04 / 9) + 
00h + 00min + 00s));
+    checkNext("0 0 0 ? * L *",
+              make_zoned(locate_zone(time_zone), local_days(2022_y / 05 / 28) 
+ 00h + 00min + 00s),
+              make_zoned(locate_zone(time_zone), local_days(2022_y / 06 / 04) 
+ 00h + 00min + 00s));
+    checkNext("0 0 0 1W * ? *",
+              make_zoned(locate_zone(time_zone), local_days(2022_y / 05 / 01) 
+ 00h + 00min + 00s),
+              make_zoned(locate_zone(time_zone), local_days(2022_y / 05 / 02) 
+ 00h + 00min + 00s));
+    checkNext("0 0 0 4W * ? *",
+              make_zoned(locate_zone(time_zone), local_days(2022_y / 05 / 01) 
+ 00h + 00min + 00s),
+              make_zoned(locate_zone(time_zone), local_days(2022_y / 05 / 04) 
+ 00h + 00min + 00s));
+    checkNext("0 0 0 14W * ? *",
+              make_zoned(locate_zone(time_zone), local_days(2022_y / 05 / 01) 
+ 00h + 00min + 00s),
+              make_zoned(locate_zone(time_zone), local_days(2022_y / 05 / 13) 
+ 00h + 00min + 00s));
+    checkNext("0 0 0 15W * ? *",
+              make_zoned(locate_zone(time_zone), local_days(2022_y / 05 / 01) 
+ 00h + 00min + 00s),
+              make_zoned(locate_zone(time_zone), local_days(2022_y / 05 / 16) 
+ 00h + 00min + 00s));
+    checkNext("0 0 0 31W * ? *",
+              make_zoned(locate_zone(time_zone), local_days(2022_y / 02 / 01) 
+ 00h + 00min + 00s),
+              make_zoned(locate_zone(time_zone), local_days(2022_y / 03 / 31) 
+ 00h + 00min + 00s));
+    checkNext("0 0 0 1W * ? *",
+              make_zoned(locate_zone(time_zone), local_days(2021_y / 12 / 15) 
+ 00h + 00min + 00s),
+              make_zoned(locate_zone(time_zone), local_days(2022_y / 01 / 03) 
+ 00h + 00min + 00s));
+    checkNext("0 0 0 31W * ? *",
+              make_zoned(locate_zone(time_zone), local_days(2022_y / 07 / 15) 
+ 00h + 00min + 00s),
+              make_zoned(locate_zone(time_zone), local_days(2022_y / 07 / 29) 
+ 00h + 00min + 00s));
+
+    checkNext("0 15 10 ? * 6L",
+              make_zoned(locate_zone(time_zone), local_days(2022_y / 07 / 15) 
+ 00h + 00min + 00s),
+              make_zoned(locate_zone(time_zone), local_days(2022_y / 07 / 29) 
+ 10h + 15min + 00s));
+
+    checkNext("0 0 0 L-3 * ?",
+              make_zoned(locate_zone(time_zone), local_days(2022_y / 01 / 10) 
+ 00h + 00min + 00s),
+              make_zoned(locate_zone(time_zone), local_days(2022_y / 01 / 28) 
+ 00h + 00min + 00s));
+
+    checkNext("0 0 0 L-30 * ?",
+              make_zoned(locate_zone(time_zone), local_days(2022_y / 01 / 10) 
+ 00h + 00min + 00s),
+              make_zoned(locate_zone(time_zone), local_days(2022_y / 03 / 01) 
+ 00h + 00min + 00s));
+  }
+}
diff --git a/libminifi/test/unit/SchedulingAgentTests.cpp 
b/libminifi/test/unit/SchedulingAgentTests.cpp
new file mode 100644
index 000000000..c3e03a356
--- /dev/null
+++ b/libminifi/test/unit/SchedulingAgentTests.cpp
@@ -0,0 +1,140 @@
+/**
+ * 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 ?");
+    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_);
+    if (first_task_reschedule_info.wait_time_ > 1min) {  // To avoid possibly 
failing around dec 31 23:59:59
+      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);
+
+    std::this_thread::sleep_for(first_task_reschedule_info.wait_time_ + 1ms);
+    auto second_task_reschedule_info = 
cron_driven_agent->run(count_proc.get(), context, factory);
+    CHECK(!second_task_reschedule_info.finished_);
+    CHECK(second_task_reschedule_info.wait_time_ <= 1s);
+    CHECK(count_proc->getNumberOfTriggers() == 1);
+  }
+
+  SECTION("Cron Driven no future triggers") {
+    count_proc->setCronPeriod("* * * * * * 2012");
+    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_);
+  }
+}
+}  // namespace org::apache::nifi::minifi::testing
diff --git a/libminifi/test/unit/TimeUtilTests.cpp 
b/libminifi/test/unit/TimeUtilTests.cpp
index 574cc837c..0a96e5134 100644
--- a/libminifi/test/unit/TimeUtilTests.cpp
+++ b/libminifi/test/unit/TimeUtilTests.cpp
@@ -150,3 +150,94 @@ TEST_CASE("Test string to duration conversion", 
"[timedurationtests]") {
   REQUIRE_FALSE(StringToDuration<std::chrono::seconds>("5 apples") == 1s);
   REQUIRE_FALSE(StringToDuration<std::chrono::seconds>("1 year") == 1s);
 }
+
+namespace {
+date::local_time<std::chrono::seconds> parseLocalTimePoint(const std::string& 
str) {
+  date::local_time<std::chrono::seconds> tp;
+  std::stringstream stream(str);
+  date::from_stream(stream, "%Y-%m-%d %T", tp);
+  return tp;
+}
+}  // namespace
+
+TEST_CASE("Test roundToNextYear", "[roundingTests]") {
+  using org::apache::nifi::minifi::utils::timeutils::roundToNextYear;
+
+  CHECK(parseLocalTimePoint("2022-01-01 00:00:00") == 
roundToNextYear(parseLocalTimePoint("2021-12-21 08:20:53")));
+  CHECK(parseLocalTimePoint("2023-01-01 00:00:00") == 
roundToNextYear(parseLocalTimePoint("2022-01-01 13:59:59")));
+  CHECK(parseLocalTimePoint("1974-01-01 00:00:00") == 
roundToNextYear(parseLocalTimePoint("1973-02-01 00:00:01")));
+  CHECK(parseLocalTimePoint("1971-01-01 00:00:00") == 
roundToNextYear(parseLocalTimePoint("1970-11-03 23:59:59")));
+  CHECK(parseLocalTimePoint("2023-01-01 00:00:00") == 
roundToNextYear(parseLocalTimePoint("2022-12-03 15:22:22")));
+  CHECK(parseLocalTimePoint("2254-01-01 00:00:00") == 
roundToNextYear(parseLocalTimePoint("2253-05-01 23:59:59")));
+  CHECK(parseLocalTimePoint("1951-01-01 00:00:00") == 
roundToNextYear(parseLocalTimePoint("1950-11-03 23:59:59")));
+}
+
+TEST_CASE("Test roundToNextMonth", "[roundingTests]") {
+  using org::apache::nifi::minifi::utils::timeutils::roundToNextMonth;
+
+  CHECK(parseLocalTimePoint("2022-01-01 00:00:00") == 
roundToNextMonth(parseLocalTimePoint("2021-12-21 01:00:00")));
+  CHECK(parseLocalTimePoint("2022-02-01 00:00:00") == 
roundToNextMonth(parseLocalTimePoint("2022-01-31 02:00:00")));
+  CHECK(parseLocalTimePoint("2022-03-01 00:00:00") == 
roundToNextMonth(parseLocalTimePoint("2022-02-01 12:00:00")));
+  CHECK(parseLocalTimePoint("2022-12-01 00:00:00") == 
roundToNextMonth(parseLocalTimePoint("2022-11-30 00:00:00")));
+  CHECK(parseLocalTimePoint("2023-01-01 00:00:00") == 
roundToNextMonth(parseLocalTimePoint("2022-12-31 00:00:00")));
+  CHECK(parseLocalTimePoint("2022-06-01 00:00:00") == 
roundToNextMonth(parseLocalTimePoint("2022-05-12 23:00:58")));
+  CHECK(parseLocalTimePoint("2022-07-01 00:00:00") == 
roundToNextMonth(parseLocalTimePoint("2022-06-21 11:00:00")));
+  CHECK(parseLocalTimePoint("2022-08-01 00:00:00") == 
roundToNextMonth(parseLocalTimePoint("2022-07-21 12:12:00")));
+  CHECK(parseLocalTimePoint("2022-09-01 00:00:00") == 
roundToNextMonth(parseLocalTimePoint("2022-08-31 06:00:00")));
+}
+
+TEST_CASE("Test roundToNextDay", "[roundingTests]") {
+  using org::apache::nifi::minifi::utils::timeutils::roundToNextDay;
+
+  CHECK(parseLocalTimePoint("2021-02-01 00:00:00") == 
roundToNextDay(parseLocalTimePoint("2021-01-31 01:00:00")));
+  CHECK(parseLocalTimePoint("2022-03-01 00:00:00") == 
roundToNextDay(parseLocalTimePoint("2022-02-28 02:00:00")));
+  CHECK(parseLocalTimePoint("2024-02-29 00:00:00") == 
roundToNextDay(parseLocalTimePoint("2024-02-28 02:00:00")));
+  CHECK(parseLocalTimePoint("2023-01-01 00:00:00") == 
roundToNextDay(parseLocalTimePoint("2022-12-31 12:00:00")));
+  CHECK(parseLocalTimePoint("2022-07-11 00:00:00") == 
roundToNextDay(parseLocalTimePoint("2022-07-10 00:00:00")));
+  CHECK(parseLocalTimePoint("2023-01-01 00:00:00") == 
roundToNextDay(parseLocalTimePoint("2022-12-31 00:00:00")));
+  CHECK(parseLocalTimePoint("2022-05-13 00:00:00") == 
roundToNextDay(parseLocalTimePoint("2022-05-12 23:00:58")));
+  CHECK(parseLocalTimePoint("2022-06-22 00:00:00") == 
roundToNextDay(parseLocalTimePoint("2022-06-21 11:00:00")));
+  CHECK(parseLocalTimePoint("2022-07-22 00:00:00") == 
roundToNextDay(parseLocalTimePoint("2022-07-21 12:12:00")));
+}
+
+TEST_CASE("Test roundToNextHour", "[roundingTests]") {
+  using org::apache::nifi::minifi::utils::timeutils::roundToNextHour;
+
+  CHECK(parseLocalTimePoint("2021-01-31 02:00:00") == 
roundToNextHour(parseLocalTimePoint("2021-01-31 01:00:00")));
+  CHECK(parseLocalTimePoint("2022-03-01 00:00:00") == 
roundToNextHour(parseLocalTimePoint("2022-02-28 23:00:00")));
+  CHECK(parseLocalTimePoint("2024-02-29 00:00:00") == 
roundToNextHour(parseLocalTimePoint("2024-02-28 23:00:00")));
+  CHECK(parseLocalTimePoint("2022-12-31 13:00:00") == 
roundToNextHour(parseLocalTimePoint("2022-12-31 12:00:00")));
+  CHECK(parseLocalTimePoint("2022-07-10 01:00:00") == 
roundToNextHour(parseLocalTimePoint("2022-07-10 00:00:00")));
+  CHECK(parseLocalTimePoint("2022-12-31 01:00:00") == 
roundToNextHour(parseLocalTimePoint("2022-12-31 00:00:00")));
+  CHECK(parseLocalTimePoint("2022-05-13 00:00:00") == 
roundToNextHour(parseLocalTimePoint("2022-05-12 23:00:58")));
+  CHECK(parseLocalTimePoint("2022-06-21 12:00:00") == 
roundToNextHour(parseLocalTimePoint("2022-06-21 11:00:00")));
+  CHECK(parseLocalTimePoint("2022-07-21 13:00:00") == 
roundToNextHour(parseLocalTimePoint("2022-07-21 12:12:00")));
+}
+
+TEST_CASE("Test roundToNextMinute", "[roundingTests]") {
+  using org::apache::nifi::minifi::utils::timeutils::roundToNextMinute;
+
+  CHECK(parseLocalTimePoint("2021-01-31 01:24:00") == 
roundToNextMinute(parseLocalTimePoint("2021-01-31 01:23:00")));
+  CHECK(parseLocalTimePoint("2022-03-01 00:00:00") == 
roundToNextMinute(parseLocalTimePoint("2022-02-28 23:59:59")));
+  CHECK(parseLocalTimePoint("2024-02-28 23:01:00") == 
roundToNextMinute(parseLocalTimePoint("2024-02-28 23:00:00")));
+  CHECK(parseLocalTimePoint("2022-12-31 12:01:00") == 
roundToNextMinute(parseLocalTimePoint("2022-12-31 12:00:00")));
+  CHECK(parseLocalTimePoint("2022-07-10 00:01:00") == 
roundToNextMinute(parseLocalTimePoint("2022-07-10 00:00:00")));
+  CHECK(parseLocalTimePoint("2022-12-31 00:01:00") == 
roundToNextMinute(parseLocalTimePoint("2022-12-31 00:00:00")));
+  CHECK(parseLocalTimePoint("2022-05-12 23:01:00") == 
roundToNextMinute(parseLocalTimePoint("2022-05-12 23:00:58")));
+  CHECK(parseLocalTimePoint("2022-06-21 11:01:00") == 
roundToNextMinute(parseLocalTimePoint("2022-06-21 11:00:00")));
+  CHECK(parseLocalTimePoint("2022-07-21 12:13:00") == 
roundToNextMinute(parseLocalTimePoint("2022-07-21 12:12:00")));
+}
+
+TEST_CASE("Test roundToNextSecond", "[roundingTests]") {
+  using org::apache::nifi::minifi::utils::timeutils::roundToNextSecond;
+
+  CHECK(parseLocalTimePoint("2021-01-31 01:23:01") == 
roundToNextSecond(parseLocalTimePoint("2021-01-31 01:23:00")));
+  CHECK(parseLocalTimePoint("2022-03-01 00:00:00") == 
roundToNextSecond(parseLocalTimePoint("2022-02-28 23:59:59")));
+  CHECK(parseLocalTimePoint("2024-02-28 23:00:01") == 
roundToNextSecond(parseLocalTimePoint("2024-02-28 23:00:00")));
+  CHECK(parseLocalTimePoint("2022-12-31 12:00:01") == 
roundToNextSecond(parseLocalTimePoint("2022-12-31 12:00:00")));
+  CHECK(parseLocalTimePoint("2022-07-10 00:00:01") == 
roundToNextSecond(parseLocalTimePoint("2022-07-10 00:00:00")));
+  CHECK(parseLocalTimePoint("2022-12-31 00:00:01") == 
roundToNextSecond(parseLocalTimePoint("2022-12-31 00:00:00")));
+  CHECK(parseLocalTimePoint("2022-05-12 23:00:59") == 
roundToNextSecond(parseLocalTimePoint("2022-05-12 23:00:58")));
+  CHECK(parseLocalTimePoint("2022-06-21 11:00:01") == 
roundToNextSecond(parseLocalTimePoint("2022-06-21 11:00:00")));
+  CHECK(parseLocalTimePoint("2022-07-21 12:12:01") == 
roundToNextSecond(parseLocalTimePoint("2022-07-21 12:12:00")));
+}
diff --git a/thirdparty/cron/Cron.h b/thirdparty/cron/Cron.h
deleted file mode 100644
index 6992d47e2..000000000
--- a/thirdparty/cron/Cron.h
+++ /dev/null
@@ -1,121 +0,0 @@
-#include <chrono>
-#include <string>
-#include <sstream>
-#include <vector>
-#include <iterator>
-
-namespace Bosma {
-    using Clock = std::chrono::system_clock;
-
-    inline void add(std::tm &tm, Clock::duration time) {
-      auto tp = Clock::from_time_t(std::mktime(&tm));
-      auto tp_adjusted = tp + time;
-      auto tm_adjusted = Clock::to_time_t(tp_adjusted);
-      tm = *std::localtime(&tm_adjusted);
-    }
-
-    class BadCronExpression : public std::exception {
-    public:
-        explicit BadCronExpression(std::string msg) : msg_(std::move(msg)) {}
-
-        const char *what() const noexcept override { return (msg_.c_str()); }
-
-    private:
-        std::string msg_;
-    };
-
-    inline void
-    verify_and_set(const std::string &token, const std::string &expression, 
int &field, const int lower_bound,
-                   const int upper_bound, const bool adjust = false) {
-      if (token == "*")
-        field = -1;
-      else {
-        try {
-          field = std::stoi(token);
-        } catch (const std::invalid_argument &) {
-          throw BadCronExpression("malformed cron string (`" + token + "` not 
an integer or *): " + expression);
-        } catch (const std::out_of_range &) {
-          throw BadCronExpression("malformed cron string (`" + token + "` not 
convertable to int): " + expression);
-        }
-        if (field < lower_bound || field > upper_bound) {
-          std::ostringstream oss;
-          oss << "malformed cron string ('" << token << "' must be <= " << 
upper_bound << " and >= " << lower_bound
-              << "): " << expression;
-          throw BadCronExpression(oss.str());
-        }
-        if (adjust)
-          field--;
-      }
-    }
-
-    class Cron {
-    public:
-        explicit Cron(const std::string &expression) {
-          std::istringstream iss(expression);
-          std::vector<std::string> 
tokens{std::istream_iterator<std::string>{iss},
-                                          
std::istream_iterator<std::string>{}};
-
-          if (tokens.size() != 5) throw BadCronExpression("malformed cron 
string (must be 5 fields): " + expression);
-
-          verify_and_set(tokens[0], expression, minute, 0, 59);
-          verify_and_set(tokens[1], expression, hour, 0, 23);
-          verify_and_set(tokens[2], expression, day, 1, 31);
-          verify_and_set(tokens[3], expression, month, 1, 12, true);
-          verify_and_set(tokens[4], expression, day_of_week, 0, 6);
-        }
-
-        // http://stackoverflow.com/a/322058/1284550
-        Clock::time_point cron_to_next(const Clock::time_point from = 
Clock::now()) const {
-          // get current time as a tm object
-          auto now = Clock::to_time_t(from);
-          std::tm next(*std::localtime(&now));
-          // it will always at least run the next minute
-          next.tm_sec = 0;
-          add(next, std::chrono::minutes(1));
-          while (true) {
-            if (month != -1 && next.tm_mon != month) {
-              // add a month
-              // if this will bring us over a year, increment the year instead 
and reset the month
-              if (next.tm_mon + 1 > 11) {
-                next.tm_mon = 0;
-                next.tm_year++;
-              } else
-                next.tm_mon++;
-
-              next.tm_mday = 1;
-              next.tm_hour = 0;
-              next.tm_min = 0;
-              continue;
-            }
-            if (day != -1 && next.tm_mday != day) {
-              add(next, std::chrono::hours(24));
-              next.tm_hour = 0;
-              next.tm_min = 0;
-              continue;
-            }
-            if (day_of_week != -1 && next.tm_wday != day_of_week) {
-              add(next, std::chrono::hours(24));
-              next.tm_hour = 0;
-              next.tm_min = 0;
-              continue;
-            }
-            if (hour != -1 && next.tm_hour != hour) {
-              add(next, std::chrono::hours(1));
-              next.tm_min = 0;
-              continue;
-            }
-            if (minute != -1 && next.tm_min != minute) {
-              add(next, std::chrono::minutes(1));
-              continue;
-            }
-            break;
-          }
-
-          // telling mktime to figure out dst
-          next.tm_isdst = -1;
-          return Clock::from_time_t(std::mktime(&next));
-        }
-
-        int minute, hour, day, month, day_of_week;
-    };
-}
diff --git a/thirdparty/cron/LICENSE.TXT b/thirdparty/cron/LICENSE.TXT
deleted file mode 100644
index 8e86f896d..000000000
--- a/thirdparty/cron/LICENSE.TXT
+++ /dev/null
@@ -1,21 +0,0 @@
-MIT License
-
-Copyright (c) 2017 Bosma
-
-Permission is hereby granted, free of charge, to any person obtaining a copy
-of this software and associated documentation files (the "Software"), to deal
-in the Software without restriction, including without limitation the rights
-to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
-copies of the Software, and to permit persons to whom the Software is
-furnished to do so, subject to the following conditions:
-
-The above copyright notice and this permission notice shall be included in all
-copies or substantial portions of the Software.
-
-THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
-AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
-OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
-SOFTWARE.
\ No newline at end of file

Reply via email to