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