icalendar/icalendar — pull request #51 by jnraine (2014-01-29 00:08:11+00:00)
Title: Add recurrence scheduling of RRULE/EXDATE

Hi there!
I needed an ICS parser for a project I'm working on and your library worked wonderfully. When it came time to calculate the schedule for a recurring event, I noticed the implementation was incomplete so I thought I could help.

I'm using the [ice_cube](https://github.com/seejohnrun/ice_cube) gem to do most the heavy lifting, mapping RRULEs and EXDATEs to ice_cube recurrence rules. The result is something that just works for commonly used recurrence rules.

Here's an a few examples:
```ruby
require 'icalendar'

include Icalendar

ics_string = <<-EOF
BEGIN:VCALENDAR
X-WR-CALNAME:Test Public
X-WR-CALID:f512e378-050c-4366-809a-ef471ce45b09:101165
PRODID:Zimbra-Calendar-Provider
VERSION:2.0
METHOD:PUBLISH
BEGIN:VEVENT
UID:efcb99ae-d540-419c-91fa-42cc2bd9d302
RRULE:FREQ=DAILY;INTERVAL=1
SUMMARY:Every day, except the 28th
DTSTART;VALUE=DATE:20140101
DTEND;VALUE=DATE:20140102
STATUS:CONFIRMED
CLASS:PUBLIC
X-MICROSOFT-CDO-ALLDAYEVENT:TRUE
TRANSP:TRANSPARENT
LAST-MODIFIED:20140113T200625Z
DTSTAMP:20140113T200625Z
SEQUENCE:0
EXDATE;VALUE=DATE:20140128
END:VEVENT
END:VCALENDAR
EOF

# An event that occurs every day, starting January 1, 2014 with one excluded
# date. January 28, 2014 will not appear in the occurrences.
calendars = Icalendar.parse(ics_string)
every_day_except_jan_28 = Array(calendars).first.events.first
puts "Every day except January 28, 2014, occurrences from 2014-01-01 to 2014-02-01:"
puts every_day_except_jan_28.occurrences_between(Date.parse("2014-01-01"), Date.parse("2014-02-01"))

ics_string = <<-EOF
BEGIN:VCALENDAR
X-WR-CALNAME:Scheduled Maintenance
X-WR-CALID:733c049c-f30e-43dc-979b-447fcdd1978c:257
PRODID:Zimbra-Calendar-Provider
VERSION:2.0
METHOD:PUBLISH
BEGIN:VTIMEZONE
TZID:America/Los_Angeles
BEGIN:STANDARD
DTSTART:19710101T020000
TZOFFSETTO:-0800
TZOFFSETFROM:-0700
RRULE:FREQ=YEARLY;WKST=MO;INTERVAL=1;BYMONTH=11;BYDAY=1SU
TZNAME:PST
END:STANDARD
BEGIN:DAYLIGHT
DTSTART:19710101T020000
TZOFFSETTO:-0700
TZOFFSETFROM:-0800
RRULE:FREQ=YEARLY;WKST=MO;INTERVAL=1;BYMONTH=3;BYDAY=2SU
TZNAME:PDT
END:DAYLIGHT
END:VTIMEZONE
BEGIN:VEVENT
UID:d317c7c7-a6cc-4a85-baed-1542a336899d
RRULE:FREQ=MONTHLY;INTERVAL=1;BYDAY=1SA
SUMMARY:Email/SFU Connect
DESCRIPTION: Scheduled maintenance
DTSTART;TZID="America/Los_Angeles":20140104T000000
DTEND;TZID="America/Los_Angeles":20140104T040000
STATUS:CONFIRMED
CLASS:PUBLIC
X-MICROSOFT-CDO-INTENDEDSTATUS:BUSY
TRANSP:OPAQUE
LAST-MODIFIED:20110514T001357Z
DTSTAMP:20140104T075122Z
SEQUENCE:1
END:VEVENT
END:VCALENDAR
EOF

# A more complex example, showing an event that occurs on the first saturday of
# every month. Illustrates the handling of timezones and DST: event scheduled
# at midnight to 4am in America/Los_Angeles timezone (8am to 12pm UTC). DST
# occurs on March 9th, causing the last occurrence on April 5th to occur at
# 7am to 11am UTC.
calendars = Icalendar.parse(ics_string)
first_saturday_of_month = Array(calendars).first.events.first
puts "First Saturday of the month, occurrences from 2014-01-01 to 2014-05-01:"
puts first_saturday_of_month.occurrences_between(Date.parse("2014-01-01"), Date.parse("2014-05-01"))
```

I've included tests to ensure all is working as expected (doing otherwise would have melted my brain...).

I noticed the library has no runtime dependencies at the moment. I was hoping to keep it that way when I started, but recurrence can get ugly and ice_cube does a great job at providing an understandable interface.

I've kept the changes to existing classes minimal — excluding tests/gemspec/requires, I added a few methods to the `Icalendar::Event` and added two new classes: `Icalendar::Schedule` (talks to ice_cube) and `Icalendar::TimeUtil` (simplifies `Date`, `DateTime` coercion).

Please let me know if there is something that requires changing to keep in line with past development work.

Cheers,
Jordan