Repository: sebbo2002/ical-generator

Issue:
Cancel or reschedule recurring event.
how can we give a recurrence-id in order to specify a particular event date-time to cancel that event in a recurring event?

Hints from issue comments:
@sebbo2002: This feature is currently not implemented in ical-generator. Sorry…@sedenardi: @sebbo2002 Hi there, would you be receptive to a pull-request implementing this feature?@github-actions: This issue has been automatically locked since there has not been any recent activity after it was closed. Please open a new issue for related issues and reference this one.

Solution patch:
diff --git a/README.md b/README.md
index 02ca8cf..c56430f 100755
--- a/README.md
+++ b/README.md
@@ -341,6 +341,11 @@ event.repeating({
 ```
 
 
+#### recurrenceid([_moment_|_Date_ stamp])
+
+Recurrence date as Date object.
+
+
 #### summary([_String_ summary])
 
 Appointment summary, defaults to empty string.
diff --git a/examples/example-recurrence.js b/examples/example-recurrence.js
new file mode 100644
index 0000000..33a877d
--- /dev/null
+++ b/examples/example-recurrence.js
@@ -0,0 +1,17 @@
+'use strict';
+
+const ical = require('../src/index');
+const moment = require('moment');
+const cal = ical({domain: 'localhost'});
+
+// overwrite domain
+cal.domain('example.com');
+
+cal.createEvent({
+    start: moment().add(1, 'hour'),
+    end: moment().add(2, 'hours'),
+    summary: 'Example Recurrence-Id',
+    recurrenceid: moment().add(4, 'hour')
+});
+
+console.log(cal.toString());
diff --git a/index.d.ts b/index.d.ts
index 0b044e7..11065da 100644
--- a/index.d.ts
+++ b/index.d.ts
@@ -73,6 +73,7 @@ declare module 'ical-generator' {
       alarms?: AlarmData[];
       status?: string;
       timezone?: string;
+      recurrenceid?: moment.Moment | Date;
     }
 
     interface RepeatingData {
diff --git a/src/event.js b/src/event.js
index 63c4737..8c93c08 100755
--- a/src/event.js
+++ b/src/event.js
@@ -62,7 +62,8 @@ class ICalEvent {
             'busystatus',
             'url',
             'created',
-            'lastModified'
+            'lastModified',
+            'recurrenceid'
         ];
         this._vars = {
             allowedRepeatingFreq: ['SECONDLY', 'MINUTELY', 'HOURLY', 'DAILY', 'WEEKLY', 'MONTHLY', 'YEARLY'],
@@ -215,6 +216,36 @@ class ICalEvent {
         return this;
     }
 
+    /**
+     * Set/Get the event's recurrence id
+     *
+     * @since 0.2.0
+     * @param {Date|moment|String|null} [recurrenceid] Recurrence date as moment.js object
+     * @returns {ICalEvent|Date}
+     */
+    recurrenceid(recurrenceid) {
+        if (recurrenceid === undefined) {
+            return this._data.recurrenceid;
+        }
+
+        if (typeof recurrenceid === 'string') {
+            recurrenceid = moment(recurrenceid);
+        }
+        else if (recurrenceid instanceof Date) {
+            recurrenceid = moment(recurrenceid);
+        }
+        else if (!(recurrenceid instanceof moment)) {
+            throw new Error('`recurrenceid` must be a Date or a moment object!');
+        }
+
+        if (!recurrenceid.isValid()) {
+            throw new Error('`recurrenceid` has to be a valid date!');
+        }
+
+        this._data.recurrenceid = recurrenceid;
+        return this;
+    }
+
     /**
      * Set/Get the event's timezone.  This unsets the event's floating flag.
      * Used on date properties
@@ -995,6 +1026,11 @@ class ICalEvent {
             }
         }
 
+        // RECURRENCE
+        if (this._data.recurrenceid) {
+            g += ICalTools.formatDateTZ(this._calendar.timezone(), 'RECURRENCE-ID', this._data.recurrenceid, this._data) + '\r\n';
+        }
+
         // SUMMARY
         g += 'SUMMARY:' + ICalTools.escape(this._data.summary) + '\r\n';
 
diff --git a/test.js b/test.js
new file mode 100644
index 0000000..e69de29


Test patch:
diff --git a/test/test_event.js b/test/test_event.js
index f04bd09..5180526 100644
--- a/test/test_event.js
+++ b/test/test_event.js
@@ -199,6 +199,48 @@ describe('ical-generator Event', function () {
         });
     });
 
+    describe('recurrenceid()', function () {
+        it('getter should return value', function () {
+            const now = moment();
+            const event = new ICalEvent(null, new ICalCalendar());
+            event._data.recurrenceid = now;
+            assert.ok(event.recurrenceid().isSame(now));
+        });
+
+        it('setter should parse string if required', function () {
+            const event = new ICalEvent(null, new ICalCalendar());
+            const date = moment().add(1, 'week');
+            assert.deepStrictEqual(event, event.recurrenceid(date.toJSON()));
+            assert.ok(event._data.recurrenceid.isSame(date));
+        });
+
+        it('setter should handle Dates if required', function () {
+            const event = new ICalEvent(null, new ICalCalendar());
+            const date = moment().add(1, 'week');
+            assert.deepStrictEqual(event, event.recurrenceid(date.toDate()));
+            assert.ok(event._data.recurrenceid.isSame(date));
+        });
+
+        it('setter should throw error when time is not a Date', function () {
+            const event = new ICalEvent(null, new ICalCalendar());
+            assert.throws(function () {
+                event.recurrenceid(3);
+            }, /`recurrenceid`/, 'Number');
+            assert.throws(function () {
+                event.recurrenceid(NaN);
+            }, /`recurrenceid`/, 'NaN');
+            assert.throws(function () {
+                event.recurrenceid(new Date('hallo'));
+            }, /`recurrenceid`/, 'Invalid Date');
+        });
+
+        it('setter should return this', function () {
+            const event = new ICalEvent(null, new ICalCalendar());
+            assert.deepStrictEqual(event, event.recurrenceid(moment()));
+            assert.deepStrictEqual(event, event.recurrenceid(new Date()));
+        });
+    });
+
     describe('timezone()', function () {
         it('getter should return value', function () {
             const e = new ICalEvent(null, new ICalCalendar()).timezone('Europe/Berlin');