From 423faefe5fd4955db153126f648a55caa656a482 Mon Sep 17 00:00:00 2001 From: andjosh Date: Tue, 5 Feb 2019 15:52:37 -0600 Subject: [PATCH 1/5] Add support for BYSETPOS (monthly and yearly frequency) In August 2016, @NicolasMarlier added BYSETPOS support (https://github.com/seejohnrun/ice_cube/pull/349). Then, in July 2018, @nehresma added a few small changes to run in modern Ruby and a more modern rspec (https://github.com/seejohnrun/ice_cube/pull/449). Then, in January 2019, @davidstosik and @k3rni suggested changes to reduce complexity. This incorporates all the above into a single diff. --- 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 | 2 + lib/ice_cube/validated_rule.rb | 3 +- .../validations/monthly_by_set_pos.rb | 73 ++++++++++++++++++ lib/ice_cube/validations/yearly_by_set_pos.rb | 74 +++++++++++++++++++ spec/examples/by_set_pos_spec.rb | 31 ++++++++ spec/examples/from_ical_spec.rb | 5 ++ 10 files changed, 193 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/lib/ice_cube.rb b/lib/ice_cube.rb index 4a38232e..c7c3339d 100644 --- a/lib/ice_cube.rb +++ b/lib/ice_cube.rb @@ -50,6 +50,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 c6b91a1a..92d9a78d 100644 --- a/lib/ice_cube/parsers/ical_parser.rb +++ b/lib/ice_cube/parsers/ical_parser.rb @@ -75,6 +75,7 @@ def self.rule_from_ical(ical) when 'BYYEARDAY' validations[:day_of_year] = value.split(',').map(&:to_i) when 'BYSETPOS' + params[:validations][:by_set_pos] = value.split(',').collect(&:to_i) else validations[name] = nil # invalid type end diff --git a/lib/ice_cube/rules/monthly_rule.rb b/lib/ice_cube/rules/monthly_rule.rb index 3e1307fb..d5518574 100644 --- a/lib/ice_cube/rules/monthly_rule.rb +++ b/lib/ice_cube/rules/monthly_rule.rb @@ -12,6 +12,7 @@ class MonthlyRule < ValidatedRule # include Validations::DayOfYear # n/a include Validations::MonthlyInterval + include Validations::MonthlyBySetPos def initialize(interval = 1) super diff --git a/lib/ice_cube/rules/yearly_rule.rb b/lib/ice_cube/rules/yearly_rule.rb index 3a18b0a6..832570c0 100644 --- a/lib/ice_cube/rules/yearly_rule.rb +++ b/lib/ice_cube/rules/yearly_rule.rb @@ -12,6 +12,7 @@ class YearlyRule < ValidatedRule include Validations::DayOfYear include Validations::YearlyInterval + include Validations::YearlyBySetPos def initialize(interval = 1) super diff --git a/lib/ice_cube/time_util.rb b/lib/ice_cube/time_util.rb index ff43399e..516a649f 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 diff --git a/lib/ice_cube/validated_rule.rb b/lib/ice_cube/validated_rule.rb index 69830fab..97055133 100644 --- a/lib/ice_cube/validated_rule.rb +++ b/lib/ice_cube/validated_rule.rb @@ -20,7 +20,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..a3ec2eff --- /dev/null +++ b/lib/ice_cube/validations/monthly_by_set_pos.rb @@ -0,0 +1,73 @@ +module IceCube + module Validations::MonthlyBySetPos + + def by_set_pos(*by_set_pos) + by_set_pos.flatten! + by_set_pos.each do |set_pos| + unless (-366..366).include(set_pos) && set_pos != 0 + 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, [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 = step_time.start_of_month + end_of_month = step_time.end_of_month + + new_schedule = IceCube::Schedule.new(step_time.last_month) do |s| + s.add_recurrence_rule IceCube::Rule.from_hash(rule.to_hash.except(:by_set_pos, :count, :util)) + end + + occurrences = new_schedule.occurrences_between(start_of_month, end_of_month) + 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..6715c36b --- /dev/null +++ b/lib/ice_cube/validations/yearly_by_set_pos.rb @@ -0,0 +1,74 @@ +module IceCube + module Validations::YearlyBySetPos + + def by_set_pos(*by_set_pos) + by_set_pos.flatten! + by_set_pos.each do |set_pos| + unless (-366..366).include(set_pos) && set_pos != 0 + 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, [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 = step_time.beginning_of_year + end_of_year = step_time.end_of_year + + new_schedule = IceCube::Schedule.new(step_time.last_year) do |s| + s.add_recurrence_rule IceCube::Rule.from_hash(rule.to_hash.except(:by_set_pos, :count, :util)) + 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..4af91c30 --- /dev/null +++ b/spec/examples/by_set_pos_spec.rb @@ -0,0 +1,31 @@ +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 12cf8bf8..b5716221 100644 --- a/spec/examples/from_ical_spec.rb +++ b/spec/examples/from_ical_spec.rb @@ -86,6 +86,11 @@ module IceCube expect(rule).to eq(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") + expect(rule).to eq(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")) From 9ec3b0654c5e9a338afb3cc2c124b3c5c87a9697 Mon Sep 17 00:00:00 2001 From: andjosh Date: Tue, 5 Feb 2019 16:03:10 -0600 Subject: [PATCH 2/5] Fix reference to include over range Rubocop recommends cover? instead. --- lib/ice_cube/validations/monthly_by_set_pos.rb | 2 +- lib/ice_cube/validations/yearly_by_set_pos.rb | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/ice_cube/validations/monthly_by_set_pos.rb b/lib/ice_cube/validations/monthly_by_set_pos.rb index a3ec2eff..d10cfe8d 100644 --- a/lib/ice_cube/validations/monthly_by_set_pos.rb +++ b/lib/ice_cube/validations/monthly_by_set_pos.rb @@ -4,7 +4,7 @@ module Validations::MonthlyBySetPos def by_set_pos(*by_set_pos) by_set_pos.flatten! by_set_pos.each do |set_pos| - unless (-366..366).include(set_pos) && set_pos != 0 + unless (-366..366).cover?(set_pos) && set_pos != 0 raise ArgumentError, "Expecting number in [-366, -1] or [1, 366], got #{set_pos} (#{by_set_pos})" end end diff --git a/lib/ice_cube/validations/yearly_by_set_pos.rb b/lib/ice_cube/validations/yearly_by_set_pos.rb index 6715c36b..410e2bfe 100644 --- a/lib/ice_cube/validations/yearly_by_set_pos.rb +++ b/lib/ice_cube/validations/yearly_by_set_pos.rb @@ -4,7 +4,7 @@ module Validations::YearlyBySetPos def by_set_pos(*by_set_pos) by_set_pos.flatten! by_set_pos.each do |set_pos| - unless (-366..366).include(set_pos) && set_pos != 0 + unless (-366..366).cover?(set_pos) && set_pos != 0 raise ArgumentError, "Expecting number in [-366, -1] or [1, 366], got #{set_pos} (#{by_set_pos})" end end From 65dca74cc24a7b8e54e4a6bc67a9aee3b7698c68 Mon Sep 17 00:00:00 2001 From: andjosh Date: Tue, 5 Feb 2019 16:13:47 -0600 Subject: [PATCH 3/5] Fix reference to start_of_month on Time (it's beginning_of_month) --- lib/ice_cube/time_util.rb | 2 -- lib/ice_cube/validations/monthly_by_set_pos.rb | 2 +- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/lib/ice_cube/time_util.rb b/lib/ice_cube/time_util.rb index 516a649f..ff43399e 100644 --- a/lib/ice_cube/time_util.rb +++ b/lib/ice_cube/time_util.rb @@ -1,7 +1,5 @@ require 'date' require 'time' -require 'active_support' -require 'active_support/core_ext' module IceCube module TimeUtil diff --git a/lib/ice_cube/validations/monthly_by_set_pos.rb b/lib/ice_cube/validations/monthly_by_set_pos.rb index d10cfe8d..012f0449 100644 --- a/lib/ice_cube/validations/monthly_by_set_pos.rb +++ b/lib/ice_cube/validations/monthly_by_set_pos.rb @@ -32,7 +32,7 @@ def dst_adjust? end def validate(step_time, schedule) - start_of_month = step_time.start_of_month + start_of_month = step_time.beginning_of_month end_of_month = step_time.end_of_month new_schedule = IceCube::Schedule.new(step_time.last_month) do |s| From 2f26348b91c6198056330dfcc72c3dc4ebb47750 Mon Sep 17 00:00:00 2001 From: andjosh Date: Tue, 5 Feb 2019 16:21:58 -0600 Subject: [PATCH 4/5] Use underlying prev_month/_year for greater ActuveSupport versions Ref: https://apidock.com/rails/DateAndTime/Calculations/prev_month --- lib/ice_cube/validations/monthly_by_set_pos.rb | 2 +- lib/ice_cube/validations/yearly_by_set_pos.rb | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/ice_cube/validations/monthly_by_set_pos.rb b/lib/ice_cube/validations/monthly_by_set_pos.rb index 012f0449..8cabf4dc 100644 --- a/lib/ice_cube/validations/monthly_by_set_pos.rb +++ b/lib/ice_cube/validations/monthly_by_set_pos.rb @@ -35,7 +35,7 @@ def validate(step_time, schedule) start_of_month = step_time.beginning_of_month end_of_month = step_time.end_of_month - new_schedule = IceCube::Schedule.new(step_time.last_month) do |s| + new_schedule = IceCube::Schedule.new(step_time.prev_month) do |s| s.add_recurrence_rule IceCube::Rule.from_hash(rule.to_hash.except(:by_set_pos, :count, :util)) end diff --git a/lib/ice_cube/validations/yearly_by_set_pos.rb b/lib/ice_cube/validations/yearly_by_set_pos.rb index 410e2bfe..47976290 100644 --- a/lib/ice_cube/validations/yearly_by_set_pos.rb +++ b/lib/ice_cube/validations/yearly_by_set_pos.rb @@ -35,7 +35,7 @@ def validate(step_time, schedule) start_of_year = step_time.beginning_of_year end_of_year = step_time.end_of_year - new_schedule = IceCube::Schedule.new(step_time.last_year) do |s| + new_schedule = IceCube::Schedule.new(step_time.prev_year) do |s| s.add_recurrence_rule IceCube::Rule.from_hash(rule.to_hash.except(:by_set_pos, :count, :util)) end From 8d9d2ec86a372cb3dd159731c23d7a175fef6b2d Mon Sep 17 00:00:00 2001 From: andjosh Date: Tue, 5 Feb 2019 16:42:23 -0600 Subject: [PATCH 5/5] Fix reference to Hash#except This is not available within the currently supported Rails versions Ref: https://apidock.com/rails/Hash/except --- lib/ice_cube/validations/monthly_by_set_pos.rb | 2 +- lib/ice_cube/validations/yearly_by_set_pos.rb | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/ice_cube/validations/monthly_by_set_pos.rb b/lib/ice_cube/validations/monthly_by_set_pos.rb index 8cabf4dc..b8d12cb3 100644 --- a/lib/ice_cube/validations/monthly_by_set_pos.rb +++ b/lib/ice_cube/validations/monthly_by_set_pos.rb @@ -36,7 +36,7 @@ def validate(step_time, schedule) end_of_month = step_time.end_of_month new_schedule = IceCube::Schedule.new(step_time.prev_month) do |s| - s.add_recurrence_rule IceCube::Rule.from_hash(rule.to_hash.except(:by_set_pos, :count, :util)) + 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_month, end_of_month) diff --git a/lib/ice_cube/validations/yearly_by_set_pos.rb b/lib/ice_cube/validations/yearly_by_set_pos.rb index 47976290..c3c0b311 100644 --- a/lib/ice_cube/validations/yearly_by_set_pos.rb +++ b/lib/ice_cube/validations/yearly_by_set_pos.rb @@ -36,7 +36,7 @@ def validate(step_time, schedule) end_of_year = step_time.end_of_year new_schedule = IceCube::Schedule.new(step_time.prev_year) do |s| - s.add_recurrence_rule IceCube::Rule.from_hash(rule.to_hash.except(:by_set_pos, :count, :util)) + 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)