diff --git a/lib/montrose.rb b/lib/montrose.rb index 68803e3..0c1c741 100644 --- a/lib/montrose.rb +++ b/lib/montrose.rb @@ -21,6 +21,14 @@ 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" + autoload :YearDay, "montrose/year_day" + autoload :Week, "montrose/week" + extend Chainable class << self diff --git a/lib/montrose/day.rb b/lib/montrose/day.rb new file mode 100644 index 0000000..f3b25f8 --- /dev/null +++ b/lib/montrose/day.rb @@ -0,0 +1,83 @@ +module Montrose + class Day + extend Montrose::Utils + + NAMES = ::Date::DAYNAMES + 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 } + + ICAL_MATCH = /(?[+-]?\d+)?(?[A-Z]{2})/ # e.g. 1FR + + 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) { |(k, v), hash| + index = number!(k) + hash[index] = hash[index] + [*v] + } + 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 + + 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/frequency.rb b/lib/montrose/frequency.rb index dcafe58..6444cb3 100644 --- a/lib/montrose/frequency.rb +++ b/lib/montrose/frequency.rb @@ -25,24 +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}" + } + + 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 + + # @private + def assert(frequency) + FREQUENCY_TERMS.key?(frequency.to_s) || fail(ConfigurationError, + "Don't know how to enumerate every: #{frequency}") + + frequency.to_sym + end - # @private - def self.assert(frequency) - FREQUENCY_TERMS.key?(frequency.to_s) || fail(ConfigurationError, - "Don't know how to enumerate every: #{frequency}") + # @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 - frequency.to_sym + # @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 new file mode 100644 index 0000000..81b7e4f --- /dev/null +++ b/lib/montrose/ical.rb @@ -0,0 +1,100 @@ +# 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 + dtstart, rrule = @ical.split("RRULE:") + dtstart, exdate = dtstart.split("\n") + _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 = 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? + + _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("=") + case prop + when "FREQ" + [:every, Montrose::Frequency.from_term(value)] + when "INTERVAL" + [:interval, value.to_i] + when "COUNT" + [:total, value.to_i] + when "UNTIL" + [:until, parse_ical_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" + [:day, Montrose::Day.parse(value)] + when "BYMONTHDAY" + [:mday, Montrose::MonthDay.parse(value)] + when "BYYEARDAY" + [:yday, Montrose::YearDay.parse(value)] + when "BYWEEKNO" + [:week, Montrose::Week.parse(value)] + when "WKST" + [:week_start, value] + when "BYSETPOS" + warn "BYSETPOS not currently supported!" + else + raise "Unrecognized rrule '#{rule}'" + end + end + end + end +end 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/month.rb b/lib/montrose/month.rb new file mode 100644 index 0000000..c4fbe45 --- /dev/null +++ b/lib/montrose/month.rb @@ -0,0 +1,47 @@ +module Montrose + 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) + + 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 names + NAMES + end + + 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 + + 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/month_day.rb b/lib/montrose/month_day.rb new file mode 100644 index 0000000..d808ff4 --- /dev/null +++ b/lib/montrose/month_day.rb @@ -0,0 +1,25 @@ +module Montrose + class MonthDay + class << self + MDAYS = (-31.upto(-1).to_a + 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| 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 fd48e4e..7d35b37 100644 --- a/lib/montrose/options.rb +++ b/lib/montrose/options.rb @@ -90,11 +90,13 @@ def default_options def_option :between def_option :covering def_option :during + def_option :minute def_option :hour def_option :day def_option :mday def_option :yday def_option :week + def_option :week_start def_option :month def_option :interval def_option :total @@ -115,6 +117,7 @@ def initialize(opts = {}) week: nil, month: nil, total: nil, + week_start: nil, exclude_end: nil } @@ -179,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 @@ -198,23 +205,23 @@ def during=(during_arg) end def day=(days) - @day = nested_map_arg(days) { |d| day_number!(d) } + @day = Day.parse(days) end def mday=(mdays) - @mday = map_mdays(mdays) + @mday = MonthDay.parse(mdays) end def yday=(ydays) - @yday = map_ydays(ydays) + @yday = YearDay.parse(ydays) end def week=(weeks) - @week = map_arg(weeks) { |w| assert_week(w) } + @week = Week.parse(weeks) end def month=(months) - @month = map_arg(months) { |d| month_number!(d) } + @month = Month.parse(months) end def between=(range) @@ -267,51 +274,16 @@ 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 Array(arg).map(&block) end - def map_days(arg) - map_arg(arg) { |d| day_number!(d) } - 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 - 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 - def decompose_on_arg(arg) case arg when Hash @@ -319,18 +291,18 @@ 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: map_days(arg)} + {day: Montrose::Day.parse(arg)} end end def month_or_day(key) - month = month_number(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/recurrence.rb b/lib/montrose/recurrence.rb index 74f620c..5866595 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 @@ -250,6 +251,16 @@ def load(json) rescue JSON::ParserError => e fail SerializationError, "Could not parse JSON: #{e}" end + + alias_method :from_json, :load + + def from_yaml(yaml) + new(YAML.safe_load(yaml)) + end + + def from_ical(ical) + new(Montrose::ICal.parse(ical)) + end end def initialize(opts = {}) @@ -331,7 +342,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/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/lib/montrose/utils.rb b/lib/montrose/utils.rb index 28e9dcf..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,40 +40,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 - 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/lib/montrose/week.rb b/lib/montrose/week.rb new file mode 100644 index 0000000..05f5580 --- /dev/null +++ b/lib/montrose/week.rb @@ -0,0 +1,20 @@ +module Montrose + class Week + class << self + NUMBERS = (-53.upto(-1).to_a + 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/lib/montrose/year_day.rb b/lib/montrose/year_day.rb new file mode 100644 index 0000000..ccace53 --- /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 new file mode 100644 index 0000000..f28972b --- /dev/null +++ b/spec/from_ical_rfc_spec.rb @@ -0,0 +1,857 @@ +require "spec_helper" + +# 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 + + 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 + ical = <<~ICAL + DTSTART;TZID=America/New_York:19970902T090000 + RRULE:FREQ=DAILY;COUNT=10" + ICAL + # ==> (1997 9:00 AM EDT) September 2-11 + + 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 + + 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 + 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) + + 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 + + it "every other day forever" do + ical = <<~ICAL + DTSTART;TZID=America/New_York:19970902T090000 + RRULE:FREQ=DAILY;INTERVAL=2 + ICAL + # ==> (1997 9:00 AM EDT) September 2,4,6,8,10.. + + recurrence = Montrose::Recurrence.from_ical(ical) + 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 + + it "every 10 days, 5 occurrences" do + ical = <<~ICAL + 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) + expected_events = parse_expected_events( + "1997 9:00 AM EDT" => {"Sep" => [2, 12, 22], + "Oct" => [2, 12]} + ) + + _(recurrence).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).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 + # ==> (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) + + expected_events = 1998.upto(2000) + .flat_map { |yyyy| + 1.upto(31).map { |dd| + Time.parse("Jan #{dd} 09:00:00 EST #{yyyy}") + } + } + + _(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 + # ==> (1997 9:00 AM EDT) September 2,9,16,23,30;October 7,14,21 + # (1997 9:00 AM EST) October 28;November 4 + + 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 + + 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 = 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 + + 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 = 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 + + 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 = 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 "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 + + 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 + + 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 + + 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 + + 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]} + ) + + recurrence = Montrose::Recurrence.from_ical(ical) + _(recurrence).must_pair_with expected_events + end + + 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]} + ) + + 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 + + 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]} + ) + + recurrence = Montrose::Recurrence.from_ical(ical) + _(recurrence).must_pair_with expected_events + end + + 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]} + ) + + recurrence = Montrose::Recurrence.from_ical(ical) + _(recurrence).must_pair_with expected_events + end + + 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]} + ) + + recurrence = Montrose::Recurrence.from_ical(ical) + _(recurrence).must_pair_with expected_events + end + + 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]} + ) + + recurrence = Montrose::Recurrence.from_ical(ical) + _(recurrence).must_pair_with expected_events + end + + 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]} + ) + + recurrence = Montrose::Recurrence.from_ical(ical) + _(recurrence).must_pair_with expected_events + end + + 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 + + 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]} + ) + + recurrence = Montrose::Recurrence.from_ical(ical) + _(recurrence).must_pair_with expected_events + end + + 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 + # ... + + 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]} + ) + + recurrence = Montrose::Recurrence.from_ical(ical) + _(recurrence).must_pair_with expected_events + end + + 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]} + ) + + recurrence = Montrose::Recurrence.from_ical(ical) + _(recurrence).must_pair_with expected_events + end + + 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]} + ) + + recurrence = Montrose::Recurrence.from_ical(ical) + _(recurrence).must_pair_with expected_events + end + + 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 + # ... + + 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]} + ) + + recurrence = Montrose::Recurrence.from_ical(ical) + _(recurrence).must_pair_with expected_events + _(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 + 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... + # ... + + 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]} + ) + + recurrence = Montrose::Recurrence.from_ical(ical) + _(recurrence).must_pair_with expected_events + end + + 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]} + ) + + recurrence = Montrose::Recurrence.from_ical(ical) + _(recurrence).must_pair_with expected_events + end + + # 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: + + # 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 + # ... + + 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_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 + + it "every 15 minutes for 6 occurrences" do + ical = <<~ICAL + DTSTART;TZID=America/New_York:19970902T090000 + RRULE:FREQ=MINUTELY;INTERVAL=15;COUNT=6 + 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( + "Sept 2 1997" => %w[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 + + 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 + + # ==> (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] + ) + + recurrence = Montrose::Recurrence.from_ical(ical) + _(recurrence).must_pair_with expected_events + end + + 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] + ) + + recurrence = Montrose::Recurrence.from_ical(ical) + _(recurrence).must_pair_with expected_events + end + + 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: + + # 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 + + 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]} + ) + + recurrence = Montrose::Recurrence.from_ical(ical) + _(recurrence).must_pair_with expected_events + end +end 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/day_spec.rb b/spec/montrose/day_spec.rb new file mode 100644 index 0000000..b2c1fd7 --- /dev/null +++ b/spec/montrose/day_spec.rb @@ -0,0 +1,73 @@ +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 } + 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/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/month_spec.rb b/spec/montrose/month_spec.rb new file mode 100644 index 0000000..929712a --- /dev/null +++ b/spec/montrose/month_spec.rb @@ -0,0 +1,64 @@ +# 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/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 b877407..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,46 @@ 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 + + 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 ".from_ical" do + it "returns Recurrence instance" do + 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 + _(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 @@ -182,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 @@ -191,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 @@ -290,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 eec089b..d4dff2a 100644 --- a/spec/montrose/utils_spec.rb +++ b/spec/montrose/utils_spec.rb @@ -14,148 +14,59 @@ 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 - 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 } - 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 - 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