move recurrence text helpers into patterns. move next_date calculation into

respective models
This commit is contained in:
Reinier Balt 2014-02-27 16:01:01 +01:00
parent d8507bf8b7
commit b84adfc172
13 changed files with 291 additions and 289 deletions

View file

@ -40,14 +40,6 @@ class RecurringTodo < ActiveRecord::Base
end
end
def pattern
if valid_period?
@pattern = eval("RecurringTodos::#{recurring_period.capitalize}RepeatPattern.new(user)")
@pattern.build_from_recurring_todo(self)
end
@pattern
end
def valid_period?
%W[daily weekly monthly yearly].include?(recurring_period)
end
@ -78,16 +70,12 @@ class RecurringTodo < ActiveRecord::Base
# choosing between both options is done on recurrence_selector where 0 is
# for first type and 1 for second type
def switch_week_day(day, position)
self.every_day = ' ' if self.every_day.nil?
self.every_day = every_day[0, position] + day + every_day[position+1, every_day.length]
end
{ monday: 1, tuesday: 2, wednesday: 3, thursday: 4, friday: 5, saturday: 6, sunday: 0 }.each do |day, number|
define_method("weekly_return_#{day}=") do |selector|
switch_week_day(selector, number) if recurring_period=='weekly'
def pattern
if valid_period?
@pattern = eval("RecurringTodos::#{recurring_period.capitalize}RepeatPattern.new(user)")
@pattern.build_from_recurring_todo(self)
end
@pattern
end
def recurrence_pattern
@ -103,214 +91,11 @@ class RecurringTodo < ActiveRecord::Base
end
def get_due_date(previous)
case self.target
when 'due_date'
get_next_date(previous)
when 'show_from_date'
# so leave due date empty
nil
else
raise Exception.new, "unexpected value of recurrence target '#{self.target}'"
end
pattern.get_due_date(previous)
end
def get_show_from_date(previous)
case self.target
when 'due_date'
# so set show from date relative to due date unless show_always is true or show_from_delta is nil
(self.show_always? || self.show_from_delta.nil?) ? nil : get_due_date(previous) - self.show_from_delta.days
when 'show_from_date'
# Leave due date empty
get_next_date(previous)
else
raise Exception.new, "unexpected value of recurrence target '#{self.target}'"
end
end
def get_next_date(previous)
case self.recurring_period
when 'daily' then get_daily_date(previous)
when 'weekly' then get_weekly_date(previous)
when 'monthly' then get_monthly_date(previous)
when 'yearly' then get_yearly_date(previous)
else
raise Exception.new, "unknown recurrence pattern: '#{self.recurring_period}'"
end
end
def get_daily_date(previous)
# previous is the due date of the previous todo or it is the completed_at
# date when the completed_at date is after due_date (i.e. you did not make
# the due date in time)
#
# assumes self.recurring_period == 'daily'
start = determine_start(previous, 1.day)
if self.only_work_days
return start + 2.day if start.wday() == 6 # saturday
return start + 1.day if start.wday() == 0 # sunday
return start
else # every nth day; n = every_other1
# if there was no previous todo, do not add n: the first todo starts on
# today or on start_from
return previous == nil ? start : start+every_other1.day-1.day
end
end
def get_weekly_date(previous)
# determine start
if previous == nil
start = self.start_from.nil? ? Time.zone.now : self.start_from
else
start = previous + 1.day
if start.wday() == 0
# we went to a new week , go to the nth next week and find first match
# that week. Note that we already went into the next week, so -1
start += (self.every_other1-1).week
end
unless self.start_from.nil?
# check if the start_from date is later than previous. If so, use
# start_from as start to search for next date
start = self.start_from if self.start_from > previous
end
end
day = find_first_day_in_this_week(start)
return day unless day == -1
# we did not find anything this week, so check the nth next, starting from
# sunday
start = start + self.every_other1.week - (start.wday()).days
start = find_first_day_in_this_week(start)
return start unless start == -1
raise Exception.new, "unable to find next weekly date (#{self.every_day})"
end
def get_monthly_date(previous)
start = determine_start(previous)
day = self.every_other1
n = self.every_other2
case self.recurrence_selector
when 0 # specific day of the month
if (previous && start.mday >= day) || (previous.nil? && start.mday > day)
# there is no next day n in this month, search in next month
#
# start += n.months
#
# The above seems to not work. Fiddle with timezone. Looks like we hit a
# bug in rails here where 2008-12-01 +0100 plus 1.month becomes
# 2008-12-31 +0100. For now, just calculate in UTC and convert back to
# local timezone.
#
# TODO: recheck if future rails versions have this problem too
start = Time.utc(start.year, start.month, start.day)+n.months
start = Time.zone.local(start.year, start.month, start.day)
# go back to day
end
Time.zone.local(start.year, start.month, day)
when 1 # relative weekday of a month
the_next = get_xth_day_of_month(self.every_other3, self.every_count, start.month, start.year)
if the_next.nil? || the_next <= start
# the nth day is already passed in this month, go to next month and try
# again
# fiddle with timezone. Looks like we hit a bug in rails here where
# 2008-12-01 +0100 plus 1.month becomes 2008-12-31 +0100. For now, just
# calculate in UTC and convert back to local timezone.
# TODO: recheck if future rails versions have this problem too
the_next = Time.utc(the_next.year, the_next.month, the_next.day)+n.months
the_next = Time.zone.local(the_next.year, the_next.month, the_next.day)
# TODO: if there is still no match, start will be set to nil. if we ever
# support 5th day of the month, we need to handle this case
the_next = get_xth_day_of_month(self.every_other3, self.every_count, the_next.month, the_next.year)
end
the_next
else
raise Exception.new, "unknown monthly recurrence selection (#{self.recurrence_selector})"
end
end
def get_xth_day_of_month(x, weekday, month, year)
if x == 5
# last -> count backwards. use UTC to avoid strange timezone oddities
# where last_day -= 1.day seems to shift tz+0100 to tz+0000
last_day = Time.utc(year, month, Time.days_in_month(month))
while last_day.wday != weekday
last_day -= 1.day
end
# convert back to local timezone
Time.zone.local(last_day.year, last_day.month, last_day.day)
else
# 1-4th -> count upwards last -> count backwards. use UTC to avoid strange
# timezone oddities where last_day -= 1.day seems to shift tz+0100 to
# tz+0000
start = Time.utc(year,month,1)
n = x
while n > 0
while start.wday() != weekday
start+= 1.day
end
n -= 1
start += 1.day unless n==0
end
# convert back to local timezone
Time.zone.local(start.year, start.month, start.day)
end
end
def get_yearly_date(previous)
start = determine_start(previous)
day = self.every_other1
month = self.every_other2
case self.recurrence_selector
when 0 # specific day of a specific month
if start.month > month || (start.month == month && start.day >= day)
# if there is no next month n and day m in this year, search in next
# year
start = Time.zone.local(start.year+1, month, 1)
else
# if there is a next month n, stay in this year
start = Time.zone.local(start.year, month, 1)
end
Time.zone.local(start.year, month, day)
when 1 # relative weekday of a specific month
# if there is no next month n in this year, search in next year
the_next = start.month > month ? Time.zone.local(start.year+1, month, 1) : start
# get the xth day of the month
the_next = get_xth_day_of_month(self.every_other3, self.every_count, month, the_next.year)
# if the_next is before previous, we went back into the past, so try next
# year
the_next = get_xth_day_of_month(self.every_other3, self.every_count, month, start.year+1) if the_next <= start
the_next
else
raise Exception.new, "unknown monthly recurrence selection (#{self.recurrence_selector})"
end
end
def continues_recurring?(previous)
return self.occurences_count < self.number_of_occurences unless self.number_of_occurences.nil?
return true if self.end_date.nil? || self.ends_on == 'no_end_date'
case self.target
when 'due_date'
get_due_date(previous) <= self.end_date
when 'show_from_date'
get_show_from_date(previous) <= self.end_date
else
raise Exception.new, "unexpected value of recurrence target '#{self.target}'"
end
pattern.get_show_from_date(previous)
end
def done?(end_date)
@ -350,29 +135,8 @@ class RecurringTodo < ActiveRecord::Base
self.save
end
protected
# Determine start date to calculate next date for recurring todo
# offset needs to be 1.day for daily patterns
def determine_start(previous, offset=0.day)
start = self.start_from || NullTime.new
now = Time.zone.now
if previous
# check if the start_from date is later than previous. If so, use
# start_from as start to search for next date
start > previous ? start : previous + offset
else
# skip to present
start > now ? start : now
end
end
def find_first_day_in_this_week(start)
# check if there are any days left this week for the next todo
start.wday().upto 6 do |i|
return start + (i-start.wday()).days unless self.every_day[i,1] == ' '
end
-1
def continues_recurring?(previous)
pattern.continues_recurring?(previous)
end
end

View file

@ -24,7 +24,7 @@ module RecurringTodos
get :target
end
def show_always
def show_always?
get :show_always
end
@ -104,8 +104,8 @@ module RecurringTodos
when 'show_from_date'
# no validations
when 'due_date'
validate_not_nil(show_always, "Please select when to show the action")
validate_not_blank(show_from_delta, "Please fill in the number of days to show the todo before the due date") unless show_always
validate_not_nil(show_always?, "Please select when to show the action")
validate_not_blank(show_from_delta, "Please fill in the number of days to show the todo before the due date") unless show_always?
else
errors[:base] << "Unexpected value of recurrence target selector '#{target}'"
end
@ -119,6 +119,93 @@ module RecurringTodos
@attributes[attribute]
end
# gets the next due date. returns nil if recurrence_target is not 'due_date'
def get_due_date(previous)
case target
when 'due_date'
get_next_date(previous)
when 'show_from_date'
nil
end
end
def get_show_from_date(previous)
case target
when 'due_date'
# so set show from date relative to due date unless show_always is true or show_from_delta is nil
return nil unless put_in_tickler?
get_due_date(previous) - show_from_delta.days
when 'show_from_date'
# Leave due date empty
get_next_date(previous)
end
end
# checks if the next todos should be put in the tickler for recurrence_target == 'due_date'
def put_in_tickler?
!( show_always? || show_from_delta.nil?)
end
def get_next_date(previous)
raise "Should not call AbstractRepeatPattern.get_next_date directly. Overwrite in subclass"
end
def continues_recurring?(previous)
return @recurring_todo.occurences_count < @recurring_todo.number_of_occurences unless @recurring_todo.number_of_occurences.nil?
return true if self.end_date.nil? || self.ends_on == 'no_end_date'
case self.target
when 'due_date'
get_due_date(previous) <= self.end_date
when 'show_from_date'
get_show_from_date(previous) <= self.end_date
end
end
private
# Determine start date to calculate next date for recurring todo
# offset needs to be 1.day for daily patterns
def determine_start(previous, offset=0.day)
start = self.start_from || NullTime.new
now = Time.zone.now
if previous
# check if the start_from date is later than previous. If so, use
# start_from as start to search for next date
start > previous ? start : previous + offset
else
# skip to present
start > now ? start : now
end
end
def get_xth_day_of_month(x, weekday, month, year)
if x == 5
# last -> count backwards. use UTC to avoid strange timezone oddities
# where last_day -= 1.day seems to shift tz+0100 to tz+0000
last_day = Time.utc(year, month, Time.days_in_month(month))
while last_day.wday != weekday
last_day -= 1.day
end
# convert back to local timezone
Time.zone.local(last_day.year, last_day.month, last_day.day)
else
# 1-4th -> count upwards last -> count backwards. use UTC to avoid strange
# timezone oddities where last_day -= 1.day seems to shift tz+0100 to
# tz+0000
start = Time.utc(year,month,1)
n = x
while n > 0
while start.wday() != weekday
start+= 1.day
end
n -= 1
start += 1.day unless n==0
end
# convert back to local timezone
Time.zone.local(start.year, start.month, start.day)
end
end
end
end

View file

@ -29,7 +29,23 @@ module RecurringTodos
errors[:base] << "Every other nth day may not be empty for this daily recurrence setting" if (!only_work_days?) && every_x_days.blank?
end
def get_next_date(previous)
# previous is the due date of the previous todo or it is the completed_at
# date when the completed_at date is after due_date (i.e. you did not make
# the due date in time)
start = determine_start(previous, 1.day)
if only_work_days?
return start + 2.day if start.wday() == 6 # saturday
return start + 1.day if start.wday() == 0 # sunday
return start
else # every nth day; n = every_other1
# if there was no previous todo, do not add n: the first todo starts on
# today or on start_from
return previous == nil ? start : start+every_x_days.day-1.day
end
end
end
end

View file

@ -75,6 +75,54 @@ module RecurringTodos
end
end
def get_next_date(previous)
start = determine_start(previous)
day = every_x_day
n = get(:every_other2)
case recurrence_selector
when 0 # specific day of the month
if (previous && start.mday >= day) || (previous.nil? && start.mday > day)
# there is no next day n in this month, search in next month
#
# start += n.months
#
# The above seems to not work. Fiddle with timezone. Looks like we hit a
# bug in rails here where 2008-12-01 +0100 plus 1.month becomes
# 2008-12-31 +0100. For now, just calculate in UTC and convert back to
# local timezone.
#
# TODO: recheck if future rails versions have this problem too
start = Time.utc(start.year, start.month, start.day)+n.months
start = Time.zone.local(start.year, start.month, start.day)
# go back to day
end
Time.zone.local(start.year, start.month, day)
when 1 # relative weekday of a month
the_next = get_xth_day_of_month(every_xth_day, day_of_week, start.month, start.year)
if the_next.nil? || the_next <= start
# the nth day is already passed in this month, go to next month and try
# again
# fiddle with timezone. Looks like we hit a bug in rails here where
# 2008-12-01 +0100 plus 1.month becomes 2008-12-31 +0100. For now, just
# calculate in UTC and convert back to local timezone.
# TODO: recheck if future rails versions have this problem too
the_next = Time.utc(the_next.year, the_next.month, the_next.day)+n.months
the_next = Time.zone.local(the_next.year, the_next.month, the_next.day)
# TODO: if there is still no match, start will be set to nil. if we ever
# support 5th day of the month, we need to handle this case
the_next = get_xth_day_of_month(every_xth_day, day_of_week, the_next.month, the_next.year)
end
the_next
else
raise Exception.new, "unknown monthly recurrence selection (#{self.recurrence_selector})"
end
end
end
end

View file

@ -13,7 +13,10 @@ module RecurringTodos
def map_attributes(mapping)
mapping = map(mapping, :every_other1, 'weekly_every_x_week')
{ monday: 1, tuesday: 2, wednesday: 3, thursday: 4, friday: 5, saturday: 6, sunday: 0 }.each{|day, index| mapping = map_day(mapping, :every_day, "weekly_return_#{day}", index)}
{ monday: 1, tuesday: 2, wednesday: 3, thursday: 4, friday: 5, saturday: 6, sunday: 0 }
.each do |day, index|
mapping = map_day(mapping, :every_day, "weekly_return_#{day}", index)
end
mapping
end
@ -23,7 +26,7 @@ module RecurringTodos
mapping.set_if_nil(source_key, ' ') # avoid nil
mapping.set(key, mapping.get(key)[0, index] + mapping.get(source_key) + mapping.get(key)[index+1, mapping.get(key).length])
mapping
mapping.except(source_key)
end
def selector_key

View file

@ -35,6 +35,48 @@ module RecurringTodos
errors[:base] << "You must specify at least one day on which the todo recurs" unless something_set
end
def get_next_date(previous)
# determine start
if previous.nil?
start = start_from.nil? ? Time.zone.now : self.start_from
else
start = previous + 1.day
if start.wday() == 0
# we went to a new week , go to the nth next week and find first match
# that week. Note that we already went into the next week, so -1
start += (every_x_week-1).week
end
unless self.start_from.nil?
# check if the start_from date is later than previous. If so, use
# start_from as start to search for next date
start = self.start_from if self.start_from > previous
end
end
day = find_first_day_in_this_week(start)
return day unless day == -1
# we did not find anything this week, so check the nth next, starting from
# sunday
start = start + self.every_x_week.week - (start.wday()).days
start = find_first_day_in_this_week(start)
return start unless start == -1
raise Exception.new, "unable to find next weekly date (#{self.every_day})"
end
private
def find_first_day_in_this_week(start)
# check if there are any days left this week for the next todo
start.wday().upto 6 do |i|
return start + (i-start.wday()).days if on_xday(i)
end
-1
end
end
end

View file

@ -61,6 +61,37 @@ module RecurringTodos
end
end
def get_next_date(previous)
start = determine_start(previous)
day = every_x_day
month = get(:every_other2)
case recurrence_selector
when 0 # specific day of a specific month
if start.month > month || (start.month == month && start.day >= day)
# if there is no next month n and day m in this year, search in next
# year
start = Time.zone.local(start.year+1, month, 1)
else
# if there is a next month n, stay in this year
start = Time.zone.local(start.year, month, 1)
end
Time.zone.local(start.year, month, day)
when 1 # relative weekday of a specific month
# if there is no next month n in this year, search in next year
the_next = start.month > month ? Time.zone.local(start.year+1, month, 1) : start
# get the xth day of the month
the_next = get_xth_day_of_month(self.every_xth_day, day_of_week, month, the_next.year)
# if the_next is before previous, we went back into the past, so try next
# year
the_next = get_xth_day_of_month(self.every_xth_day, day_of_week, month, start.year+1) if the_next <= start
the_next
end
end
end
end

View file

@ -25,13 +25,6 @@ class RecurringTodoTest < ActiveSupport::TestCase
@thursday = Time.zone.local(2008,6,12)
end
def test_pattern_text
assert_equal "every day", @every_day.recurrence_pattern
assert_equal "on work days", @every_workday.recurrence_pattern
assert_equal "every last friday of every 2 months", @monthly_every_last_friday.recurrence_pattern
assert_equal "every year on June 08", @yearly.recurrence_pattern
end
def test_daily_every_day
# every_day should return todays date if there was no previous date
due_date = @every_day.get_due_date(nil)
@ -96,29 +89,6 @@ class RecurringTodoTest < ActiveSupport::TestCase
assert_equal false, @every_day.continues_recurring?(@in_four_days)
end
def test_weekly_every_day_setters
@weekly_every_day.every_day = ' '
@weekly_every_day.weekly_return_sunday=('s')
assert_equal 's ', @weekly_every_day.every_day
@weekly_every_day.weekly_return_monday=('m')
assert_equal 'sm ', @weekly_every_day.every_day
@weekly_every_day.weekly_return_tuesday=('t')
assert_equal 'smt ', @weekly_every_day.every_day
@weekly_every_day.weekly_return_wednesday=('w')
assert_equal 'smtw ', @weekly_every_day.every_day
@weekly_every_day.weekly_return_thursday=('t')
assert_equal 'smtwt ', @weekly_every_day.every_day
@weekly_every_day.weekly_return_friday=('f')
assert_equal 'smtwtf ', @weekly_every_day.every_day
@weekly_every_day.weekly_return_saturday=('s')
assert_equal 'smtwtfs', @weekly_every_day.every_day
# test remove
@weekly_every_day.weekly_return_wednesday=(' ')
assert_equal 'smt tfs', @weekly_every_day.every_day
end
def test_weekly_pattern
assert_equal true, @weekly_every_day.continues_recurring?(nil)
@ -132,9 +102,7 @@ class RecurringTodoTest < ActiveSupport::TestCase
assert_equal @sunday + 2.weeks, due_date
# remove tuesday and wednesday
@weekly_every_day.weekly_return_tuesday=(' ')
@weekly_every_day.weekly_return_wednesday=(' ')
assert_equal 'sm tfs', @weekly_every_day.every_day
@weekly_every_day.every_day = 'sm tfs'
due_date = @weekly_every_day.get_due_date(@monday)
assert_equal @thursday, due_date

View file

@ -32,6 +32,17 @@ module RecurringTodos
assert rt.valid?, "should be valid again"
end
def test_pattern_text
@every_day = recurring_todos(:call_bill_gates_every_day)
@every_workday = recurring_todos(:call_bill_gates_every_workday)
assert_equal "every day", @every_day.recurrence_pattern
assert_equal "on work days", @every_workday.recurrence_pattern
@every_day.every_other1 = 2
assert_equal "every 2 days", @every_day.recurrence_pattern
end
end
end

View file

@ -98,6 +98,20 @@ module RecurringTodos
assert !rt.valid?, "should not be valid since day_of_week is empty"
end
def test_pattern_text
rt = recurring_todos(:check_with_bill_every_last_friday_of_month)
assert_equal "every last friday of every 2 months", rt.recurrence_pattern
rt.every_other2 = 1
assert_equal "every last friday of every month", rt.recurrence_pattern
rt.recurrence_selector = 0
assert_equal "every 5 months on day 1", rt.recurrence_pattern
rt.every_other3 = 1
assert_equal "every month on day 1", rt.recurrence_pattern
end
end
end

View file

@ -40,6 +40,14 @@ module RecurringTodos
assert !rt.valid?, "missing selected days in every_day"
end
def test_pattern_text
rt = @admin.recurring_todos.where(recurring_period: 'weekly').first
assert_equal "every 2 weeks", rt.recurrence_pattern
rt.every_other1 = 1
assert_equal "weekly", rt.recurrence_pattern
end
end
end

View file

@ -64,6 +64,16 @@ module RecurringTodos
assert !rt.valid?, "should not be valid since day_of_week is empty"
end
def test_pattern_text
rt = recurring_todos(:birthday_reinier)
assert_equal "every year on June 08", rt.recurrence_pattern
rt.recurrence_selector = 1
rt.every_count = 3
rt.every_other3 = 3
assert_equal "every year on the third wednesday of June", rt.recurrence_pattern
end
end
end