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/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) 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.rb b/lib/ice_cube.rb index 3c283f53..12ede342 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/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 429bd84c..36a0a548 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/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 001d927d..d14c74bf 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 @@ -51,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 @@ -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) @@ -286,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 @@ -318,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/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/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 new file mode 100644 index 00000000..04098430 --- /dev/null +++ b/lib/ice_cube/validations/monthly_by_set_pos.rb @@ -0,0 +1,77 @@ +module IceCube + module Validations::MonthlyBySetPos + def by_set_pos(*by_set_pos) + return by_set_pos([by_set_pos]) if by_set_pos.is_a?(Integer) + + unless by_set_pos.nil? || by_set_pos.is_a?(Array) + raise ArgumentError, "Expecting Array or nil value for count, got #{by_set_pos.inspect}" + end + by_set_pos.flatten! + by_set_pos.each do |set_pos| + unless (set_pos >= -366 && set_pos <= -1) || + (set_pos <= 366 && set_pos >= 1) + raise ArgumentError, "Expecting number in [-366, -1] or [1, 366], got #{set_pos} (#{by_set_pos})" + end + end + + @by_set_pos = by_set_pos + replace_validations_for(:by_set_pos, by_set_pos && [Validation.new(by_set_pos, self)]) + self + end + + class Validation + attr_reader :rule, :by_set_pos + + def initialize(by_set_pos, rule) + @by_set_pos = by_set_pos + @rule = rule + end + + def type + :day + end + + def dst_adjust? + true + end + + def validate(step_time, schedule) + start_of_month = 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.except(:by_set_pos, :count, :until)) + end + + occurrences = new_schedule.occurrences_between(start_of_month, end_of_month) + index = occurrences.index(step_time) + if index.nil? + 1 + else + positive_set_pos = index + 1 + negative_set_pos = index - occurrences.length + + if @by_set_pos.include?(positive_set_pos) || @by_set_pos.include?(negative_set_pos) + 0 + else + 1 + end + end + end + + def build_s(builder) + builder.piece(:by_set_pos) << by_set_pos + end + + def build_hash(builder) + builder[:by_set_pos] = by_set_pos + end + + def build_ical(builder) + builder["BYSETPOS"] << by_set_pos + end + + nil + end + end +end diff --git a/lib/ice_cube/validations/yearly_by_set_pos.rb b/lib/ice_cube/validations/yearly_by_set_pos.rb new file mode 100644 index 00000000..d31c57af --- /dev/null +++ b/lib/ice_cube/validations/yearly_by_set_pos.rb @@ -0,0 +1,78 @@ +module IceCube + module Validations::YearlyBySetPos + def by_set_pos(*by_set_pos) + return by_set_pos([by_set_pos]) if by_set_pos.is_a?(Integer) + + unless by_set_pos.nil? || by_set_pos.is_a?(Array) + raise ArgumentError, "Expecting Array or nil value for count, got #{by_set_pos.inspect}" + end + by_set_pos.flatten! + by_set_pos.each do |set_pos| + unless (set_pos >= -366 && set_pos <= -1) || + (set_pos <= 366 && set_pos >= 1) + raise ArgumentError, "Expecting number in [-366, -1] or [1, 366], got #{set_pos} (#{by_set_pos})" + end + end + + @by_set_pos = by_set_pos + replace_validations_for(:by_set_pos, by_set_pos && [Validation.new(by_set_pos, self)]) + self + end + + class Validation + attr_reader :rule, :by_set_pos + + def initialize(by_set_pos, rule) + @by_set_pos = by_set_pos + @rule = rule + end + + def type + :day + end + + def dst_adjust? + true + end + + def validate(step_time, schedule) + start_of_year = 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.except(:by_set_pos, :count, :until)) + 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..af3b51ca --- /dev/null +++ b/spec/examples/by_set_pos_spec.rb @@ -0,0 +1,155 @@ +require File.dirname(__FILE__) + "/../spec_helper" + +module IceCube + 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, 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) + ] + ) + 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 + end + end + end + + describe YearlyRule, "BYSETPOS" do + subject(:schedule) { IceCube::Schedule.from_ical(from_ical) } + let(:from_ical) { "RRULE:FREQ=YEARLY;BYMONTH=7;BYDAY=SU,MO,TU,WE,TH,FR,SA;BYSETPOS=-1" } + before(:each) do + schedule.start_time = schedule_start_time + end + let(:occurrences) { schedule.occurrences_between(from_time, to_time) } + let(:schedule_start_time) { Time.new(1966, 7, 5) } + let(:from_time) { Time.new(2015, 1, 1) } + let(:to_time) { Time.new(2017, 1, 1) } + it "returns only the last day of each July in 2015 and 2016" do + expect(occurrences).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..0912becd 100644 --- a/spec/examples/from_ical_spec.rb +++ b/spec/examples/from_ical_spec.rb @@ -84,6 +84,15 @@ module IceCube expect(rule).to eq(IceCube::Rule.weekly(2, :monday)) end + context "when from_ical contains BYSETPOS" do + let(:from_ical) { "FREQ=MONTHLY;BYDAY=MO,WE;BYSETPOS=-1,1" } + let(:rule) { IceCube::Rule.from_ical(from_ical) } + it "parses BYSETPOS input" do + expect(rule.to_ical).to eq(from_ical) + expect(rule).to eq(IceCube::Rule.monthly.day(:monday, :wednesday).by_set_pos([-1, 1])) + end + 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")) 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/occurrence_spec.rb b/spec/examples/occurrence_spec.rb index 80c18421..be358104 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 @@ -28,7 +30,7 @@ time_now = Time.current occurrence = Occurrence.new(time_now) - expect(occurrence.to_s(:short)).to eq time_now.to_s(:short) + expect(occurrence.strftime("%Y-%m-%d")).to eq time_now.strftime("%Y-%m-%d") end end diff --git a/spec/examples/time_util_spec.rb b/spec/examples/time_util_spec.rb index 43491a8b..ce2918c9 100644 --- a/spec/examples/time_util_spec.rb +++ b/spec/examples/time_util_spec.rb @@ -92,6 +92,15 @@ module IceCube end 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(serialize_time).to eq(iso_time_str) + 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 e7c62c59..28bd9d83 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 @@ -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 @@ -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 @@ -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 @@ -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)