diff --git a/.gitignore b/.gitignore index 96ef6c0..37a4f98 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,4 @@ /target Cargo.lock +.idea +.DS_Store \ No newline at end of file diff --git a/Cargo.toml b/Cargo.toml index 3af7857..dffbff2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,7 +6,7 @@ rust-version = "1.74.0" [workspace] members = [ "rrule", - "rrule-debugger", + "rrule-debugger" ] resolver = "2" @@ -20,4 +20,12 @@ overflow-checks = true [profile.release] # Always have overflow checks until crate is stable, see roadmap. -overflow-checks = true +overflow-checks = false + # Optimize for size +opt-level = "z" +# Enable Link Time Optimization +lto = true +# Reduce the number of codegen units to increase optimization +codegen-units = 1 +# Disable debug info +debug = false \ No newline at end of file diff --git a/rrule/Cargo.toml b/rrule/Cargo.toml index 50d495f..298cb6c 100644 --- a/rrule/Cargo.toml +++ b/rrule/Cargo.toml @@ -21,17 +21,22 @@ regex = { version = "1.5.5", default-features = false, features = ["perf", "std" clap = { version = "4.1.9", optional = true, features = ["derive"] } thiserror = "1.0.30" serde_with = { version = "3.8.1", optional = true } +wasm-bindgen = { version="0.2.92", optional = true } +js-sys = { version="0.3.69", optional = true } +wee_alloc = { version="0.4.1", optional = true } +console_error_panic_hook = { version = "0.1.7", optional = true } [dev-dependencies] serde_json = "1.0.80" orig_serde = { package = "serde", version = "1.0.137", default-features = false, features = ["derive"]} +wasm-bindgen-test = "0.3.42" [[bin]] name = "rrule" required-features = ["cli-tool"] [features] -default = [] +default = ["wasm", "wee_alloc", "console_error_panic_hook"] # Allows the enabling of the `by_easter` field and `BYEASTER` parser. by-easter = [] @@ -43,4 +48,10 @@ cli-tool = ["clap"] serde = ["serde_with", "chrono/serde", "chrono-tz/serde"] # Allows EXRULE's to be used in the `RRuleSet`. -exrule = [] \ No newline at end of file +exrule = [] + +# Allows to use WASM +wasm = ["dep:wasm-bindgen", "dep:js-sys"] + +[lib] +crate-type = ["cdylib", "rlib"] \ No newline at end of file diff --git a/rrule/Makefile b/rrule/Makefile new file mode 100644 index 0000000..158d753 --- /dev/null +++ b/rrule/Makefile @@ -0,0 +1,20 @@ +install-wasm-pack: + curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh + +build-wasm-nodejs: + wasm-pack build --release --target nodejs --out-dir pkg/nodejs --features "wasm" + +test-wasm-on-nodejs: + node examples/wasm/nodejs/app.js + +build-wasm-web: + wasm-pack build --release --target web --out-dir pkg/web --features "wasm" + +test-wasm-on-web-browser: + npx http-server -o /examples/wasm/web/index.html + +build-wasm-bundler: + wasm-pack build --release --target bundler --features "wasm" + +pack: + wasm-pack pack pkg \ No newline at end of file diff --git a/rrule/examples/wasm/nodejs/app.js b/rrule/examples/wasm/nodejs/app.js new file mode 100644 index 0000000..2b077d1 --- /dev/null +++ b/rrule/examples/wasm/nodejs/app.js @@ -0,0 +1,11 @@ +const { get_all_date_recurrences_between } = require('../../../pkg/nodejs/rrule.js'); + +const rule_set = 'DTSTART:20120201T093000Z\nRRULE:FREQ=DAILY'; +const data = get_all_date_recurrences_between( + rule_set, + 10, + new Date(2021, 0, 1), + new Date(2023, 0, 1) +); + +console.log(data); \ No newline at end of file diff --git a/rrule/examples/wasm/web/.gitignore b/rrule/examples/wasm/web/.gitignore new file mode 100644 index 0000000..30bc162 --- /dev/null +++ b/rrule/examples/wasm/web/.gitignore @@ -0,0 +1 @@ +/node_modules \ No newline at end of file diff --git a/rrule/examples/wasm/web/benchmarking.js b/rrule/examples/wasm/web/benchmarking.js new file mode 100644 index 0000000..ca30d65 --- /dev/null +++ b/rrule/examples/wasm/web/benchmarking.js @@ -0,0 +1,144 @@ +import init, { getAllRecurrencesBetween } from '../../../pkg/web/rrule.js'; +import { tryParseEventRecurrenceRules, createValidDateTimeFromISO, getInstanceStartAt } from './rrule_utils.js'; + +function executeRRulePerformanceTest(ruleSet, after, before, limit) { + var rruleWork = () => { + const rule = new rrule.rrulestr(ruleSet); + const results = rule.between(after, before); + } + return executeWork(rruleWork, "rrule"); +} +async function executeRustRRulePerformanceTest(ruleSet, after, before, limit) { + var rustWork = () => { + const data = getAllRecurrencesBetween(ruleSet, after, before, limit); + } + return executeWork(rustWork, "rust-rrule"); +} + +const performance = window.performance; + +function executeWork(work, framework, rounds = 100) { + const measurements = []; + + for (let round = 0; round < rounds; round++) { + const t0 = performance.now(); + work(); + const t1 = performance.now(); + measurements.push(t1 - t0); + } + + // Calculate mean + const mean = measurements.reduce((a, b) => a + b, 0) / measurements.length; + + // Calculate standard deviation + const standardDeviation = Math.sqrt( + measurements.map(x => Math.pow(x - mean, 2)).reduce((a, b) => a + b) / measurements.length + ); + + // Calculate confidence interval (95% confidence level) + const zScore = 1.96; // Z-score for 95% confidence + const marginOfError = zScore * (standardDeviation / Math.sqrt(measurements.length)); + const confidenceInterval = [mean - marginOfError, mean + marginOfError]; + + return `Call to ${framework} took an average of ${mean.toFixed(2)} milliseconds with a 95% confidence interval of (${confidenceInterval[0].toFixed(2)}, ${confidenceInterval[1].toFixed(2)}) milliseconds.`; +} + +async function executePerformanceTests() { + const ruleSet = document.getElementById("ruleSet").value.replaceAll('\\n', '\n'); + const afterDateString = document.getElementById("after").value; + const beforeDateString = document.getElementById("before").value; + const limit = document.getElementById("limit").value; + let after = new Date(afterDateString); + let before = new Date(beforeDateString) + + const wasmInitTimeDiv = document.querySelector("#wasmInitTime"); + + wasmInitTimeDiv.innerHTML = "Loading ..."; + const t0 = performance.now(); + await init(); + const t1 = performance.now(); + wasmInitTimeDiv.innerHTML = (t1 - t0) + " milliseconds."; + + const rustRRuleResultDiv = document.querySelector("#rustRRuleResult"); + rustRRuleResultDiv.innerHTML = "Executing ..."; + rustRRuleResultDiv.innerHTML = await executeRustRRulePerformanceTest(ruleSet, after, before, limit); + + setTimeout(() => { + const rruleResultDiv = document.querySelector("#rruleResult"); + rruleResultDiv.innerHTML = "Executing ..."; + rruleResultDiv.innerHTML = executeRRulePerformanceTest(ruleSet, after, before, limit); + + const matchErrorsDiv = document.querySelector("#matchErrors"); + + // const sourceEvent = { + // startAt: '2023-05-31T20:00:00+05:30', + // startTimeZone: 'Asia/Kolkata', + // recurrence: [ + // // 'DTSTART;TZID=Asia/Kolkata:20230531T200000', + // 'EXDATE;TZID=Asia/Kolkata:20230810T200000', + // 'RRULE:FREQ=DAILY;UNTIL=20230818T143000Z', + // ], + // }; + // + // after = new Date('2023-05-30T00:00:00Z'); + // before = new Date('2023-09-01T00:00:00Z'); + + const sourceEvent = { + startAt: '2019-08-13T15:30:00', + startTimeZone: 'Europe/Moscow', + recurrence: [ + // 'DTSTART;TZID=Europe/Moscow:20190813T153000', + 'RRULE:FREQ=DAILY', + ], + }; + + after = new Date('2019-06-05T21:00:00Z'); + before = new Date('2022-06-22T20:59:59Z'); + + const event = { + recurrenceRules: sourceEvent.recurrence, + startAt: createValidDateTimeFromISO(sourceEvent.startAt, { + zone: sourceEvent.startTimeZone, + }), + } + + const rruleSet = tryParseEventRecurrenceRules(event, { useStartDate: true }) + + console.log(rruleSet.toString()); + + const dates1 = getAllRecurrencesBetween([ + // `DTSTART;TZID=Asia/Kolkata:20230531T200000`, + 'DTSTART;TZID=Europe/Moscow:20190813T153000', + ...sourceEvent.recurrence].join('\n'), after, before, limit); + const dates2 = rruleSet.between(after, before); + + console.log(dates1, dates2); + + let isFullMatch = true; + + for (let i = 0; i < dates1.length; i++) { + let d1 = dates1.at(i); + let d2 = dates2.at(i); + + d1 = d1 ? new Date(d1) : null; + d2 = getInstanceStartAt(d2, event.startAt).toJSDate(); + + if (d1?.getTime() !== d2?.getTime()) { + matchErrorsDiv.innerHTML += `
  • Dates do not match at index ${i}: ${d1?.toISOString()} !== ${d2?.toISOString()}
  • `; + isFullMatch = false; + } + } + + if (isFullMatch) { + matchErrorsDiv.innerHTML = `All dates match! (${dates1.length})`; + } + }); +} + +document.addEventListener("DOMContentLoaded", () => { + const performanceButton = document.querySelector("#performanceButton"); + + performanceButton.addEventListener("click", () => { + executePerformanceTests(); + }); +}); \ No newline at end of file diff --git a/rrule/examples/wasm/web/index.html b/rrule/examples/wasm/web/index.html new file mode 100644 index 0000000..dbf4cff --- /dev/null +++ b/rrule/examples/wasm/web/index.html @@ -0,0 +1,40 @@ + + + + + + + + + + +

    rust-rrule x rrule

    + + + + +

    + + +

    + + +

    + + +

    + + + +


    + +
    WASM init time: ...
    +
    No rust-rrule results yet
    +
    No rrule results yet
    +
    Match: ...
    + + \ No newline at end of file diff --git a/rrule/examples/wasm/web/package.json b/rrule/examples/wasm/web/package.json new file mode 100644 index 0000000..0c8b979 --- /dev/null +++ b/rrule/examples/wasm/web/package.json @@ -0,0 +1,7 @@ +{ + "name": "rrule-wasm", + "dependencies": { + "rrule": "latest", + "luxon": "latest" + } +} \ No newline at end of file diff --git a/rrule/examples/wasm/web/rrule_utils.js b/rrule/examples/wasm/web/rrule_utils.js new file mode 100644 index 0000000..3ec6807 --- /dev/null +++ b/rrule/examples/wasm/web/rrule_utils.js @@ -0,0 +1,210 @@ +import { DateTime } from './node_modules/luxon/build/es6/luxon.js'; + +const rrulestr = rrule.rrulestr; + +/** The max number of recurring events (2 years) */ +export const MAX_OCCURRENCES_COUNT = 730; +const DEFAULT_TZID = 'utc'; +const TZID_REGEX = /;TZID=([^;:]+)/; +const DATE_FORMAT = `yyyyMMdd'T'HHmmss`; +const DATE_FORMAT_UTC = `yyyyMMdd'T'HHmmss'Z'`; +export function parseRecurrenceRules(rules, options) { + var _a, _b, _c, _d; + const dtstart = options === null || options === void 0 ? void 0 : options.dtstart; + const tzid = options === null || options === void 0 ? void 0 : options.tzid; + const count = (_a = options === null || options === void 0 ? void 0 : options.count) !== null && _a !== void 0 ? _a : 0; + const timeZone = (_b = tzid === null || tzid === void 0 ? void 0 : tzid.toLowerCase()) !== null && _b !== void 0 ? _b : DEFAULT_TZID; + if (dtstart) { + rules = [ + `DTSTART${formatRuleTzid(tzid)}:${formatDateInZone(dtstart, timeZone)}`, + ...rules, + ]; + } + for (let i = 0; i < rules.length; i++) { + const rule = rules[i]; + if (rule.startsWith('RDATE') || rule.startsWith('EXDATE')) { + const [key, value] = rule.split(':'); + const ruleZone = (_d = (_c = key.match(TZID_REGEX)) === null || _c === void 0 ? void 0 : _c[1]) !== null && _d !== void 0 ? _d : 'utc'; + if (ruleZone.toLowerCase() !== timeZone) { + const filteredKey = key.replace(TZID_REGEX, ''); + const adjustedValues = value + .split(',') + .map((date) => reformatDateInZone(date, timeZone, ruleZone)) + .join(','); + rules[i] = `${filteredKey}:${adjustedValues}`; + } + } + } + const rruleSet = rrulestr(sanitizeRecurrenceRules(rules), { + compatible: true, + cache: false, + dtstart, + tzid, + }); + for (const rrule of rruleSet._rrule) { + const options = rrule.options; + if (count > 0) { + // Hard limit the number of generated instances to the provided count. + options.count = count; + } + else { + // Limit the number of generated instances to 2 years for daily events. + // See: https://support.google.com/calendar/thread/51073472/daily-recurring-event-has-stopped-recurring + options.count = options.count + ? Math.min(options.count, MAX_OCCURRENCES_COUNT) + : MAX_OCCURRENCES_COUNT; + } + if (options.until && tzid) { + // TODO(@vk): Should we do the same for all day events? + // UNTIL date usually represented in UTC timezone. so we need to convert it to the specified timezone (tzid). + options.until = transformDateToZone(options.until, 'utc', tzid); + } + } + return rruleSet; +} +export function tryParseRecurrenceRules(rules, options) { + try { + return parseRecurrenceRules(rules, options); + } + catch (error) { + console.error('Failed to parse recurrence rules', rules, error); + } +} +/** + * Normalizes recurrence rules by filtering out unsupported rules. + */ +export function sanitizeRecurrenceRules(rules) { + return rules.join('\n').replaceAll(/;X-EVOLUTION-ENDDATE=\d{8}T\d{6}Z/gm, ''); +} +export function tryParseEventRecurrenceRules(event, options) { + if (!(event === null || event === void 0 ? void 0 : event.recurrenceRules)) { + return; + } + const parseOptions = {}; + if (options === null || options === void 0 ? void 0 : options.useStartDate) { + if (event.startDate /*&& event.endDate*/) { + parseOptions.dtstart = new Date(event.startDate); + } + else if (event.startAt /*&& event.endAt*/) { + parseOptions.dtstart = event.startAt.setZone('system').toJSDate(); + parseOptions.tzid = event.startAt.zoneName; + } + } + if (options === null || options === void 0 ? void 0 : options.count) { + parseOptions.count = options.count; + } + return tryParseRecurrenceRules(event.recurrenceRules, parseOptions); +} + +export function normalizeRecurringRules(rruleSet, isAllDay = false) { + const rules = rruleSet.valueOf(); + if (!isAllDay) { + return rules; + } + for (let i = 0; i < rules.length; i++) { + let rule = rules[i]; + if (rule.startsWith('EXDATE')) { + rule = rule + .replace('EXDATE', 'EXDATE;VALUE=DATE') + .replaceAll('T000000Z', ''); + rules[i] = rule; + } + else if (rule.startsWith('RDATE')) { + rule = rule + .replace('RDATE', 'RDATE;VALUE=DATE') + .replaceAll('T000000Z', ''); + rules[i] = rule; + } + else if (rule.startsWith('RRULE')) { + rule = rule.replaceAll('T000000Z', ''); + rules[i] = rule; + } + } + return rules; +} +export function getStartUTCDate(date) { + return date.setZone('utc').toJSDate(); +} +export function getInstanceStartAt(instanceDate, parentStartAt) { + if (parentStartAt.zone.isUniversal) { + return DateTime.fromJSDate(instanceDate); + } + return DateTime.fromJSDate(instanceDate) + .toUTC() + .setZone('system', { keepLocalTime: true }) + .setZone(parentStartAt.zone); +} +export function getInstanceStartDate(instanceDate) { + return DateTime.fromJSDate(instanceDate) + .toUTC() + .setZone('system', { keepLocalTime: true }); +} +export function buildCutoffUntilUTCDate(date) { + return date + .setZone('utc', { keepLocalTime: true }) + .minus({ days: 1 }) + .endOf('day') + .toJSDate(); +} +function reformatDateInZone(dateString, targetZone, sourceZone) { + for (const format of [DATE_FORMAT, DATE_FORMAT_UTC]) { + const dt = DateTime.fromFormat(dateString, format, { + zone: sourceZone, + }); + if (dt.isValid) { + return dt.setZone(targetZone).toFormat(DATE_FORMAT); + } + } + throw new Error(`💥 [reformatDateInZone] Invalid date string: ${dateString}`); +} +function formatRuleTzid(tzid) { + return tzid ? `;TZID=${tzid}` : ''; +} +function formatDateInZone(date, targetZone, sourceZone) { + return DateTime.fromJSDate(date, { zone: sourceZone }) + .setZone(targetZone) + .toFormat(DATE_FORMAT); +} +function transformDateToZone(date, targetZone, sourceZone) { + return DateTime.fromJSDate(date, { zone: sourceZone }) + .setZone(targetZone, { keepLocalTime: true }) + .toJSDate(); +} +export function createValidDateTimeFromISO(text, opts) { + const date = DateTime.fromISO(text, opts); + assertDateTimeValid(date); + return date; +} +export function createValidDateTimeFromObject(obj, opts) { + const date = DateTime.fromObject(obj, opts); + assertDateTimeValid(date); + return date; +} +export function createValidDateTimeFromJSDate(obj, opts) { + const date = DateTime.fromJSDate(obj, opts); + assertDateTimeValid(date); + return date; +} +export function createValidDateTimeFromFormat(text, fmt, opts) { + const date = DateTime.fromFormat(text, fmt, opts); + assertDateTimeValid(date); + return date; +} +export function createValidDateTimeFromMillis(millis, opts) { + const date = DateTime.fromMillis(millis, opts); + assertDateTimeValid(date); + return date; +} +export function setValidDateTimeZone(date, zone, opts) { + const dateInZone = date.setZone(zone, opts); + assertDateTimeValid(dateInZone); + return dateInZone; +} +export function assertDateTimeValid(date) { + if (!date.isValid) { + const errorMessage = date.invalidExplanation + ? `${date.invalidReason}: ${date.invalidExplanation}` + : date.invalidReason; + throw new Error(`Invalid DateTime: ${errorMessage !== null && errorMessage !== void 0 ? errorMessage : 'unknown'}`); + } +} diff --git a/rrule/examples/wasm/web/yarn.lock b/rrule/examples/wasm/web/yarn.lock new file mode 100644 index 0000000..73d88b5 --- /dev/null +++ b/rrule/examples/wasm/web/yarn.lock @@ -0,0 +1,20 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + +luxon@latest: + version "3.4.4" + resolved "https://registry.yarnpkg.com/luxon/-/luxon-3.4.4.tgz#cf20dc27dc532ba41a169c43fdcc0063601577af" + integrity sha512-zobTr7akeGHnv7eBOXcRgMeCP6+uyYsczwmeRCauvpvaAltgNyTbLH/+VaEAPUeWBT+1GuNmz4wC/6jtQzbbVA== + +rrule@latest: + version "2.8.1" + resolved "https://registry.yarnpkg.com/rrule/-/rrule-2.8.1.tgz#e8341a9ce3e68ce5b8da4d502e893cd9f286805e" + integrity sha512-hM3dHSBMeaJ0Ktp7W38BJZ7O1zOgaFEsn41PDk+yHoEtfLV+PoJt9E9xAlZiWgf/iqEqionN0ebHFZIDAp+iGw== + dependencies: + tslib "^2.4.0" + +tslib@^2.4.0: + version "2.6.3" + resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.6.3.tgz#0438f810ad7a9edcde7a241c3d80db693c8cbfe0" + integrity sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ== diff --git a/rrule/src/lib.rs b/rrule/src/lib.rs index 8a39dfd..ec56e61 100644 --- a/rrule/src/lib.rs +++ b/rrule/src/lib.rs @@ -93,12 +93,21 @@ #![warn(missing_docs)] #![deny(rustdoc::broken_intra_doc_links)] +extern crate wee_alloc; + +// When the `wee_alloc` feature is enabled, use `wee_alloc` as the global allocator. +#[cfg(feature = "wee_alloc")] +#[global_allocator] +static ALLOC: wee_alloc::WeeAlloc = wee_alloc::WeeAlloc::INIT; + mod core; mod error; mod iter; mod parser; mod tests; mod validator; +#[cfg(feature = "wasm")] +mod wasm; pub use crate::core::{Frequency, NWeekday, RRule, RRuleResult, RRuleSet, Tz}; pub use crate::core::{Unvalidated, Validated}; diff --git a/rrule/src/wasm/datetime_utils.rs b/rrule/src/wasm/datetime_utils.rs new file mode 100644 index 0000000..e85557a --- /dev/null +++ b/rrule/src/wasm/datetime_utils.rs @@ -0,0 +1,59 @@ +use crate::{core::Tz}; +use chrono::{DateTime, TimeZone}; + +pub fn convert_js_date_to_datetime(date: &js_sys::Date) -> Result, DateTimeError> { + if !is_valid_date(date) { + return Err(DateTimeError::new("invalid datetime")); + } + let timestamp_ms = date.get_time(); + let timestamp_secs = (timestamp_ms / 1000.0) as i64; + let nanosecs = ((timestamp_ms % 1000.0) * 1_000_000.0) as u32; + { + let datetime = chrono::NaiveDateTime::from_timestamp_opt(timestamp_secs, nanosecs); + match datetime { + Some(datetime) => { + match convert_to_timezone(datetime, Tz::UTC) { + Ok(datetime) => Ok(datetime), + Err(e) => Err(e) + } + }, + None => Err(DateTimeError::new("invalid or out-of-range datetime")) + } + } +} + +fn is_valid_date(date: &js_sys::Date) -> bool { + let milliseconds = date.get_time(); + let is_nan = milliseconds.is_nan(); + !is_nan +} + +fn convert_to_timezone(datetime: chrono::NaiveDateTime, timezone: Tz) -> Result, DateTimeError> { + let result = timezone.from_local_datetime(&datetime); + match result { + chrono::LocalResult::Single(datetime) => Ok(datetime), + chrono::LocalResult::Ambiguous(_, _) => Err(DateTimeError::new("ambiguous or out-of-range datetime")), + chrono::LocalResult::None => Err(DateTimeError::new("d invalid or out-of-range datetime d")) + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct DateTimeError { + message: String, +} + +impl DateTimeError { + fn new(message: &str) -> Self { + Self { + message: message.to_owned(), + } + } +} + +impl std::error::Error for DateTimeError {} + +impl std::fmt::Display for DateTimeError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "DateTimeError Error: {}", self.message) + } +} \ No newline at end of file diff --git a/rrule/src/wasm/mod.rs b/rrule/src/wasm/mod.rs new file mode 100644 index 0000000..4ae36c5 --- /dev/null +++ b/rrule/src/wasm/mod.rs @@ -0,0 +1,67 @@ +mod datetime_utils; + +use wasm_bindgen::prelude::*; +use crate::{RRuleSet, RRuleError}; + +const MAX_OCCURRENCES_COUNT: u16 = 730; + +pub fn set_panic_hook() { + // When the `console_error_panic_hook` feature is enabled, we can call the + // `set_panic_hook` function at least once during initialization, and then + // we will get better error messages if our code ever panics. + // + // For more details see + // https://github.com/rustwasm/console_error_panic_hook#readme + #[cfg(feature = "console_error_panic_hook")] + console_error_panic_hook::set_once(); +} + +/// Get all recurrences of the rrule +#[wasm_bindgen(js_name = getAllRecurrencesBetween)] +pub fn get_all_recurrences_between(rrule_set_str: &str, after: js_sys::Date, before: js_sys::Date, count: Option) -> Result, JsError> { + set_panic_hook(); + + let after = datetime_utils::convert_js_date_to_datetime(&after).map_err(JsError::from); + let before = datetime_utils::convert_js_date_to_datetime(&before).map_err(JsError::from); + + match (parser_rule_set(rrule_set_str), after, before) { + (Ok(rrule_set), Ok(after), Ok(before)) => { + let mut cloned_rrules = rrule_set.get_rrule().clone(); + let max_count: u32 = MAX_OCCURRENCES_COUNT.into(); + + cloned_rrules.iter_mut().for_each(|rrule| { + if rrule.count.is_none() || rrule.count.unwrap() > max_count { + rrule.count = Some(max_count); + } + }); + + let final_rrule_set = rrule_set.set_rrules(cloned_rrules).after(after).before(before); + + Ok(get_all_recurrences_for(final_rrule_set)) + }, + (Err(e), _, _) => Err(e), + (_, Err(e), _) => Err(e), + (_, _, Err(e)) => Err(e), + } +} + +fn parser_rule_set(rrule_set_str: &str) -> Result { + let rrule_set_result: Result = rrule_set_str.parse(); + + match rrule_set_result { + Ok(rrule_set) => Ok(rrule_set), + Err(e) => Err(JsError::from(e)) + } +} + +fn get_all_recurrences_for(rrule_set: RRuleSet) -> Vec { + let rrule_set_collection = rrule_set.all(MAX_OCCURRENCES_COUNT); + let result: Vec = rrule_set_collection.dates + .into_iter() + .map(|dt| { + JsValue::from_str(&dt.to_rfc3339()) + }) + .collect(); + + result +} \ No newline at end of file