From 21f3f27f4553c94ce0b547242765f57fedd50e10 Mon Sep 17 00:00:00 2001 From: John Thomas Date: Thu, 18 May 2017 10:17:35 -0700 Subject: [PATCH] Fixes #21 Allowing multiple month days This allows multiple month days to be used in the :on option ``` Recurrence.new(:every => :month, :on => [15, 31]) ``` - Separating monthday and weekday logic into modules to make it easier to understand the flow - Adding tests - Shift is disallowed for multiple :on days --- README.md | 4 +- lib/recurrence/event.rb | 2 + lib/recurrence/event/monthly.rb | 78 +++---------------- lib/recurrence/event/monthly/monthday.rb | 59 ++++++++++++++ lib/recurrence/event/monthly/weekday.rb | 53 +++++++++++++ lib/recurrence/namespace.rb | 1 + test/recurrence/monthly_recurring/day_test.rb | 26 +++++++ 7 files changed, 153 insertions(+), 70 deletions(-) create mode 100644 lib/recurrence/event/monthly/monthday.rb create mode 100644 lib/recurrence/event/monthly/weekday.rb diff --git a/README.md b/README.md index 3f7b874..ed8a302 100644 --- a/README.md +++ b/README.md @@ -32,9 +32,10 @@ r = Recurrence.new(:every => :week, :on => :friday, :interval => 2) r = Recurrence.new(:every => :week, :on => :friday, :repeat => 4) r = Recurrence.weekly(:on => :thursday) -# Monthly by month day +# Monthly by month day(s) 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.today, '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 d10de49..6479f7e 100644 --- a/lib/recurrence/event.rb +++ b/lib/recurrence/event.rb @@ -3,6 +3,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 bb4e6ee..062497e 100644 --- a/lib/recurrence/event/monthly.rb +++ b/lib/recurrence/event/monthly.rb @@ -12,82 +12,22 @@ class Monthly < Base # :nodoc: all def validate if @options.key?(:weekday) - - # 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] = 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 - if @options[:interval].kind_of?(Symbol) - valid_interval?(@options[:interval]) - @options[:interval] = INTERVALS[@options[:interval]] - end + validate_and_prepare! end def next_in_recurrence - return next_month if self.respond_to?(:next_month) - type = @options.key?(:weekday) ? :weekday : :monthday - - class_eval <<-METHOD - def next_month - if initialized? - advance_to_month_by_#{type}(@date) - else - new_date = advance_to_month_by_#{type}(@date, 0) - new_date = advance_to_month_by_#{type}(new_date) if @date > new_date - new_date - end - end - METHOD - - next_month - end - - def advance_to_month_by_monthday(date, interval=@options[:interval]) - # 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], next_month, next_year) - end - - def advance_to_month_by_weekday(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 + if initialized? + advance(@date) + else + new_date = advance(@date, 0) + new_date = advance(new_date) if @date > new_date + new_date end - - @options[:handler].call(date.day, date.month, date.year) - end - - def shift_to(date) - @options[:on] = date.day end private 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 c260d29..aa41856 100644 --- a/lib/recurrence/namespace.rb +++ b/lib/recurrence/namespace.rb @@ -94,6 +94,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 bfcfa0a..c805dcb 100644 --- a/test/recurrence/monthly_recurring/day_test.rb +++ b/test/recurrence/monthly_recurring/day_test.rb @@ -141,4 +141,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