From 136faf0a2b7a5f5fc4b2b43281b608f7e9a603a0 Mon Sep 17 00:00:00 2001 From: Nicolas Marlier Date: Tue, 23 Aug 2016 12:47:23 +0200 Subject: [PATCH 01/22] Support BYSETPOS for MONTHLY AND YEARLY freq --- .gitignore | 3 + lib/ice_cube.rb | 3 + lib/ice_cube/parsers/ical_parser.rb | 2 +- lib/ice_cube/rules/monthly_rule.rb | 1 + lib/ice_cube/rules/yearly_rule.rb | 1 + lib/ice_cube/time_util.rb | 36 +++++++- 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 | 7 +- 11 files changed, 256 insertions(+), 5 deletions(-) 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 3c283f53..5ef72e4a 100644 --- a/lib/ice_cube.rb +++ b/lib/ice_cube.rb @@ -49,6 +49,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 429bd84c..3b00fa1a 100644 --- a/lib/ice_cube/parsers/ical_parser.rb +++ b/lib/ice_cube/parsers/ical_parser.rb @@ -75,7 +75,7 @@ def self.rule_from_ical(ical) when "BYYEARDAY" validations[:day_of_year] = value.split(",").map(&:to_i) when "BYSETPOS" - # noop + 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 6aadf5e7..9decdbca 100644 --- a/lib/ice_cube/rules/monthly_rule.rb +++ b/lib/ice_cube/rules/monthly_rule.rb @@ -10,6 +10,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 d92148c1..0ebb3057 100644 --- a/lib/ice_cube/rules/yearly_rule.rb +++ b/lib/ice_cube/rules/yearly_rule.rb @@ -10,6 +10,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 001d927d..b6669573 100644 --- a/lib/ice_cube/time_util.rb +++ b/lib/ice_cube/time_util.rb @@ -1,5 +1,7 @@ -require "date" -require "time" +require 'date' +require 'time' +require 'active_support' +require 'active_support/core_ext' module IceCube module TimeUtil @@ -193,6 +195,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 068356ea..faa5ee95 100644 --- a/lib/ice_cube/validated_rule.rb +++ b/lib/ice_cube/validated_rule.rb @@ -18,7 +18,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 2ab66c3c..74236611 100644 --- a/spec/examples/from_ical_spec.rb +++ b/spec/examples/from_ical_spec.rb @@ -84,7 +84,12 @@ module IceCube expect(rule).to eq(IceCube::Rule.weekly(2, :monday)) end - it "should return no occurrences after daily interval with count is over" do + 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")) expect(schedule.occurrences_between(Time.now + (IceCube::ONE_DAY * 7), Time.now + (IceCube::ONE_DAY * 14)).count).to eq(0) From 84120b5616ac802d05e35f233630d9cd45dff94b Mon Sep 17 00:00:00 2001 From: Nathan Ehresman Date: Mon, 30 Jul 2018 16:03:35 -0400 Subject: [PATCH 02/22] 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 74236611..2031b334 100644 --- a/spec/examples/from_ical_spec.rb +++ b/spec/examples/from_ical_spec.rb @@ -86,7 +86,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 115d5a8afb89e943b12208e3f5d5d99a83c36029 Mon Sep 17 00:00:00 2001 From: Nathan Ehresman Date: Mon, 12 Dec 2022 11:36:18 -0500 Subject: [PATCH 03/22] address the spec DST sensitivity in .to_yaml round trips --- spec/examples/to_yaml_spec.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/spec/examples/to_yaml_spec.rb b/spec/examples/to_yaml_spec.rb index e7c62c59..7166110d 100644 --- a/spec/examples/to_yaml_spec.rb +++ b/spec/examples/to_yaml_spec.rb @@ -78,7 +78,7 @@ module IceCube end it "should be able to make a round-trip to YAML with .day_of_year" do - schedule1 = Schedule.new(Time.now) + schedule1 = Schedule.new(Time.zone.now) schedule1.add_recurrence_rule Rule.yearly.day_of_year(100, 200) yaml_string = schedule1.to_yaml @@ -112,7 +112,7 @@ module IceCube end it "should be able to make a round-trip to YAML with .month_of_year" do - schedule = Schedule.new(Time.now) + schedule = Schedule.new(Time.zone.now) schedule.add_recurrence_rule Rule.yearly.month_of_year(:april, :may) yaml_string = schedule.to_yaml From b3042c6d6fe5229bdd183251c745456fa5217e4e Mon Sep 17 00:00:00 2001 From: Nathan Ehresman Date: Mon, 12 Dec 2022 12:29:02 -0500 Subject: [PATCH 04/22] update PR from feedback rebased against master -- its been 4 years --- .gitignore | 3 -- lib/ice_cube/time_util.rb | 30 ------------------- .../validations/monthly_by_set_pos.rb | 22 ++++---------- lib/ice_cube/validations/yearly_by_set_pos.rb | 25 ++++------------ spec/examples/by_set_pos_spec.rb | 24 +++++++-------- spec/examples/from_ical_spec.rb | 14 +++++++++ 6 files changed, 38 insertions(+), 80 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 diff --git a/lib/ice_cube/time_util.rb b/lib/ice_cube/time_util.rb index b6669573..3e914799 100644 --- a/lib/ice_cube/time_util.rb +++ b/lib/ice_cube/time_util.rb @@ -195,36 +195,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..a26d5e24 100644 --- a/lib/ice_cube/validations/monthly_by_set_pos.rb +++ b/lib/ice_cube/validations/monthly_by_set_pos.rb @@ -1,23 +1,16 @@ 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) + 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, by_set_pos && [Validation.new(by_set_pos, self)]) + replace_validations_for(:by_set_pos, [Validation.new(by_set_pos, self)]) self end @@ -26,7 +19,6 @@ class Validation attr_reader :rule, :by_set_pos def initialize(by_set_pos, rule) - @by_set_pos = by_set_pos @rule = rule end @@ -40,12 +32,11 @@ 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| - s.add_recurrence_rule IceCube::Rule.from_hash(rule.to_hash.reject{|k, v| [:by_set_pos, :count, :until].include? k}) + new_schedule = IceCube::Schedule.new(step_time - 1.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) @@ -64,7 +55,6 @@ def validate(step_time, schedule) end end - def build_s(builder) builder.piece(:by_set_pos) << by_set_pos 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..e1275b67 100644 --- a/lib/ice_cube/validations/yearly_by_set_pos.rb +++ b/lib/ice_cube/validations/yearly_by_set_pos.rb @@ -3,21 +3,15 @@ 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) + 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, by_set_pos && [Validation.new(by_set_pos, self)]) + replace_validations_for(:by_set_pos, [Validation.new(by_set_pos, self)]) self end @@ -40,12 +34,11 @@ 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| - s.add_recurrence_rule IceCube::Rule.from_hash(rule.to_hash.reject{|k, v| [:by_set_pos, :count, :until].include? k}) + new_schedule = IceCube::Schedule.new(step_time - 1.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) @@ -63,12 +56,8 @@ def validate(step_time, schedule) 1 end end - - - end - def build_s(builder) builder.piece(:by_set_pos) << by_set_pos end @@ -83,7 +72,5 @@ def build_ical(builder) nil end - end - end diff --git a/spec/examples/by_set_pos_spec.rb b/spec/examples/by_set_pos_spec.rb index c9d0a124..ee0eafcf 100644 --- a/spec/examples/by_set_pos_spec.rb +++ b/spec/examples/by_set_pos_spec.rb @@ -1,29 +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) - ]) + 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) - ]) + 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 2031b334..9cb685bf 100644 --- a/spec/examples/from_ical_spec.rb +++ b/spec/examples/from_ical_spec.rb @@ -89,6 +89,20 @@ module IceCube expect(rule).to eq(IceCube::Rule.monthly.day(:monday, :wednesday).by_set_pos([-1, 1])) end + it 'should raise when by_set_pos is out of range (BYSETPOS)' do + expect { + IceCube::Rule.from_ical("FREQ=MONTHLY;BYDAY=MO,WE;BYSETPOS=-367") + }.to raise_error(/Expecting number in \[-366, -1\] or \[1, 366\]/) + + expect { + IceCube::Rule.from_ical("FREQ=MONTHLY;BYDAY=MO,WE;BYSETPOS=367") + }.to raise_error(/Expecting number in \[-366, -1\] or \[1, 366\]/) + + expect { + IceCube::Rule.from_ical("FREQ=MONTHLY;BYDAY=MO,WE;BYSETPOS=0") + }.to raise_error(/Expecting number in \[-366, -1\] or \[1, 366\]/) + 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 6dd602d2c006f1651925354beb736d00cbdff5eb Mon Sep 17 00:00:00 2001 From: Nathan Ehresman Date: Wed, 21 Dec 2022 15:21:10 -0500 Subject: [PATCH 05/22] excluding until, not util --- 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 a26d5e24..111255c0 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 - 1.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.except(:by_set_pos, :count, :until))) 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 e1275b67..82f90349 100644 --- a/lib/ice_cube/validations/yearly_by_set_pos.rb +++ b/lib/ice_cube/validations/yearly_by_set_pos.rb @@ -38,7 +38,7 @@ def validate(step_time, schedule) end_of_year = step_time.end_of_year new_schedule = IceCube::Schedule.new(step_time - 1.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.except(:by_set_pos, :count, :until))) end occurrences = new_schedule.occurrences_between(start_of_year, end_of_year) From c70d7f6658b4e486c481eb6b732ce028f0267cc5 Mon Sep 17 00:00:00 2001 From: Nathan Ehresman Date: Fri, 23 Dec 2022 15:34:37 -0500 Subject: [PATCH 06/22] remove no longer needed TimeUtil active_support require This was a holdover from the original PR back in 2016. TimeUtil has since been refactored to not need this, but the require was inadvertently left. --- lib/ice_cube/time_util.rb | 2 -- 1 file changed, 2 deletions(-) diff --git a/lib/ice_cube/time_util.rb b/lib/ice_cube/time_util.rb index 3e914799..d632f0a1 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 From 0f5c71a55d297cc0e62a0572aece0e1486084067 Mon Sep 17 00:00:00 2001 From: Nathan Ehresman Date: Fri, 23 Dec 2022 15:35:07 -0500 Subject: [PATCH 07/22] fix interval use with bysetpos --- .../validations/monthly_by_set_pos.rb | 5 +- lib/ice_cube/validations/yearly_by_set_pos.rb | 5 +- spec/examples/by_set_pos_spec.rb | 46 +++++++++++++++++++ 3 files changed, 52 insertions(+), 4 deletions(-) diff --git a/lib/ice_cube/validations/monthly_by_set_pos.rb b/lib/ice_cube/validations/monthly_by_set_pos.rb index 111255c0..6208b283 100644 --- a/lib/ice_cube/validations/monthly_by_set_pos.rb +++ b/lib/ice_cube/validations/monthly_by_set_pos.rb @@ -31,11 +31,12 @@ def dst_adjust? true end - def validate(step_time, schedule) + def validate(step_time, start_time) start_of_month = step_time.beginning_of_month end_of_month = step_time.end_of_month - new_schedule = IceCube::Schedule.new(step_time - 1.month) do |s| + # Needs to start on the first day of the month + new_schedule = IceCube::Schedule.new(start_of_month.change(hour: step_time.hour, min: step_time.min, sec: step_time.sec)) do |s| s.add_recurrence_rule(IceCube::Rule.from_hash(rule.to_hash.except(:by_set_pos, :count, :until))) end diff --git a/lib/ice_cube/validations/yearly_by_set_pos.rb b/lib/ice_cube/validations/yearly_by_set_pos.rb index 82f90349..94c98c54 100644 --- a/lib/ice_cube/validations/yearly_by_set_pos.rb +++ b/lib/ice_cube/validations/yearly_by_set_pos.rb @@ -33,11 +33,12 @@ def dst_adjust? true end - def validate(step_time, schedule) + def validate(step_time, start_time) start_of_year = step_time.beginning_of_year end_of_year = step_time.end_of_year - new_schedule = IceCube::Schedule.new(step_time - 1.year) do |s| + # Needs to start on the first day of the year + new_schedule = IceCube::Schedule.new(start_of_year.change(hour: step_time.hour, min: step_time.min, sec: step_time.sec)) do |s| s.add_recurrence_rule(IceCube::Rule.from_hash(rule.to_hash.except(:by_set_pos, :count, :until))) end diff --git a/spec/examples/by_set_pos_spec.rb b/spec/examples/by_set_pos_spec.rb index ee0eafcf..97490d66 100644 --- a/spec/examples/by_set_pos_spec.rb +++ b/spec/examples/by_set_pos_spec.rb @@ -13,6 +13,18 @@ module IceCube Time.new(2015,9,23,12,0,0) ]) end + + it 'should work with intervals' do + schedule = IceCube::Schedule.from_ical "RRULE:FREQ=MONTHLY;COUNT=4;BYDAY=WE;BYSETPOS=4;INTERVAL=2" + 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,7,22,12,0,0), + Time.new(2015,9,23,12,0,0), + Time.new(2015,11,25,12,0,0), + Time.new(2016,1,27,12,0,0), + ]) + end end describe YearlyRule, 'BYSETPOS' do @@ -25,5 +37,39 @@ module IceCube Time.new(2016, 7, 31) ]) end + + it 'should work with intervals' do + schedule = IceCube::Schedule.from_ical "RRULE:FREQ=YEARLY;BYMONTH=7;BYDAY=SU,MO,TU,WE,TH,FR,SA;BYSETPOS=-1;INTERVAL=2" + schedule.start_time = Time.new(1966,7,5) + expect(schedule.occurrences_between(Time.new(2015, 01, 01), Time.new(2023, 01, 01))). + to eq([ + Time.new(2016, 7, 31), + Time.new(2018, 7, 31), + Time.new(2020, 7, 31), + Time.new(2022, 7, 31), + ]) + end + + it 'should work with counts' do + schedule = IceCube::Schedule.from_ical "RRULE:FREQ=YEARLY;BYMONTH=7;BYDAY=SU,MO,TU,WE,TH,FR,SA;BYSETPOS=-1;COUNT=3" + schedule.start_time = Time.new(2016,1,1) + expect(schedule.occurrences_between(Time.new(2016, 01, 01), Time.new(2050, 01, 01))). + to eq([ + Time.new(2016, 7, 31), + Time.new(2017, 7, 31), + Time.new(2018, 7, 31), + ]) + end + + it 'should work with counts and intervals' do + schedule = IceCube::Schedule.from_ical "RRULE:FREQ=YEARLY;BYMONTH=7;BYDAY=SU,MO,TU,WE,TH,FR,SA;BYSETPOS=-1;COUNT=3;INTERVAL=2" + schedule.start_time = Time.new(2016,1,1) + expect(schedule.occurrences_between(Time.new(2016, 01, 01), Time.new(2050, 01, 01))). + to eq([ + Time.new(2016, 7, 31), + Time.new(2018, 7, 31), + Time.new(2020, 7, 31), + ]) + end end end From 621bf3db42b20b38563da1cb70ca202bc6ce6f4a Mon Sep 17 00:00:00 2001 From: Nathan Ehresman Date: Tue, 27 Dec 2022 09:27:03 -0500 Subject: [PATCH 08/22] remove unneeded use of activesupport for date arithmetic --- lib/ice_cube/validations/monthly_by_set_pos.rb | 7 ++++--- lib/ice_cube/validations/yearly_by_set_pos.rb | 6 +++--- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/lib/ice_cube/validations/monthly_by_set_pos.rb b/lib/ice_cube/validations/monthly_by_set_pos.rb index 6208b283..40d93926 100644 --- a/lib/ice_cube/validations/monthly_by_set_pos.rb +++ b/lib/ice_cube/validations/monthly_by_set_pos.rb @@ -32,11 +32,12 @@ def dst_adjust? end def validate(step_time, start_time) - start_of_month = step_time.beginning_of_month - end_of_month = step_time.end_of_month + start_of_month = TimeUtil.build_in_zone([step_time.year, step_time.month, 1, 0, 0, 0], step_time) + eom_date = Date.new(step_time.year, step_time.month, -1) + end_of_month = TimeUtil.build_in_zone([eom_date.year, eom_date.month, eom_date.day, 23, 59, 59], step_time) # Needs to start on the first day of the month - new_schedule = IceCube::Schedule.new(start_of_month.change(hour: step_time.hour, min: step_time.min, sec: step_time.sec)) do |s| + new_schedule = IceCube::Schedule.new(IceCube::TimeUtil.build_in_zone([start_of_month.year, start_of_month.month, start_of_month.day, step_time.hour, step_time.min, step_time.sec], start_of_month)) do |s| s.add_recurrence_rule(IceCube::Rule.from_hash(rule.to_hash.except(:by_set_pos, :count, :until))) end diff --git a/lib/ice_cube/validations/yearly_by_set_pos.rb b/lib/ice_cube/validations/yearly_by_set_pos.rb index 94c98c54..f5cc9a19 100644 --- a/lib/ice_cube/validations/yearly_by_set_pos.rb +++ b/lib/ice_cube/validations/yearly_by_set_pos.rb @@ -34,11 +34,11 @@ def dst_adjust? end def validate(step_time, start_time) - start_of_year = step_time.beginning_of_year - end_of_year = step_time.end_of_year + start_of_year = TimeUtil.build_in_zone([step_time.year, 1, 1, 0, 0, 0], step_time) + end_of_year = TimeUtil.build_in_zone([step_time.year, 12, 31, 23, 59, 59], step_time) # Needs to start on the first day of the year - new_schedule = IceCube::Schedule.new(start_of_year.change(hour: step_time.hour, min: step_time.min, sec: step_time.sec)) do |s| + new_schedule = IceCube::Schedule.new(IceCube::TimeUtil.build_in_zone([start_of_year.year, start_of_year.month, start_of_year.day, step_time.hour, step_time.min, step_time.sec], start_of_year)) do |s| s.add_recurrence_rule(IceCube::Rule.from_hash(rule.to_hash.except(:by_set_pos, :count, :until))) end From ee3171d80b271e06246e7c6db3f54d9e2808cb83 Mon Sep 17 00:00:00 2001 From: Nathan Ehresman Date: Tue, 27 Dec 2022 11:20:32 -0500 Subject: [PATCH 09/22] support for bysetpos with freq=weekly --- lib/ice_cube.rb | 1 + lib/ice_cube/rules/weekly_rule.rb | 1 + lib/ice_cube/validations/weekly_by_set_pos.rb | 91 +++++++++++++++++++ spec/examples/by_set_pos_spec.rb | 28 ++++++ 4 files changed, 121 insertions(+) create mode 100644 lib/ice_cube/validations/weekly_by_set_pos.rb diff --git a/lib/ice_cube.rb b/lib/ice_cube.rb index 5ef72e4a..a6d5078d 100644 --- a/lib/ice_cube.rb +++ b/lib/ice_cube.rb @@ -49,6 +49,7 @@ module Validations autoload :YearlyInterval, "ice_cube/validations/yearly_interval" autoload :HourlyInterval, "ice_cube/validations/hourly_interval" + autoload :WeeklyBySetPos, 'ice_cube/validations/weekly_by_set_pos' autoload :MonthlyBySetPos, 'ice_cube/validations/monthly_by_set_pos' autoload :YearlyBySetPos, 'ice_cube/validations/yearly_by_set_pos' diff --git a/lib/ice_cube/rules/weekly_rule.rb b/lib/ice_cube/rules/weekly_rule.rb index e24f77f9..12e8b485 100644 --- a/lib/ice_cube/rules/weekly_rule.rb +++ b/lib/ice_cube/rules/weekly_rule.rb @@ -10,6 +10,7 @@ class WeeklyRule < ValidatedRule # include Validations::DayOfYear # n/a include Validations::WeeklyInterval + include Validations::WeeklyBySetPos attr_reader :week_start diff --git a/lib/ice_cube/validations/weekly_by_set_pos.rb b/lib/ice_cube/validations/weekly_by_set_pos.rb new file mode 100644 index 00000000..f7c9f791 --- /dev/null +++ b/lib/ice_cube/validations/weekly_by_set_pos.rb @@ -0,0 +1,91 @@ +module IceCube + module Validations::WeeklyBySetPos + 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, start_time) + # Use vanilla Ruby Date objects so we can add and subtract dates across DST changes + step_time_date = step_time.to_date + start_day_of_week = TimeUtil.sym_to_wday(rule.week_start) + step_time_day_of_week = step_time_date.wday + days_delta = step_time_day_of_week - start_day_of_week + days_to_start = days_delta >= 0 ? days_delta : 7 + days_delta + start_of_week_date = step_time_date - days_to_start + end_of_week_date = start_of_week_date + 6 + start_of_week = IceCube::TimeUtil.build_in_zone( + [start_of_week_date.year, start_of_week_date.month, start_of_week_date.day, 0, 0, 0], step_time + ) + end_of_week = IceCube::TimeUtil.build_in_zone( + [end_of_week_date.year, end_of_week_date.month, end_of_week_date.day, 23, 59, 59], step_time + ) + + # Needs to start on the first day of the week at the step_time's hour, min, sec + start_of_week_adjusted = IceCube::TimeUtil.build_in_zone( + [ + start_of_week_date.year, start_of_week_date.month, start_of_week_date.day, + step_time.hour, step_time.min, step_time.sec + ], step_time + ) + new_schedule = IceCube::Schedule.new(start_of_week_adjusted) do |s| + s.add_recurrence_rule(IceCube::Rule.from_hash(rule.to_hash.except(:by_set_pos, :count, :until))) + end + + occurrences = new_schedule.occurrences_between(start_of_week, end_of_week) + 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 index 97490d66..245790d4 100644 --- a/spec/examples/by_set_pos_spec.rb +++ b/spec/examples/by_set_pos_spec.rb @@ -1,6 +1,34 @@ require File.dirname(__FILE__) + '/../spec_helper' module IceCube + describe WeeklyRule, 'BYSETPOS' do + it 'should behave correctly' do + # Weekly on Monday, Wednesday, and Friday with the week starting on Wednesday, the last day of the set + schedule = IceCube::Schedule.from_ical("RRULE:FREQ=WEEKLY;COUNT=4;WKST=WE;BYDAY=MO,WE,FR;BYSETPOS=-1") + schedule.start_time = Time.new(2022, 12, 27, 12, 0, 0) + expect(schedule.occurrences_between(Time.new(2022, 01, 01), Time.new(2024, 01, 01))). + to eq([ + Time.new(2023,1,2,12,0,0), + Time.new(2023,1,9,12,0,0), + Time.new(2023,1,16,12,0,0), + Time.new(2023,1,23,12,0,0) + ]) + end + + it 'should work with intervals' do + # Every 2 weeks on Monday, Wednesday, and Friday with the week starting on Wednesday, the last day of the set + schedule = IceCube::Schedule.from_ical("RRULE:FREQ=WEEKLY;COUNT=4;INTERVAL=2;WKST=WE;BYDAY=MO,WE,FR;BYSETPOS=-1") + schedule.start_time = Time.new(2022, 12, 27, 12, 0, 0) + expect(schedule.occurrences_between(Time.new(2022, 01, 01), Time.new(2024, 01, 01))). + to eq([ + Time.new(2023,1,9,12,0,0), + Time.new(2023,1,23,12,0,0), + Time.new(2023,2,6,12,0,0), + Time.new(2023,2,20,12,0,0) + ]) + end + end + describe MonthlyRule, 'BYSETPOS' do it 'should behave correctly' do schedule = IceCube::Schedule.from_ical "RRULE:FREQ=MONTHLY;COUNT=4;BYDAY=WE;BYSETPOS=4" From 6a50aadb4578652e87abee7693777aad93890b0c Mon Sep 17 00:00:00 2001 From: Nathan Ehresman Date: Tue, 31 Jan 2023 08:49:38 -0500 Subject: [PATCH 10/22] support for parsing rrules from ical that are very long and wrap --- lib/ice_cube/parsers/ical_parser.rb | 11 +++++++++++ spec/examples/from_ical_spec.rb | 8 ++++++++ 2 files changed, 19 insertions(+) diff --git a/lib/ice_cube/parsers/ical_parser.rb b/lib/ice_cube/parsers/ical_parser.rb index 3b00fa1a..d04beb99 100644 --- a/lib/ice_cube/parsers/ical_parser.rb +++ b/lib/ice_cube/parsers/ical_parser.rb @@ -2,7 +2,18 @@ module IceCube class IcalParser def self.schedule_from_ical(ical_string, options = {}) data = {} + + # First join lines that are wrapped + lines = [] ical_string.each_line do |line| + if lines[-1] && line =~ /\A[ \t].+\z/ + lines[-1] = lines[-1].strip + line.sub(/\A[ \t]+/, "") + else + lines << line + end + end + + lines.each do |line| (property, value) = line.split(":") (property, _tzid) = property.split(";") case property diff --git a/spec/examples/from_ical_spec.rb b/spec/examples/from_ical_spec.rb index 9cb685bf..5de567af 100644 --- a/spec/examples/from_ical_spec.rb +++ b/spec/examples/from_ical_spec.rb @@ -448,5 +448,13 @@ def sorted_ical(ical) it_behaves_like "an invalid ical string" end end + + describe "ical data with wrapping" do + it "matches simple daily" do + ical_string = "DTSTART:20130314T201500Z\nDTEND:20130314T201545Z\nRRULE:FREQ=WEEKLY;BYDAY=TH;UNT\n IL=20130531T100000Z" + schedule = IceCube::Schedule.from_ical(ical_string) + expect(schedule.to_ical.split(/\n/).select {|x| x =~ /RRULE/}.first).to eq("RRULE:FREQ=WEEKLY;UNTIL=20130531T100000Z;BYDAY=TH") + end + end end end From d7ea51efcdb06b3d6f8ccc9f940893103a6f568f Mon Sep 17 00:00:00 2001 From: Nathan Ehresman Date: Tue, 31 Jan 2023 09:01:12 -0500 Subject: [PATCH 11/22] dont require the wrapped line to be the last the ical string --- lib/ice_cube/parsers/ical_parser.rb | 2 +- spec/examples/from_ical_spec.rb | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/ice_cube/parsers/ical_parser.rb b/lib/ice_cube/parsers/ical_parser.rb index d04beb99..04ae679f 100644 --- a/lib/ice_cube/parsers/ical_parser.rb +++ b/lib/ice_cube/parsers/ical_parser.rb @@ -6,7 +6,7 @@ def self.schedule_from_ical(ical_string, options = {}) # First join lines that are wrapped lines = [] ical_string.each_line do |line| - if lines[-1] && line =~ /\A[ \t].+\z/ + if lines[-1] && line =~ /\A[ \t].+/ lines[-1] = lines[-1].strip + line.sub(/\A[ \t]+/, "") else lines << line diff --git a/spec/examples/from_ical_spec.rb b/spec/examples/from_ical_spec.rb index 5de567af..e3d50df9 100644 --- a/spec/examples/from_ical_spec.rb +++ b/spec/examples/from_ical_spec.rb @@ -451,7 +451,7 @@ def sorted_ical(ical) describe "ical data with wrapping" do it "matches simple daily" do - ical_string = "DTSTART:20130314T201500Z\nDTEND:20130314T201545Z\nRRULE:FREQ=WEEKLY;BYDAY=TH;UNT\n IL=20130531T100000Z" + ical_string = "DTSTART:20130314T201500Z\nDTEND:20130314T201545Z\nRRULE:FREQ=WEEKLY;BYDAY=TH;UNT\n IL=20130531T100000Z\nDESCRIPTION:This is a test event\nSUMMARY:Test Event\n" schedule = IceCube::Schedule.from_ical(ical_string) expect(schedule.to_ical.split(/\n/).select {|x| x =~ /RRULE/}.first).to eq("RRULE:FREQ=WEEKLY;UNTIL=20130531T100000Z;BYDAY=TH") end From 3da01b5bba6f1faec547db06547d043f0ddcf5dd Mon Sep 17 00:00:00 2001 From: Nicolas Marlier Date: Tue, 23 Aug 2016 12:47:23 +0200 Subject: [PATCH 12/22] Support BYSETPOS for MONTHLY AND YEARLY freq --- .gitignore | 3 +++ lib/ice_cube.rb | 6 +++--- lib/ice_cube/time_util.rb | 32 ++++++++++++++++++++++++++++++++ spec/examples/from_ical_spec.rb | 2 +- 4 files changed, 39 insertions(+), 4 deletions(-) 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 a6d5078d..4d7108f6 100644 --- a/lib/ice_cube.rb +++ b/lib/ice_cube.rb @@ -49,9 +49,9 @@ module Validations autoload :YearlyInterval, "ice_cube/validations/yearly_interval" autoload :HourlyInterval, "ice_cube/validations/hourly_interval" - autoload :WeeklyBySetPos, 'ice_cube/validations/weekly_by_set_pos' - autoload :MonthlyBySetPos, 'ice_cube/validations/monthly_by_set_pos' - autoload :YearlyBySetPos, 'ice_cube/validations/yearly_by_set_pos' + autoload :WeeklyBySetPos, "ice_cube/validations/weekly_by_set_pos" + 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" diff --git a/lib/ice_cube/time_util.rb b/lib/ice_cube/time_util.rb index d632f0a1..b6669573 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 @@ -193,6 +195,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/spec/examples/from_ical_spec.rb b/spec/examples/from_ical_spec.rb index e3d50df9..d58b2bd3 100644 --- a/spec/examples/from_ical_spec.rb +++ b/spec/examples/from_ical_spec.rb @@ -86,7 +86,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") - expect(rule).to eq(IceCube::Rule.monthly.day(:monday, :wednesday).by_set_pos([-1, 1])) + rule.should == IceCube::Rule.monthly.day(:monday, :wednesday).by_set_pos([-1, 1]) end it 'should raise when by_set_pos is out of range (BYSETPOS)' do From 63e387de4c40b066544c025e38f9059ba25a0f7e Mon Sep 17 00:00:00 2001 From: Christopher Nelson Date: Wed, 23 Nov 2022 15:48:10 +1100 Subject: [PATCH 13/22] Fix bugs --- spec/examples/occurrence_spec.rb | 2 +- spec/examples/to_yaml_spec.rb | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/spec/examples/occurrence_spec.rb b/spec/examples/occurrence_spec.rb index 80c18421..cfbec38e 100644 --- a/spec/examples/occurrence_spec.rb +++ b/spec/examples/occurrence_spec.rb @@ -28,7 +28,7 @@ time_now = Time.current occurrence = Occurrence.new(time_now) - expect(occurrence.to_s(:short)).to eq time_now.to_s(:short) + expect(occurrence.to_fs(:short)).to eq time_now.to_fs(:short) end end diff --git a/spec/examples/to_yaml_spec.rb b/spec/examples/to_yaml_spec.rb index 7166110d..599b85d3 100644 --- a/spec/examples/to_yaml_spec.rb +++ b/spec/examples/to_yaml_spec.rb @@ -90,7 +90,7 @@ module IceCube end it "should be able to make a round-trip to YAML with .hour_of_day" do - schedule = Schedule.new(Time.now) + schedule = Schedule.new(Time.zone.now) schedule.add_recurrence_rule Rule.daily.hour_of_day(1, 2) yaml_string = schedule.to_yaml @@ -101,7 +101,7 @@ module IceCube end it "should be able to make a round-trip to YAML with .minute_of_hour" do - schedule = Schedule.new(Time.now) + schedule = Schedule.new(Time.zone.now) schedule.add_recurrence_rule Rule.daily.minute_of_hour(0, 30) yaml_string = schedule.to_yaml @@ -123,7 +123,7 @@ module IceCube end it "should be able to make a round-trip to YAML with .second_of_minute" do - schedule = Schedule.new(Time.now) + schedule = Schedule.new(Time.zone.now) schedule.add_recurrence_rule Rule.daily.second_of_minute(1, 2) yaml_string = schedule.to_yaml From cf415651c6b8c5416684cc9839ce7966aab9f22b Mon Sep 17 00:00:00 2001 From: Juan Deniz Date: Wed, 22 Feb 2023 13:59:30 +1100 Subject: [PATCH 14/22] NEB-1737 Added pertinent RRULE test, fixed minor issue in another spec --- spec/examples/by_set_pos_spec.rb | 23 ++++++++++++++++++----- spec/examples/time_util_spec.rb | 6 ++++++ spec/examples/to_yaml_spec.rb | 2 +- 3 files changed, 25 insertions(+), 6 deletions(-) diff --git a/spec/examples/by_set_pos_spec.rb b/spec/examples/by_set_pos_spec.rb index 245790d4..cabce01b 100644 --- a/spec/examples/by_set_pos_spec.rb +++ b/spec/examples/by_set_pos_spec.rb @@ -47,11 +47,24 @@ module IceCube 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,7,22,12,0,0), - Time.new(2015,9,23,12,0,0), - Time.new(2015,11,25,12,0,0), - Time.new(2016,1,27,12,0,0), - ]) + Time.new(2015,7,22,12,0,0), + Time.new(2015,9,23,12,0,0), + Time.new(2015,11,25,12,0,0), + Time.new(2016,1,27,12,0,0), + ]) + end + + context 'when billing occurs at the end of the month' do + it 'should return the correct end of the months dates' do + schedule = IceCube::Schedule.from_ical "RRULE:FREQ=MONTHLY;COUNT=4;BYMONTHDAY=28,29,30,31;BYSETPOS=-1" + schedule.start_time = Time.new(2022, 11, 1, 12, 0, 0) + expect(schedule.occurrences_between(Time.new(2022, 10, 01), Time.new(2023, 10, 31))).to eq([ + Time.new(2022,11,30,12,0,0), + Time.new(2022,12,31,12,0,0), + Time.new(2023,01,31,12,0,0), + Time.new(2023,02,28,12,0,0) + ]) + end end end diff --git a/spec/examples/time_util_spec.rb b/spec/examples/time_util_spec.rb index 43491a8b..4d7834bb 100644 --- a/spec/examples/time_util_spec.rb +++ b/spec/examples/time_util_spec.rb @@ -92,6 +92,12 @@ module IceCube end end + describe :serialize_time do + it "supports ISO8601 time strings" do + expect(TimeUtil.serialize_time(Time.utc(2014, 4, 4, 10, 30, 0))).to eq("2014-04-04T18:30:00+08:00") + end + end + describe :match_zone do let(:date) { Date.new(2014, 1, 1) } diff --git a/spec/examples/to_yaml_spec.rb b/spec/examples/to_yaml_spec.rb index 599b85d3..28bd9d83 100644 --- a/spec/examples/to_yaml_spec.rb +++ b/spec/examples/to_yaml_spec.rb @@ -134,7 +134,7 @@ module IceCube end it "should be able to make a round-trip to YAML whilst preserving exception rules" do - original_schedule = Schedule.new(Time.now) + original_schedule = Schedule.new(Time.zone.now) original_schedule.add_recurrence_rule Rule.daily.day(:monday, :wednesday) original_schedule.add_exception_rule Rule.daily.day(:wednesday) From 0df8ee03ff5941525364111ebceb675ff9e8e608 Mon Sep 17 00:00:00 2001 From: Juan Deniz Date: Wed, 22 Feb 2023 14:55:41 +1100 Subject: [PATCH 15/22] NEB-1737 Updating changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index c0022b92..ffc1259e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] ### Added - Indonesian translations. ([#505](https://github.com/seejohnrun/ice_cube/pull/505)) by [@achmiral](https://github.com/achmiral) +- Support for BYSETPOS ### Changed - Removed use of `delegate` method added in [66f1d797](https://github.com/ice-cube-ruby/ice_cube/commit/66f1d797092734563bfabd2132c024c7d087f683) , reverting to previous implementation. ([#522](https://github.com/ice-cube-ruby/ice_cube/pull/522)) by [@pacso](https://github.com/pacso) From 021723cec7e118a007297524a5d6061c8378e18a Mon Sep 17 00:00:00 2001 From: Juan Deniz Date: Wed, 22 Feb 2023 17:12:08 +1100 Subject: [PATCH 16/22] NEB-1737 Linted the code, added more bysetops tests --- ice_cube.gemspec | 1 - lib/ice_cube/builders/string_builder.rb | 2 +- lib/ice_cube/input_alignment.rb | 4 +- lib/ice_cube/null_i18n.rb | 2 +- lib/ice_cube/occurrence.rb | 2 +- lib/ice_cube/parsers/ical_parser.rb | 2 +- lib/ice_cube/schedule.rb | 2 +- lib/ice_cube/time_util.rb | 30 ++-- lib/ice_cube/validations/day_of_week.rb | 4 +- lib/ice_cube/validations/day_of_year.rb | 4 +- lib/ice_cube/validations/fixed_value.rb | 6 +- lib/ice_cube/validations/lock.rb | 6 +- .../validations/monthly_by_set_pos.rb | 8 +- lib/ice_cube/validations/yearly_by_set_pos.rb | 8 +- spec/examples/by_set_pos_spec.rb | 148 +++++++++++++++--- spec/examples/from_ical_spec.rb | 2 +- spec/examples/ice_cube_spec.rb | 2 +- spec/examples/time_util_spec.rb | 5 +- 18 files changed, 171 insertions(+), 67 deletions(-) diff --git a/ice_cube.gemspec b/ice_cube.gemspec index 5191c6fa..b1924c18 100644 --- a/ice_cube.gemspec +++ b/ice_cube.gemspec @@ -17,7 +17,6 @@ Gem::Specification.new do |s| s.version = IceCube::VERSION s.platform = Gem::Platform::RUBY s.files = Dir["lib/**/*.rb", "config/**/*.yml"] - s.test_files = Dir.glob("spec/*.rb") s.require_paths = ["lib"] s.add_development_dependency("rake") diff --git a/lib/ice_cube/builders/string_builder.rb b/lib/ice_cube/builders/string_builder.rb index c87e97ed..07703631 100644 --- a/lib/ice_cube/builders/string_builder.rb +++ b/lib/ice_cube/builders/string_builder.rb @@ -61,7 +61,7 @@ def ordinal(number) ord = IceCube::I18n.t("ice_cube.integer.ordinals")[number] || IceCube::I18n.t("ice_cube.integer.ordinals")[number % 10] || IceCube::I18n.t("ice_cube.integer.ordinals")[:default] - number >= 0 ? ord : IceCube::I18n.t("ice_cube.integer.negative", ordinal: ord) + (number >= 0) ? ord : IceCube::I18n.t("ice_cube.integer.negative", ordinal: ord) end end diff --git a/lib/ice_cube/input_alignment.rb b/lib/ice_cube/input_alignment.rb index 8900fa15..3cdd5070 100644 --- a/lib/ice_cube/input_alignment.rb +++ b/lib/ice_cube/input_alignment.rb @@ -28,12 +28,12 @@ def interval_validation end def interval_value - @interval_value ||= rule_part == :interval ? value : interval_validation.interval + @interval_value ||= (rule_part == :interval) ? value : interval_validation.interval end def fixed_validations @fixed_validations ||= @rule.validations.values.flatten.select { |v| - interval_type = (v.type == :wday ? :day : v.type) + interval_type = ((v.type == :wday) ? :day : v.type) v.class < Validations::FixedValue && interval_type == rule.base_interval_validation.type } diff --git a/lib/ice_cube/null_i18n.rb b/lib/ice_cube/null_i18n.rb index 028ab2c5..0ec25e23 100644 --- a/lib/ice_cube/null_i18n.rb +++ b/lib/ice_cube/null_i18n.rb @@ -5,7 +5,7 @@ module NullI18n def self.t(key, options = {}) base = key.to_s.split(".").reduce(config) { |hash, current_key| hash[current_key] } - base = base[options[:count] == 1 ? "one" : "other"] if options[:count] + base = base[(options[:count] == 1) ? "one" : "other"] if options[:count] case base when Hash diff --git a/lib/ice_cube/occurrence.rb b/lib/ice_cube/occurrence.rb index 4c51f962..3892f6c6 100644 --- a/lib/ice_cube/occurrence.rb +++ b/lib/ice_cube/occurrence.rb @@ -90,7 +90,7 @@ def to_s(format = nil) else t0, t1 = start_time.to_s, end_time.to_s end - duration > 0 ? "#{t0} - #{t1}" : t0 + (duration > 0) ? "#{t0} - #{t1}" : t0 end def overnight? diff --git a/lib/ice_cube/parsers/ical_parser.rb b/lib/ice_cube/parsers/ical_parser.rb index 04ae679f..946b96a8 100644 --- a/lib/ice_cube/parsers/ical_parser.rb +++ b/lib/ice_cube/parsers/ical_parser.rb @@ -86,7 +86,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) + params[:validations][:by_set_pos] = value.split(",").collect(&:to_i) else validations[name] = nil # invalid type end diff --git a/lib/ice_cube/schedule.rb b/lib/ice_cube/schedule.rb index 68f01cc9..036c9f2b 100644 --- a/lib/ice_cube/schedule.rb +++ b/lib/ice_cube/schedule.rb @@ -191,7 +191,7 @@ def previous_occurrences(num, from) from = TimeUtil.match_zone(from, start_time) or raise ArgumentError, "Time required, got #{from.inspect}" return [] if from <= start_time a = enumerate_occurrences(start_time, from - 1).to_a - a.size > num ? a[-1 * num, a.size] : a + (a.size > num) ? a[-1 * num, a.size] : a end # The remaining occurrences (same requirements as all_occurrences) diff --git a/lib/ice_cube/time_util.rb b/lib/ice_cube/time_util.rb index b6669573..d14c74bf 100644 --- a/lib/ice_cube/time_util.rb +++ b/lib/ice_cube/time_util.rb @@ -1,7 +1,7 @@ -require 'date' -require 'time' -require 'active_support' -require 'active_support/core_ext' +require "date" +require "time" +require "active_support" +require "active_support/core_ext" module IceCube module TimeUtil @@ -53,7 +53,7 @@ def self.match_zone(input_time, reference) else time.getlocal(reference.utc_offset) end - Date === input_time ? beginning_of_date(time, reference) : time + (Date === input_time) ? beginning_of_date(time, reference) : time end # Ensure that this is either nil, or a time @@ -318,12 +318,12 @@ def to_time def add(type, val) type = :day if type == :wday @time += case type - when :year then TimeUtil.days_in_n_years(@time, val) * ONE_DAY - when :month then TimeUtil.days_in_n_months(@time, val) * ONE_DAY - when :day then val * ONE_DAY - when :hour then val * ONE_HOUR - when :min then val * ONE_MINUTE - when :sec then val + when :year then TimeUtil.days_in_n_years(@time, val) * ONE_DAY + when :month then TimeUtil.days_in_n_months(@time, val) * ONE_DAY + when :day then val * ONE_DAY + when :hour then val * ONE_HOUR + when :min then val * ONE_MINUTE + when :sec then val end end @@ -350,20 +350,20 @@ def sec=(value) end def clear_sec - @time.sec > 0 ? @time -= @time.sec : @time + (@time.sec > 0) ? @time -= @time.sec : @time end def clear_min - @time.min > 0 ? @time -= (@time.min * ONE_MINUTE) : @time + (@time.min > 0) ? @time -= (@time.min * ONE_MINUTE) : @time end def clear_hour - @time.hour > 0 ? @time -= (@time.hour * ONE_HOUR) : @time + (@time.hour > 0) ? @time -= (@time.hour * ONE_HOUR) : @time end # Move to the first of the month, 0 hours def clear_day - @time.day > 1 ? @time -= (@time.day - 1) * ONE_DAY : @time + (@time.day > 1) ? @time -= (@time.day - 1) * ONE_DAY : @time end # Clear to january 1st diff --git a/lib/ice_cube/validations/day_of_week.rb b/lib/ice_cube/validations/day_of_week.rb index e00193b7..c04695bc 100644 --- a/lib/ice_cube/validations/day_of_week.rb +++ b/lib/ice_cube/validations/day_of_week.rb @@ -29,12 +29,12 @@ def dst_adjust? def validate(step_time, start_time) wday = step_time.wday - offset = day < wday ? (7 - wday + day) : (day - wday) + offset = (day < wday) ? (7 - wday + day) : (day - wday) wrapper = TimeUtil::TimeWrapper.new(step_time) wrapper.add :day, offset loop do which_occ, num_occ = TimeUtil.which_occurrence_in_month(wrapper.to_time, day) - this_occ = occ < 0 ? (num_occ + occ + 1) : occ + this_occ = (occ < 0) ? (num_occ + occ + 1) : occ break offset if which_occ == this_occ wrapper.add :day, 7 offset += 7 diff --git a/lib/ice_cube/validations/day_of_year.rb b/lib/ice_cube/validations/day_of_year.rb index 239f241e..c9efecdb 100644 --- a/lib/ice_cube/validations/day_of_year.rb +++ b/lib/ice_cube/validations/day_of_year.rb @@ -28,9 +28,9 @@ def dst_adjust? def validate(step_time, start_time) days_in_year = TimeUtil.days_in_year(step_time) - yday = day < 0 ? day + days_in_year + 1 : day + yday = (day < 0) ? day + days_in_year + 1 : day offset = yday - step_time.yday - offset >= 0 ? offset : offset + days_in_year + (offset >= 0) ? offset : offset + days_in_year end def build_s(builder) diff --git a/lib/ice_cube/validations/fixed_value.rb b/lib/ice_cube/validations/fixed_value.rb index 2cfdd5fb..243d4b8c 100644 --- a/lib/ice_cube/validations/fixed_value.rb +++ b/lib/ice_cube/validations/fixed_value.rb @@ -26,7 +26,7 @@ def validate(time, start_time) def validate_interval_lock(time, start_time) t0 = starting_unit(start_time) t1 = time.send(type) - t0 >= t1 ? t0 - t1 : INTERVALS[type] - t1 + t0 + (t0 >= t1) ? t0 - t1 : INTERVALS[type] - t1 + t0 end # Lock the hour if explicitly set by hour_of_day, but allow for the nearest @@ -73,11 +73,11 @@ def validate_day_lock(time, start_time) if value && value > 0 until_next_month = days_in_month + sleeps else - until_next_month = start < 28 ? days_in_month : TimeUtil.days_to_next_month(date) + until_next_month = (start < 28) ? days_in_month : TimeUtil.days_to_next_month(date) until_next_month += sleeps - month_overflow end - sleeps >= 0 ? sleeps : until_next_month + (sleeps >= 0) ? sleeps : until_next_month end def starting_unit(start_time) diff --git a/lib/ice_cube/validations/lock.rb b/lib/ice_cube/validations/lock.rb index 954463c1..33249382 100644 --- a/lib/ice_cube/validations/lock.rb +++ b/lib/ice_cube/validations/lock.rb @@ -26,7 +26,7 @@ def validate(time, start_time) def validate_interval_lock(time, start_time) t0 = starting_unit(start_time) t1 = time.send(type) - t0 >= t1 ? t0 - t1 : INTERVALS[type] - t1 + t0 + (t0 >= t1) ? t0 - t1 : INTERVALS[type] - t1 + t0 end # Lock the hour if explicitly set by hour_of_day, but allow for the nearest @@ -73,11 +73,11 @@ def validate_day_lock(time, start_time) if value && value > 0 until_next_month = days_in_month + sleeps else - until_next_month = start < 28 ? days_in_month : TimeUtil.days_to_next_month(date) + until_next_month = (start < 28) ? days_in_month : TimeUtil.days_to_next_month(date) until_next_month += sleeps - month_overflow end - sleeps >= 0 ? sleeps : until_next_month + (sleeps >= 0) ? sleeps : until_next_month end def starting_unit(start_time) diff --git a/lib/ice_cube/validations/monthly_by_set_pos.rb b/lib/ice_cube/validations/monthly_by_set_pos.rb index 40d93926..700c1cac 100644 --- a/lib/ice_cube/validations/monthly_by_set_pos.rb +++ b/lib/ice_cube/validations/monthly_by_set_pos.rb @@ -1,5 +1,4 @@ module IceCube - module Validations::MonthlyBySetPos def by_set_pos(*by_set_pos) by_set_pos.flatten! @@ -15,7 +14,6 @@ def by_set_pos(*by_set_pos) end class Validation - attr_reader :rule, :by_set_pos def initialize(by_set_pos, rule) @@ -43,7 +41,7 @@ def validate(step_time, start_time) occurrences = new_schedule.occurrences_between(start_of_month, end_of_month) index = occurrences.index(step_time) - if index == nil + if index.nil? 1 else positive_set_pos = index + 1 @@ -66,12 +64,10 @@ def build_hash(builder) end def build_ical(builder) - builder['BYSETPOS'] << by_set_pos + 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 index f5cc9a19..4f67ddd7 100644 --- a/lib/ice_cube/validations/yearly_by_set_pos.rb +++ b/lib/ice_cube/validations/yearly_by_set_pos.rb @@ -1,7 +1,5 @@ module IceCube - module Validations::YearlyBySetPos - def by_set_pos(*by_set_pos) by_set_pos.flatten! by_set_pos.each do |set_pos| @@ -16,11 +14,9 @@ def by_set_pos(*by_set_pos) end class Validation - attr_reader :rule, :by_set_pos def initialize(by_set_pos, rule) - @by_set_pos = by_set_pos @rule = rule end @@ -45,7 +41,7 @@ def validate(step_time, start_time) occurrences = new_schedule.occurrences_between(start_of_year, end_of_year) index = occurrences.index(step_time) - if index == nil + if index.nil? 1 else positive_set_pos = index + 1 @@ -68,7 +64,7 @@ def build_hash(builder) end def build_ical(builder) - builder['BYSETPOS'] << by_set_pos + builder["BYSETPOS"] << by_set_pos end nil diff --git a/spec/examples/by_set_pos_spec.rb b/spec/examples/by_set_pos_spec.rb index cabce01b..38c91605 100644 --- a/spec/examples/by_set_pos_spec.rb +++ b/spec/examples/by_set_pos_spec.rb @@ -1,4 +1,4 @@ -require File.dirname(__FILE__) + '/../spec_helper' +require File.dirname(__FILE__) + "/../spec_helper" module IceCube describe WeeklyRule, 'BYSETPOS' do @@ -8,11 +8,11 @@ module IceCube schedule.start_time = Time.new(2022, 12, 27, 12, 0, 0) expect(schedule.occurrences_between(Time.new(2022, 01, 01), Time.new(2024, 01, 01))). to eq([ - Time.new(2023,1,2,12,0,0), - Time.new(2023,1,9,12,0,0), - Time.new(2023,1,16,12,0,0), - Time.new(2023,1,23,12,0,0) - ]) + Time.new(2023,1,2,12,0,0), + Time.new(2023,1,9,12,0,0), + Time.new(2023,1,16,12,0,0), + Time.new(2023,1,23,12,0,0) + ]) end it 'should work with intervals' do @@ -21,11 +21,11 @@ module IceCube schedule.start_time = Time.new(2022, 12, 27, 12, 0, 0) expect(schedule.occurrences_between(Time.new(2022, 01, 01), Time.new(2024, 01, 01))). to eq([ - Time.new(2023,1,9,12,0,0), - Time.new(2023,1,23,12,0,0), - Time.new(2023,2,6,12,0,0), - Time.new(2023,2,20,12,0,0) - ]) + Time.new(2023,1,9,12,0,0), + Time.new(2023,1,23,12,0,0), + Time.new(2023,2,6,12,0,0), + Time.new(2023,2,20,12,0,0) + ]) end end @@ -35,11 +35,11 @@ module IceCube 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) - ]) + 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 it 'should work with intervals' do @@ -68,15 +68,125 @@ module IceCube end end + describe MonthlyRule, "BYSETPOS" do + subject(:schedule) { IceCube::Schedule.from_ical(from_ical) } + before(:each) do + schedule.start_time = schedule_start + end + let(:occurrences) { schedule.occurrences_between(from_time, to_time) } + context "when the rule is the first 4 Wednesdays ..." do + let(:from_ical) { "RRULE:FREQ=MONTHLY;COUNT=4;BYDAY=WE;BYSETPOS=4" } + let(:schedule_start) { Time.new(2015, 5, 28, 12, 0, 0) } + let(:from_time) { Time.new(2015, 1, 1) } + let(:to_time) { Time.new(2017, 1, 1) } + it "returns the first 4 Wednesdays ..." do + expect(occurrences).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 + + context "when event occurs on a leap year" do + let(:from_ical) { "RRULE:FREQ=MONTHLY;COUNT=4;BYMONTHDAY=28,29,30,31;BYSETPOS=-1" } + let(:schedule_start) { Time.new(2019, 11, 1, 12, 0, 0) } + let(:from_time) { Time.new(2019, 10, 1) } + let(:to_time) { Time.new(2020, 10, 31) } + it "returns the correct end of the months date including the leap month" do + expect(occurrences).to eq( + [ + Time.new(2019, 11, 30, 12, 0, 0), + Time.new(2019, 12, 31, 12, 0, 0), + Time.new(2020, 1, 31, 12, 0, 0), + Time.new(2020, 2, 29, 12, 0, 0) + ] + ) + end + end + + context "when the rule is the first 4 last days of the month by set pos" do + let(:from_ical) { "RRULE:FREQ=MONTHLY;COUNT=4;BYMONTHDAY=28,29,30,31;BYSETPOS=-1" } + let(:schedule_start) { Time.new(2022, 11, 1, 12, 0, 0) } + let(:from_time) { Time.new(2022, 10, 1) } + let(:to_time) { Time.new(2023, 10, 31) } + it "returns the first 4 last days of the month by set pos" do + expect(occurrences).to eq( + [ + Time.new(2022, 11, 30, 12, 0, 0), + Time.new(2022, 12, 31, 12, 0, 0), + Time.new(2023, 1, 31, 12, 0, 0), + Time.new(2023, 2, 28, 12, 0, 0) + ] + ) + end + end + + context "when the rule is for the first 4 second last days of the month by set pos" do + let(:from_ical) { "RRULE:FREQ=MONTHLY;COUNT=4;BYMONTHDAY=27,28,29,30,31;BYSETPOS=-2" } + let(:schedule_start) { Time.new(2022, 11, 1, 12, 0, 0) } + let(:from_time) { Time.new(2022, 10, 1) } + let(:to_time) { Time.new(2023, 10, 31) } + it "returns the first 4 previous last days of the month by set pos" do + expect(occurrences).to eq( + [ + Time.new(2022, 11, 29, 12, 0, 0), + Time.new(2022, 12, 30, 12, 0, 0), + Time.new(2023, 1, 30, 12, 0, 0), + Time.new(2023, 2, 27, 12, 0, 0) + ] + ) + end + end + + context "when the rule is the first 4 after the last days of the month by set pos" do + let(:from_ical) { "RRULE:FREQ=MONTHLY;COUNT=4;BYMONTHDAY=28,29,30,31;BYSETPOS=4" } + let(:schedule_start) { Time.new(2023, 02, 22, 12, 0, 0) } + let(:from_time) { Time.new(2022, 10, 1) } + let(:to_time) { Time.new(2023, 10, 31) } + it "returns the first 4 previous last days of the month by set pos" do + expect(occurrences).to eq( + [ + Time.new(2023, 3, 31, 12, 0, 0), + Time.new(2023, 5, 31, 12, 0, 0), + Time.new(2023, 7, 31, 12, 0, 0), + Time.new(2023, 8, 31, 12, 0, 0) + ] + ) + end + end + + context "when the rule is the first 4 after the last wednesday of the month" do + let(:from_ical) { "RRULE:FREQ=MONTHLY;COUNT=4;BYDAY=WE;BYSETPOS=-1" } + let(:schedule_start) { Time.new(2023, 02, 22, 12, 0, 0) } + let(:from_time) { Time.new(2022, 10, 1) } + let(:to_time) { Time.new(2023, 10, 31) } + it "returns the first 4 previous last days of the month by set pos" do + expect(occurrences).to eq( + [ + Time.new(2023, 2, 22, 12, 0, 0), + Time.new(2023, 3, 29, 12, 0, 0), + Time.new(2023, 4, 26, 12, 0, 0), + Time.new(2023, 5, 31, 12, 0, 0) + ] + ) + end + 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) - ]) + Time.new(2015, 7, 31), + Time.new(2016, 7, 31) + ]) end it 'should work with intervals' do diff --git a/spec/examples/from_ical_spec.rb b/spec/examples/from_ical_spec.rb index d58b2bd3..d332e026 100644 --- a/spec/examples/from_ical_spec.rb +++ b/spec/examples/from_ical_spec.rb @@ -84,7 +84,7 @@ module IceCube expect(rule).to eq(IceCube::Rule.weekly(2, :monday)) end - it 'should be able to parse by_set_pos start (BYSETPOS)' do + 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 diff --git a/spec/examples/ice_cube_spec.rb b/spec/examples/ice_cube_spec.rb index 354d2e48..d925f63d 100644 --- a/spec/examples/ice_cube_spec.rb +++ b/spec/examples/ice_cube_spec.rb @@ -668,7 +668,7 @@ def quick_attempt_test time = Time.now 10.times do - (yield).next_occurrence(Time.now) + yield.next_occurrence(Time.now) end total = Time.now - time expect(total).to be < 0.1 diff --git a/spec/examples/time_util_spec.rb b/spec/examples/time_util_spec.rb index 4d7834bb..ce2918c9 100644 --- a/spec/examples/time_util_spec.rb +++ b/spec/examples/time_util_spec.rb @@ -93,8 +93,11 @@ module IceCube end describe :serialize_time do + subject(:serialize_time) { TimeUtil.serialize_time(time) } + let(:time) { Time.utc(2014, 4, 4, 10, 30, 0) } + let(:iso_time_str) { "2014-04-04T18:30:00+08:00" } it "supports ISO8601 time strings" do - expect(TimeUtil.serialize_time(Time.utc(2014, 4, 4, 10, 30, 0))).to eq("2014-04-04T18:30:00+08:00") + expect(serialize_time).to eq(iso_time_str) end end From 424fab65309547f060937d5c66751ad829f491e3 Mon Sep 17 00:00:00 2001 From: Juan Deniz Date: Wed, 22 Feb 2023 17:15:23 +1100 Subject: [PATCH 17/22] NEB-1737 Linted the code more --- spec/examples/by_set_pos_spec.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/spec/examples/by_set_pos_spec.rb b/spec/examples/by_set_pos_spec.rb index 38c91605..f73ebbf7 100644 --- a/spec/examples/by_set_pos_spec.rb +++ b/spec/examples/by_set_pos_spec.rb @@ -144,7 +144,7 @@ module IceCube context "when the rule is the first 4 after the last days of the month by set pos" do let(:from_ical) { "RRULE:FREQ=MONTHLY;COUNT=4;BYMONTHDAY=28,29,30,31;BYSETPOS=4" } - let(:schedule_start) { Time.new(2023, 02, 22, 12, 0, 0) } + let(:schedule_start) { Time.new(2023, 0o2, 22, 12, 0, 0) } let(:from_time) { Time.new(2022, 10, 1) } let(:to_time) { Time.new(2023, 10, 31) } it "returns the first 4 previous last days of the month by set pos" do @@ -161,7 +161,7 @@ module IceCube context "when the rule is the first 4 after the last wednesday of the month" do let(:from_ical) { "RRULE:FREQ=MONTHLY;COUNT=4;BYDAY=WE;BYSETPOS=-1" } - let(:schedule_start) { Time.new(2023, 02, 22, 12, 0, 0) } + let(:schedule_start) { Time.new(2023, 0o2, 22, 12, 0, 0) } let(:from_time) { Time.new(2022, 10, 1) } let(:to_time) { Time.new(2023, 10, 31) } it "returns the first 4 previous last days of the month by set pos" do From 23f4cc221d01e9661944fc31561588b4b0dd5ccc Mon Sep 17 00:00:00 2001 From: Juan Deniz Date: Thu, 23 Feb 2023 09:48:28 +1100 Subject: [PATCH 18/22] NEB-1737 Updating missing require library in the test --- spec/examples/occurrence_spec.rb | 2 ++ 1 file changed, 2 insertions(+) diff --git a/spec/examples/occurrence_spec.rb b/spec/examples/occurrence_spec.rb index cfbec38e..1496993f 100644 --- a/spec/examples/occurrence_spec.rb +++ b/spec/examples/occurrence_spec.rb @@ -1,4 +1,6 @@ require File.dirname(__FILE__) + "/../spec_helper" +require "active_support/all" +require "active_support/core_ext" describe Occurrence do it "reports as a Time" do From cd1d753d043b7554bc8b7cc2bca64979e4f9e465 Mon Sep 17 00:00:00 2001 From: Juan Deniz Date: Thu, 23 Feb 2023 10:44:40 +1100 Subject: [PATCH 19/22] NEB-1737 Refined rspec tests --- spec/examples/occurrence_spec.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/examples/occurrence_spec.rb b/spec/examples/occurrence_spec.rb index 1496993f..3a64ccde 100644 --- a/spec/examples/occurrence_spec.rb +++ b/spec/examples/occurrence_spec.rb @@ -30,7 +30,7 @@ time_now = Time.current occurrence = Occurrence.new(time_now) - expect(occurrence.to_fs(:short)).to eq time_now.to_fs(:short) + expect(occurrence.strftime('%Y-%m-%d')).to eq time_now.strftime('%Y-%m-%d') end end From 860b74e1fdbd836cc19128fe27ea8f7c9d3ea6c4 Mon Sep 17 00:00:00 2001 From: Juan Deniz Date: Thu, 23 Feb 2023 10:46:30 +1100 Subject: [PATCH 20/22] NEB-1737 Refined rspec tests --- spec/examples/occurrence_spec.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/examples/occurrence_spec.rb b/spec/examples/occurrence_spec.rb index 3a64ccde..be358104 100644 --- a/spec/examples/occurrence_spec.rb +++ b/spec/examples/occurrence_spec.rb @@ -30,7 +30,7 @@ time_now = Time.current occurrence = Occurrence.new(time_now) - expect(occurrence.strftime('%Y-%m-%d')).to eq time_now.strftime('%Y-%m-%d') + expect(occurrence.strftime("%Y-%m-%d")).to eq time_now.strftime("%Y-%m-%d") end end From 31cd8d27bb04c43aa1e84e4682b968f1caf65d89 Mon Sep 17 00:00:00 2001 From: Juan Deniz Date: Thu, 23 Feb 2023 11:21:42 +1100 Subject: [PATCH 21/22] NEB-1737 Refined rspec tests --- spec/examples/by_set_pos_spec.rb | 32 ++++++++++++++++++++++++++++++-- 1 file changed, 30 insertions(+), 2 deletions(-) diff --git a/spec/examples/by_set_pos_spec.rb b/spec/examples/by_set_pos_spec.rb index f73ebbf7..b0d80cdc 100644 --- a/spec/examples/by_set_pos_spec.rb +++ b/spec/examples/by_set_pos_spec.rb @@ -144,7 +144,7 @@ module IceCube context "when the rule is the first 4 after the last days of the month by set pos" do let(:from_ical) { "RRULE:FREQ=MONTHLY;COUNT=4;BYMONTHDAY=28,29,30,31;BYSETPOS=4" } - let(:schedule_start) { Time.new(2023, 0o2, 22, 12, 0, 0) } + let(:schedule_start) { Time.new(2023, 2, 22, 12, 0, 0) } let(:from_time) { Time.new(2022, 10, 1) } let(:to_time) { Time.new(2023, 10, 31) } it "returns the first 4 previous last days of the month by set pos" do @@ -161,7 +161,7 @@ module IceCube context "when the rule is the first 4 after the last wednesday of the month" do let(:from_ical) { "RRULE:FREQ=MONTHLY;COUNT=4;BYDAY=WE;BYSETPOS=-1" } - let(:schedule_start) { Time.new(2023, 0o2, 22, 12, 0, 0) } + let(:schedule_start) { Time.new(2023, 2, 22, 12, 0, 0) } let(:from_time) { Time.new(2022, 10, 1) } let(:to_time) { Time.new(2023, 10, 31) } it "returns the first 4 previous last days of the month by set pos" do @@ -177,6 +177,34 @@ module IceCube end end + describe MonthlyRule, "BYSETPOS and BYDAY" do + context "when the rules include more varied set of BYDAY values" do + let(:start_date) { Date.new(2023, 1, 1) } + + it "generates the expected dates" do + rrules = [ + "RRULE:FREQ=MONTHLY;BYDAY=1FR,3FR;BYSETPOS=1,3", + "RRULE:FREQ=MONTHLY;BYDAY=1MO,-1MO;BYSETPOS=1,-1", + "RRULE:FREQ=MONTHLY;BYDAY=2WE,4WE;BYSETPOS=2,4", + "RRULE:FREQ=MONTHLY;BYDAY=MO,TU,WE,TH,FR;BYSETPOS=-1", + "RRULE:FREQ=MONTHLY;BYDAY=2FR;BYMONTHDAY=16,17,18,19,20,21,22;BYSETPOS=2" + ] + + expected_dates = [ + Date.new(2023, 3, 3), Date.new(2023, 2, 27), + Date.new(2023, 3, 22), Date.new(2023, 2, 28), + nil + ] + + rrules.each_with_index do |rrule, i| + schedule = IceCube::Schedule.from_ical(rrule) + dates = schedule.occurrences_between(start_date, start_date + 6.months) + expect(dates.map(&:to_date).first).to eq(expected_dates[i]) + end + end + end + end + describe YearlyRule, 'BYSETPOS' do it 'should behave correctly' do From d2f2c830ab06f41f10466f296f7bc57139222dbd Mon Sep 17 00:00:00 2001 From: Juan Deniz Date: Thu, 23 Feb 2023 16:59:54 +1100 Subject: [PATCH 22/22] NEB-1731 Fully merged with latest PR-449 --- .../validations/monthly_by_set_pos.rb | 2 +- lib/ice_cube/validations/weekly_by_set_pos.rb | 7 +- lib/ice_cube/validations/yearly_by_set_pos.rb | 2 +- spec/examples/by_set_pos_spec.rb | 286 +++++++----------- spec/examples/from_ical_spec.rb | 8 +- 5 files changed, 122 insertions(+), 183 deletions(-) diff --git a/lib/ice_cube/validations/monthly_by_set_pos.rb b/lib/ice_cube/validations/monthly_by_set_pos.rb index 700c1cac..c4a53b3f 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 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/weekly_by_set_pos.rb b/lib/ice_cube/validations/weekly_by_set_pos.rb index f7c9f791..1e8fd083 100644 --- a/lib/ice_cube/validations/weekly_by_set_pos.rb +++ b/lib/ice_cube/validations/weekly_by_set_pos.rb @@ -3,7 +3,7 @@ module Validations::WeeklyBySetPos 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 @@ -14,7 +14,6 @@ def by_set_pos(*by_set_pos) end class Validation - attr_reader :rule, :by_set_pos def initialize(by_set_pos, rule) @@ -36,7 +35,7 @@ def validate(step_time, start_time) start_day_of_week = TimeUtil.sym_to_wday(rule.week_start) step_time_day_of_week = step_time_date.wday days_delta = step_time_day_of_week - start_day_of_week - days_to_start = days_delta >= 0 ? days_delta : 7 + days_delta + days_to_start = (days_delta >= 0) ? days_delta : 7 + days_delta start_of_week_date = step_time_date - days_to_start end_of_week_date = start_of_week_date + 6 start_of_week = IceCube::TimeUtil.build_in_zone( @@ -82,7 +81,7 @@ def build_hash(builder) end def build_ical(builder) - builder['BYSETPOS'] << by_set_pos + builder["BYSETPOS"] << by_set_pos end nil diff --git a/lib/ice_cube/validations/yearly_by_set_pos.rb b/lib/ice_cube/validations/yearly_by_set_pos.rb index 4f67ddd7..441ec14e 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 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 diff --git a/spec/examples/by_set_pos_spec.rb b/spec/examples/by_set_pos_spec.rb index b0d80cdc..0ebc14a7 100644 --- a/spec/examples/by_set_pos_spec.rb +++ b/spec/examples/by_set_pos_spec.rb @@ -1,253 +1,193 @@ require File.dirname(__FILE__) + "/../spec_helper" module IceCube - describe WeeklyRule, 'BYSETPOS' do - it 'should behave correctly' do + describe WeeklyRule, "BYSETPOS" do + it "should behave correctly" do # Weekly on Monday, Wednesday, and Friday with the week starting on Wednesday, the last day of the set schedule = IceCube::Schedule.from_ical("RRULE:FREQ=WEEKLY;COUNT=4;WKST=WE;BYDAY=MO,WE,FR;BYSETPOS=-1") schedule.start_time = Time.new(2022, 12, 27, 12, 0, 0) - expect(schedule.occurrences_between(Time.new(2022, 01, 01), Time.new(2024, 01, 01))). - to eq([ - Time.new(2023,1,2,12,0,0), - Time.new(2023,1,9,12,0,0), - Time.new(2023,1,16,12,0,0), - Time.new(2023,1,23,12,0,0) - ]) + expect(schedule.occurrences_between(Time.new(2022, 1, 1), Time.new(2024, 1, 1))) + .to eq([ + Time.new(2023, 1, 2, 12, 0, 0), + Time.new(2023, 1, 9, 12, 0, 0), + Time.new(2023, 1, 16, 12, 0, 0), + Time.new(2023, 1, 23, 12, 0, 0) + ]) end - it 'should work with intervals' do + it "should work with intervals" do # Every 2 weeks on Monday, Wednesday, and Friday with the week starting on Wednesday, the last day of the set schedule = IceCube::Schedule.from_ical("RRULE:FREQ=WEEKLY;COUNT=4;INTERVAL=2;WKST=WE;BYDAY=MO,WE,FR;BYSETPOS=-1") schedule.start_time = Time.new(2022, 12, 27, 12, 0, 0) - expect(schedule.occurrences_between(Time.new(2022, 01, 01), Time.new(2024, 01, 01))). - to eq([ - Time.new(2023,1,9,12,0,0), - Time.new(2023,1,23,12,0,0), - Time.new(2023,2,6,12,0,0), - Time.new(2023,2,20,12,0,0) - ]) + expect(schedule.occurrences_between(Time.new(2022, 1, 1), Time.new(2024, 1, 1))) + .to eq([ + Time.new(2023, 1, 9, 12, 0, 0), + Time.new(2023, 1, 23, 12, 0, 0), + Time.new(2023, 2, 6, 12, 0, 0), + Time.new(2023, 2, 20, 12, 0, 0) + ]) end end - describe MonthlyRule, 'BYSETPOS' do - it 'should behave correctly' do + 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) - ]) + expect(schedule.occurrences_between(Time.new(2015, 1, 1), Time.new(2017, 1, 1))) + .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 - it 'should work with intervals' do + it "should work with intervals" do schedule = IceCube::Schedule.from_ical "RRULE:FREQ=MONTHLY;COUNT=4;BYDAY=WE;BYSETPOS=4;INTERVAL=2" 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,7,22,12,0,0), - Time.new(2015,9,23,12,0,0), - Time.new(2015,11,25,12,0,0), - Time.new(2016,1,27,12,0,0), - ]) + expect(schedule.occurrences_between(Time.new(2015, 1, 1), Time.new(2017, 1, 1))) + .to eq([ + Time.new(2015, 7, 22, 12, 0, 0), + Time.new(2015, 9, 23, 12, 0, 0), + Time.new(2015, 11, 25, 12, 0, 0), + Time.new(2016, 1, 27, 12, 0, 0) + ]) end - context 'when billing occurs at the end of the month' do - it 'should return the correct end of the months dates' do + context "when billing occurs at the end of the month" do + it "should return the correct end of the months dates" do schedule = IceCube::Schedule.from_ical "RRULE:FREQ=MONTHLY;COUNT=4;BYMONTHDAY=28,29,30,31;BYSETPOS=-1" schedule.start_time = Time.new(2022, 11, 1, 12, 0, 0) - expect(schedule.occurrences_between(Time.new(2022, 10, 01), Time.new(2023, 10, 31))).to eq([ - Time.new(2022,11,30,12,0,0), - Time.new(2022,12,31,12,0,0), - Time.new(2023,01,31,12,0,0), - Time.new(2023,02,28,12,0,0) - ]) + expect(schedule.occurrences_between(Time.new(2022, 10, 1), Time.new(2023, 10, 31))).to eq([ + Time.new(2022, 11, 30, 12, 0, 0), + Time.new(2022, 12, 31, 12, 0, 0), + Time.new(2023, 1, 31, 12, 0, 0), + Time.new(2023, 2, 28, 12, 0, 0) + ]) end end - end - describe MonthlyRule, "BYSETPOS" do - subject(:schedule) { IceCube::Schedule.from_ical(from_ical) } - before(:each) do - schedule.start_time = schedule_start - end - let(:occurrences) { schedule.occurrences_between(from_time, to_time) } context "when the rule is the first 4 Wednesdays ..." do - let(:from_ical) { "RRULE:FREQ=MONTHLY;COUNT=4;BYDAY=WE;BYSETPOS=4" } - let(:schedule_start) { Time.new(2015, 5, 28, 12, 0, 0) } - let(:from_time) { Time.new(2015, 1, 1) } - let(:to_time) { Time.new(2017, 1, 1) } it "returns the first 4 Wednesdays ..." do - expect(occurrences).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) - ] - ) + 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, 1, 1), Time.new(2017, 1, 1))).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 context "when event occurs on a leap year" do - let(:from_ical) { "RRULE:FREQ=MONTHLY;COUNT=4;BYMONTHDAY=28,29,30,31;BYSETPOS=-1" } - let(:schedule_start) { Time.new(2019, 11, 1, 12, 0, 0) } - let(:from_time) { Time.new(2019, 10, 1) } - let(:to_time) { Time.new(2020, 10, 31) } it "returns the correct end of the months date including the leap month" do - expect(occurrences).to eq( - [ - Time.new(2019, 11, 30, 12, 0, 0), - Time.new(2019, 12, 31, 12, 0, 0), - Time.new(2020, 1, 31, 12, 0, 0), - Time.new(2020, 2, 29, 12, 0, 0) - ] - ) + schedule = IceCube::Schedule.from_ical "RRULE:FREQ=MONTHLY;COUNT=4;BYMONTHDAY=28,29,30,31;BYSETPOS=-1" + schedule.start_time = Time.new(2019, 11, 1, 12, 0, 0) + expect(schedule.occurrences_between(Time.new(2019, 10, 1), Time.new(2020, 10, 31))).to eq([ + Time.new(2019, 11, 30, 12, 0, 0), + Time.new(2019, 12, 31, 12, 0, 0), + Time.new(2020, 1, 31, 12, 0, 0), + Time.new(2020, 2, 29, 12, 0, 0) + ]) end end context "when the rule is the first 4 last days of the month by set pos" do - let(:from_ical) { "RRULE:FREQ=MONTHLY;COUNT=4;BYMONTHDAY=28,29,30,31;BYSETPOS=-1" } - let(:schedule_start) { Time.new(2022, 11, 1, 12, 0, 0) } - let(:from_time) { Time.new(2022, 10, 1) } - let(:to_time) { Time.new(2023, 10, 31) } it "returns the first 4 last days of the month by set pos" do - expect(occurrences).to eq( - [ - Time.new(2022, 11, 30, 12, 0, 0), - Time.new(2022, 12, 31, 12, 0, 0), - Time.new(2023, 1, 31, 12, 0, 0), - Time.new(2023, 2, 28, 12, 0, 0) - ] - ) + schedule = IceCube::Schedule.from_ical "RRULE:FREQ=MONTHLY;COUNT=4;BYMONTHDAY=28,29,30,31;BYSETPOS=-1" + schedule.start_time = Time.new(2022, 11, 1, 12, 0, 0) + expect(schedule.occurrences_between(Time.new(2022, 10, 1), Time.new(2023, 10, 31))).to eq([ + Time.new(2022, 11, 30, 12, 0, 0), + Time.new(2022, 12, 31, 12, 0, 0), + Time.new(2023, 1, 31, 12, 0, 0), + Time.new(2023, 2, 28, 12, 0, 0) + ]) end end context "when the rule is for the first 4 second last days of the month by set pos" do - let(:from_ical) { "RRULE:FREQ=MONTHLY;COUNT=4;BYMONTHDAY=27,28,29,30,31;BYSETPOS=-2" } - let(:schedule_start) { Time.new(2022, 11, 1, 12, 0, 0) } - let(:from_time) { Time.new(2022, 10, 1) } - let(:to_time) { Time.new(2023, 10, 31) } it "returns the first 4 previous last days of the month by set pos" do - expect(occurrences).to eq( - [ - Time.new(2022, 11, 29, 12, 0, 0), - Time.new(2022, 12, 30, 12, 0, 0), - Time.new(2023, 1, 30, 12, 0, 0), - Time.new(2023, 2, 27, 12, 0, 0) - ] - ) + schedule = IceCube::Schedule.from_ical "RRULE:FREQ=MONTHLY;COUNT=4;BYMONTHDAY=27,28,29,30,31;BYSETPOS=-2" + schedule.start_time = Time.new(2022, 11, 1, 12, 0, 0) + expect(schedule.occurrences_between(Time.new(2022, 10, 1), Time.new(2023, 10, 31))).to eq([ + Time.new(2022, 11, 29, 12, 0, 0), + Time.new(2022, 12, 30, 12, 0, 0), + Time.new(2023, 1, 30, 12, 0, 0), + Time.new(2023, 2, 27, 12, 0, 0) + ]) end end context "when the rule is the first 4 after the last days of the month by set pos" do - let(:from_ical) { "RRULE:FREQ=MONTHLY;COUNT=4;BYMONTHDAY=28,29,30,31;BYSETPOS=4" } - let(:schedule_start) { Time.new(2023, 2, 22, 12, 0, 0) } - let(:from_time) { Time.new(2022, 10, 1) } - let(:to_time) { Time.new(2023, 10, 31) } it "returns the first 4 previous last days of the month by set pos" do - expect(occurrences).to eq( - [ - Time.new(2023, 3, 31, 12, 0, 0), - Time.new(2023, 5, 31, 12, 0, 0), - Time.new(2023, 7, 31, 12, 0, 0), - Time.new(2023, 8, 31, 12, 0, 0) - ] - ) + schedule = IceCube::Schedule.from_ical "RRULE:FREQ=MONTHLY;COUNT=4;BYMONTHDAY=28,29,30,31;BYSETPOS=4" + schedule.start_time = Time.new(2023, 2, 22, 12, 0, 0) + expect(schedule.occurrences_between(Time.new(2022, 10, 1), Time.new(2023, 10, 31))).to eq([ + Time.new(2023, 3, 31, 12, 0, 0), + Time.new(2023, 5, 31, 12, 0, 0), + Time.new(2023, 7, 31, 12, 0, 0), + Time.new(2023, 8, 31, 12, 0, 0) + ]) end end context "when the rule is the first 4 after the last wednesday of the month" do - let(:from_ical) { "RRULE:FREQ=MONTHLY;COUNT=4;BYDAY=WE;BYSETPOS=-1" } - let(:schedule_start) { Time.new(2023, 2, 22, 12, 0, 0) } - let(:from_time) { Time.new(2022, 10, 1) } - let(:to_time) { Time.new(2023, 10, 31) } it "returns the first 4 previous last days of the month by set pos" do - expect(occurrences).to eq( - [ - Time.new(2023, 2, 22, 12, 0, 0), - Time.new(2023, 3, 29, 12, 0, 0), - Time.new(2023, 4, 26, 12, 0, 0), - Time.new(2023, 5, 31, 12, 0, 0) - ] - ) - end - end - end - - describe MonthlyRule, "BYSETPOS and BYDAY" do - context "when the rules include more varied set of BYDAY values" do - let(:start_date) { Date.new(2023, 1, 1) } - - it "generates the expected dates" do - rrules = [ - "RRULE:FREQ=MONTHLY;BYDAY=1FR,3FR;BYSETPOS=1,3", - "RRULE:FREQ=MONTHLY;BYDAY=1MO,-1MO;BYSETPOS=1,-1", - "RRULE:FREQ=MONTHLY;BYDAY=2WE,4WE;BYSETPOS=2,4", - "RRULE:FREQ=MONTHLY;BYDAY=MO,TU,WE,TH,FR;BYSETPOS=-1", - "RRULE:FREQ=MONTHLY;BYDAY=2FR;BYMONTHDAY=16,17,18,19,20,21,22;BYSETPOS=2" - ] - - expected_dates = [ - Date.new(2023, 3, 3), Date.new(2023, 2, 27), - Date.new(2023, 3, 22), Date.new(2023, 2, 28), - nil - ] - - rrules.each_with_index do |rrule, i| - schedule = IceCube::Schedule.from_ical(rrule) - dates = schedule.occurrences_between(start_date, start_date + 6.months) - expect(dates.map(&:to_date).first).to eq(expected_dates[i]) - end + schedule = IceCube::Schedule.from_ical "RRULE:FREQ=MONTHLY;COUNT=4;BYDAY=WE;BYSETPOS=-1" + schedule.start_time = Time.new(2023, 2, 22, 12, 0, 0) + expect(schedule.occurrences_between(Time.new(2022, 10, 1), Time.new(2023, 10, 31))).to eq([ + Time.new(2023, 2, 22, 12, 0, 0), + Time.new(2023, 3, 29, 12, 0, 0), + Time.new(2023, 4, 26, 12, 0, 0), + Time.new(2023, 5, 31, 12, 0, 0) + ]) end end end - - describe YearlyRule, 'BYSETPOS' do - it 'should behave correctly' do + 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) + expect(schedule.occurrences_between(Time.new(2015, 1, 1), Time.new(2017, 1, 1))) + .to eq([ + Time.new(2015, 7, 31), + Time.new(2016, 7, 31) + ]) end - it 'should work with intervals' do + it "should work with intervals" do schedule = IceCube::Schedule.from_ical "RRULE:FREQ=YEARLY;BYMONTH=7;BYDAY=SU,MO,TU,WE,TH,FR,SA;BYSETPOS=-1;INTERVAL=2" - schedule.start_time = Time.new(1966,7,5) - expect(schedule.occurrences_between(Time.new(2015, 01, 01), Time.new(2023, 01, 01))). - to eq([ + schedule.start_time = Time.new(1966, 7, 5) + expect(schedule.occurrences_between(Time.new(2015, 1, 1), Time.new(2023, 1, 1))) + .to eq([ Time.new(2016, 7, 31), Time.new(2018, 7, 31), Time.new(2020, 7, 31), - Time.new(2022, 7, 31), + Time.new(2022, 7, 31) ]) end - it 'should work with counts' do + it "should work with counts" do schedule = IceCube::Schedule.from_ical "RRULE:FREQ=YEARLY;BYMONTH=7;BYDAY=SU,MO,TU,WE,TH,FR,SA;BYSETPOS=-1;COUNT=3" - schedule.start_time = Time.new(2016,1,1) - expect(schedule.occurrences_between(Time.new(2016, 01, 01), Time.new(2050, 01, 01))). - to eq([ + schedule.start_time = Time.new(2016, 1, 1) + expect(schedule.occurrences_between(Time.new(2016, 1, 1), Time.new(2050, 1, 1))) + .to eq([ Time.new(2016, 7, 31), Time.new(2017, 7, 31), - Time.new(2018, 7, 31), + Time.new(2018, 7, 31) ]) end - it 'should work with counts and intervals' do + it "should work with counts and intervals" do schedule = IceCube::Schedule.from_ical "RRULE:FREQ=YEARLY;BYMONTH=7;BYDAY=SU,MO,TU,WE,TH,FR,SA;BYSETPOS=-1;COUNT=3;INTERVAL=2" - schedule.start_time = Time.new(2016,1,1) - expect(schedule.occurrences_between(Time.new(2016, 01, 01), Time.new(2050, 01, 01))). - to eq([ + schedule.start_time = Time.new(2016, 1, 1) + expect(schedule.occurrences_between(Time.new(2016, 1, 1), Time.new(2050, 1, 1))) + .to eq([ Time.new(2016, 7, 31), Time.new(2018, 7, 31), - Time.new(2020, 7, 31), + Time.new(2020, 7, 31) ]) end end diff --git a/spec/examples/from_ical_spec.rb b/spec/examples/from_ical_spec.rb index d332e026..257bf7b6 100644 --- a/spec/examples/from_ical_spec.rb +++ b/spec/examples/from_ical_spec.rb @@ -86,10 +86,10 @@ 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 raise when by_set_pos is out of range (BYSETPOS)' do + it "should raise when by_set_pos is out of range (BYSETPOS)" do expect { IceCube::Rule.from_ical("FREQ=MONTHLY;BYDAY=MO,WE;BYSETPOS=-367") }.to raise_error(/Expecting number in \[-366, -1\] or \[1, 366\]/) @@ -103,7 +103,7 @@ module IceCube }.to raise_error(/Expecting number in \[-366, -1\] or \[1, 366\]/) end - it 'should return no occurrences after daily interval with count is over' do + 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")) expect(schedule.occurrences_between(Time.now + (IceCube::ONE_DAY * 7), Time.now + (IceCube::ONE_DAY * 14)).count).to eq(0) @@ -453,7 +453,7 @@ def sorted_ical(ical) it "matches simple daily" do ical_string = "DTSTART:20130314T201500Z\nDTEND:20130314T201545Z\nRRULE:FREQ=WEEKLY;BYDAY=TH;UNT\n IL=20130531T100000Z\nDESCRIPTION:This is a test event\nSUMMARY:Test Event\n" schedule = IceCube::Schedule.from_ical(ical_string) - expect(schedule.to_ical.split(/\n/).select {|x| x =~ /RRULE/}.first).to eq("RRULE:FREQ=WEEKLY;UNTIL=20130531T100000Z;BYDAY=TH") + expect(schedule.to_ical.split("\n").find { |x| x =~ /RRULE/ }).to eq("RRULE:FREQ=WEEKLY;UNTIL=20130531T100000Z;BYDAY=TH") end end end