~aleteoryx/muditaos

25e9af8750ece7aff1a93c771983b533d385ae87 — Paweł Joński 4 years ago 76fed48
[BH-721] RRule wrapper

Wrapper to icallib recurence rules
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