Skip to content

Commit

Permalink
Fixes fnando#21 Allowing multiple month days
Browse files Browse the repository at this point in the history
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
  • Loading branch information
thomas07vt committed May 18, 2017
1 parent 7a8159c commit 21f3f27
Show file tree
Hide file tree
Showing 7 changed files with 153 additions and 70 deletions.
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions lib/recurrence/event.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
78 changes: 9 additions & 69 deletions lib/recurrence/event/monthly.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
59 changes: 59 additions & 0 deletions lib/recurrence/event/monthly/monthday.rb
Original file line number Diff line number Diff line change
@@ -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

53 changes: 53 additions & 0 deletions lib/recurrence/event/monthly/weekday.rb
Original file line number Diff line number Diff line change
@@ -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
1 change: 1 addition & 0 deletions lib/recurrence/namespace.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
26 changes: 26 additions & 0 deletions test/recurrence/monthly_recurring/day_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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

0 comments on commit 21f3f27

Please sign in to comment.