M module-utils/CMakeLists.txt => module-utils/CMakeLists.txt +1 -0
@@ 60,6 60,7 @@ add_subdirectory(EventStore)
add_subdirectory(i18n)
add_subdirectory(log)
add_subdirectory(math)
+add_subdirectory(rrule)
add_subdirectory(time)
add_subdirectory(ucs2)
add_subdirectory(utf8)
A module-utils/rrule/CMakeLists.txt => module-utils/rrule/CMakeLists.txt +24 -0
@@ 0,0 1,24 @@
+add_library(rrule)
+
+target_sources(rrule
+ PRIVATE
+ rrule/rrule.cpp
+
+ PUBLIC
+ rrule/rrule.hpp
+)
+
+target_include_directories(rrule PUBLIC $<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}>)
+
+target_link_libraries(rrule
+ PUBLIC
+ date
+ PRIVATE
+ ical_cxx
+)
+
+module_is_test_entity(rrule)
+
+if (${ENABLE_TESTS})
+ add_subdirectory(test)
+endif()
A module-utils/rrule/rrule/rrule.cpp => module-utils/rrule/rrule/rrule.cpp +236 -0
@@ 0,0 1,236 @@
+// Copyright (c) 2017-2021, Mudita Sp. z.o.o. All rights reserved.
+// For licensing, see https://github.com/mudita/MuditaOS/LICENSE.md
+
+#include "rrule.hpp"
+
+extern "C"
+{
+#include <icalrecur.h>
+}
+
+namespace rrule
+{
+ icalrecurrencetype RRuletoIcalRecurrenceType(const RRule &rrule);
+
+ RRule::RRuleFrequency icalFrequencyToRRuleFrequency(const icalrecurrencetype_frequency freq);
+ RRule::RRuleWeekday icalWeekStartToRRuleWeekStart(const icalrecurrencetype_weekday weekday);
+ icalrecurrencetype_frequency RRuleFrequencyToIcalFrequency(const RRule::RRuleFrequency freq);
+ icalrecurrencetype_weekday RRuleWeekStartToIcalWeekStart(const RRule::RRuleWeekday weekday);
+
+ template <typename arrayT, typename vectT>
+ std::vector<vectT> icalArrayToVector(const arrayT *array, unsigned max_size);
+ template <typename arrayT, typename vectT>
+ unsigned vectorToIcalArray(const std::vector<vectT> &vect, arrayT *array, unsigned max_size);
+
+ void RRule::parseFromString(std::string_view str)
+ {
+ const auto icalRrule = icalrecurrencetype_from_string(str.data());
+ const time_t untilTimeT = icaltime_as_timet(icalRrule.until);
+
+ if (untilTimeT == 0) {
+ until = TimePoint::min();
+ }
+ else {
+ until = std::chrono::system_clock::from_time_t(untilTimeT);
+ }
+ freq = icalFrequencyToRRuleFrequency(icalRrule.freq);
+ count = icalRrule.count;
+ interval = icalRrule.interval;
+ weekStart = icalWeekStartToRRuleWeekStart(icalRrule.week_start);
+
+ bySecond = icalArrayToVector<short, decltype(bySecond)::value_type>(icalRrule.by_second, ICAL_BY_SECOND_SIZE);
+ byMinute = icalArrayToVector<short, decltype(byMinute)::value_type>(icalRrule.by_minute, ICAL_BY_MINUTE_SIZE);
+ byHour = icalArrayToVector<short, decltype(byHour)::value_type>(icalRrule.by_hour, ICAL_BY_HOUR_SIZE);
+ byDay = icalArrayToVector<short, decltype(byDay)::value_type>(icalRrule.by_day, ICAL_BY_DAY_SIZE);
+ byMonthDay =
+ icalArrayToVector<short, decltype(byMonthDay)::value_type>(icalRrule.by_month_day, ICAL_BY_MONTHDAY_SIZE);
+ byYearDay =
+ icalArrayToVector<short, decltype(byYearDay)::value_type>(icalRrule.by_year_day, ICAL_BY_YEARDAY_SIZE);
+ byWeekNo = icalArrayToVector<short, decltype(byWeekNo)::value_type>(icalRrule.by_week_no, ICAL_BY_WEEKNO_SIZE);
+ byMonth = icalArrayToVector<short, decltype(byMonth)::value_type>(icalRrule.by_month, ICAL_BY_MONTH_SIZE);
+ bySetPos = icalArrayToVector<short, decltype(bySetPos)::value_type>(icalRrule.by_set_pos, ICAL_BY_SETPOS_SIZE);
+ }
+
+ std::string RRule::parseToString()
+ {
+ auto icalRRule = RRuletoIcalRecurrenceType(*this);
+ return std::string{icalrecurrencetype_as_string(&icalRRule)};
+ }
+
+ std::vector<TimePoint> RRule::generateEventTimePoints(const TimePoint start,
+ const TimePoint end,
+ const unsigned int count)
+ {
+ std::vector<TimePoint> eventsTimePoints;
+ auto icalTimeStart = icaltime_from_timet_with_zone(std::chrono::system_clock::to_time_t(start), 0, NULL);
+ auto icalTimeEnd = icaltime_from_timet_with_zone(std::chrono::system_clock::to_time_t(end), 0, NULL);
+ unsigned int counter = 0;
+ const icalrecurrencetype recur = RRuletoIcalRecurrenceType(*this);
+
+ auto ritr = icalrecur_iterator_new(recur, icalTimeStart);
+ icalrecur_iterator_set_range(ritr, icalTimeStart, icalTimeEnd);
+ icaltimetype next = icalrecur_iterator_next(ritr);
+
+ while (!icaltime_is_null_time(next) && counter < count) {
+ auto singleEventTimePoint = std::chrono::system_clock::from_time_t(icaltime_as_timet(next));
+ next = icalrecur_iterator_next(ritr);
+ eventsTimePoints.push_back(singleEventTimePoint);
+ counter++;
+ }
+
+ icalrecur_iterator_free(ritr);
+ return eventsTimePoints;
+ }
+
+ icalrecurrencetype RRuletoIcalRecurrenceType(const RRule &rrule)
+ {
+ icalrecurrencetype icalRrule = ICALRECURRENCETYPE_INITIALIZER;
+
+ icalRrule.freq = RRuleFrequencyToIcalFrequency(rrule.freq);
+ icalRrule.count = rrule.count;
+
+ if (rrule.until != TimePoint::min()) {
+ icalRrule.until = icaltime_from_timet_with_zone(std::chrono::system_clock::to_time_t(rrule.until), 0, NULL);
+ }
+ icalRrule.interval = rrule.interval;
+ icalRrule.week_start = RRuleWeekStartToIcalWeekStart(rrule.weekStart);
+
+ vectorToIcalArray<short, decltype(rrule.bySecond)::value_type>(
+ rrule.bySecond, icalRrule.by_second, ICAL_BY_SECOND_SIZE);
+ vectorToIcalArray<short, decltype(rrule.byMinute)::value_type>(
+ rrule.byMinute, icalRrule.by_minute, ICAL_BY_MINUTE_SIZE);
+ vectorToIcalArray<short, decltype(rrule.byHour)::value_type>(
+ rrule.byHour, icalRrule.by_hour, ICAL_BY_HOUR_SIZE);
+ vectorToIcalArray<short, decltype(rrule.byDay)::value_type>(rrule.byDay, icalRrule.by_day, ICAL_BY_DAY_SIZE);
+
+ vectorToIcalArray<short, decltype(rrule.byMonthDay)::value_type>(
+ rrule.byMonthDay, icalRrule.by_month_day, ICAL_BY_MONTHDAY_SIZE);
+ vectorToIcalArray<short, decltype(rrule.byYearDay)::value_type>(
+ rrule.byYearDay, icalRrule.by_year_day, ICAL_BY_YEARDAY_SIZE);
+ vectorToIcalArray<short, decltype(rrule.byWeekNo)::value_type>(
+ rrule.byWeekNo, icalRrule.by_week_no, ICAL_BY_WEEKNO_SIZE);
+ vectorToIcalArray<short, decltype(rrule.byMonth)::value_type>(
+ rrule.byMonth, icalRrule.by_month, ICAL_BY_MONTH_SIZE);
+ vectorToIcalArray<short, decltype(rrule.bySetPos)::value_type>(
+ rrule.bySetPos, icalRrule.by_set_pos, ICAL_BY_SETPOS_SIZE);
+
+ return icalRrule;
+ }
+
+ RRule::RRuleFrequency icalFrequencyToRRuleFrequency(const icalrecurrencetype_frequency freq)
+ {
+ switch (freq) {
+ case ICAL_SECONDLY_RECURRENCE:
+ return RRule::RRuleFrequency::SECONDLY_RECURRENCE;
+ case ICAL_MINUTELY_RECURRENCE:
+ return RRule::RRuleFrequency::MINUTELY_RECURRENCE;
+ case ICAL_HOURLY_RECURRENCE:
+ return RRule::RRuleFrequency::HOURLY_RECURRENCE;
+ case ICAL_DAILY_RECURRENCE:
+ return RRule::RRuleFrequency::DAILY_RECURRENCE;
+ case ICAL_WEEKLY_RECURRENCE:
+ return RRule::RRuleFrequency::WEEKLY_RECURRENCE;
+ case ICAL_MONTHLY_RECURRENCE:
+ return RRule::RRuleFrequency::MONTHLY_RECURRENCE;
+ case ICAL_YEARLY_RECURRENCE:
+ return RRule::RRuleFrequency::YEARLY_RECURRENCE;
+ case ICAL_NO_RECURRENCE:
+ default:
+ return RRule::RRuleFrequency::NO_RECURRENCE;
+ }
+ }
+
+ RRule::RRuleWeekday icalWeekStartToRRuleWeekStart(const icalrecurrencetype_weekday weekday)
+ {
+ switch (weekday) {
+ case ICAL_SUNDAY_WEEKDAY:
+ return RRule::RRuleWeekday::SUNDAY_WEEKDAY;
+ case ICAL_MONDAY_WEEKDAY:
+ return RRule::RRuleWeekday::MONDAY_WEEKDAY;
+ case ICAL_TUESDAY_WEEKDAY:
+ return RRule::RRuleWeekday::TUESDAY_WEEKDAY;
+ case ICAL_WEDNESDAY_WEEKDAY:
+ return RRule::RRuleWeekday::WEDNESDAY_WEEKDAY;
+ case ICAL_THURSDAY_WEEKDAY:
+ return RRule::RRuleWeekday::THURSDAY_WEEKDAY;
+ case ICAL_FRIDAY_WEEKDAY:
+ return RRule::RRuleWeekday::FRIDAY_WEEKDAY;
+ case ICAL_SATURDAY_WEEKDAY:
+ return RRule::RRuleWeekday::SATURDAY_WEEKDAY;
+ case ICAL_NO_WEEKDAY:
+ default:
+ return RRule::RRuleWeekday::NO_WEEKDAY;
+ }
+ }
+
+ icalrecurrencetype_frequency RRuleFrequencyToIcalFrequency(const RRule::RRuleFrequency freq)
+ {
+ switch (freq) {
+ case RRule::RRuleFrequency::SECONDLY_RECURRENCE:
+ return ICAL_SECONDLY_RECURRENCE;
+ case RRule::RRuleFrequency::MINUTELY_RECURRENCE:
+ return ICAL_MINUTELY_RECURRENCE;
+ case RRule::RRuleFrequency::HOURLY_RECURRENCE:
+ return ICAL_HOURLY_RECURRENCE;
+ case RRule::RRuleFrequency::DAILY_RECURRENCE:
+ return ICAL_DAILY_RECURRENCE;
+ case RRule::RRuleFrequency::WEEKLY_RECURRENCE:
+ return ICAL_WEEKLY_RECURRENCE;
+ case RRule::RRuleFrequency::MONTHLY_RECURRENCE:
+ return ICAL_MONTHLY_RECURRENCE;
+ case RRule::RRuleFrequency::YEARLY_RECURRENCE:
+ return ICAL_YEARLY_RECURRENCE;
+ case RRule::RRuleFrequency::NO_RECURRENCE:
+ default:
+ return ICAL_NO_RECURRENCE;
+ }
+ }
+
+ icalrecurrencetype_weekday RRuleWeekStartToIcalWeekStart(const RRule::RRuleWeekday weekday)
+ {
+ switch (weekday) {
+ case RRule::RRuleWeekday::SUNDAY_WEEKDAY:
+ return ICAL_SUNDAY_WEEKDAY;
+ case RRule::RRuleWeekday::MONDAY_WEEKDAY:
+ return ICAL_MONDAY_WEEKDAY;
+ case RRule::RRuleWeekday::TUESDAY_WEEKDAY:
+ return ICAL_TUESDAY_WEEKDAY;
+ case RRule::RRuleWeekday::WEDNESDAY_WEEKDAY:
+ return ICAL_WEDNESDAY_WEEKDAY;
+ case RRule::RRuleWeekday::THURSDAY_WEEKDAY:
+ return ICAL_THURSDAY_WEEKDAY;
+ case RRule::RRuleWeekday::FRIDAY_WEEKDAY:
+ return ICAL_FRIDAY_WEEKDAY;
+ case RRule::RRuleWeekday::SATURDAY_WEEKDAY:
+ return ICAL_SATURDAY_WEEKDAY;
+ case RRule::RRuleWeekday::NO_WEEKDAY:
+ default:
+ return ICAL_NO_WEEKDAY;
+ }
+ }
+
+ template <typename arrayT, typename vectT>
+ std::vector<vectT> icalArrayToVector(const arrayT *array, unsigned max_size)
+ {
+ std::vector<vectT> vect;
+ for (unsigned i = 0; (i < max_size) && (array[i] != ICAL_RECURRENCE_ARRAY_MAX); i++) {
+ vect.push_back(array[i]);
+ }
+ return vect;
+ }
+
+ template <typename arrayT, typename vectT>
+ unsigned vectorToIcalArray(const std::vector<vectT> &vect, arrayT *array, unsigned max_size)
+ {
+ if (vect.size() > max_size) {
+ return 0;
+ }
+
+ unsigned i;
+ for (i = 0; (i < vect.size()) && (i < max_size); i++) {
+ array[i] = vect[i];
+ }
+ std::fill(array + vect.size(), array + max_size - vect.size(), ICAL_RECURRENCE_ARRAY_MAX);
+ return i;
+ }
+} // namespace rrule
A module-utils/rrule/rrule/rrule.hpp => module-utils/rrule/rrule/rrule.hpp +64 -0
@@ 0,0 1,64 @@
+// Copyright (c) 2017-2021, Mudita Sp. z.o.o. All rights reserved.
+// For licensing, see https://github.com/mudita/MuditaOS/LICENSE.md
+
+#include <chrono>
+#include <cstdint>
+#include <string>
+#include <vector>
+
+namespace rrule
+{
+
+ using TimePoint = std::chrono::time_point<std::chrono::system_clock>;
+
+ class RRule
+ {
+
+ public:
+ enum class RRuleFrequency
+ {
+ SECONDLY_RECURRENCE,
+ MINUTELY_RECURRENCE,
+ HOURLY_RECURRENCE,
+ DAILY_RECURRENCE,
+ WEEKLY_RECURRENCE,
+ MONTHLY_RECURRENCE,
+ YEARLY_RECURRENCE,
+ NO_RECURRENCE,
+ };
+ enum class RRuleWeekday
+ {
+ NO_WEEKDAY,
+ SUNDAY_WEEKDAY,
+ MONDAY_WEEKDAY,
+ TUESDAY_WEEKDAY,
+ WEDNESDAY_WEEKDAY,
+ THURSDAY_WEEKDAY,
+ FRIDAY_WEEKDAY,
+ SATURDAY_WEEKDAY,
+ };
+
+ RRuleFrequency freq{RRuleFrequency::NO_RECURRENCE};
+ /* until and count are mutually exclusive. */
+ std::uint32_t count{0};
+ TimePoint until{TimePoint::min()};
+ std::uint16_t interval{0};
+ RRuleWeekday weekStart{RRuleWeekday::NO_WEEKDAY};
+
+ std::vector<std::uint8_t> bySecond;
+ std::vector<std::uint8_t> byMinute;
+ std::vector<std::uint8_t> byHour;
+ std::vector<std::uint8_t> byDay;
+ std::vector<std::uint8_t> byMonthDay;
+ std::vector<std::uint16_t> byYearDay;
+ std::vector<std::uint8_t> byWeekNo;
+ std::vector<std::uint8_t> byMonth;
+ std::vector<std::uint16_t> bySetPos;
+
+ void parseFromString(std::string_view str);
+ std::string parseToString();
+ std::vector<TimePoint> generateEventTimePoints(const TimePoint start,
+ const TimePoint end,
+ const unsigned int count);
+ };
+} // namespace rrule
A module-utils/rrule/test/CMakeLists.txt => module-utils/rrule/test/CMakeLists.txt +8 -0
@@ 0,0 1,8 @@
+add_catch2_executable(
+ NAME
+ utils-rrule
+ SRCS
+ unittest_rrule.cpp
+ LIBS
+ rrule
+)
A module-utils/rrule/test/unittest_rrule.cpp => module-utils/rrule/test/unittest_rrule.cpp +181 -0
@@ 0,0 1,181 @@
+// Copyright (c) 2017-2021, Mudita Sp. z.o.o. All rights reserved.
+// For licensing, see https://github.com/mudita/MuditaOS/LICENSE.md
+
+#include <rrule/rrule.hpp>
+
+#include <date/date.h>
+#include <cstring>
+#include <iostream>
+#include <memory>
+#include <unistd.h>
+#include <chrono>
+#include <regex>
+
+#define CATCH_CONFIG_MAIN // This tells Catch to provide a main() - only do this in one cpp file
+
+#include <catch2/catch.hpp>
+
+namespace rrule
+{
+ static TimePoint TimePointFromString(const char *s1)
+ {
+ TimePoint tp;
+ std::istringstream(s1) >> date::parse("%F %T", tp);
+ return tp;
+ }
+ TEST_CASE("RRule")
+ {
+ SECTION("ParseFromString")
+ {
+ SECTION("Basic")
+ {
+ RRule rrule;
+ rrule.parseFromString("FREQ=DAILY;INTERVAL=1");
+ REQUIRE(rrule.freq == RRule::RRuleFrequency::DAILY_RECURRENCE);
+ REQUIRE(rrule.count == 0);
+ REQUIRE(rrule.until == TimePoint::min());
+ REQUIRE(rrule.interval == 1);
+ REQUIRE(rrule.weekStart == RRule::RRuleWeekday::MONDAY_WEEKDAY);
+ REQUIRE((rrule.bySecond.empty() && rrule.byMinute.empty() && rrule.byHour.empty() &&
+ rrule.byDay.empty() && rrule.byMonthDay.empty() && rrule.byYearDay.empty() &&
+ rrule.byMonth.empty() && rrule.byWeekNo.empty() && rrule.bySetPos.empty()));
+ }
+
+ SECTION("Monthly")
+ {
+ RRule rrule;
+ rrule.parseFromString("FREQ=MONTHLY;BYMONTHDAY=1;INTERVAL=1");
+ REQUIRE(rrule.freq == RRule::RRuleFrequency::MONTHLY_RECURRENCE);
+ REQUIRE(rrule.count == 0);
+ REQUIRE(rrule.until == TimePoint::min());
+ REQUIRE(rrule.interval == 1);
+ REQUIRE(rrule.weekStart == RRule::RRuleWeekday::MONDAY_WEEKDAY);
+ REQUIRE(rrule.byMonthDay.size() == 1);
+ REQUIRE(rrule.byMonthDay[0] == 1);
+ REQUIRE((rrule.bySecond.empty() && rrule.byMinute.empty() && rrule.byHour.empty() &&
+ rrule.byDay.empty() && rrule.byYearDay.empty() && rrule.byMonth.empty() &&
+ rrule.byWeekNo.empty() && rrule.bySetPos.empty()));
+ }
+
+ SECTION("Until")
+ {
+ RRule rrule;
+ rrule.parseFromString("FREQ=DAILY;INTERVAL=1;UNTIL=20210806T000000");
+ REQUIRE(rrule.freq == RRule::RRuleFrequency::DAILY_RECURRENCE);
+ REQUIRE(rrule.count == 0);
+ REQUIRE(rrule.until == date::sys_days{date::August / 6 / 2021});
+ REQUIRE(rrule.interval == 1);
+ REQUIRE(rrule.weekStart == RRule::RRuleWeekday::MONDAY_WEEKDAY);
+ REQUIRE((rrule.bySecond.empty() && rrule.byMinute.empty() && rrule.byHour.empty() &&
+ rrule.byDay.empty() && rrule.byMonthDay.empty() && rrule.byYearDay.empty() &&
+ rrule.byMonth.empty() && rrule.byWeekNo.empty() && rrule.bySetPos.empty()));
+ }
+ SECTION("Complex rule")
+ {
+ RRule rrule;
+ rrule.parseFromString("FREQ=MONTHLY;BYSETPOS=4;BYDAY=TU;INTERVAL=2;COUNT=10");
+ REQUIRE(rrule.freq == RRule::RRuleFrequency::MONTHLY_RECURRENCE);
+ REQUIRE(rrule.count == 10);
+ REQUIRE(rrule.until == TimePoint::min());
+ REQUIRE(rrule.interval == 2);
+ REQUIRE(rrule.weekStart == RRule::RRuleWeekday::MONDAY_WEEKDAY);
+ REQUIRE(rrule.byDay.size() == 1);
+ REQUIRE(rrule.byDay[0] == 3);
+ REQUIRE(rrule.bySetPos.size() == 1);
+ REQUIRE(rrule.bySetPos[0] == 4);
+ REQUIRE((rrule.bySecond.empty() && rrule.byMinute.empty() && rrule.byHour.empty() &&
+ rrule.byMonthDay.empty() && rrule.byYearDay.empty() && rrule.byMonth.empty() &&
+ rrule.byWeekNo.empty()));
+ }
+ }
+
+ SECTION("ParseString")
+ {
+ SECTION("Basic")
+ {
+ RRule rrule;
+ auto teststring{""};
+
+ teststring = "FREQ=DAILY";
+ rrule.parseFromString(teststring);
+ REQUIRE(rrule.parseToString() == teststring);
+
+ teststring = "FREQ=MONTHLY;BYMONTHDAY=1";
+ rrule.parseFromString(teststring);
+ REQUIRE(rrule.parseToString() == teststring);
+ }
+
+ SECTION("String simplification")
+ {
+ RRule rrule;
+ auto teststring{""};
+
+ teststring = "FREQ=DAILY;INTERVAL=1";
+ rrule.parseFromString(teststring);
+ REQUIRE(rrule.parseToString() == "FREQ=DAILY");
+
+ teststring = "FREQ=MONTHLY;BYMONTHDAY=1;INTERVAL=1";
+ rrule.parseFromString(teststring);
+ REQUIRE(rrule.parseToString() == "FREQ=MONTHLY;BYMONTHDAY=1");
+ }
+ SECTION("Until")
+ {
+ RRule rrule;
+ const auto teststring = "FREQ=DAILY;UNTIL=20210806T000000";
+
+ rrule.parseFromString(teststring);
+ REQUIRE(rrule.parseToString() == "FREQ=DAILY;UNTIL=20210806T000000");
+ }
+ SECTION("Complex rule")
+ {
+ RRule rrule;
+ auto teststring{""};
+
+ teststring = "FREQ=MONTHLY;INTERVAL=2;BYDAY=TU;BYSETPOS=4;COUNT=10";
+ rrule.parseFromString(teststring);
+ REQUIRE(rrule.parseToString() == teststring);
+
+ teststring = "FREQ=YEARLY;BYMONTH=4;BYDAY=TH;BYSETPOS=1;COUNT=10";
+ rrule.parseFromString(teststring);
+ REQUIRE(rrule.parseToString() == teststring);
+ }
+ }
+
+ SECTION("Generate timestamps")
+ {
+ RRule rrule;
+ TimePoint start = TimePointFromString("2020-01-01 12:00:00");
+ TimePoint end = TimePointFromString("2020-02-01 12:00:00");
+ const auto GENERATE_ALL = 99999;
+
+ SECTION("Basic daily")
+ {
+ const auto teststring = "FREQ=DAILY";
+ start = TimePointFromString("2020-01-01 12:00:00");
+ end = TimePointFromString("2020-02-01 12:00:00");
+ rrule.parseFromString(teststring);
+
+ auto timestamps = rrule.generateEventTimePoints(start, end, GENERATE_ALL);
+
+ REQUIRE(timestamps.size() == 31);
+ REQUIRE(timestamps[0] == TimePointFromString("2020-01-01 12:00:00"));
+ REQUIRE(timestamps[1] == TimePointFromString("2020-01-02 12:00:00"));
+ REQUIRE(timestamps[30] == TimePointFromString("2020-01-31 12:00:00"));
+ }
+ SECTION("Basic hourly interval count")
+ {
+ const auto teststring = "FREQ=HOURLY;INTERVAL=2;COUNT=10";
+ start = TimePointFromString("2020-01-01 12:00:00");
+ end = TimePointFromString("2020-02-01 12:00:00");
+ rrule.parseFromString(teststring);
+
+ auto timestamps = rrule.generateEventTimePoints(start, end, GENERATE_ALL);
+
+ REQUIRE(timestamps.size() == 10);
+ REQUIRE(timestamps[0] == TimePointFromString("2020-01-01 12:00:00"));
+ REQUIRE(timestamps[1] == TimePointFromString("2020-01-01 14:00:00"));
+ REQUIRE(timestamps[9] == TimePointFromString("2020-01-02 06:00:00"));
+ }
+ }
+ }
+} // namespace rrule