diff --git a/README.md b/README.md index 123953a..201e9e6 100644 --- a/README.md +++ b/README.md @@ -35,6 +35,7 @@ r = Recurrence.weekly(on: :thursday) # Monthly by month day r = Recurrence.new(every: :month, on: 15) r = Recurrence.new(every: :month, on: 31) +r = Recurrence.new(:every => :month, :on => [15, 31]) r = Recurrence.new(every: :month, on: 7, interval: 2) r = Recurrence.new(every: :month, on: 7, interval: :monthly) r = Recurrence.new(every: :month, on: 7, interval: :bimonthly) @@ -79,6 +80,7 @@ r = Recurrence.new(every: :day, except: [Date.current, '2010-01-31']) r = Recurrence.new(every: :month, on: 1, handler: Proc.new { |day, month, year| raise("Date not allowed!") if year == 2011 && month == 12 && day == 31 }) # Shift the recurrences to maintain dates around boundaries (Jan 31 -> Feb 28 -> Mar 28) +# Shift cannot be used with multiple month days e.g: :on => [1,15] r = Recurrence.new(every: :month, on: 31, shift: true) # Getting an array with all events diff --git a/lib/recurrence/event.rb b/lib/recurrence/event.rb index b5c831e..7284871 100644 --- a/lib/recurrence/event.rb +++ b/lib/recurrence/event.rb @@ -5,6 +5,8 @@ module Event # :nodoc: all require "recurrence/event/base" require "recurrence/event/daily" require "recurrence/event/monthly" + require "recurrence/event/monthly/monthday" + require "recurrence/event/monthly/weekday" require "recurrence/event/weekly" require "recurrence/event/yearly" end diff --git a/lib/recurrence/event/monthly.rb b/lib/recurrence/event/monthly.rb index 63695f5..6c86310 100644 --- a/lib/recurrence/event/monthly.rb +++ b/lib/recurrence/event/monthly.rb @@ -12,27 +12,12 @@ class Monthly < Base # :nodoc: all private def validate if @options.key?(:weekday) - - # Allow :on => :last, :weekday => :thursday contruction. - if @options[:on].to_s == "last" - @options[:on] = 5 - elsif @options[:on].is_a?(Numeric) - valid_week?(@options[:on]) - else - valid_ordinal?(@options[:on]) - @options[:on] = ORDINALS.index(@options[:on].to_s) + 1 - end - - @options[:weekday] = - valid_weekday_or_weekday_name?(@options[:weekday]) + extend Weekday else - valid_month_day?(@options[:on]) + extend Monthday end - return unless @options[:interval].is_a?(Symbol) - - valid_interval?(@options[:interval]) - @options[:interval] = INTERVALS[@options[:interval]] + validate_and_prepare! end private def next_in_recurrence @@ -100,8 +85,6 @@ class Monthly < Base # :nodoc: all weeks = (date.day - 1).div(7) + 1 date -= weeks * 7 end - - @options[:handler].call(date.day, date.month, date.year) end private def shift_to(date) diff --git a/lib/recurrence/event/monthly/monthday.rb b/lib/recurrence/event/monthly/monthday.rb new file mode 100644 index 0000000..97229d0 --- /dev/null +++ b/lib/recurrence/event/monthly/monthday.rb @@ -0,0 +1,59 @@ +class Recurrence_ + module Event + class Monthly < Base + module Monthday + def advance(date, interval=@options[:interval]) + if initialized? && @_day_count > @_day_pointer += 1 + @options[:handler].call( + @options[:on][@_day_pointer], + date.month, + date.year + ) + else + @_day_pointer = 0 + + # Have a raw month from 0 to 11 interval + raw_month = date.month + interval - 1 + + next_year = date.year + raw_month.div(12) + next_month = (raw_month % 12) + 1 # change back to ruby interval + + @options[:handler].call( + @options[:on][@_day_pointer], + next_month, + next_year + ) + end + end + + def validate_and_prepare! + @options[:on] = Array.wrap(@options[:on]).map do |day| + valid_month_day?(day) + day + end.sort + + valid_shift_options? + + if @options[:interval].kind_of?(Symbol) + valid_interval?(@options[:interval]) + @options[:interval] = INTERVALS[@options[:interval]] + end + + @_day_pointer = 0 + @_day_count = @options[:on].length + end + + def valid_shift_options? + if @options[:shift] && @options[:on].length > 1 + raise ArgumentError, "Invalid options. Unable to use :shift with multiple :on days" + end + end + + def shift_to(date) + @options[:on][0] = date.day + end + end + end + end +end + diff --git a/lib/recurrence/event/monthly/weekday.rb b/lib/recurrence/event/monthly/weekday.rb new file mode 100644 index 0000000..3662140 --- /dev/null +++ b/lib/recurrence/event/monthly/weekday.rb @@ -0,0 +1,53 @@ +class Recurrence_ + module Event + class Monthly < Base + module Weekday + def advance(date, interval=@options[:interval]) + raw_month = date.month + interval - 1 + next_year = date.year + raw_month.div(12) + next_month = (raw_month % 12) + 1 # change back to ruby interval + date = Date.new(next_year, next_month, 1) + + weekday, month = @options[:weekday], date.month + + # Adjust week day + to_add = weekday - date.wday + to_add += 7 if to_add < 0 + to_add += (@options[:on] - 1) * 7 + date += to_add + + # Go to the previous month if we lost it + if date.month != month + weeks = (date.day - 1).div(7) + 1 + date -= weeks * 7 + end + + @options[:handler].call(date.day, date.month, date.year) + end + + def validate_and_prepare! + # Allow :on => :last, :weekday => :thursday contruction. + if @options[:on].to_s == "last" + @options[:on] = 5 + elsif @options[:on].kind_of?(Numeric) + valid_week?(@options[:on]) + else + valid_ordinal?(@options[:on]) + @options[:on] = Monthly::ORDINALS.index(@options[:on].to_s) + 1 + end + + @options[:weekday] = valid_weekday_or_weekday_name?(@options[:weekday]) + + if @options[:interval].kind_of?(Symbol) + valid_interval?(@options[:interval]) + @options[:interval] = INTERVALS[@options[:interval]] + end + end + + def shift_to(date) + @options[:on] = date.day + end + end + end + end +end diff --git a/lib/recurrence/namespace.rb b/lib/recurrence/namespace.rb index 463d81e..65234ab 100644 --- a/lib/recurrence/namespace.rb +++ b/lib/recurrence/namespace.rb @@ -100,6 +100,7 @@ def self.weekly(options = {}) # Create a monthly recurrence. # # Recurrence.monthly(on: 15) #=> every 15th day + # Recurrence.monthly(:on => [1, 15]) #=> every 1st and 15th day # Recurrence.monthly(on: :first, :weekday => :sunday) # Recurrence.monthly(on: :second, :weekday => :sunday) # Recurrence.monthly(on: :third, :weekday => :sunday) diff --git a/test/recurrence/monthly_recurring/day_test.rb b/test/recurrence/monthly_recurring/day_test.rb index c941dc3..e2ee841 100644 --- a/test/recurrence/monthly_recurring/day_test.rb +++ b/test/recurrence/monthly_recurring/day_test.rb @@ -144,4 +144,30 @@ class MonthlyRecurringDayTest < Minitest::Test assert r.events.include?(7.months.from_now.to_date) refute r.events.include?(8.months.from_now.to_date) end + + test "allows multiple days" do + r = recurrence( + every: :month, + on: [1, 15], + starts: "2017-05-01", + until: "2017-06-30" + ) + assert_equal "2017-05-01", r.events[0].to_s + assert_equal "2017-05-15", r.events[1].to_s + assert_equal "2017-06-01", r.events[2].to_s + assert_equal "2017-06-15", r.events[3].to_s + assert_nil r.events[4] + end + + test "raises when :shift is true and :on is multiple days" do + assert_raises(ArgumentError) { + recurrence( + every: :month, + on: [1, 15], + starts: "2017-05-01", + until: "2017-06-30", + shift: true + ) + } + end end