From 04290ecc8a4afbe86e8a800d1fdfa9b9627e102a Mon Sep 17 00:00:00 2001 From: Nicolas Marlier Date: Tue, 23 Aug 2016 12:47:23 +0200 Subject: [PATCH 01/10] Support BYSETPOS for MONTHLY AND YEARLY freq --- .gitignore | 3 + 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, 253 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 8eb3b06b..64d30ba1 100644 --- a/.gitignore +++ b/.gitignore @@ -8,5 +8,8 @@ /spec/reports/ /tmp/ +# rubymine +.idea + # rspec failure tracking .rspec_status 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..bb11efcb 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 @@ -198,6 +200,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 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..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 12cf8bf8..1bbc0ceb 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") + 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")) From 51ea4a11c6eb46e45e599506ae1b6d0f0defd8fe Mon Sep 17 00:00:00 2001 From: Nathan Ehresman Date: Mon, 30 Jul 2018 16:03:35 -0400 Subject: [PATCH 02/10] Modernize BYSETPOS commit A few small updates to Nicolas Marlier's BYSETPOS support added in PR #349 --- lib/ice_cube/validations/monthly_by_set_pos.rb | 4 +--- lib/ice_cube/validations/yearly_by_set_pos.rb | 2 +- spec/examples/from_ical_spec.rb | 2 +- 3 files changed, 3 insertions(+), 5 deletions(-) diff --git a/lib/ice_cube/validations/monthly_by_set_pos.rb b/lib/ice_cube/validations/monthly_by_set_pos.rb index dc87deaf..3bc887be 100644 --- a/lib/ice_cube/validations/monthly_by_set_pos.rb +++ b/lib/ice_cube/validations/monthly_by_set_pos.rb @@ -3,7 +3,7 @@ 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) + return by_set_pos([by_set_pos]) if by_set_pos.is_a?(Integer) 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}" @@ -48,9 +48,7 @@ def validate(step_time, schedule) 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 diff --git a/lib/ice_cube/validations/yearly_by_set_pos.rb b/lib/ice_cube/validations/yearly_by_set_pos.rb index af629117..7ecb807e 100644 --- a/lib/ice_cube/validations/yearly_by_set_pos.rb +++ b/lib/ice_cube/validations/yearly_by_set_pos.rb @@ -3,7 +3,7 @@ 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) + return by_set_pos([by_set_pos]) if by_set_pos.is_a?(Integer) 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}" diff --git a/spec/examples/from_ical_spec.rb b/spec/examples/from_ical_spec.rb index 1bbc0ceb..b5716221 100644 --- a/spec/examples/from_ical_spec.rb +++ b/spec/examples/from_ical_spec.rb @@ -88,7 +88,7 @@ module IceCube 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]) + 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 From dede2768ecbe75f9755e44c7bc14755dc3ca2f8c Mon Sep 17 00:00:00 2001 From: David Stosik Date: Fri, 4 Jan 2019 10:44:26 +0000 Subject: [PATCH 03/10] Remove .gitignore change unrelated to this branch --- .gitignore | 3 --- 1 file changed, 3 deletions(-) diff --git a/.gitignore b/.gitignore index 64d30ba1..8eb3b06b 100644 --- a/.gitignore +++ b/.gitignore @@ -8,8 +8,5 @@ /spec/reports/ /tmp/ -# rubymine -.idea - # rspec failure tracking .rspec_status From 7d5bc51b6f0af08272a44ed233c64a6c13f60722 Mon Sep 17 00:00:00 2001 From: David Stosik Date: Fri, 4 Jan 2019 11:06:19 +0000 Subject: [PATCH 04/10] Be more specific when including ActiveSupport core extensions --- lib/ice_cube/time_util.rb | 32 ------------------- .../validations/monthly_by_set_pos.rb | 11 ++++--- lib/ice_cube/validations/yearly_by_set_pos.rb | 11 ++++--- 3 files changed, 14 insertions(+), 40 deletions(-) diff --git a/lib/ice_cube/time_util.rb b/lib/ice_cube/time_util.rb index bb11efcb..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 @@ -200,36 +198,6 @@ 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/validations/monthly_by_set_pos.rb b/lib/ice_cube/validations/monthly_by_set_pos.rb index 3bc887be..58b79cac 100644 --- a/lib/ice_cube/validations/monthly_by_set_pos.rb +++ b/lib/ice_cube/validations/monthly_by_set_pos.rb @@ -1,3 +1,7 @@ +require "active_support/core_ext/date/calculations" +require "active_support/core_ext/time/calculations" +require "active_support/core_ext/date_time/calculations" + module IceCube module Validations::MonthlyBySetPos @@ -40,11 +44,10 @@ def dst_adjust? end def validate(step_time, schedule) - start_of_month = TimeUtil.start_of_month step_time - end_of_month = TimeUtil.end_of_month step_time - + start_of_month = step_time.beginning_of_month + end_of_month = step_time.end_of_month - new_schedule = IceCube::Schedule.new(TimeUtil.previous_month(step_time)) do |s| + new_schedule = IceCube::Schedule.new(step_time.last_month) do |s| s.add_recurrence_rule IceCube::Rule.from_hash(rule.to_hash.reject{|k, v| [:by_set_pos, :count, :until].include? k}) end diff --git a/lib/ice_cube/validations/yearly_by_set_pos.rb b/lib/ice_cube/validations/yearly_by_set_pos.rb index 7ecb807e..7130a744 100644 --- a/lib/ice_cube/validations/yearly_by_set_pos.rb +++ b/lib/ice_cube/validations/yearly_by_set_pos.rb @@ -1,3 +1,7 @@ +require "active_support/core_ext/date/calculations" +require "active_support/core_ext/time/calculations" +require "active_support/core_ext/date_time/calculations" + module IceCube module Validations::YearlyBySetPos @@ -40,11 +44,10 @@ def dst_adjust? end def validate(step_time, schedule) - start_of_year = TimeUtil.start_of_year step_time - end_of_year = TimeUtil.end_of_year step_time - + start_of_year = step_time.beginning_of_year + end_of_year = step_time.end_of_year - new_schedule = IceCube::Schedule.new(TimeUtil.previous_year(step_time)) do |s| + new_schedule = IceCube::Schedule.new(step_time.last_year) do |s| s.add_recurrence_rule IceCube::Rule.from_hash(rule.to_hash.reject{|k, v| [:by_set_pos, :count, :until].include? k}) end From 0d321394eb4a03f6edc753ad2e4439e5be891bac Mon Sep 17 00:00:00 2001 From: David Stosik Date: Fri, 4 Jan 2019 18:38:56 +0000 Subject: [PATCH 05/10] Refactor BYSETPOS validation to apply to all frequencies --- lib/ice_cube.rb | 3 +- lib/ice_cube/rules/monthly_rule.rb | 1 - lib/ice_cube/rules/yearly_rule.rb | 1 - lib/ice_cube/validated_rule.rb | 1 + lib/ice_cube/validations/by_set_pos.rb | 130 ++++++++++++++++++ .../validations/monthly_by_set_pos.rb | 88 ------------ lib/ice_cube/validations/yearly_by_set_pos.rb | 92 ------------- 7 files changed, 132 insertions(+), 184 deletions(-) create mode 100644 lib/ice_cube/validations/by_set_pos.rb delete mode 100644 lib/ice_cube/validations/monthly_by_set_pos.rb delete mode 100644 lib/ice_cube/validations/yearly_by_set_pos.rb diff --git a/lib/ice_cube.rb b/lib/ice_cube.rb index c7c3339d..e690c15f 100644 --- a/lib/ice_cube.rb +++ b/lib/ice_cube.rb @@ -50,8 +50,7 @@ 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 :BySetPos, 'ice_cube/validations/by_set_pos' autoload :HourOfDay, 'ice_cube/validations/hour_of_day' autoload :MonthOfYear, 'ice_cube/validations/month_of_year' diff --git a/lib/ice_cube/rules/monthly_rule.rb b/lib/ice_cube/rules/monthly_rule.rb index d5518574..3e1307fb 100644 --- a/lib/ice_cube/rules/monthly_rule.rb +++ b/lib/ice_cube/rules/monthly_rule.rb @@ -12,7 +12,6 @@ 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 832570c0..3a18b0a6 100644 --- a/lib/ice_cube/rules/yearly_rule.rb +++ b/lib/ice_cube/rules/yearly_rule.rb @@ -12,7 +12,6 @@ class YearlyRule < ValidatedRule include Validations::DayOfYear include Validations::YearlyInterval - include Validations::YearlyBySetPos def initialize(interval = 1) super diff --git a/lib/ice_cube/validated_rule.rb b/lib/ice_cube/validated_rule.rb index 97055133..f307a92b 100644 --- a/lib/ice_cube/validated_rule.rb +++ b/lib/ice_cube/validated_rule.rb @@ -8,6 +8,7 @@ class ValidatedRule < Rule include Validations::Count include Validations::Until + include Validations::BySetPos # Validations ordered for efficiency in sequence of: # * descending intervals diff --git a/lib/ice_cube/validations/by_set_pos.rb b/lib/ice_cube/validations/by_set_pos.rb new file mode 100644 index 00000000..95c8cba1 --- /dev/null +++ b/lib/ice_cube/validations/by_set_pos.rb @@ -0,0 +1,130 @@ +require "active_support/core_ext/date/calculations" +require "active_support/core_ext/date_time/calculations" +require "active_support/core_ext/time/calculations" + +require "active_support/core_ext/hash/except" + +module IceCube + + module Validations::BySetPos + + def by_set_pos(*bysplist) + bysplist.flatten.each do |set_pos_day| + unless set_pos_day.is_a?(Integer) && (-366..366).include?(set_pos_day) && set_pos_day != 0 + raise ArgumentError, "expecting Integer value in [-366, -1] or [1, 366] for setposday, got #{set_pos_day} (#{bysplist})" + end + + validations_for(:by_set_pos) << Validation.new(set_pos_day, self) + end + + self + end + + class Validation + + attr_reader :source_rule, :set_pos_day + + def initialize(set_pos_day, source_rule) + @set_pos_day = set_pos_day + @source_rule = source_rule + end + + def type + :day + end + + def dst_adjust? + true + end + + def validate(step_time, _start_time) + @step_time = step_time + + if step_time == occurrences_this_period[zero_indexed_position] + 0 + else + 1 + end + end + + def build_s(builder) + builder.piece(:by_set_pos) << set_pos_day + end + + def build_hash(builder) + builder.validations_array(:by_set_pos) << set_pos_day + end + + def build_ical(builder) + builder['BYSETPOS'] << set_pos_day + end + + private + + attr_reader :step_time + + def zero_indexed_position + if set_pos_day > 0 + set_pos_day - 1 + else + set_pos_day + end + end + + def occurrences_this_period + schedule_for_rule.occurrences_between( + beginning_of_period, + end_of_period + ) + end + + def period_type + case source_rule + when SecondlyRule + :second + when MinutelyRule + :minute + when HourlyRule + :hour + when DailyRule + :day + when WeeklyRule + :week + when MonthlyRule + :month + when YearlyRule + :year + end + end + + def beginning_of_period + step_time.public_send("beginning_of_#{period_type}") + end + + def end_of_period + step_time.public_send("end_of_#{period_type}") + end + + def last_period + step_time.public_send("last_#{period_type}") + end + + def schedule_for_rule + IceCube::Schedule.new(last_period) do |s| + s.add_recurrence_rule Rule.from_hash(rule_hash_for_all_occurrences) + end + end + + def rule_hash_for_all_occurrences + source_rule.to_hash.except(:count, :until).tap do |hash| + if hash[:validations] + hash[:validations].delete(:by_set_pos) + end + end + end + + end + + end + +end diff --git a/lib/ice_cube/validations/monthly_by_set_pos.rb b/lib/ice_cube/validations/monthly_by_set_pos.rb deleted file mode 100644 index 58b79cac..00000000 --- a/lib/ice_cube/validations/monthly_by_set_pos.rb +++ /dev/null @@ -1,88 +0,0 @@ -require "active_support/core_ext/date/calculations" -require "active_support/core_ext/time/calculations" -require "active_support/core_ext/date_time/calculations" - -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?(Integer) - - 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 = step_time.beginning_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.reject{|k, v| [:by_set_pos, :count, :until].include? k}) - 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 deleted file mode 100644 index 7130a744..00000000 --- a/lib/ice_cube/validations/yearly_by_set_pos.rb +++ /dev/null @@ -1,92 +0,0 @@ -require "active_support/core_ext/date/calculations" -require "active_support/core_ext/time/calculations" -require "active_support/core_ext/date_time/calculations" - -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?(Integer) - - 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 = 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.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 From 39bc6fe02e207626e8b133f9ce2a33aa131c0d73 Mon Sep 17 00:00:00 2001 From: David Stosik Date: Sat, 5 Jan 2019 18:03:59 +0000 Subject: [PATCH 06/10] Refactor BYSETPOS module to be more generic and more readable --- lib/ice_cube/rule.rb | 8 +++++ lib/ice_cube/validations/by_set_pos.rb | 46 +++++++++++++++----------- spec/examples/from_ical_spec.rb | 40 ++++++++++++++++++++++ spec/examples/rfc_spec.rb | 18 ++++++++++ 4 files changed, 93 insertions(+), 19 deletions(-) diff --git a/lib/ice_cube/rule.rb b/lib/ice_cube/rule.rb index c8cd992c..944c6f37 100644 --- a/lib/ice_cube/rule.rb +++ b/lib/ice_cube/rule.rb @@ -58,6 +58,10 @@ def on?(time, schedule) next_time(time, schedule, time).to_i == time.to_i end + def interval_type + self.class.interval_type + end + class << self # Convert from a hash and create a rule @@ -90,6 +94,10 @@ def from_hash(original_hash) rule end + def interval_type + @_interval_type ||= self.name.split('::').last.sub(/Rule$/, '').downcase + end + private def apply_validation(rule, name, args) diff --git a/lib/ice_cube/validations/by_set_pos.rb b/lib/ice_cube/validations/by_set_pos.rb index 95c8cba1..50dd1d4b 100644 --- a/lib/ice_cube/validations/by_set_pos.rb +++ b/lib/ice_cube/validations/by_set_pos.rb @@ -78,35 +78,43 @@ def occurrences_this_period ) end - def period_type - case source_rule - when SecondlyRule - :second - when MinutelyRule - :minute - when HourlyRule - :hour - when DailyRule - :day - when WeeklyRule - :week - when MonthlyRule - :month - when YearlyRule - :year + def interval_type + if source_rule.interval_type == 'daily' + 'day' + else + source_rule.interval_type.sub(/ly$/, '') end end def beginning_of_period - step_time.public_send("beginning_of_#{period_type}") + if interval_type == 'second' + fail 'boo' + else + step_time.public_send("beginning_of_#{interval_type}") + end end def end_of_period - step_time.public_send("end_of_#{period_type}") + if interval_type == 'second' + fail 'boo' + else + step_time.public_send("end_of_#{interval_type}") + end end def last_period - step_time.public_send("last_#{period_type}") + case interval_type + when 'second' + fail 'boo' + when 'minute' + step_time - ONE_MINUTE + when 'hour' + step_time - ONE_HOUR + when 'day' + step_time.yesterday + when 'week', 'month', 'year' + step_time.public_send("last_#{interval_type}") + end end def schedule_for_rule diff --git a/spec/examples/from_ical_spec.rb b/spec/examples/from_ical_spec.rb index b5716221..c76fb5b8 100644 --- a/spec/examples/from_ical_spec.rb +++ b/spec/examples/from_ical_spec.rb @@ -190,6 +190,16 @@ def sorted_ical(ical) expect(sorted_ical(IceCube::Schedule.from_ical(ical).to_ical)).to eq(sorted_ical(ical)) end + it 'handles bysetpos' do + start_time = Time.now + + schedule = IceCube::Schedule.new(start_time) + schedule.add_recurrence_rule(IceCube::Rule.weekly.day(:monday).by_set_pos(1, -1)) + + ical = schedule.to_ical + expect(sorted_ical(IceCube::Schedule.from_ical(ical).to_ical)).to eq(sorted_ical(ical)) + end + end describe 'weekly frequency' do @@ -242,6 +252,16 @@ def sorted_ical(ical) ical = schedule.to_ical expect(sorted_ical(IceCube::Schedule.from_ical(ical).to_ical)).to eq(sorted_ical(ical)) end + + it 'handles bysetpos' do + start_time = Time.now + + schedule = IceCube::Schedule.new(start_time) + schedule.add_recurrence_rule(IceCube::Rule.weekly.day(:monday).by_set_pos(1, -1)) + + ical = schedule.to_ical + expect(sorted_ical(IceCube::Schedule.from_ical(ical).to_ical)).to eq(sorted_ical(ical)) + end end describe 'monthly frequency' do @@ -284,6 +304,16 @@ def sorted_ical(ical) ical = schedule.to_ical expect(sorted_ical(IceCube::Schedule.from_ical(ical).to_ical)).to eq(sorted_ical(ical)) end + + it 'handles bysetpos' do + start_time = Time.now + + schedule = IceCube::Schedule.new(start_time) + schedule.add_recurrence_rule(IceCube::Rule.monthly.day(:monday).by_set_pos(1, -1)) + + ical = schedule.to_ical + expect(sorted_ical(IceCube::Schedule.from_ical(ical).to_ical)).to eq(sorted_ical(ical)) + end end describe 'yearly frequency' do @@ -337,6 +367,16 @@ def sorted_ical(ical) expect(sorted_ical(IceCube::Schedule.from_ical(ical).to_ical)).to eq(sorted_ical(ical)) end + it 'handles bysetpos' do + start_time = Time.now + + schedule = IceCube::Schedule.new(start_time) + schedule.add_recurrence_rule(IceCube::Rule.yearly.month_of_year(:february).day(:monday).by_set_pos(1, -1)) + + ical = schedule.to_ical + expect(sorted_ical(IceCube::Schedule.from_ical(ical).to_ical)).to eq(sorted_ical(ical)) + end + it 'handles specific months' do start_time = Time.now diff --git a/spec/examples/rfc_spec.rb b/spec/examples/rfc_spec.rb index b7f4cfdc..4c6e1ebb 100644 --- a/spec/examples/rfc_spec.rb +++ b/spec/examples/rfc_spec.rb @@ -325,6 +325,24 @@ expect(dates).to eq(expecation) end + it 'should ~ third instance into the month of one of Tuesday, Wednesday, or Thursday, for the next 3 months' do + start_time = Time.utc(1997, 9, 4, 9, 0, 0) + schedule = IceCube::Schedule.new(start_time) + schedule.add_recurrence_rule IceCube::Rule.monthly.count(3).day(:tuesday , :wednesday , :thursday).by_set_pos(3) + dates = schedule.all_occurrences + expectation = [Time.utc(1997, 9, 4, 9), Time.utc(1997, 10, 7, 9), Time.utc(1997, 11, 6, 9)] + expect(dates).to eq(expectation) + end + + it 'should ~ second-to-last weekday of the month' do + start_time = Time.utc(1997, 9, 29, 9, 0, 0) + schedule = IceCube::Schedule.new(start_time) + schedule.add_recurrence_rule IceCube::Rule.monthly.day(:monday, :tuesday , :wednesday , :thursday, :friday).by_set_pos(-2) + next_dates = schedule.occurrences(Time.utc(1997, 12, 31)) + expectation = [Time.utc(1997, 9, 29, 9), Time.utc(1997, 10, 30, 9), Time.utc(1997, 11, 27, 9), Time.utc(1997, 12, 30, 9)] + expect(next_dates).to eq(expectation) + end + end def test_expectations(schedule, dates_array) From 638732ec2bfb0cdffe2d4324f411472fc20afcc2 Mon Sep 17 00:00:00 2001 From: David Stosik Date: Wed, 6 Feb 2019 10:36:43 +0900 Subject: [PATCH 07/10] Remove warning about unused variable --- lib/ice_cube/parsers/ical_parser.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/ice_cube/parsers/ical_parser.rb b/lib/ice_cube/parsers/ical_parser.rb index 92d9a78d..bac7e121 100644 --- a/lib/ice_cube/parsers/ical_parser.rb +++ b/lib/ice_cube/parsers/ical_parser.rb @@ -4,7 +4,7 @@ def self.schedule_from_ical(ical_string, options = {}) data = {} ical_string.each_line do |line| (property, value) = line.split(':') - (property, tzid) = property.split(';') + (property, _tzid) = property.split(';') case property when 'DTSTART' data[:start_time] = TimeUtil.deserialize_time(value) From 41dce1f2ebf8d2e0bf8df563d53b70afe176979c Mon Sep 17 00:00:00 2001 From: David Stosik Date: Sat, 5 Jan 2019 18:34:51 +0000 Subject: [PATCH 08/10] Refactor spec for readability --- spec/examples/by_set_pos_spec.rb | 24 +++++++++++++----------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/spec/examples/by_set_pos_spec.rb b/spec/examples/by_set_pos_spec.rb index c9d0a124..f8bfc931 100644 --- a/spec/examples/by_set_pos_spec.rb +++ b/spec/examples/by_set_pos_spec.rb @@ -6,12 +6,13 @@ module IceCube 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) - ]) + expectations = [ + 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) + ] + expect(schedule.occurrences(Time.new(2017, 01, 01))).to eq(expectations) end end @@ -19,11 +20,12 @@ module IceCube 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) - ]) + schedule.start_time = Time.new(1966, 7, 5) + expectations = [ + Time.new(2015, 7, 31), + Time.new(2016, 7, 31) + ] + expect(schedule.occurrences_between(Time.new(2015, 01, 01), Time.new(2017, 01, 01))).to eq(expectations) end end end From e5060379026939d039629995925446629952b814 Mon Sep 17 00:00:00 2001 From: David Stosik Date: Wed, 6 Feb 2019 10:24:10 +0900 Subject: [PATCH 09/10] Add extra spec (currently fails) --- spec/examples/yearly_rule_spec.rb | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/spec/examples/yearly_rule_spec.rb b/spec/examples/yearly_rule_spec.rb index fe725e65..e4375871 100644 --- a/spec/examples/yearly_rule_spec.rb +++ b/spec/examples/yearly_rule_spec.rb @@ -77,6 +77,14 @@ expect(schedule.occurrences(Time.utc(2010, 12, 31))).to eq days_of_year end + it 'should produce the correct days for @interval = 1 when you specify week days' do + start_time = Time.utc(2010, 1, 1) + schedule = IceCube::Schedule.new(start_time) + schedule.add_recurrence_rule IceCube::Rule.yearly.day(:monday) + + expect(schedule.occurrences(Time.utc(2010, 12, 31)).count).to eq 52 + end + it 'should produce the correct days for @interval = 1 when you specify negative days' do schedule = IceCube::Schedule.new(Time.utc(2010, 1, 1)) schedule.add_recurrence_rule IceCube::Rule.yearly.day_of_year(100, -1) From 007eeea9c50ec87d6dc65ffacc1429e08eeb7805 Mon Sep 17 00:00:00 2001 From: David Stosik Date: Wed, 6 Feb 2019 10:51:30 +0900 Subject: [PATCH 10/10] Try to fix Travis for old Ruby versions Inspired of https://github.com/tzinfo/tzinfo/commit/ea78810555f812ab41b476102e7c9d067fb26a45 --- .travis.yml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 45d99a55..be216d88 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,7 +1,10 @@ sudo: false language: ruby before_install: - - gem install bundler + - if [[ $TRAVIS_RUBY_VERSION =~ ^(1|2\.[012]|jruby-1)\. ]]; then gem install rubygems-update --version '~> 2.7' --no-document && update_rubygems; elif [[ ! $TRAVIS_RUBY_VERSION =~ ^truffleruby- ]]; then gem update --system; fi + - gem --version + - if [[ $TRAVIS_RUBY_VERSION =~ ^(1|2\.[012]|jruby-1)\. ]]; then gem install bundler --version '~> 1.17'; else gem install bundler; fi + - bundle --version notifications: email: - john.crepezzi@gmail.com