From d6a7b8e5c568acdee2e3a16c2eff4d40e8edba6b Mon Sep 17 00:00:00 2001 From: mbalfour Date: Sat, 16 Jul 2016 10:33:15 -0500 Subject: [PATCH 1/2] Fixes for Recurrence Rules Added better support for Recurrence Rules: - Updates to specific recurrences of events now get their own entries in a recurrences[] array, keyed by ISO date/time. (Previously, their records silently overwrote the main recurrence event) - Exceptions to recurrences now appear in an exdate[] array, keyed by ISO date/time. Also added "knowledge" of the dtstamp, created, and lastmodified fields so that they get stored as dates, not strings. This should solve issues #57 and #63. --- ical.js | 143 +++++++++++++++++++++++++++++++++++++++++++------------- 1 file changed, 111 insertions(+), 32 deletions(-) diff --git a/ical.js b/ical.js index 252e426..9516b26 100755 --- a/ical.js +++ b/ical.js @@ -55,36 +55,42 @@ return val; } - var storeParam = function(name){ - return function(val, params, curr){ - var data; - if (params && params.length && !(params.length==1 && params[0]==='CHARSET=utf-8')){ - data = {params:parseParams(params), val:text(val)} + var storeValParam = function (name) { + return function (val, curr) { + var current = curr[name]; + if (Array.isArray(current)) { + current.push(val); + return curr; + } + + if (current != null) { + curr[name] = [current, val]; + return curr; + } + + curr[name] = val; + return curr } - else - data = text(val) + } - var current = curr[name]; - if (Array.isArray(current)){ - current.push(data); - return curr; - } + var storeParam = function (name) { + return function (val, params, curr) { + var data; + if (params && params.length && !(params.length == 1 && params[0] === 'CHARSET=utf-8')) { + data = { params: parseParams(params), val: text(val) } + } + else + data = text(val) - if (current != null){ - curr[name] = [current, data]; - return curr; + return storeValParam(name)(data, curr); } - - curr[name] = data; - return curr - } } - var addTZ = function(dt, name, params){ + var addTZ = function (dt, params) { var p = parseParams(params); if (params && p){ - dt[name].tz = p.TZID + dt.tz = p.TZID } return dt @@ -92,10 +98,10 @@ var dateParam = function(name){ - return function(val, params, curr){ + return function (val, params, curr) { + + var newDate = text(val); - // Store as string - worst case scenario - storeParam(name)(val, undefined, curr) if (params && params[0] === "VALUE=DATE") { // Just Date @@ -103,13 +109,16 @@ var comps = /^(\d{4})(\d{2})(\d{2})$/.exec(val); if (comps !== null) { // No TZ info - assume same timezone as this computer - curr[name] = new Date( + newDate = new Date( comps[1], parseInt(comps[2], 10)-1, comps[3] ); - return addTZ(curr, name, params); + newDate = addTZ(newDate, params); + + // Store as string - worst case scenario + return storeValParam(name)(newDate, curr) } } @@ -118,7 +127,7 @@ var comps = /^(\d{4})(\d{2})(\d{2})T(\d{2})(\d{2})(\d{2})(Z)?$/.exec(val); if (comps !== null) { if (comps[7] == 'Z'){ // GMT - curr[name] = new Date(Date.UTC( + newDate = new Date(Date.UTC( parseInt(comps[1], 10), parseInt(comps[2], 10)-1, parseInt(comps[3], 10), @@ -128,7 +137,7 @@ )); // TODO add tz } else { - curr[name] = new Date( + newDate = new Date( parseInt(comps[1], 10), parseInt(comps[2], 10)-1, parseInt(comps[3], 10), @@ -137,10 +146,14 @@ parseInt(comps[6], 10) ); } - } - return addTZ(curr, name, params) + newDate = addTZ(newDate, params); } + + + // Store as string - worst case scenario + return storeValParam(name)(newDate, curr) + } } @@ -166,7 +179,27 @@ } } - var addFBType = function(fb, params){ + // EXDATE is an entry that represents exceptions to a recurrence rule (ex: "repeat every day except on 7/4"). + // There can be more than one of these in a calendar record, so we create an array of them. + // The index into the array is the ISO string of the date itself, for ease of use. + // i.e. You can check if ((curr.exdate != undefined) && (curr.exdate[date iso string] != undefined)) to see if a date is an exception. + var exdateParam = function (name) { + return function (val, params, curr) { + var exdate = new Array(); + dateParam(name)(val, params, exdate); + curr[name] = curr[name] || []; + curr[name][exdate[name].toISOString()] = exdate[name]; + return curr; + } + } + + // RECURRENCE-ID is the ID of a specific recurrence within a recurrence rule. + // TODO: It's also possible for it to have a range, like "THISANDPRIOR", "THISANDFUTURE". This isn't currently handled. + var recurrenceParam = function (name) { + return dateParam(name); + } + + var addFBType = function (fb, params) { var p = parseParams(params); if (params && p){ @@ -226,7 +259,47 @@ var par = stack.pop() if (curr.uid) - par[curr.uid] = curr + { + // If this is the first time we run into this UID, just save it. + if (par[curr.uid] === undefined) + { + par[curr.uid] = curr + } + else + { + // If we have multiple ical entries with the same UID, it's either going to be a + // modification to a recurrence (RECURRENCE-ID), and/or a significant modification + // to the entry (SEQUENCE). + + // TODO: Look into proper sequence logic. + + // If we have recurrence-id entries, list them as an array of recurrences keyed off of recurrence-id. + // To use - as you're running through the dates of an rrule, you can try looking it up in the recurrences + // array. If it exists, then use the data from the calendar object in the recurrence instead of the parent + // for that day. + + var parent = par[curr.uid]; + if (curr.recurrenceid != null) { + if (parent.recurrences === undefined) { + parent.recurrences = new Array(); + } + + // TODO: Is there ever a case where we have to worry about overwriting an existing entry here? + + parent.recurrences[curr.recurrenceid.toISOString()] = curr; + } + else + { + // If we have the same UID as an existing record, and it *isn't* a specific recurrence ID, + // not quite sure what the correct behaviour should be. For now, just take the new information + // and merge it with the old record by overwriting only the fields that appear in the new record. + var key; + for (key in curr) { + par[key] = curr[key]; + } + } + } + } else par[Math.random()*100000] = curr // Randomly assign ID : TODO - use true GUID @@ -247,6 +320,12 @@ , 'COMPLETED': dateParam('completed') , 'CATEGORIES': categoriesParam('categories') , 'FREEBUSY': freebusyParam('freebusy') + , 'DTSTAMP': dateParam('dtstamp') + , 'EXDATE': exdateParam('exdate') + , 'CREATED': dateParam('created') + , 'LAST-MODIFIED': dateParam('lastmodified') + , 'RECURRENCE-ID': recurrenceParam('recurrenceid') + }, From b9f2e93c81f98743bd5a5c9344e82d1a31e59a1d Mon Sep 17 00:00:00 2001 From: mbalfour Date: Mon, 18 Jul 2016 22:01:46 -0500 Subject: [PATCH 2/2] Added tests and maintained old code Code maintenance problems: * Example.js should be pointing to node-ical, not ical now. * ical should depend on rrule 2.1.0 or greater due to dependency issues with underscore Added tests: * Verify that a recurrence-id doesn't overwrite the main entry * Verify that exdates appear in the exdate array * Verify that recurrence-id entries appear in the recurrences array * Verify that the arrays are keyed by ISO date strings --- example.js | 2 +- package.json | 2 +- test/test.js | 35 ++++++++++++++++++++++++++++++++--- test/test12.ics | 19 +++++++++++++++++++ 4 files changed, 53 insertions(+), 5 deletions(-) create mode 100644 test/test12.ics diff --git a/example.js b/example.js index aceb0e7..c93b290 100644 --- a/example.js +++ b/example.js @@ -1,4 +1,4 @@ -var ical = require('ical') +var ical = require('./node-ical') , months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'] diff --git a/package.json b/package.json index 991b144..c7f246c 100644 --- a/package.json +++ b/package.json @@ -16,7 +16,7 @@ }, "dependencies": { "request": "2.68.0", - "rrule": "2.0.0" + "rrule": "2.1.0" }, "devDependencies": { "vows": "0.7.0", diff --git a/test/test.js b/test/test.js index 2aeff1e..9b67f9f 100755 --- a/test/test.js +++ b/test/test.js @@ -197,7 +197,7 @@ vows.describe('node-ical').addBatch({ } } - , 'with test6.ics (testing assembly.org)' : { + , 'with test6.ics (testing assembly.org)': { topic: function () { return ical.parseFile('./test/test6.ics') } @@ -370,9 +370,38 @@ vows.describe('node-ical').addBatch({ assert.equal(topic.end.getUTCMinutes(), 00); } } - }, + } + + , 'with test12.ics (testing recurrences and exdates)': { + topic: function () { + return ical.parseFile('./test/test12.ics') + } + , 'event with rrule': { + topic: function (events) { + return _.select(_.values(events), function (x) { + return x.uid === '0000001'; + })[0]; + } + , "Has an RRULE": function (topic) { + assert.notEqual(topic.rrule, undefined); + } + , "Has summary Treasure Hunting": function (topic) { + assert.equal(topic.summary, 'Treasure Hunting'); + } + , "Has two EXDATES": function (topic) { + assert.notEqual(topic.exdate, undefined); + assert.notEqual(topic.exdate[new Date(2015, 06, 08, 12, 0, 0).toISOString()], undefined); + assert.notEqual(topic.exdate[new Date(2015, 06, 10, 12, 0, 0).toISOString()], undefined); + } + , "Has a RECURRENCE-ID override": function (topic) { + assert.notEqual(topic.recurrences, undefined); + assert.notEqual(topic.recurrences[new Date(2015, 06, 07, 12, 0, 0).toISOString()], undefined); + assert.equal(topic.recurrences[new Date(2015, 06, 07, 12, 0, 0).toISOString()].summary, 'More Treasure Hunting'); + } + } + } - 'url request errors' : { + , 'url request errors' : { topic : function () { ical.fromURL('http://not.exist/', {}, this.callback); } diff --git a/test/test12.ics b/test/test12.ics new file mode 100644 index 0000000..4e78603 --- /dev/null +++ b/test/test12.ics @@ -0,0 +1,19 @@ +BEGIN:VCALENDAR +BEGIN:VEVENT +UID:0000001 +SUMMARY:Treasure Hunting +DTSTART;TZID=America/Los_Angeles:20150706T120000 +DTEND;TZID=America/Los_Angeles:20150706T130000 +RRULE:FREQ=DAILY;COUNT=10 +EXDATE;TZID=America/Los_Angeles:20150708T120000 +EXDATE;TZID=America/Los_Angeles:20150710T120000 +END:VEVENT +BEGIN:VEVENT +UID:0000001 +SUMMARY:More Treasure Hunting +LOCATION:The other island +DTSTART;TZID=America/Los_Angeles:20150709T150000 +DTEND;TZID=America/Los_Angeles:20150707T160000 +RECURRENCE-ID;TZID=America/Los_Angeles:20150707T120000 +END:VEVENT +END:VCALENDAR