Skip to content

Commit db94e5d

Browse files
committed
Introduce the recurrence tester
1 parent 4c9f94c commit db94e5d

9 files changed

+267
-19
lines changed

.gitignore

+1
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ bower_components
44
dist/
55
docs/api/
66
docs/validator.html
7+
docs/recur-tester.html
78
tools/vzic/
89
tools/tzdb/
910
tools/libical/

README.md

+6-6
Original file line numberDiff line numberDiff line change
@@ -17,14 +17,14 @@ bugfixing this library, please check if the fix can be upstreamed to libical.
1717
## Sandbox and Validator
1818

1919
If you want to try out ICAL.js right now, there is a
20-
[jsfiddle](http://jsfiddle.net/kewisch/227efboL/) set up and ready to use. Read on for documentation
21-
and example links.
20+
[jsfiddle](http://jsfiddle.net/kewisch/227efboL/) set up and ready to use.
2221

23-
There is also a validator that demonstrates how to use the library in a webpage in the
24-
[tools/](https://github.com/kewisch/ical.js/tree/main/tools) subdirectory.
22+
The ICAL validator demonstrates how to use the library in a webpage, and helps verify iCalendar and
23+
jCal. [Try the validator online](http://kewisch.github.io/ical.js/validator.html)
2524

26-
[Try the validator online](http://kewisch.github.io/ical.js/validator.html), it always uses the
27-
latest release of ICAL.js.
25+
The recurrence tester calculates occurrences based on a RRULE. It can be used to aid in
26+
creating test cases for the recurrence iterator.
27+
[Try the recurrence tester online](https://kewisch.github.io/ical.js/recur-tester.html).
2828

2929
## Installing
3030

eslint.config.js

+10
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import js from "@eslint/js";
22
import globals from "globals";
33
import stylistic from '@stylistic/eslint-plugin';
4+
import html from "eslint-plugin-html";
45

56
export default [
67
{
@@ -374,5 +375,14 @@ export default [
374375
rules: {
375376
"@stylistic/quote-props": ["error", "consistent-as-needed"]
376377
}
378+
},
379+
{
380+
files: ["tools/**/*.html"],
381+
plugins: {
382+
"@html": html
383+
},
384+
languageOptions: {
385+
globals: globals.browser
386+
}
377387
}
378388
];

lib/ical/recur_expansion.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -210,7 +210,7 @@ class RecurExpansion {
210210
let maxTries = 500;
211211
let currentTry = 0;
212212

213-
while (true) { // eslint-disable-line no-constant-condition
213+
while (true) {
214214
if (currentTry++ > maxTries) {
215215
throw new Error(
216216
'max tries have occurred, rule may be impossible to fulfill.'

package-lock.json

+87
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

+4-2
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
"chai": "^5.1.0",
3232
"clean-jsdoc-theme": "^4.2.18",
3333
"eslint": "^9.0.0",
34+
"eslint-plugin-html": "^8.1.1",
3435
"globals": "^15.0.0",
3536
"jsdoc": "^4.0.2",
3637
"karma": "^6.4.3",
@@ -57,8 +58,9 @@
5758
"build": "rollup -c",
5859
"lint": "eslint",
5960
"jsdoc": "rm -rf docs/api && jsdoc --configure jsdoc.json --verbose",
60-
"validator": "sed -e 's#unpkg.com/ical.js#unpkg.com/ical.js@'`grep '\"version\"' package.json | cut -d '\"' -f 4`'/dist/ical.js#' tools/validator.html > docs/validator.html && echo 'Validator written to docs/validator.html'",
61-
"ghpages": "npm run jsdoc && npm run validator"
61+
"validator": "node tools/scriptutils.js replace-unpkg tools/validator.html docs/validator.html",
62+
"recurtester": "node tools/scriptutils.js replace-unpkg tools/recur-tester.html docs/recur-tester.html",
63+
"ghpages": "npm run jsdoc && npm run validator && npm run recurtester"
6264
},
6365
"exports": {
6466
"import": "./dist/ical.min.js",

tools/recur-tester.html

+140
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
<!DOCTYPE html>
2+
<html>
3+
<head>
4+
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
5+
<style>
6+
body {
7+
font-family: 'Verdana', sans-serif;
8+
color: #333;
9+
background-color: #f4f4f4;
10+
padding: 20px;
11+
font-size: 16px;
12+
width: 42em;
13+
}
14+
input[type="submit"] {
15+
font-size: 1.2rem;
16+
border-radius: 5px;
17+
border: 1px solid black;
18+
margin-top: 1ex;
19+
padding: 10px;
20+
}
21+
input[type="text"] {
22+
width: 100%;
23+
font-family: monospace;
24+
padding: 5px;
25+
}
26+
input[type="datetime-local"] {
27+
font-family: monospace;
28+
padding: 5px;
29+
}
30+
form > div {
31+
margin-bottom: 10px;
32+
}
33+
</style>
34+
<script type="module">
35+
import ICAL from "https://unpkg.com/ical.js";
36+
37+
document.addEventListener('DOMContentLoaded', function() {
38+
const now = new Date();
39+
now.setMinutes(0);
40+
now.setSeconds(0);
41+
now.setMilliseconds(0);
42+
const oneMonthLater = new Date(now.getFullYear(), now.getMonth() + 1, now.getDate());
43+
44+
// Format dates for input fields
45+
const dtstartValue = now.toISOString().slice(0, 16); // Formats to YYYY-MM-DDTHH:MM
46+
const rangeEndValue = oneMonthLater.toISOString().slice(0, 16); // Formats to YYYY-MM-DDTHH:MM
47+
48+
// Set default values
49+
document.getElementById('dtstart').value = dtstartValue;
50+
document.getElementById('rangeStart').value = dtstartValue;
51+
document.getElementById('rangeEnd').value = rangeEndValue;
52+
});
53+
54+
document.getElementById("form").addEventListener("submit", (event) => {
55+
event.preventDefault();
56+
57+
let dtstart = new Date(document.getElementById('dtstart').value);
58+
let rruleString = document.getElementById('rrule').value;
59+
let rangeStart = new Date(document.getElementById('rangeStart').value);
60+
let rangeEnd = new Date(document.getElementById('rangeEnd').value);
61+
62+
if (isNaN(dtstart.getTime()) || !rruleString || isNaN(rangeStart.getTime()) || isNaN(rangeEnd.getTime())) {
63+
alert('Please fill all fields correctly.');
64+
return;
65+
}
66+
67+
let icDtStart = ICAL.Time.fromJSDate(dtstart);
68+
let icRangeEnd = ICAL.Time.fromJSDate(rangeEnd);
69+
let icRangeStart = ICAL.Time.fromJSDate(rangeStart);
70+
71+
if (rruleString.startsWith("RRULE;")) {
72+
rruleString = rruleString.substring(6);
73+
}
74+
75+
let rrule = ICAL.Recur.fromString(`RRULE;${rruleString}`);
76+
let vevent = new ICAL.Component('vevent');
77+
vevent.addPropertyWithValue('dtstart', icDtStart);
78+
vevent.addPropertyWithValue('rrule', rrule);
79+
80+
let iter = rrule.iterator(icDtStart);
81+
82+
let occurrences = [];
83+
let next;
84+
while ((next = iter.next())) {
85+
if (next.compare(icRangeStart) < 0) {
86+
continue;
87+
} else if (next.compare(icRangeEnd) > 0) {
88+
break;
89+
}
90+
91+
occurrences.push(next.toString());
92+
}
93+
94+
console.log(dtstart, rangeStart);
95+
96+
document.getElementById("testcase").textContent =
97+
`// <describe testcase here>\n` +
98+
`testRRULE('${rruleString}', {\n` +
99+
(dtstart.getTime() == rangeStart.getTime() ? ""
100+
: ` dtStart: '${icRangeStart.toString()}',\n`
101+
) +
102+
" dates: [\n" +
103+
` '${occurrences.join("',\n '")}'\n` +
104+
" ]\n" +
105+
"});";
106+
107+
document.getElementById('occurrences').textContent = occurrences.join('\n');
108+
});
109+
</script>
110+
</head>
111+
<body>
112+
<form id="form" method="post" action="#">
113+
<h1>Recurrence Rule Tester</h1>
114+
<p id="descr">
115+
This tool allows you to calculate occurrences for RRULEs and prepare testcases for them. It
116+
will use ICAL.js from https://unpkg.com/ical.js. <b>Be sure to manually validate the
117+
occurrences, as otherwise it wouldn't be a good test</b>.
118+
</p>
119+
<div>
120+
<label for="dtstart">DTSTART:</label>
121+
<input type="datetime-local" id="dtstart">
122+
</div>
123+
<div>
124+
<label for="rrule">RRULE:</label>
125+
<input type="text" id="rrule">
126+
</div>
127+
<div>
128+
<label for="rangeStart">Expand range:</label>
129+
<input type="datetime-local" id="rangeStart">
130+
<label for="rangeEnd"></label>
131+
<input type="datetime-local" id="rangeEnd">
132+
</div>
133+
<input type="submit" id="calculate" value="Calculate Occurrences"/><br/>
134+
<hr>
135+
<pre id="occurrences"></pre>
136+
<hr>
137+
<pre id="testcase"></pre>
138+
</form>
139+
</body>
140+
</html>

tools/scriptutils.js

+9
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,13 @@ async function get_tzdb_version() {
132132
return match[1];
133133
}
134134

135+
async function replace_unpkg(input, output) {
136+
let content = await fs.readFile(input, { encoding: "utf-8" });
137+
let pkg = JSON.parse(await fs.readFile(path.join(import.meta.dirname, "..", "package.json"), { encoding: "utf-8" }));
138+
await fs.writeFile(output, content.replace(/unpkg.com\/ical.js/g, `unpkg.com/ical.js@${pkg.version}/dist/ical.js`));
139+
console.log(`unpkg link from ${input} updated to ${pkg.version} and written to ${output}`);
140+
}
141+
135142
async function main() {
136143
switch (process.argv[2]) {
137144
case "tzdb-version":
@@ -143,6 +150,8 @@ async function main() {
143150
case "performance-downloader":
144151
await performance_downloader();
145152
break;
153+
case "replace-unpkg":
154+
await replace_unpkg(process.argv[3], process.argv[4]);
146155
}
147156
}
148157
main();

0 commit comments

Comments
 (0)