Skip to content

Commit

Permalink
Refactored REWeekWithIntervalTE class contributed Jeff Whitmire
Browse files Browse the repository at this point in the history
Replaced REWeekWithIntervalTE class with the WeekInterval class
which (when combined with other existing expressions) provides
the same functionality and more. See the RDoc comments in the
source.
  • Loading branch information
mlipper committed Apr 24, 2013
1 parent 415fdec commit f374988
Show file tree
Hide file tree
Showing 2 changed files with 207 additions and 67 deletions.
168 changes: 101 additions & 67 deletions lib/runt/temporalexpression.rb
Original file line number Diff line number Diff line change
Expand Up @@ -465,73 +465,6 @@ def validate(start_day,end_day)
end
end

# Extends REWeek to also allow intervals to specify bi-weekly events, every 3rd week events, etc.
# Requires a start date to calculate which weeks should fire the events. All date arguments are
# converted to DPrecision::DAY precision.
#
# NOTE: one major difference from REWeek is that the days are given as an array so you can specify
# [2,4] for events that should happen on Tuesdays and Thursdays, or [1,3,5] for Monday, Wednesday, Friday
# events.
#
# Contributed by Jeff Whitmire
class REWeekWithIntervalTE

include TExpr

VALID_RANGE = 0..6

attr_reader :interval, :base_date, :week_days

def initialize(base_date, interval, week_days)
validate(base_date, interval, week_days)
@base_date = DPrecision.to_p(base_date,DPrecision::DAY)
# convert base_date to the start of the week
@base_date -= base_date.wday

@interval = interval || 2
@week_days = week_days
end

def ==(o)
o.is_a?(REWeekWithIntervalTE) ? base_date == o.base_date && interval == o.interval && week_days == o.week_days : super(o)
end

def interval_days
interval * 7
end

def include?(date)
return false if date < base_date
num_of_intervals_to_jump = ((date - base_date) / interval_days).to_i
start_of_active_week = base_date + (num_of_intervals_to_jump * interval_days)
date_offset = DPrecision.to_p(date,DPrecision::DAY) - start_of_active_week

week_days.is_a?(Fixnum) ? (date_offset == week_days) : week_days.include?(date_offset)
end

def to_s
"every #{Runt.ordinalize(@interval)} week after #{Runt.format_date(@base_date)}"
end

private

def validate(base_date, interval, weekdays)
raise ArgumentError, 'starting date is required' unless base_date
raise ArgumentError, 'starting date must be a valid date' unless base_date.is_a?(Date)
raise ArgumentError, 'interval is required' unless interval
unless interval.is_a?(Fixnum) && interval >= 2 && interval <= 10
raise ArgumentError, 'interval must be in the range (2..10).'
end
unless weekdays && (weekdays.is_a?(Fixnum) || (weekdays.is_a?(Array) && !weekdays.empty?))
raise ArgumentError, 'weekdays are required'
end
unless (weekdays.is_a?(Fixnum) && VALID_RANGE.include?(weekdays)) || (weekdays.is_a?(Array) && weekdays.all?{|day| VALID_RANGE.include?(day) })
raise ArgumentError, "weekdays must be in the range (#{VALID_RANGE.to_s})."
end
end

end

#
# TExpr that matches date ranges within a single year. Assumes that the start
# and end parameters occur within the same year.
Expand Down Expand Up @@ -870,6 +803,107 @@ def to_s

end

#
# This class creates an expression which matches dates occuring during the weeks
# alternating at the given interval begining on the week containing the date
# used to create the instance.
#
# WeekInterval.new(starting_date, interval)
#
# Weeks are defined as Sunday to Saturday, as opposed to the commercial week
# which starts on a Monday. For example,
#
# every_other_week = WeekInterval.new(Date.new(2013,04,24), 2)
#
# will match any date that occurs during every other week begining with the
# week of 2013-04-21 (2013-04-24 is a Wednesday and 2013-04-21 is the Sunday
# that begins the containing week).
#
# # Sunday of starting week
# every_other_week.include?(Date.new(2013,04,21)) #==> true
# # Saturday of starting week
# every_other_week.include?(Date.new(2013,04,27)) #==> true
# # First week _after_ start week
# every_other_week.include?(Date.new(2013,05,01)) #==> false
# # Second week _after_ start week
# every_other_week.include?(Date.new(2013,05,06)) #==> true
#
# NOTE: The idea and tests for this class were originally contributed as the
# REWeekWithIntervalTE class by Jeff Whitmire. The behavior of the original class
# provided both the matching of every n weeks and the specification of specific
# days of that week in a single class. This class only provides the matching
# of every n weeks. The exact functionality of the original class is easy to create
# using the Runt set operators and the DIWeek class:
#
# # Old way
# tu_thurs_every_third_week = REWeekWithIntervalTE.new(Date.new(2013,04,24),2,[2,4])
#
# # New way
# tu_thurs_every_third_week =
# WeekInterval.new(Date.new(2013,04,24),2) & (DIWeek.new(Tuesday) | DIWeek.new(Thursday))
#
# Notice that the compound expression (in parens after the "&") can be replaced
# or combined with any other appropriate temporal expression to provide different
# functionality (REWeek to provide a range of days, REDay to provide certain times, etc...).
#
# Contributed by Jeff Whitmire
class WeekInterval
include TExpr
def initialize(start_date,interval=2)
@start_date = DPrecision.to_p(start_date,DPrecision::DAY)
# convert base_date to the start of the week
@base_date = @start_date - @start_date.wday
@interval = interval
end

def include?(date)
return false if @base_date > date
((adjust_for_year(date) - week_num(@base_date)) % @interval) == 0
end

def to_s
"every #{Runt.ordinalize(@interval)} week starting with the week containing #{Runt.format_date(@start_date)}"
end

private
def week_num(date)
# %U - Week number of the year. The week starts with Sunday. (00..53)
date.strftime("%U").to_i
end
def max_week_num(year)
d = Date.new(year,12,31)
max = week_num(d)
while max < 52
d = d - 1
max = week_num(d)
end
max
end
def adjust_for_year(date)
# Exclusive range: if date.year == @base_date.year, this will be empty
range_of_years = @base_date.year...date.year
in_same_year = range_of_years.to_a.empty?
# Week number of the given date argument
week_number = week_num(date)
# Default (most common case) date argument is in same year as @base_date
# and the week number is also part of the same year. This starting value
# is also necessary for the case where they're not in the same year.
adjustment = week_number
if in_same_year && (week_number < week_num(@base_date)) then
# The given date occurs within the same year
# but is actually week number 1 of the next year
adjustment = adjustment + max_week_num(date.year)
elsif !in_same_year then
# Date occurs in different year
range_of_years.each do |year|
# Max week number taking into account we are not using commercial week
adjustment = adjustment + max_week_num(year)
end
end
adjustment
end
end

# Simple expression which returns true if the supplied arguments
# occur within the given year.
#
Expand Down
106 changes: 106 additions & 0 deletions test/weekintervaltest.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
#!/usr/bin/env ruby

require 'baseexpressiontest'

class WeekIntervalTest < BaseExpressionTest

def test_every_other_week
expr = WeekInterval.new(Date.new(2013,4,23),2)
good_dates = [Date.new(2013,4,21), Date.new(2013,4,27),Date.new(2013,5,8)]
good_dates.each do |date|
assert(expr.include?(date),"Expr<#{expr}> should include #{date.ctime}")
end
bad_dates = [Date.new(2013,4,20), Date.new(2013,4,28)]
bad_dates.each do |date|
assert(!expr.include?(date),"Expr<#{expr}> should not include #{date.ctime}")
end
end

def test_every_third_week_spans_a_year
expr = WeekInterval.new(Date.new(2013,12,25),3)
good_dates = [Date.new(2013,12,22),Date.new(2014,1,12)]
good_dates.each do |date|
assert(expr.include?(date),"Expr<#{expr}> should include #{date.ctime}")
end
bad_dates = [Date.new(2013,12,21), Date.new(2013,12,31),Date.new(2014,01,11),Date.new(2014,01,19)]
bad_dates.each do |date|
assert(!expr.include?(date),"Expr<#{expr}> should not include #{date.ctime}")
end
end

def test_biweekly_with_sunday_start_with_diweek
every_other_friday = WeekInterval.new(Date.new(2006,2,26), 2) & DIWeek.new(Friday)

# should match the First friday and every other Friday after that
good_dates = [Date.new(2006,3,3), Date.new(2006,3,17), Date.new(2006,3,31)]
bad_dates = [Date.new(2006,3,1), Date.new(2006,3,10), Date.new(2006,3,24)]

good_dates.each do |date|
assert every_other_friday.include?(date), "Expression #{every_other_friday.to_s} should include #{date}"
end

bad_dates.each do |date|
assert !every_other_friday.include?(date), "Expression #{every_other_friday.to_s} should not include #{date}"
end
end

def test_biweekly_with_friday_start_with_diweek
every_other_wednesday = WeekInterval.new(Date.new(2006,3,3), 2) & DIWeek.new(Wednesday)

# should match the First friday and every other Friday after that
good_dates = [Date.new(2006,3,1), Date.new(2006,3,15), Date.new(2006,3,29)]
bad_dates = [Date.new(2006,3,2), Date.new(2006,3,8), Date.new(2006,3,22)]

good_dates.each do |date|
assert every_other_wednesday.include?(date), "Expression #{every_other_wednesday.to_s} should include #{date}"
end

bad_dates.each do |date|
assert !every_other_wednesday.include?(date), "Expression #{every_other_wednesday.to_s} should not include #{date}"
end
end

def test_tue_thur_every_third_week_with_diweek
every_tth_every_3 = WeekInterval.new(Date.new(2006,5,1), 3) & (DIWeek.new(Tuesday) | DIWeek.new(Thursday))

# should match the First tuesday and thursday for week 1 and every 3 weeks thereafter
good_dates = [Date.new(2006,5,2), Date.new(2006,5,4), Date.new(2006,5,23), Date.new(2006,5,25), Date.new(2006,6,13), Date.new(2006,6,15)]
bad_dates = [Date.new(2006,5,3), Date.new(2006,5,9), Date.new(2006,5,18)]

good_dates.each do |date|
assert every_tth_every_3.include?(date), "Expression #{every_tth_every_3.to_s} should include #{date}"
end

bad_dates.each do |date|
assert !every_tth_every_3.include?(date), "Expression #{every_tth_every_3.to_s} should not include #{date}"
end

range_start = Date.new(2006,5,1)
range_end = Date.new(2006,8,1)
expected_dates = [
Date.new(2006,5,2), Date.new(2006,5,4),
Date.new(2006,5,23), Date.new(2006,5,25),
Date.new(2006,6,13), Date.new(2006,6,15),
Date.new(2006,7,4), Date.new(2006,7,6),
Date.new(2006,7,25), Date.new(2006,7,27)
]

dates = every_tth_every_3.dates(DateRange.new(range_start, range_end))
assert_equal dates, expected_dates
end

def test_to_s
date = Date.new(2006,2,26)
assert_equal "every 2nd week starting with the week containing #{Runt.format_date(date)}", WeekInterval.new(date, 2).to_s
assert_equal "every 3rd week starting with the week containing #{Runt.format_date(date)}", WeekInterval.new(date, 3).to_s
assert_equal "every 4th week starting with the week containing #{Runt.format_date(date)}", WeekInterval.new(date, 4).to_s
assert_equal "every 5th week starting with the week containing #{Runt.format_date(date)}", WeekInterval.new(date, 5).to_s
assert_equal "every 6th week starting with the week containing #{Runt.format_date(date)}", WeekInterval.new(date, 6).to_s
assert_equal "every 7th week starting with the week containing #{Runt.format_date(date)}", WeekInterval.new(date, 7).to_s
assert_equal "every 8th week starting with the week containing #{Runt.format_date(date)}", WeekInterval.new(date, 8).to_s
assert_equal "every 9th week starting with the week containing #{Runt.format_date(date)}", WeekInterval.new(date, 9).to_s
assert_equal "every 10th week starting with the week containing #{Runt.format_date(date)}", WeekInterval.new(date, 10).to_s
end


end

0 comments on commit f374988

Please sign in to comment.