Repository: adamgibbons/ics
Pull request #246 (2023-07-13 20:58:06)

EXDATE support (string)
### Summary
> simple inclusion of EXDATE support in formatEvent() supporting a simple string
> 
> Includes a unit test and a detailed entry in the README

### Who does this help?
This satisfies my own needs, and the needs of those expressed in:
- #219 
- #179

Both contain discussions on how to "hack" a solution: appending `EXDATE:` to any other string input that doesn't get sanitized by `foldLine()`.

That definitely works. But I thought this was kinda silly. It wouldn't take much to support `EXDATE`, right?

### Attempt using `DateArray[]`
I spent a little time flirting with the idea of defining `excludeDate` as an array of `DateArray[]`, but I realized that it would "imply" the allowance of a mix DATE and DATE-TIME input. 

Then I started thinking about how I might have it be `[number, number, number][] | [number, number, number, number][] | [number, number, number, number, number][]`. But that seemed ugly.

And _then_ we'd have to provide more attributes like `excludeDateInputType` and `excludeDateOutputType`.

### Keep it simple with a string input
I realized that it was far simpler to let the end user to provide the string themself, like is already expected with `recurrenceRule`.

### Note on README Doc example
In testing, using MacOS's Calendar App, I found that support for `EXDATE;VALUE=DATE:YYYYMMDD` doesn't work. So, as a result of this test revealing that iCal support is spotty, I removed any mention of it in the README.

### Testing
I tested EXDATE with MacOS's Calendar App with a recurrence rule calendar.
<img width="500" alt="Screenshot 2023-07-13 at 3 34 41 PM" src="https://github.com/adamgibbons/ics/assets/2433505/5350a199-2101-4d7c-b922-1eeb0ac16b83">
In this example I had a weekly repeating event on Tuesday at 6pm EDT (-04:00 = 22:00 UTC).

```
DTSTART:20221101T220000Z
EXDATE:20230711T220000Z,20230718T220000Z
```
As you can see, the recurring events on July 11 and July 18 are excluded.

(This was really just to make sure that my README examples are correct).

### Thanks!
I hope this addition is a decent contribution. I'm proud to say it's not my first to this lovely project (#220)

Please let me know if revisions to the README or anything else is desired. Cheers!


Patch:
diff --git a/README.md b/README.md
index d0d5f6a..d77840f 100644
--- a/README.md
+++ b/README.md
@@ -213,22 +213,22 @@ async function handleDownload() {
       if (error) {
         reject(error)
       }
-      
+
       resolve(new File([value], filename, { type: 'text/calendar' }))
     })
   })
   const url = URL.createObjectURL(file);
-  
+
   // trying to assign the file URL to a window could cause cross-site
   // issues so this is a workaround using HTML5
   const anchor = document.createElement('a');
   anchor.href = url;
   anchor.download = filename;
-  
+
   document.body.appendChild(anchor);
   anchor.click();
   document.body.removeChild(anchor);
-  
+
   URL.revokeObjectURL(url);
 }
 ```
@@ -270,7 +270,8 @@ The following properties are accepted:
 | productId     | Product which created ics, `PRODID` field | `'adamgibbons/ics'`
 | uid           | Universal unique id for event, produced by default with `nanoid`.  **Warning:** This value must be **globally unique**.  It is recommended that it follow the [RFC 822 addr-spec](https://www.w3.org/Protocols/rfc822/) (i.e. `localpart@domain`).  Including the `@domain` half is a good way to ensure uniqueness. | `'LZfXLFzPPR4NNrgjlWDxn'`
 | method        | This property defines the iCalendar object method associated with the calendar object. When used in a MIME message entity, the value of this property MUST be the same as the Content-Type "method" parameter value.  If either the "METHOD" property or the Content-Type "method" parameter is specified, then the other MUST also be specified. | `PUBLISH`
-| recurrenceRule        | A recurrence rule, commonly referred to as an RRULE, defines the repeat pattern or rule for to-dos, journal entries and events. If specified, RRULE can be used to compute the recurrence set (the complete set of recurrence instances in a calendar component). You can use a generator like this [one](https://www.textmagic.com/free-tools/rrule-generator) | `FREQ=DAILY`
+| recurrenceRule        | A recurrence rule, commonly referred to as an RRULE, defines the repeat pattern or rule for to-dos, journal entries and events. If specified, RRULE can be used to compute the recurrence set (the complete set of recurrence instances in a calendar component). You can use a generator like this [one](https://www.textmagic.com/free-tools/rrule-generator). | `FREQ=DAILY`
+| exclusionDates| This property defines the list of DATE-TIME exceptions for recurring events, to-dos, journal entries, or time zone definitions. Uses a comma-delimited list of [Date-Time](https://tools.ietf.org/html/rfc5545#section-3.3.5) values. See [EXDATE spec](https://tools.ietf.org/html/rfc5545#section-3.8.5.1).|`'20230620T131500Z,20230621T131500'` (June 20th, 2023 at 1:15pm UTC and June 21st, 2000 at 1:15pm LOCAL)
 | sequence      | For sending an update for an event (with the same uid), defines the revision sequence number. | `2`
 | busyStatus    | Used to specify busy status for Microsoft applications, like Outlook. See [Microsoft spec](https://docs.microsoft.com/en-us/openspecs/exchange_server_protocols/ms-oxcical/cd68eae7-ed65-4dd3-8ea7-ad585c76c736). | `'BUSY'` OR `'FREE'` OR `'TENTATIVE`' OR `'OOF'`
 | transp        | Used to specify event transparency (does event consume actual time of an individual). Used by Google Calendar to determine if event should change attendees availability to 'Busy' or not. | `'TRANSPARENT'` OR `'OPAQUE'`
diff --git a/index.d.ts b/index.d.ts
index 7193085..8cba459 100644
--- a/index.d.ts
+++ b/index.d.ts
@@ -104,6 +104,7 @@ export type EventAttributes = {
   uid?: string;
   method?: string;
   recurrenceRule?: string;
+  exclusionDates?: string;
   sequence?: number;
   calName?: string;
   classification?: classificationType;
diff --git a/src/pipeline/format.js b/src/pipeline/format.js
index a587161..92d2fa9 100644
--- a/src/pipeline/format.js
+++ b/src/pipeline/format.js
@@ -37,6 +37,7 @@ export default function formatEvent(attributes = {}) {
     attendees,
     alarms,
     recurrenceRule,
+    exclusionDates,
     busyStatus,
     transp,
     classification,
@@ -89,6 +90,7 @@ export default function formatEvent(attributes = {}) {
     })
   }
   icsFormat += recurrenceRule ? `RRULE:${recurrenceRule}\r\n` : ''
+  icsFormat += exclusionDates ? `EXDATE:${exclusionDates}\r\n` : ''
   icsFormat += duration ? `DURATION:${formatDuration(duration)}\r\n` : ''
   if (alarms) {
     alarms.map(function (alarm) {


Test patch:
diff --git a/test/pipeline/format.spec.js b/test/pipeline/format.spec.js
index dff01cc..5bf9928 100644
--- a/test/pipeline/format.spec.js
+++ b/test/pipeline/format.spec.js
@@ -265,4 +265,9 @@ describe('pipeline.formatEvent', () => {
 
     expect(formattedEvent).to.contain('RRULE:FREQ=DAILY')
   })
+  it('writes exception date-time', () => {
+    const formattedEvent = formatEvent({ exclusionDates: '20000620T010000Z,20000621T010000Z' })
+
+    expect(formattedEvent).to.contain('EXDATE:20000620T010000Z,20000621T010000Z')
+  })
 })