From d30098a5cd7524911eb9e5e54e19630718920e49 Mon Sep 17 00:00:00 2001 From: Ross Kaffenberger Date: Wed, 17 Jul 2019 21:06:16 -0400 Subject: [PATCH 01/33] Add Montrose::Recurrence.from_yaml --- lib/montrose/recurrence.rb | 8 +++++++- spec/montrose/recurrence_spec.rb | 9 +++++++++ 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/lib/montrose/recurrence.rb b/lib/montrose/recurrence.rb index 74f620c..a01c138 100644 --- a/lib/montrose/recurrence.rb +++ b/lib/montrose/recurrence.rb @@ -250,6 +250,12 @@ def load(json) rescue JSON::ParserError => e fail SerializationError, "Could not parse JSON: #{e}" end + + alias from_json load + + def from_yaml(yaml) + new(YAML.safe_load(yaml)) + end end def initialize(opts = {}) @@ -331,7 +337,7 @@ def as_json(*args) # @return [String] YAML-formatted recurrence options # def to_yaml(*args) - YAML.dump(JSON.parse(to_json(*args))) + YAML.dump(as_json(*args)) end def inspect diff --git a/spec/montrose/recurrence_spec.rb b/spec/montrose/recurrence_spec.rb index b877407..6f38b95 100644 --- a/spec/montrose/recurrence_spec.rb +++ b/spec/montrose/recurrence_spec.rb @@ -175,6 +175,15 @@ end end + describe ".from_yaml" do + it "returns Recurrence instance" do + yaml = "---\nevery: day\n" + recurrence = Montrose::Recurrence.from_yaml(yaml) + + recurrence.default_options[:every].must_equal :day + end + end + describe "#inspect" do let(:now) { time_now } let(:recurrence) { new_recurrence(every: :month, starts: now, interval: 1) } From ffa7098b0243c2e26a1015cce406dd462ce0f893 Mon Sep 17 00:00:00 2001 From: Ross Kaffenberger Date: Wed, 17 Jul 2019 22:01:52 -0400 Subject: [PATCH 02/33] Partially implement Recurrence#from_ical --- lib/montrose/frequency.rb | 6 ++++ lib/montrose/ical.rb | 54 ++++++++++++++++++++++++++++++++ lib/montrose/recurrence.rb | 5 +++ spec/montrose/recurrence_spec.rb | 12 +++++++ 4 files changed, 77 insertions(+) create mode 100644 lib/montrose/ical.rb diff --git a/lib/montrose/frequency.rb b/lib/montrose/frequency.rb index dcafe58..4e120fb 100644 --- a/lib/montrose/frequency.rb +++ b/lib/montrose/frequency.rb @@ -37,6 +37,12 @@ def self.from_options(opts) Montrose::Frequency.const_get(class_name).new(opts) end + def self.from_term(term) + FREQUENCY_TERMS.invert.map { |k, v| [k.downcase, v] }.to_h.fetch(term.downcase) do + fail "Don't know how to convert #{term} to a Montrose frequency" + end + end + # @private def self.assert(frequency) FREQUENCY_TERMS.key?(frequency.to_s) || fail(ConfigurationError, diff --git a/lib/montrose/ical.rb b/lib/montrose/ical.rb new file mode 100644 index 0000000..84388ee --- /dev/null +++ b/lib/montrose/ical.rb @@ -0,0 +1,54 @@ +# frozen_string_literal: true + +require "montrose/frequency" + +module Montrose + class ICal + # DTSTART;TZID=US-Eastern:19970902T090000 + # RRULE:FREQ=DAILY;INTERVAL=2 + def self.parse(ical) + new(ical).parse + end + + def initialize(ical) + @ical = ical + end + + def parse + Hash[*@ical.each_line.flat_map { |line| parse_line(line) }] + end + + private + + def parse_line(line) + line = line.strip + case line + when %r{^DTSTART} + parse_dtstart(line) + when %r{^RRULE} + parse_rrule(line) + end + end + + def parse_dtstart(line) + _label, time_string = line.split(";") + + [:starts, Montrose::Utils.parse_time(time_string)] + end + + def parse_rrule(line) + _label, rule_string = line.split(":") + rule_string.split(";").flat_map do |rule| + prop, value = rule.split("=") + case prop + when "FREQ" + [:every, Montrose::Frequency.from_term(value)] + when "INTERVAL" + [:interval, value.to_i] + when "COUNT" + [:total, value.to_i] + end + end + end + end +end diff --git a/lib/montrose/recurrence.rb b/lib/montrose/recurrence.rb index a01c138..5cab162 100644 --- a/lib/montrose/recurrence.rb +++ b/lib/montrose/recurrence.rb @@ -6,6 +6,7 @@ require "montrose/errors" require "montrose/stack" require "montrose/clock" +require "montrose/ical" module Montrose # Represents the rules for a set of recurring events. Can be instantiated @@ -256,6 +257,10 @@ def load(json) def from_yaml(yaml) new(YAML.safe_load(yaml)) end + + def from_ical(ical) + new(Montrose::ICal.parse(ical)) + end end def initialize(opts = {}) diff --git a/spec/montrose/recurrence_spec.rb b/spec/montrose/recurrence_spec.rb index 6f38b95..7040ae2 100644 --- a/spec/montrose/recurrence_spec.rb +++ b/spec/montrose/recurrence_spec.rb @@ -184,6 +184,18 @@ end end + describe ".from_ical" do + it "returns Recurrence instance" do + ical = "DTSTART;TZID=US-Eastern:19970902T090000\nRRULE:FREQ=DAILY;COUNT=10;INTERVAL=2" + recurrence = Montrose::Recurrence.from_ical(ical) + + recurrence.default_options[:every].must_equal :day + recurrence.default_options[:total].must_equal 10 + recurrence.default_options[:interval].must_equal 2 + recurrence.default_options[:starts].must_equal Time.parse("1997-09-02 09:00:00 -0400") + end + end + describe "#inspect" do let(:now) { time_now } let(:recurrence) { new_recurrence(every: :month, starts: now, interval: 1) } From f049de877a44919165209234dfe7a349017a39a1 Mon Sep 17 00:00:00 2001 From: Ross Kaffenberger Date: Fri, 5 Feb 2021 16:26:26 -0500 Subject: [PATCH 03/33] Extract Montrose::Month for month number parsing --- lib/montrose.rb | 2 ++ lib/montrose/month.rb | 31 ++++++++++++++++++ lib/montrose/options.rb | 4 +-- lib/montrose/utils.rb | 16 --------- spec/montrose/month_spec.rb | 65 +++++++++++++++++++++++++++++++++++++ spec/montrose/utils_spec.rb | 55 ------------------------------- 6 files changed, 100 insertions(+), 73 deletions(-) create mode 100644 lib/montrose/month.rb create mode 100644 spec/montrose/month_spec.rb diff --git a/lib/montrose.rb b/lib/montrose.rb index 68803e3..5a9bf7f 100644 --- a/lib/montrose.rb +++ b/lib/montrose.rb @@ -21,6 +21,8 @@ require "montrose/version" module Montrose + autoload :Month, "montrose/month" + extend Chainable class << self diff --git a/lib/montrose/month.rb b/lib/montrose/month.rb new file mode 100644 index 0000000..71cc3e4 --- /dev/null +++ b/lib/montrose/month.rb @@ -0,0 +1,31 @@ +module Montrose + class Month + extend Montrose::Utils + + NAMES = ::Date::MONTHNAMES # starts with nil to match 1-12 numbering + + def self.names + NAMES + end + + def self.numbers + @numbers ||= NAMES.map.with_index { |_n, i| i.to_s }.slice(1, 12) + end + + def self.number(name) + case name + when Symbol, String + string = name.to_s + NAMES.index(string.titleize) || number(to_index(string)) + when 1..12 + name + end + end + + def self.number!(name) + numbers = NAMES.map.with_index { |_n, i| i.to_s }.slice(1, 12) + number(name) || raise(ConfigurationError, + "Did not recognize month #{name}, must be one of #{(NAMES + numbers).inspect}") + end + end +end diff --git a/lib/montrose/options.rb b/lib/montrose/options.rb index fd48e4e..9f6b8f3 100644 --- a/lib/montrose/options.rb +++ b/lib/montrose/options.rb @@ -214,7 +214,7 @@ def week=(weeks) end def month=(months) - @month = map_arg(months) { |d| month_number!(d) } + @month = map_arg(months) { |d| Montrose::Month.number!(d) } end def between=(range) @@ -327,7 +327,7 @@ def decompose_on_arg(arg) end def month_or_day(key) - month = month_number(key) + month = Montrose::Month.number(key) return [:month, month] if month day = day_number(key) diff --git a/lib/montrose/utils.rb b/lib/montrose/utils.rb index 28e9dcf..fd47e93 100644 --- a/lib/montrose/utils.rb +++ b/lib/montrose/utils.rb @@ -44,22 +44,6 @@ def current_time ::Time.current end - def month_number(name) - case name - when Symbol, String - string = name.to_s - MONTHS.index(string.titleize) || month_number(to_index(string)) - when 1..12 - name - end - end - - def month_number!(name) - month_numbers = MONTHS.map.with_index { |_n, i| i.to_s }.slice(1, 12) - month_number(name) || raise(ConfigurationError, - "Did not recognize month #{name}, must be one of #{(MONTHS + month_numbers).inspect}") - end - def day_number(name) case name when 0..6 diff --git a/spec/montrose/month_spec.rb b/spec/montrose/month_spec.rb new file mode 100644 index 0000000..4e000cb --- /dev/null +++ b/spec/montrose/month_spec.rb @@ -0,0 +1,65 @@ +# frozen_string_literal: true + +require "spec_helper" + +describe Montrose::Month do + def number!(name) + Montrose::Month.number!(name) + end + + describe "#number!" do + it { _(number!(:january)).must_equal 1 } + it { _(number!(:february)).must_equal 2 } + it { _(number!(:march)).must_equal 3 } + it { _(number!(:april)).must_equal 4 } + it { _(number!(:may)).must_equal 5 } + it { _(number!(:june)).must_equal 6 } + it { _(number!(:july)).must_equal 7 } + it { _(number!(:august)).must_equal 8 } + it { _(number!(:september)).must_equal 9 } + it { _(number!(:october)).must_equal 10 } + it { _(number!(:november)).must_equal 11 } + it { _(number!(:december)).must_equal 12 } + it { _(number!("january")).must_equal 1 } + it { _(number!("february")).must_equal 2 } + it { _(number!("march")).must_equal 3 } + it { _(number!("april")).must_equal 4 } + it { _(number!("may")).must_equal 5 } + it { _(number!("june")).must_equal 6 } + it { _(number!("july")).must_equal 7 } + it { _(number!("august")).must_equal 8 } + it { _(number!("september")).must_equal 9 } + it { _(number!("october")).must_equal 10 } + it { _(number!("november")).must_equal 11 } + it { _(number!("december")).must_equal 12 } + it { _(number!(1)).must_equal 1 } + it { _(number!(2)).must_equal 2 } + it { _(number!(3)).must_equal 3 } + it { _(number!(4)).must_equal 4 } + it { _(number!(5)).must_equal 5 } + it { _(number!(6)).must_equal 6 } + it { _(number!(7)).must_equal 7 } + it { _(number!(8)).must_equal 8 } + it { _(number!(9)).must_equal 9 } + it { _(number!(10)).must_equal 10 } + it { _(number!(11)).must_equal 11 } + it { _(number!(12)).must_equal 12 } + it { _(number!("1")).must_equal 1 } + it { _(number!("2")).must_equal 2 } + it { _(number!("3")).must_equal 3 } + it { _(number!("4")).must_equal 4 } + it { _(number!("5")).must_equal 5 } + it { _(number!("6")).must_equal 6 } + it { _(number!("7")).must_equal 7 } + it { _(number!("8")).must_equal 8 } + it { _(number!("9")).must_equal 9 } + it { _(number!("10")).must_equal 10 } + it { _(number!("11")).must_equal 11 } + it { _(number!("12")).must_equal 12 } + it { _(-> { number!(:foo) }).must_raise Montrose::ConfigurationError } + it { _(-> { number!("foo") }).must_raise Montrose::ConfigurationError } + it { _(-> { number!(0) }).must_raise Montrose::ConfigurationError } + it { _(-> { number!(13) }).must_raise Montrose::ConfigurationError } + end + +end diff --git a/spec/montrose/utils_spec.rb b/spec/montrose/utils_spec.rb index eec089b..c177444 100644 --- a/spec/montrose/utils_spec.rb +++ b/spec/montrose/utils_spec.rb @@ -45,61 +45,6 @@ end end - describe "#month_number!" do - it { month_number!(:january).must_equal 1 } - it { month_number!(:february).must_equal 2 } - it { month_number!(:march).must_equal 3 } - it { month_number!(:april).must_equal 4 } - it { month_number!(:may).must_equal 5 } - it { month_number!(:june).must_equal 6 } - it { month_number!(:july).must_equal 7 } - it { month_number!(:august).must_equal 8 } - it { month_number!(:september).must_equal 9 } - it { month_number!(:october).must_equal 10 } - it { month_number!(:november).must_equal 11 } - it { month_number!(:december).must_equal 12 } - it { month_number!("january").must_equal 1 } - it { month_number!("february").must_equal 2 } - it { month_number!("march").must_equal 3 } - it { month_number!("april").must_equal 4 } - it { month_number!("may").must_equal 5 } - it { month_number!("june").must_equal 6 } - it { month_number!("july").must_equal 7 } - it { month_number!("august").must_equal 8 } - it { month_number!("september").must_equal 9 } - it { month_number!("october").must_equal 10 } - it { month_number!("november").must_equal 11 } - it { month_number!("december").must_equal 12 } - it { month_number!(1).must_equal 1 } - it { month_number!(2).must_equal 2 } - it { month_number!(3).must_equal 3 } - it { month_number!(4).must_equal 4 } - it { month_number!(5).must_equal 5 } - it { month_number!(6).must_equal 6 } - it { month_number!(7).must_equal 7 } - it { month_number!(8).must_equal 8 } - it { month_number!(9).must_equal 9 } - it { month_number!(10).must_equal 10 } - it { month_number!(11).must_equal 11 } - it { month_number!(12).must_equal 12 } - it { month_number!("1").must_equal 1 } - it { month_number!("2").must_equal 2 } - it { month_number!("3").must_equal 3 } - it { month_number!("4").must_equal 4 } - it { month_number!("5").must_equal 5 } - it { month_number!("6").must_equal 6 } - it { month_number!("7").must_equal 7 } - it { month_number!("8").must_equal 8 } - it { month_number!("9").must_equal 9 } - it { month_number!("10").must_equal 10 } - it { month_number!("11").must_equal 11 } - it { month_number!("12").must_equal 12 } - it { -> { month_number!(:foo) }.must_raise Montrose::ConfigurationError } - it { -> { month_number!("foo") }.must_raise Montrose::ConfigurationError } - it { -> { month_number!(0) }.must_raise Montrose::ConfigurationError } - it { -> { month_number!(13) }.must_raise Montrose::ConfigurationError } - end - describe "#day_number!" do it { day_number!(:sunday).must_equal 0 } it { day_number!(:monday).must_equal 1 } From e479ae789c277034c680090f49b1f79e68b12702 Mon Sep 17 00:00:00 2001 From: Ross Kaffenberger Date: Fri, 5 Feb 2021 16:45:28 -0500 Subject: [PATCH 04/33] Extract Montrose::Day for day number parsing --- lib/montrose.rb | 1 + lib/montrose/day.rb | 29 +++++++++++++++++++++++++ lib/montrose/options.rb | 6 +++--- lib/montrose/utils.rb | 22 ------------------- spec/montrose/day_spec.rb | 42 +++++++++++++++++++++++++++++++++++++ spec/montrose/utils_spec.rb | 34 ------------------------------ 6 files changed, 75 insertions(+), 59 deletions(-) create mode 100644 lib/montrose/day.rb create mode 100644 spec/montrose/day_spec.rb diff --git a/lib/montrose.rb b/lib/montrose.rb index 5a9bf7f..8892ffa 100644 --- a/lib/montrose.rb +++ b/lib/montrose.rb @@ -21,6 +21,7 @@ require "montrose/version" module Montrose + autoload :Day, "montrose/day" autoload :Month, "montrose/month" extend Chainable diff --git a/lib/montrose/day.rb b/lib/montrose/day.rb new file mode 100644 index 0000000..5f58e01 --- /dev/null +++ b/lib/montrose/day.rb @@ -0,0 +1,29 @@ +module Montrose + class Day + extend Montrose::Utils + + NAMES = ::Date::DAYNAMES + + def self.names + NAMES + end + + def self.number(name) + case name + when 0..6 + name + when Symbol, String + string = name.to_s + NAMES.index(string.titleize) || number(to_index(string)) + when Array + number name.first + end + end + + def self.number!(name) + numbers = NAMES.map.with_index { |_n, i| i.to_s } + number(name) || raise(ConfigurationError, + "Did not recognize day #{name}, must be one of #{(NAMES + numbers).inspect}") + end + end +end diff --git a/lib/montrose/options.rb b/lib/montrose/options.rb index 9f6b8f3..1ab42f3 100644 --- a/lib/montrose/options.rb +++ b/lib/montrose/options.rb @@ -198,7 +198,7 @@ def during=(during_arg) end def day=(days) - @day = nested_map_arg(days) { |d| day_number!(d) } + @day = nested_map_arg(days) { |d| Montrose::Day.number!(d) } end def mday=(mdays) @@ -285,7 +285,7 @@ def map_arg(arg, &block) end def map_days(arg) - map_arg(arg) { |d| day_number!(d) } + map_arg(arg) { |d| Montrose::Day.number!(d) } end def map_mdays(arg) @@ -330,7 +330,7 @@ def month_or_day(key) month = Montrose::Month.number(key) return [:month, month] if month - day = day_number(key) + day = Montrose::Day.number(key) return [:day, day] if day raise ConfigurationError, "Did not recognize #{key} as a month or day" diff --git a/lib/montrose/utils.rb b/lib/montrose/utils.rb index fd47e93..cedc32b 100644 --- a/lib/montrose/utils.rb +++ b/lib/montrose/utils.rb @@ -4,10 +4,6 @@ module Montrose module Utils module_function - MONTHS = ::Date::MONTHNAMES - - DAYS = ::Date::DAYNAMES - MAX_HOURS_IN_DAY = 24 MAX_DAYS_IN_YEAR = 366 MAX_WEEKS_IN_YEAR = 53 @@ -44,24 +40,6 @@ def current_time ::Time.current end - def day_number(name) - case name - when 0..6 - name - when Symbol, String - string = name.to_s - DAYS.index(string.titleize) || day_number(to_index(string)) - when Array - day_number name.first - end - end - - def day_number!(name) - day_numbers = DAYS.map.with_index { |_n, i| i.to_s } - day_number(name) || raise(ConfigurationError, - "Did not recognize day #{name}, must be one of #{(DAYS + day_numbers).inspect}") - end - def days_in_month(month, year = current_time.year) date = ::Date.new(year, month, 1) ((date >> 1) - date).to_i diff --git a/spec/montrose/day_spec.rb b/spec/montrose/day_spec.rb new file mode 100644 index 0000000..b89b00c --- /dev/null +++ b/spec/montrose/day_spec.rb @@ -0,0 +1,42 @@ +require "spec_helper" + +describe Montrose::Day do + def number!(name) + Montrose::Day.number!(name) + end + + describe "#number!" do + it { _(number!(:sunday)).must_equal 0 } + it { _(number!(:monday)).must_equal 1 } + it { _(number!(:tuesday)).must_equal 2 } + it { _(number!(:wednesday)).must_equal 3 } + it { _(number!(:thursday)).must_equal 4 } + it { _(number!(:friday)).must_equal 5 } + it { _(number!(:saturday)).must_equal 6 } + it { _(number!("sunday")).must_equal 0 } + it { _(number!("monday")).must_equal 1 } + it { _(number!("tuesday")).must_equal 2 } + it { _(number!("wednesday")).must_equal 3 } + it { _(number!("thursday")).must_equal 4 } + it { _(number!("friday")).must_equal 5 } + it { _(number!("saturday")).must_equal 6 } + it { _(number!(0)).must_equal 0 } + it { _(number!(1)).must_equal 1 } + it { _(number!(2)).must_equal 2 } + it { _(number!(3)).must_equal 3 } + it { _(number!(4)).must_equal 4 } + it { _(number!(5)).must_equal 5 } + it { _(number!(6)).must_equal 6 } + it { _(number!("0")).must_equal 0 } + it { _(number!("1")).must_equal 1 } + it { _(number!("2")).must_equal 2 } + it { _(number!("3")).must_equal 3 } + it { _(number!("4")).must_equal 4 } + it { _(number!("5")).must_equal 5 } + it { _(number!("6")).must_equal 6 } + it { _(-> { number!(-3) }).must_raise Montrose::ConfigurationError } + it { _(-> { number!(:foo) }).must_raise Montrose::ConfigurationError } + it { _(-> { number!("foo") }).must_raise Montrose::ConfigurationError } + end + +end diff --git a/spec/montrose/utils_spec.rb b/spec/montrose/utils_spec.rb index c177444..60f110f 100644 --- a/spec/montrose/utils_spec.rb +++ b/spec/montrose/utils_spec.rb @@ -45,40 +45,6 @@ end end - describe "#day_number!" do - it { day_number!(:sunday).must_equal 0 } - it { day_number!(:monday).must_equal 1 } - it { day_number!(:tuesday).must_equal 2 } - it { day_number!(:wednesday).must_equal 3 } - it { day_number!(:thursday).must_equal 4 } - it { day_number!(:friday).must_equal 5 } - it { day_number!(:saturday).must_equal 6 } - it { day_number!("sunday").must_equal 0 } - it { day_number!("monday").must_equal 1 } - it { day_number!("tuesday").must_equal 2 } - it { day_number!("wednesday").must_equal 3 } - it { day_number!("thursday").must_equal 4 } - it { day_number!("friday").must_equal 5 } - it { day_number!("saturday").must_equal 6 } - it { day_number!(0).must_equal 0 } - it { day_number!(1).must_equal 1 } - it { day_number!(2).must_equal 2 } - it { day_number!(3).must_equal 3 } - it { day_number!(4).must_equal 4 } - it { day_number!(5).must_equal 5 } - it { day_number!(6).must_equal 6 } - it { day_number!("0").must_equal 0 } - it { day_number!("1").must_equal 1 } - it { day_number!("2").must_equal 2 } - it { day_number!("3").must_equal 3 } - it { day_number!("4").must_equal 4 } - it { day_number!("5").must_equal 5 } - it { day_number!("6").must_equal 6 } - it { -> { day_number!(-3) }.must_raise Montrose::ConfigurationError } - it { -> { day_number!(:foo) }.must_raise Montrose::ConfigurationError } - it { -> { day_number!("foo") }.must_raise Montrose::ConfigurationError } - end - describe "#days_in_month" do non_leap_year = 2015 leap_year = 2016 From 247c4dd07ee5199b4fa60e6168e80ba1d37595bf Mon Sep 17 00:00:00 2001 From: Ross Kaffenberger Date: Fri, 5 Feb 2021 17:13:50 -0500 Subject: [PATCH 05/33] Additional implementation of ICAL RRULE parsing --- lib/montrose/day.rb | 5 +- lib/montrose/ical.rb | 33 ++++++------ lib/montrose/recurrence.rb | 2 +- spec/from_ical_rfc_spec.rb | 100 ++++++++++++++++++++++++++++++++++++ spec/montrose/day_spec.rb | 1 - spec/montrose/month_spec.rb | 1 - 6 files changed, 121 insertions(+), 21 deletions(-) create mode 100644 spec/from_ical_rfc_spec.rb diff --git a/lib/montrose/day.rb b/lib/montrose/day.rb index 5f58e01..5f35f35 100644 --- a/lib/montrose/day.rb +++ b/lib/montrose/day.rb @@ -3,6 +3,7 @@ class Day extend Montrose::Utils NAMES = ::Date::DAYNAMES + ABBREVIATIONS = %w[SU MO TU WE TH FR SA].freeze def self.names NAMES @@ -14,7 +15,9 @@ def self.number(name) name when Symbol, String string = name.to_s - NAMES.index(string.titleize) || number(to_index(string)) + NAMES.index(string.titleize) || + ABBREVIATIONS.index(string.upcase) || + number(to_index(string)) when Array number name.first end diff --git a/lib/montrose/ical.rb b/lib/montrose/ical.rb index 84388ee..fb44cb8 100644 --- a/lib/montrose/ical.rb +++ b/lib/montrose/ical.rb @@ -15,30 +15,21 @@ def initialize(ical) end def parse - Hash[*@ical.each_line.flat_map { |line| parse_line(line) }] + dtstart, rrule = @ical.split("RRULE:") + Hash[*parse_dtstart(dtstart) + parse_rrule(rrule)] end private - def parse_line(line) - line = line.strip - case line - when %r{^DTSTART} - parse_dtstart(line) - when %r{^RRULE} - parse_rrule(line) - end - end - - def parse_dtstart(line) - _label, time_string = line.split(";") + def parse_dtstart(dtstart) + _label, time_string = dtstart.split(";") + @starts_at = Montrose::Utils.parse_time(time_string) - [:starts, Montrose::Utils.parse_time(time_string)] + [:starts, @starts_at] end - def parse_rrule(line) - _label, rule_string = line.split(":") - rule_string.split(";").flat_map do |rule| + def parse_rrule(rrule) + rrule.gsub(/\s+/, "").split(";").flat_map do |rule| prop, value = rule.split("=") case prop when "FREQ" @@ -47,6 +38,14 @@ def parse_rrule(line) [:interval, value.to_i] when "COUNT" [:total, value.to_i] + when "UNTIL" + [:until, Montrose::Utils.parse_time(value)] + when "BYMONTH" + [:month, value.split(",").compact.map { |m| + Montrose::Month.number!(m) + }] + when "BYDAY" + [:day, value.split(",").map { |d| Montrose::Day.number!(d) }] end end end diff --git a/lib/montrose/recurrence.rb b/lib/montrose/recurrence.rb index 5cab162..5866595 100644 --- a/lib/montrose/recurrence.rb +++ b/lib/montrose/recurrence.rb @@ -252,7 +252,7 @@ def load(json) fail SerializationError, "Could not parse JSON: #{e}" end - alias from_json load + alias_method :from_json, :load def from_yaml(yaml) new(YAML.safe_load(yaml)) diff --git a/spec/from_ical_rfc_spec.rb b/spec/from_ical_rfc_spec.rb new file mode 100644 index 0000000..8bc3390 --- /dev/null +++ b/spec/from_ical_rfc_spec.rb @@ -0,0 +1,100 @@ +require "spec_helper" + +describe "Parsing ICAL RRULE examples from RFC 5545" do + let(:now) { Time.parse("Tue Sep 2 09:00:00 EDT 1997") } # Tuesday + + before do + Timecop.freeze(now) + end + + it "daily for 10 occurrences" do + ical = <<~ICAL + DTSTART;TZID=America/New_York:19970902T090000 + RRULE:FREQ=DAILY;COUNT=10" + ICAL + + recurrence = Montrose::Recurrence.from_ical(ical) + + _(recurrence.events.to_a).must_equal([ + Time.parse("Tue Sep 2 09:00:00 EDT 1997"), + Time.parse("Wed Sep 3 09:00:00 EDT 1997"), + Time.parse("Thu Sep 4 09:00:00 EDT 1997"), + Time.parse("Fri Sep 5 09:00:00 EDT 1997"), + Time.parse("Sat Sep 6 09:00:00 EDT 1997"), + Time.parse("Sun Sep 7 09:00:00 EDT 1997"), + Time.parse("Mon Sep 8 09:00:00 EDT 1997"), + Time.parse("Tue Sep 9 09:00:00 EDT 1997"), + Time.parse("Wed Sep 10 09:00:00 EDT 1997"), + Time.parse("Thu Sep 11 09:00:00 EDT 1997") + ]) + end + + it "daily until December 24, 1997" do + ical = <<~ICAL + DTSTART;TZID=America/New_York:19970902T090000 + RRULE:FREQ=DAILY;UNTIL=19971224T000000Z + ICAL + + recurrence = Montrose::Recurrence.from_ical(ical) + + starts_on = now.to_date + ends_on = Time.parse("19971224T000000Z").to_date + days = starts_on.upto(ends_on).count - 1 + expected_events = consecutive_days(days, starts: now).take(days) + + events = recurrence.events.to_a + _(events).must_equal expected_events + _(events.size).must_equal days + end + + it "every other day forever" do + ical = <<~ICAL + DTSTART;TZID=America/New_York:19970902T090000 + RRULE:FREQ=DAILY;INTERVAL=2 + ICAL + + recurrence = Montrose::Recurrence.from_ical(ical) + + expected_events = consecutive_days(5, interval: 2) + events = recurrence.events.take(5) + + _(events).must_pair_with expected_events + end + + it "every day in January, for 3 years, by frequency" do + ical = <<~ICAL + DTSTART;TZID=America/New_York:19980101T090000 + RRULE:FREQ=YEARLY;UNTIL=20000131T140000Z; + BYMONTH=1;BYDAY=SU,MO,TU,WE,TH,FR,SA + ICAL + + recurrence = Montrose::Recurrence.from_ical(ical) + + expected_events = 1998.upto(2000) + .flat_map { |yyyy| + 1.upto(31).map { |dd| + Time.parse("Jan #{dd} 09:00:00 EST #{yyyy}") + } + } + + _(recurrence.events).must_pair_with expected_events + end + + it "every day in January, for 3 years, by day" do + ical = <<~ICAL + DTSTART;TZID=America/New_York:19980101T090000 + RRULE:FREQ=DAILY;UNTIL=20000131T140000Z;BYMONTH=1 + ICAL + + recurrence = Montrose::Recurrence.from_ical(ical) + + expected_events = 1998.upto(2000) + .flat_map { |yyyy| + 1.upto(31).map { |dd| + Time.parse("Jan #{dd} 09:00:00 EST #{yyyy}") + } + } + + _(recurrence.events).must_pair_with expected_events + end +end diff --git a/spec/montrose/day_spec.rb b/spec/montrose/day_spec.rb index b89b00c..2f8586d 100644 --- a/spec/montrose/day_spec.rb +++ b/spec/montrose/day_spec.rb @@ -38,5 +38,4 @@ def number!(name) it { _(-> { number!(:foo) }).must_raise Montrose::ConfigurationError } it { _(-> { number!("foo") }).must_raise Montrose::ConfigurationError } end - end diff --git a/spec/montrose/month_spec.rb b/spec/montrose/month_spec.rb index 4e000cb..929712a 100644 --- a/spec/montrose/month_spec.rb +++ b/spec/montrose/month_spec.rb @@ -61,5 +61,4 @@ def number!(name) it { _(-> { number!(0) }).must_raise Montrose::ConfigurationError } it { _(-> { number!(13) }).must_raise Montrose::ConfigurationError } end - end From f183affe15b7db00b598655c03f4f61f6f3b5de3 Mon Sep 17 00:00:00 2001 From: Ross Kaffenberger Date: Fri, 5 Feb 2021 20:41:27 -0500 Subject: [PATCH 06/33] Add ical spec --- spec/from_ical_rfc_spec.rb | 39 +++++++++++++++++++++----------------- 1 file changed, 22 insertions(+), 17 deletions(-) diff --git a/spec/from_ical_rfc_spec.rb b/spec/from_ical_rfc_spec.rb index 8bc3390..32f15da 100644 --- a/spec/from_ical_rfc_spec.rb +++ b/spec/from_ical_rfc_spec.rb @@ -1,11 +1,7 @@ require "spec_helper" describe "Parsing ICAL RRULE examples from RFC 5545" do - let(:now) { Time.parse("Tue Sep 2 09:00:00 EDT 1997") } # Tuesday - - before do - Timecop.freeze(now) - end + let(:starts_on) { Time.parse("Sep 2 09:00:00 EDT 1997") } it "daily for 10 occurrences" do ical = <<~ICAL @@ -37,14 +33,12 @@ recurrence = Montrose::Recurrence.from_ical(ical) - starts_on = now.to_date - ends_on = Time.parse("19971224T000000Z").to_date - days = starts_on.upto(ends_on).count - 1 - expected_events = consecutive_days(days, starts: now).take(days) + ends_on = Time.parse("Dec 24 00:00:00 EDT 1997") + days = starts_on.to_date.upto(ends_on.to_date).count - 1 + expected_events = consecutive_days(days, starts: starts_on).take(days) - events = recurrence.events.to_a - _(events).must_equal expected_events - _(events.size).must_equal days + _(recurrence).must_pair_with expected_events + _(recurrence.events.to_a.size).must_equal days end it "every other day forever" do @@ -55,10 +49,21 @@ recurrence = Montrose::Recurrence.from_ical(ical) - expected_events = consecutive_days(5, interval: 2) - events = recurrence.events.take(5) + expected_events = consecutive_days(5, starts: starts_on, interval: 2) + + _(recurrence.take(5)).must_pair_with expected_events + end + + it "every 10 days, 5 occurrences" do + ical = <<~ical + DTSTART;TZID=America/New_York:19970902T090000 + RRULE:FREQ=DAILY;INTERVAL=10;COUNT=5 + ical + + recurrence = Montrose::Recurrence.from_ical(ical) + expected_events = consecutive_days(5, starts: starts_on, interval: 10) - _(events).must_pair_with expected_events + _(recurrence).must_pair_with expected_events end it "every day in January, for 3 years, by frequency" do @@ -77,7 +82,7 @@ } } - _(recurrence.events).must_pair_with expected_events + _(recurrence).must_pair_with expected_events end it "every day in January, for 3 years, by day" do @@ -95,6 +100,6 @@ } } - _(recurrence.events).must_pair_with expected_events + _(recurrence).must_pair_with expected_events end end From 4ec279c9f8513c76289e486599d419949d8f812e Mon Sep 17 00:00:00 2001 From: Ross Kaffenberger Date: Fri, 5 Feb 2021 21:52:13 -0500 Subject: [PATCH 07/33] Add ical spec --- spec/from_ical_rfc_spec.rb | 65 +++++++++++++++++++++++++++++--------- 1 file changed, 50 insertions(+), 15 deletions(-) diff --git a/spec/from_ical_rfc_spec.rb b/spec/from_ical_rfc_spec.rb index 32f15da..09d9c06 100644 --- a/spec/from_ical_rfc_spec.rb +++ b/spec/from_ical_rfc_spec.rb @@ -12,17 +12,17 @@ recurrence = Montrose::Recurrence.from_ical(ical) _(recurrence.events.to_a).must_equal([ - Time.parse("Tue Sep 2 09:00:00 EDT 1997"), - Time.parse("Wed Sep 3 09:00:00 EDT 1997"), - Time.parse("Thu Sep 4 09:00:00 EDT 1997"), - Time.parse("Fri Sep 5 09:00:00 EDT 1997"), - Time.parse("Sat Sep 6 09:00:00 EDT 1997"), - Time.parse("Sun Sep 7 09:00:00 EDT 1997"), - Time.parse("Mon Sep 8 09:00:00 EDT 1997"), - Time.parse("Tue Sep 9 09:00:00 EDT 1997"), - Time.parse("Wed Sep 10 09:00:00 EDT 1997"), - Time.parse("Thu Sep 11 09:00:00 EDT 1997") - ]) + "Sep 2 09:00:00 EDT 1997", + "Sep 3 09:00:00 EDT 1997", + "Sep 4 09:00:00 EDT 1997", + "Sep 5 09:00:00 EDT 1997", + "Sep 6 09:00:00 EDT 1997", + "Sep 7 09:00:00 EDT 1997", + "Sep 8 09:00:00 EDT 1997", + "Sep 9 09:00:00 EDT 1997", + "Sep 10 09:00:00 EDT 1997", + "Sep 11 09:00:00 EDT 1997" + ].map { |t| Time.parse(t) }) end it "daily until December 24, 1997" do @@ -36,6 +36,8 @@ ends_on = Time.parse("Dec 24 00:00:00 EDT 1997") days = starts_on.to_date.upto(ends_on.to_date).count - 1 expected_events = consecutive_days(days, starts: starts_on).take(days) + # ==> (1997 9:00 AM EDT) September 2-30;October 1-25 + # (1997 9:00 AM EST) October 26-31;November 1-30;December 1-23 _(recurrence).must_pair_with expected_events _(recurrence.events.to_a.size).must_equal days @@ -49,19 +51,35 @@ recurrence = Montrose::Recurrence.from_ical(ical) - expected_events = consecutive_days(5, starts: starts_on, interval: 2) + expected_events = [ + "1997-09-02 09:00:00 -0400", + "1997-09-04 09:00:00 -0400", + "1997-09-06 09:00:00 -0400", + "1997-09-08 09:00:00 -0400", + "1997-09-10 09:00:00 -0400" + ].map { |t| Time.parse(t) } + # ==> (1997 9:00 AM EDT) September 2,4,6,8.. _(recurrence.take(5)).must_pair_with expected_events end it "every 10 days, 5 occurrences" do - ical = <<~ical + ical = <<~ICAL DTSTART;TZID=America/New_York:19970902T090000 RRULE:FREQ=DAILY;INTERVAL=10;COUNT=5 - ical + ICAL recurrence = Montrose::Recurrence.from_ical(ical) - expected_events = consecutive_days(5, starts: starts_on, interval: 10) + + expected_events = [ + "1997-09-02 09:00:00 -0400", + "1997-09-12 09:00:00 -0400", + "1997-09-22 09:00:00 -0400", + "1997-10-02 09:00:00 -0400", + "1997-10-12 09:00:00 -0400" + ].map { |t| Time.parse(t) } + # ==> (1997 9:00 AM EDT) September 2,12,22; + # October 2,12 _(recurrence).must_pair_with expected_events end @@ -99,7 +117,24 @@ Time.parse("Jan #{dd} 09:00:00 EST #{yyyy}") } } + # ==> (1998 9:00 AM EST)January 1-31 + # (1999 9:00 AM EST)January 1-31 + # (2000 9:00 AM EST)January 1-31 + + _(recurrence).must_pair_with expected_events + end + + it "weekly for 10 occurrences" do + ical = <<~ICAL + DTSTART;TZID=America/New_York:19970902T090000 + RRULE:FREQ=WEEKLY;COUNT=10 + ICAL + + recurrence = Montrose::Recurrence.from_ical(ical) + expected_events = consecutive_days(10, starts: starts_on, interval: 7) + # ==> (1997 9:00 AM EDT) September 2,9,16,23,30;October 7,14,21 + # (1997 9:00 AM EST) October 28;November 4 _(recurrence).must_pair_with expected_events end end From 93908bc20e854b8715c5017f72ed31b2aec50926 Mon Sep 17 00:00:00 2001 From: Ross Kaffenberger Date: Fri, 5 Feb 2021 21:56:33 -0500 Subject: [PATCH 08/33] Add ical spec --- spec/from_ical_rfc_spec.rb | 38 ++++++++++++++++++++++++++++---------- 1 file changed, 28 insertions(+), 10 deletions(-) diff --git a/spec/from_ical_rfc_spec.rb b/spec/from_ical_rfc_spec.rb index 09d9c06..8d960d8 100644 --- a/spec/from_ical_rfc_spec.rb +++ b/spec/from_ical_rfc_spec.rb @@ -8,6 +8,7 @@ DTSTART;TZID=America/New_York:19970902T090000 RRULE:FREQ=DAILY;COUNT=10" ICAL + # ==> (1997 9:00 AM EDT) September 2-11 recurrence = Montrose::Recurrence.from_ical(ical) @@ -30,14 +31,14 @@ DTSTART;TZID=America/New_York:19970902T090000 RRULE:FREQ=DAILY;UNTIL=19971224T000000Z ICAL + # ==> (1997 9:00 AM EDT) September 2-30;October 1-25 + # (1997 9:00 AM EST) October 26-31;November 1-30;December 1-23 recurrence = Montrose::Recurrence.from_ical(ical) ends_on = Time.parse("Dec 24 00:00:00 EDT 1997") days = starts_on.to_date.upto(ends_on.to_date).count - 1 expected_events = consecutive_days(days, starts: starts_on).take(days) - # ==> (1997 9:00 AM EDT) September 2-30;October 1-25 - # (1997 9:00 AM EST) October 26-31;November 1-30;December 1-23 _(recurrence).must_pair_with expected_events _(recurrence.events.to_a.size).must_equal days @@ -48,6 +49,7 @@ DTSTART;TZID=America/New_York:19970902T090000 RRULE:FREQ=DAILY;INTERVAL=2 ICAL + # ==> (1997 9:00 AM EDT) September 2,4,6,8.. recurrence = Montrose::Recurrence.from_ical(ical) @@ -58,7 +60,6 @@ "1997-09-08 09:00:00 -0400", "1997-09-10 09:00:00 -0400" ].map { |t| Time.parse(t) } - # ==> (1997 9:00 AM EDT) September 2,4,6,8.. _(recurrence.take(5)).must_pair_with expected_events end @@ -68,6 +69,8 @@ DTSTART;TZID=America/New_York:19970902T090000 RRULE:FREQ=DAILY;INTERVAL=10;COUNT=5 ICAL + # ==> (1997 9:00 AM EDT) September 2,12,22; + # October 2,12 recurrence = Montrose::Recurrence.from_ical(ical) @@ -78,8 +81,6 @@ "1997-10-02 09:00:00 -0400", "1997-10-12 09:00:00 -0400" ].map { |t| Time.parse(t) } - # ==> (1997 9:00 AM EDT) September 2,12,22; - # October 2,12 _(recurrence).must_pair_with expected_events end @@ -108,6 +109,9 @@ DTSTART;TZID=America/New_York:19980101T090000 RRULE:FREQ=DAILY;UNTIL=20000131T140000Z;BYMONTH=1 ICAL + # ==> (1998 9:00 AM EST)January 1-31 + # (1999 9:00 AM EST)January 1-31 + # (2000 9:00 AM EST)January 1-31 recurrence = Montrose::Recurrence.from_ical(ical) @@ -117,9 +121,6 @@ Time.parse("Jan #{dd} 09:00:00 EST #{yyyy}") } } - # ==> (1998 9:00 AM EST)January 1-31 - # (1999 9:00 AM EST)January 1-31 - # (2000 9:00 AM EST)January 1-31 _(recurrence).must_pair_with expected_events end @@ -129,12 +130,29 @@ DTSTART;TZID=America/New_York:19970902T090000 RRULE:FREQ=WEEKLY;COUNT=10 ICAL + # ==> (1997 9:00 AM EDT) September 2,9,16,23,30;October 7,14,21 + # (1997 9:00 AM EST) October 28;November 4 recurrence = Montrose::Recurrence.from_ical(ical) expected_events = consecutive_days(10, starts: starts_on, interval: 7) - # ==> (1997 9:00 AM EDT) September 2,9,16,23,30;October 7,14,21 - # (1997 9:00 AM EST) October 28;November 4 + _(recurrence).must_pair_with expected_events + end + + it "weekly until December 24, 1997" do + ical = <<~ical + DTSTART;TZID=America/New_York:19970902T090000 + RRULE:FREQ=WEEKLY;UNTIL=19971224T000000Z + ical + # ==> (1997 9:00 AM EDT) September 2,9,16,23,30; + # October 7,14,21 + # (1997 9:00 AM EST) October 28; + # November 4,11,18,25; + # December 2,9,16,23" + + recurrence = Montrose::Recurrence.from_ical(ical) + expected_events = consecutive_days(17, starts: starts_on, interval: 7) + _(recurrence).must_pair_with expected_events end end From 267695841d584d7573cc303befe9dc739f0eb4bf Mon Sep 17 00:00:00 2001 From: Ross Kaffenberger Date: Fri, 5 Feb 2021 22:06:28 -0500 Subject: [PATCH 09/33] Add week_start option --- lib/montrose/ical.rb | 2 ++ lib/montrose/options.rb | 2 ++ spec/from_ical_rfc_spec.rb | 21 +++++++++++++++++++++ 3 files changed, 25 insertions(+) diff --git a/lib/montrose/ical.rb b/lib/montrose/ical.rb index fb44cb8..e01c83b 100644 --- a/lib/montrose/ical.rb +++ b/lib/montrose/ical.rb @@ -46,6 +46,8 @@ def parse_rrule(rrule) }] when "BYDAY" [:day, value.split(",").map { |d| Montrose::Day.number!(d) }] + when "WKST" + [:week_start, value] end end end diff --git a/lib/montrose/options.rb b/lib/montrose/options.rb index 1ab42f3..f8f38d7 100644 --- a/lib/montrose/options.rb +++ b/lib/montrose/options.rb @@ -95,6 +95,7 @@ def default_options def_option :mday def_option :yday def_option :week + def_option :week_start def_option :month def_option :interval def_option :total @@ -115,6 +116,7 @@ def initialize(opts = {}) week: nil, month: nil, total: nil, + week_start: nil, exclude_end: nil } diff --git a/spec/from_ical_rfc_spec.rb b/spec/from_ical_rfc_spec.rb index 8d960d8..455c1f5 100644 --- a/spec/from_ical_rfc_spec.rb +++ b/spec/from_ical_rfc_spec.rb @@ -155,4 +155,25 @@ _(recurrence).must_pair_with expected_events end + + it "every other week - forever" do + ical = <<~ical + DTSTART;TZID=America/New_York:19970902T090000 + RRULE:FREQ=WEEKLY;INTERVAL=2;WKST=SU + ical + # ==> (1997 9:00 AM EDT) September 2,16,30; + # October 14 + # (1997 9:00 AM EST) October 28... + + recurrence = Montrose::Recurrence.from_ical(ical) + expected_events = [ + '1997-09-02 09:00:00 -0400', + '1997-09-16 09:00:00 -0400', + '1997-09-30 09:00:00 -0400', + '1997-10-14 09:00:00 -0400', + '1997-10-28 09:00:00 -0500' + ].map { |t| Time.parse(t) } + + _(recurrence.take(5)).must_pair_with expected_events + end end From ff73e4d2edc2025fc1a4c970e9533be53cf73484 Mon Sep 17 00:00:00 2001 From: Ross Kaffenberger Date: Fri, 5 Feb 2021 22:11:36 -0500 Subject: [PATCH 10/33] Add ical spec --- spec/from_ical_rfc_spec.rb | 52 +++++++++++++++++++++++++++++++------- 1 file changed, 43 insertions(+), 9 deletions(-) diff --git a/spec/from_ical_rfc_spec.rb b/spec/from_ical_rfc_spec.rb index 455c1f5..21d4214 100644 --- a/spec/from_ical_rfc_spec.rb +++ b/spec/from_ical_rfc_spec.rb @@ -140,10 +140,10 @@ end it "weekly until December 24, 1997" do - ical = <<~ical + ical = <<~ICAL DTSTART;TZID=America/New_York:19970902T090000 RRULE:FREQ=WEEKLY;UNTIL=19971224T000000Z - ical + ICAL # ==> (1997 9:00 AM EDT) September 2,9,16,23,30; # October 7,14,21 # (1997 9:00 AM EST) October 28; @@ -157,23 +157,57 @@ end it "every other week - forever" do - ical = <<~ical + ical = <<~ICAL DTSTART;TZID=America/New_York:19970902T090000 RRULE:FREQ=WEEKLY;INTERVAL=2;WKST=SU - ical + ICAL # ==> (1997 9:00 AM EDT) September 2,16,30; # October 14 # (1997 9:00 AM EST) October 28... recurrence = Montrose::Recurrence.from_ical(ical) expected_events = [ - '1997-09-02 09:00:00 -0400', - '1997-09-16 09:00:00 -0400', - '1997-09-30 09:00:00 -0400', - '1997-10-14 09:00:00 -0400', - '1997-10-28 09:00:00 -0500' + "1997-09-02 09:00:00 -0400", + "1997-09-16 09:00:00 -0400", + "1997-09-30 09:00:00 -0400", + "1997-10-14 09:00:00 -0400", + "1997-10-28 09:00:00 -0500" ].map { |t| Time.parse(t) } _(recurrence.take(5)).must_pair_with expected_events end + + it "weekly on Tuesday and Thursday for five weeks" do + ical = <<~ICAL + DTSTART;TZID=America/New_York:19970902T090000 + RRULE:FREQ=WEEKLY;UNTIL=19971007T000000Z;WKST=SU;BYDAY=TU,TH + ICAL + # ==> (1997 9:00 AM EDT) September 2,4,9,11,16,18,23,25,30; + # October 2 + + recurrence = Montrose::Recurrence.from_ical(ical) + expected_events = [ + "1997-09-02 09:00:00 -0400", + "1997-09-04 09:00:00 -0400", + "1997-09-09 09:00:00 -0400", + "1997-09-11 09:00:00 -0400", + "1997-09-16 09:00:00 -0400", + "1997-09-18 09:00:00 -0400", + "1997-09-23 09:00:00 -0400", + "1997-09-25 09:00:00 -0400", + "1997-09-30 09:00:00 -0400", + "1997-10-02 09:00:00 -0400" + ].map { |t| Time.parse(t) } + + _(recurrence).must_pair_with expected_events + end + + it "weekly on Tuesday and Thursday for five weeks" do + ical = <<~ICAL + DTSTART;TZID=America/New_York:19970902T090000 + RRULE:FREQ=WEEKLY;COUNT=10;WKST=SU;BYDAY=TU,TH + ICAL + # ==> (1997 9:00 AM EDT) September 2,4,9,11,16,18,23,25,30; + # October 2 + end end From bf5d62a49f994a17cd315f2b17dec223e959c226 Mon Sep 17 00:00:00 2001 From: Ross Kaffenberger Date: Fri, 5 Feb 2021 22:31:33 -0500 Subject: [PATCH 11/33] Add and update ical specs --- spec/from_ical_rfc_spec.rb | 119 +++++++++++++++++++++---------------- 1 file changed, 69 insertions(+), 50 deletions(-) diff --git a/spec/from_ical_rfc_spec.rb b/spec/from_ical_rfc_spec.rb index 21d4214..b391480 100644 --- a/spec/from_ical_rfc_spec.rb +++ b/spec/from_ical_rfc_spec.rb @@ -1,6 +1,7 @@ require "spec_helper" -describe "Parsing ICAL RRULE examples from RFC 5545" do +# https://tools.ietf.org/html/rfc5545#section-3.8.5 +describe "Parsing ICAL RRULE examples from RFC 5545 Section 3.8.5" do let(:starts_on) { Time.parse("Sep 2 09:00:00 EDT 1997") } it "daily for 10 occurrences" do @@ -11,19 +12,11 @@ # ==> (1997 9:00 AM EDT) September 2-11 recurrence = Montrose::Recurrence.from_ical(ical) + expected_events = parse_expected_events( + "1997 9:00 AM EDT" => {"Sep" => 2.upto(11)} + ) - _(recurrence.events.to_a).must_equal([ - "Sep 2 09:00:00 EDT 1997", - "Sep 3 09:00:00 EDT 1997", - "Sep 4 09:00:00 EDT 1997", - "Sep 5 09:00:00 EDT 1997", - "Sep 6 09:00:00 EDT 1997", - "Sep 7 09:00:00 EDT 1997", - "Sep 8 09:00:00 EDT 1997", - "Sep 9 09:00:00 EDT 1997", - "Sep 10 09:00:00 EDT 1997", - "Sep 11 09:00:00 EDT 1997" - ].map { |t| Time.parse(t) }) + _(recurrence).must_pair_with expected_events end it "daily until December 24, 1997" do @@ -49,17 +42,12 @@ DTSTART;TZID=America/New_York:19970902T090000 RRULE:FREQ=DAILY;INTERVAL=2 ICAL - # ==> (1997 9:00 AM EDT) September 2,4,6,8.. + # ==> (1997 9:00 AM EDT) September 2,4,6,8,10.. recurrence = Montrose::Recurrence.from_ical(ical) - - expected_events = [ - "1997-09-02 09:00:00 -0400", - "1997-09-04 09:00:00 -0400", - "1997-09-06 09:00:00 -0400", - "1997-09-08 09:00:00 -0400", - "1997-09-10 09:00:00 -0400" - ].map { |t| Time.parse(t) } + expected_events = parse_expected_events( + "1997 9:00 AM EDT" => {"Sep" => [2, 4, 6, 8, 10]} + ) _(recurrence.take(5)).must_pair_with expected_events end @@ -73,14 +61,10 @@ # October 2,12 recurrence = Montrose::Recurrence.from_ical(ical) - - expected_events = [ - "1997-09-02 09:00:00 -0400", - "1997-09-12 09:00:00 -0400", - "1997-09-22 09:00:00 -0400", - "1997-10-02 09:00:00 -0400", - "1997-10-12 09:00:00 -0400" - ].map { |t| Time.parse(t) } + expected_events = parse_expected_events( + "1997 9:00 AM EDT" => {"Sep" => [2, 12, 22], + "Oct" => [2, 12]} + ) _(recurrence).must_pair_with expected_events end @@ -151,7 +135,13 @@ # December 2,9,16,23" recurrence = Montrose::Recurrence.from_ical(ical) - expected_events = consecutive_days(17, starts: starts_on, interval: 7) + expected_events = parse_expected_events( + "1997 9:00 AM EDT" => {"Sep" => [2, 9, 16, 23, 30], + "Oct" => [7, 14, 21]}, + "1997 9:00 AM EST" => {"Oct" => [28], + "Nov" => [4, 11, 18, 25], + "Dec" => [2, 9, 16, 23]} + ) _(recurrence).must_pair_with expected_events end @@ -166,13 +156,11 @@ # (1997 9:00 AM EST) October 28... recurrence = Montrose::Recurrence.from_ical(ical) - expected_events = [ - "1997-09-02 09:00:00 -0400", - "1997-09-16 09:00:00 -0400", - "1997-09-30 09:00:00 -0400", - "1997-10-14 09:00:00 -0400", - "1997-10-28 09:00:00 -0500" - ].map { |t| Time.parse(t) } + expected_events = parse_expected_events( + "1997 9:00 AM EDT" => {"Sep" => [2, 16, 30], + "Oct" => [14]}, + "1997 9:00 AM EST" => {"Oct" => [28]} + ) _(recurrence.take(5)).must_pair_with expected_events end @@ -186,18 +174,10 @@ # October 2 recurrence = Montrose::Recurrence.from_ical(ical) - expected_events = [ - "1997-09-02 09:00:00 -0400", - "1997-09-04 09:00:00 -0400", - "1997-09-09 09:00:00 -0400", - "1997-09-11 09:00:00 -0400", - "1997-09-16 09:00:00 -0400", - "1997-09-18 09:00:00 -0400", - "1997-09-23 09:00:00 -0400", - "1997-09-25 09:00:00 -0400", - "1997-09-30 09:00:00 -0400", - "1997-10-02 09:00:00 -0400" - ].map { |t| Time.parse(t) } + expected_events = parse_expected_events( + "1997 9:00 AM EDT" => {"Sep" => [2, 4, 9, 11, 16, 18, 23, 25, 30], + "Oct" => [2]} + ) _(recurrence).must_pair_with expected_events end @@ -209,5 +189,44 @@ ICAL # ==> (1997 9:00 AM EDT) September 2,4,9,11,16,18,23,25,30; # October 2 + + recurrence = Montrose::Recurrence.from_ical(ical) + expected_events = parse_expected_events( + "1997 9:00 AM EDT" => {"Sep" => [2, 4, 9, 11, 16, 18, 23, 25, 30], + "Oct" => [2]} + ) + + _(recurrence).must_pair_with expected_events + end + + it 'Every other week on Monday, Wednesday, and Friday until December + 24, 1997, starting on Monday, September 1, 1997' do + ical = <<~ICAL + DTSTART;TZID=America/New_York:19970901T090000 + RRULE:FREQ=WEEKLY;INTERVAL=2;UNTIL=19971224T000000Z;WKST=SU; + BYDAY=MO,WE,FR + ICAL + # ==> (1997 9:00 AM EDT) September 1,3,5,15,17,19,29; + # October 1,3,13,15,17 + # (1997 9:00 AM EST) October 27,29,31; + # November 10,12,14,24,26,28; + + recurrence = Montrose::Recurrence.from_ical(ical) + expected_events = parse_expected_events( + "1997 9:00 AM EDT" => {"Sep" => [1, 3, 5, 15, 17, 19, 29], + "Oct" => [1, 3, 13, 15, 17]}, + "1997 9:00 AM EST" => {"Oct" => [27, 29, 31], + "Nov" => [10, 12, 14, 24, 26, 28]} + ) + + _(recurrence).must_pair_with expected_events + end + + def parse_expected_events(event_map) + event_map.flat_map { |yyyyz, ms| + ms.flat_map { |mm, ds| + ds.map { |dd| Time.parse "#{mm}, #{dd} #{yyyyz}" } + } + } end end From 90b44adbb6218bf9b1188297fe629fdeca8a50de Mon Sep 17 00:00:00 2001 From: Ross Kaffenberger Date: Fri, 5 Feb 2021 22:36:22 -0500 Subject: [PATCH 12/33] Add ical spec --- spec/from_ical_rfc_spec.rb | 29 +++++++++++++++++++++++------ 1 file changed, 23 insertions(+), 6 deletions(-) diff --git a/spec/from_ical_rfc_spec.rb b/spec/from_ical_rfc_spec.rb index b391480..98b1497 100644 --- a/spec/from_ical_rfc_spec.rb +++ b/spec/from_ical_rfc_spec.rb @@ -2,6 +2,14 @@ # https://tools.ietf.org/html/rfc5545#section-3.8.5 describe "Parsing ICAL RRULE examples from RFC 5545 Section 3.8.5" do + def parse_expected_events(event_map) + event_map.flat_map { |yyyyz, ms| + ms.flat_map { |mm, ds| + ds.map { |dd| Time.parse "#{mm}, #{dd} #{yyyyz}" } + } + } + end + let(:starts_on) { Time.parse("Sep 2 09:00:00 EDT 1997") } it "daily for 10 occurrences" do @@ -222,11 +230,20 @@ _(recurrence).must_pair_with expected_events end - def parse_expected_events(event_map) - event_map.flat_map { |yyyyz, ms| - ms.flat_map { |mm, ds| - ds.map { |dd| Time.parse "#{mm}, #{dd} #{yyyyz}" } - } - } + it "every other week on Tuesday and Thursday, for 8 occurrences" do + ical = <<~ICAL + DTSTART;TZID=America/New_York:19970902T090000 + RRULE:FREQ=WEEKLY;INTERVAL=2;COUNT=8;WKST=SU;BYDAY=TU,TH + ICAL + # ==> (1997 9:00 AM EDT) September 2,4,16,18,30; + # October 2,14,16 + + recurrence = Montrose::Recurrence.from_ical(ical) + expected_events = parse_expected_events( + "1997 9:00 AM EDT" => {"Sep" => [2, 4, 16, 18, 30], + "Oct" => [2, 14, 16]} + ) + + _(recurrence).must_pair_with expected_events end end From b97cae2a5947d36e6611067556f9e6a9973a80c9 Mon Sep 17 00:00:00 2001 From: Ross Kaffenberger Date: Sat, 6 Feb 2021 12:04:10 -0500 Subject: [PATCH 13/33] Extract and expand day parsing to include ical --- lib/montrose/day.rb | 91 ++++++++++++++++++++++++++++++--------- lib/montrose/options.rb | 8 +--- spec/montrose/day_spec.rb | 32 ++++++++++++++ 3 files changed, 105 insertions(+), 26 deletions(-) diff --git a/lib/montrose/day.rb b/lib/montrose/day.rb index 5f35f35..46d8a0a 100644 --- a/lib/montrose/day.rb +++ b/lib/montrose/day.rb @@ -3,30 +3,81 @@ class Day extend Montrose::Utils NAMES = ::Date::DAYNAMES - ABBREVIATIONS = %w[SU MO TU WE TH FR SA].freeze + TWO_LETTER_ABBREVIATIONS = %w[SU MO TU WE TH FR SA].freeze + THREE_LETTER_ABBREVIATIONS = %w[SUN MON TUE WED THU FRI SAT] + NUMBERS = NAMES.map.with_index { |_n, i| i.to_s } - def self.names - NAMES - end + ICAL_MATCH = /(?[+-]?\d+)?(?[A-Z]{2})/ # e.g. 1FR - def self.number(name) - case name - when 0..6 - name - when Symbol, String - string = name.to_s - NAMES.index(string.titleize) || - ABBREVIATIONS.index(string.upcase) || - number(to_index(string)) - when Array - number name.first + class << self + def parse(arg) + case arg + when Hash + parse_entries(arg.entries) + when String + parse(arg.split(',')) + else + parse_entries(map_arg(arg) { |value| parse_value(value) }) + end + end + + def parse_entries(entries) + hash = Hash.new {|h,k| h[k] = []} + result = entries.each_with_object(hash) do |(k, v), hash| + index = number!(k) + hash[index] = hash[index] + [*v] + end + result.values.all?(&:empty?) ? result.keys : result + end + + def parse_value(value) + parse_ical(value) || [number!(value), nil] + end + + def parse_ical(value) + (match = ICAL_MATCH.match(value.to_s)) || (return nil) + index = number!(match[:day]) + ordinal = match[:ordinal]&.to_i + [index, ordinal] + end + + def map_arg(arg, &block) + return nil unless arg.present? + + Array(arg).map(&block) + end + + def names + NAMES + end + + def number(name) + case name + when 0..6 + name + when Symbol, String + string = name.to_s.downcase + NAMES.index(string.titleize) || + TWO_LETTER_ABBREVIATIONS.index(string.upcase) || + THREE_LETTER_ABBREVIATIONS.index(string.upcase) || + number(to_index(string)) + when Array + number name.first + end end - end - def self.number!(name) - numbers = NAMES.map.with_index { |_n, i| i.to_s } - number(name) || raise(ConfigurationError, - "Did not recognize day #{name}, must be one of #{(NAMES + numbers).inspect}") + def number!(name) + number(name) || raise(ConfigurationError, + "Did not recognize day #{name}, must be one of #{(names + abbreviations + numbers).inspect}") + end + + def numbers + NUMBERS + end + + def abbreviations + TWO_LETTER_ABBREVIATIONS + THREE_LETTER_ABBREVIATIONS + end end end end diff --git a/lib/montrose/options.rb b/lib/montrose/options.rb index f8f38d7..f531e2c 100644 --- a/lib/montrose/options.rb +++ b/lib/montrose/options.rb @@ -200,7 +200,7 @@ def during=(during_arg) end def day=(days) - @day = nested_map_arg(days) { |d| Montrose::Day.number!(d) } + @day = Montrose::Day.parse(days) end def mday=(mdays) @@ -286,10 +286,6 @@ def map_arg(arg, &block) Array(arg).map(&block) end - def map_days(arg) - map_arg(arg) { |d| Montrose::Day.number!(d) } - end - def map_mdays(arg) map_arg(arg) { |d| assert_mday(d) } end @@ -324,7 +320,7 @@ def decompose_on_arg(arg) result[:mday] += map_mdays(v) end else - {day: map_days(arg)} + {day: Montrose::Day.parse(arg)} end end diff --git a/spec/montrose/day_spec.rb b/spec/montrose/day_spec.rb index 2f8586d..fd17008 100644 --- a/spec/montrose/day_spec.rb +++ b/spec/montrose/day_spec.rb @@ -1,10 +1,42 @@ require "spec_helper" describe Montrose::Day do + def parse(arg) + Montrose::Day.parse(arg) + end + def number!(name) Montrose::Day.number!(name) end + describe "#parse" do + it { _(parse(:friday)).must_equal([5]) } + it { _(parse(:friday)).must_equal([5]) } + it { _(parse([:friday])).must_equal([5]) } + it { _(parse('friday')).must_equal([5]) } + it { _(parse(%w[thursday friday])).must_equal([4, 5]) } + it { _(parse(%w[Thursday Friday])).must_equal([4, 5]) } + + it { _(parse(:fri)).must_equal([5]) } + it { _(parse(:fri)).must_equal([5]) } + it { _(parse([:fri])).must_equal([5]) } + it { _(parse('fri')).must_equal([5]) } + it { _(parse(%w[thu fri])).must_equal([4, 5]) } + it { _(parse(%w[Thu Fri])).must_equal([4, 5]) } + + it { _(parse(friday: 1)).must_equal(5 => [1]) } + it { _(parse(friday: [1])).must_equal(5 => [1]) } + it { _(parse(5 => [1])).must_equal(5 => [1]) } + + it { _(parse(friday: [1, -1])).must_equal(5 => [1, -1]) } + it { _(parse(5 => [1, -1])).must_equal(5 => [1, -1]) } + + it { _(parse("FR")).must_equal([5]) } + it { _(parse("1FR")).must_equal(5 => [1]) } + it { _(parse("1FR,-1FR")).must_equal(5 => [1, -1]) } + it { _(parse(%w[1FR -1FR])).must_equal(5 => [1, -1]) } + end + describe "#number!" do it { _(number!(:sunday)).must_equal 0 } it { _(number!(:monday)).must_equal 1 } From 678d58df3a3a905ebe4762c23faa21c90bc49e4e Mon Sep 17 00:00:00 2001 From: Ross Kaffenberger Date: Sat, 6 Feb 2021 12:04:56 -0500 Subject: [PATCH 14/33] Add ical spec --- lib/montrose/ical.rb | 2 +- spec/from_ical_rfc_spec.rb | 64 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 65 insertions(+), 1 deletion(-) diff --git a/lib/montrose/ical.rb b/lib/montrose/ical.rb index e01c83b..5a2d6e9 100644 --- a/lib/montrose/ical.rb +++ b/lib/montrose/ical.rb @@ -45,7 +45,7 @@ def parse_rrule(rrule) Montrose::Month.number!(m) }] when "BYDAY" - [:day, value.split(",").map { |d| Montrose::Day.number!(d) }] + [:day, Montrose::Day.parse(value)] when "WKST" [:week_start, value] end diff --git a/spec/from_ical_rfc_spec.rb b/spec/from_ical_rfc_spec.rb index 98b1497..af2437a 100644 --- a/spec/from_ical_rfc_spec.rb +++ b/spec/from_ical_rfc_spec.rb @@ -246,4 +246,68 @@ def parse_expected_events(event_map) _(recurrence).must_pair_with expected_events end + + it "monthly on the first Friday for 10 occurrences" do + ical = <<~ICAL + DTSTART;TZID=America/New_York:19970905T090000 + RRULE:FREQ=MONTHLY;COUNT=10;BYDAY=1FR + ICAL + # ==> (1997 9:00 AM EDT) September 5;October 3 + # (1997 9:00 AM EST) November 7;December 5 + # (1998 9:00 AM EST) January 2;February 6;March 6;April 3 + # (1998 9:00 AM EDT) May 1;June 5 + expected_events = parse_expected_events( + "1997 9:00 AM EDT" => {"Sep" => [5], + "Oct" => [3]}, + "1997 9:00 AM EST" => {"Nov" => [7], + "Dec" => [5]}, + "1998 9:00 AM EST" => {"Jan" => [2], + "Feb" => [6], + "Mar" => [6], + "Apr" => [3]}, + "1998 9:00 AM EDT" => {"May" => [1], + "Jun" => [5]} + ) + + recurrence = Montrose::Recurrence.from_ical(ical) + _(recurrence).must_pair_with expected_events + end + + it "monthly on the first Friday until December 24, 1997" do + ical = <<~ICAL + DTSTART;TZID=America/New_York:19970905T090000 + RRULE:FREQ=MONTHLY;UNTIL=19971224T000000Z;BYDAY=1FR + ICAL + # ==> (1997 9:00 AM EDT) September 5; October 3 + # (1997 9:00 AM EST) November 7; December 5 + expected_events = parse_expected_events( + "1997 9:00 AM EDT" => {"Sep" => [5], + "Oct" => [3]}, + "1997 9:00 AM EST" => {"Nov" => [7], + "Dec" => [5]} + ) + + recurrence = Montrose::Recurrence.from_ical(ical) + _(recurrence).must_pair_with expected_events + end + + # Every other month on the first and last Sunday of the month for 10 + # occurrences: + + # DTSTART;TZID=America/New_York:19970907T090000 + # RRULE:FREQ=MONTHLY;INTERVAL=2;COUNT=10;BYDAY=1SU,-1SU + + # ==> (1997 9:00 AM EDT) September 7,28 + # (1997 9:00 AM EST) November 2,30 + # (1998 9:00 AM EST) January 4,25;March 1,29 + # (1998 9:00 AM EDT) May 3,31 + + # Monthly on the second-to-last Monday of the month for 6 months: + + # DTSTART;TZID=America/New_York:19970922T090000 + # RRULE:FREQ=MONTHLY;COUNT=6;BYDAY=-2MO + + # ==> (1997 9:00 AM EDT) September 22;October 20 + # (1997 9:00 AM EST) November 17;December 22 + # (1998 9:00 AM EST) January 19;February 16 end From c77cdf584cd748ef848a86950c0d7edba981ef14 Mon Sep 17 00:00:00 2001 From: Ross Kaffenberger Date: Sat, 6 Feb 2021 12:08:10 -0500 Subject: [PATCH 15/33] Extract Month parsing --- lib/montrose/ical.rb | 4 +--- lib/montrose/month.rb | 52 +++++++++++++++++++++++++++-------------- lib/montrose/options.rb | 2 +- 3 files changed, 36 insertions(+), 22 deletions(-) diff --git a/lib/montrose/ical.rb b/lib/montrose/ical.rb index 5a2d6e9..fa0b7fd 100644 --- a/lib/montrose/ical.rb +++ b/lib/montrose/ical.rb @@ -41,9 +41,7 @@ def parse_rrule(rrule) when "UNTIL" [:until, Montrose::Utils.parse_time(value)] when "BYMONTH" - [:month, value.split(",").compact.map { |m| - Montrose::Month.number!(m) - }] + [:month, Montrose::Month.parse(value)] when "BYDAY" [:day, Montrose::Day.parse(value)] when "WKST" diff --git a/lib/montrose/month.rb b/lib/montrose/month.rb index 71cc3e4..c4fbe45 100644 --- a/lib/montrose/month.rb +++ b/lib/montrose/month.rb @@ -3,29 +3,45 @@ class Month extend Montrose::Utils NAMES = ::Date::MONTHNAMES # starts with nil to match 1-12 numbering + NUMBERS = NAMES.map.with_index { |_n, i| i.to_s }.slice(1, 12) - def self.names - NAMES - end + class << self + def parse(value) + case value + when String + parse(value.split(",").compact) + when Array + value.map { |m| + Montrose::Month.number!(m) + }.presence + else + parse(Array(value)) + end + end - def self.numbers - @numbers ||= NAMES.map.with_index { |_n, i| i.to_s }.slice(1, 12) - end + def names + NAMES + end - def self.number(name) - case name - when Symbol, String - string = name.to_s - NAMES.index(string.titleize) || number(to_index(string)) - when 1..12 - name + def numbers + NUMBERS + end + + def number(name) + case name + when Symbol, String + string = name.to_s + NAMES.index(string.titleize) || number(to_index(string)) + when 1..12 + name + end end - end - def self.number!(name) - numbers = NAMES.map.with_index { |_n, i| i.to_s }.slice(1, 12) - number(name) || raise(ConfigurationError, - "Did not recognize month #{name}, must be one of #{(NAMES + numbers).inspect}") + def number!(name) + numbers = NAMES.map.with_index { |_n, i| i.to_s }.slice(1, 12) + number(name) || raise(ConfigurationError, + "Did not recognize month #{name}, must be one of #{(NAMES + numbers).inspect}") + end end end end diff --git a/lib/montrose/options.rb b/lib/montrose/options.rb index f531e2c..7819713 100644 --- a/lib/montrose/options.rb +++ b/lib/montrose/options.rb @@ -216,7 +216,7 @@ def week=(weeks) end def month=(months) - @month = map_arg(months) { |d| Montrose::Month.number!(d) } + @month = Montrose::Month.parse(months) end def between=(range) From 54522510018acc16586e1b21affb9497b6790275 Mon Sep 17 00:00:00 2001 From: Ross Kaffenberger Date: Sat, 6 Feb 2021 12:30:39 -0500 Subject: [PATCH 16/33] Add ical spec --- lib/montrose/day.rb | 8 ++++---- spec/from_ical_rfc_spec.rb | 29 ++++++++++++++++++++--------- spec/montrose/day_spec.rb | 4 ++-- 3 files changed, 26 insertions(+), 15 deletions(-) diff --git a/lib/montrose/day.rb b/lib/montrose/day.rb index 46d8a0a..f3b25f8 100644 --- a/lib/montrose/day.rb +++ b/lib/montrose/day.rb @@ -15,18 +15,18 @@ def parse(arg) when Hash parse_entries(arg.entries) when String - parse(arg.split(',')) + parse(arg.split(",")) else parse_entries(map_arg(arg) { |value| parse_value(value) }) end end def parse_entries(entries) - hash = Hash.new {|h,k| h[k] = []} - result = entries.each_with_object(hash) do |(k, v), hash| + hash = Hash.new { |h, k| h[k] = [] } + result = entries.each_with_object(hash) { |(k, v), hash| index = number!(k) hash[index] = hash[index] + [*v] - end + } result.values.all?(&:empty?) ? result.keys : result end diff --git a/spec/from_ical_rfc_spec.rb b/spec/from_ical_rfc_spec.rb index af2437a..a08f0e0 100644 --- a/spec/from_ical_rfc_spec.rb +++ b/spec/from_ical_rfc_spec.rb @@ -291,16 +291,27 @@ def parse_expected_events(event_map) _(recurrence).must_pair_with expected_events end - # Every other month on the first and last Sunday of the month for 10 - # occurrences: - - # DTSTART;TZID=America/New_York:19970907T090000 - # RRULE:FREQ=MONTHLY;INTERVAL=2;COUNT=10;BYDAY=1SU,-1SU + it "every other month on the first and last Sunday of the month for 10 + occurrences" do + ical = <<~ICAL + DTSTART;TZID=America/New_York:19970907T090000 + RRULE:FREQ=MONTHLY;INTERVAL=2;COUNT=10;BYDAY=1SU,-1SU + ICAL + # ==> (1997 9:00 AM EDT) September 7,28 + # (1997 9:00 AM EST) November 2,30 + # (1998 9:00 AM EST) January 4,25;March 1,29 + # (1998 9:00 AM EDT) May 3,31 + expected_events = parse_expected_events( + "1997 9:00 AM EDT" => {"Sep" => [7, 28]}, + "1997 9:00 AM EST" => {"Nov" => [2, 30]}, + "1998 9:00 AM EST" => {"Jan" => [4, 25], + "Mar" => [1, 29]}, + "1998 9:00 AM EDT" => {"May" => [3, 31]} + ) - # ==> (1997 9:00 AM EDT) September 7,28 - # (1997 9:00 AM EST) November 2,30 - # (1998 9:00 AM EST) January 4,25;March 1,29 - # (1998 9:00 AM EDT) May 3,31 + recurrence = Montrose::Recurrence.from_ical(ical) + _(recurrence).must_pair_with expected_events + end # Monthly on the second-to-last Monday of the month for 6 months: diff --git a/spec/montrose/day_spec.rb b/spec/montrose/day_spec.rb index fd17008..b2c1fd7 100644 --- a/spec/montrose/day_spec.rb +++ b/spec/montrose/day_spec.rb @@ -13,14 +13,14 @@ def number!(name) it { _(parse(:friday)).must_equal([5]) } it { _(parse(:friday)).must_equal([5]) } it { _(parse([:friday])).must_equal([5]) } - it { _(parse('friday')).must_equal([5]) } + it { _(parse("friday")).must_equal([5]) } it { _(parse(%w[thursday friday])).must_equal([4, 5]) } it { _(parse(%w[Thursday Friday])).must_equal([4, 5]) } it { _(parse(:fri)).must_equal([5]) } it { _(parse(:fri)).must_equal([5]) } it { _(parse([:fri])).must_equal([5]) } - it { _(parse('fri')).must_equal([5]) } + it { _(parse("fri")).must_equal([5]) } it { _(parse(%w[thu fri])).must_equal([4, 5]) } it { _(parse(%w[Thu Fri])).must_equal([4, 5]) } From e4662a07c1786838b052179fa6811ec6987c3aea Mon Sep 17 00:00:00 2001 From: Ross Kaffenberger Date: Sat, 6 Feb 2021 14:09:10 -0500 Subject: [PATCH 17/33] Add ical spec --- spec/from_ical_rfc_spec.rb | 26 +++++++++++++++++++------- 1 file changed, 19 insertions(+), 7 deletions(-) diff --git a/spec/from_ical_rfc_spec.rb b/spec/from_ical_rfc_spec.rb index a08f0e0..684d16d 100644 --- a/spec/from_ical_rfc_spec.rb +++ b/spec/from_ical_rfc_spec.rb @@ -313,12 +313,24 @@ def parse_expected_events(event_map) _(recurrence).must_pair_with expected_events end - # Monthly on the second-to-last Monday of the month for 6 months: - - # DTSTART;TZID=America/New_York:19970922T090000 - # RRULE:FREQ=MONTHLY;COUNT=6;BYDAY=-2MO + it "monthly on the second-to-last Monday of the month for 6 months" do + ical = <<~ical + DTSTART;TZID=America/New_York:19970922T090000 + RRULE:FREQ=MONTHLY;COUNT=6;BYDAY=-2MO + ical + # ==> (1997 9:00 AM EDT) September 22;October 20 + # (1997 9:00 AM EST) November 17;December 22 + # (1998 9:00 AM EST) January 19;February 16 + expected_events = parse_expected_events( + "1997 9:00 AM EDT" => {"Sep" => [22], + "Oct" => [20]}, + "1997 9:00 AM EST" => {"Nov" => [17], + "Dec" => [22]}, + "1998 9:00 AM EST" => {"Jan" => [19], + "Feb" => [16]} + ) - # ==> (1997 9:00 AM EDT) September 22;October 20 - # (1997 9:00 AM EST) November 17;December 22 - # (1998 9:00 AM EST) January 19;February 16 + recurrence = Montrose::Recurrence.from_ical(ical) + _(recurrence).must_pair_with expected_events + end end From 136e1d86a03dfedfa5503db0826fcf4c97aaed91 Mon Sep 17 00:00:00 2001 From: Ross Kaffenberger Date: Sat, 6 Feb 2021 15:06:25 -0500 Subject: [PATCH 18/33] Extract MonthDay parsing --- lib/montrose.rb | 1 + lib/montrose/ical.rb | 4 + lib/montrose/month_day.rb | 25 ++++ lib/montrose/options.rb | 8 +- spec/from_ical_rfc_spec.rb | 270 ++++++++++++++++++++++++++++++++++++- 5 files changed, 300 insertions(+), 8 deletions(-) create mode 100644 lib/montrose/month_day.rb diff --git a/lib/montrose.rb b/lib/montrose.rb index 8892ffa..a93c604 100644 --- a/lib/montrose.rb +++ b/lib/montrose.rb @@ -23,6 +23,7 @@ module Montrose autoload :Day, "montrose/day" autoload :Month, "montrose/month" + autoload :MonthDay, "montrose/month_day" extend Chainable diff --git a/lib/montrose/ical.rb b/lib/montrose/ical.rb index fa0b7fd..79160f3 100644 --- a/lib/montrose/ical.rb +++ b/lib/montrose/ical.rb @@ -44,8 +44,12 @@ def parse_rrule(rrule) [:month, Montrose::Month.parse(value)] when "BYDAY" [:day, Montrose::Day.parse(value)] + when "BYMONTHDAY" + [:mday, Montrose::MonthDay.parse(value)] when "WKST" [:week_start, value] + else + raise "Unrecognized rrule '#{rule}'" end end end diff --git a/lib/montrose/month_day.rb b/lib/montrose/month_day.rb new file mode 100644 index 0000000..f58bfd2 --- /dev/null +++ b/lib/montrose/month_day.rb @@ -0,0 +1,25 @@ +module Montrose + class MonthDay + class << self + MDAYS = (-31.upto(-1) + 1.upto(31)).to_a + + def parse(mdays) + return nil unless mdays.present? + + case mdays + when String + parse(mdays.split(",")) + else + Array(mdays).map { |d| Montrose::MonthDay.assert(d.to_i) } + end + end + + def assert(number) + test = number.abs + raise ConfigurationError, "Out of range: #{MDAYS.inspect} does not include #{test}" unless MDAYS.include?(number.abs) + + number + end + end + end +end diff --git a/lib/montrose/options.rb b/lib/montrose/options.rb index 7819713..b753dc0 100644 --- a/lib/montrose/options.rb +++ b/lib/montrose/options.rb @@ -204,7 +204,7 @@ def day=(days) end def mday=(mdays) - @mday = map_mdays(mdays) + @mday = Montrose::MonthDay.parse(mdays) end def yday=(ydays) @@ -286,10 +286,6 @@ def map_arg(arg, &block) Array(arg).map(&block) end - def map_mdays(arg) - map_arg(arg) { |d| assert_mday(d) } - end - def map_ydays(arg) map_arg(arg) { |d| assert_yday(d) } end @@ -317,7 +313,7 @@ def decompose_on_arg(arg) key, val = month_or_day(k) result[key] = val result[:mday] ||= [] - result[:mday] += map_mdays(v) + result[:mday] += Montrose::MonthDay.parse(v) end else {day: Montrose::Day.parse(arg)} diff --git a/spec/from_ical_rfc_spec.rb b/spec/from_ical_rfc_spec.rb index 684d16d..a1513e3 100644 --- a/spec/from_ical_rfc_spec.rb +++ b/spec/from_ical_rfc_spec.rb @@ -314,10 +314,10 @@ def parse_expected_events(event_map) end it "monthly on the second-to-last Monday of the month for 6 months" do - ical = <<~ical + ical = <<~ICAL DTSTART;TZID=America/New_York:19970922T090000 RRULE:FREQ=MONTHLY;COUNT=6;BYDAY=-2MO - ical + ICAL # ==> (1997 9:00 AM EDT) September 22;October 20 # (1997 9:00 AM EST) November 17;December 22 # (1998 9:00 AM EST) January 19;February 16 @@ -333,4 +333,270 @@ def parse_expected_events(event_map) recurrence = Montrose::Recurrence.from_ical(ical) _(recurrence).must_pair_with expected_events end + + it "monthly on the third-to-the-last day of the month, forever" do + ical = <<~ICAL + DTSTART;TZID=America/New_York:19970928T090000 + RRULE:FREQ=MONTHLY;BYMONTHDAY=-3 + ICAL + # ==> (1997 9:00 AM EDT) September 28 + # (1997 9:00 AM EST) October 29;November 28;December 29 + # (1998 9:00 AM EST) January 29;February 26 + # ... + expected_events = parse_expected_events( + "1997 9:00 AM EDT" => {"Sep" => [28]}, + "1997 9:00 AM EST" => {"Oct" => [29], + "Nov" => [28], + "Dec" => [29]}, + "1998 9:00 AM EST" => {"Jan" => [29], + "Feb" => [26]} + ) + + recurrence = Montrose::Recurrence.from_ical(ical) + _(recurrence).must_pair_with expected_events + end + + it "monthly on the 2nd and 15th of the month for 10 occurrences" do + ical = <<~ICAL + DTSTART;TZID=America/New_York:19970902T090000 + RRULE:FREQ=MONTHLY;COUNT=10;BYMONTHDAY=2,15 + ICAL + # ==> (1997 9:00 AM EDT) September 2,15;October 2,15 + # (1997 9:00 AM EST) November 2,15;December 2,15 + # (1998 9:00 AM EST) January 2,15 + expected_events = parse_expected_events( + "1997 9:00 AM EDT" => {"Sep" => [2, 15], + "Oct" => [2, 15]}, + "1997 9:00 AM EST" => {"Nov" => [2, 15], + "Dec" => [2, 15]}, + "1998 9:00 AM EST" => {"Jan" => [2, 15]} + ) + + recurrence = Montrose::Recurrence.from_ical(ical) + _(recurrence).must_pair_with expected_events + end + + # Monthly on the first and last day of the month for 10 occurrences: + + # DTSTART;TZID=America/New_York:19970930T090000 + # RRULE:FREQ=MONTHLY;COUNT=10;BYMONTHDAY=1,-1 + + # ==> (1997 9:00 AM EDT) September 30;October 1 + # (1997 9:00 AM EST) October 31;November 1,30;December 1,31 + # (1998 9:00 AM EST) January 1,31;February 1 + + # Every 18 months on the 10th thru 15th of the month for 10 + # occurrences: + + # DTSTART;TZID=America/New_York:19970910T090000 + # RRULE:FREQ=MONTHLY;INTERVAL=18;COUNT=10;BYMONTHDAY=10,11,12, + # 13,14,15 + + # ==> (1997 9:00 AM EDT) September 10,11,12,13,14,15 + # (1999 9:00 AM EST) March 10,11,12,13 + + # Every Tuesday, every other month: + + # DTSTART;TZID=America/New_York:19970902T090000 + # RRULE:FREQ=MONTHLY;INTERVAL=2;BYDAY=TU + + # ==> (1997 9:00 AM EDT) September 2,9,16,23,30 + # (1997 9:00 AM EST) November 4,11,18,25 + # (1998 9:00 AM EST) January 6,13,20,27;March 3,10,17,24,31 + # ... + + # Yearly in June and July for 10 occurrences: + + # DTSTART;TZID=America/New_York:19970610T090000 + # RRULE:FREQ=YEARLY;COUNT=10;BYMONTH=6,7 + + # ==> (1997 9:00 AM EDT) June 10;July 10 + # (1998 9:00 AM EDT) June 10;July 10 + # (1999 9:00 AM EDT) June 10;July 10 + # (2000 9:00 AM EDT) June 10;July 10 + # (2001 9:00 AM EDT) June 10;July 10 + + # Note: Since none of the BYDAY, BYMONTHDAY, or BYYEARDAY + # components are specified, the day is gotten from "DTSTART". + + # Every other year on January, February, and March for 10 + # occurrences: + + # DTSTART;TZID=America/New_York:19970310T090000 + # RRULE:FREQ=YEARLY;INTERVAL=2;COUNT=10;BYMONTH=1,2,3 + + # ==> (1997 9:00 AM EST) March 10 + # (1999 9:00 AM EST) January 10;February 10;March 10 + # (2001 9:00 AM EST) January 10;February 10;March 10 + # (2003 9:00 AM EST) January 10;February 10;March 10 + + # Every third year on the 1st, 100th, and 200th day for 10 + # occurrences: + + # DTSTART;TZID=America/New_York:19970101T090000 + # RRULE:FREQ=YEARLY;INTERVAL=3;COUNT=10;BYYEARDAY=1,100,200 + + # ==> (1997 9:00 AM EST) January 1 + # (1997 9:00 AM EDT) April 10;July 19 + # (2000 9:00 AM EST) January 1 + # (2000 9:00 AM EDT) April 9;July 18 + # (2003 9:00 AM EST) January 1 + # (2003 9:00 AM EDT) April 10;July 19 + # (2006 9:00 AM EST) January 1 + + # Every 20th Monday of the year, forever: + + # DTSTART;TZID=America/New_York:19970519T090000 + # RRULE:FREQ=YEARLY;BYDAY=20MO + + # ==> (1997 9:00 AM EDT) May 19 + # (1998 9:00 AM EDT) May 18 + # (1999 9:00 AM EDT) May 17 + # ... + + # Monday of week number 20 (where the default start of the week is + # Monday), forever: + + # DTSTART;TZID=America/New_York:19970512T090000 + # RRULE:FREQ=YEARLY;BYWEEKNO=20;BYDAY=MO + + # ==> (1997 9:00 AM EDT) May 12 + # (1998 9:00 AM EDT) May 11 + # (1999 9:00 AM EDT) May 17 + # ... + + # Every Thursday in March, forever: + + # DTSTART;TZID=America/New_York:19970313T090000 + # RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=TH + + # ==> (1997 9:00 AM EST) March 13,20,27 + # (1998 9:00 AM EST) March 5,12,19,26 + # (1999 9:00 AM EST) March 4,11,18,25 + # ... + + # Every Thursday, but only during June, July, and August, forever: + + # DTSTART;TZID=America/New_York:19970605T090000 + # RRULE:FREQ=YEARLY;BYDAY=TH;BYMONTH=6,7,8 + + # ==> (1997 9:00 AM EDT) June 5,12,19,26;July 3,10,17,24,31; + # August 7,14,21,28 + # (1998 9:00 AM EDT) June 4,11,18,25;July 2,9,16,23,30; + # August 6,13,20,27 + # (1999 9:00 AM EDT) June 3,10,17,24;July 1,8,15,22,29; + # August 5,12,19,26 + # ... + + # Every Friday the 13th, forever: + + # DTSTART;TZID=America/New_York:19970902T090000 + # EXDATE;TZID=America/New_York:19970902T090000 + # RRULE:FREQ=MONTHLY;BYDAY=FR;BYMONTHDAY=13 + + # ==> (1998 9:00 AM EST) February 13;March 13;November 13 + # (1999 9:00 AM EDT) August 13 + # (2000 9:00 AM EDT) October 13 + # ... + + # The first Saturday that follows the first Sunday of the month, + # forever: + + # DTSTART;TZID=America/New_York:19970913T090000 + # RRULE:FREQ=MONTHLY;BYDAY=SA;BYMONTHDAY=7,8,9,10,11,12,13 + + # ==> (1997 9:00 AM EDT) September 13;October 11 + # (1997 9:00 AM EST) November 8;December 13 + # (1998 9:00 AM EST) January 10;February 7;March 7 + # (1998 9:00 AM EDT) April 11;May 9;June 13... + # ... + + # Every 4 years, the first Tuesday after a Monday in November, + # forever (U.S. Presidential Election day): + + # DTSTART;TZID=America/New_York:19961105T090000 + # RRULE:FREQ=YEARLY;INTERVAL=4;BYMONTH=11;BYDAY=TU; + # BYMONTHDAY=2,3,4,5,6,7,8 + + # ==> (1996 9:00 AM EST) November 5 + # (2000 9:00 AM EST) November 7 + # (2004 9:00 AM EST) November 2 + # ... + + # The third instance into the month of one of Tuesday, Wednesday, or + # Thursday, for the next 3 months: + + # DTSTART;TZID=America/New_York:19970904T090000 + # RRULE:FREQ=MONTHLY;COUNT=3;BYDAY=TU,WE,TH;BYSETPOS=3 + + # ==> (1997 9:00 AM EDT) September 4;October 7 + # (1997 9:00 AM EST) November 6 + + # The second-to-last weekday of the month: + + # DTSTART;TZID=America/New_York:19970929T090000 + # RRULE:FREQ=MONTHLY;BYDAY=MO,TU,WE,TH,FR;BYSETPOS=-2 + + # ==> (1997 9:00 AM EDT) September 29 + # (1997 9:00 AM EST) October 30;November 27;December 30 + # (1998 9:00 AM EST) January 29;February 26;March 30 + # ... + + # Every 3 hours from 9:00 AM to 5:00 PM on a specific day: + + # DTSTART;TZID=America/New_York:19970902T090000 + # RRULE:FREQ=HOURLY;INTERVAL=3;UNTIL=19970902T170000Z + + # ==> (September 2, 1997 EDT) 09:00,12:00,15:00 + + # Every 15 minutes for 6 occurrences: + + # DTSTART;TZID=America/New_York:19970902T090000 + # RRULE:FREQ=MINUTELY;INTERVAL=15;COUNT=6 + + # ==> (September 2, 1997 EDT) 09:00,09:15,09:30,09:45,10:00,10:15 + + # Every hour and a half for 4 occurrences: + + # DTSTART;TZID=America/New_York:19970902T090000 + # RRULE:FREQ=MINUTELY;INTERVAL=90;COUNT=4 + + # ==> (September 2, 1997 EDT) 09:00,10:30;12:00;13:30 + + # Every 20 minutes from 9:00 AM to 4:40 PM every day: + + # DTSTART;TZID=America/New_York:19970902T090000 + # RRULE:FREQ=DAILY;BYHOUR=9,10,11,12,13,14,15,16;BYMINUTE=0,20,40 + # or + # RRULE:FREQ=MINUTELY;INTERVAL=20;BYHOUR=9,10,11,12,13,14,15,16 + + # ==> (September 2, 1997 EDT) 9:00,9:20,9:40,10:00,10:20, + # ... 16:00,16:20,16:40 + # (September 3, 1997 EDT) 9:00,9:20,9:40,10:00,10:20, + # ...16:00,16:20,16:40 + # ... + + # An example where the days generated makes a difference because of + # WKST: + + # DTSTART;TZID=America/New_York:19970805T090000 + # RRULE:FREQ=WEEKLY;INTERVAL=2;COUNT=4;BYDAY=TU,SU;WKST=MO + + # ==> (1997 EDT) August 5,10,19,24 + + # changing only WKST from MO to SU, yields different results... + + # DTSTART;TZID=America/New_York:19970805T090000 + # RRULE:FREQ=WEEKLY;INTERVAL=2;COUNT=4;BYDAY=TU,SU;WKST=SU + + # ==> (1997 EDT) August 5,17,19,31 + + # An example where an invalid date (i.e., February 30) is ignored. + + # DTSTART;TZID=America/New_York:20070115T090000 + # RRULE:FREQ=MONTHLY;BYMONTHDAY=15,30;COUNT=5 + + # ==> (2007 EST) January 15,30 + # (2007 EST) February 15 + # (2007 EDT) March 15,30 end From 7623a6d897c41f763cc55c4a91c38de591d99ea4 Mon Sep 17 00:00:00 2001 From: Ross Kaffenberger Date: Sat, 6 Feb 2021 15:57:02 -0500 Subject: [PATCH 19/33] Add ical specs --- spec/from_ical_rfc_spec.rb | 76 ++++++++++++++++++++++++++------------ 1 file changed, 53 insertions(+), 23 deletions(-) diff --git a/spec/from_ical_rfc_spec.rb b/spec/from_ical_rfc_spec.rb index a1513e3..b6ab9f2 100644 --- a/spec/from_ical_rfc_spec.rb +++ b/spec/from_ical_rfc_spec.rb @@ -376,34 +376,64 @@ def parse_expected_events(event_map) _(recurrence).must_pair_with expected_events end - # Monthly on the first and last day of the month for 10 occurrences: - - # DTSTART;TZID=America/New_York:19970930T090000 - # RRULE:FREQ=MONTHLY;COUNT=10;BYMONTHDAY=1,-1 - - # ==> (1997 9:00 AM EDT) September 30;October 1 - # (1997 9:00 AM EST) October 31;November 1,30;December 1,31 - # (1998 9:00 AM EST) January 1,31;February 1 - - # Every 18 months on the 10th thru 15th of the month for 10 - # occurrences: + it "monthly on the first and last day of the month for 10 occurrences" do + ical = <<~ICAL + DTSTART;TZID=America/New_York:19970930T090000 + RRULE:FREQ=MONTHLY;COUNT=10;BYMONTHDAY=1,-1 + ICAL + # ==> (1997 9:00 AM EDT) September 30;October 1 + # (1997 9:00 AM EST) October 31;November 1,30;December 1,31 + # (1998 9:00 AM EST) January 1,31;February 1 + expected_events = parse_expected_events( + "1997 9:00 AM EDT" => {"Sep" => [30], + "Oct" => [1]}, + "1997 9:00 AM EST" => {"Oct" => [31], + "Nov" => [1, 30], + "Dec" => [1, 31]}, + "1998 9:00 AM EST" => {"Jan" => [1, 31], + "Feb" => [1]} + ) - # DTSTART;TZID=America/New_York:19970910T090000 - # RRULE:FREQ=MONTHLY;INTERVAL=18;COUNT=10;BYMONTHDAY=10,11,12, - # 13,14,15 + recurrence = Montrose::Recurrence.from_ical(ical) + _(recurrence).must_pair_with expected_events + end - # ==> (1997 9:00 AM EDT) September 10,11,12,13,14,15 - # (1999 9:00 AM EST) March 10,11,12,13 + it "every 18 months on the 10th thru 15th of the month for 10 + occurrences" do + ical = <<~ICAL + DTSTART;TZID=America/New_York:19970910T090000 + RRULE:FREQ=MONTHLY;INTERVAL=18;COUNT=10;BYMONTHDAY=10,11,12, + 13,14,15 + ICAL + # ==> (1997 9:00 AM EDT) September 10,11,12,13,14,15 + # (1999 9:00 AM EST) March 10,11,12,13 + expected_events = parse_expected_events( + "1997 9:00 AM EDT" => {"Sep" => [10, 11, 12, 13, 14, 15]}, + "1999 9:00 AM EST" => {"Mar" => [10, 11, 12, 13]} + ) - # Every Tuesday, every other month: + recurrence = Montrose::Recurrence.from_ical(ical) + _(recurrence).must_pair_with expected_events + end - # DTSTART;TZID=America/New_York:19970902T090000 - # RRULE:FREQ=MONTHLY;INTERVAL=2;BYDAY=TU + it "every Tuesday, every other month" do + ical = <<~ICAL + DTSTART;TZID=America/New_York:19970902T090000 + RRULE:FREQ=MONTHLY;INTERVAL=2;BYDAY=TU + ICAL + # ==> (1997 9:00 AM EDT) September 2,9,16,23,30 + # (1997 9:00 AM EST) November 4,11,18,25 + # (1998 9:00 AM EST) January 6,13,20,27;March 3,10,17,24,31 + expected_events = parse_expected_events( + "1997 9:00 AM EDT" => {"Sep" => [2, 9, 16, 23, 30]}, + "1997 9:00 AM EST" => {"Nov" => [4, 11, 18, 25]}, + "1998 9:00 AM EST" => {"Jan" => [6, 13, 20, 27], + "Mar" => [3, 10, 17, 24, 31]} + ) - # ==> (1997 9:00 AM EDT) September 2,9,16,23,30 - # (1997 9:00 AM EST) November 4,11,18,25 - # (1998 9:00 AM EST) January 6,13,20,27;March 3,10,17,24,31 - # ... + recurrence = Montrose::Recurrence.from_ical(ical) + _(recurrence).must_pair_with expected_events + end # Yearly in June and July for 10 occurrences: From e3e5b34a34023dff300bfe3babaa8195f66419dc Mon Sep 17 00:00:00 2001 From: Ross Kaffenberger Date: Sat, 6 Feb 2021 16:12:44 -0500 Subject: [PATCH 20/33] Add ical specs --- spec/from_ical_rfc_spec.rb | 71 +++++++++++++++++++++++++++----------- 1 file changed, 50 insertions(+), 21 deletions(-) diff --git a/spec/from_ical_rfc_spec.rb b/spec/from_ical_rfc_spec.rb index b6ab9f2..217dd47 100644 --- a/spec/from_ical_rfc_spec.rb +++ b/spec/from_ical_rfc_spec.rb @@ -435,30 +435,59 @@ def parse_expected_events(event_map) _(recurrence).must_pair_with expected_events end - # Yearly in June and July for 10 occurrences: - - # DTSTART;TZID=America/New_York:19970610T090000 - # RRULE:FREQ=YEARLY;COUNT=10;BYMONTH=6,7 - - # ==> (1997 9:00 AM EDT) June 10;July 10 - # (1998 9:00 AM EDT) June 10;July 10 - # (1999 9:00 AM EDT) June 10;July 10 - # (2000 9:00 AM EDT) June 10;July 10 - # (2001 9:00 AM EDT) June 10;July 10 - - # Note: Since none of the BYDAY, BYMONTHDAY, or BYYEARDAY - # components are specified, the day is gotten from "DTSTART". + it "yearly in June and July for 10 occurrences" do + ical = <<~ical + DTSTART;TZID=America/New_York:19970610T090000 + RRULE:FREQ=YEARLY;COUNT=10;BYMONTH=6,7 + ical + # ==> (1997 9:00 AM EDT) June 10;July 10 + # (1998 9:00 AM EDT) June 10;July 10 + # (1999 9:00 AM EDT) June 10;July 10 + # (2000 9:00 AM EDT) June 10;July 10 + # (2001 9:00 AM EDT) June 10;July 10 + expected_events = parse_expected_events( + "1997 9:00 AM EDT" => {"Jun" => [10], + "Jul" => [10]}, + "1998 9:00 AM EDT" => {"Jun" => [10], + "Jul" => [10]}, + "1999 9:00 AM EDT" => {"Jun" => [10], + "Jul" => [10]}, + "2000 9:00 AM EDT" => {"Jun" => [10], + "Jul" => [10]}, + "2001 9:00 AM EDT" => {"Jun" => [10], + "Jul" => [10]}, + ) - # Every other year on January, February, and March for 10 - # occurrences: + recurrence = Montrose::Recurrence.from_ical(ical) + _(recurrence).must_pair_with expected_events + end - # DTSTART;TZID=America/New_York:19970310T090000 - # RRULE:FREQ=YEARLY;INTERVAL=2;COUNT=10;BYMONTH=1,2,3 + it "Every other year on January, February, and March for 10 + occurrences" do + ical = <<~ical + DTSTART;TZID=America/New_York:19970310T090000 + RRULE:FREQ=YEARLY;INTERVAL=2;COUNT=10;BYMONTH=1,2,3 + ical + # ==> (1997 9:00 AM EST) March 10 + # (1999 9:00 AM EST) January 10;February 10;March 10 + # (2001 9:00 AM EST) January 10;February 10;March 10 + # (2003 9:00 AM EST) January 10;February 10;March 10 + expected_events = parse_expected_events( + "1997 9:00 AM EST" => {"Mar" => [10]}, + "1999 9:00 AM EST" => {"Jan" => [10], + "Feb" => [10], + "Mar" => [10]}, + "2001 9:00 AM EST" => {"Jan" => [10], + "Feb" => [10], + "Mar" => [10]}, + "2003 9:00 AM EST" => {"Jan" => [10], + "Feb" => [10], + "Mar" => [10]}, + ) - # ==> (1997 9:00 AM EST) March 10 - # (1999 9:00 AM EST) January 10;February 10;March 10 - # (2001 9:00 AM EST) January 10;February 10;March 10 - # (2003 9:00 AM EST) January 10;February 10;March 10 + recurrence = Montrose::Recurrence.from_ical(ical) + _(recurrence).must_pair_with expected_events + end # Every third year on the 1st, 100th, and 200th day for 10 # occurrences: From c232d6c3cf260cc2add972a1962aade64fee6354 Mon Sep 17 00:00:00 2001 From: Ross Kaffenberger Date: Sat, 6 Feb 2021 16:28:17 -0500 Subject: [PATCH 21/33] Extract YearDay parsing --- lib/montrose.rb | 1 + lib/montrose/ical.rb | 2 ++ lib/montrose/options.rb | 14 +------------ lib/montrose/year_day.rb | 25 +++++++++++++++++++++++ spec/from_ical_rfc_spec.rb | 42 ++++++++++++++++++++++++++------------ 5 files changed, 58 insertions(+), 26 deletions(-) create mode 100644 lib/montrose/year_day.rb diff --git a/lib/montrose.rb b/lib/montrose.rb index a93c604..0f691da 100644 --- a/lib/montrose.rb +++ b/lib/montrose.rb @@ -24,6 +24,7 @@ module Montrose autoload :Day, "montrose/day" autoload :Month, "montrose/month" autoload :MonthDay, "montrose/month_day" + autoload :YearDay, "montrose/year_day" extend Chainable diff --git a/lib/montrose/ical.rb b/lib/montrose/ical.rb index 79160f3..1f871c3 100644 --- a/lib/montrose/ical.rb +++ b/lib/montrose/ical.rb @@ -46,6 +46,8 @@ def parse_rrule(rrule) [:day, Montrose::Day.parse(value)] when "BYMONTHDAY" [:mday, Montrose::MonthDay.parse(value)] + when "BYYEARDAY" + [:yday, Montrose::YearDay.parse(value)] when "WKST" [:week_start, value] else diff --git a/lib/montrose/options.rb b/lib/montrose/options.rb index b753dc0..fb9872b 100644 --- a/lib/montrose/options.rb +++ b/lib/montrose/options.rb @@ -208,7 +208,7 @@ def mday=(mdays) end def yday=(ydays) - @yday = map_ydays(ydays) + @yday = YearDay.parse(ydays) end def week=(weeks) @@ -286,22 +286,10 @@ def map_arg(arg, &block) Array(arg).map(&block) end - def map_ydays(arg) - map_arg(arg) { |d| assert_yday(d) } - end - def assert_hour(hour) assert_range_includes(1..::Montrose::Utils::MAX_HOURS_IN_DAY, hour) end - def assert_mday(mday) - assert_range_includes(1..::Montrose::Utils::MAX_DAYS_IN_MONTH, mday, :absolute) - end - - def assert_yday(yday) - assert_range_includes(1..::Montrose::Utils::MAX_DAYS_IN_YEAR, yday, :absolute) - end - def assert_week(week) assert_range_includes(1..::Montrose::Utils::MAX_WEEKS_IN_YEAR, week, :absolute) end diff --git a/lib/montrose/year_day.rb b/lib/montrose/year_day.rb new file mode 100644 index 0000000..f84d780 --- /dev/null +++ b/lib/montrose/year_day.rb @@ -0,0 +1,25 @@ +module Montrose + class YearDay + class << self + YDAYS = (1.upto(366)).to_a + + def parse(ydays) + return nil unless ydays.present? + + case ydays + when String + parse(ydays.split(",")) + else + Array(ydays).map { |d| assert(d.to_i) } + end + end + + def assert(number) + test = number.abs + raise ConfigurationError, "Out of range: #{YDAYS.inspect} does not include #{test}" unless YDAYS.include?(number.abs) + + number + end + end + end +end diff --git a/spec/from_ical_rfc_spec.rb b/spec/from_ical_rfc_spec.rb index 217dd47..a88add9 100644 --- a/spec/from_ical_rfc_spec.rb +++ b/spec/from_ical_rfc_spec.rb @@ -489,19 +489,35 @@ def parse_expected_events(event_map) _(recurrence).must_pair_with expected_events end - # Every third year on the 1st, 100th, and 200th day for 10 - # occurrences: - - # DTSTART;TZID=America/New_York:19970101T090000 - # RRULE:FREQ=YEARLY;INTERVAL=3;COUNT=10;BYYEARDAY=1,100,200 - - # ==> (1997 9:00 AM EST) January 1 - # (1997 9:00 AM EDT) April 10;July 19 - # (2000 9:00 AM EST) January 1 - # (2000 9:00 AM EDT) April 9;July 18 - # (2003 9:00 AM EST) January 1 - # (2003 9:00 AM EDT) April 10;July 19 - # (2006 9:00 AM EST) January 1 + it "every third year on the 1st, 100th, and 200th day for 10 + occurrences" do + ical = <<~ical + DTSTART;TZID=America/New_York:19970101T090000 + RRULE:FREQ=YEARLY;INTERVAL=3;COUNT=10;BYYEARDAY=1,100,200 + ical + # ==> (1997 9:00 AM EST) January 1 + # (1997 9:00 AM EDT) April 10;July 19 + # (2000 9:00 AM EST) January 1 + # (2000 9:00 AM EDT) April 9;July 18 + # (2003 9:00 AM EST) January 1 + # (2003 9:00 AM EDT) April 10;July 19 + # (2006 9:00 AM EST) January 1 + expected_events = parse_expected_events( + "1997 9:00 AM EST" => {"Jan" => [1]}, + "1997 9:00 AM EDT" => {"Apr" => [10], + "Jul" => [19]}, + "2000 9:00 AM EST" => {"Jan" => [1]}, + "2000 9:00 AM EDT" => {"Apr" => [9], + "Jul" => [18]}, + "2003 9:00 AM EST" => {"Jan" => [1]}, + "2003 9:00 AM EDT" => {"Apr" => [10], + "Jul" => [19]}, + "2006 9:00 AM EST" => {"Jan" => [1]}, + ) + + recurrence = Montrose::Recurrence.from_ical(ical) + _(recurrence).must_pair_with expected_events + end # Every 20th Monday of the year, forever: From 07fa755674ef73dfb6f07ddcfb290f30538c5e74 Mon Sep 17 00:00:00 2001 From: Ross Kaffenberger Date: Sat, 6 Feb 2021 16:28:29 -0500 Subject: [PATCH 22/33] Update method references --- lib/montrose/month_day.rb | 2 +- lib/montrose/options.rb | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/montrose/month_day.rb b/lib/montrose/month_day.rb index f58bfd2..2b9bca0 100644 --- a/lib/montrose/month_day.rb +++ b/lib/montrose/month_day.rb @@ -10,7 +10,7 @@ def parse(mdays) when String parse(mdays.split(",")) else - Array(mdays).map { |d| Montrose::MonthDay.assert(d.to_i) } + Array(mdays).map { |d| assert(d.to_i) } end end diff --git a/lib/montrose/options.rb b/lib/montrose/options.rb index fb9872b..5435df1 100644 --- a/lib/montrose/options.rb +++ b/lib/montrose/options.rb @@ -200,11 +200,11 @@ def during=(during_arg) end def day=(days) - @day = Montrose::Day.parse(days) + @day = Day.parse(days) end def mday=(mdays) - @mday = Montrose::MonthDay.parse(mdays) + @mday = MonthDay.parse(mdays) end def yday=(ydays) @@ -216,7 +216,7 @@ def week=(weeks) end def month=(months) - @month = Montrose::Month.parse(months) + @month = Month.parse(months) end def between=(range) From 1ef0ef3d3d7fef34ebf726f52da523e88ca63ac2 Mon Sep 17 00:00:00 2001 From: Ross Kaffenberger Date: Sat, 6 Feb 2021 20:29:02 -0500 Subject: [PATCH 23/33] Remove method --- lib/montrose/options.rb | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/lib/montrose/options.rb b/lib/montrose/options.rb index 5435df1..56ffc41 100644 --- a/lib/montrose/options.rb +++ b/lib/montrose/options.rb @@ -269,17 +269,6 @@ def default_until self.class.default_until end - def nested_map_arg(arg, &block) - case arg - when Hash - arg.each_with_object({}) do |(k, v), hash| - hash[yield k] = [*v] - end - else - map_arg(arg, &block) - end - end - def map_arg(arg, &block) return nil unless arg From 6d318adfa9f5098108b8895ebe21bfb88c401a3e Mon Sep 17 00:00:00 2001 From: Ross Kaffenberger Date: Sat, 6 Feb 2021 20:36:12 -0500 Subject: [PATCH 24/33] Extract Week number parsing --- lib/montrose.rb | 1 + lib/montrose/ical.rb | 2 ++ lib/montrose/options.rb | 6 +---- lib/montrose/week.rb | 20 +++++++++++++++ spec/from_ical_rfc_spec.rb | 51 ++++++++++++++++++++++++++------------ 5 files changed, 59 insertions(+), 21 deletions(-) create mode 100644 lib/montrose/week.rb diff --git a/lib/montrose.rb b/lib/montrose.rb index 0f691da..390dcad 100644 --- a/lib/montrose.rb +++ b/lib/montrose.rb @@ -25,6 +25,7 @@ module Montrose autoload :Month, "montrose/month" autoload :MonthDay, "montrose/month_day" autoload :YearDay, "montrose/year_day" + autoload :Week, "montrose/week" extend Chainable diff --git a/lib/montrose/ical.rb b/lib/montrose/ical.rb index 1f871c3..3edf209 100644 --- a/lib/montrose/ical.rb +++ b/lib/montrose/ical.rb @@ -48,6 +48,8 @@ def parse_rrule(rrule) [:mday, Montrose::MonthDay.parse(value)] when "BYYEARDAY" [:yday, Montrose::YearDay.parse(value)] + when "BYWEEKNO" + [:week, Montrose::Week.parse(value)] when "WKST" [:week_start, value] else diff --git a/lib/montrose/options.rb b/lib/montrose/options.rb index 56ffc41..ac7e1f0 100644 --- a/lib/montrose/options.rb +++ b/lib/montrose/options.rb @@ -212,7 +212,7 @@ def yday=(ydays) end def week=(weeks) - @week = map_arg(weeks) { |w| assert_week(w) } + @week = Week.parse(weeks) end def month=(months) @@ -279,10 +279,6 @@ def assert_hour(hour) assert_range_includes(1..::Montrose::Utils::MAX_HOURS_IN_DAY, hour) end - def assert_week(week) - assert_range_includes(1..::Montrose::Utils::MAX_WEEKS_IN_YEAR, week, :absolute) - end - def decompose_on_arg(arg) case arg when Hash diff --git a/lib/montrose/week.rb b/lib/montrose/week.rb new file mode 100644 index 0000000..0d3802b --- /dev/null +++ b/lib/montrose/week.rb @@ -0,0 +1,20 @@ +module Montrose + class Week + class << self + NUMBERS = (-53.upto(-1) + 1.upto(53)).to_a + + def parse(arg) + return nil unless arg.present? + + Array(arg).map { |value| assert(value.to_i) } + end + + def assert(number) + test = number.abs + raise ConfigurationError, "Out of range: #{NUMBERS.inspect} does not include #{test}" unless NUMBERS.include?(number.abs) + + number + end + end + end +end diff --git a/spec/from_ical_rfc_spec.rb b/spec/from_ical_rfc_spec.rb index a88add9..4dc527d 100644 --- a/spec/from_ical_rfc_spec.rb +++ b/spec/from_ical_rfc_spec.rb @@ -519,26 +519,45 @@ def parse_expected_events(event_map) _(recurrence).must_pair_with expected_events end - # Every 20th Monday of the year, forever: - - # DTSTART;TZID=America/New_York:19970519T090000 - # RRULE:FREQ=YEARLY;BYDAY=20MO + it "every 20th Monday of the year, forever" do + ical = <<~ical + DTSTART;TZID=America/New_York:19970519T090000 + RRULE:FREQ=YEARLY;BYDAY=20MO + ical + # ==> (1997 9:00 AM EDT) May 19 + # (1998 9:00 AM EDT) May 18 + # (1999 9:00 AM EDT) May 17 + # ... + expected_events = parse_expected_events( + "1997 9:00 AM EDT" => {"May" => [19]}, + "1998 9:00 AM EDT" => {"May" => [18]}, + "1999 9:00 AM EDT" => {"May" => [17]}, + ) - # ==> (1997 9:00 AM EDT) May 19 - # (1998 9:00 AM EDT) May 18 - # (1999 9:00 AM EDT) May 17 - # ... + recurrence = Montrose::Recurrence.from_ical(ical) + _(recurrence).must_pair_with expected_events + end - # Monday of week number 20 (where the default start of the week is - # Monday), forever: + it "Monday of week number 20 (where the default start of the week is + Monday), forever" do + ical = <<~ical + DTSTART;TZID=America/New_York:19970512T090000 + RRULE:FREQ=YEARLY;BYWEEKNO=20;BYDAY=MO + ical + # ==> (1997 9:00 AM EDT) May 12 + # (1998 9:00 AM EDT) May 11 + # (1999 9:00 AM EDT) May 17 + # ... - # DTSTART;TZID=America/New_York:19970512T090000 - # RRULE:FREQ=YEARLY;BYWEEKNO=20;BYDAY=MO + expected_events = parse_expected_events( + "1997 9:00 AM EDT" => {"May" => [12]}, + "1998 9:00 AM EDT" => {"May" => [11]}, + "1999 9:00 AM EDT" => {"May" => [17]}, + ) - # ==> (1997 9:00 AM EDT) May 12 - # (1998 9:00 AM EDT) May 11 - # (1999 9:00 AM EDT) May 17 - # ... + recurrence = Montrose::Recurrence.from_ical(ical) + _(recurrence).must_pair_with expected_events + end # Every Thursday in March, forever: From 77ee6542f3891428e2e44e37e3b8719f5fe47733 Mon Sep 17 00:00:00 2001 From: Ross Kaffenberger Date: Sat, 6 Feb 2021 20:52:02 -0500 Subject: [PATCH 25/33] Add ical spec for exdate --- lib/montrose/ical.rb | 14 ++++- lib/montrose/year_day.rb | 2 +- spec/from_ical_rfc_spec.rb | 121 ++++++++++++++++++++++++------------- 3 files changed, 93 insertions(+), 44 deletions(-) diff --git a/lib/montrose/ical.rb b/lib/montrose/ical.rb index 3edf209..f3852c9 100644 --- a/lib/montrose/ical.rb +++ b/lib/montrose/ical.rb @@ -16,18 +16,30 @@ def initialize(ical) def parse dtstart, rrule = @ical.split("RRULE:") - Hash[*parse_dtstart(dtstart) + parse_rrule(rrule)] + dtstart, exdate = dtstart.split(/\s+/) + Hash[*parse_dtstart(dtstart) + parse_exdate(exdate) + parse_rrule(rrule)] end private def parse_dtstart(dtstart) + return [] unless dtstart.present? + _label, time_string = dtstart.split(";") @starts_at = Montrose::Utils.parse_time(time_string) [:starts, @starts_at] end + def parse_exdate(exdate) + return [] unless exdate.present? + + _label, date_string = exdate.split(";") + @except = Montrose::Utils.as_date(date_string) # only currently supports dates + + [:except, @except] + end + def parse_rrule(rrule) rrule.gsub(/\s+/, "").split(";").flat_map do |rule| prop, value = rule.split("=") diff --git a/lib/montrose/year_day.rb b/lib/montrose/year_day.rb index f84d780..ccace53 100644 --- a/lib/montrose/year_day.rb +++ b/lib/montrose/year_day.rb @@ -1,7 +1,7 @@ module Montrose class YearDay class << self - YDAYS = (1.upto(366)).to_a + YDAYS = 1.upto(366).to_a def parse(ydays) return nil unless ydays.present? diff --git a/spec/from_ical_rfc_spec.rb b/spec/from_ical_rfc_spec.rb index 4dc527d..51ba7b0 100644 --- a/spec/from_ical_rfc_spec.rb +++ b/spec/from_ical_rfc_spec.rb @@ -436,10 +436,10 @@ def parse_expected_events(event_map) end it "yearly in June and July for 10 occurrences" do - ical = <<~ical + ical = <<~ICAL DTSTART;TZID=America/New_York:19970610T090000 RRULE:FREQ=YEARLY;COUNT=10;BYMONTH=6,7 - ical + ICAL # ==> (1997 9:00 AM EDT) June 10;July 10 # (1998 9:00 AM EDT) June 10;July 10 # (1999 9:00 AM EDT) June 10;July 10 @@ -455,7 +455,7 @@ def parse_expected_events(event_map) "2000 9:00 AM EDT" => {"Jun" => [10], "Jul" => [10]}, "2001 9:00 AM EDT" => {"Jun" => [10], - "Jul" => [10]}, + "Jul" => [10]} ) recurrence = Montrose::Recurrence.from_ical(ical) @@ -464,10 +464,10 @@ def parse_expected_events(event_map) it "Every other year on January, February, and March for 10 occurrences" do - ical = <<~ical + ical = <<~ICAL DTSTART;TZID=America/New_York:19970310T090000 RRULE:FREQ=YEARLY;INTERVAL=2;COUNT=10;BYMONTH=1,2,3 - ical + ICAL # ==> (1997 9:00 AM EST) March 10 # (1999 9:00 AM EST) January 10;February 10;March 10 # (2001 9:00 AM EST) January 10;February 10;March 10 @@ -482,7 +482,7 @@ def parse_expected_events(event_map) "Mar" => [10]}, "2003 9:00 AM EST" => {"Jan" => [10], "Feb" => [10], - "Mar" => [10]}, + "Mar" => [10]} ) recurrence = Montrose::Recurrence.from_ical(ical) @@ -491,10 +491,10 @@ def parse_expected_events(event_map) it "every third year on the 1st, 100th, and 200th day for 10 occurrences" do - ical = <<~ical + ical = <<~ICAL DTSTART;TZID=America/New_York:19970101T090000 RRULE:FREQ=YEARLY;INTERVAL=3;COUNT=10;BYYEARDAY=1,100,200 - ical + ICAL # ==> (1997 9:00 AM EST) January 1 # (1997 9:00 AM EDT) April 10;July 19 # (2000 9:00 AM EST) January 1 @@ -512,7 +512,7 @@ def parse_expected_events(event_map) "2003 9:00 AM EST" => {"Jan" => [1]}, "2003 9:00 AM EDT" => {"Apr" => [10], "Jul" => [19]}, - "2006 9:00 AM EST" => {"Jan" => [1]}, + "2006 9:00 AM EST" => {"Jan" => [1]} ) recurrence = Montrose::Recurrence.from_ical(ical) @@ -520,10 +520,10 @@ def parse_expected_events(event_map) end it "every 20th Monday of the year, forever" do - ical = <<~ical + ical = <<~ICAL DTSTART;TZID=America/New_York:19970519T090000 RRULE:FREQ=YEARLY;BYDAY=20MO - ical + ICAL # ==> (1997 9:00 AM EDT) May 19 # (1998 9:00 AM EDT) May 18 # (1999 9:00 AM EDT) May 17 @@ -531,7 +531,7 @@ def parse_expected_events(event_map) expected_events = parse_expected_events( "1997 9:00 AM EDT" => {"May" => [19]}, "1998 9:00 AM EDT" => {"May" => [18]}, - "1999 9:00 AM EDT" => {"May" => [17]}, + "1999 9:00 AM EDT" => {"May" => [17]} ) recurrence = Montrose::Recurrence.from_ical(ical) @@ -540,10 +540,10 @@ def parse_expected_events(event_map) it "Monday of week number 20 (where the default start of the week is Monday), forever" do - ical = <<~ical + ical = <<~ICAL DTSTART;TZID=America/New_York:19970512T090000 RRULE:FREQ=YEARLY;BYWEEKNO=20;BYDAY=MO - ical + ICAL # ==> (1997 9:00 AM EDT) May 12 # (1998 9:00 AM EDT) May 11 # (1999 9:00 AM EDT) May 17 @@ -552,46 +552,83 @@ def parse_expected_events(event_map) expected_events = parse_expected_events( "1997 9:00 AM EDT" => {"May" => [12]}, "1998 9:00 AM EDT" => {"May" => [11]}, - "1999 9:00 AM EDT" => {"May" => [17]}, + "1999 9:00 AM EDT" => {"May" => [17]} ) recurrence = Montrose::Recurrence.from_ical(ical) _(recurrence).must_pair_with expected_events end - # Every Thursday in March, forever: - - # DTSTART;TZID=America/New_York:19970313T090000 - # RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=TH - - # ==> (1997 9:00 AM EST) March 13,20,27 - # (1998 9:00 AM EST) March 5,12,19,26 - # (1999 9:00 AM EST) March 4,11,18,25 - # ... + it "every Thursday in March, forever" do + ical = <<~ICAL + DTSTART;TZID=America/New_York:19970313T090000 + RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=TH + ICAL + # ==> (1997 9:00 AM EST) March 13,20,27 + # (1998 9:00 AM EST) March 5,12,19,26 + # (1999 9:00 AM EST) March 4,11,18,25 + # ... + expected_events = parse_expected_events( + "1997 9:00 AM EST" => {"Mar" => [13, 20, 27]}, + "1998 9:00 AM EST" => {"Mar" => [5, 12, 19, 26]}, + "1999 9:00 AM EST" => {"Mar" => [4, 11, 18, 25]} + ) - # Every Thursday, but only during June, July, and August, forever: + recurrence = Montrose::Recurrence.from_ical(ical) + _(recurrence).must_pair_with expected_events + end - # DTSTART;TZID=America/New_York:19970605T090000 - # RRULE:FREQ=YEARLY;BYDAY=TH;BYMONTH=6,7,8 + it "every Thursday, but only during June, July, and August, forever" do + ical = <<~ICAL + DTSTART;TZID=America/New_York:19970605T090000 + RRULE:FREQ=YEARLY;BYDAY=TH;BYMONTH=6,7,8 + ICAL + # ==> (1997 9:00 AM EDT) June 5,12,19,26;July 3,10,17,24,31; + # August 7,14,21,28 + # (1998 9:00 AM EDT) June 4,11,18,25;July 2,9,16,23,30; + # August 6,13,20,27 + # (1999 9:00 AM EDT) June 3,10,17,24;July 1,8,15,22,29; + # August 5,12,19,26 + # ... + expected_events = parse_expected_events( + "1997 9:00 AM EDT" => {"Jun" => [5, 12, 19, 26], + "Jul" => [3, 10, 17, 24, 31], + "Aug" => [7, 14, 21, 28]}, + "1998 9:00 AM EDT" => {"Jun" => [4, 11, 18, 25], + "Jul" => [2, 9, 16, 23, 30], + "Aug" => [6, 13, 20, 27]}, + "1999 9:00 AM EDT" => {"Jun" => [3, 10, 17, 24], + "Jul" => [1, 8, 15, 22, 29], + "Aug" => [5, 12, 19, 26]} + ) - # ==> (1997 9:00 AM EDT) June 5,12,19,26;July 3,10,17,24,31; - # August 7,14,21,28 - # (1998 9:00 AM EDT) June 4,11,18,25;July 2,9,16,23,30; - # August 6,13,20,27 - # (1999 9:00 AM EDT) June 3,10,17,24;July 1,8,15,22,29; - # August 5,12,19,26 - # ... + recurrence = Montrose::Recurrence.from_ical(ical) + _(recurrence).must_pair_with expected_events + end - # Every Friday the 13th, forever: + it "every Friday the 13th, forever" do + ical = <<~ICAL + DTSTART;TZID=America/New_York:19970902T090000 + EXDATE;TZID=America/New_York:19970902T090000 + RRULE:FREQ=MONTHLY;BYDAY=FR;BYMONTHDAY=13 + ICAL + # ==> (1998 9:00 AM EST) February 13;March 13;November 13 + # (1999 9:00 AM EDT) August 13 + # (2000 9:00 AM EDT) October 13 + # ... - # DTSTART;TZID=America/New_York:19970902T090000 - # EXDATE;TZID=America/New_York:19970902T090000 - # RRULE:FREQ=MONTHLY;BYDAY=FR;BYMONTHDAY=13 + expected_events = parse_expected_events( + "1998 9:00 AM EST" => {"Feb" => [13], + "Mar" => [13], + "Nov" => [13]}, + "1999 9:00 AM EDT" => {"Aug" => [13]}, + "2000 9:00 AM EDT" => {"Oct" => [13]}, + ) - # ==> (1998 9:00 AM EST) February 13;March 13;November 13 - # (1999 9:00 AM EDT) August 13 - # (2000 9:00 AM EDT) October 13 - # ... + recurrence = Montrose::Recurrence.from_ical(ical) + _(recurrence).must_pair_with expected_events + _(recurrence.default_options[:except]).must_equal([Date.parse('19970902')]) + end # The first Saturday that follows the first Sunday of the month, # forever: From 0995291e29eb486d39dfa35f7c9ba41ce7a22dde Mon Sep 17 00:00:00 2001 From: Ross Kaffenberger Date: Sat, 6 Feb 2021 21:16:01 -0500 Subject: [PATCH 26/33] Add ical specs --- lib/montrose/ical.rb | 4 +- spec/from_ical_rfc_spec.rb | 113 ++++++++++++++++++++++++++----------- 2 files changed, 82 insertions(+), 35 deletions(-) diff --git a/lib/montrose/ical.rb b/lib/montrose/ical.rb index f3852c9..a998b39 100644 --- a/lib/montrose/ical.rb +++ b/lib/montrose/ical.rb @@ -16,7 +16,7 @@ def initialize(ical) def parse dtstart, rrule = @ical.split("RRULE:") - dtstart, exdate = dtstart.split(/\s+/) + dtstart, exdate = dtstart.split("\n") Hash[*parse_dtstart(dtstart) + parse_exdate(exdate) + parse_rrule(rrule)] end @@ -64,6 +64,8 @@ def parse_rrule(rrule) [:week, Montrose::Week.parse(value)] when "WKST" [:week_start, value] + when "BYSETPOS" + warn "BYSETPOS not currently supported!" else raise "Unrecognized rrule '#{rule}'" end diff --git a/spec/from_ical_rfc_spec.rb b/spec/from_ical_rfc_spec.rb index 51ba7b0..d56a5f5 100644 --- a/spec/from_ical_rfc_spec.rb +++ b/spec/from_ical_rfc_spec.rb @@ -622,46 +622,82 @@ def parse_expected_events(event_map) "Mar" => [13], "Nov" => [13]}, "1999 9:00 AM EDT" => {"Aug" => [13]}, - "2000 9:00 AM EDT" => {"Oct" => [13]}, + "2000 9:00 AM EDT" => {"Oct" => [13]} ) recurrence = Montrose::Recurrence.from_ical(ical) _(recurrence).must_pair_with expected_events - _(recurrence.default_options[:except]).must_equal([Date.parse('19970902')]) + _(recurrence.default_options[:except]).must_equal([Date.parse("19970902")]) end - # The first Saturday that follows the first Sunday of the month, - # forever: - - # DTSTART;TZID=America/New_York:19970913T090000 - # RRULE:FREQ=MONTHLY;BYDAY=SA;BYMONTHDAY=7,8,9,10,11,12,13 - - # ==> (1997 9:00 AM EDT) September 13;October 11 - # (1997 9:00 AM EST) November 8;December 13 - # (1998 9:00 AM EST) January 10;February 7;March 7 - # (1998 9:00 AM EDT) April 11;May 9;June 13... - # ... - - # Every 4 years, the first Tuesday after a Monday in November, - # forever (U.S. Presidential Election day): + it "The first Saturday that follows the first Sunday of the month, + forever" do + ical = <<~ICAL + DTSTART;TZID=America/New_York:19970913T090000 + RRULE:FREQ=MONTHLY;BYDAY=SA;BYMONTHDAY=7,8,9,10,11,12,13 + ICAL + # ==> (1997 9:00 AM EDT) September 13;October 11 + # (1997 9:00 AM EST) November 8;December 13 + # (1998 9:00 AM EST) January 10;February 7;March 7 + # (1998 9:00 AM EDT) April 11;May 9;June 13... + # ... - # DTSTART;TZID=America/New_York:19961105T090000 - # RRULE:FREQ=YEARLY;INTERVAL=4;BYMONTH=11;BYDAY=TU; - # BYMONTHDAY=2,3,4,5,6,7,8 + expected_events = parse_expected_events( + "1997 9:00 AM EDT" => {"Sep" => [13], + "Oct" => [11]}, + "1997 9:00 AM EST" => {"Nov" => [8], + "Dec" => [13]}, + "1998 9:00 AM EST" => {"Jan" => [10], + "Feb" => [7], + "Mar" => [7]}, + "1998 9:00 AM EDT" => {"Apr" => [11], + "May" => [9], + "Jun" => [13]} + ) - # ==> (1996 9:00 AM EST) November 5 - # (2000 9:00 AM EST) November 7 - # (2004 9:00 AM EST) November 2 - # ... + recurrence = Montrose::Recurrence.from_ical(ical) + _(recurrence).must_pair_with expected_events + end - # The third instance into the month of one of Tuesday, Wednesday, or - # Thursday, for the next 3 months: + it "every 4 years, the first Tuesday after a Monday in November, + forever (U.S. Presidential Election day)" do + ical = <<~ICAL + DTSTART;TZID=America/New_York:19961105T090000 + RRULE:FREQ=YEARLY;INTERVAL=4;BYMONTH=11;BYDAY=TU; + BYMONTHDAY=2,3,4,5,6,7,8 + ICAL + # ==> (1996 9:00 AM EST) November 5 + # (2000 9:00 AM EST) November 7 + # (2004 9:00 AM EST) November 2 + # ... + expected_events = parse_expected_events( + "1996 9:00 AM EST" => {"Nov" => [5]}, + "2000 9:00 AM EST" => {"Nov" => [7]}, + "2004 9:00 AM EST" => {"Nov" => [2]} + ) - # DTSTART;TZID=America/New_York:19970904T090000 - # RRULE:FREQ=MONTHLY;COUNT=3;BYDAY=TU,WE,TH;BYSETPOS=3 + recurrence = Montrose::Recurrence.from_ical(ical) + _(recurrence).must_pair_with expected_events + end - # ==> (1997 9:00 AM EDT) September 4;October 7 - # (1997 9:00 AM EST) November 6 + # TODO support BYSETPOS + # it "the third instance into the month of one of Tuesday, Wednesday, or + # Thursday, for the next 3 months" do + # ical = <<~ical + # DTSTART;TZID=America/New_York:19970904T090000 + # RRULE:FREQ=MONTHLY;COUNT=3;BYDAY=TU,WE,TH;BYSETPOS=3 + # ical + # # ==> (1997 9:00 AM EDT) September 4;October 7 + # # (1997 9:00 AM EST) November 6 + # expected_events = parse_expected_events( + # "1997 9:00 AM EDT" => {"Sep" => [4], + # "Oct" => [7]}, + # "1997 9:00 AM EST" => {"Nov" => [6]}, + # ) + + # recurrence = Montrose::Recurrence.from_ical(ical) + # _(recurrence).must_pair_with expected_event + # end # The second-to-last weekday of the month: @@ -673,12 +709,21 @@ def parse_expected_events(event_map) # (1998 9:00 AM EST) January 29;February 26;March 30 # ... - # Every 3 hours from 9:00 AM to 5:00 PM on a specific day: - - # DTSTART;TZID=America/New_York:19970902T090000 - # RRULE:FREQ=HOURLY;INTERVAL=3;UNTIL=19970902T170000Z + it "every 3 hours from 9:00 AM to 5:00 PM on a specific day" do + ical = <<~ICAL + DTSTART;TZID=America/New_York:19970902T090000 + RRULE:FREQ=HOURLY;INTERVAL=3;UNTIL=19970902T170000 + ICAL + # ==> (September 2, 1997 EDT) 09:00,12:00,15:00 + expected_events = parse_expected_events( + "1997 09:00 AM EDT" => {"Sept" => [2]}, + "1997 12:00 PM EDT" => {"Sept" => [2]}, + "1997 15:00 PM EDT" => {"Sept" => [2]} + ) - # ==> (September 2, 1997 EDT) 09:00,12:00,15:00 + recurrence = Montrose::Recurrence.from_ical(ical) + _(recurrence).must_pair_with expected_events + end # Every 15 minutes for 6 occurrences: From c104d467d139a8eb57cfb89ac95c5fe83a07ffd5 Mon Sep 17 00:00:00 2001 From: Ross Kaffenberger Date: Sun, 7 Feb 2021 05:03:15 -0500 Subject: [PATCH 27/33] Add ical spec --- spec/from_ical_rfc_spec.rb | 48 ++++++++++++++++++++++++++------------ 1 file changed, 33 insertions(+), 15 deletions(-) diff --git a/spec/from_ical_rfc_spec.rb b/spec/from_ical_rfc_spec.rb index d56a5f5..16acc05 100644 --- a/spec/from_ical_rfc_spec.rb +++ b/spec/from_ical_rfc_spec.rb @@ -10,6 +10,13 @@ def parse_expected_events(event_map) } end + def parse_expect_by_time_of_day(event_map) + event_map.flat_map { |date, times| + times.map { |time| Time.parse "#{date} #{time} EDT" } + } + end + + let(:starts_on) { Time.parse("Sep 2 09:00:00 EDT 1997") } it "daily for 10 occurrences" do @@ -622,20 +629,20 @@ def parse_expected_events(event_map) "Mar" => [13], "Nov" => [13]}, "1999 9:00 AM EDT" => {"Aug" => [13]}, - "2000 9:00 AM EDT" => {"Oct" => [13]} + "2000 9:00 AM EDT" => {"Oct" => [13]}, ) recurrence = Montrose::Recurrence.from_ical(ical) _(recurrence).must_pair_with expected_events - _(recurrence.default_options[:except]).must_equal([Date.parse("19970902")]) + _(recurrence.default_options[:except]).must_equal([Date.parse('19970902')]) end it "The first Saturday that follows the first Sunday of the month, forever" do - ical = <<~ICAL + ical = <<~ical DTSTART;TZID=America/New_York:19970913T090000 RRULE:FREQ=MONTHLY;BYDAY=SA;BYMONTHDAY=7,8,9,10,11,12,13 - ICAL + ical # ==> (1997 9:00 AM EDT) September 13;October 11 # (1997 9:00 AM EST) November 8;December 13 # (1998 9:00 AM EST) January 10;February 7;March 7 @@ -652,7 +659,7 @@ def parse_expected_events(event_map) "Mar" => [7]}, "1998 9:00 AM EDT" => {"Apr" => [11], "May" => [9], - "Jun" => [13]} + "Jun" => [13]}, ) recurrence = Montrose::Recurrence.from_ical(ical) @@ -661,11 +668,11 @@ def parse_expected_events(event_map) it "every 4 years, the first Tuesday after a Monday in November, forever (U.S. Presidential Election day)" do - ical = <<~ICAL + ical = <<~ical DTSTART;TZID=America/New_York:19961105T090000 RRULE:FREQ=YEARLY;INTERVAL=4;BYMONTH=11;BYDAY=TU; BYMONTHDAY=2,3,4,5,6,7,8 - ICAL + ical # ==> (1996 9:00 AM EST) November 5 # (2000 9:00 AM EST) November 7 # (2004 9:00 AM EST) November 2 @@ -673,7 +680,7 @@ def parse_expected_events(event_map) expected_events = parse_expected_events( "1996 9:00 AM EST" => {"Nov" => [5]}, "2000 9:00 AM EST" => {"Nov" => [7]}, - "2004 9:00 AM EST" => {"Nov" => [2]} + "2004 9:00 AM EST" => {"Nov" => [2]}, ) recurrence = Montrose::Recurrence.from_ical(ical) @@ -710,27 +717,38 @@ def parse_expected_events(event_map) # ... it "every 3 hours from 9:00 AM to 5:00 PM on a specific day" do - ical = <<~ICAL + ical = <<~ical DTSTART;TZID=America/New_York:19970902T090000 RRULE:FREQ=HOURLY;INTERVAL=3;UNTIL=19970902T170000 - ICAL + ical # ==> (September 2, 1997 EDT) 09:00,12:00,15:00 expected_events = parse_expected_events( "1997 09:00 AM EDT" => {"Sept" => [2]}, "1997 12:00 PM EDT" => {"Sept" => [2]}, - "1997 15:00 PM EDT" => {"Sept" => [2]} + "1997 15:00 PM EDT" => {"Sept" => [2]}, + ) + expected_events = parse_expect_by_time_of_day( + "Sept 2 1997" => %w[09:00 12:00 15:00] ) recurrence = Montrose::Recurrence.from_ical(ical) _(recurrence).must_pair_with expected_events end - # Every 15 minutes for 6 occurrences: + it "every 15 minutes for 6 occurrences" do + ical = <<~ical + DTSTART;TZID=America/New_York:19970902T090000 + RRULE:FREQ=MINUTELY;INTERVAL=15;COUNT=6 + ical - # DTSTART;TZID=America/New_York:19970902T090000 - # RRULE:FREQ=MINUTELY;INTERVAL=15;COUNT=6 + # ==> (September 2, 1997 EDT) 09:00,09:15,09:30,09:45,10:00,10:15 + expected_events = parse_expect_by_time_of_day( + "Sept 2 1997" => %w[09:00 09:15 09:30 09:45 10:00 10:15] + ) - # ==> (September 2, 1997 EDT) 09:00,09:15,09:30,09:45,10:00,10:15 + recurrence = Montrose::Recurrence.from_ical(ical) + _(recurrence).must_pair_with expected_events + end # Every hour and a half for 4 occurrences: From 256aa3d5ac23061a9da680a17bab4e624b9161fe Mon Sep 17 00:00:00 2001 From: Ross Kaffenberger Date: Sun, 7 Feb 2021 06:04:40 -0500 Subject: [PATCH 28/33] Support BYHOUR and BYMINUTE ical rrules Adds Hour and Minute parsing Adds support for :minute option to iterate by "MinuteofHour" rule --- lib/montrose.rb | 2 + lib/montrose/frequency.rb | 70 +++++++++++++++------- lib/montrose/hour.rb | 22 +++++++ lib/montrose/ical.rb | 4 ++ lib/montrose/minute.rb | 22 +++++++ lib/montrose/options.rb | 5 ++ lib/montrose/rule.rb | 1 + lib/montrose/rule/minute_of_hour.rb | 25 ++++++++ lib/montrose/stack.rb | 1 + spec/from_ical_rfc_spec.rb | 91 +++++++++++++++++++---------- 10 files changed, 191 insertions(+), 52 deletions(-) create mode 100644 lib/montrose/hour.rb create mode 100644 lib/montrose/minute.rb create mode 100644 lib/montrose/rule/minute_of_hour.rb diff --git a/lib/montrose.rb b/lib/montrose.rb index 390dcad..0c1c741 100644 --- a/lib/montrose.rb +++ b/lib/montrose.rb @@ -21,6 +21,8 @@ require "montrose/version" module Montrose + autoload :Minute, "montrose/minute" + autoload :Hour, "montrose/hour" autoload :Day, "montrose/day" autoload :Month, "montrose/month" autoload :MonthDay, "montrose/month_day" diff --git a/lib/montrose/frequency.rb b/lib/montrose/frequency.rb index 4e120fb..6444cb3 100644 --- a/lib/montrose/frequency.rb +++ b/lib/montrose/frequency.rb @@ -25,30 +25,60 @@ class Frequency attr_reader :time, :starts - # Factory method for instantiating the appropriate Frequency - # subclass. - # - def self.from_options(opts) - frequency = opts.fetch(:every) { fail ConfigurationError, "Please specify the :every option" } - class_name = FREQUENCY_TERMS.fetch(frequency.to_s) { - fail "Don't know how to enumerate every: #{frequency}" - } - - Montrose::Frequency.const_get(class_name).new(opts) - end + class << self + def parse(input) + if input.respond_to?(:parts) + frequency, interval = duration_to_frequency_parts(input) + {every: frequency.to_s.singularize.to_sym, interval: interval} + elsif input.is_a?(Numeric) + frequency, interval = numeric_to_frequency_parts(input) + {every: frequency, interval: interval} + else + {every: Frequency.assert(input)} + end + end + + # Factory method for instantiating the appropriate Frequency + # subclass. + # + def from_options(opts) + frequency = opts.fetch(:every) { fail ConfigurationError, "Please specify the :every option" } + class_name = FREQUENCY_TERMS.fetch(frequency.to_s) { + fail "Don't know how to enumerate every: #{frequency}" + } - def self.from_term(term) - FREQUENCY_TERMS.invert.map { |k, v| [k.downcase, v] }.to_h.fetch(term.downcase) do - fail "Don't know how to convert #{term} to a Montrose frequency" + Montrose::Frequency.const_get(class_name).new(opts) + end + + def from_term(term) + FREQUENCY_TERMS.invert.map { |k, v| [k.downcase, v] }.to_h.fetch(term.downcase) do + fail "Don't know how to convert #{term} to a Montrose frequency" + end end - end - # @private - def self.assert(frequency) - FREQUENCY_TERMS.key?(frequency.to_s) || fail(ConfigurationError, - "Don't know how to enumerate every: #{frequency}") + # @private + def assert(frequency) + FREQUENCY_TERMS.key?(frequency.to_s) || fail(ConfigurationError, + "Don't know how to enumerate every: #{frequency}") - frequency.to_sym + frequency.to_sym + end + + # @private + def numeric_to_frequency_parts(number) + parts = nil + %i[year month week day hour minute].each do |freq| + div, mod = number.divmod(1.send(freq)) + parts = [freq, div] + return parts if mod.zero? + end + parts + end + + # @private + def duration_to_frequency_parts(duration) + duration.parts.first + end end def initialize(opts = {}) diff --git a/lib/montrose/hour.rb b/lib/montrose/hour.rb new file mode 100644 index 0000000..f05244a --- /dev/null +++ b/lib/montrose/hour.rb @@ -0,0 +1,22 @@ +module Montrose + class Hour + HOURS_IN_DAY = 1.upto(24).to_a.freeze + + class << self + def parse(arg) + case arg + when String + parse(arg.split(",")) + else + Array(arg).map { |h| assert(h.to_i) }.presence + end + end + + def assert(hour) + raise ConfigurationError, "Out of range: #{HOURS_IN_DAY.inspect} does not include #{hour}" unless HOURS_IN_DAY.include?(hour) + + hour + end + end + end +end diff --git a/lib/montrose/ical.rb b/lib/montrose/ical.rb index a998b39..ce9de8d 100644 --- a/lib/montrose/ical.rb +++ b/lib/montrose/ical.rb @@ -52,6 +52,10 @@ def parse_rrule(rrule) [:total, value.to_i] when "UNTIL" [:until, Montrose::Utils.parse_time(value)] + when "BYMINUTE" + [:minute, Montrose::Minute.parse(value)] + when "BYHOUR" + [:hour, Montrose::Hour.parse(value)] when "BYMONTH" [:month, Montrose::Month.parse(value)] when "BYDAY" diff --git a/lib/montrose/minute.rb b/lib/montrose/minute.rb new file mode 100644 index 0000000..76436fd --- /dev/null +++ b/lib/montrose/minute.rb @@ -0,0 +1,22 @@ +module Montrose + class Minute + MINUTES_IN_HOUR = 0.upto(59).to_a.freeze + + class << self + def parse(arg) + case arg + when String + parse(arg.split(",")) + else + Array(arg).map { |m| assert(m.to_i) }.presence + end + end + + def assert(minute) + raise ConfigurationError, "Out of range: #{MINUTES_IN_HOUR.inspect} does not include #{minute}" unless MINUTES_IN_HOUR.include?(minute) + + minute + end + end + end +end diff --git a/lib/montrose/options.rb b/lib/montrose/options.rb index ac7e1f0..7d35b37 100644 --- a/lib/montrose/options.rb +++ b/lib/montrose/options.rb @@ -90,6 +90,7 @@ def default_options def_option :between def_option :covering def_option :during + def_option :minute def_option :hour def_option :day def_option :mday @@ -181,6 +182,10 @@ def until=(time) @until = normalize_time(as_time(time)) || default_until end + def minute=(minutes) + @minute = Minute.parse(minutes) + end + def hour=(hours) @hour = map_arg(hours) { |h| assert_hour(h) } end diff --git a/lib/montrose/rule.rb b/lib/montrose/rule.rb index 3243d2f..d602b08 100644 --- a/lib/montrose/rule.rb +++ b/lib/montrose/rule.rb @@ -42,6 +42,7 @@ def from_options(opts) require "montrose/rule/day_of_year" require "montrose/rule/during" require "montrose/rule/except" +require "montrose/rule/minute_of_hour" require "montrose/rule/hour_of_day" require "montrose/rule/month_of_year" require "montrose/rule/nth_day_of_month" diff --git a/lib/montrose/rule/minute_of_hour.rb b/lib/montrose/rule/minute_of_hour.rb new file mode 100644 index 0000000..4c44faf --- /dev/null +++ b/lib/montrose/rule/minute_of_hour.rb @@ -0,0 +1,25 @@ +# frozen_string_literal: true + +module Montrose + module Rule + class MinuteOfHour + include Montrose::Rule + + def self.apply_options(opts) + opts[:minute] + end + + # Initializes rule + # + # @param minutes [Array] valid minutes of hour, e.g. [0, 20, 59] + # + def initialize(minutes) + @minutes = minutes + end + + def include?(time) + @minutes.include?(time.min) + end + end + end +end diff --git a/lib/montrose/stack.rb b/lib/montrose/stack.rb index a4786d3..ca46f05 100644 --- a/lib/montrose/stack.rb +++ b/lib/montrose/stack.rb @@ -18,6 +18,7 @@ def self.build(opts = {}) Rule::Except, Rule::Total, Rule::TimeOfDay, + Rule::MinuteOfHour, Rule::HourOfDay, Rule::NthDayOfMonth, Rule::NthDayOfYear, diff --git a/spec/from_ical_rfc_spec.rb b/spec/from_ical_rfc_spec.rb index 16acc05..97d9003 100644 --- a/spec/from_ical_rfc_spec.rb +++ b/spec/from_ical_rfc_spec.rb @@ -16,7 +16,6 @@ def parse_expect_by_time_of_day(event_map) } end - let(:starts_on) { Time.parse("Sep 2 09:00:00 EDT 1997") } it "daily for 10 occurrences" do @@ -629,20 +628,20 @@ def parse_expect_by_time_of_day(event_map) "Mar" => [13], "Nov" => [13]}, "1999 9:00 AM EDT" => {"Aug" => [13]}, - "2000 9:00 AM EDT" => {"Oct" => [13]}, + "2000 9:00 AM EDT" => {"Oct" => [13]} ) recurrence = Montrose::Recurrence.from_ical(ical) _(recurrence).must_pair_with expected_events - _(recurrence.default_options[:except]).must_equal([Date.parse('19970902')]) + _(recurrence.default_options[:except]).must_equal([Date.parse("19970902")]) end it "The first Saturday that follows the first Sunday of the month, forever" do - ical = <<~ical + ical = <<~ICAL DTSTART;TZID=America/New_York:19970913T090000 RRULE:FREQ=MONTHLY;BYDAY=SA;BYMONTHDAY=7,8,9,10,11,12,13 - ical + ICAL # ==> (1997 9:00 AM EDT) September 13;October 11 # (1997 9:00 AM EST) November 8;December 13 # (1998 9:00 AM EST) January 10;February 7;March 7 @@ -659,7 +658,7 @@ def parse_expect_by_time_of_day(event_map) "Mar" => [7]}, "1998 9:00 AM EDT" => {"Apr" => [11], "May" => [9], - "Jun" => [13]}, + "Jun" => [13]} ) recurrence = Montrose::Recurrence.from_ical(ical) @@ -668,11 +667,11 @@ def parse_expect_by_time_of_day(event_map) it "every 4 years, the first Tuesday after a Monday in November, forever (U.S. Presidential Election day)" do - ical = <<~ical + ical = <<~ICAL DTSTART;TZID=America/New_York:19961105T090000 RRULE:FREQ=YEARLY;INTERVAL=4;BYMONTH=11;BYDAY=TU; BYMONTHDAY=2,3,4,5,6,7,8 - ical + ICAL # ==> (1996 9:00 AM EST) November 5 # (2000 9:00 AM EST) November 7 # (2004 9:00 AM EST) November 2 @@ -680,7 +679,7 @@ def parse_expect_by_time_of_day(event_map) expected_events = parse_expected_events( "1996 9:00 AM EST" => {"Nov" => [5]}, "2000 9:00 AM EST" => {"Nov" => [7]}, - "2004 9:00 AM EST" => {"Nov" => [2]}, + "2004 9:00 AM EST" => {"Nov" => [2]} ) recurrence = Montrose::Recurrence.from_ical(ical) @@ -717,16 +716,11 @@ def parse_expect_by_time_of_day(event_map) # ... it "every 3 hours from 9:00 AM to 5:00 PM on a specific day" do - ical = <<~ical + ical = <<~ICAL DTSTART;TZID=America/New_York:19970902T090000 RRULE:FREQ=HOURLY;INTERVAL=3;UNTIL=19970902T170000 - ical + ICAL # ==> (September 2, 1997 EDT) 09:00,12:00,15:00 - expected_events = parse_expected_events( - "1997 09:00 AM EDT" => {"Sept" => [2]}, - "1997 12:00 PM EDT" => {"Sept" => [2]}, - "1997 15:00 PM EDT" => {"Sept" => [2]}, - ) expected_events = parse_expect_by_time_of_day( "Sept 2 1997" => %w[09:00 12:00 15:00] ) @@ -736,10 +730,10 @@ def parse_expect_by_time_of_day(event_map) end it "every 15 minutes for 6 occurrences" do - ical = <<~ical + ical = <<~ICAL DTSTART;TZID=America/New_York:19970902T090000 RRULE:FREQ=MINUTELY;INTERVAL=15;COUNT=6 - ical + ICAL # ==> (September 2, 1997 EDT) 09:00,09:15,09:30,09:45,10:00,10:15 expected_events = parse_expect_by_time_of_day( @@ -750,25 +744,58 @@ def parse_expect_by_time_of_day(event_map) _(recurrence).must_pair_with expected_events end - # Every hour and a half for 4 occurrences: + it "every hour and a half for 4 occurrences" do + ical = <<~ICAL + DTSTART;TZID=America/New_York:19970902T090000 + RRULE:FREQ=MINUTELY;INTERVAL=90;COUNT=4 + ICAL - # DTSTART;TZID=America/New_York:19970902T090000 - # RRULE:FREQ=MINUTELY;INTERVAL=90;COUNT=4 + # ==> (September 2, 1997 EDT) 09:00,10:30;12:00;13:30 + expected_events = parse_expect_by_time_of_day( + "Sept 2 1997" => %w[09:00 10:30 12:00 13:30] + ) - # ==> (September 2, 1997 EDT) 09:00,10:30;12:00;13:30 + recurrence = Montrose::Recurrence.from_ical(ical) + _(recurrence).must_pair_with expected_events + end - # Every 20 minutes from 9:00 AM to 4:40 PM every day: + it "every 20 minutes from 9:00 AM to 4:40 PM every day - daily" do + ical = <<~ICAL + DTSTART;TZID=America/New_York:19970902T090000 + RRULE:FREQ=DAILY;BYHOUR=9,10,11,12,13,14,15,16;BYMINUTE=0,20,40 + ICAL + # ==> (September 2, 1997 EDT) 9:00,9:20,9:40,10:00,10:20, + # ... 16:00,16:20,16:40 + # (September 3, 1997 EDT) 9:00,9:20,9:40,10:00,10:20, + # ...16:00,16:20,16:40 + # ... + expected_events = parse_expect_by_time_of_day( + "Sept 2 1997" => %w[9:00 9:20 9:40 10:00 10:20 10:40 11:00 11:20 11:40 12:00 12:20 12:40 13:00 13:20 13:40 14:00 14:20 14:40 15:00 15:20 15:40 16:00 16:20 16:40], + "Sept 3 1997" => %w[9:00 9:20 9:40 10:00 10:20 10:40 11:00 11:20 11:40 12:00 12:20 12:40 13:00 13:20 13:40 14:00 14:20 14:40 15:00 15:20 15:40 16:00 16:20 16:40] + ) - # DTSTART;TZID=America/New_York:19970902T090000 - # RRULE:FREQ=DAILY;BYHOUR=9,10,11,12,13,14,15,16;BYMINUTE=0,20,40 - # or - # RRULE:FREQ=MINUTELY;INTERVAL=20;BYHOUR=9,10,11,12,13,14,15,16 + recurrence = Montrose::Recurrence.from_ical(ical) + _(recurrence).must_pair_with expected_events + end - # ==> (September 2, 1997 EDT) 9:00,9:20,9:40,10:00,10:20, - # ... 16:00,16:20,16:40 - # (September 3, 1997 EDT) 9:00,9:20,9:40,10:00,10:20, - # ...16:00,16:20,16:40 - # ... + it "every 20 minutes from 9:00 AM to 4:40 PM every day - minutely" do + ical = <<~ICAL + DTSTART;TZID=America/New_York:19970902T090000 + RRULE:FREQ=MINUTELY;INTERVAL=20;BYHOUR=9,10,11,12,13,14,15,16 + ICAL + # ==> (September 2, 1997 EDT) 9:00,9:20,9:40,10:00,10:20, + # ... 16:00,16:20,16:40 + # (September 3, 1997 EDT) 9:00,9:20,9:40,10:00,10:20, + # ...16:00,16:20,16:40 + # ... + expected_events = parse_expect_by_time_of_day( + "Sept 2 1997" => %w[9:00 9:20 9:40 10:00 10:20 10:40 11:00 11:20 11:40 12:00 12:20 12:40 13:00 13:20 13:40 14:00 14:20 14:40 15:00 15:20 15:40 16:00 16:20 16:40], + "Sept 3 1997" => %w[9:00 9:20 9:40 10:00 10:20 10:40 11:00 11:20 11:40 12:00 12:20 12:40 13:00 13:20 13:40 14:00 14:20 14:40 15:00 15:20 15:40 16:00 16:20 16:40] + ) + + recurrence = Montrose::Recurrence.from_ical(ical) + _(recurrence).must_pair_with expected_events + end # An example where the days generated makes a difference because of # WKST: From 96ebd0f29ba3afb4f702cfcbc491b5c3461ad91f Mon Sep 17 00:00:00 2001 From: Ross Kaffenberger Date: Sun, 7 Feb 2021 06:09:40 -0500 Subject: [PATCH 29/33] Add ical spec --- spec/from_ical_rfc_spec.rb | 23 ++++++++++++++++------- 1 file changed, 16 insertions(+), 7 deletions(-) diff --git a/spec/from_ical_rfc_spec.rb b/spec/from_ical_rfc_spec.rb index 97d9003..14d1aeb 100644 --- a/spec/from_ical_rfc_spec.rb +++ b/spec/from_ical_rfc_spec.rb @@ -812,12 +812,21 @@ def parse_expect_by_time_of_day(event_map) # ==> (1997 EDT) August 5,17,19,31 - # An example where an invalid date (i.e., February 30) is ignored. - - # DTSTART;TZID=America/New_York:20070115T090000 - # RRULE:FREQ=MONTHLY;BYMONTHDAY=15,30;COUNT=5 + it "an example where an invalid date (i.e., February 30) is ignored" do + ical = <<~ICAL + DTSTART;TZID=America/New_York:20070115T090000 + RRULE:FREQ=MONTHLY;BYMONTHDAY=15,30;COUNT=5 + ICAL + # ==> (2007 EST) January 15,30 + # (2007 EST) February 15 + # (2007 EDT) March 15,30 + expected_events = parse_expected_events( + "2007 9:00 AM EST" => {"Jan" => [15, 30], + "Feb" => [15]}, + "2007 9:00 AM EDT" => {"Mar" => [15, 30]} + ) - # ==> (2007 EST) January 15,30 - # (2007 EST) February 15 - # (2007 EDT) March 15,30 + recurrence = Montrose::Recurrence.from_ical(ical) + _(recurrence).must_pair_with expected_events + end end From 72b7b0e43befaed2aa70fd14febac5f99477a640 Mon Sep 17 00:00:00 2001 From: Ross Kaffenberger Date: Sun, 7 Feb 2021 10:27:21 -0500 Subject: [PATCH 30/33] Make ical parsing time zone aware --- lib/montrose/ical.rb | 27 ++++++++++++++++++++++++--- spec/from_ical_rfc_spec.rb | 15 +++++++++++++++ spec/montrose/recurrence_spec.rb | 5 ++++- 3 files changed, 43 insertions(+), 4 deletions(-) diff --git a/lib/montrose/ical.rb b/lib/montrose/ical.rb index ce9de8d..81b7e4f 100644 --- a/lib/montrose/ical.rb +++ b/lib/montrose/ical.rb @@ -17,20 +17,41 @@ def initialize(ical) def parse dtstart, rrule = @ical.split("RRULE:") dtstart, exdate = dtstart.split("\n") - Hash[*parse_dtstart(dtstart) + parse_exdate(exdate) + parse_rrule(rrule)] + _label, time_string = (dtstart || "").split(";") + time_zone = parse_timezone(time_string) + + Time.use_zone(time_zone) do + Hash[ + *parse_dtstart(dtstart) + + parse_exdate(exdate) + + parse_rrule(rrule) + ] + end end private + def parse_timezone(time_string) + time_zone_rule, _ = (time_string || "").split(":") + _label, time_zone = (time_zone_rule || "").split("=") + time_zone + end + def parse_dtstart(dtstart) return [] unless dtstart.present? _label, time_string = dtstart.split(";") - @starts_at = Montrose::Utils.parse_time(time_string) + @starts_at = parse_ical_time(time_string) [:starts, @starts_at] end + def parse_ical_time(time_string) + time_zone = parse_timezone(time_string) + + Montrose::Utils.parse_time(time_string).in_time_zone(time_zone) + end + def parse_exdate(exdate) return [] unless exdate.present? @@ -51,7 +72,7 @@ def parse_rrule(rrule) when "COUNT" [:total, value.to_i] when "UNTIL" - [:until, Montrose::Utils.parse_time(value)] + [:until, parse_ical_time(value)] when "BYMINUTE" [:minute, Montrose::Minute.parse(value)] when "BYHOUR" diff --git a/spec/from_ical_rfc_spec.rb b/spec/from_ical_rfc_spec.rb index 14d1aeb..996d8d8 100644 --- a/spec/from_ical_rfc_spec.rb +++ b/spec/from_ical_rfc_spec.rb @@ -33,6 +33,21 @@ def parse_expect_by_time_of_day(event_map) _(recurrence).must_pair_with expected_events end + it "daily for 10 occurrences in America/Los_Angeles" do + ical = <<~ICAL + DTSTART;TZID=America/Los_Angeles:19970902T090000 + RRULE:FREQ=DAILY;COUNT=10" + ICAL + # ==> (1997 9:00 AM EDT) September 2-11 + + recurrence = Montrose::Recurrence.from_ical(ical) + expected_events = parse_expected_events( + "1997 9:00 AM PDT" => {"Sep" => 2.upto(11)} + ) + + _(recurrence).must_pair_with expected_events + end + it "daily until December 24, 1997" do ical = <<~ICAL DTSTART;TZID=America/New_York:19970902T090000 diff --git a/spec/montrose/recurrence_spec.rb b/spec/montrose/recurrence_spec.rb index 7040ae2..bc455a1 100644 --- a/spec/montrose/recurrence_spec.rb +++ b/spec/montrose/recurrence_spec.rb @@ -186,7 +186,10 @@ describe ".from_ical" do it "returns Recurrence instance" do - ical = "DTSTART;TZID=US-Eastern:19970902T090000\nRRULE:FREQ=DAILY;COUNT=10;INTERVAL=2" + ical = <<~ICAL + DTSTART;TZID=America/New_York:19970902T090000 + RRULE:FREQ=DAILY;COUNT=10;INTERVAL=2" + ICAL recurrence = Montrose::Recurrence.from_ical(ical) recurrence.default_options[:every].must_equal :day From 7aeabee3b41e8e5e0248e7829ca0b41b3908927d Mon Sep 17 00:00:00 2001 From: Ross Kaffenberger Date: Sun, 7 Feb 2021 11:44:55 -0500 Subject: [PATCH 31/33] Use to_a for Enumerator Enumerator#+ does not appear to be implemented in Ruby 2.5 --- lib/montrose/month_day.rb | 2 +- lib/montrose/week.rb | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/montrose/month_day.rb b/lib/montrose/month_day.rb index 2b9bca0..d808ff4 100644 --- a/lib/montrose/month_day.rb +++ b/lib/montrose/month_day.rb @@ -1,7 +1,7 @@ module Montrose class MonthDay class << self - MDAYS = (-31.upto(-1) + 1.upto(31)).to_a + MDAYS = (-31.upto(-1).to_a + 1.upto(31).to_a) def parse(mdays) return nil unless mdays.present? diff --git a/lib/montrose/week.rb b/lib/montrose/week.rb index 0d3802b..05f5580 100644 --- a/lib/montrose/week.rb +++ b/lib/montrose/week.rb @@ -1,7 +1,7 @@ module Montrose class Week class << self - NUMBERS = (-53.upto(-1) + 1.upto(53)).to_a + NUMBERS = (-53.upto(-1).to_a + 1.upto(53).to_a) def parse(arg) return nil unless arg.present? From 05c73edfe14d15afef99fc595caecff448666a76 Mon Sep 17 00:00:00 2001 From: Ross Kaffenberger Date: Sun, 7 Feb 2021 12:04:12 -0500 Subject: [PATCH 32/33] Update specs to use explicit tz for expected dates --- spec/from_ical_rfc_spec.rb | 22 ++++++++++++++++------ 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/spec/from_ical_rfc_spec.rb b/spec/from_ical_rfc_spec.rb index 996d8d8..f28972b 100644 --- a/spec/from_ical_rfc_spec.rb +++ b/spec/from_ical_rfc_spec.rb @@ -25,11 +25,11 @@ def parse_expect_by_time_of_day(event_map) ICAL # ==> (1997 9:00 AM EDT) September 2-11 - recurrence = Montrose::Recurrence.from_ical(ical) expected_events = parse_expected_events( "1997 9:00 AM EDT" => {"Sep" => 2.upto(11)} ) + recurrence = Montrose::Recurrence.from_ical(ical) _(recurrence).must_pair_with expected_events end @@ -58,10 +58,15 @@ def parse_expect_by_time_of_day(event_map) recurrence = Montrose::Recurrence.from_ical(ical) - ends_on = Time.parse("Dec 24 00:00:00 EDT 1997") - days = starts_on.to_date.upto(ends_on.to_date).count - 1 - expected_events = consecutive_days(days, starts: starts_on).take(days) + expected_events = parse_expected_events( + "1997 9:00 AM EDT" => {"Sep" => 2.upto(30), + "Oct" => 1.upto(25)}, + "1997 9:00 AM EST" => {"Oct" => 26.upto(31), + "Nov" => 1.upto(30), + "Dec" => 1.upto(23)} + ) + days = ((expected_events.last - expected_events.first) / 1.day).to_i + 1 _(recurrence).must_pair_with expected_events _(recurrence.events.to_a.size).must_equal days end @@ -146,9 +151,14 @@ def parse_expect_by_time_of_day(event_map) # ==> (1997 9:00 AM EDT) September 2,9,16,23,30;October 7,14,21 # (1997 9:00 AM EST) October 28;November 4 - recurrence = Montrose::Recurrence.from_ical(ical) - expected_events = consecutive_days(10, starts: starts_on, interval: 7) + expected_events = parse_expected_events( + "1997 9:00 AM EDT" => {"Sep" => [2, 9, 16, 23, 30], + "Oct" => [7, 14, 21]}, + "1997 9:00 AM EST" => {"Oct" => [28], + "Nov" => [4]} + ) + recurrence = Montrose::Recurrence.from_ical(ical) _(recurrence).must_pair_with expected_events end From ab12943db66c038b9ecc4e6f26438c989428403f Mon Sep 17 00:00:00 2001 From: Ross Kaffenberger Date: Sun, 7 Feb 2021 12:20:43 -0500 Subject: [PATCH 33/33] Address Minitest 6 deprecation warnings deprecated use of global #must_ helpers; now wrap with _().must.. --- spec/montrose/chainable_spec.rb | 96 ++++---- spec/montrose/clock_spec.rb | 38 +-- spec/montrose/examples_spec.rb | 26 +- spec/montrose/frequency_spec.rb | 18 +- spec/montrose/options_spec.rb | 396 +++++++++++++++---------------- spec/montrose/recurrence_spec.rb | 102 ++++---- spec/montrose/schedule_spec.rb | 113 ++++----- spec/montrose/utils_spec.rb | 56 ++--- spec/montrose_spec.rb | 16 +- spec/rfc_spec.rb | 118 ++++----- 10 files changed, 490 insertions(+), 489 deletions(-) diff --git a/spec/montrose/chainable_spec.rb b/spec/montrose/chainable_spec.rb index bba98a1..93d7b2f 100644 --- a/spec/montrose/chainable_spec.rb +++ b/spec/montrose/chainable_spec.rb @@ -12,92 +12,92 @@ describe "#every" do it "returns recurrence" do recurrence = Montrose.every(:minute) - recurrence.must_be_kind_of Montrose::Recurrence + _(recurrence).must_be_kind_of Montrose::Recurrence end it "emits given frequency default" do recurrence = Montrose.every(:minute) - recurrence.events.must_have_interval 1.minute + _(recurrence.events).must_have_interval 1.minute recurrence = Montrose.every(2.hours) - recurrence.events.must_have_interval 2.hours + _(recurrence.events).must_have_interval 2.hours end it "accepts options" do recurrence = Montrose.every(:minute, total: 2) - recurrence.events.to_a.size.must_equal 2 + _(recurrence.events.to_a.size).must_equal 2 end end describe "#minutely" do it "returns recurrence" do recurrence = Montrose.minutely - recurrence.must_be_kind_of Montrose::Recurrence + _(recurrence).must_be_kind_of Montrose::Recurrence end it "emits per hour by default" do recurrence = Montrose.minutely - recurrence.events.must_have_interval 1.minute + _(recurrence.events).must_have_interval 1.minute end end describe "#hourly" do it "returns recurrence" do recurrence = Montrose.hourly - recurrence.must_be_kind_of Montrose::Recurrence + _(recurrence).must_be_kind_of Montrose::Recurrence end it "emits per hour by default" do recurrence = Montrose.hourly - recurrence.events.must_have_interval 1.hour + _(recurrence.events).must_have_interval 1.hour end end describe "#daily" do it "returns recurrence" do recurrence = Montrose.daily - recurrence.must_be_kind_of Montrose::Recurrence + _(recurrence).must_be_kind_of Montrose::Recurrence end it "emits per day by default" do recurrence = Montrose.daily - recurrence.events.must_have_interval 1.day + _(recurrence.events).must_have_interval 1.day end end describe "#weekly" do it "returns recurrence" do recurrence = Montrose.weekly - recurrence.must_be_kind_of Montrose::Recurrence + _(recurrence).must_be_kind_of Montrose::Recurrence end it "emits per week by default" do recurrence = Montrose.weekly - recurrence.events.must_have_interval 1.week + _(recurrence.events).must_have_interval 1.week end end describe "#monthly" do it "returns recurrence" do recurrence = Montrose.daily - recurrence.must_be_kind_of Montrose::Recurrence + _(recurrence).must_be_kind_of Montrose::Recurrence end it "emits per week by default" do recurrence = Montrose.monthly - recurrence.events.must_have_interval 1.month + _(recurrence.events).must_have_interval 1.month end end describe "#yearly" do it "returns recurrence" do recurrence = Montrose.yearly - recurrence.must_be_kind_of Montrose::Recurrence + _(recurrence).must_be_kind_of Montrose::Recurrence end it "emits per year by default" do recurrence = Montrose.yearly - recurrence.events.must_have_interval((now + 1.year) - now) + _(recurrence.events).must_have_interval((now + 1.year) - now) end end @@ -105,7 +105,7 @@ it "returns new recurrence starting at given time" do recurrence = Montrose.daily.starting(3.days.from_now) - recurrence.events.first.must_equal 3.days.from_now + _(recurrence.events.first).must_equal 3.days.from_now end end @@ -113,7 +113,7 @@ it "returns new recurrence ending before given time" do recurrence = Montrose.daily.ending(3.days.from_now + 1.minute) - recurrence.events.to_a.last.must_equal 3.days.from_now + _(recurrence.events.to_a.last).must_equal 3.days.from_now end end @@ -123,14 +123,14 @@ it "returns recurrence" do recurrence = Montrose.hourly.between(starts...ends) - recurrence.must_be_kind_of Montrose::Recurrence + _(recurrence).must_be_kind_of Montrose::Recurrence end it "specifies start and end" do recurrence = Montrose.hourly.between(starts...ends) events = recurrence.events.to_a - events.first.must_equal starts - events.last.must_equal ends + _(events.first).must_equal starts + _(events.last).must_equal ends end end @@ -140,14 +140,14 @@ it "returns recurrence" do recurrence = Montrose.hourly.covering(from...to) - recurrence.must_be_kind_of Montrose::Recurrence + _(recurrence).must_be_kind_of Montrose::Recurrence end it "specifies start and end of mask" do recurrence = Montrose.hourly.covering(from...to) events = recurrence.events.to_a - events.first.must_equal(Time.local(2015, 9, 2, 0, 0, 0)) - events.last.must_equal(Time.local(2015, 9, 3, 23, 0, 0)) + _(events.first).must_equal(Time.local(2015, 9, 2, 0, 0, 0)) + _(events.last).must_equal(Time.local(2015, 9, 3, 23, 0, 0)) end end @@ -156,12 +156,12 @@ it "returns recurrence" do recurrence = Montrose.every(20.minutes).during("9am-10am") - recurrence.must_be_kind_of Montrose::Recurrence + _(recurrence).must_be_kind_of Montrose::Recurrence end it "emits within given time-of-day range" do recurrence = Montrose.every(20.minutes).during("9am-10am") - recurrence.events.must_pair_with [ + _(recurrence.events).must_pair_with [ Time.local(2015, 9, 1, 9, 0, 0), Time.local(2015, 9, 1, 9, 20, 0), Time.local(2015, 9, 1, 9, 40, 0), @@ -171,7 +171,7 @@ it "emits within given time-of-day range" do recurrence = Montrose.every(20.minutes).during("9am-10am") - recurrence.events.must_pair_with [ + _(recurrence.events).must_pair_with [ Time.local(2015, 9, 1, 9, 0, 0), Time.local(2015, 9, 1, 9, 20, 0), Time.local(2015, 9, 1, 9, 40, 0), @@ -185,7 +185,7 @@ recurrence = Montrose.monthly.starting(Time.local(2016, 1, 2)) recurrence = recurrence.day_of_month(1, -1) - recurrence.events.must_pair_with [ + _(recurrence.events).must_pair_with [ Time.local(2016, 1, 31), Time.local(2016, 2, 1), Time.local(2016, 2, 29), @@ -199,20 +199,20 @@ recurrence = Montrose.weekly recurrence = recurrence.day_of_week(:sunday, :saturday) - recurrence.events.must_pair_with [ + _(recurrence.events).must_pair_with [ Time.local(2015, 9, 5, 12), Time.local(2015, 9, 6, 12) ] - Time.local(2015, 9, 5, 12).wday.must_equal 6 - Time.local(2015, 9, 6, 12).wday.must_equal 0 + _(Time.local(2015, 9, 5, 12).wday).must_equal 6 + _(Time.local(2015, 9, 6, 12).wday).must_equal 0 end it "returns new recurrence by given range of days" do recurrence = Montrose.weekly recurrence = recurrence.day_of_week(1..3) - recurrence.events.must_pair_with [ + _(recurrence.events).must_pair_with [ Time.local(2015, 9, 1, 12), # Tuesday Time.local(2015, 9, 2, 12), Time.local(2015, 9, 7, 12) @@ -223,7 +223,7 @@ recurrence = Montrose.monthly recurrence = recurrence.day_of_week(tuesday: [2]) # 2nd Tuesday of month - recurrence.events.must_pair_with [ + _(recurrence.events).must_pair_with [ Time.local(2015, 9, 8, 12), # Tuesday Time.local(2015, 10, 13, 12), Time.local(2015, 11, 10, 12) @@ -236,7 +236,7 @@ recurrence = Montrose.yearly recurrence = recurrence.day_of_year(1, 10, 100) - recurrence.events.must_pair_with [ + _(recurrence.events).must_pair_with [ Time.local(2016, 1, 1, 12), Time.local(2016, 1, 10, 12), Time.local(2016, 4, 9, 12), # 100th day @@ -248,7 +248,7 @@ recurrence = Montrose.yearly recurrence = recurrence.day_of_year([1, 10, 100]) - recurrence.events.must_pair_with [ + _(recurrence.events).must_pair_with [ Time.local(2016, 1, 1, 12), Time.local(2016, 1, 10, 12), Time.local(2016, 4, 9, 12), # 100th day @@ -260,7 +260,7 @@ recurrence = Montrose.yearly recurrence = recurrence.day_of_year(98..100) - recurrence.events.must_pair_with [ + _(recurrence.events).must_pair_with [ Time.local(2016, 4, 7, 12), Time.local(2016, 4, 8, 12), Time.local(2016, 4, 9, 12), # 100th day @@ -274,7 +274,7 @@ recurrence = Montrose.daily recurrence = recurrence.hour_of_day(6, 7, 8) - recurrence.events.must_pair_with [ + _(recurrence.events).must_pair_with [ Time.local(2015, 9, 2, 6), Time.local(2015, 9, 2, 7), Time.local(2015, 9, 2, 8), @@ -286,7 +286,7 @@ recurrence = Montrose.daily recurrence = recurrence.hour_of_day([6, 7, 8]) - recurrence.events.must_pair_with [ + _(recurrence.events).must_pair_with [ Time.local(2015, 9, 2, 6), Time.local(2015, 9, 2, 7), Time.local(2015, 9, 2, 8), @@ -298,7 +298,7 @@ recurrence = Montrose.daily recurrence = recurrence.hour_of_day(6..8) - recurrence.events.must_pair_with [ + _(recurrence.events).must_pair_with [ Time.local(2015, 9, 2, 6), Time.local(2015, 9, 2, 7), Time.local(2015, 9, 2, 8), @@ -312,7 +312,7 @@ recurrence = Montrose.yearly recurrence = recurrence.month_of_year(:january, :april) - recurrence.events.must_pair_with [ + _(recurrence.events).must_pair_with [ Time.local(2016, 1, 1, 12), Time.local(2016, 4, 1, 12), Time.local(2017, 1, 1, 12) @@ -323,7 +323,7 @@ recurrence = Montrose.yearly recurrence = recurrence.month_of_year([:january, :april]) - recurrence.events.must_pair_with [ + _(recurrence.events).must_pair_with [ Time.local(2016, 1, 1, 12), Time.local(2016, 4, 1, 12), Time.local(2017, 1, 1, 12) @@ -334,7 +334,7 @@ recurrence = Montrose.yearly recurrence = recurrence.month_of_year(1..3) - recurrence.events.must_pair_with [ + _(recurrence.events).must_pair_with [ Time.local(2016, 1, 1, 12), Time.local(2016, 2, 1, 12), Time.local(2016, 3, 1, 12), @@ -349,13 +349,13 @@ recurrence = recurrence.total(3) events = recurrence.events.to_a - events.must_pair_with [ + _(events).must_pair_with [ Time.local(2015, 9, 1, 12), Time.local(2016, 9, 1, 12), Time.local(2017, 9, 1, 12) ] - events.size.must_equal 3 + _(events.size).must_equal 3 end end @@ -364,7 +364,7 @@ recurrence = Montrose.yearly recurrence = recurrence.day_of_week(:monday).week_of_year(1, 52) - recurrence.events.must_pair_with [ + _(recurrence.events).must_pair_with [ Time.local(2015, 12, 21, 12), # Monday, 52nd week Time.local(2016, 1, 4, 12) # Monday, 1st week ] @@ -374,7 +374,7 @@ recurrence = Montrose.yearly recurrence = recurrence.day_of_week(:monday).week_of_year(50..52) - recurrence.events.must_pair_with [ + _(recurrence.events).must_pair_with [ Time.local(2015, 12, 7, 12), Time.local(2015, 12, 14, 12), Time.local(2015, 12, 21, 12) # Monday, 52nd week @@ -387,7 +387,7 @@ recurrence = Montrose.weekly recurrence = recurrence.on(:monday) - recurrence.events.must_pair_with [ + _(recurrence.events).must_pair_with [ Time.local(2015, 9, 7, 12), Time.local(2015, 9, 14, 12), Time.local(2015, 9, 21, 12) @@ -400,7 +400,7 @@ recurrence = Montrose.daily recurrence = recurrence.at("4:05pm") - recurrence.events.must_pair_with [ + _(recurrence.events).must_pair_with [ Time.local(2015, 9, 1, 16, 5), Time.local(2015, 9, 2, 16, 5), Time.local(2015, 9, 3, 16, 5) diff --git a/spec/montrose/clock_spec.rb b/spec/montrose/clock_spec.rb index ffc4955..542a5a6 100644 --- a/spec/montrose/clock_spec.rb +++ b/spec/montrose/clock_spec.rb @@ -12,107 +12,107 @@ describe "#tick" do it "must start with given starts time" do clock = new_clock(every: :minute, starts: now) - clock.tick.must_equal now + _(clock.tick).must_equal now end it "emits 1 minute increments" do clock = new_clock(every: :minute) - clock.must_have_tick 1.minute + _(clock).must_have_tick 1.minute end it "emits minute interval increments" do clock = new_clock(every: :minute, interval: 30) - clock.must_have_tick 30.minutes + _(clock).must_have_tick 30.minutes end it "emits 1 hour increments" do clock = new_clock(every: :hour) - clock.must_have_tick 1.hour + _(clock).must_have_tick 1.hour end it "emits 1 hour increments when hour smallest part" do clock = new_clock(every: :day, hour: 9..10) - clock.must_have_tick 1.hour + _(clock).must_have_tick 1.hour end it "emits hour interval increments" do clock = new_clock(every: :hour, interval: 6) - clock.must_have_tick 6.hours + _(clock).must_have_tick 6.hours end it "emits 1 day increments for :day" do clock = new_clock(every: :day) - clock.must_have_tick 1.day + _(clock).must_have_tick 1.day end it "emits 1 day increments for :mday" do clock = new_clock(every: :month, mday: [1, -1]) - clock.must_have_tick 1.day + _(clock).must_have_tick 1.day end it "emits 1 day increments for :yday" do clock = new_clock(every: :year, yday: [1, 10, 100]) - clock.must_have_tick 1.day + _(clock).must_have_tick 1.day end it "emits day interval increments" do clock = new_clock(every: :day, interval: 3) - clock.must_have_tick 3.days + _(clock).must_have_tick 3.days end it "emits 1 day increments when day smallest part" do clock = new_clock(every: :month, day: :tuesday) - clock.must_have_tick 1.day + _(clock).must_have_tick 1.day end it "emits 1 week increments" do clock = new_clock(every: :week) - clock.must_have_tick 1.week + _(clock).must_have_tick 1.week end it "emits week interval increments" do clock = new_clock(every: :week, interval: 2) - clock.must_have_tick 2.weeks + _(clock).must_have_tick 2.weeks end it "emits 1 year increments" do clock = new_clock(every: :year) - clock.must_have_tick 1.year + _(clock).must_have_tick 1.year end it "emits year interval increments" do clock = new_clock(every: :year, interval: 10) - clock.must_have_tick 10.years + _(clock).must_have_tick 10.years end it "emits increments on at values" do Timecop.freeze(Time.local(2018, 5, 31, 11)) do clock = new_clock(every: :day, at: [[10, 0, 59], [15, 30, 34]]) - clock.tick.must_equal Time.local(2018, 5, 31, 15, 30, 34) - clock.tick.must_equal Time.local(2018, 6, 1, 10, 0, 59) - clock.tick.must_equal Time.local(2018, 6, 1, 15, 30, 34) + _(clock.tick).must_equal Time.local(2018, 5, 31, 15, 30, 34) + _(clock.tick).must_equal Time.local(2018, 6, 1, 10, 0, 59) + _(clock.tick).must_equal Time.local(2018, 6, 1, 15, 30, 34) end end it "emits correct tick when at values empty" do clock = new_clock(every: :day, at: []) - clock.must_have_tick 1.day + _(clock).must_have_tick 1.day end end end diff --git a/spec/montrose/examples_spec.rb b/spec/montrose/examples_spec.rb index d7c3b05..600c295 100644 --- a/spec/montrose/examples_spec.rb +++ b/spec/montrose/examples_spec.rb @@ -13,7 +13,7 @@ it "every day at 3:30pm" do recurrence = new_recurrence(every: :day, at: "3:30 PM") - recurrence.events.take(3).must_pair_with [ + _(recurrence.events.take(3)).must_pair_with [ Time.local(2015, 9, 1, 15, 30), Time.local(2015, 9, 2, 15, 30), Time.local(2015, 9, 3, 15, 30) @@ -23,7 +23,7 @@ it "specifying starts and at option" do recurrence = new_recurrence(every: :week, on: "tuesday", at: "5:00", starts: "2016-06-23") - recurrence.events.take(3).must_pair_with [ + _(recurrence.events.take(3)).must_pair_with [ Time.local(2016, 6, 28, 5, 0), Time.local(2016, 7, 5, 5, 0), Time.local(2016, 7, 12, 5, 0) @@ -41,7 +41,7 @@ it "multiple at values" do recurrence = new_recurrence(every: :day, at: ["7:00am", "3:30pm"]) - recurrence.events.take(3).must_pair_with [ + _(recurrence.events.take(3)).must_pair_with [ Time.local(2015, 9, 1, 15, 30), Time.local(2015, 9, 2, 7, 0), Time.local(2015, 9, 2, 15, 30) @@ -54,7 +54,7 @@ starts: 1.day.ago, between: Date.today..7.days.from_now) - recurrence.events.to_a.must_pair_with [ + _(recurrence.events.to_a).must_pair_with [ Time.local(2015, 8, 31, 12), Time.local(2015, 9, 3, 12) ] @@ -66,7 +66,7 @@ starts: 1.day.from_now, between: Date.today..7.days.from_now) - recurrence.events.to_a.must_pair_with [ + _(recurrence.events.to_a).must_pair_with [ Time.local(2015, 9, 2, 12), Time.local(2015, 9, 5, 12), Time.local(2015, 9, 8, 12) @@ -79,7 +79,7 @@ starts: 1.day.ago, covering: Date.today..7.days.from_now) - recurrence.events.to_a.must_pair_with [ + _(recurrence.events.to_a).must_pair_with [ Time.local(2015, 9, 3, 12), Time.local(2015, 9, 6, 12) ] @@ -91,7 +91,7 @@ starts: 1.day.from_now, covering: Date.today..7.days.from_now) - recurrence.events.to_a.must_pair_with [ + _(recurrence.events.to_a).must_pair_with [ Time.local(2015, 9, 2, 12), Time.local(2015, 9, 5, 12), Time.local(2015, 9, 8, 12) @@ -105,7 +105,7 @@ starts: 1.day.ago, between: Date.today..7.days.from_now) - recurrence.events.to_a.must_pair_with [ + _(recurrence.events.to_a).must_pair_with [ Time.local(2015, 9, 3, 12), Time.local(2015, 9, 6, 12) ] @@ -119,7 +119,7 @@ starts: 1.day.from_now, between: Date.today..7.days.from_now) - recurrence.events.to_a.must_pair_with [ + _(recurrence.events.to_a).must_pair_with [ Time.local(2015, 9, 2, 12), Time.local(2015, 9, 5, 12), Time.local(2015, 9, 8, 12) @@ -132,14 +132,14 @@ at = "6:00am" recurrence = new_recurrence(every: :day, starts: starts, at: at) - recurrence.take(3).length.must_equal 3 + _(recurrence.take(3).length).must_equal 3 end it "returns daily events after current time" do at = "6:00am" recurrence = new_recurrence(every: :day, at: at) - recurrence.take(3).must_pair_with [ + _(recurrence.take(3)).must_pair_with [ Time.local(2015, 9, 2, 6), Time.local(2015, 9, 3, 6), Time.local(2015, 9, 4, 6) @@ -153,7 +153,7 @@ month: 1, day: {friday: [2]}) - recurrence.events.take(3).to_a.must_pair_with [ + _(recurrence.events.take(3).to_a).must_pair_with [ Time.local(2016, 1, 8, 12), Time.local(2017, 1, 13, 12), Time.local(2018, 1, 12, 12) @@ -165,7 +165,7 @@ month: 2, day: {friday: [2]}) - recurrence.events.take(3).to_a.must_pair_with [ + _(recurrence.events.take(3).to_a).must_pair_with [ Time.local(2016, 2, 12, 12), Time.local(2017, 2, 10, 12), Time.local(2018, 2, 9, 12) diff --git a/spec/montrose/frequency_spec.rb b/spec/montrose/frequency_spec.rb index 6b595e8..6af0200 100644 --- a/spec/montrose/frequency_spec.rb +++ b/spec/montrose/frequency_spec.rb @@ -12,45 +12,45 @@ describe "self.from_options" do it "every: :year" do frequency = Montrose::Frequency.from_options(every: :year) - frequency.must_be_instance_of Montrose::Frequency::Yearly + _(frequency).must_be_instance_of Montrose::Frequency::Yearly end it "every: :week" do frequency = Montrose::Frequency.from_options(every: :week) - frequency.must_be_instance_of Montrose::Frequency::Weekly + _(frequency).must_be_instance_of Montrose::Frequency::Weekly end it "every: :month" do frequency = Montrose::Frequency.from_options(every: :month) - frequency.must_be_instance_of Montrose::Frequency::Monthly + _(frequency).must_be_instance_of Montrose::Frequency::Monthly end it "every: :day" do frequency = Montrose::Frequency.from_options(every: :day) - frequency.must_be_instance_of Montrose::Frequency::Daily + _(frequency).must_be_instance_of Montrose::Frequency::Daily end it "every: :hour" do frequency = Montrose::Frequency.from_options(every: :hour) - frequency.must_be_instance_of Montrose::Frequency::Hourly + _(frequency).must_be_instance_of Montrose::Frequency::Hourly end it "every: :minute" do frequency = Montrose::Frequency.from_options(every: :minute) - frequency.must_be_instance_of Montrose::Frequency::Minutely + _(frequency).must_be_instance_of Montrose::Frequency::Minutely end it "every: 'minute' as string value" do frequency = Montrose::Frequency.from_options(every: "minute") - frequency.must_be_instance_of Montrose::Frequency::Minutely + _(frequency).must_be_instance_of Montrose::Frequency::Minutely end it "every: :other" do - -> { Montrose::Frequency.from_options(every: :other) }.must_raise + _(-> { Montrose::Frequency.from_options(every: :other) }).must_raise end it "missing every" do - -> { Montrose::Frequency.from_options({}) }.must_raise + _(-> { Montrose::Frequency.from_options({}) }).must_raise end end end diff --git a/spec/montrose/options_spec.rb b/spec/montrose/options_spec.rb index 3e6ad07..e598caa 100644 --- a/spec/montrose/options_spec.rb +++ b/spec/montrose/options_spec.rb @@ -5,7 +5,7 @@ describe Montrose::Options do let(:options) { new_options } - it { Montrose::Options.new(nil).must_be_instance_of(Montrose::Options) } + it { _(Montrose::Options.new(nil)).must_be_instance_of(Montrose::Options) } describe "#start_time" do before do @@ -19,19 +19,19 @@ it "defaults to :starts" do options[:starts] = 3.days.from_now - options.start_time.must_equal 3.days.from_now - options[:start_time].must_equal 3.days.from_now + _(options.start_time).must_equal 3.days.from_now + _(options[:start_time]).must_equal 3.days.from_now end it "defaults to default_starts time" do Montrose::Options.default_starts = 3.days.from_now - options.start_time.must_equal 3.days.from_now - options[:start_time].must_equal 3.days.from_now + _(options.start_time).must_equal 3.days.from_now + _(options[:start_time]).must_equal 3.days.from_now end it "cannot be set" do - -> { options[:start_time] = 3.days.from_now }.must_raise + _(-> { options[:start_time] = 3.days.from_now }).must_raise end it "is :at time on default_starts date" do @@ -39,21 +39,21 @@ Timecop.freeze(noon) options[:at] = "7pm" - options[:start_time].must_equal Time.local(2015, 9, 1, 19) + _(options[:start_time]).must_equal Time.local(2015, 9, 1, 19) end it "is current time despite :at time earlier in day" do options[:starts] = Time.local(2019, 12, 25, 20) options[:at] = %w[7pm 10am] - options[:start_time].must_equal Time.local(2019, 12, 25, 20) + _(options[:start_time]).must_equal Time.local(2019, 12, 25, 20) end it "is :starts when :at empty" do options[:starts] = Time.local(2019, 12, 25, 20) options[:at] = [] - options[:start_time].must_equal Time.local(2019, 12, 25, 20) + _(options[:start_time]).must_equal Time.local(2019, 12, 25, 20) end end @@ -65,19 +65,19 @@ it "accepts time" do Montrose::Options.default_starts = Time.local(2016, 9, 2, 12) - Montrose::Options.default_starts.must_equal Time.local(2016, 9, 2, 12) + _(Montrose::Options.default_starts).must_equal Time.local(2016, 9, 2, 12) end it "accepts string" do Montrose::Options.default_starts = "September 2, 2016 at 12 PM" - Montrose::Options.default_starts.must_equal Time.local(2016, 9, 2, 12) + _(Montrose::Options.default_starts).must_equal Time.local(2016, 9, 2, 12) end it "accepts proc" do Montrose::Options.default_starts = -> { Time.local(2016, 9, 2, 12) } - Montrose::Options.default_starts.must_equal Time.local(2016, 9, 2, 12) + _(Montrose::Options.default_starts).must_equal Time.local(2016, 9, 2, 12) end end @@ -89,19 +89,19 @@ it "accepts time" do Montrose::Options.default_until = Time.local(2016, 9, 2, 12) - Montrose::Options.default_until.must_equal Time.local(2016, 9, 2, 12) + _(Montrose::Options.default_until).must_equal Time.local(2016, 9, 2, 12) end it "accepts string" do Montrose::Options.default_until = "September 2, 2016 at 12 PM" - Montrose::Options.default_until.must_equal Time.local(2016, 9, 2, 12) + _(Montrose::Options.default_until).must_equal Time.local(2016, 9, 2, 12) end it "accepts proc" do Montrose::Options.default_until = -> { Time.local(2016, 9, 2, 12) } - Montrose::Options.default_until.must_equal Time.local(2016, 9, 2, 12) + _(Montrose::Options.default_until).must_equal Time.local(2016, 9, 2, 12) end end @@ -111,108 +111,108 @@ end it "defaults to nil" do - options.every.must_be_nil - options[:every].must_be_nil + _(options.every).must_be_nil + _(options[:every]).must_be_nil end it "defaults to default_frequency time" do Montrose::Options.default_every = :month - options.every.must_equal :month - options[:every].must_equal :month + _(options.every).must_equal :month + _(options[:every]).must_equal :month end it "can be set with valid symbol name" do options[:every] = :month - options.every.must_equal :month - options[:every].must_equal :month + _(options.every).must_equal :month + _(options[:every]).must_equal :month end it "can be set with valid string name" do options[:every] = "month" - options.every.must_equal :month - options[:every].must_equal :month + _(options.every).must_equal :month + _(options[:every]).must_equal :month end it "must be a valid frequency" do - -> { options[:every] = :nonsense }.must_raise + _(-> { options[:every] = :nonsense }).must_raise end it "aliases to :frequency" do options[:frequency] = "month" - options.every.must_equal :month - options[:every].must_equal :month - options[:frequency].must_equal :month + _(options.every).must_equal :month + _(options[:every]).must_equal :month + _(options[:frequency]).must_equal :month end describe "from integer" do it "parses as every: :minute with interval" do options[:every] = 30.minutes - options_duration(options).must_equal 30.minutes + _(options_duration(options)).must_equal 30.minutes options[:every] = 90.minutes - options_duration(options).must_equal 90.minutes + _(options_duration(options)).must_equal 90.minutes end it "parses as every: :hour, with interval" do options[:every] = 1.hour - options_duration(options).must_equal 1.hour + _(options_duration(options)).must_equal 1.hour options[:every] = 5.hours - options_duration(options).must_equal 5.hours + _(options_duration(options)).must_equal 5.hours end it "parses as every: :day, with interval" do options[:every] = 1.day - options_duration(options).must_equal 1.day + _(options_duration(options)).must_equal 1.day options[:every] = 30.days - options_duration(options).must_equal 30.days + _(options_duration(options)).must_equal 30.days end it "parses as every: :week, with interval" do options[:every] = 1.week - options_duration(options).must_equal 1.week + _(options_duration(options)).must_equal 1.week options[:every] = 12.weeks - options_duration(options).must_equal 12.weeks + _(options_duration(options)).must_equal 12.weeks end it "parses as every: :month, with interval" do options[:every] = 1.month - options_duration(options).must_equal 1.month + _(options_duration(options)).must_equal 1.month options[:every] = 12.months - options_duration(options).must_equal 12.months + _(options_duration(options)).must_equal 12.months end it "parses as every: :year, with interval" do options[:every] = 1.year - options_duration(options).must_equal 1.year + _(options_duration(options)).must_equal 1.year options[:every] = 12.years - options_duration(options).must_equal 12.years + _(options_duration(options)).must_equal 12.years end it "parses on initialize, ignores given interval" do options = new_options(every: 5.years, interval: 2) - options_duration(options).must_equal 5.years + _(options_duration(options)).must_equal 5.years end end end @@ -225,30 +225,30 @@ it "can be set" do options[:starts] = 3.days.from_now - options.starts.must_equal 3.days.from_now - options[:starts].must_equal 3.days.from_now + _(options.starts).must_equal 3.days.from_now + _(options[:starts]).must_equal 3.days.from_now end it "can't be nil" do options[:starts] = nil - options.starts.must_equal time_now - options[:starts].must_equal time_now + _(options.starts).must_equal time_now + _(options[:starts]).must_equal time_now end it "parses string" do options[:starts] = "2015-09-01" - options.starts.must_equal Time.parse("2015-09-01") - options[:starts].must_equal Time.parse("2015-09-01") + _(options.starts).must_equal Time.parse("2015-09-01") + _(options[:starts]).must_equal Time.parse("2015-09-01") end it "converts Date to Time" do date = Date.parse("2015-09-01") options[:starts] = date - options.starts.must_equal date.to_time - options[:starts].must_equal date.to_time + _(options.starts).must_equal date.to_time + _(options[:starts]).must_equal date.to_time end end @@ -262,38 +262,38 @@ end it "defaults to nil" do - options.until.must_be_nil - options[:until].must_be_nil + _(options.until).must_be_nil + _(options[:until]).must_be_nil end it "defaults to default_until time" do Montrose::Options.default_until = 3.days.from_now default = Montrose::Options.merge(options) - default.until.must_equal 3.days.from_now - default[:until].must_equal 3.days.from_now + _(default.until).must_equal 3.days.from_now + _(default[:until]).must_equal 3.days.from_now end it "can be set" do options[:until] = 3.days.from_now - options.until.must_equal 3.days.from_now - options[:until].must_equal 3.days.from_now + _(options.until).must_equal 3.days.from_now + _(options[:until]).must_equal 3.days.from_now end it "parses string" do options[:until] = "2015-09-01" - options.until.must_equal Time.parse("2015-09-01") - options[:until].must_equal Time.parse("2015-09-01") + _(options.until).must_equal Time.parse("2015-09-01") + _(options[:until]).must_equal Time.parse("2015-09-01") end it "converts Date to Time" do date = Date.parse("2015-09-01") options[:until] = date - options.until.must_equal date.to_time - options[:until].must_equal date.to_time + _(options.until).must_equal date.to_time + _(options[:until]).must_equal date.to_time end end @@ -309,22 +309,22 @@ it "sets starts and until times" do options[:between] = Date.today..1.month.from_now.to_date - options.starts.must_equal Date.today.to_time - options.until.must_equal 1.month.from_now.beginning_of_day + _(options.starts).must_equal Date.today.to_time + _(options.until).must_equal 1.month.from_now.beginning_of_day end it "defers to separate starts time outside of range" do options[:between] = Date.today..1.month.from_now.to_date options[:starts] = 1.day.ago - options.starts.must_equal 1.day.ago.to_time + _(options.starts).must_equal 1.day.ago.to_time end it "defers to separate starts time within range" do options[:between] = Date.today..1.month.from_now.to_date options[:starts] = 1.day.from_now - options.starts.must_equal 1.day.from_now.to_time + _(options.starts).must_equal 1.day.from_now.to_time end end @@ -336,7 +336,7 @@ it "returns given date range" do options[:covering] = Date.today..1.month.from_now.to_date - options.covering.must_equal(Date.today..1.month.from_now.to_date) + _(options.covering).must_equal(Date.today..1.month.from_now.to_date) end end @@ -344,340 +344,340 @@ it "defaults to 1" do default = Montrose::Options.merge(options) - default.interval.must_equal 1 - default[:interval].must_equal 1 + _(default.interval).must_equal 1 + _(default[:interval]).must_equal 1 end it "can be set" do options[:interval] = 2 - options.interval.must_equal 2 - options[:interval].must_equal 2 + _(options.interval).must_equal 2 + _(options[:interval]).must_equal 2 end end describe "#total" do it "defaults to nil" do - options.total.must_be_nil - options[:total].must_be_nil + _(options.total).must_be_nil + _(options[:total]).must_be_nil end it "can be set" do options[:total] = 2 - options.total.must_equal 2 - options[:total].must_equal 2 + _(options.total).must_equal 2 + _(options[:total]).must_equal 2 end end describe "#day" do it "defaults to nil" do - options.day.must_be_nil - options[:day].must_be_nil + _(options.day).must_be_nil + _(options[:day]).must_be_nil end it "casts day names to day numbers" do options[:day] = %i[monday tuesday] - options.day.must_equal [1, 2] - options[:day].must_equal [1, 2] + _(options.day).must_equal [1, 2] + _(options[:day]).must_equal [1, 2] end it "casts to element to array" do options[:day] = :monday - options.day.must_equal [1] - options[:day].must_equal [1] + _(options.day).must_equal [1] + _(options[:day]).must_equal [1] end it "can set numbers" do options[:day] = 1 - options.day.must_equal [1] - options[:day].must_equal [1] + _(options.day).must_equal [1] + _(options[:day]).must_equal [1] end describe "nested hash" do it "converts day name keys" do options[:day] = {friday: [1]} - options.day.must_equal(5 => [1]) - options[:day].must_equal(5 => [1]) + _(options.day).must_equal(5 => [1]) + _(options[:day]).must_equal(5 => [1]) end it "casts day number values to arrays" do options[:day] = {5 => 1} - options.day.must_equal(5 => [1]) - options[:day].must_equal(5 => [1]) + _(options.day).must_equal(5 => [1]) + _(options[:day]).must_equal(5 => [1]) end end end describe "#mday" do it "defaults to nil" do - options.mday.must_be_nil - options[:mday].must_be_nil + _(options.mday).must_be_nil + _(options[:mday]).must_be_nil end it "can be set" do options[:mday] = [1, 20, 31] - options.mday.must_equal [1, 20, 31] - options[:mday].must_equal [1, 20, 31] + _(options.mday).must_equal [1, 20, 31] + _(options[:mday]).must_equal [1, 20, 31] end it "casts to element to array" do options[:mday] = 1 - options.mday.must_equal [1] - options[:mday].must_equal [1] + _(options.mday).must_equal [1] + _(options[:mday]).must_equal [1] end it "allows negative numbers" do options[:yday] = [-1] - options.yday.must_equal [-1] - options[:yday].must_equal [-1] + _(options.yday).must_equal [-1] + _(options[:yday]).must_equal [-1] end it "casts range to array" do options[:mday] = 6..8 - options.mday.must_equal [6, 7, 8] - options[:mday].must_equal [6, 7, 8] + _(options.mday).must_equal [6, 7, 8] + _(options[:mday]).must_equal [6, 7, 8] end it "casts nil to empty array" do options[:mday] = nil - options.day.must_be_nil - options[:day].must_be_nil + _(options.day).must_be_nil + _(options[:day]).must_be_nil end it "raises for out of range" do - -> { options[:mday] = [1, 100] }.must_raise + _(-> { options[:mday] = [1, 100] }).must_raise end end describe "#yday" do it "defaults to nil" do - options.yday.must_be_nil - options[:yday].must_be_nil + _(options.yday).must_be_nil + _(options[:yday]).must_be_nil end it "can be set" do options[:yday] = [1, 200, 366] - options.yday.must_equal [1, 200, 366] - options[:yday].must_equal [1, 200, 366] + _(options.yday).must_equal [1, 200, 366] + _(options[:yday]).must_equal [1, 200, 366] end it "casts to element to array" do options[:yday] = 1 - options.yday.must_equal [1] - options[:yday].must_equal [1] + _(options.yday).must_equal [1] + _(options[:yday]).must_equal [1] end it "allows negative numbers" do options[:yday] = [-1] - options.yday.must_equal [-1] - options[:yday].must_equal [-1] + _(options.yday).must_equal [-1] + _(options[:yday]).must_equal [-1] end it "casts range to array" do options[:yday] = 6..8 - options.yday.must_equal [6, 7, 8] - options[:yday].must_equal [6, 7, 8] + _(options.yday).must_equal [6, 7, 8] + _(options[:yday]).must_equal [6, 7, 8] end it "can be set to nil" do options[:yday] = nil - options.day.must_be_nil - options[:day].must_be_nil + _(options.day).must_be_nil + _(options[:day]).must_be_nil end it "raises for out of range" do - -> { options[:yday] = [1, 400] }.must_raise + _(-> { options[:yday] = [1, 400] }).must_raise end end describe "#week" do it "defaults to nil" do - options.week.must_be_nil - options[:week].must_be_nil + _(options.week).must_be_nil + _(options[:week]).must_be_nil end it "can be set" do options[:week] = [1, 10, 53] - options.week.must_equal [1, 10, 53] - options[:week].must_equal [1, 10, 53] + _(options.week).must_equal [1, 10, 53] + _(options[:week]).must_equal [1, 10, 53] end it "casts element to array" do options[:week] = 1 - options.week.must_equal [1] - options[:week].must_equal [1] + _(options.week).must_equal [1] + _(options[:week]).must_equal [1] end it "allows negative numbers" do options[:week] = [-1] - options.week.must_equal [-1] - options[:week].must_equal [-1] + _(options.week).must_equal [-1] + _(options[:week]).must_equal [-1] end it "casts range to array" do options[:week] = 6..8 - options.week.must_equal [6, 7, 8] - options[:week].must_equal [6, 7, 8] + _(options.week).must_equal [6, 7, 8] + _(options[:week]).must_equal [6, 7, 8] end it "can be set to nil" do options[:week] = nil - options.week.must_be_nil - options[:week].must_be_nil + _(options.week).must_be_nil + _(options[:week]).must_be_nil end it "raises for out of range" do - -> { options[:week] = [1, 56] }.must_raise + _(-> { options[:week] = [1, 56] }).must_raise end it "raises for negative out of range" do - -> { options[:hour] = -1 }.must_raise + _(-> { options[:hour] = -1 }).must_raise end end describe "#month" do it "defaults to nil" do - options.month.must_be_nil - options[:month].must_be_nil + _(options.month).must_be_nil + _(options[:month]).must_be_nil end it "can be set by month number" do options[:month] = [1, 12] - options.month.must_equal [1, 12] - options[:month].must_equal [1, 12] + _(options.month).must_equal [1, 12] + _(options[:month]).must_equal [1, 12] end it "casts month names to month numbers" do options[:month] = %i[january december] - options.month.must_equal [1, 12] - options[:month].must_equal [1, 12] + _(options.month).must_equal [1, 12] + _(options[:month]).must_equal [1, 12] options[:month] = %w[january december] - options.month.must_equal [1, 12] - options[:month].must_equal [1, 12] + _(options.month).must_equal [1, 12] + _(options[:month]).must_equal [1, 12] options[:month] = %w[January December] - options.month.must_equal [1, 12] - options[:month].must_equal [1, 12] + _(options.month).must_equal [1, 12] + _(options[:month]).must_equal [1, 12] end it "casts element to array" do options[:month] = 1 - options.month.must_equal [1] - options[:month].must_equal [1] + _(options.month).must_equal [1] + _(options[:month]).must_equal [1] end it "casts range to array" do options[:month] = 6..8 - options.month.must_equal [6, 7, 8] - options[:month].must_equal [6, 7, 8] + _(options.month).must_equal [6, 7, 8] + _(options[:month]).must_equal [6, 7, 8] end it "can be set to nil" do options[:month] = nil - options.month.must_be_nil - options[:month].must_be_nil + _(options.month).must_be_nil + _(options[:month]).must_be_nil end it "raises for out of range" do - -> { options[:month] = [1, 13] }.must_raise + _(-> { options[:month] = [1, 13] }).must_raise end it "raises for negative out of range" do - -> { options[:month] = -1 }.must_raise + _(-> { options[:month] = -1 }).must_raise end end describe "#hour" do it "defaults to nil" do - options.hour.must_be_nil - options[:hour].must_be_nil + _(options.hour).must_be_nil + _(options[:hour]).must_be_nil end it "can be set by hour number" do options[:hour] = [1, 24] - options.hour.must_equal [1, 24] - options[:hour].must_equal [1, 24] + _(options.hour).must_equal [1, 24] + _(options[:hour]).must_equal [1, 24] end it "casts element to array" do options[:hour] = 1 - options.hour.must_equal [1] - options[:hour].must_equal [1] + _(options.hour).must_equal [1] + _(options[:hour]).must_equal [1] end it "casts range to array" do options[:hour] = 6..8 - options.hour.must_equal [6, 7, 8] - options[:hour].must_equal [6, 7, 8] + _(options.hour).must_equal [6, 7, 8] + _(options[:hour]).must_equal [6, 7, 8] end it "can be set to nil" do options[:hour] = nil - options.hour.must_be_nil - options[:hour].must_be_nil + _(options.hour).must_be_nil + _(options[:hour]).must_be_nil end it "raises for out of range" do - -> { options[:hour] = [1, 25] }.must_raise + _(-> { options[:hour] = [1, 25] }).must_raise end it "raises for negative out of range" do - -> { options[:hour] = -1 }.must_raise + _(-> { options[:hour] = -1 }).must_raise end end describe "#during" do it "defaults to nil" do - options.during.must_be_nil - options[:during].must_be_nil + _(options.during).must_be_nil + _(options[:during]).must_be_nil end it "handles ranges of time" do range = Time.parse("9am")..Time.parse("5pm") options[:during] = range - options.during.must_equal [[[9, 0, 0], [17, 0, 0]]] - options[:during].must_equal [[[9, 0, 0], [17, 0, 0]]] + _(options.during).must_equal [[[9, 0, 0], [17, 0, 0]]] + _(options[:during]).must_equal [[[9, 0, 0], [17, 0, 0]]] end it "handles string of beginning and end times" do options[:during] = "9am - 5pm" - options.during.must_equal [[[9, 0, 0], [17, 0, 0]]] - options[:during].must_equal [[[9, 0, 0], [17, 0, 0]]] + _(options.during).must_equal [[[9, 0, 0], [17, 0, 0]]] + _(options[:during]).must_equal [[[9, 0, 0], [17, 0, 0]]] end it "can be set by an array time ranges" do @@ -685,29 +685,29 @@ range_2 = "7:30pm-11:30pm" options[:during] = [range_1, range_2] - options.during.must_equal [[[9, 0, 0], [17, 0, 0]], [[19, 30, 0], [23, 30, 0]]] - options[:during].must_equal [[[9, 0, 0], [17, 0, 0]], [[19, 30, 0], [23, 30, 0]]] + _(options.during).must_equal [[[9, 0, 0], [17, 0, 0]], [[19, 30, 0], [23, 30, 0]]] + _(options[:during]).must_equal [[[9, 0, 0], [17, 0, 0]], [[19, 30, 0], [23, 30, 0]]] end it "can be set by an array time arrays" do options[:during] = [[[9, 0, 0], [17, 0, 0]], [[19, 30, 0], [23, 30, 0]]] - options.during.must_equal [[[9, 0, 0], [17, 0, 0]], [[19, 30, 0], [23, 30, 0]]] - options[:during].must_equal [[[9, 0, 0], [17, 0, 0]], [[19, 30, 0], [23, 30, 0]]] + _(options.during).must_equal [[[9, 0, 0], [17, 0, 0]], [[19, 30, 0], [23, 30, 0]]] + _(options[:during]).must_equal [[[9, 0, 0], [17, 0, 0]], [[19, 30, 0], [23, 30, 0]]] end it "splits args parts before and after midnight when spanning overnight" do options[:during] = "5pm - 9am" - options.during.must_equal [[[17, 0, 0], [23, 59, 59]], [[0, 0, 0], [9, 0, 0]]] - options[:during].must_equal [[[17, 0, 0], [23, 59, 59]], [[0, 0, 0], [9, 0, 0]]] + _(options.during).must_equal [[[17, 0, 0], [23, 59, 59]], [[0, 0, 0], [9, 0, 0]]] + _(options[:during]).must_equal [[[17, 0, 0], [23, 59, 59]], [[0, 0, 0], [9, 0, 0]]] end it "can be set to nil" do options[:during] = nil - options.during.must_be_nil - options[:during].must_be_nil + _(options.during).must_be_nil + _(options[:during]).must_be_nil end end @@ -717,16 +717,16 @@ end it "defaults to nil" do - options.at.must_be_nil - options[:at].must_be_nil + _(options.at).must_be_nil + _(options[:at]).must_be_nil end it "sets :at to hour, min, sec parts" do options[:at] = "3:30 PM" time = Time.parse("3:30 PM") - options.at.must_equal [[time.hour, time.min, time.sec]] - options[:at].must_equal [[time.hour, time.min, time.sec]] + _(options.at).must_equal [[time.hour, time.min, time.sec]] + _(options[:at]).must_equal [[time.hour, time.min, time.sec]] end it "accepts an array of time strings" do @@ -734,7 +734,7 @@ time_1 = Time.local(2015, 9, 1, 10, 30) time_2 = Time.local(2015, 9, 1, 15, 45) - options[:at].must_equal [[time_1.hour, time_1.min, time_1.sec], [time_2.hour, time_2.min, time_2.sec]] + _(options[:at]).must_equal [[time_1.hour, time_1.min, time_1.sec], [time_2.hour, time_2.min, time_2.sec]] end it "retains seconds info" do @@ -742,8 +742,8 @@ time = Time.parse("23:59:59") - options.at.must_equal [[time.hour, time.min, time.sec]] - options[:at].must_equal [[time.hour, time.min, time.sec]] + _(options.at).must_equal [[time.hour, time.min, time.sec]] + _(options[:at]).must_equal [[time.hour, time.min, time.sec]] end it "accepts an array of time part arrays" do @@ -751,7 +751,7 @@ time_1 = Time.local(2015, 9, 1, 10, 30) time_2 = Time.local(2015, 9, 1, 15, 45) - options[:at].must_equal [[time_1.hour, time_1.min], [time_2.hour, time_2.min]] + _(options[:at]).must_equal [[time_1.hour, time_1.min], [time_2.hour, time_2.min]] end end @@ -759,51 +759,51 @@ it "decomposes day name to wday" do options[:on] = :friday - options[:day].must_equal [5] - options[:on].must_equal :friday + _(options[:day]).must_equal [5] + _(options[:on]).must_equal :friday end it "decomposes day name => month day to wday and mday" do options[:on] = {friday: 13} - options[:day].must_equal [5] - options[:mday].must_equal [13] - options[:on].must_equal(friday: 13) + _(options[:day]).must_equal [5] + _(options[:mday]).must_equal [13] + _(options[:on]).must_equal(friday: 13) end it "decomposes day name => month day to wday and mday as range" do options[:month] = :november options[:on] = {tuesday: 2..8} - options[:day].must_equal [2] - options[:mday].must_equal((2..8).to_a) - options[:month].must_equal [11] + _(options[:day]).must_equal [2] + _(options[:mday]).must_equal((2..8).to_a) + _(options[:month]).must_equal [11] end it "decompose month name => month day to month and mday" do options[:on] = {january: 31} - options[:month].must_equal [1] - options[:mday].must_equal [31] + _(options[:month]).must_equal [1] + _(options[:mday]).must_equal [31] end - it { -> { options[:on] = -3 }.must_raise Montrose::ConfigurationError } + it { _(-> { options[:on] = -3 }).must_raise Montrose::ConfigurationError } end describe "#except" do it "defaults to nil" do - options.except.must_be_nil - options[:except].must_be_nil + _(options.except).must_be_nil + _(options[:except]).must_be_nil end it "accepts a single date" do options[:except] = "2016-03-01" - options[:except].must_equal ["2016-03-01".to_date] + _(options[:except]).must_equal ["2016-03-01".to_date] end it "accepts multiple dates" do options[:except] = [Date.today, "2016-03-01"] - options[:except].must_equal [Date.today, "2016-03-01".to_date] + _(options[:except]).must_equal [Date.today, "2016-03-01".to_date] end end @@ -815,7 +815,7 @@ end it "returns Hash with non-nil key-value pairs" do - options.to_hash.must_equal(every: :day) + _(options.to_hash).must_equal(every: :day) end end @@ -823,27 +823,27 @@ it "returns key if present" do options[:every] = :month - options.fetch(:every).must_equal :month + _(options.fetch(:every)).must_equal :month end it "returns default if given" do - options.fetch(:every, :foo).must_equal :foo + _(options.fetch(:every, :foo)).must_equal :foo end it "return nil if given as default" do - options.fetch(:every, nil).must_be_nil + _(options.fetch(:every, nil)).must_be_nil end it "calls block if not found" do - options.fetch(:every, :foo).must_equal :foo + _(options.fetch(:every, :foo)).must_equal :foo end it "raises for no block given and value not found" do - -> { options.fetch(:every) }.must_raise + _(-> { options.fetch(:every) }).must_raise end it "raises for more than two args" do - -> { options.fetch(:every, nil, nil) }.must_raise + _(-> { options.fetch(:every, nil, nil) }).must_raise end end @@ -856,6 +856,6 @@ options[:interval] = 1 end - it { options.inspect.must_equal "#:month, :starts=>#{now.inspect}, :interval=>1}>" } + it { _(options.inspect).must_equal "#:month, :starts=>#{now.inspect}, :interval=>1}>" } end end diff --git a/spec/montrose/recurrence_spec.rb b/spec/montrose/recurrence_spec.rb index bc455a1..9e45a52 100644 --- a/spec/montrose/recurrence_spec.rb +++ b/spec/montrose/recurrence_spec.rb @@ -13,7 +13,7 @@ describe "#events" do it "returns Enumerator" do recurrence = new_recurrence(every: :hour) - recurrence.events.must_be_instance_of Enumerator + _(recurrence.events).must_be_instance_of Enumerator end end @@ -26,33 +26,33 @@ times << time end - times.must_pair_with([now, 1.hour.from_now, 2.hours.from_now]) - times.size.must_equal 3 + _(times).must_pair_with([now, 1.hour.from_now, 2.hours.from_now]) + _(times.size).must_equal 3 end it "is mappable" do recurrence = new_recurrence(every: :day, total: 3) - recurrence.map(&:to_date).must_equal [Date.today, Date.tomorrow, 2.days.from_now.to_date] + _(recurrence.map(&:to_date)).must_equal [Date.today, Date.tomorrow, 2.days.from_now.to_date] end it "enumerates anew each time" do recurrence = new_recurrence(every: :day, total: 3) - recurrence.map(&:to_date).must_equal [Date.today, Date.tomorrow, 2.days.from_now.to_date] - recurrence.map(&:to_date).must_equal [Date.today, Date.tomorrow, 2.days.from_now.to_date] + _(recurrence.map(&:to_date)).must_equal [Date.today, Date.tomorrow, 2.days.from_now.to_date] + _(recurrence.map(&:to_date)).must_equal [Date.today, Date.tomorrow, 2.days.from_now.to_date] end it "returns first" do recurrence = new_recurrence(every: :day) - recurrence.first.must_equal now + _(recurrence.first).must_equal now end it "returns enumerator" do recurrence = new_recurrence(every: :day) - recurrence.each.must_be_kind_of Enumerator + _(recurrence.each).must_be_kind_of Enumerator end end @@ -62,11 +62,11 @@ recurrence = new_recurrence(options) hash = recurrence.to_hash - hash.size.must_equal 4 - hash[:every].must_equal :day - hash[:interval].must_equal 1 - hash[:total].must_equal 3 - hash[:starts].must_equal now + _(hash.size).must_equal 4 + _(hash[:every]).must_equal :day + _(hash[:interval]).must_equal 1 + _(hash[:total]).must_equal 3 + _(hash[:starts]).must_equal now end end @@ -76,11 +76,11 @@ recurrence = new_recurrence(options) hash = recurrence.as_json - hash.size.must_equal 4 - hash["every"].must_equal "day" - hash["interval"].must_equal 1 - hash["total"].must_equal 3 - hash["starts"].must_equal now.as_json + _(hash.size).must_equal 4 + _(hash["every"]).must_equal "day" + _(hash["interval"]).must_equal 1 + _(hash["total"]).must_equal 3 + _(hash["starts"]).must_equal now.as_json end end @@ -93,10 +93,10 @@ recurrence_from_yaml = new_recurrence(YAML.safe_load(yaml)) hash = recurrence_from_yaml.to_hash - hash[:every].must_equal :day - hash[:interval].must_equal 1 - hash[:total].must_equal 3 - hash[:starts].must_equal now + _(hash[:every]).must_equal :day + _(hash[:interval]).must_equal 1 + _(hash[:total]).must_equal 3 + _(hash[:starts]).must_equal now end end @@ -107,10 +107,10 @@ dump = Montrose::Recurrence.dump(recurrence) parsed = JSON.parse(dump).symbolize_keys - parsed[:every].must_equal "day" - parsed[:total].must_equal 3 - parsed[:interval].must_equal 1 - parsed[:starts].must_equal now.to_s + _(parsed[:every]).must_equal "day" + _(parsed[:total]).must_equal 3 + _(parsed[:interval]).must_equal 1 + _(parsed[:starts]).must_equal now.to_s end it "accepts json hash" do @@ -118,10 +118,10 @@ dump = Montrose::Recurrence.dump(hash) parsed = JSON.parse(dump).symbolize_keys - parsed[:every].must_equal "day" - parsed[:total].must_equal 3 - parsed[:interval].must_equal 1 - parsed[:starts].must_equal now.to_s + _(parsed[:every]).must_equal "day" + _(parsed[:total]).must_equal 3 + _(parsed[:interval]).must_equal 1 + _(parsed[:starts]).must_equal now.to_s end it "accepts json string" do @@ -130,20 +130,20 @@ dump = Montrose::Recurrence.dump(str) parsed = JSON.parse(dump).symbolize_keys - parsed[:every].must_equal "day" - parsed[:total].must_equal 3 - parsed[:interval].must_equal 1 - parsed[:starts].must_equal now.to_s + _(parsed[:every]).must_equal "day" + _(parsed[:total]).must_equal 3 + _(parsed[:interval]).must_equal 1 + _(parsed[:starts]).must_equal now.to_s end - it { Montrose::Recurrence.dump(nil).must_be_nil } + it { _(Montrose::Recurrence.dump(nil)).must_be_nil } it "raises error if str not parseable as JSON" do - -> { Montrose::Recurrence.dump("foo") }.must_raise Montrose::SerializationError + _(-> { Montrose::Recurrence.dump("foo") }).must_raise Montrose::SerializationError end it "raises error otherwise" do - -> { Montrose::Recurrence.dump(Object.new) }.must_raise Montrose::SerializationError + _(-> { Montrose::Recurrence.dump(Object.new) }).must_raise Montrose::SerializationError end end @@ -156,22 +156,22 @@ loaded = Montrose::Recurrence.load(dump) default_options = loaded.default_options - default_options[:every].must_equal :day - default_options[:total].must_equal 3 - default_options[:starts].to_i.must_equal now.to_i - default_options[:interval].must_equal 1 + _(default_options[:every]).must_equal :day + _(default_options[:total]).must_equal 3 + _(default_options[:starts].to_i).must_equal now.to_i + _(default_options[:interval]).must_equal 1 end it "returns nil for nil dump" do loaded = Montrose::Recurrence.load(nil) - loaded.must_be_nil + _(loaded).must_be_nil end it "returns nil for empty dump" do loaded = Montrose::Recurrence.load("") - loaded.must_be_nil + _(loaded).must_be_nil end end @@ -180,7 +180,7 @@ yaml = "---\nevery: day\n" recurrence = Montrose::Recurrence.from_yaml(yaml) - recurrence.default_options[:every].must_equal :day + _(recurrence.default_options[:every]).must_equal :day end end @@ -192,10 +192,10 @@ ICAL recurrence = Montrose::Recurrence.from_ical(ical) - recurrence.default_options[:every].must_equal :day - recurrence.default_options[:total].must_equal 10 - recurrence.default_options[:interval].must_equal 2 - recurrence.default_options[:starts].must_equal Time.parse("1997-09-02 09:00:00 -0400") + _(recurrence.default_options[:every]).must_equal :day + _(recurrence.default_options[:total]).must_equal 10 + _(recurrence.default_options[:interval]).must_equal 2 + _(recurrence.default_options[:starts]).must_equal Time.parse("1997-09-02 09:00:00 -0400") end end @@ -206,7 +206,7 @@ it "is readable" do inspected = "#:month, :starts=>#{now.inspect}, :interval=>1}>" - recurrence.inspect.must_equal inspected + _(recurrence.inspect).must_equal inspected end end @@ -215,7 +215,7 @@ options = {every: :day, at: "3:45pm"} recurrence = new_recurrence(options) - recurrence.to_json.must_equal "{\"every\":\"day\",\"at\":[[15,45,0]]}" + _(recurrence.to_json).must_equal "{\"every\":\"day\",\"at\":[[15,45,0]]}" end end @@ -314,7 +314,7 @@ recurrence = new_recurrence(every: interval) begin Timeout.timeout(2) do - recurrence.take(5).length.must_equal 5 + _(recurrence.take(5).length).must_equal 5 end rescue Timeout::Error assert false, "Expected recurrence for every #{interval.inspect} to return 5 results but timed out." diff --git a/spec/montrose/schedule_spec.rb b/spec/montrose/schedule_spec.rb index 5b78880..1573a1b 100644 --- a/spec/montrose/schedule_spec.rb +++ b/spec/montrose/schedule_spec.rb @@ -1,13 +1,14 @@ # frozen_string_literal: true require "spec_helper" +require "benchmark" describe Montrose::Schedule do let(:schedule) { new_schedule } describe ".build" do it "returns a new instance" do - Montrose::Schedule.build.must_be_kind_of Montrose::Schedule + _(Montrose::Schedule.build).must_be_kind_of Montrose::Schedule end it "yields a new instance" do @@ -16,7 +17,7 @@ s << {every: :year} } - schedule.rules.size.must_equal 2 + _(schedule.rules.size).must_equal 2 end end @@ -29,12 +30,12 @@ dump = Montrose::Schedule.dump(schedule) parsed = JSON.parse(dump).map(&:symbolize_keys) - parsed[0][:every].must_equal "week" - parsed[0][:on].must_equal "thursday" - parsed[0][:at].must_equal [[19, 0, 0]] - parsed[1][:every].must_equal "week" - parsed[1][:on].must_equal "tuesday" - parsed[1][:at].must_equal [[18, 0, 0]] + _(parsed[0][:every]).must_equal "week" + _(parsed[0][:on]).must_equal "thursday" + _(parsed[0][:at]).must_equal [[19, 0, 0]] + _(parsed[1][:every]).must_equal "week" + _(parsed[1][:on]).must_equal "tuesday" + _(parsed[1][:at]).must_equal [[18, 0, 0]] end it "accepts json array" do @@ -45,12 +46,12 @@ dump = Montrose::Schedule.dump(array) parsed = JSON.parse(dump).map(&:symbolize_keys) - parsed[0][:every].must_equal "week" - parsed[0][:on].must_equal "thursday" - parsed[0][:at].must_equal [[19, 0, 0]] - parsed[1][:every].must_equal "week" - parsed[1][:on].must_equal "tuesday" - parsed[1][:at].must_equal [[18, 0, 0]] + _(parsed[0][:every]).must_equal "week" + _(parsed[0][:on]).must_equal "thursday" + _(parsed[0][:at]).must_equal [[19, 0, 0]] + _(parsed[1][:every]).must_equal "week" + _(parsed[1][:on]).must_equal "tuesday" + _(parsed[1][:at]).must_equal [[18, 0, 0]] end it "accepts json string" do @@ -61,22 +62,22 @@ dump = Montrose::Schedule.dump(str) parsed = JSON.parse(dump).map(&:symbolize_keys) - parsed[0][:every].must_equal "week" - parsed[0][:on].must_equal "thursday" - parsed[0][:at].must_equal [[19, 0, 0]] - parsed[1][:every].must_equal "week" - parsed[1][:on].must_equal "tuesday" - parsed[1][:at].must_equal [[18, 0, 0]] + _(parsed[0][:every]).must_equal "week" + _(parsed[0][:on]).must_equal "thursday" + _(parsed[0][:at]).must_equal [[19, 0, 0]] + _(parsed[1][:every]).must_equal "week" + _(parsed[1][:on]).must_equal "tuesday" + _(parsed[1][:at]).must_equal [[18, 0, 0]] end - it { Montrose::Schedule.dump(nil).must_be_nil } + it { _(Montrose::Schedule.dump(nil)).must_be_nil } it "raises error if str not parseable as JSON" do - -> { Montrose::Schedule.dump("foo") }.must_raise Montrose::SerializationError + _(-> { Montrose::Schedule.dump("foo") }).must_raise Montrose::SerializationError end it "raises error otherwise" do - -> { Montrose::Schedule.dump(Object.new) }.must_raise Montrose::SerializationError + _(-> { Montrose::Schedule.dump(Object.new) }).must_raise Montrose::SerializationError end end @@ -90,24 +91,24 @@ dump = Montrose::Schedule.dump(schedule) loaded = Montrose::Schedule.load(dump).to_a - loaded[0][:every].must_equal :week - loaded[0][:on].must_equal "thursday" - loaded[0][:at].must_equal [[19, 0, 0]] - loaded[1][:every].must_equal :week - loaded[1][:on].must_equal "tuesday" - loaded[1][:at].must_equal [[18, 0, 0]] + _(loaded[0][:every]).must_equal :week + _(loaded[0][:on]).must_equal "thursday" + _(loaded[0][:at]).must_equal [[19, 0, 0]] + _(loaded[1][:every]).must_equal :week + _(loaded[1][:on]).must_equal "tuesday" + _(loaded[1][:at]).must_equal [[18, 0, 0]] end it "returns nil for nil dump" do loaded = Montrose::Schedule.load(nil) - loaded.must_be_nil + _(loaded).must_be_nil end it "returns nil for empty dump" do loaded = Montrose::Schedule.load("") - loaded.must_be_nil + _(loaded).must_be_nil end end @@ -116,32 +117,32 @@ options = {every: :year, total: 3} schedule.add(options) - schedule.rules.size.must_equal 1 + _(schedule.rules.size).must_equal 1 rule = schedule.rules.first - rule.default_options[:every].must_equal :year - rule.default_options[:total].must_equal 3 + _(rule.default_options[:every]).must_equal :year + _(rule.default_options[:total]).must_equal 3 end it "accepts a recurrence rule" do schedule.add(Montrose.yearly.total(3)) - schedule.rules.size.must_equal 1 + _(schedule.rules.size).must_equal 1 rule = schedule.rules.first - rule.default_options[:every].must_equal :year - rule.default_options[:total].must_equal 3 + _(rule.default_options[:every]).must_equal :year + _(rule.default_options[:total]).must_equal 3 end it "is aliased to #<<" do options = {every: :year, total: 3} schedule << options - schedule.rules.size.must_equal 1 + _(schedule.rules.size).must_equal 1 rule = schedule.rules.first - rule.default_options[:every].must_equal :year - rule.default_options[:total].must_equal 3 + _(rule.default_options[:every]).must_equal :year + _(rule.default_options[:total]).must_equal 3 end end @@ -153,23 +154,23 @@ schedule.add(every: 2.days, total: 2, starts: today + 1.day) events = schedule.events.to_a - events.must_pair_with [ + _(events).must_pair_with [ today, today + 1.day, today + 2.days, today + 3.days ] - events.size.must_equal 4 + _(events.size).must_equal 4 end it "is an enumerator" do - schedule.events.must_be_instance_of(Enumerator) + _(schedule.events).must_be_instance_of(Enumerator) end end describe "#each" do it "is defined" do - schedule.must_respond_to :each + _(schedule).must_respond_to :each end it "responsds to enumerable methods" do @@ -179,13 +180,13 @@ schedule.add(every: 2.days, total: 2, starts: today + 1.day) events = schedule.take(4).to_a - events.must_pair_with [ + _(events).must_pair_with [ today, today + 1.day, today + 2.days, today + 3.days ] - events.size.must_equal 4 + _(events.size).must_equal 4 end end @@ -196,7 +197,7 @@ it "is readable" do inspected = "#:month, :starts=>#{now.inspect}, :interval=>1}>" - recurrence.inspect.must_equal inspected + _(recurrence.inspect).must_equal inspected end end @@ -205,7 +206,7 @@ options = {every: :day, at: "3:45pm"} recurrence = new_recurrence(options) - recurrence.to_json.must_equal "{\"every\":\"day\",\"at\":[[15,45,0]]}" + _(recurrence.to_json).must_equal "{\"every\":\"day\",\"at\":[[15,45,0]]}" end end @@ -279,7 +280,7 @@ it "is readable" do inspected = "#:month}, {:every=>:day}]>" - schedule.inspect.must_equal inspected + _(schedule.inspect).must_equal inspected end end @@ -290,7 +291,7 @@ end it "returns json string of its options" do - schedule.to_json.must_equal "[{\"every\":\"month\"},{\"every\":\"day\"}]" + _(schedule.to_json).must_equal "[{\"every\":\"month\"},{\"every\":\"day\"}]" end end @@ -303,8 +304,8 @@ it "returns default options as array" do array = schedule.to_a - array.size.must_equal 2 - array.must_equal [{every: :month}, {every: :day}] + _(array.size).must_equal 2 + _(array).must_equal [{every: :month}, {every: :day}] end end @@ -317,8 +318,8 @@ it "returns default options as array" do array = schedule.as_json - array.size.must_equal 2 - array.must_equal [{"every" => "month"}, {"every" => "day"}] + _(array.size).must_equal 2 + _(array).must_equal [{"every" => "month"}, {"every" => "day"}] end end @@ -332,8 +333,8 @@ yaml = schedule.to_yaml array = YAML.safe_load(yaml) - array.size.must_equal 2 - array.must_equal [{"every" => "month"}, {"every" => "day"}] + _(array.size).must_equal 2 + _(array).must_equal [{"every" => "month"}, {"every" => "day"}] end end end diff --git a/spec/montrose/utils_spec.rb b/spec/montrose/utils_spec.rb index 60f110f..d4dff2a 100644 --- a/spec/montrose/utils_spec.rb +++ b/spec/montrose/utils_spec.rb @@ -14,33 +14,33 @@ describe "#as_time" do it "parses Strings" do time_string = Time.now.to_s - as_time(time_string).class.must_equal Time - as_time(time_string).must_equal Time.parse(time_string) + _(as_time(time_string).class).must_equal Time + _(as_time(time_string)).must_equal Time.parse(time_string) end it "returns unmodified ActiveSupport::TimeWithZone objects" do Time.use_zone("Beijing") do time_with_zone = Time.zone.now - time_with_zone.class.must_equal ActiveSupport::TimeWithZone - as_time(time_with_zone).must_equal time_with_zone + _(time_with_zone.class).must_equal ActiveSupport::TimeWithZone + _(as_time(time_with_zone)).must_equal time_with_zone end end it "casts to_time if available" do - as_time(Date.today).must_equal Date.today.to_time + _(as_time(Date.today)).must_equal Date.today.to_time end end describe "#parse_time" do - it { parse_time("Sept 1, 2015 12:00PM").must_equal Time.parse("Sept 1, 2015 12:00PM") } + it { _(parse_time("Sept 1, 2015 12:00PM")).must_equal Time.parse("Sept 1, 2015 12:00PM") } it "uses Time.zone if available" do Time.use_zone("Hawaii") do time = parse_time("Sept 1, 2015 12:00PM") - time.month.must_equal 9 - time.day.must_equal 1 - time.year.must_equal 2015 - time.hour.must_equal 12 - time.utc_offset.must_equal(-10.hours) + _(time.month).must_equal 9 + _(time.day).must_equal 1 + _(time.year).must_equal 2015 + _(time.hour).must_equal 12 + _(time.utc_offset).must_equal(-10.hours) end end end @@ -48,25 +48,25 @@ describe "#days_in_month" do non_leap_year = 2015 leap_year = 2016 - it { days_in_month(1).must_equal 31 } - it { days_in_month(2, non_leap_year).must_equal 28 } - it { days_in_month(2, leap_year).must_equal 29 } - it { days_in_month(3).must_equal 31 } - it { days_in_month(4).must_equal 30 } - it { days_in_month(5).must_equal 31 } - it { days_in_month(6).must_equal 30 } - it { days_in_month(7).must_equal 31 } - it { days_in_month(8).must_equal 31 } - it { days_in_month(9).must_equal 30 } - it { days_in_month(10).must_equal 31 } - it { days_in_month(11).must_equal 30 } - it { days_in_month(12).must_equal 31 } + it { _(days_in_month(1)).must_equal 31 } + it { _(days_in_month(2, non_leap_year)).must_equal 28 } + it { _(days_in_month(2, leap_year)).must_equal 29 } + it { _(days_in_month(3)).must_equal 31 } + it { _(days_in_month(4)).must_equal 30 } + it { _(days_in_month(5)).must_equal 31 } + it { _(days_in_month(6)).must_equal 30 } + it { _(days_in_month(7)).must_equal 31 } + it { _(days_in_month(8)).must_equal 31 } + it { _(days_in_month(9)).must_equal 30 } + it { _(days_in_month(10)).must_equal 31 } + it { _(days_in_month(11)).must_equal 30 } + it { _(days_in_month(12)).must_equal 31 } end describe "#days_in_year" do - it { days_in_year(2005).must_equal 365 } - it { days_in_year(2004).must_equal 366 } - it { days_in_year(2000).must_equal 366 } - it { days_in_year(1900).must_equal 365 } + it { _(days_in_year(2005)).must_equal 365 } + it { _(days_in_year(2004)).must_equal 366 } + it { _(days_in_year(2000)).must_equal 366 } + it { _(days_in_year(1900)).must_equal 365 } end end diff --git a/spec/montrose_spec.rb b/spec/montrose_spec.rb index edb1863..655b103 100644 --- a/spec/montrose_spec.rb +++ b/spec/montrose_spec.rb @@ -6,16 +6,16 @@ it { assert ::Montrose::VERSION } describe ".r" do - it { Montrose.r.must_be_kind_of(Montrose::Recurrence) } - it { Montrose.r.default_options.to_h.must_equal({}) } + it { _(Montrose.r).must_be_kind_of(Montrose::Recurrence) } + it { _(Montrose.r.default_options.to_h).must_equal({}) } - it { Montrose.r(every: :day).must_be_kind_of(Montrose::Recurrence) } - it { Montrose.r(every: :day).default_options[:every].must_equal(:day) } + it { _(Montrose.r(every: :day)).must_be_kind_of(Montrose::Recurrence) } + it { _(Montrose.r(every: :day).default_options[:every]).must_equal(:day) } - it { Montrose.recurrence.must_be_kind_of(Montrose::Recurrence) } - it { Montrose.recurrence.default_options.to_h.must_equal({}) } + it { _(Montrose.recurrence).must_be_kind_of(Montrose::Recurrence) } + it { _(Montrose.recurrence.default_options.to_h).must_equal({}) } - it { Montrose.recurrence(every: :day).must_be_kind_of(Montrose::Recurrence) } - it { Montrose.recurrence(every: :day).default_options[:every].must_equal(:day) } + it { _(Montrose.recurrence(every: :day)).must_be_kind_of(Montrose::Recurrence) } + it { _(Montrose.recurrence(every: :day).default_options[:every]).must_equal(:day) } end end diff --git a/spec/rfc_spec.rb b/spec/rfc_spec.rb index 8a01cd8..2ce7283 100644 --- a/spec/rfc_spec.rb +++ b/spec/rfc_spec.rb @@ -18,8 +18,8 @@ dates = recurrence.events.to_a - dates.must_pair_with consecutive_days(10, starts: now) - dates.size.must_equal 10 + _(dates).must_pair_with consecutive_days(10, starts: now) + _(dates.size).must_equal 10 end it "daily until December 23, 2015" do @@ -32,8 +32,8 @@ expected_dates = consecutive_days(days, starts: starts_on) dates = recurrence.events.to_a - dates.must_pair_with expected_dates - dates.size.must_equal days + _(dates).must_pair_with expected_dates + _(dates.size).must_equal days end it "every other day forever" do @@ -42,7 +42,7 @@ expected_dates = consecutive_days(5, interval: 2) dates = recurrence.events.take(5) - dates.must_pair_with expected_dates + _(dates).must_pair_with expected_dates end it "every 10 days 5 occurrences" do @@ -51,8 +51,8 @@ expected_dates = consecutive_days(5, interval: 10) dates = recurrence.events.to_a - dates.must_pair_with expected_dates - dates.size.must_equal 5 + _(dates).must_pair_with expected_dates + _(dates.size).must_equal 5 end describe "everyday in January for 3 years" do @@ -74,8 +74,8 @@ dates = recurrence.events.to_a - dates.must_pair_with expected_dates - dates.size.must_equal 31 * 3 + _(dates).must_pair_with expected_dates + _(dates.size).must_equal 31 * 3 end it "yearly" do @@ -88,8 +88,8 @@ dates = recurrence.events.to_a - dates.must_pair_with expected_dates - dates.size.must_equal 31 * 3 + _(dates).must_pair_with expected_dates + _(dates.size).must_equal 31 * 3 end end @@ -99,7 +99,7 @@ expected_dates = consecutive(:weeks, 10) dates = recurrence.events.take(10) - dates.must_pair_with expected_dates + _(dates).must_pair_with expected_dates end it "weekly until December 23, 2015" do @@ -111,8 +111,8 @@ expected_dates = consecutive(:weeks, 15, starts: starts_on) dates = recurrence.events.to_a - dates.must_pair_with expected_dates - dates.size.must_equal 15 + _(dates).must_pair_with expected_dates + _(dates.size).must_equal 15 end it "every other week forever" do @@ -121,7 +121,7 @@ expected_dates = consecutive(:weeks, 5, interval: 2) dates = recurrence.events.take(5) - dates.must_pair_with expected_dates + _(dates).must_pair_with expected_dates end describe "weekly on Tuesday and Thursday for five weeks" do @@ -136,8 +136,8 @@ expected_dates = cherry_pick 2015 => {9 => [1, 3, 8, 10, 15, 17, 22, 24, 29], 10 => [1]} dates = recurrence.events.to_a - dates.must_pair_with expected_dates - dates.size.must_equal 10 + _(dates).must_pair_with expected_dates + _(dates.size).must_equal 10 end it "by count" do @@ -151,8 +151,8 @@ expected_dates = cherry_pick 2015 => {11 => [24, 26], 12 => [1, 3, 8, 10, 15, 17, 22, 24]} dates = recurrence.events.to_a - dates.must_pair_with expected_dates - dates.size.must_equal 10 + _(dates).must_pair_with expected_dates + _(dates.size).must_equal 10 end end @@ -177,8 +177,8 @@ } dates = recurrence.events.to_a - dates.must_pair_with expected_dates - dates.size.must_equal expected_dates.size + _(dates).must_pair_with expected_dates + _(dates.size).must_equal expected_dates.size end it "every other week on Tuesday and Thursday, for 8 occurrences" do @@ -195,8 +195,8 @@ dates = recurrence.events.to_a - dates.must_pair_with expected_dates - dates.size.must_equal expected_dates.size + _(dates).must_pair_with expected_dates + _(dates.size).must_equal expected_dates.size end it "monthly on the first Friday for ten occurrences" do @@ -213,8 +213,8 @@ dates = recurrence.events.to_a - dates.must_pair_with expected_dates - dates.size.must_equal expected_dates.size + _(dates).must_pair_with expected_dates + _(dates.size).must_equal expected_dates.size end it "monthly on the first Friday until December 23, 2015" do @@ -230,8 +230,8 @@ dates = recurrence.events.to_a - dates.must_pair_with expected_dates - dates.size.must_equal expected_dates.size + _(dates).must_pair_with expected_dates + _(dates.size).must_equal expected_dates.size end it "every other month on the first and last Sunday of the month for 10 occurrences" do @@ -253,8 +253,8 @@ dates = recurrence.events.to_a - dates.must_pair_with expected_dates - dates.size.must_equal expected_dates.size + _(dates).must_pair_with expected_dates + _(dates.size).must_equal expected_dates.size end it "monthly on the second-to-last Monday of the month for 6 months" do @@ -271,8 +271,8 @@ dates = recurrence.events.to_a - dates.must_pair_with expected_dates - dates.size.must_equal expected_dates.size + _(dates).must_pair_with expected_dates + _(dates.size).must_equal expected_dates.size end it "monthly on the third-to-the-last day of the month, forever" do @@ -285,8 +285,8 @@ dates = recurrence.events.take(6) - dates.must_pair_with expected_dates - dates.size.must_equal expected_dates.size + _(dates).must_pair_with expected_dates + _(dates.size).must_equal expected_dates.size end it "monthly on the 2nd and 15th of the month for 10 occurrences" do @@ -299,8 +299,8 @@ dates = recurrence.events.to_a - dates.must_pair_with expected_dates - dates.size.must_equal expected_dates.size + _(dates).must_pair_with expected_dates + _(dates.size).must_equal expected_dates.size end it "monthly on the first and last day of the month for 10 occurrences" do @@ -314,8 +314,8 @@ dates = recurrence.events.to_a - dates.must_pair_with expected_dates - dates.size.must_equal expected_dates.size + _(dates).must_pair_with expected_dates + _(dates.size).must_equal expected_dates.size end it "every 18 months on the 10th thru 15th of the month for 10 occurrences" do @@ -329,8 +329,8 @@ dates = recurrence.events.to_a - dates.must_pair_with expected_dates - dates.size.must_equal expected_dates.size + _(dates).must_pair_with expected_dates + _(dates.size).must_equal expected_dates.size end it "every Tuesday, every other month" do @@ -344,7 +344,7 @@ dates = recurrence.events.take(expected_dates.size) - dates.must_pair_with expected_dates + _(dates).must_pair_with expected_dates end it "yearly in June and July for 10 occurrences" do @@ -360,8 +360,8 @@ dates = recurrence.events.to_a - dates.must_pair_with expected_dates - dates.size.must_equal 10 + _(dates).must_pair_with expected_dates + _(dates.size).must_equal 10 end it "every other year on January, February, and March for 10 occurrences" do @@ -383,8 +383,8 @@ dates = recurrence.events.to_a - dates.must_pair_with expected_dates - dates.size.must_equal 10 + _(dates).must_pair_with expected_dates + _(dates.size).must_equal 10 end it "every third year on the 1st, 100th and 200th day for 10 occurrences" do @@ -403,8 +403,8 @@ dates = recurrence.events.to_a - dates.must_pair_with expected_dates - dates.size.must_equal 10 + _(dates).must_pair_with expected_dates + _(dates.size).must_equal 10 end it "every 20th Monday of the year, forever" do @@ -417,7 +417,7 @@ ).map { |i| i + 12.hours } dates = recurrence.events.take(3) - dates.must_pair_with expected_dates + _(dates).must_pair_with expected_dates end it "Monday of week number 20 forever" do @@ -431,7 +431,7 @@ dates = recurrence.events.take(3) - dates.must_pair_with expected_dates + _(dates).must_pair_with expected_dates end it "every Thursday in March, forever" do @@ -445,7 +445,7 @@ ).map { |i| i + 12.hours } dates = recurrence.events.take(expected_dates.size) - dates.must_pair_with expected_dates + _(dates).must_pair_with expected_dates end it "every Thursday, but only during June, July, and August, forever" do @@ -459,7 +459,7 @@ dates = recurrence.events.take(expected_dates.size) - dates.must_pair_with expected_dates + _(dates).must_pair_with expected_dates end it "every Friday 13th, forever" do @@ -474,7 +474,7 @@ dates = recurrence.events.take(expected_dates.size) - dates.must_pair_with expected_dates + _(dates).must_pair_with expected_dates end it "first Saturday that follows the first Sunday of the month, forever" do @@ -487,7 +487,7 @@ dates = recurrence.events.take(expected_dates.size) - dates.must_pair_with expected_dates + _(dates).must_pair_with expected_dates end it "every four years, the first Tuesday after a Monday in November, forever (U.S. Presidential Election day)" do @@ -502,7 +502,7 @@ dates = recurrence.events.take(expected_dates.size) - dates.must_pair_with expected_dates + _(dates).must_pair_with expected_dates end # TODO: Support set position @@ -539,8 +539,8 @@ dates = recurrence.events.to_a - dates.must_pair_with expected_dates - dates.size.must_equal expected_dates.size + _(dates).must_pair_with expected_dates + _(dates.size).must_equal expected_dates.size end it "every 15 minutes for 6 occurrences" do @@ -550,7 +550,7 @@ dates = recurrence.events.to_a - dates.must_pair_with expected_dates + _(dates).must_pair_with expected_dates end it "every hour and a half for four occurrences" do @@ -560,7 +560,7 @@ dates = recurrence.events.to_a - dates.must_pair_with expected_dates + _(dates).must_pair_with expected_dates end it "every 20 minutes from 9:00 AM to 4:40 PM every day" do @@ -573,7 +573,7 @@ dates = recurrence.events.take(72) - dates.must_pair_with expected_dates + _(dates).must_pair_with expected_dates end it "every 20 minutes from 9:00 AM to 4:40 PM every day (alt)" do @@ -586,7 +586,7 @@ dates = recurrence.events.take(72) - dates.must_pair_with expected_dates + _(dates).must_pair_with expected_dates end # TODO: Support week start on Monday