#- ©2009 Rick DeNatale, All rights reserved. Refer to the file README.txt for the license require File.join(File.dirname(__FILE__), %w[.. spec_helper.rb]) def mock_enumerator(name, next_occurrence) mock(name, :next_occurrence => next_occurrence, :bounded? => true, :empty? => false) end # Note that this is more of a functional spec describe RiCal::OccurrenceEnumerator do Fr13Unbounded_Zulu = <<-TEXT BEGIN:VEVENT DTSTART:19970902T090000Z EXDATE:19970902T090000Z RRULE:FREQ=MONTHLY;BYDAY=FR;BYMONTHDAY=13 END:VEVENT TEXT Fr13Unbounded_Eastern = <<-TEXT BEGIN:VEVENT DTSTART;TZID=US-Eastern:19970902T090000 EXDATE;TZID=US-Eastern:19970902T090000 RRULE:FREQ=MONTHLY;BYDAY=FR;BYMONTHDAY=13 END:VEVENT TEXT Fr13UnboundedZuluExpectedFive = [ "19980213T090000Z", "19980313T090000Z", "19981113T090000Z", "19990813T090000Z", "20001013T090000Z" ].map {|start| src = <<-TEXT BEGIN:VEVENT DTSTART:#{start} RECURRENCE-ID:#{start} END:VEVENT TEXT RiCal.parse_string(src).first } describe ".occurrences" do describe "with an unbounded component" do before(:each) do @it = RiCal.parse_string(Fr13Unbounded_Zulu).first end it "should raise an ArgumentError with no options to limit result" do lambda {@it.occurrences}.should raise_error(ArgumentError) end it "should have the right five occurrences when :count => 5 option is used" do result = @it.occurrences(:count => 5) result.should == Fr13UnboundedZuluExpectedFive end end end describe ".occurrences" do before(:each) do @it = RiCal.parse_string(Fr13Unbounded_Zulu).first end describe "with :starting specified" do it "should exclude dates before :starting" do result = @it.occurrences(:starting => Fr13UnboundedZuluExpectedFive[1].dtstart, :before => Fr13UnboundedZuluExpectedFive[-1].dtstart) result.map{|o|o.dtstart}.should == Fr13UnboundedZuluExpectedFive[1..-2].map{|e| e.dtstart} end end describe "with :before specified" do it "should exclude dates after :before" do result = @it.occurrences(:before => Fr13UnboundedZuluExpectedFive[3].dtstart, :count => 5) result.map{|o|o.dtstart}.should == Fr13UnboundedZuluExpectedFive[0..2].map{|e| e.dtstart} end end end describe ".each" do describe " for Every Friday the 13th, forever" do before(:each) do event = RiCal.parse_string(Fr13Unbounded_Zulu).first @result = [] event.each do |occurrence| break if @result.length >= 5 @result << occurrence end end it "should have the right first six occurrences" do # TODO - Need to properly deal with timezones @result.should == Fr13UnboundedZuluExpectedFive end end end end describe RiCal::OccurrenceEnumerator::OccurrenceMerger do before(:each) do @merger = RiCal::OccurrenceEnumerator::OccurrenceMerger end describe ".for" do it "should return an EmptyEnumerator if the rules parameter is nil" do @merger.for(nil, nil).should == RiCal::OccurrenceEnumerator::EmptyRulesEnumerator end it "should return an EmptyEnumerator if the rules parameter is empty" do @merger.for(nil, []).should == RiCal::OccurrenceEnumerator::EmptyRulesEnumerator end describe "with a single rrule" do before(:each) do @component = mock("component", :dtstart => :dtstart_value) @rrule = mock("rrule", :enumerator => :rrule_enumerator) end it "should return the enumerator the rrule" do @merger.for(@component, [@rrule]).should == :rrule_enumerator end it "should pass the component to the enumerator instantiation" do @rrule.should_receive(:enumerator).with(@component) @merger.for(@component, [@rrule]) end end describe "with multiple rrules" do before(:each) do @component = mock("component", :dtstart => :dtstart_value) @enum1 = mock_enumerator("rrule_enumerator1", :occ1) @enum2 = mock_enumerator("rrule_enumerator2", :occ2) @rrule1 = mock("rrule", :enumerator => @enum1) @rrule2 = mock("rrule", :enumerator => @enum2) end it "should return an instance of RiCal::OccurrenceEnumerator::OccurrenceMerger" do @merger.for(@component, [@rrule1, @rrule2]).should be_kind_of(RiCal::OccurrenceEnumerator::OccurrenceMerger) end it "should pass the component to the enumerator instantiation" do @rrule1.should_receive(:enumerator).with(@component).and_return(@enum1) @rrule2.should_receive(:enumerator).with(@component).and_return(@enum2) @merger.for(@component, [@rrule1, @rrule2]) end it "should preload the next occurrences" do @enum1.should_receive(:next_occurrence).and_return(:occ1) @enum2.should_receive(:next_occurrence).and_return(:occ2) @merger.for(@component, [@rrule1, @rrule2]) end end end describe "#zulu_occurrence_range" do end describe "#next_occurence" do describe "with unique nexts" do before(:each) do @enum1 = mock_enumerator("rrule_enumerator1",3) @enum2 = mock_enumerator("rrule_enumerator2", 2) @rrule1 = mock("rrule", :enumerator => @enum1) @rrule2 = mock("rrule", :enumerator => @enum2) @it = @merger.new(0, [@rrule1, @rrule2]) end it "should return the earliest occurrence" do @it.next_occurrence.should == 2 end it "should advance the enumerator which returned the result" do @enum2.should_receive(:next_occurrence).and_return(4) @it.next_occurrence end it "should not advance the other enumerator" do @enum1.should_not_receive(:next_occurrence) @it.next_occurrence end it "should properly update the next array" do @enum2.stub!(:next_occurrence).and_return(4) @it.next_occurrence @it.nexts.should == [3, 4] end end describe "with duplicated nexts" do before(:each) do @enum1 = mock_enumerator("rrule_enumerator1", 2) @enum2 = mock_enumerator("rrule_enumerator2", 2) @rrule1 = mock("rrule", :enumerator => @enum1) @rrule2 = mock("rrule", :enumerator => @enum2) @it = @merger.new(0, [@rrule1, @rrule2]) end it "should return the earliest occurrence" do @it.next_occurrence.should == 2 end it "should advance both enumerators" do @enum1.should_receive(:next_occurrence).and_return(5) @enum2.should_receive(:next_occurrence).and_return(4) @it.next_occurrence end it "should properly update the next array" do @enum1.stub!(:next_occurrence).and_return(5) @enum2.stub!(:next_occurrence).and_return(4) @it.next_occurrence @it.nexts.should == [5, 4] end end describe "with all enumerators at end" do before(:each) do @enum1 = mock_enumerator("rrule_enumerator1", nil) @enum2 = mock_enumerator("rrule_enumerator2", nil) @rrule1 = mock("rrule", :enumerator => @enum1) @rrule2 = mock("rrule", :enumerator => @enum2) @it = @merger.new(0, [@rrule1, @rrule2]) end it "should return nil" do @it.next_occurrence.should == nil end it "should not advance the enumerators which returned the result" do @enum1.should_not_receive(:next_occurrence) @enum2.should_not_receive(:next_occurrence) @it.next_occurrence end end end context "Ticket #4 from paulsm" do it "should produce 4 occurrences" do cal = RiCal.parse_string < Date.parse('01/01/1990'), :before => Date.parse("01/01/2010") ) end it "should produce the right dtstart values" do @occurrences.map {|o| o.dtstart}.should == [ Date.parse("2009-06-03"), Date.parse("2009-07-03"), Date.parse("2009-08-03"), Date.parse("2009-09-03"), Date.parse("2009-10-03"), Date.parse("2009-11-03"), Date.parse("2009-12-03") ] end it "should produce events whose dtstarts are all dates" do @occurrences.all? {|o| o.dtstart.class == ::Date}.should be_true end it "should produce the right dtend values" do @occurrences.map {|o| o.dtend}.should == [ Date.parse("2009-06-04"), Date.parse("2009-07-04"), Date.parse("2009-08-04"), Date.parse("2009-09-04"), Date.parse("2009-10-04"), Date.parse("2009-11-04"), Date.parse("2009-12-04") ] end it "should produce events whose dtstends are all dates" do @occurrences.all? {|o| o.dtend.class == ::Date}.should be_true end end context "bounded? bug" do before(:each) do events = RiCal.parse_string rectify_ical <<-ENDCAL BEGIN:VEVENT EXDATE:20090114T163000 EXDATE:20090128T163000 EXDATE:20090121T163000 EXDATE:20090211T163000 EXDATE:20090204T163000 EXDATE:20090218T163000 TRANSP:OPAQUE DTSTAMP;VALUE=DATE-TIME:20090107T024340Z CREATED;VALUE=DATE-TIME:20090107T024012Z DTEND;TZID=US/Mountain;VALUE=DATE-TIME:20090114T180000 DTSTART;TZID=US/Mountain;VALUE=DATE-TIME:20090114T163000 UID:15208112-E0FA-4A7C-954C-CFDF19D1B0E7 RRULE:FREQ=WEEKLY;INTERVAL=1;UNTIL=20090219T065959Z SUMMARY:Wild Rose XC/Skate Training Series SEQUENCE:11 LOCATION:Mountain Dell Golf Course END:VEVENT ENDCAL @event = events.first end it "should be able to enumerate occurrences" do @event.should be_bounded end end context "EXDATES with timezones bug" do before(:each) do cals = RiCal.parse_string rectify_ical <<-ENDCAL BEGIN:VCALENDAR METHOD:PUBLISH PRODID:-//Apple Inc.//iCal 3.0//EN CALSCALE:GREGORIAN X-WR-CALNAME:Utah Cycling X-WR-RELCALID:BF579011-36BF-49C6-8C7D-E96F03DE8055 VERSION:2.0 X-WR-TIMEZONE:US/Mountain BEGIN:VTIMEZONE TZID:US/Mountain BEGIN:DAYLIGHT TZOFFSETFROM:-0700 TZOFFSETTO:-0600 DTSTART:20070311T020000 RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=2SU TZNAME:MDT END:DAYLIGHT BEGIN:STANDARD TZOFFSETFROM:-0600 TZOFFSETTO:-0700 DTSTART:20071104T020000 RRULE:FREQ=YEARLY;BYMONTH=11;BYDAY=1SU TZNAME:MST END:STANDARD END:VTIMEZONE BEGIN:VEVENT SEQUENCE:11 TRANSP:OPAQUE UID:15208112-E0FA-4A7C-954C-CFDF19D1B0E7 DTSTART;TZID=US/Mountain:20090114T163000 DTSTAMP:20090107T024340Z SUMMARY:Wild Rose XC/Skate Training Series EXDATE;TZID=US/Mountain:20090114T163000 EXDATE;TZID=US/Mountain:20090128T163000 EXDATE;TZID=US/Mountain:20090121T163000 EXDATE;TZID=US/Mountain:20090211T163000 EXDATE;TZID=US/Mountain:20090204T163000 EXDATE;TZID=US/Mountain:20090218T163000 CREATED:20090107T024012Z DTEND;TZID=US/Mountain:20090114T180000 LOCATION:Mountain Dell Golf Course RRULE:FREQ=WEEKLY;INTERVAL=1;UNTIL=20090219T065959Z END:VEVENT END:VCALENDAR ENDCAL @event = cals.first.events.first end it "should have no occurrences" do @event.occurrences.length.should == 0 end end describe "#zulu_occurrence_range" do context "For an unbounded recurring event" do before(:each) do @event = RiCal.Event do |e| e.dtstart = "TZID=America/New_York:20090525T143500" e.dtend = "TZID=America/New_York:20090525T153500" e.add_rrule("FREQ=DAILY") end it "should return an array with the first dtstart and nil" do @event.zulu_occurrence_range.should == [DateTime.civil(2009,5,25,18,35,00, 0), nil] end end end context "For a bounded recurring event" do before(:each) do @event = RiCal.Event do |e| e.dtstart = "TZID=America/New_York:20090525T143500" e.dtend = "TZID=America/New_York:20090525T153500" e.add_rrule("FREQ=DAILY;COUNT=3") end it "should return an array with the first dtstart last dtend converted to utc" do @event.zulu_occurrence_range.should == [DateTime.civil(2009,5,25,18,35,00, 0), DateTime.civil(2009,5,27,19,35,00, 0)] end end end context "For an event with no recurrence rules" do context "with a non-floating dtstart and dtend" do before(:each) do @event = RiCal.Event do |e| e.dtstart = "TZID=America/New_York:20090525T143500" e.dtend = "TZID=America/New_York:20090525T153500" end end it "should return an array with dtstart and dtend converted to zulu time" do @event.zulu_occurrence_range.should == [DateTime.civil(2009,5,25,18,35,00, 0), DateTime.civil(2009,5,25,19,35,00, 0)] end end context "with a floating dtstart and dtend" do before(:each) do @event = RiCal.Event do |e| e.dtstart = "20090525T143500" e.dtend = "20090525T153500" end end it "should return an array with dtstart in the first timezone and dtend in the last time zone converted to zulu time" do @event.zulu_occurrence_range.should == [DateTime.civil(2009,5,25,2,35,00, 0), DateTime.civil(2009,5,26,3,35,00, 0)] end end end end end