Skip to content
Browse files

* fixed http://rick_denatale.lighthouseapp.com/projects/30941/tickets/14

    component without recurrence properties should enumerate just itself only if it is within the period between starting and before
* added the :overlapping option to OccurrenceEnumerator#occurrences - Allows the enumeration of occurrences which are either partiall or completely within a timespan given by a pair of Dates/Times/DateTimes
* Fixed some Ruby 1.9 incompatibilities
* Added some new rake tasks to run specs under multi-ruby
  • Loading branch information...
1 parent 4d3707e commit f7d0c18ad18f34d6ddde0e85336fe5b2bd0e2103 @rubyredrick committed
View
6 History.txt
@@ -1,3 +1,9 @@
+=== 0.7.0 - 29 June 2009
+ * fixed http://rick_denatale.lighthouseapp.com/projects/30941/tickets/14
+ component without recurrence properties should enumerate just itself only if it is within the period between starting and before
+ * added the :overlapping option to OccurrenceEnumerator#occurrences - Allows the enumeration of occurrences which are either partiall or completely within a timespan given by a pair of Dates/Times/DateTimes
+ * Fixed some Ruby 1.9 incompatibilities
+ * Added some new rake tasks to run specs under multi-ruby
=== 0.6.3 - 14 June 2009
* Fixed http://rick_denatale.lighthouseapp.com/projects/30941-ri_cal/tickets/13
tzinfotimezones-with-no-transitions-fail-on-export
View
2 README.txt
@@ -326,6 +326,8 @@ In the case of unbounded components, you must either use the :count, or :before
or
event.occurrences(:before => Date.today >> 1)
+
+Another option on the occurrences method is the :overlapping option, which takes an array of two Dates, Times or DateTimes which are expected to be in chronological order. Only events which occur either partially or fully within the range given by the :overlapping option will be enumerated.
Alternately, you can use the RiCal::OccurrenceEnumerator#each method,
or another Enumerable method (RiCal::OccurrenceEnumerator includes Enumerable), and terminate when you wish by breaking out of the block.
View
2 lib/ri_cal.rb
@@ -14,7 +14,7 @@ module RiCal
autoload :OccurrenceEnumerator, "#{my_dir}/ri_cal/occurrence_enumerator.rb"
# :stopdoc:
- VERSION = '0.6.3'
+ VERSION = '0.7.0'
LIBPATH = ::File.expand_path(::File.dirname(__FILE__)) + ::File::SEPARATOR
PATH = ::File.dirname(LIBPATH) + ::File::SEPARATOR
View
46 lib/ri_cal/component/calendar.rb
@@ -172,23 +172,39 @@ def initialize(stream) #:nodoc:
def string #:nodoc:
stream.string
end
-
- def valid_utf8?(string)
- string.unpack("U") rescue nil
- end
-
- def utf8_safe_split(string, n)
- if string.length <= n
- [string, nil]
- else
- before = string[0, n]
- after = string[n..-1]
- until valid_utf8?(after)
- n = n - 1
+
+ if RUBY_VERSION =~ /^1\.9/
+ def utf8_safe_split(string, n)
+ if string.bytesize <= n
+ [string, nil]
+ else
+ bytes = string.bytes.to_a
+ while (128..191).include?(bytes[n])
+ n = n - 1
+ end
+ before = bytes[0,n]
+ after = bytes[n..-1]
+ [before.pack("C*").force_encoding("utf-8"), after.empty? ? nil : after.pack("C*").force_encoding("utf-8")]
+ end
+ end
+ else
+ def valid_utf8?(string)
+ string.unpack("U") rescue nil
+ end
+
+ def utf8_safe_split(string, n)
+ if string.length <= n
+ [string, nil]
+ else
before = string[0, n]
after = string[n..-1]
- end
- [before, after.empty? ? nil : after]
+ until valid_utf8?(after)
+ n = n - 1
+ before = string[0, n]
+ after = string[n..-1]
+ end
+ [before, after.empty? ? nil : after]
+ end
end
end
View
2 lib/ri_cal/component/event.rb
@@ -29,7 +29,7 @@ def start_time
dtstart_property ? dtstart.to_datetime : nil
end
- # Return a date_time representing the time at which the event starts
+ # Return a date_time_property representing the time at which the event ends
def finish_property
if dtend_property
dtend_property
View
8 lib/ri_cal/core_extensions/date/conversions.rb
@@ -23,6 +23,14 @@ def to_ri_cal_property_value(timezone_finder = nil)
to_ri_cal_date_value(timezone_finder)
end
+ def to_overlap_range_start
+ to_datetime
+ end
+
+ def to_overlap_range_end
+ to_ri_cal_date_time_value.end_of_day.to_datetime
+ end
+
unless defined? ActiveSupport
# A method to keep Time, Date and DateTime instances interchangeable on conversions.
# In this case, it simply returns +self+.
View
5 lib/ri_cal/core_extensions/date_time/conversions.rb
@@ -22,6 +22,11 @@ def to_ri_cal_property_value(timezone_finder = nil) #:nodoc:
to_ri_cal_date_time_value(timezone_finder)
end
+ def to_overlap_range_start
+ self
+ end
+ alias_method :to_overlap_range_end, :to_overlap_range_start
+
# Return a copy of this object which will be interpreted as a floating time.
def with_floating_timezone
dup.set_tzid(:floating)
View
6 lib/ri_cal/core_extensions/time/conversions.rb
@@ -21,6 +21,12 @@ def to_ri_cal_property_value(timezone_finder = nil) #:nodoc:
to_ri_cal_date_time_value(timezone_finder)
end
+ def to_overlap_range_start
+ to_datetime
+ end
+
+ alias_method :to_overlap_range_end, :to_overlap_range_start
+
# Return a copy of this object which will be interpreted as a floating time.
def with_floating_timezone
dup.set_tzid(:floating)
View
12 lib/ri_cal/fast_date_time.rb
@@ -20,6 +20,18 @@ def initialize(year, month, day, hour, min, sec, offset_seconds)
def self.from_date_time(date_time)
new(date_time.year, date_time.month, date_time.day, date_time.hour, date_time.min, date_time.sec, (date_time.offset * SECONDS_IN_A_DAY).to_i)
end
+
+ def self.from_time(time)
+ new(time.year, time.month, time.day, time.hour, time.min, time.sec, (time.utc_offset.offset * SECONDS_IN_A_DAY))
+ end
+
+ def self.from_date(date)
+ new(date.year, date.month, date.day, 0, 0, 0, 0)
+ end
+
+ def self.from_date_at_end_of_day(date)
+ new(date.year, date.month, date.day, 23, 59, 59, 0)
+ end
alias_method :utc_offset_seconds, :offset
View
36 lib/ri_cal/occurrence_enumerator.rb
@@ -96,7 +96,8 @@ def exclusion_match?(occurrence, exclusion)
# Also exclude occurrences before the :starting date_time
def before_start?(occurrence)
- (@start && occurrence.dtstart.to_datetime < @start)
+ (@start && occurrence.dtstart.to_datetime < @start) ||
+ @overlap_range && occurrence.before_range?(@overlap_range)
end
def next_occurrence
@@ -118,7 +119,9 @@ def next_occurrence
def options_stop(occurrence)
occurrence != :excluded &&
- (@cutoff && occurrence.dtstart.to_datetime >= @cutoff) || (@count && @yielded >= @count)
+ (@cutoff && occurrence.dtstart.to_datetime >= @cutoff) ||
+ (@count && @yielded >= @count) ||
+ (@overlap_range && occurrence.after_range?(@overlap_range))
end
@@ -133,26 +136,34 @@ def each(options = nil)
else
occurrence = next_occurrence
while (occurrence)
- if options_stop(occurrence)
+ candidate = @component.recurrence(occurrence)
+ if options_stop(candidate)
occurrence = nil
else
- unless before_start?(occurrence)
+ unless before_start?(candidate)
@yielded += 1
- yield @component.recurrence(occurrence)
+ yield candidate
end
occurrence = next_occurrence
end
end
end
end
-
+
def bounded?
- @rrules.bounded? || @count || @cutoff
+ @rrules.bounded? || @count || @cutoff || @overlap_range
+ end
+
+ def process_overlap_range(overlap_range)
+ if overlap_range
+ @overlap_range = [overlap_range.first.to_overlap_range_start, overlap_range.last.to_overlap_range_end]
+ end
end
def process_options(options)
@start = options[:starting] && options[:starting].to_datetime
@cutoff = options[:before] && options[:before].to_datetime
+ @overlap_range = process_overlap_range(options[:overlapping])
@count = options[:count]
end
@@ -177,6 +188,7 @@ def to_a(options = {})
# * :starting:: a Date, Time, or DateTime, no occurrences starting before this argument will be returned
# * :before:: a Date, Time, or DateTime, no occurrences starting on or after this argument will be returned.
# * :count:: an integer which limits the number of occurrences returned.
+ # * :overlapping:: a two element array of Dates, Times, or DateTimes, assumed to be in chronological order. Only occurrences which are either totally or partially within the range will be returned.
def occurrences(options={})
enumeration_instance.to_a(options)
end
@@ -185,7 +197,17 @@ def occurrences(options={})
def enumeration_instance #:nodoc:
EnumerationInstance.new(self)
end
+
+ def before_range?(overlap_range)
+ finish = finish_time
+ !finish_time || finish_time < overlap_range.first
+ end
+ def after_range?(overlap_range)
+ start = start_time
+ !start || start > overlap_range.last
+ end
+
# execute the block for each occurrence
def each(&block) # :yields: Component
enumeration_instance.each(&block)
View
2 lib/ri_cal/parser.rb
@@ -23,7 +23,7 @@ def self.parse_params(string) #:nodoc:
if string
string.split(";").inject({}) { |result, val|
m = /^(.+)=(.+)$/.match(val)
- invalid unless m
+ raise "Invalid parameter value #{val.inspect}" unless m
result[m[1]] = m[2]
result
}
View
4 ri_cal.gemspec
@@ -2,11 +2,11 @@
Gem::Specification.new do |s|
s.name = %q{ri_cal}
- s.version = "0.6.3"
+ s.version = "0.7.0"
s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
s.authors = ["author=Rick DeNatale"]
- s.date = %q{2009-06-14}
+ s.date = %q{2009-06-29}
s.default_executable = %q{ri_cal}
s.description = %q{A new Ruby implementation of RFC2445 iCalendar.
View
1 spec/ri_cal/component/calendar_spec.rb
@@ -1,3 +1,4 @@
+# encoding: utf-8
#- ©2009 Rick DeNatale, All rights reserved. Refer to the file README.txt for the license
require File.join(File.dirname(__FILE__), %w[.. .. spec_helper])
View
113 spec/ri_cal/component/event_spec.rb
@@ -1,9 +1,94 @@
+# encoding: utf-8
#- ©2009 Rick DeNatale, All rights reserved. Refer to the file README.txt for the license
require File.join(File.dirname(__FILE__), %w[.. .. spec_helper])
describe RiCal::Component::Event do
+ context ".finish_time" do
+ it "should be the end of the start day for an event with a date dtstart and no dtend or duration" do
+ @it = RiCal.Event do |evt|
+ evt.dtstart = "20090704"
+ end
+ @it.finish_time.should == DateTime.parse("20090704T235959")
+ end
+
+ it "should be the end of the end day for an event with a date dtstart and a dtend" do
+ @it = RiCal.Event do |evt|
+ evt.dtstart = "20090704"
+ evt.dtend = "20090706"
+ end
+ @it.finish_time.should == DateTime.parse("20090706T235959")
+ end
+
+ it "should be the start time for an event with a datetime dtstart and no dtend or duration" do
+ @it = RiCal.Event do |evt|
+ evt.dtstart = "20090704T013000Z"
+ end
+ @it.finish_time.should == DateTime.parse("20090704T013000Z")
+ end
+
+ it "should be the end time for an event with a datetime dtend" do
+ @it = RiCal.Event do |evt|
+ evt.dtstart = "20090704"
+ evt.dtend = "20090706T120000"
+ end
+ @it.finish_time.should == DateTime.parse("20090706T120000")
+ end
+
+ it "should be the end time for an event with a datetime dtstart and a duration" do
+ @it = RiCal.Event do |evt|
+ evt.dtstart = "20090704T120000Z"
+ evt.duration = "P1H30M"
+ end
+ @it.finish_time.should == DateTime.parse("20090704T133000Z")
+ end
+
+ end
+
+ context ".before_range?" do
+ context "with a Date dtstart and no dtend" do
+ before(:each) do
+ @it = RiCal.Event do |evt|
+ evt.dtstart = "20090704"
+ end
+ end
+
+ it "should be false if the range start is a date before the start date" do
+ @it.before_range?([Date.parse("20090703"), :anything]).should_not be
+ end
+
+ it "should be false if the range start is the start date" do
+ @it.before_range?([Date.parse("20090704"), :anything]).should_not be
+ end
+
+ it "should be true if the range start is a date after the start date" do
+ @it.before_range?([Date.parse("20090705"), :anything]).should be
+ end
+ end
+
+ context "with a Date dtstart and date dtend" do
+ before(:each) do
+ @it = RiCal.Event do |evt|
+ evt.dtstart = "20090704"
+ evt.dtend = "20090706"
+ end
+ end
+
+ it "should be false if the range start is a date before the end date" do
+ @it.before_range?([Date.parse("20090705"), :anything]).should_not be
+ end
+
+ it "should be false if the range start is the end date" do
+ @it.before_range?([Date.parse("20090706"), :anything]).should_not be
+ end
+
+ it "should be true if the range start is a date after the end date" do
+ @it.before_range?([Date.parse("20090707"), :anything]).should be
+ end
+ end
+ end
+
context "bug report from Noboyuki Tomizawa" do
before(:each) do
@@ -512,7 +597,7 @@ def unfold(string)
end
it "should have the timezone in the ical representation of the exdate property" do
- @event.exdate_property.to_s.should match(%r{;TZID=America/New_York[:;]})
+ @event.exdate_property.first.to_s.should match(%r{;TZID=America/New_York[:;]})
end
end
@@ -604,23 +689,23 @@ def unfold(string)
end
end
- context "An event with a floating start" do
+ context "An event with a floating start" do
- before(:each) do
- cal = RiCal.Calendar do |ical|
- ical.event do |ievent|
- ievent.dtstart "20090530T120000"
- end
+ before(:each) do
+ cal = RiCal.Calendar do |ical|
+ ical.event do |ievent|
+ ievent.dtstart "20090530T120000"
end
- @event = cal.events.first
end
+ @event = cal.events.first
+ end
- it "should produce a DateTime for dtstart" do
- @event.dtstart.should be_instance_of(DateTime)
- end
+ it "should produce a DateTime for dtstart" do
+ @event.dtstart.should be_instance_of(DateTime)
+ end
- it "should have a floating dtstart" do
- @event.dtstart.should have_floating_timezone
- end
+ it "should have a floating dtstart" do
+ @event.dtstart.should have_floating_timezone
end
+ end
end
View
49 spec/ri_cal/occurrence_enumerator_spec.rb
@@ -1,3 +1,4 @@
+# encoding: utf-8
#- ©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])
@@ -12,6 +13,7 @@ def mock_enumerator(name, next_occurrence)
Fr13Unbounded_Zulu = <<-TEXT
BEGIN:VEVENT
DTSTART:19970902T090000Z
+DTEND: 19970902T100000Z
EXDATE:19970902T090000Z
RRULE:FREQ=MONTHLY;BYDAY=FR;BYMONTHDAY=13
END:VEVENT
@@ -20,6 +22,7 @@ def mock_enumerator(name, next_occurrence)
Fr13Unbounded_Eastern = <<-TEXT
BEGIN:VEVENT
DTSTART;TZID=US-Eastern:19970902T090000
+DTEND;TZID=US-Eastern:19970902T100000
EXDATE;TZID=US-Eastern:19970902T090000
RRULE:FREQ=MONTHLY;BYDAY=FR;BYMONTHDAY=13
END:VEVENT
@@ -30,10 +33,11 @@ def mock_enumerator(name, next_occurrence)
"19980313T090000Z",
"19981113T090000Z",
"19990813T090000Z",
- "20001013T090000Z"
+ "20001013T090000Z"
].map {|start| src = <<-TEXT
BEGIN:VEVENT
DTSTART:#{start}
+DTEND:#{start.gsub("T09","T10")}
RECURRENCE-ID:#{start}
END:VEVENT
TEXT
@@ -70,25 +74,34 @@ def mock_enumerator(name, next_occurrence)
result.map{|o|o.dtstart}.should == Fr13UnboundedZuluExpectedFive[0..2].map{|e| e.dtstart}
end
end
- end
- end
-
- describe "for a non-recurring event" do
- before(:each) do
- @event_start = Time.now.utc
- @event = RiCal.Event do |event|
- event.dtstart = @event_start
- event.dtend = @event_start + 3600
- # event.rrule (no recurrence, single event)
+
+ describe "with :overlapping specified" do
+ it "should include occurrences which overlap" do
+ result = @it.occurrences(:overlapping =>
+ [DateTime.parse("19981113T093000Z"), # occurrence[2].dtstart + 1/2 hour
+ DateTime.parse("20001013T083000Z")]) # occurrence[4].dtstart - 1/2 hour
+ result.map{|o|o.dtstart}.should == Fr13UnboundedZuluExpectedFive[2..3].map{|e| e.dtstart}
+ end
end
end
-
- it "should enumerate no occurrences if dtstart is before :starting" do
- @event.occurrences(:starting => @event_start + 1).should be_empty
- end
-
- it "should enumerate no occurrences if dtstart is after :before" do
- @event.occurrences(:before => @event_start - 1).should be_empty
+
+ describe "for a non-recurring event" do
+ before(:each) do
+ @event_start = Time.now.utc
+ @event = RiCal.Event do |event|
+ event.dtstart = @event_start
+ event.dtend = @event_start + 3600
+ # event.rrule (no recurrence, single event)
+ end
+ end
+
+ it "should enumerate no occurrences if dtstart is before :starting" do
+ @event.occurrences(:starting => @event_start + 1).should be_empty
+ end
+
+ it "should enumerate no occurrences if dtstart is after :before" do
+ @event.occurrences(:before => @event_start - 1).should be_empty
+ end
end
end
View
25 tasks/spec.rake
@@ -35,6 +35,31 @@ namespace :spec do
t.spec_files = FileList['spec/**/*_spec.rb']
t.ruby_opts << "-r #{File.join(File.dirname(__FILE__), *%w[gem_loader load_tzinfo_gem])}"
end
+ multiruby_path = `which multiruby`.chomp
+ if multiruby_path.length > 0 && Spec::Rake::SpecTask.instance_methods.include?("ruby_cmd")
+ namespace :multi do
+ desc "Run all specs with multiruby and ActiveSupport"
+ Spec::Rake::SpecTask.new(:with_active_support) do |t|
+ t.spec_opts = ['--options', "spec/spec.opts"]
+ t.spec_files = FileList['spec/**/*_spec.rb']
+ t.ruby_cmd = "#{multiruby_path}"
+ t.verbose = true
+ t.ruby_opts << "-r #{File.join(File.dirname(__FILE__), *%w[gem_loader load_active_support])}"
+ end
+
+ desc "Run all specs multiruby and the tzinfo gem"
+ Spec::Rake::SpecTask.new(:with_tzinfo_gem) do |t|
+ t.spec_opts = ['--options', "spec/spec.opts"]
+ t.spec_files = FileList['spec/**/*_spec.rb']
+ t.ruby_cmd = "#{multiruby_path}"
+ t.verbose = true
+ t.ruby_opts << "-r #{File.join(File.dirname(__FILE__), *%w[gem_loader load_tzinfo_gem])}"
+ end
+ end
+
+ desc "run all specs under multiruby with ActiveSupport and also with the tzinfo gem"
+ task :multi => [:"spec:multi:with_active_support", :"spec:multi:with_tzinfo_gem"]
+ end
end
if RUBY_VERSION.match(/^1\.8\./)

0 comments on commit f7d0c18

Please sign in to comment.
Something went wrong with that request. Please try again.