diff --git a/.gitignore b/.gitignore index d3133a1a..225bd11a 100644 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,5 @@ coverage coverage.data *.gem .bundle +*.idea +/nbproject diff --git a/.travis.yml b/.travis.yml index c488383c..7d3b9695 100644 --- a/.travis.yml +++ b/.travis.yml @@ -5,7 +5,7 @@ notifications: branches: only: - master - - v0.7 + - issues/50-from_ical rvm: - 1.9.3 - 2.0.0 diff --git a/README.md b/README.md index d256ac1a..ad6cc801 100644 --- a/README.md +++ b/README.md @@ -90,7 +90,7 @@ schedule.add_recurrence_rule Rule.daily schedule.occurring_at?(now + 1800) # true schedule.occurring_between?(t1, t2) -# using end_time also sets the duration +# using end_time also sets the duration schedule = Schedule.new(start = Time.now, :end_time => start + 3600) schedule.add_recurrence_rule Rule.daily schedule.occurring_at?(start + 3599) # true diff --git a/ice_cube-0.7.4.gem b/ice_cube-0.7.4.gem new file mode 100644 index 00000000..e6efcb68 Binary files /dev/null and b/ice_cube-0.7.4.gem differ diff --git a/ice_cube-0.7.5.gem b/ice_cube-0.7.5.gem new file mode 100644 index 00000000..a2dd0e86 Binary files /dev/null and b/ice_cube-0.7.5.gem differ diff --git a/ice_cube-0.7.71.gem b/ice_cube-0.7.71.gem new file mode 100644 index 00000000..e8f08315 Binary files /dev/null and b/ice_cube-0.7.71.gem differ diff --git a/ice_cube.gemspec b/ice_cube.gemspec index 9854163d..ec7c1e26 100644 --- a/ice_cube.gemspec +++ b/ice_cube.gemspec @@ -18,7 +18,7 @@ Gem::Specification.new do |s| s.rubyforge_project = "ice-cube" s.add_development_dependency('rake') - s.add_development_dependency('rspec', '~> 2.12.0') - s.add_development_dependency('activesupport', '>= 3.0.0') - s.add_development_dependency('tzinfo') + s.add_development_dependency('rspec') + s.add_development_dependency('activesupport', '~> 4.0.0') + s.add_development_dependency('tzinfo', '~> 0.3') end diff --git a/lib/ice_cube.rb b/lib/ice_cube.rb index 1bbae701..b9706ef0 100644 --- a/lib/ice_cube.rb +++ b/lib/ice_cube.rb @@ -18,9 +18,11 @@ module IceCube autoload :HashParser, 'ice_cube/parsers/hash_parser' autoload :YamlParser, 'ice_cube/parsers/yaml_parser' + autoload :IcalParser, 'ice_cube/parsers/ical_parser' autoload :CountExceeded, 'ice_cube/errors/count_exceeded' autoload :UntilExceeded, 'ice_cube/errors/until_exceeded' + autoload :ZeroInterval, 'ice_cube/errors/zero_interval' autoload :ValidatedRule, 'ice_cube/validated_rule' autoload :SingleOccurrenceRule, 'ice_cube/single_occurrence_rule' diff --git a/lib/ice_cube/errors/zero_interval.rb b/lib/ice_cube/errors/zero_interval.rb new file mode 100644 index 00000000..8e5f306f --- /dev/null +++ b/lib/ice_cube/errors/zero_interval.rb @@ -0,0 +1,7 @@ +module IceCube + + # An exception for when interval is set to zero + class ZeroInterval < Exception + end + +end diff --git a/lib/ice_cube/parsers/hash_parser.rb b/lib/ice_cube/parsers/hash_parser.rb index 772362d9..6660323f 100644 --- a/lib/ice_cube/parsers/hash_parser.rb +++ b/lib/ice_cube/parsers/hash_parser.rb @@ -53,7 +53,9 @@ def apply_end_time(schedule, data) def apply_rrules(schedule, data) return unless data[:rrules] data[:rrules].each do |h| - schedule.rrule(IceCube::Rule.from_hash(h)) + rrule = h.is_a?(IceCube::Rule) ? h : IceCube::Rule.from_hash(h) + + schedule.rrule(rrule) end end @@ -61,7 +63,9 @@ def apply_exrules(schedule, data) return unless data[:exrules] warn "IceCube: :exrules deprecated. (This will be going away)" data[:exrules].each do |h| - schedule.exrule(IceCube::Rule.from_hash(h)) + rrule = h.is_a?(IceCube::Rule) ? h : IceCube::Rule.from_hash(h) + + schedule.exrule(rrule) end end diff --git a/lib/ice_cube/parsers/ical_parser.rb b/lib/ice_cube/parsers/ical_parser.rb new file mode 100644 index 00000000..525356ba --- /dev/null +++ b/lib/ice_cube/parsers/ical_parser.rb @@ -0,0 +1,89 @@ +module IceCube + class IcalParser + def self.schedule_from_ical(ical_string, options = {}) + data = {} + ical_string.each_line do |line| + (property, value) = line.split(':') + (property, tzid) = property.split(';') + case property + when 'DTSTART' + data[:start_time] = Time.parse(value) + when 'DTEND' + data[:end_time] = Time.parse(value) + when 'EXDATE' + data[:extimes] ||= [] + data[:extimes] += value.split(',').map{|v| Time.parse(v)} + when 'DURATION' + data[:duration] # FIXME + when 'RRULE' + data[:rrules] = [rule_from_ical(value)] + end + end + Schedule.from_hash data + end + + def self.rule_from_ical(ical) + params = { validations: { } } + + ical.split(';').each do |rule| + (name, value) = rule.split('=') + value.strip! + case name + when 'FREQ' + params[:freq] = value.downcase + when 'INTERVAL' + params[:interval] = value.to_i + when 'COUNT' + params[:count] = value.to_i + when 'UNTIL' + params[:until] = DateTime.parse(value).to_time.utc + when 'WKST' + params[:wkst] = TimeUtil.ical_day_to_symbol(value) + when 'BYSECOND' + params[:validations][:second_of_minute] = value.split(',').collect{ |v| v.to_i } + when "BYMINUTE" + params[:validations][:minute_of_hour] = value.split(',').collect{ |v| v.to_i } + when "BYHOUR" + params[:validations][:hour_of_day] = value.split(',').collect{ |v| v.to_i } + when "BYDAY" + dows = {} + days = [] + value.split(',').each do |expr| + day = TimeUtil.ical_day_to_symbol(expr.strip[-2..-1]) + if expr.strip.length > 2 # day with occurence + occ = expr[0..-3].to_i + dows[day].nil? ? dows[day] = [occ] : dows[day].push(occ) + days.delete(TimeUtil.sym_to_wday(day)) + else + days.push TimeUtil.sym_to_wday(day) if dows[day].nil? + end + end + params[:validations][:day_of_week] = dows unless dows.empty? + params[:validations][:day] = days unless days.empty? + when "BYMONTHDAY" + params[:validations][:day_of_month] = value.split(',').collect{ |v| v.to_i } + when "BYMONTH" + params[:validations][:month_of_year] = value.split(',').collect{ |v| v.to_i } + when "BYYEARDAY" + params[:validations][:day_of_year] = value.split(',').collect{ |v| v.to_i } + when "BYSETPOS" + else + raise "Invalid or unsupported rrule command : #{name}" + end + end + + params[:interval] ||= 1 + # WKST only valid for weekly rules + params.delete(:wkst) unless params[:freq] == 'weekly' + + rule = Rule.send(*params.values_at(:freq, :interval, :wkst).compact) + rule.count(params[:count]) if params[:count] + rule.until(params[:until]) if params[:until] + params[:validations].each do |key, value| + value.is_a?(Array) ? rule.send(key, *value) : rule.send(key, value) + end + + rule + end + end +end diff --git a/lib/ice_cube/schedule.rb b/lib/ice_cube/schedule.rb index 82f90827..0fb4d18a 100644 --- a/lib/ice_cube/schedule.rb +++ b/lib/ice_cube/schedule.rb @@ -431,6 +431,8 @@ def next_time(time, closing_time) [min_time, new_time].compact.min rescue StopIteration min_time + rescue CountExceeded, UntilExceeded, ZeroInterval + next end end break nil unless min_time diff --git a/lib/ice_cube/time_util.rb b/lib/ice_cube/time_util.rb index f64bc3af..f19df7da 100644 --- a/lib/ice_cube/time_util.rb +++ b/lib/ice_cube/time_util.rb @@ -10,6 +10,11 @@ module TimeUtil :thursday => 4, :friday => 5, :saturday => 6 } + ICAL_DAYS = { + 'SU' => :sunday, 'MO' => :monday, 'TU' => :tuesday, 'WE' => :wednesday, + 'TH' => :thursday, 'FR' => :friday, 'SA' => :saturday + } + MONTHS = { :january => 1, :february => 2, :march => 3, :april => 4, :may => 5, :june => 6, :july => 7, :august => 8, :september => 9, :october => 10, @@ -142,12 +147,25 @@ def self.wday_to_sym(wday) end end + # Convert a symbol to an ical day (SU, MO) + def self.week_start(sym) + raise ArgumentError, "Invalid day: #{str}" unless DAYS.keys.include?(sym) + day = sym.to_s.upcase[0..1] + day + end + # Convert weekday from base sunday to the schedule's week start. def self.normalize_wday(wday, week_start) (wday - sym_to_wday(week_start)) % 7 end deprecated_alias :normalize_weekday, :normalize_wday + def self.ical_day_to_symbol(str) + day = ICAL_DAYS[str] + raise ArgumentError, "Invalid day: #{str}" if day.nil? + day + end + # Return the count of the number of times wday appears in the month, # and which of those time falls on def self.which_occurrence_in_month(time, wday) diff --git a/lib/ice_cube/validations/daily_interval.rb b/lib/ice_cube/validations/daily_interval.rb index 1b9fdc95..64d22fb9 100644 --- a/lib/ice_cube/validations/daily_interval.rb +++ b/lib/ice_cube/validations/daily_interval.rb @@ -26,14 +26,6 @@ def dst_adjust? true end - def validate(step_time, schedule) - t0, t1 = schedule.start_time, step_time - days = Date.new(t1.year, t1.month, t1.day) - - Date.new(t0.year, t0.month, t0.day) - offset = (days % interval).nonzero? - interval - offset if offset - end - def build_s(builder) builder.base = interval == 1 ? 'Daily' : "Every #{interval} days" end @@ -47,6 +39,16 @@ def build_ical(builder) builder['INTERVAL'] << interval unless interval == 1 end + def validate(time, schedule) + raise ZeroInterval if interval == 0 + time_date = Date.new(time.year, time.month, time.day) + start_date = Date.new(schedule.start_time.year, schedule.start_time.month, schedule.start_time.day) + days = time_date - start_date + unless days % interval === 0 + interval - (days % interval) + end + end + end end diff --git a/lib/ice_cube/validations/hourly_interval.rb b/lib/ice_cube/validations/hourly_interval.rb index 703d3fc6..ca52d4a3 100644 --- a/lib/ice_cube/validations/hourly_interval.rb +++ b/lib/ice_cube/validations/hourly_interval.rb @@ -25,15 +25,6 @@ def dst_adjust? false end - def validate(step_time, schedule) - t0, t1 = schedule.start_time.to_i, step_time.to_i - sec = (t1 - t1 % ONE_HOUR) - - (t0 - t0 % ONE_HOUR) - hours = sec / ONE_HOUR - offset = (hours % interval).nonzero? - interval - offset if offset - end - def build_s(builder) builder.base = interval == 1 ? 'Hourly' : "Every #{interval} hours" end @@ -47,6 +38,17 @@ def build_ical(builder) builder['INTERVAL'] << interval unless interval == 1 end + def validate(time, schedule) + raise ZeroInterval if interval == 0 + start_time = schedule.start_time + sec = (time.to_i - time.to_i % ONE_HOUR) - + (start_time.to_i - start_time.to_i % ONE_HOUR) + hours = sec / ONE_HOUR + unless hours % interval == 0 + interval - (hours % interval) + end + end + end end diff --git a/lib/ice_cube/validations/minutely_interval.rb b/lib/ice_cube/validations/minutely_interval.rb index 7f0b484f..f6ca4c94 100644 --- a/lib/ice_cube/validations/minutely_interval.rb +++ b/lib/ice_cube/validations/minutely_interval.rb @@ -25,15 +25,6 @@ def dst_adjust? false end - def validate(step_time, schedule) - t0, t1 = schedule.start_time.to_i, step_time.to_i - sec = (t1 - t1 % ONE_MINUTE) - - (t0 - t0 % ONE_MINUTE) - minutes = sec / ONE_MINUTE - offset = (minutes % interval).nonzero? - interval - offset if offset - end - def build_s(builder) builder.base = interval == 1 ? 'Minutely' : "Every #{interval} minutes" end @@ -47,6 +38,17 @@ def build_ical(builder) builder['INTERVAL'] << interval unless interval == 1 end + def validate(time, schedule) + raise ZeroInterval if interval == 0 + start_time = schedule.start_time + sec = (time.to_i - time.to_i % ONE_MINUTE) - + (start_time.to_i - start_time.to_i % ONE_MINUTE) + minutes = sec / ONE_MINUTE + unless minutes % interval == 0 + interval - (minutes % interval) + end + end + end end diff --git a/lib/ice_cube/validations/monthly_interval.rb b/lib/ice_cube/validations/monthly_interval.rb index 5214da0e..eab91117 100644 --- a/lib/ice_cube/validations/monthly_interval.rb +++ b/lib/ice_cube/validations/monthly_interval.rb @@ -25,14 +25,6 @@ def dst_adjust? true end - def validate(step_time, schedule) - t0, t1 = schedule.start_time, step_time - months = (t1.month - t0.month) + - (t1.year - t0.year) * 12 - offset = (months % interval).nonzero? - interval - offset if offset - end - def build_s(builder) builder.base = interval == 1 ? 'Monthly' : "Every #{interval} months" end @@ -46,6 +38,15 @@ def build_ical(builder) builder['INTERVAL'] << interval unless interval == 1 end + def validate(time, schedule) + raise ZeroInterval if interval == 0 + start_time = schedule.start_time + months_to_start = (time.month - start_time.month) + (time.year - start_time.year) * 12 + unless months_to_start % interval == 0 + interval - (months_to_start % interval) + end + end + end end diff --git a/lib/ice_cube/validations/secondly_interval.rb b/lib/ice_cube/validations/secondly_interval.rb index a60a141a..77fd272d 100644 --- a/lib/ice_cube/validations/secondly_interval.rb +++ b/lib/ice_cube/validations/secondly_interval.rb @@ -25,12 +25,6 @@ def dst_adjust? false end - def validate(step_time, schedule) - seconds = step_time.to_i - schedule.start_time.to_i - offset = (seconds % interval).nonzero? - interval - offset if offset - end - def build_s(builder) builder.base = interval == 1 ? 'Secondly' : "Every #{interval} seconds" end @@ -44,6 +38,14 @@ def build_ical(builder) builder['INTERVAL'] << interval unless interval == 1 end + def validate(time, schedule) + raise ZeroInterval if interval == 0 + seconds = time.to_i - schedule.start_time.to_i + unless seconds % interval == 0 + interval - (seconds % interval) + end + end + end end diff --git a/lib/ice_cube/validations/weekly_interval.rb b/lib/ice_cube/validations/weekly_interval.rb index 0d78de17..c5787be8 100644 --- a/lib/ice_cube/validations/weekly_interval.rb +++ b/lib/ice_cube/validations/weekly_interval.rb @@ -33,16 +33,6 @@ def dst_adjust? true end - def validate(step_time, schedule) - t0, t1 = schedule.start_time, step_time - d0 = Date.new(t0.year, t0.month, t0.day) - d1 = Date.new(t1.year, t1.month, t1.day) - days = (d1 - TimeUtil.normalize_wday(d1.wday, week_start)) - - (d0 - TimeUtil.normalize_wday(d0.wday, week_start)) - offset = ((days / 7) % interval).nonzero? - (interval - offset) * 7 if offset - end - def build_s(builder) builder.base = interval == 1 ? 'Weekly' : "Every #{interval} weeks" end @@ -60,6 +50,17 @@ def build_ical(builder) end end + def validate(step_time, schedule) + raise ZeroInterval if interval == 0 + t0, t1 = schedule.start_time, step_time + d0 = Date.new(t0.year, t0.month, t0.day) + d1 = Date.new(t1.year, t1.month, t1.day) + days = (d1 - TimeUtil.normalize_wday(d1.wday, week_start)) - + (d0 - TimeUtil.normalize_wday(d0.wday, week_start)) + offset = ((days / 7) % interval).nonzero? + (interval - offset) * 7 if offset + end + end end diff --git a/lib/ice_cube/validations/yearly_interval.rb b/lib/ice_cube/validations/yearly_interval.rb index 79b8ca22..c01394f7 100644 --- a/lib/ice_cube/validations/yearly_interval.rb +++ b/lib/ice_cube/validations/yearly_interval.rb @@ -24,12 +24,6 @@ def dst_adjust? true end - def validate(step_time, schedule) - years = step_time.year - schedule.start_time.year - offset = (years % interval).nonzero? - interval - offset if offset - end - def build_s(builder) builder.base = interval == 1 ? 'Yearly' : "Every #{interval} years" end @@ -45,6 +39,14 @@ def build_ical(builder) end end + def validate(time, schedule) + raise ZeroInterval if interval == 0 + years_to_start = time.year - schedule.start_time.year + unless years_to_start % interval == 0 + interval - (years_to_start % interval) + end + end + end end diff --git a/lib/ice_cube/version.rb b/lib/ice_cube/version.rb index 8f5ecc8d..d9d6feae 100644 --- a/lib/ice_cube/version.rb +++ b/lib/ice_cube/version.rb @@ -1,5 +1,3 @@ module IceCube - VERSION = '0.12.0' - end diff --git a/spec/examples/daily_rule_spec.rb b/spec/examples/daily_rule_spec.rb index 56c54bfb..d4ed5e44 100644 --- a/spec/examples/daily_rule_spec.rb +++ b/spec/examples/daily_rule_spec.rb @@ -1,86 +1,110 @@ require File.dirname(__FILE__) + '/../spec_helper' -module IceCube - describe DailyRule, 'occurs_on?' do +describe IceCube::DailyRule, 'occurs_on?' do - context :system_time_zone => 'America/Vancouver' do + it 'should not produce results for @interval = 0' do + start_date = DAY + schedule = IceCube::Schedule.new(start_date) + schedule.add_recurrence_rule IceCube::Rule.daily(0) + #check assumption + dates = schedule.occurrences(start_date + 2 * IceCube::ONE_DAY) + dates.size.should == 0 + dates.should == [] + end - it 'should include nearest time in DST start hour' do - schedule = Schedule.new(t0 = Time.local(2013, 3, 9, 2, 30, 0)) - schedule.add_recurrence_rule Rule.daily - schedule.first(3).should == [ - Time.local(2013, 3, 9, 2, 30, 0), # -0800 - Time.local(2013, 3, 10, 3, 30, 0), # -0700 - Time.local(2013, 3, 11, 2, 30, 0) # -0700 - ] - end + it 'should produce the correct days for @interval = 1' do + start_date = DAY + schedule = IceCube::Schedule.new(start_date) + schedule.add_recurrence_rule IceCube::Rule.daily + #check assumption + dates = schedule.occurrences(start_date + 2 * IceCube::ONE_DAY) + dates.size.should == 3 + dates.should == [DAY, DAY + 1 * IceCube::ONE_DAY, DAY + 2 * IceCube::ONE_DAY] + end - it 'should not skip times in DST end hour' do - schedule = Schedule.new(t0 = Time.local(2013, 11, 2, 2, 30, 0)) - schedule.add_recurrence_rule Rule.daily - schedule.first(3).should == [ - Time.local(2013, 11, 2, 2, 30, 0), # -0700 - Time.local(2013, 11, 3, 2, 30, 0), # -0800 - Time.local(2013, 11, 4, 2, 30, 0) # -0800 - ] - end + context system_time_zone: 'America/Vancouver' do - it 'should include nearest time to DST start when locking hour_of_day' do - schedule = Schedule.new(t0 = Time.local(2013, 3, 9, 2, 0, 0)) - schedule.add_recurrence_rule Rule.daily.hour_of_day(2) - schedule.first(3).should == [ - Time.local(2013, 3, 9, 2, 0, 0), # -0800 - Time.local(2013, 3, 10, 3, 0, 0), # -0700 - Time.local(2013, 3, 11, 2, 0, 0) # -0700 - ] - end + it 'should include nearest time in DST start hour' do + schedule = IceCube::Schedule.new(t0 = Time.local(2013, 3, 9, 2, 30, 0)) + schedule.add_recurrence_rule IceCube::Rule.daily + schedule.first(3).should == [ + Time.local(2013, 3, 9, 2, 30, 0), # -0800 + Time.local(2013, 3, 10, 3, 30, 0), # -0700 + Time.local(2013, 3, 11, 2, 30, 0) # -0700 + ] + end + it 'should not skip times in DST end hour' do + schedule = IceCube::Schedule.new(t0 = Time.local(2013, 11, 2, 2, 30, 0)) + schedule.add_recurrence_rule IceCube::Rule.daily + schedule.first(3).should == [ + Time.local(2013, 11, 2, 2, 30, 0), # -0700 + Time.local(2013, 11, 3, 2, 30, 0), # -0800 + Time.local(2013, 11, 4, 2, 30, 0) # -0800 + ] end it 'should update previous interval' do schedule = double(start_time: t0 = Time.now) - rule = Rule.daily(7) + rule = IceCube::Rule.daily(7) rule.interval(5) - rule.next_time(t0 + 1, schedule, nil).should == t0 + 5 * ONE_DAY + rule.next_time(t0 + 1, schedule, nil).should == t0 + 5 * IceCube::ONE_DAY end - it 'should produce the correct days for @interval = 1' do - schedule = Schedule.new(t0 = Time.now) - schedule.add_recurrence_rule Rule.daily - #check assumption - times = schedule.occurrences(t0 + 2 * ONE_DAY) - times.size.should == 3 - times.should == [t0, t0 + ONE_DAY, t0 + 2 * ONE_DAY] + it 'should include nearest time to DST start when locking hour_of_day' do + schedule = IceCube::Schedule.new(t0 = Time.local(2013, 3, 9, 2, 0, 0)) + schedule.add_recurrence_rule IceCube::Rule.daily.hour_of_day(2) + schedule.first(3).should == [ + Time.local(2013, 3, 9, 2, 0, 0), # -0800 + Time.local(2013, 3, 10, 3, 0, 0), # -0700 + Time.local(2013, 3, 11, 2, 0, 0) # -0700 + ] end - it 'should produce the correct days for @interval = 2' do - schedule = Schedule.new(t0 = Time.now) - schedule.add_recurrence_rule Rule.daily(2) - #check assumption (3) -- (1) 2 (3) 4 (5) 6 - times = schedule.occurrences(t0 + 5 * ONE_DAY) - times.size.should == 3 - times.should == [t0, t0 + 2 * ONE_DAY, t0 + 4 * ONE_DAY] - end + end - it 'should produce the correct days for @interval = 2 when crossing into a new year' do - schedule = Schedule.new(t0 = Time.utc(2011, 12, 29)) - schedule.add_recurrence_rule Rule.daily(2) - #check assumption (3) -- (1) 2 (3) 4 (5) 6 - times = schedule.occurrences(t0 + 5 * ONE_DAY) - times.size.should == 3 - times.should == [t0, t0 + 2 * ONE_DAY, t0 + 4 * ONE_DAY] - end + it 'should update previous interval' do + schedule = double(start_time: t0 = Time.now) + rule = IceCube::Rule.daily(7) + rule.interval(5) + rule.next_time(t0 + 1, schedule, nil).should == t0 + 5 * IceCube::ONE_DAY + end - it 'should produce the correct days for interval of 4 day with hour and minute of day set' do - schedule = Schedule.new(t0 = Time.local(2010, 3, 1)) - schedule.add_recurrence_rule Rule.daily(4).hour_of_day(5).minute_of_hour(45) - #check assumption 2 -- 1 (2) (3) (4) 5 (6) - times = schedule.occurrences(t0 + 5 * ONE_DAY) - times.should == [ - t0 + 5 * ONE_HOUR + 45 * ONE_MINUTE, - t0 + 4 * ONE_DAY + 5 * ONE_HOUR + 45 * ONE_MINUTE - ] - end + it 'should produce the correct days for @interval = 1' do + schedule = IceCube::Schedule.new(t0 = Time.now) + schedule.add_recurrence_rule IceCube::Rule.daily + #check assumption + times = schedule.occurrences(t0 + 2 * IceCube::ONE_DAY) + times.size.should == 3 + times.should == [t0, t0 + IceCube::ONE_DAY, t0 + 2 * IceCube::ONE_DAY] + end + + it 'should produce the correct days for @interval = 2' do + schedule = IceCube::Schedule.new(t0 = Time.now) + schedule.add_recurrence_rule IceCube::Rule.daily(2) + #check assumption (3) -- (1) 2 (3) 4 (5) 6 + times = schedule.occurrences(t0 + 5 * IceCube::ONE_DAY) + times.size.should == 3 + times.should == [t0, t0 + 2 * IceCube::ONE_DAY, t0 + 4 * IceCube::ONE_DAY] + end + + it 'should produce the correct days for @interval = 2 when crossing into a new year' do + schedule = IceCube::Schedule.new(t0 = Time.utc(2011, 12, 29)) + schedule.add_recurrence_rule IceCube::Rule.daily(2) + #check assumption (3) -- (1) 2 (3) 4 (5) 6 + times = schedule.occurrences(t0 + 5 * IceCube::ONE_DAY) + times.size.should == 3 + times.should == [t0, t0 + 2 * IceCube::ONE_DAY, t0 + 4 * IceCube::ONE_DAY] + end + it 'should produce the correct days for interval of 4 day with hour and minute of day set' do + schedule = IceCube::Schedule.new(t0 = Time.local(2010, 3, 1)) + schedule.add_recurrence_rule IceCube::Rule.daily(4).hour_of_day(5).minute_of_hour(45) + #check assumption 2 -- 1 (2) (3) (4) 5 (6) + times = schedule.occurrences(t0 + 5 * IceCube::ONE_DAY) + times.should == [ + t0 + 5 * IceCube::ONE_HOUR + 45 * IceCube::ONE_MINUTE, + t0 + 4 * IceCube::ONE_DAY + 5 * IceCube::ONE_HOUR + 45 * IceCube::ONE_MINUTE + ] end end diff --git a/spec/examples/from_ical_spec.rb b/spec/examples/from_ical_spec.rb new file mode 100644 index 00000000..4d6f715c --- /dev/null +++ b/spec/examples/from_ical_spec.rb @@ -0,0 +1,363 @@ +require File.dirname(__FILE__) + '/../spec_helper' +require 'active_support/time' + +describe IceCube::Rule, 'from_ical' do + + it 'should return a IceCube DailyRule class for a basic daily rule' do + rule = IceCube::IcalParser.rule_from_ical "FREQ=DAILY" + rule.class.should == IceCube::DailyRule + end + + it 'should return a IceCube WeeklyRule class for a basic monthly rule' do + rule = IceCube::IcalParser.rule_from_ical "FREQ=WEEKLY" + rule.class.should == IceCube::WeeklyRule + end + + it 'should return a IceCube MonthlyRule class for a basic monthly rule' do + rule = IceCube::IcalParser.rule_from_ical "FREQ=MONTHLY" + rule.class.should == IceCube::MonthlyRule + end + + it 'should return a IceCube YearlyRule class for a basic yearly rule' do + rule = IceCube::IcalParser.rule_from_ical "FREQ=YEARLY" + rule.class.should == IceCube::YearlyRule + end + + it 'should be able to parse a .day rule' do + rule = IceCube::IcalParser.rule_from_ical("FREQ=DAILY;BYDAY=MO,TU") + rule.should == IceCube::Rule.daily.day(:monday, :tuesday) + end + + it 'should be able to parse a .day_of_week rule' do + rule = IceCube::IcalParser.rule_from_ical("FREQ=DAILY;BYDAY=-1TU,-2TU") + rule.should == IceCube::Rule.daily.day_of_week(:tuesday => [-1, -2]) + end + + it 'should be able to parse both .day and .day_of_week rules' do + rule = IceCube::IcalParser.rule_from_ical("FREQ=DAILY;BYDAY=MO,-1TU,-2TU") + rule.should == IceCube::Rule.daily.day_of_week(:tuesday => [-1, -2]).day(:monday) + end + + it 'should be able to parse a .day_of_month rule' do + rule = IceCube::IcalParser.rule_from_ical("FREQ=DAILY;BYMONTHDAY=23") + rule.should == IceCube::Rule.daily.day_of_month(23) + end + + it 'should be able to parse a .day_of_year rule' do + rule = IceCube::IcalParser.rule_from_ical("FREQ=DAILY;BYYEARDAY=100,200") + rule.should == IceCube::Rule.daily.day_of_year(100,200) + end + + it 'should be able to serialize a .month_of_year rule' do + rule = IceCube::IcalParser.rule_from_ical("FREQ=DAILY;BYMONTH=1,4") + rule.should == IceCube::Rule.daily.month_of_year(:january, :april) + end + + it 'should be able to split to a combination of day_of_week and day (day_of_week has priority)' do + rule = IceCube::IcalParser.rule_from_ical("FREQ=DAILY;BYDAY=TU,MO,1MO,-1MO") + rule.should == IceCube::Rule.daily.day(:tuesday).day_of_week(:monday => [1, -1]) + end + + it 'should be able to parse of .day_of_week rule with multiple days' do + rule = IceCube::IcalParser.rule_from_ical("FREQ=DAILY;BYDAY=WE,1MO,-1MO,2TU") + rule.should == IceCube::Rule.daily.day_of_week(:monday => [1, -1], :tuesday => [2]).day(:wednesday) + end + + it 'should be able to parse a rule with an until date' do + t = Time.now.utc + rule = IceCube::IcalParser.rule_from_ical("FREQ=WEEKLY;UNTIL=#{t.strftime("%Y%m%dT%H%M%SZ")}") + rule.to_s.should == IceCube::Rule.weekly.until(t).to_s + end + + it 'should be able to parse a rule with a count date' do + rule = IceCube::IcalParser.rule_from_ical("FREQ=WEEKLY;COUNT=5") + rule.should == IceCube::Rule.weekly.count(5) + end + + it 'should be able to parse a rule with an interval' do + rule = IceCube::IcalParser.rule_from_ical("FREQ=DAILY;INTERVAL=2") + rule.should == IceCube::Rule.daily.interval(2) + end + + it 'should be able to parse week start (WKST)' do + rule = IceCube::IcalParser.rule_from_ical("FREQ=WEEKLY;INTERVAL=2;WKST=MO") + rule.should == IceCube::Rule.weekly(2, :monday) + end + + it 'should return no occurrences after daily interval with count is over' do + schedule = IceCube::Schedule.new(Time.now) + schedule.add_recurrence_rule(IceCube::IcalParser.rule_from_ical("FREQ=DAILY;COUNT=5")) + schedule.occurrences_between(Time.now + 7.days, Time.now + 14.days).count.should == 0 + end +end + +describe IceCube::Schedule, "from_ical" do + + ical_string = <<-ICAL +DTSTART:20130314T201500Z +DTEND:20130314T201545Z +RRULE:FREQ=WEEKLY;BYDAY=TH;UNTIL=20130531T100000Z +ICAL + + ical_string_woth_multiple_exdates = <<-ICAL +DTSTART;TZID=America/Denver:20130731T143000 +DTEND;TZID=America/Denver:20130731T153000 +RRULE:FREQ=WEEKLY;UNTIL=20140730T203000Z;BYDAY=MO,WE,FR +EXDATE;TZID=America/Denver:20130823T143000 +EXDATE;TZID=America/Denver:20130812T143000 +EXDATE;TZID=America/Denver:20130807T143000 +ICAL + + + def sorted_ical(ical) + ical.split(/\n/).sort.map { |field| + k, v = field.split(':') + v = v.split(';').sort.join(';') if k == 'RRULE' + + "#{ k }:#{ v }" + }.join("\n") + end + + context "instantiation" do + it "loads an ICAL string" do + expect(IceCube::IcalParser.schedule_from_ical(ical_string)).to be_a(IceCube::Schedule) + end + end + + context "daily frequency" do + it 'matches simple daily' do + start_time = Time.now + + schedule = IceCube::Schedule.new(start_time) + schedule.add_recurrence_rule(IceCube::Rule.daily) + + ical = schedule.to_ical + sorted_ical(IceCube::IcalParser.schedule_from_ical(ical).to_ical).should eq(sorted_ical(ical)) + end + + it 'handles counts' do + start_time = Time.now + + schedule = IceCube::Schedule.new(start_time) + schedule.add_recurrence_rule(IceCube::Rule.daily.count(4)) + + ical = schedule.to_ical + sorted_ical(IceCube::IcalParser.schedule_from_ical(ical).to_ical).should eq(sorted_ical(ical)) + end + + it 'handles intervals' do + start_time = Time.now + + schedule = IceCube::Schedule.new(start_time) + schedule.add_recurrence_rule(IceCube::Rule.daily(4)) + + ical = schedule.to_ical + sorted_ical(IceCube::IcalParser.schedule_from_ical(ical).to_ical).should eq(sorted_ical(ical)) + end + + it 'handles intervals and counts' do + start_time = Time.now + + schedule = IceCube::Schedule.new(start_time) + schedule.add_recurrence_rule(IceCube::Rule.daily(4).count(10)) + + ical = schedule.to_ical + sorted_ical(IceCube::IcalParser.schedule_from_ical(ical).to_ical).should eq(sorted_ical(ical)) + end + + it 'handles until dates' do + start_time = Time.now + + schedule = IceCube::Schedule.new(start_time) + schedule.add_recurrence_rule(IceCube::Rule.daily.until(start_time + 15.days)) + + ical = schedule.to_ical + sorted_ical(IceCube::IcalParser.schedule_from_ical(ical).to_ical).should eq(sorted_ical(ical)) + end + + end + + context 'weekly frequency' do + it 'matches simple weekly' do + start_time = Time.now + + schedule = IceCube::Schedule.new(start_time) + schedule.add_recurrence_rule(IceCube::Rule.weekly) + + ical = schedule.to_ical + sorted_ical(IceCube::IcalParser.schedule_from_ical(ical).to_ical).should eq(sorted_ical(ical)) + end + + it 'handles weekdays' do + start_time = Time.now + + schedule = IceCube::Schedule.new(start_time) + schedule.add_recurrence_rule(IceCube::Rule.weekly.day(:monday, :thursday)) + + ical = schedule.to_ical + sorted_ical(IceCube::IcalParser.schedule_from_ical(ical).to_ical).should eq(sorted_ical(ical)) + end + + it 'handles intervals' do + start_time = Time.now + + schedule = IceCube::Schedule.new(start_time) + schedule.add_recurrence_rule(IceCube::Rule.weekly(2)) + + ical = schedule.to_ical + sorted_ical(IceCube::IcalParser.schedule_from_ical(ical).to_ical).should eq(sorted_ical(ical)) + end + + it 'handles intervals and counts' do + start_time = Time.now + + schedule = IceCube::Schedule.new(start_time) + schedule.add_recurrence_rule(IceCube::Rule.weekly(2).count(4)) + + ical = schedule.to_ical + sorted_ical(IceCube::IcalParser.schedule_from_ical(ical).to_ical).should eq(sorted_ical(ical)) + end + + it 'handles intervals and counts on given weekdays' do + start_time = Time.now + + schedule = IceCube::Schedule.new(start_time) + schedule.add_recurrence_rule(IceCube::Rule.weekly(2).day(:monday, :wednesday).count(4)) + + ical = schedule.to_ical + sorted_ical(IceCube::IcalParser.schedule_from_ical(ical).to_ical).should eq(sorted_ical(ical)) + end + end + + context 'monthly frequency' do + it 'matches simple monthly' do + start_time = Time.now + + schedule = IceCube::Schedule.new(start_time) + schedule.add_recurrence_rule(IceCube::Rule.monthly) + + ical = schedule.to_ical + sorted_ical(IceCube::IcalParser.schedule_from_ical(ical).to_ical).should eq(sorted_ical(ical)) + end + + it 'handles intervals' do + start_time = Time.now + + schedule = IceCube::Schedule.new(start_time) + schedule.add_recurrence_rule(IceCube::Rule.monthly(2)) + + ical = schedule.to_ical + sorted_ical(IceCube::IcalParser.schedule_from_ical(ical).to_ical).should eq(sorted_ical(ical)) + end + + it 'handles intervals and counts' do + start_time = Time.now + + schedule = IceCube::Schedule.new(start_time) + schedule.add_recurrence_rule(IceCube::Rule.monthly(2).count(5)) + + ical = schedule.to_ical + sorted_ical(IceCube::IcalParser.schedule_from_ical(ical).to_ical).should eq(sorted_ical(ical)) + end + + it 'handles intervals and counts on specific days' do + start_time = Time.now + + schedule = IceCube::Schedule.new(start_time) + schedule.add_recurrence_rule(IceCube::Rule.monthly(2).day_of_month(1, 15).count(5)) + + ical = schedule.to_ical + sorted_ical(IceCube::IcalParser.schedule_from_ical(ical).to_ical).should eq(sorted_ical(ical)) + end + end + + context 'yearly frequency' do + it 'matches simple yearly' do + start_time = Time.now + + schedule = IceCube::Schedule.new(start_time) + schedule.add_recurrence_rule(IceCube::Rule.yearly) + + ical = schedule.to_ical + sorted_ical(IceCube::IcalParser.schedule_from_ical(ical).to_ical).should eq(sorted_ical(ical)) + end + + it 'handles intervals' do + start_time = Time.now + + schedule = IceCube::Schedule.new(start_time) + schedule.add_recurrence_rule(IceCube::Rule.yearly(2)) + + ical = schedule.to_ical + sorted_ical(IceCube::IcalParser.schedule_from_ical(ical).to_ical).should eq(sorted_ical(ical)) + end + + it 'handles a specific day' do + start_time = Time.now + + schedule = IceCube::Schedule.new(start_time) + schedule.add_recurrence_rule(IceCube::Rule.yearly.day_of_year(15)) + + ical = schedule.to_ical + sorted_ical(IceCube::IcalParser.schedule_from_ical(ical).to_ical).should eq(sorted_ical(ical)) + end + + it 'handles specific days' do + start_time = Time.now + + schedule = IceCube::Schedule.new(start_time) + schedule.add_recurrence_rule(IceCube::Rule.yearly.day_of_year(1, 15, -1)) + + ical = schedule.to_ical + sorted_ical(IceCube::IcalParser.schedule_from_ical(ical).to_ical).should eq(sorted_ical(ical)) + end + + it 'handles counts' do + start_time = Time.now + + schedule = IceCube::Schedule.new(start_time) + schedule.add_recurrence_rule(IceCube::Rule.yearly.count(5)) + + ical = schedule.to_ical + sorted_ical(IceCube::IcalParser.schedule_from_ical(ical).to_ical).should eq(sorted_ical(ical)) + end + + it 'handles specific months' do + start_time = Time.now + + schedule = IceCube::Schedule.new(start_time) + schedule.add_recurrence_rule(IceCube::Rule.yearly.month_of_year(:january, :december)) + + ical = schedule.to_ical + sorted_ical(IceCube::IcalParser.schedule_from_ical(ical).to_ical).should eq(sorted_ical(ical)) + end + + it 'handles specific months and counts' do + start_time = Time.now + + schedule = IceCube::Schedule.new(start_time) + schedule.add_recurrence_rule(IceCube::Rule.yearly.month_of_year(:january, :december).count(15)) + + ical = schedule.to_ical + sorted_ical(IceCube::IcalParser.schedule_from_ical(ical).to_ical).should eq(sorted_ical(ical)) + end + end + + context "exceptions" do + it 'handles single EXDATE lines' do + start_time = Time.now + + schedule = IceCube::Schedule.new(start_time) + schedule.add_recurrence_rule(IceCube::Rule.daily) + schedule.add_exception_time(Time.now + 2.days) + + ical = schedule.to_ical + sorted_ical(IceCube::IcalParser.schedule_from_ical(ical).to_ical).should eq(sorted_ical(ical)) + end + + it 'handles mulitple EXDATE lines' do + schedule = IceCube::IcalParser.schedule_from_ical ical_string_woth_multiple_exdates + schedule.exception_times.count.should == 3 + end + end +end diff --git a/spec/examples/hourly_rule_spec.rb b/spec/examples/hourly_rule_spec.rb index 02d56ecf..f62b4003 100644 --- a/spec/examples/hourly_rule_spec.rb +++ b/spec/examples/hourly_rule_spec.rb @@ -1,50 +1,68 @@ require File.dirname(__FILE__) + '/../spec_helper' -module IceCube - describe HourlyRule do - - context :system_time_zone => 'America/Vancouver' do - - it 'should work across DST start hour' do - schedule = Schedule.new(t0 = Time.local(2013, 3, 10, 1, 0, 0)) - schedule.add_recurrence_rule Rule.hourly - schedule.first(3).should == [ - Time.local(2013, 3, 10, 1, 0, 0), # -0800 - Time.local(2013, 3, 10, 3, 0, 0), # -0700 - Time.local(2013, 3, 10, 4, 0, 0) # -0700 - ] - end - - it 'should not skip times in DST end hour' do - schedule = Schedule.new(t0 = Time.local(2013, 11, 3, 0, 0, 0)) - schedule.add_recurrence_rule Rule.hourly - schedule.first(4).should == [ - Time.local(2013, 11, 3, 0, 0, 0), # -0700 - Time.local(2013, 11, 3, 1, 0, 0) - ONE_HOUR, # -0700 - Time.local(2013, 11, 3, 1, 0, 0), # -0800 - Time.local(2013, 11, 3, 2, 0, 0), # -0800 - ] - end +describe IceCube::HourlyRule do + context system_time_zone: 'America/Vancouver' do + it 'should work across DST start hour' do + schedule = IceCube::Schedule.new(t0 = Time.local(2013, 3, 10, 1, 0, 0)) + schedule.add_recurrence_rule IceCube::Rule.hourly + schedule.first(3).should == [ + Time.local(2013, 3, 10, 1, 0, 0), # -0800 + Time.local(2013, 3, 10, 3, 0, 0), # -0700 + Time.local(2013, 3, 10, 4, 0, 0) # -0700 + ] end - it 'should update previous interval' do - schedule = double(start_time: t0 = Time.now) - rule = Rule.hourly(7) - rule.interval(5) - rule.next_time(t0 + 1, schedule, nil).should == t0 + 5.hours + it 'should not skip times in DST end hour' do + schedule = IceCube::Schedule.new(t0 = Time.local(2013, 11, 3, 0, 0, 0)) + schedule.add_recurrence_rule IceCube::Rule.hourly + schedule.first(4).should == [ + Time.local(2013, 11, 3, 0, 0, 0), # -0700 + Time.local(2013, 11, 3, 1, 0, 0) - IceCube::ONE_HOUR, # -0700 + Time.local(2013, 11, 3, 1, 0, 0), # -0800 + Time.local(2013, 11, 3, 2, 0, 0), # -0800 + ] end - it 'should produce the correct days for @interval = 3' do - start_date = DAY - schedule = Schedule.new(start_date) - schedule = Schedule.from_yaml(schedule.to_yaml) - schedule.add_recurrence_rule Rule.hourly(3) - #check assumption (3) -- (1) 2 (3) 4 (5) 6 - dates = schedule.first(3) - dates.size.should == 3 - dates.should == [DAY, DAY + 3 * ONE_HOUR, DAY + 6 * ONE_HOUR] - end + end + + it 'should update previous interval' do + schedule = double(start_time: t0 = Time.now) + rule = IceCube::Rule.hourly(7) + rule.interval(5) + rule.next_time(t0 + 1, schedule, nil).should == t0 + 5.hours + end + + it 'should produce the correct days for @interval = 3' do + start_date = DAY + schedule = IceCube::Schedule.new(start_date) + schedule = IceCube::Schedule.from_yaml(schedule.to_yaml) + schedule.add_recurrence_rule IceCube::Rule.hourly(3) + #check assumption (3) -- (1) 2 (3) 4 (5) 6 + dates = schedule.first(3) + dates.size.should == 3 + dates.should == [DAY, DAY + 3 * IceCube::ONE_HOUR, DAY + 6 * IceCube::ONE_HOUR] + end + + it 'should not produce results for @interval = 0' do + start_date = DAY + schedule = IceCube::Schedule.new(start_date) + schedule = IceCube::Schedule.from_yaml(schedule.to_yaml) + schedule.add_recurrence_rule IceCube::Rule.hourly(0) + #check assumption + dates = schedule.first(3) + dates.size.should == 0 + dates.should == [] + end + it 'should produce the correct days for @interval = 3' do + start_date = DAY + schedule = IceCube::Schedule.new(start_date) + schedule = IceCube::Schedule.from_yaml(schedule.to_yaml) + schedule.add_recurrence_rule IceCube::Rule.hourly(3) + #check assumption (3) -- (1) 2 (3) 4 (5) 6 + dates = schedule.first(3) + dates.size.should == 3 + dates.should == [DAY, DAY + 3 * IceCube::ONE_HOUR, DAY + 6 * IceCube::ONE_HOUR] end end diff --git a/spec/examples/monthly_rule_spec.rb b/spec/examples/monthly_rule_spec.rb index 9f9cc7b5..31a13f50 100644 --- a/spec/examples/monthly_rule_spec.rb +++ b/spec/examples/monthly_rule_spec.rb @@ -1,132 +1,146 @@ require File.dirname(__FILE__) + '/../spec_helper' -module IceCube - describe MonthlyRule do - - it 'should update previous interval' do - schedule = double(start_time: t0 = Time.utc(2013, 5, 17)) - rule = Rule.monthly(3) - rule.interval(1) - rule.next_time(t0 + 1, schedule, nil).should == t0 + 31.days - end +describe IceCube::MonthlyRule, 'occurs_on?' do + + it 'should not produce results for @interval = 0' do + start_date = DAY + schedule = IceCube::Schedule.new(start_date) + schedule.add_recurrence_rule IceCube::Rule.monthly(0) + #check assumption + schedule.occurrences(start_date + 50 * IceCube::ONE_DAY).size.should == 0 + end - it 'should produce the correct number of days for @interval = 1' do - schedule = Schedule.new(t0 = Time.now) - schedule.add_recurrence_rule Rule.monthly - #check assumption - schedule.occurrences(t0 + 50 * ONE_DAY).size.should == 2 - end + it 'should produce the correct number of days for @interval = 1' do + start_date = DAY + schedule = IceCube::Schedule.new(start_date) + schedule.add_recurrence_rule IceCube::Rule.monthly + #check assumption + schedule.occurrences(start_date + 50 * IceCube::ONE_DAY).size.should == 2 + end - it 'should produce the correct number of days for @interval = 2' do - schedule = Schedule.new(t0 = Time.now) - schedule.add_recurrence_rule Rule.monthly(2) - schedule.occurrences(t0 + 50 * ONE_DAY).size.should == 1 - end + it 'should update previous interval' do + schedule = double(start_time: t0 = Time.utc(2013, 5, 17)) + rule = IceCube::Rule.monthly(3) + rule.interval(1) + rule.next_time(t0 + 1, schedule, nil).should == t0 + 31.days + end - it 'should produce the correct number of days for @interval = 1 with only the 1st and 15th' do - schedule = Schedule.new(t0 = Time.utc(2010, 1, 1)) - schedule.add_recurrence_rule Rule.monthly.day_of_month(1, 15) - #check assumption (1) (15) (1) (15) - schedule.occurrences(t0 + 50 * ONE_DAY).map(&:day).should == [1, 15, 1, 15] - end + it 'should produce the correct number of days for @interval = 1' do + schedule = IceCube::Schedule.new(t0 = Time.now) + schedule.add_recurrence_rule IceCube::Rule.monthly + #check assumption + schedule.occurrences(t0 + 50 * IceCube::ONE_DAY).size.should == 2 + end - it 'should produce the correct number of days for @interval = 1 with only the 1st and last' do - schedule = Schedule.new(t0 = Time.utc(2010, 1, 1)) - schedule.add_recurrence_rule Rule.monthly.day_of_month(1, -1) - #check assumption (1) (31) (1) - schedule.occurrences(t0 + 60 * ONE_DAY).map(&:day).should == [1, 31, 1, 28, 1] - end + it 'should produce the correct number of days for @interval = 2' do + schedule = IceCube::Schedule.new(t0 = Time.now) + schedule.add_recurrence_rule IceCube::Rule.monthly(2) + schedule.occurrences(t0 + 50 * IceCube::ONE_DAY).size.should == 1 + end - it 'should produce the correct number of days for @interval = 1 with only the first mondays' do - schedule = Schedule.new(t0 = Time.utc(2010, 1, 1)) - schedule.add_recurrence_rule Rule.monthly.day_of_week(:monday => [1]) - #check assumption (month 1 monday) (month 2 monday) - schedule.occurrences(t0 + 50 * ONE_DAY).size.should == 2 - end + it 'should produce the correct number of days for @interval = 1 with only the 1st and 15th' do + schedule = IceCube::Schedule.new(t0 = Time.utc(2010, 1, 1)) + schedule.add_recurrence_rule IceCube::Rule.monthly.day_of_month(1, 15) + #check assumption (1) (15) (1) (15) + schedule.occurrences(t0 + 50 * IceCube::ONE_DAY).map(&:day).should == [1, 15, 1, 15] + end - it 'should produce the correct number of days for @interval = 1 with only the last mondays' do - schedule = Schedule.new(t0 = Time.utc(2010, 1, 1)) - schedule.add_recurrence_rule Rule.monthly.day_of_week(:monday => [-1]) - #check assumption (month 1 monday) - schedule.occurrences(t0 + 40 * ONE_DAY).size.should == 1 - end + it 'should produce the correct number of days for @interval = 1 with only the 1st and last' do + schedule = IceCube::Schedule.new(t0 = Time.utc(2010, 1, 1)) + schedule.add_recurrence_rule IceCube::Rule.monthly.day_of_month(1, -1) + #check assumption (1) (31) (1) + schedule.occurrences(t0 + 60 * IceCube::ONE_DAY).map(&:day).should == [1, 31, 1, 28, 1] + end - it 'should produce the correct number of days for @interval = 1 with only the first and last mondays' do - t0 = Time.utc(2010, 1, 1) - t1 = Time.utc(2010, 12, 31) - schedule = Schedule.new(t0) - schedule.add_recurrence_rule Rule.monthly.day_of_week(:monday => [1, -2]) - #check assumption (12 months - 2 dates each) - schedule.occurrences(t1).size.should == 24 - end + it 'should produce the correct number of days for @interval = 1 with only the first mondays' do + schedule = IceCube::Schedule.new(t0 = Time.utc(2010, 1, 1)) + schedule.add_recurrence_rule IceCube::Rule.monthly.day_of_week(:monday => [1]) + #check assumption (month 1 monday) (month 2 monday) + schedule.occurrences(t0 + 50 * IceCube::ONE_DAY).size.should == 2 + end - [:sunday, :monday, :tuesday, :wednesday, :thursday, :friday, :saturday].each_with_index do |weekday, wday| - context "for every first #{weekday} of a month" do - let(:schedule) { - schedule = Schedule.new(t0 = Time.local(2011, 8, 1)) - schedule.add_recurrence_rule Rule.monthly.day_of_week(weekday => [1]) - } - - it "should not skip a month when DST ends" do - schedule.first(48).inject(nil) do |last_date, current_date| - next current_date unless last_date - month_interval(current_date, last_date).should == 1 - end - end + it 'should produce the correct number of days for @interval = 1 with only the last mondays' do + schedule = IceCube::Schedule.new(t0 = Time.utc(2010, 1, 1)) + schedule.add_recurrence_rule IceCube::Rule.monthly.day_of_week(:monday => [-1]) + #check assumption (month 1 monday) + schedule.occurrences(t0 + 40 * IceCube::ONE_DAY).size.should == 1 + end + + it 'should produce the correct number of days for @interval = 1 with only the first and last mondays' do + t0 = Time.utc(2010, 1, 1) + t1 = Time.utc(2010, 12, 31) + schedule = IceCube::Schedule.new(t0) + schedule.add_recurrence_rule IceCube::Rule.monthly.day_of_week(:monday => [1, -2]) + #check assumption (12 months - 2 dates each) + schedule.occurrences(t1).size.should == 24 + end - it "should not change day when DST ends" do - schedule.first(48).inject(nil) do |last_date, current_date| - next current_date unless last_date - current_date.wday.should == wday - end + [:sunday, :monday, :tuesday, :wednesday, :thursday, :friday, :saturday].each_with_index do |weekday, wday| + context "for every first #{weekday} of a month" do + let(:schedule) { + schedule = IceCube::Schedule.new(t0 = Time.local(2011, 8, 1)) + schedule.add_recurrence_rule IceCube::Rule.monthly.day_of_week(weekday => [1]) + } + + it "should not skip a month when DST ends" do + schedule.first(48).inject(nil) do |last_date, current_date| + next current_date unless last_date + month_interval(current_date, last_date).should == 1 end + end - it "should not change hour when DST ends" do - schedule.first(48).inject(nil) do |last_date, current_date| - next current_date unless last_date - current_date.hour.should == 0 - end + it "should not change day when DST ends" do + schedule.first(48).inject(nil) do |last_date, current_date| + next current_date unless last_date + current_date.wday.should == wday end end - end - it 'should produce dates on a monthly interval for the last day of the month' do - schedule = Schedule.new(t0 = Time.utc(2010, 3, 31, 0, 0, 0)) - schedule.add_recurrence_rule Rule.monthly - schedule.first(10).should == [ - Time.utc(2010, 3, 31, 0, 0, 0), Time.utc(2010, 4, 30, 0, 0, 0), - Time.utc(2010, 5, 31, 0, 0, 0), Time.utc(2010, 6, 30, 0, 0, 0), - Time.utc(2010, 7, 31, 0, 0, 0), Time.utc(2010, 8, 31, 0, 0, 0), - Time.utc(2010, 9, 30, 0, 0, 0), Time.utc(2010, 10, 31, 0, 0, 0), - Time.utc(2010, 11, 30, 0, 0, 0), Time.utc(2010, 12, 31, 0, 0, 0) - ] + it "should not change hour when DST ends" do + schedule.first(48).inject(nil) do |last_date, current_date| + next current_date unless last_date + current_date.hour.should == 0 + end + end end + end - it 'should produce dates on a monthly interval for latter days in the month near February' do - schedule = Schedule.new(t0 = Time.utc(2010, 1, 29, 0, 0, 0)) - schedule.add_recurrence_rule Rule.monthly - schedule.first(3).should == [ - Time.utc(2010, 1, 29, 0, 0, 0), - Time.utc(2010, 2, 28, 0, 0, 0), - Time.utc(2010, 3, 29, 0, 0, 0) - ] - end + it 'should produce dates on a monthly interval for the last day of the month' do + schedule = IceCube::Schedule.new(t0 = Time.utc(2010, 3, 31, 0, 0, 0)) + schedule.add_recurrence_rule IceCube::Rule.monthly + schedule.first(10).should == [ + Time.utc(2010, 3, 31, 0, 0, 0), Time.utc(2010, 4, 30, 0, 0, 0), + Time.utc(2010, 5, 31, 0, 0, 0), Time.utc(2010, 6, 30, 0, 0, 0), + Time.utc(2010, 7, 31, 0, 0, 0), Time.utc(2010, 8, 31, 0, 0, 0), + Time.utc(2010, 9, 30, 0, 0, 0), Time.utc(2010, 10, 31, 0, 0, 0), + Time.utc(2010, 11, 30, 0, 0, 0), Time.utc(2010, 12, 31, 0, 0, 0) + ] + end - it 'should restrict to available days of month when specified' do - schedule = Schedule.new(t0 = Time.utc(2013,1,31)) - schedule.add_recurrence_rule Rule.monthly.day_of_month(31) - schedule.first(3).should == [ - Time.utc(2013, 1, 31), - Time.utc(2013, 3, 31), - Time.utc(2013, 5, 31) - ] - end + it 'should produce dates on a monthly interval for latter days in the month near February' do + schedule = IceCube::Schedule.new(t0 = Time.utc(2010, 1, 29, 0, 0, 0)) + schedule.add_recurrence_rule IceCube::Rule.monthly + schedule.first(3).should == [ + Time.utc(2010, 1, 29, 0, 0, 0), + Time.utc(2010, 2, 28, 0, 0, 0), + Time.utc(2010, 3, 29, 0, 0, 0) + ] + end - def month_interval(current_date, last_date) - current_month = current_date.year * 12 + current_date.month - last_month = last_date.year * 12 + last_date.month - current_month - last_month - end + it 'should restrict to available days of month when specified' do + schedule = IceCube::Schedule.new(t0 = Time.utc(2013,1,31)) + schedule.add_recurrence_rule IceCube::Rule.monthly.day_of_month(31) + schedule.first(3).should == [ + Time.utc(2013, 1, 31), + Time.utc(2013, 3, 31), + Time.utc(2013, 5, 31) + ] + end + def month_interval(current_date, last_date) + current_month = current_date.year * 12 + current_date.month + last_month = last_date.year * 12 + last_date.month + current_month - last_month end + end diff --git a/spec/examples/schedule_spec.rb b/spec/examples/schedule_spec.rb index 149a53e0..f7fc2cac 100644 --- a/spec/examples/schedule_spec.rb +++ b/spec/examples/schedule_spec.rb @@ -371,7 +371,6 @@ next_year = Date.new(t0.year + 1, t0.month, t0.day) s.add_recurrence_rule nonsense.until(next_year) end - trap_infinite_loop_beyond(24) schedule.next_occurrences(1).should be_empty end @@ -633,7 +632,7 @@ describe :occurs_on? do - subject(:schedule) { IceCube::Schedule.new(start_time) } + let(:schedule) { IceCube::Schedule.new(start_time) } shared_examples "occurring on a given day" do WORLD_TIME_ZONES.each do |zone| @@ -737,8 +736,4 @@ def compare_time_zone_info(start_time) occurrence.utc_offset == start_time.utc_offset end - def trap_infinite_loop_beyond(iterations) - IceCube::ValidatedRule.any_instance.should_receive(:finds_acceptable_time?). - at_most(iterations).times.and_call_original - end end diff --git a/spec/examples/weekly_rule_spec.rb b/spec/examples/weekly_rule_spec.rb index c7811150..85dc8a57 100644 --- a/spec/examples/weekly_rule_spec.rb +++ b/spec/examples/weekly_rule_spec.rb @@ -1,141 +1,163 @@ require File.dirname(__FILE__) + '/../spec_helper' -module IceCube - describe WeeklyRule do - - context :system_time_zone => 'America/Vancouver' do - - it 'should include nearest time in DST start hour' do - schedule = Schedule.new(t0 = Time.local(2013, 3, 3, 2, 30, 0)) - schedule.add_recurrence_rule Rule.weekly - schedule.first(3).should == [ - Time.local(2013, 3, 3, 2, 30, 0), # -0800 - Time.local(2013, 3, 10, 3, 30, 0), # -0700 - Time.local(2013, 3, 17, 2, 30, 0) # -0700 - ] - end - - it 'should not skip times in DST end hour' do - schedule = Schedule.new(t0 = Time.local(2013, 10, 27, 2, 30, 0)) - schedule.add_recurrence_rule Rule.weekly - schedule.first(3).should == [ - Time.local(2013, 10, 27, 2, 30, 0), # -0700 - Time.local(2013, 11, 3, 2, 30, 0), # -0700 - Time.local(2013, 11, 10, 2, 30, 0) # -0800 - ] - end +describe IceCube::WeeklyRule, 'occurs_on?' do + + it 'should not produce results for @interval = 0' do + start_date = Time.now + schedule = IceCube::Schedule.new(start_date) + schedule.add_recurrence_rule IceCube::Rule.weekly(0) + #check assumption + dates = schedule.occurrences(start_date + (7 * 3 + 1) * IceCube::ONE_DAY) + dates.size.should == 0 + end + + it 'should produce the correct number of days for @interval = 1 with no weekdays specified' do + start_date = Time.now + schedule = IceCube::Schedule.new(start_date) + schedule.add_recurrence_rule IceCube::Rule.weekly + #check assumption (2 weeks in the future) (1) (2) (3) (4) (5) + dates = schedule.occurrences(start_date + (7 * 3 + 1) * IceCube::ONE_DAY) + dates.size.should == 4 + end + + context system_time_zone: 'America/Vancouver' do + + it 'should include nearest time in DST start hour' do + schedule = IceCube::Schedule.new(t0 = Time.local(2013, 3, 3, 2, 30, 0)) + schedule.add_recurrence_rule IceCube::Rule.weekly + schedule.first(3).should == [ + Time.local(2013, 3, 3, 2, 30, 0), # -0800 + Time.local(2013, 3, 10, 3, 30, 0), # -0700 + Time.local(2013, 3, 17, 2, 30, 0) # -0700 + ] + end + it 'should not skip times in DST end hour' do + schedule = IceCube::Schedule.new(t0 = Time.local(2013, 10, 27, 2, 30, 0)) + schedule.add_recurrence_rule IceCube::Rule.weekly + schedule.first(3).should == [ + Time.local(2013, 10, 27, 2, 30, 0), # -0700 + Time.local(2013, 11, 3, 2, 30, 0), # -0700 + Time.local(2013, 11, 10, 2, 30, 0) # -0800 + ] end it 'should update previous interval' do - schedule = double(start_time: t0 = Time.new(2013, 1, 1)) - rule = Rule.weekly(7) + schedule = double(start_time: t0 = Time.now) + rule = IceCube::Rule.weekly(7) rule.interval(2) - rule.next_time(t0 + 1, schedule, nil).should == Time.new(2013, 1, 15) + rule.next_time(t0 + 1, schedule, nil).should == t0 + 14 * IceCube::ONE_DAY end - it 'should produce the correct number of days for @interval = 1 with no weekdays specified' do - schedule = Schedule.new(t0 = Time.now) - schedule.add_recurrence_rule Rule.weekly - #check assumption (2 weeks in the future) (1) (2) (3) (4) (5) - times = schedule.occurrences(t0 + (7 * 3 + 1) * ONE_DAY) - times.size.should == 4 - end + end - it 'should produce the correct number of days for @interval = 1 with only weekends' do - schedule = Schedule.new(t0 = WEDNESDAY) - schedule.add_recurrence_rule Rule.weekly.day(:saturday, :sunday) - #check assumption - schedule.occurrences(t0 + 4 * ONE_WEEK).size.should == 8 - end + it 'should update previous interval' do + schedule = double(start_time: t0 = Time.new(2013, 1, 1)) + rule = IceCube::Rule.weekly(7) + rule.interval(2) + rule.next_time(t0 + 1, schedule, nil).should == Time.new(2013, 1, 15) + end - it 'should set days from symbol args' do - schedule = Schedule.new(t0 = WEDNESDAY) - schedule.add_recurrence_rule Rule.weekly.day(:monday, :wednesday) - schedule.rrules.first.validations_for(:day).map(&:day).should == [1, 3] - end + it 'should produce the correct number of days for @interval = 1 with no weekdays specified' do + schedule = IceCube::Schedule.new(t0 = Time.now) + schedule.add_recurrence_rule IceCube::Rule.weekly + #check assumption (2 weeks in the future) (1) (2) (3) (4) (5) + times = schedule.occurrences(t0 + (7 * 3 + 1) * IceCube::ONE_DAY) + times.size.should == 4 + end - it 'should set days from array of symbols' do - schedule = Schedule.new(t0 = WEDNESDAY) - schedule.add_recurrence_rule Rule.weekly.day([:monday, :wednesday]) - schedule.rrules.first.validations_for(:day).map(&:day).should == [1, 3] - end + it 'should produce the correct number of days for @interval = 1 with only weekends' do + schedule = IceCube::Schedule.new(t0 = WEDNESDAY) + schedule.add_recurrence_rule IceCube::Rule.weekly.day(:saturday, :sunday) + #check assumption + schedule.occurrences(t0 + 4 * IceCube::ONE_WEEK).size.should == 8 + end - it 'should set days from integer args' do - schedule = Schedule.new(t0 = WEDNESDAY) - schedule.add_recurrence_rule Rule.weekly.day(1, 3) - schedule.rrules.first.validations_for(:day).map(&:day).should == [1, 3] - end + it 'should set days from symbol args' do + schedule = IceCube::Schedule.new(t0 = WEDNESDAY) + schedule.add_recurrence_rule IceCube::Rule.weekly.day(:monday, :wednesday) + schedule.rrules.first.validations_for(:day).map(&:day).should == [1, 3] + end - it 'should set days from array of integers' do - schedule = Schedule.new(t0 = WEDNESDAY) - schedule.add_recurrence_rule Rule.weekly.day([1, 3]) - schedule.rrules.first.validations_for(:day).map(&:day).should == [1, 3] - end + it 'should set days from array of symbols' do + schedule = IceCube::Schedule.new(t0 = WEDNESDAY) + schedule.add_recurrence_rule IceCube::Rule.weekly.day([:monday, :wednesday]) + schedule.rrules.first.validations_for(:day).map(&:day).should == [1, 3] + end - it 'should raise an error on invalid input' do - schedule = Schedule.new(t0 = WEDNESDAY) - expect { schedule.add_recurrence_rule Rule.weekly.day(["1", "3"]) }.to raise_error - end + it 'should set days from integer args' do + schedule = IceCube::Schedule.new(t0 = WEDNESDAY) + schedule.add_recurrence_rule IceCube::Rule.weekly.day(1, 3) + schedule.rrules.first.validations_for(:day).map(&:day).should == [1, 3] + end - it 'should produce the correct number of days for @interval = 2 with only one day per week' do - schedule = Schedule.new(t0 = WEDNESDAY) - schedule.add_recurrence_rule Rule.weekly(2).day(:wednesday) - #check assumption - times = schedule.occurrences(t0 + 3 * ONE_WEEK) - times.should == [t0, t0 + 2 * ONE_WEEK] - end + it 'should set days from array of integers' do + schedule = IceCube::Schedule.new(t0 = WEDNESDAY) + schedule.add_recurrence_rule IceCube::Rule.weekly.day([1, 3]) + schedule.rrules.first.validations_for(:day).map(&:day).should == [1, 3] + end - it 'should produce the correct days for @interval = 2, regardless of the start week' do - schedule = Schedule.new(t0 = WEDNESDAY + ONE_WEEK) - schedule.add_recurrence_rule Rule.weekly(2).day(:wednesday) - #check assumption - times = schedule.occurrences(t0 + 3 * ONE_WEEK) - times.should == [t0, t0 + 2 * ONE_WEEK] - end + it 'should raise an error on invalid input' do + schedule = IceCube::Schedule.new(t0 = WEDNESDAY) + expect { schedule.add_recurrence_rule IceCube::Rule.weekly.day(["1", "3"]) }.to raise_error + end - it 'should occur every 2nd tuesday of a month' do - schedule = Schedule.new(t0 = Time.now) - schedule.add_recurrence_rule Rule.monthly.hour_of_day(11).day_of_week(:tuesday => [2]) - schedule.first(48).each do |d| - d.hour.should == 11 - d.wday.should == 2 - end - end + it 'should produce the correct number of days for @interval = 2 with only one day per week' do + schedule = IceCube::Schedule.new(t0 = WEDNESDAY) + schedule.add_recurrence_rule IceCube::Rule.weekly(2).day(:wednesday) + #check assumption + times = schedule.occurrences(t0 + 3 * IceCube::ONE_WEEK) + times.should == [t0, t0 + 2 * IceCube::ONE_WEEK] + end - it 'should be able to start on sunday but repeat on wednesdays' do - schedule = Schedule.new(t0 = Time.local(2010, 8, 1)) - schedule.add_recurrence_rule Rule.weekly.day(:monday) - schedule.first(3).should == [ - Time.local(2010, 8, 2), - Time.local(2010, 8, 9), - Time.local(2010, 8, 16) - ] - end + it 'should produce the correct days for @interval = 2, regardless of the start week' do + schedule = IceCube::Schedule.new(t0 = WEDNESDAY + IceCube::ONE_WEEK) + schedule.add_recurrence_rule IceCube::Rule.weekly(2).day(:wednesday) + #check assumption + times = schedule.occurrences(t0 + 3 * IceCube::ONE_WEEK) + times.should == [t0, t0 + 2 * IceCube::ONE_WEEK] + end - it 'should start weekly rules on monday when monday is the week start' do - schedule = Schedule.new(t0 = Time.local(2012, 2, 7)) - schedule.add_recurrence_rule Rule.weekly(2, :monday).day(:tuesday, :sunday) - schedule.first(3).should == [ - Time.local(2012, 2, 7), - Time.local(2012, 2, 12), - Time.local(2012, 2, 21) - ] + it 'should occur every 2nd tuesday of a month' do + schedule = IceCube::Schedule.new(t0 = Time.now) + schedule.add_recurrence_rule IceCube::Rule.monthly.hour_of_day(11).day_of_week(:tuesday => [2]) + schedule.first(48).each do |d| + d.hour.should == 11 + d.wday.should == 2 end + end - it 'should start weekly rules on sunday by default' do - schedule = Schedule.new(t0 = Time.local(2012,2,7)) - schedule.add_recurrence_rule Rule.weekly(2).day(:tuesday, :sunday) - schedule.first(3).should == [ - Time.local(2012, 2, 7), - Time.local(2012, 2, 19), - Time.local(2012, 2, 21) - ] - end + it 'should be able to start on sunday but repeat on wednesdays' do + schedule = IceCube::Schedule.new(t0 = Time.local(2010, 8, 1)) + schedule.add_recurrence_rule IceCube::Rule.weekly.day(:monday) + schedule.first(3).should == [ + Time.local(2010, 8, 2), + Time.local(2010, 8, 9), + Time.local(2010, 8, 16) + ] + end - it 'should validate week_start input' do - expect { Rule.weekly(2, :someday) }.to raise_error - end + it 'should start weekly rules on monday when monday is the week start' do + schedule = IceCube::Schedule.new(t0 = Time.local(2012, 2, 7)) + schedule.add_recurrence_rule IceCube::Rule.weekly(2, :monday).day(:tuesday, :sunday) + schedule.first(3).should == [ + Time.local(2012, 2, 7), + Time.local(2012, 2, 12), + Time.local(2012, 2, 21) + ] + end + + it 'should start weekly rules on sunday by default' do + schedule = IceCube::Schedule.new(t0 = Time.local(2012,2,7)) + schedule.add_recurrence_rule IceCube::Rule.weekly(2).day(:tuesday, :sunday) + schedule.first(3).should == [ + Time.local(2012, 2, 7), + Time.local(2012, 2, 19), + Time.local(2012, 2, 21) + ] + end + it 'should validate week_start input' do + expect { IceCube::Rule.weekly(2, :someday) }.to raise_error end end diff --git a/spec/examples/yearly_rule_spec.rb b/spec/examples/yearly_rule_spec.rb index 35ce0b45..49ab1284 100644 --- a/spec/examples/yearly_rule_spec.rb +++ b/spec/examples/yearly_rule_spec.rb @@ -9,6 +9,14 @@ rule.next_time(t0 + 1, schedule, nil).should == t0 + 365.days end + it 'should not produce results for @interval = 0' do + start_date = Time.local(2010, 7, 12, 5, 0, 0) + schedule = IceCube::Schedule.new(start_date) + schedule.add_recurrence_rule IceCube::Rule.yearly(0).month_of_year(:april).day_of_week(:monday => [1, -1]) + #check assumption + schedule.occurrences(start_date + IceCube::TimeUtil.days_in_year(start_date) * IceCube::ONE_DAY).size.should == 0 + end + it 'should be able to specify complex yearly rules' do start_date = Time.local(2010, 7, 12, 5, 0, 0) schedule = IceCube::Schedule.new(start_date) diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 528a39f3..8ee68f1d 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -6,6 +6,7 @@ end require File.dirname(__FILE__) + '/../lib/ice_cube' +require 'active_support/core_ext' DAY = Time.utc(2010, 3, 1) WEDNESDAY = Time.utc(2010, 6, 23, 5, 0, 0)