diff --git a/ics/with-rdate.ics b/ics/with-rdate.ics new file mode 100644 index 0000000..6e4f2a3 --- /dev/null +++ b/ics/with-rdate.ics @@ -0,0 +1,32 @@ +BEGIN:VCALENDAR +VERSION:2.0 +PRODID:-//Test//Test//EN +BEGIN:VEVENT +UID:rdate-future@test +DTSTART:20200101T100000Z +DTEND:20200101T110000Z +RDATE:20300101T100000Z,20310101T100000Z +SUMMARY:RDATE with future dates - should be kept +END:VEVENT +BEGIN:VEVENT +UID:rdate-past@test +DTSTART:20180101T100000Z +DTEND:20180101T110000Z +RDATE:20190101T100000Z,20200101T100000Z +SUMMARY:RDATE with only past dates - should be dropped +END:VEVENT +BEGIN:VEVENT +UID:rdate-value-date@test +DTSTART:20200601T100000Z +DTEND:20200601T110000Z +RDATE;VALUE=DATE:20300601,20310601 +SUMMARY:RDATE with VALUE=DATE format - should be kept +END:VEVENT +BEGIN:VEVENT +UID:rdate-period@test +DTSTART:20200601T100000Z +DTEND:20200601T120000Z +RDATE;VALUE=PERIOD:20300601T100000Z/20300601T120000Z +SUMMARY:RDATE with VALUE=PERIOD format - should be kept +END:VEVENT +END:VCALENDAR diff --git a/src/index.ts b/src/index.ts index e69aa2a..fc81eaa 100644 --- a/src/index.ts +++ b/src/index.ts @@ -18,6 +18,8 @@ export const icsFilter = (content: string, now: Date, max?: Date): string => { let keep: boolean = true; let hasRrule: boolean = false; + let hasRdate: boolean = false; + let rdateKeep: boolean = false; let hasKeptRecurrenceId: boolean = false; let rruleKeep: boolean = true; @@ -57,6 +59,8 @@ export const icsFilter = (content: string, now: Date, max?: Date): string => { keep = true; hasRrule = false; + hasRdate = false; + rdateKeep = false; hasKeptRecurrenceId = false; rruleKeep = true; return; @@ -142,6 +146,14 @@ export const icsFilter = (content: string, now: Date, max?: Date): string => { return; } + if (hasRdate && rdateKeep) { + if (isDevRun) { + console.log("DTEND lower than now, but rdate keeps it:", dtEnd, nowStr, "-->", line); + } + + return; + } + if (hasKeptRecurrenceId) { if (isDevRun) { console.log("DTEND lower than now, but recurrenceId keeps it:", dtEnd, nowStr, "-->", line); @@ -188,6 +200,39 @@ export const icsFilter = (content: string, now: Date, max?: Date): string => { return; } + if (lineUpper.startsWith("RDATE")) { + const value: string | null = extractValue(line); + if (!value) { + return; + } + + hasRdate = true; + + for (const dateRaw of value.split(",")) { + const date: string | null = normalizeICSDateStr(dateRaw.trim()); + if (!date) { + continue; + } + + if (date >= nowStr && (!maxStr || date <= maxStr)) { + rdateKeep = true; + keep = true; + + if (isDevRun) { + console.log("RDATE date in range:", date, "-->", line); + } + + break; + } + } + + if (isDevRun && !rdateKeep) { + console.log("RDATE: no dates in range", "-->", line); + } + + return; + } + if (lineUpper.startsWith("RECURRENCE-ID")) { const value: string | null = extractValue(line); const dtRecurrence: string | null = normalizeICSDateStr(value); diff --git a/src/tests/rdate-support.test.ts b/src/tests/rdate-support.test.ts new file mode 100644 index 0000000..ea1ff13 --- /dev/null +++ b/src/tests/rdate-support.test.ts @@ -0,0 +1,37 @@ +import assert from "node:assert"; +import { readFileSync } from "node:fs"; +import { describe, test } from "node:test"; + +import { icsFilter } from "../index.js"; + +const content: string = readFileSync("./ics/with-rdate.ics", "utf-8"); +const now: Date = new Date(); +const max: Date = new Date(now.getTime() + 365 * 24 * 60 * 60 * 1000); // 1 year from now +const filteredContent: string = icsFilter(content, now); +const filteredMaxContent: string = icsFilter(content, now, max); + +describe("icsFilter on ics content with RDATE properties", () => { + test("should keep event with RDATE dates in the future", () => { + assert.ok(filteredContent.includes("rdate-future@test"), "Event with future RDATE dates should be kept"); + }); + + test("should drop event with only past RDATE dates", () => { + assert.ok(!filteredContent.includes("rdate-past@test"), "Event with only past RDATE dates should be dropped"); + }); + + test("should keep event with RDATE;VALUE=DATE format dates in the future", () => { + assert.ok(filteredContent.includes("rdate-value-date@test"), "Event with future RDATE;VALUE=DATE dates should be kept"); + }); + + test("should keep event with RDATE;VALUE=PERIOD format dates in the future", () => { + assert.ok(filteredContent.includes("rdate-period@test"), "Event with future RDATE;VALUE=PERIOD dates should be kept"); + }); +}); + +describe("icsFilter on ics content with RDATE properties and max limit", () => { + test("should drop events whose RDATE dates are all beyond max", () => { + assert.ok(!filteredMaxContent.includes("rdate-future@test"), "Event with RDATE dates beyond max should be dropped"); + assert.ok(!filteredMaxContent.includes("rdate-value-date@test"), "Event with RDATE;VALUE=DATE dates beyond max should be dropped"); + assert.ok(!filteredMaxContent.includes("rdate-period@test"), "Event with RDATE;VALUE=PERIOD dates beyond max should be dropped"); + }); +});