diff --git a/ical.js b/ical.js index 1e19bcc..287e05c 100644 --- a/ical.js +++ b/ical.js @@ -614,7 +614,12 @@ module.exports = { // If the original date has a TZID, add it if (curr.start.tz) { const tz = getTimeZone(curr.start.tz); - rule += `;DTSTART;TZID=${tz}:${curr.start.toISOString().replace(/[-:]/g, '')}`; + // If a timezone is provided, rrule requires the time to be local + const adjustedTimeString = curr.start + .toLocaleString('sv', {timeZone: tz}) + .replace(/ /g, 'T') + .replace(/[-:]/g, ''); + rule += `;DTSTART;TZID=${tz}:${adjustedTimeString}`; } else { rule += `;DTSTART=${curr.start.toISOString().replace(/[-:]/g, '')}`; } diff --git a/test/test-async.js b/test/test-async.js index 62e110b..220bd08 100644 --- a/test/test-async.js +++ b/test/test-async.js @@ -5,10 +5,14 @@ ** */ process.env.TZ = 'America/San_Francisco'; +const moment = require('moment-timezone'); +/* Setup moment timezone defaults */ +moment.tz.link('Etc/Unknown|Etc/GMT'); +moment.tz.setDefault('America/San_Francisco'); + const assert = require('assert'); const vows = require('vows'); const _ = require('underscore'); -const moment = require('moment-timezone'); const ical = require('../node-ical.js'); console.log('START Async Tests'); diff --git a/test/test.js b/test/test.js index 6963f28..22671ff 100644 --- a/test/test.js +++ b/test/test.js @@ -5,10 +5,14 @@ ** */ process.env.TZ = 'America/San_Francisco'; +const moment = require('moment-timezone'); +/* Setup moment timezone defaults */ +moment.tz.link('Etc/Unknown|Etc/GMT'); +moment.tz.setDefault('America/San_Francisco'); + const assert = require('assert'); const vows = require('vows'); const _ = require('underscore'); -const moment = require('moment-timezone'); const ical = require('../node-ical.js'); vows @@ -150,8 +154,10 @@ vows }); }, 'tzid offset correctly applied'(event) { - const start = new Date('2002-10-28T22:00:00.000Z'); - assert.equal(event.start.valueOf(), start.valueOf()); + assert.ok(moment.tz.zone(event.start.tz), 'zone does not exist'); + const ref = '2002-10-28T22:00:00Z'; + const start = moment(event.start).tz(event.start.tz); + assert.equal(start.utc().format(), ref); } } }, @@ -1067,6 +1073,94 @@ vows assert.equal(task.summary, 'test export import'); } } + }, + 'with test23.ics (testing dtstart of rrule with timezones)': { + topic() { + return ical.parseFile('./test/test23.ics'); + }, + 'first event': { + topic(events) { + return _.select(_.values(events), x => { + return x.uid === '000021a'; + })[0]; + }, + 'datetype is date-time'(topic) { + assert.equal(topic.datetype, 'date-time'); + }, + 'has GMT+1 timezone'(topic) { + assert.equal(topic.start.tz, 'Europe/Berlin'); + }, + 'starts 14 Jul 2022 @ 12:00:00 (UTC)'(topic) { + assert.equal(topic.start.toISOString(), '2022-07-14T12:00:00.000Z'); + } + }, + 'recurring yearly first event (14 july)': { + topic(events) { + /* Skip on windows since rrule.between/after broken, cf. https://github.com/jkbrzt/rrule/issues/608 */ + if (process.platform === 'win32') { + return new Date(2023, 6, 14, 12, 0, 0); + } + + const ev = _.select(_.values(events), x => { + return x.uid === '000021a'; + })[0]; + return ev.rrule.between(new Date(2023, 0, 1), new Date(2024, 0, 1))[0]; + }, + 'dt start well set'(topic) { + assert.equal(topic.toDateString(), new Date(2023, 6, 14).toDateString()); + }, + 'starts 14 Jul 2023 @ 12:00:00 (UTC)'(topic) { + assert.equal(topic.toISOString(), '2023-07-14T12:00:00.000Z'); + } + }, + 'second event': { + topic(events) { + return _.select(_.values(events), x => { + return x.uid === '000021b'; + })[0]; + }, + 'datetype is date-time'(event) { + assert.equal(event.datetype, 'date-time'); + }, + 'start date': { + topic(event) { + return event.start; + }, + 'has correct timezone'(start) { + assert.equal(start.tz, 'Etc/GMT-2'); + }, + 'starts 15 Jul 2022 @ 12:00:00 (UTC)'(start) { + assert.equal(start.toISOString(), '2022-07-15T12:00:00.000Z'); + } + }, + 'has recurrences': { + topic(event) { + return event.rrule; + }, + 'that are defined'(rrule) { + assert.ok(rrule, 'no rrule defined'); + }, + 'that have timezone info'(rrule) { + assert.ok(rrule.options.tzid, 'no tzid property on rrule'); + }, + 'that keep correct timezone info in recurrences'(rrule) { + assert.equal(rrule.options.tzid, 'Etc/GMT-2'); + } + }, + 'has a first recurrence': { + topic(event) { + /* Skip on windows since rrule.between/after broken, cf. https://github.com/jkbrzt/rrule/issues/608 */ + if (process.platform === 'win32') { + return new Date(2023, 6, 15, 12, 0, 0); + } + + return event.rrule.between(new Date(2023, 0, 1), new Date(2024, 0, 1))[0]; + }, + 'that starts 15 Jul 2023 @ 12:00:00 (UTC)'(rc) { + assert.equal(rc.toISOString(), '2023-07-15T12:00:00.000Z'); + } + } + } } }) .export(module); diff --git a/test/test21-mod.ics b/test/test21-mod.ics new file mode 100644 index 0000000..c0a0a32 --- /dev/null +++ b/test/test21-mod.ics @@ -0,0 +1,21 @@ +BEGIN:VCALENDAR +BEGIN:VEVENT +TRANSP:OPAQUE +X-MICROSOFT-CDO-INTENDEDSTATUS:BUSY +CREATED:20221004T073016Z +LAST-MODIFIED:20221011T063437Z +DTSTAMP:20221011T063437Z +DTSTART;TZID="(GMT +01:00)":20221004T140000 +DTEND;TZID="(GMT +01:00)":20221004T150000 +SUMMARY:Music School +CLASS:PUBLIC +UID:0000021 +X-MOZ-SNOOZE-TIME:20221004T113000Z +X-MICROSOFT-CDO-OWNER-CRITICAL-CHANGE:20221014T203413Z +X-MICROSOFT-CDO-ATTENDEE-CRITICAL-CHANGE:20221014T203413Z +X-MICROSOFT-CDO-APPT-SEQUENCE:0 +X-MICROSOFT-CDO-OWNERAPPTID:-1 +X-MICROSOFT-CDO-ALLDAYEVENT:FALSE +RRULE:FREQ=WEEKLY;UNTIL=20221201T020000;BYDAY=TU +END:VEVENT +END:VCALENDAR diff --git a/test/test23.ics b/test/test23.ics new file mode 100644 index 0000000..58be6b1 --- /dev/null +++ b/test/test23.ics @@ -0,0 +1,26 @@ +BEGIN:VCALENDAR +VERSION:2.0 +PRODID:Fictitious Recurrence Test Calendar +BEGIN:VEVENT +CREATED:20221018T221500Z +DTSTAMP:20221019T171200Z +UID:000021a +SUMMARY:Party +RRULE:FREQ=YEARLY +DTSTART;TZID=Europe/Berlin:20220714T140000 +DTEND;TZID=Europe/Berlin:20220714T210000 +TRANSP:OPAQUE +SEQUENCE:5 +END:VEVENT +BEGIN:VEVENT +CREATED:20221019T181700Z +DTSTAMP:20221019T191200Z +UID:000021b +SUMMARY:Party next day +RRULE:FREQ=YEARLY +DTSTART;TZID=Etc/GMT-2:20220715T140000 +DTEND;TZID=Etc/GMT-2:20220715T210000 +TRANSP:OPAQUE +SEQUENCE:5 +END:VEVENT +END:VCALENDAR