Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add wasm rrule properties #93

Draft
wants to merge 10 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ rust-version = "1.64.0"
[workspace]
members = [
"rrule",
"rrule-debugger",
"rrule-debugger"
]

# These are the 2 packages to mainly work on.
Expand Down
13 changes: 11 additions & 2 deletions rrule/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -21,17 +21,20 @@ 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 = "2.3.1", optional = true }
wasm-bindgen = { version="0.2.85", optional = true }
js-sys = { version="0.3.63", optional = true }

[dev-dependencies]
serde_json = "1.0.80"
orig_serde = { package = "serde", version = "1.0.137", default-features = false }
wasm-bindgen-test = "0.3.13"

[[bin]]
name = "rrule"
required-features = ["cli-tool"]

[features]
default = []
default = ["wasm"]

# Allows the enabling of the `by_easter` field and `BYEASTER` parser.
by-easter = []
Expand All @@ -43,4 +46,10 @@ cli-tool = ["clap"]
serde = ["serde_with", "chrono/serde", "chrono-tz/serde"]

# Allows EXRULE's to be used in the `RRuleSet`.
exrule = []
exrule = []

# Allows to use WASM
wasm = ["dep:wasm-bindgen", "dep:js-sys"]

[lib]
crate-type = ["cdylib", "rlib"]
17 changes: 17 additions & 0 deletions rrule/Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
install-wasm-pack:
curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh

build-wasm-nodejs:
wasm-pack build --release --target nodejs --features "wasm"

test-wasm-on-nodejs:
node examples/wasm/nodejs/app.js

build-wasm-web:
wasm-pack build --release --target web --features "wasm"

test-wasm-on-web-browser:
npx http-server /examples/wasm/web/index.html

pack:
wasm-pack pack pkg
21 changes: 21 additions & 0 deletions rrule/examples/wasm/nodejs/app.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
const { get_all_date_recurrences } = require('../../../pkg/rrule.js');

const http = require('http');
const url = require('url');
const hostname = '127.0.0.1';
const port = 3000;

const server = http.createServer((req, res) => {
const queryObject = url.parse(req.url,true).query;
res.statusCode = 200;
res.setHeader('Content-Type', 'text/plain');

const rule_set = "DTSTART:20120201T093000Z\nRRULE:FREQ=DAILY;COUNT=3";
const data = get_all_date_recurrences(rule_set, 100);
console.log(data);
res.end(data.toString());
});

server.listen(port, hostname, () => {
console.log(`Server running at http://${hostname}:${port}/`);
});
1 change: 1 addition & 0 deletions rrule/examples/wasm/web/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
node_modules
55 changes: 55 additions & 0 deletions rrule/examples/wasm/web/benchmarking.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import init, { get_all_date_recurrences_between } from '../../../pkg/rrule.js';

async function executeRRulePerformanceTest(ruleSet, after, before, limit) {
var rruleWork = () => {
const rule = new rrule.RRule.fromString(ruleSet);
const results = rule.between(after, before);
}
return executeWork(rruleWork, "rrule");
}
async function executeRustRRulePerformanceTest(ruleSet, after, before, limit) {
await init();
var rustWork = () => {
const data = get_all_date_recurrences_between(ruleSet, limit, after, before);
}
return executeWork(rustWork, "rust-rrule");
}
function executeWork(work, framework) {
var performance = window.performance;
var t0 = performance.now();
var i = 0;
const times = 100;
for (i = 0; i < times; ++i) {
work();
}
var t1 = performance.now();
const result = "Call to " + framework + " took " + (t1 - t0)/times + " milliseconds.";
return result;
}

async function executePerformanceTests() {
const ruleSet = document.getElementById("ruleSet").value.replace('\\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 rustRRuleResultDiv = document.querySelector("#rustRRuleResult");
rustRRuleResultDiv.innerHTML = "Executing ...";
await executeRustRRulePerformanceTest(ruleSet, after, before, limit).then((value) => {
rustRRuleResultDiv.innerHTML = value;
});
const rruleResultDiv = document.querySelector("#rruleResult");
rruleResultDiv.innerHTML = "Executing ...";
await executeRRulePerformanceTest(ruleSet, after, before, limit).then((value) => {
rruleResultDiv.innerHTML = value;
});
}

document.addEventListener("DOMContentLoaded", () => {
const performanceButton = document.querySelector("#performanceButton");

performanceButton.addEventListener("click", () => {
executePerformanceTests();
});
});
32 changes: 32 additions & 0 deletions rrule/examples/wasm/web/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
<!DOCTYPE html>
<html>
<head>
<meta content="text/html;charset=utf-8" http-equiv="Content-Type"/>
<meta http-equiv="cache-control" content="max-age=0" />
<meta http-equiv="cache-control" content="no-cache" />
<script type="module" src="./node_modules/rrule/dist/es5/rrule.js"></script>
<script type="module" src="./benchmarking.js"></script>
</head>
<body>
<h1>rust-rrule x rrule</h1>

<label for="ruleSet">RuleSet:</label>
<input type="text" id="ruleSet" name="ruleSet" value="DTSTART;TZID=Europe/Brussels:20210324T110000\nRRULE:INTERVAL=1;FREQ=WEEKLY;BYDAY=WE,TH" size="150"><br><br>

<label for="after">After:</label>
<input type="text" id="after" name="after" value="2022-04-04T10:00"><br><br>

<label for="before">Before:</label>
<input type="text" id="before" name="before" value="2023-04-04T11:00"><br><br>

<label for="limit">Date Recurrences Limit:</label>
<input type="text" id="limit" name="limit" value="500"><br><br>

<button id="performanceButton">Calculate Performance</button>

<br><br><br>
<label>Result:</label>
<div id="rustRRuleResult" name="rustRRuleResult">No rust-rrule results yet</div>
<div id="rruleResult" name="rruleResult">No rrule results yet</div>
</body>
</html>
29 changes: 29 additions & 0 deletions rrule/examples/wasm/web/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

14 changes: 14 additions & 0 deletions rrule/examples/wasm/web/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
{
"name": "web",
"version": "1.0.0",
"description": "",
"main": "benchmarking.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "",
"license": "ISC",
"dependencies": {
"rrule": "2.7.0"
}
}
2 changes: 1 addition & 1 deletion rrule/src/core/mod.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
mod datetime;
mod rrule;
pub(crate) mod rrule;
mod rruleset;
mod timezone;
mod timezone_impl;
Expand Down
2 changes: 2 additions & 0 deletions rrule/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,8 @@ 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};
Expand Down
59 changes: 59 additions & 0 deletions rrule/src/wasm/datetime_utils.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
use crate::{core::Tz};
use chrono::{DateTime, TimeZone};

pub fn convert_js_date_to_datetime(date: &js_sys::Date) -> Result<DateTime<Tz>, 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<DateTime<Tz>, 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)
}
}
55 changes: 55 additions & 0 deletions rrule/src/wasm/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
mod wasm_rrule;
mod datetime_utils;

use wasm_bindgen::prelude::*;
use crate::{RRuleSet, RRuleError};

/// Get all recurrences of the rrule
///
/// # Arguments
///
/// * `rule_set` - List of rrules
///
/// * `limit` - Limit must be set in order to prevent infinite loops
///
/// * `after` - Returns occurrences of the rrule between after and before
///
/// * `before` - Returns occurrences of the rrule between after and before
///
#[wasm_bindgen]
pub fn get_all_date_recurrences_between(rule_set: &str, limit: Option<u16>, after: js_sys::Date, before: js_sys::Date) -> Result<Vec<JsValue>, JsError> {
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(rule_set), after, before) {
(Ok(rrule), Ok(after), Ok(before)) => {
let rrule = rrule.after(after).before(before);
let result = get_all_date_recurrences_for(rrule, limit);
Ok(result)
},
(Err(e), _, _) => Err(e),
(_, Err(e), _) => Err(e),
(_, _, Err(e)) => Err(e),
}
}

fn parser_rule_set(rule_set: &str) -> Result<RRuleSet, JsError> {
let rrule_result: Result<RRuleSet, RRuleError> = rule_set.parse();
match rrule_result {
Ok(rrule) => {
Ok(rrule)
},
Err(e) => Err(JsError::from(e))
}
}

fn get_all_date_recurrences_for(rule_set: RRuleSet, limit: Option<u16>) -> Vec<JsValue> {
// Set hard limit in case of infinitely recurring rules
let rule_set_collection = rule_set.all(limit.unwrap_or(100));
let result: Vec<JsValue> = rule_set_collection.dates
.into_iter()
.map(|s| {
JsValue::from_str(&s.to_string())
})
.collect();
result
}
28 changes: 28 additions & 0 deletions rrule/src/wasm/wasm_rrule/frequency.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
use wasm_bindgen::prelude::*;
use crate::core::rrule;

#[derive(Clone, Copy)]
#[wasm_bindgen]
pub enum Frequency {
Yearly = 0,
Monthly = 1,
Weekly = 2,
Daily = 3,
Hourly = 4,
Minutely = 5,
Secondly = 6,
}
// Methods can be attached to an enum
impl Frequency {
pub fn convert(&self) -> rrule::Frequency {
match *self {
Self::Yearly => rrule::Frequency::Yearly,
Self::Monthly => rrule::Frequency::Monthly,
Self::Weekly => rrule::Frequency::Weekly,
Self::Daily => rrule::Frequency::Daily,
Self::Hourly => rrule::Frequency::Hourly,
Self::Minutely => rrule::Frequency::Minutely,
Self::Secondly => rrule::Frequency::Secondly,
}
}
}
Loading