From 5b7d88b460dbfdb35be99d5d36f160dd7bfae039 Mon Sep 17 00:00:00 2001 From: Nicolas Marlier Date: Tue, 23 Aug 2016 12:47:23 +0200 Subject: [PATCH] Support BYSETPOS for MONTHLY AND YEARLY freq --- .gitignore | 1 + lib/ice_cube.rb | 3 + lib/ice_cube/parsers/ical_parser.rb | 1 + lib/ice_cube/rules/monthly_rule.rb | 1 + lib/ice_cube/rules/yearly_rule.rb | 1 + lib/ice_cube/time_util.rb | 32 +++++++ lib/ice_cube/validated_rule.rb | 3 +- .../validations/monthly_by_set_pos.rb | 87 ++++++++++++++++++ lib/ice_cube/validations/yearly_by_set_pos.rb | 89 +++++++++++++++++++ spec/examples/by_set_pos_spec.rb | 29 ++++++ spec/examples/from_ical_spec.rb | 5 ++ 11 files changed, 251 insertions(+), 1 deletion(-) create mode 100644 lib/ice_cube/validations/monthly_by_set_pos.rb create mode 100644 lib/ice_cube/validations/yearly_by_set_pos.rb create mode 100644 spec/examples/by_set_pos_spec.rb diff --git a/.gitignore b/.gitignore index d3133a1a..01fbbf51 100644 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,4 @@ coverage coverage.data *.gem .bundle +.idea diff --git a/lib/ice_cube.rb b/lib/ice_cube.rb index cd1d4015..9a8b1fb8 100644 --- a/lib/ice_cube.rb +++ b/lib/ice_cube.rb @@ -52,6 +52,9 @@ module Validations autoload :YearlyInterval, 'ice_cube/validations/yearly_interval' autoload :HourlyInterval, 'ice_cube/validations/hourly_interval' + autoload :MonthlyBySetPos, 'ice_cube/validations/monthly_by_set_pos' + autoload :YearlyBySetPos, 'ice_cube/validations/yearly_by_set_pos' + autoload :HourOfDay, 'ice_cube/validations/hour_of_day' autoload :MonthOfYear, 'ice_cube/validations/month_of_year' autoload :MinuteOfHour, 'ice_cube/validations/minute_of_hour' diff --git a/lib/ice_cube/parsers/ical_parser.rb b/lib/ice_cube/parsers/ical_parser.rb index 2e1e2f05..e9a2015f 100644 --- a/lib/ice_cube/parsers/ical_parser.rb +++ b/lib/ice_cube/parsers/ical_parser.rb @@ -68,6 +68,7 @@ def self.rule_from_ical(ical) when 'BYYEARDAY' params[:validations][:day_of_year] = value.split(',').collect(&:to_i) when 'BYSETPOS' + params[:validations][:by_set_pos] = value.split(',').collect(&:to_i) else raise "Invalid or unsupported rrule command: #{name}" end diff --git a/lib/ice_cube/rules/monthly_rule.rb b/lib/ice_cube/rules/monthly_rule.rb index cdce2a24..7744b53c 100644 --- a/lib/ice_cube/rules/monthly_rule.rb +++ b/lib/ice_cube/rules/monthly_rule.rb @@ -3,6 +3,7 @@ module IceCube class MonthlyRule < ValidatedRule include Validations::MonthlyInterval + include Validations::MonthlyBySetPos def initialize(interval = 1, week_start = :sunday) super diff --git a/lib/ice_cube/rules/yearly_rule.rb b/lib/ice_cube/rules/yearly_rule.rb index 7368004a..de7ca3b2 100644 --- a/lib/ice_cube/rules/yearly_rule.rb +++ b/lib/ice_cube/rules/yearly_rule.rb @@ -3,6 +3,7 @@ module IceCube class YearlyRule < ValidatedRule include Validations::YearlyInterval + include Validations::YearlyBySetPos def initialize(interval = 1, week_start = :sunday) super diff --git a/lib/ice_cube/time_util.rb b/lib/ice_cube/time_util.rb index bbb5bbb1..e615c5c6 100644 --- a/lib/ice_cube/time_util.rb +++ b/lib/ice_cube/time_util.rb @@ -1,5 +1,7 @@ require 'date' require 'time' +require 'active_support' +require 'active_support/core_ext' module IceCube module TimeUtil @@ -180,6 +182,36 @@ def self.which_occurrence_in_month(time, wday) [nth_occurrence_of_weekday, this_weekday_in_month_count] end + # Use Activesupport CoreExt functions to manipulate time + def self.start_of_month time + time.beginning_of_month + end + + # Use Activesupport CoreExt functions to manipulate time + def self.end_of_month time + time.end_of_month + end + + # Use Activesupport CoreExt functions to manipulate time + def self.start_of_year time + time.beginning_of_year + end + + # Use Activesupport CoreExt functions to manipulate time + def self.end_of_year time + time.end_of_year + end + + # Use Activesupport CoreExt functions to manipulate time + def self.previous_month time + time - 1.month + end + + # Use Activesupport CoreExt functions to manipulate time + def self.previous_year time + time - 1.year + end + # Get the days in the month for +time def self.days_in_month(time) date = Date.new(time.year, time.month, 1) diff --git a/lib/ice_cube/validated_rule.rb b/lib/ice_cube/validated_rule.rb index 6dfae137..b199ff0b 100644 --- a/lib/ice_cube/validated_rule.rb +++ b/lib/ice_cube/validated_rule.rb @@ -27,7 +27,8 @@ class ValidatedRule < Rule :base_sec, :base_min, :base_day, :base_hour, :base_month, :base_wday, :day_of_year, :second_of_minute, :minute_of_hour, :day_of_month, :hour_of_day, :month_of_year, :day_of_week, - :interval + :interval, + :by_set_pos ] attr_reader :validations diff --git a/lib/ice_cube/validations/monthly_by_set_pos.rb b/lib/ice_cube/validations/monthly_by_set_pos.rb new file mode 100644 index 00000000..dc87deaf --- /dev/null +++ b/lib/ice_cube/validations/monthly_by_set_pos.rb @@ -0,0 +1,87 @@ +module IceCube + + module Validations::MonthlyBySetPos + + def by_set_pos(*by_set_pos) + return by_set_pos([by_set_pos]) if by_set_pos.is_a?(Fixnum) + + unless by_set_pos.nil? || by_set_pos.is_a?(Array) + raise ArgumentError, "Expecting Array or nil value for count, got #{by_set_pos.inspect}" + end + by_set_pos.flatten! + by_set_pos.each do |set_pos| + unless (set_pos >= -366 && set_pos <= -1) || + (set_pos <= 366 && set_pos >= 1) + raise ArgumentError, "Expecting number in [-366, -1] or [1, 366], got #{set_pos} (#{by_set_pos})" + end + end + + @by_set_pos = by_set_pos + replace_validations_for(:by_set_pos, by_set_pos && [Validation.new(by_set_pos, self)]) + self + end + + class Validation + + attr_reader :rule, :by_set_pos + + def initialize(by_set_pos, rule) + + @by_set_pos = by_set_pos + @rule = rule + end + + def type + :day + end + + def dst_adjust? + true + end + + def validate(step_time, schedule) + start_of_month = TimeUtil.start_of_month step_time + end_of_month = TimeUtil.end_of_month step_time + + + new_schedule = IceCube::Schedule.new(TimeUtil.previous_month(step_time)) do |s| + s.add_recurrence_rule IceCube::Rule.from_hash(rule.to_hash.reject{|k, v| [:by_set_pos, :count, :until].include? k}) + end + + puts step_time + occurrences = new_schedule.occurrences_between(start_of_month, end_of_month) + p occurrences + index = occurrences.index(step_time) + if index == nil + 1 + else + positive_set_pos = index + 1 + negative_set_pos = index - occurrences.length + + if @by_set_pos.include?(positive_set_pos) || @by_set_pos.include?(negative_set_pos) + 0 + else + 1 + end + end + end + + + def build_s(builder) + builder.piece(:by_set_pos) << by_set_pos + end + + def build_hash(builder) + builder[:by_set_pos] = by_set_pos + end + + def build_ical(builder) + builder['BYSETPOS'] << by_set_pos + end + + nil + end + + end + +end diff --git a/lib/ice_cube/validations/yearly_by_set_pos.rb b/lib/ice_cube/validations/yearly_by_set_pos.rb new file mode 100644 index 00000000..af629117 --- /dev/null +++ b/lib/ice_cube/validations/yearly_by_set_pos.rb @@ -0,0 +1,89 @@ +module IceCube + + module Validations::YearlyBySetPos + + def by_set_pos(*by_set_pos) + return by_set_pos([by_set_pos]) if by_set_pos.is_a?(Fixnum) + + unless by_set_pos.nil? || by_set_pos.is_a?(Array) + raise ArgumentError, "Expecting Array or nil value for count, got #{by_set_pos.inspect}" + end + by_set_pos.flatten! + by_set_pos.each do |set_pos| + unless (set_pos >= -366 && set_pos <= -1) || + (set_pos <= 366 && set_pos >= 1) + raise ArgumentError, "Expecting number in [-366, -1] or [1, 366], got #{set_pos} (#{by_set_pos})" + end + end + + @by_set_pos = by_set_pos + replace_validations_for(:by_set_pos, by_set_pos && [Validation.new(by_set_pos, self)]) + self + end + + class Validation + + attr_reader :rule, :by_set_pos + + def initialize(by_set_pos, rule) + + @by_set_pos = by_set_pos + @rule = rule + end + + def type + :day + end + + def dst_adjust? + true + end + + def validate(step_time, schedule) + start_of_year = TimeUtil.start_of_year step_time + end_of_year = TimeUtil.end_of_year step_time + + + new_schedule = IceCube::Schedule.new(TimeUtil.previous_year(step_time)) do |s| + s.add_recurrence_rule IceCube::Rule.from_hash(rule.to_hash.reject{|k, v| [:by_set_pos, :count, :until].include? k}) + end + + occurrences = new_schedule.occurrences_between(start_of_year, end_of_year) + + index = occurrences.index(step_time) + if index == nil + 1 + else + positive_set_pos = index + 1 + negative_set_pos = index - occurrences.length + + if @by_set_pos.include?(positive_set_pos) || @by_set_pos.include?(negative_set_pos) + 0 + else + 1 + end + end + + + + end + + + def build_s(builder) + builder.piece(:by_set_pos) << by_set_pos + end + + def build_hash(builder) + builder[:by_set_pos] = by_set_pos + end + + def build_ical(builder) + builder['BYSETPOS'] << by_set_pos + end + + nil + end + + end + +end diff --git a/spec/examples/by_set_pos_spec.rb b/spec/examples/by_set_pos_spec.rb new file mode 100644 index 00000000..c9d0a124 --- /dev/null +++ b/spec/examples/by_set_pos_spec.rb @@ -0,0 +1,29 @@ +require File.dirname(__FILE__) + '/../spec_helper' + +module IceCube + + describe MonthlyRule, 'BYSETPOS' do + it 'should behave correctly' do + schedule = IceCube::Schedule.from_ical "RRULE:FREQ=MONTHLY;COUNT=4;BYDAY=WE;BYSETPOS=4" + schedule.start_time = Time.new(2015, 5, 28, 12, 0, 0) + expect(schedule.occurrences_between(Time.new(2015, 01, 01), Time.new(2017, 01, 01))).to eq([ + Time.new(2015,6,24,12,0,0), + Time.new(2015,7,22,12,0,0), + Time.new(2015,8,26,12,0,0), + Time.new(2015,9,23,12,0,0) + ]) + end + + end + + describe YearlyRule, 'BYSETPOS' do + it 'should behave correctly' do + schedule = IceCube::Schedule.from_ical "RRULE:FREQ=YEARLY;BYMONTH=7;BYDAY=SU,MO,TU,WE,TH,FR,SA;BYSETPOS=-1" + schedule.start_time = Time.new(1966,7,5) + expect(schedule.occurrences_between(Time.new(2015, 01, 01), Time.new(2017, 01, 01))).to eq([ + Time.new(2015, 7, 31), + Time.new(2016, 7, 31) + ]) + end + end +end diff --git a/spec/examples/from_ical_spec.rb b/spec/examples/from_ical_spec.rb index a46cb3f1..adb72c20 100644 --- a/spec/examples/from_ical_spec.rb +++ b/spec/examples/from_ical_spec.rb @@ -86,6 +86,11 @@ module IceCube rule.should == IceCube::Rule.weekly(2, :monday) end + it 'should be able to parse by_set_pos start (BYSETPOS)' do + rule = IceCube::Rule.from_ical("FREQ=MONTHLY;BYDAY=MO,WE;BYSETPOS=-1,1") + rule.should == IceCube::Rule.monthly.day(:monday, :wednesday).by_set_pos([-1, 1]) + 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::Rule.from_ical("FREQ=DAILY;COUNT=5"))