// Implements salient bits of section 4.3.10 of the iCal spec (RFC2445)
// Doesn't support recurrence granularities of less than a day,
// or BYYEARDAY or BYWEEKNO, because... well, I couldn't be bothered ;)
// See also http://www.ietf.org/rfc/rfc2445.txt
require 'date'
require 'profiler'
class RecurringEvent
def initialize(options = {})
@options = check_options(options)
@options[:freq] ||= :daily
@options[:interval] ||= 1
@options[:start_time] ||= Time.now - Time.now.sec
@options[:by_month] = [ @options[:by_month] ].flatten if @options[:by_month]
@options[:by_month_day] = [ @options[:by_month_day] ].flatten if @options[:by_month_day]
@options[:by_day] = [ @options[:by_day] ].flatten if @options[:by_day]
@options[:by_set_pos] = [ @options[:by_set_pos] ].flatten if @options[:by_set_pos]
Date::DAYNAMES
end
def start_time
@options[:start_time]
end
def start_date
@start_date ||= Date.civil(start_time.year, start_time.month, start_time.day)
end
def frequency
@options[:freq]
end
def each(from = nil, to = nil)
from ||= Date.today - Date.today.mday
to ||= (from >> 1) - 1
times = in_range(from, to)
times.each { |t| yield t } if block_given?
times
end
alias_method :each_in, :each
private
def in_range(from, to)
window_from = from << 1
window_to = to >> 1
start = Date.civil(start_time.year, start_time.month, start_time.day)
interval = @options[:interval] || 1
s_year = start.year % interval
s_month = (start.year * 12 + start.month) % interval
s_week = start.week % interval
s_day = start.julian_day % interval
dates = (window_from..window_to).select { |d|
in_interval = (interval == 1) or case frequency
when :yearly then d.year % interval == s_year
when :monthly then (d.year * 12 + d.month) % interval == s_month
when :weekly then d.week % interval == s_week
when :daily then d.julian_day % interval == s_day
end
if in_interval
match_by_parts(d)
else
false
end
}
hours = @options[:by_hour] ? @options[:by_hour] : [ start_time.hour ]
minutes = @options[:by_minute] ? @options[:by_minute] : [ start_time.min ]
times = dates.collect { |d|
hours.collect { |h|
minutes.collect { |m|
DateTime.civil(d.year, d.month, d.day, h, m, 0)
}
}
}.flatten
if @options[:by_set_pos]
grouped_times = case frequency
when :yearly then times.group_by { |t| t.year }
when :monthly then times.group_by { |t| t.month }
when :weekly then times.group_by { |t| t.week }
when :daily then times.group_by { |t| t.day }
end
times = grouped_times.collect { |group|
@options[:by_set_pos].collect { |i| group.at(i) }.compact
}.flatten
end
times = times.first(@options[:count]) if @options[:count]
to = @options[:until] if @options[:until] and @options[:until] < to
range = (from..to)
times.select { |t| range.include? t }
end
def match_by_parts(d)
case frequency
when :yearly, :daily then
begin
(@options[:by_month] ? @options[:by_month].include?(d.month) : d.month == start.month) and
(@options[:by_month_day] ? @options[:by_month_day].include?(d.day) : d.day == start.day) and
(@options[:by_day].nil? or @options[:by_day].include?(d.week_day))
end
when :monthly then
begin
if @options[:by_month_day]
@options[:by_month_day].include?(d.day)
elsif @options[:by_day]
@options[:by_day].include?(d.week_day)
else
d.day == start.day
end
end
when :weekly then
begin
(@options[:by_month].nil? or @options[:by_month].include?(d.month)) and
(@options[:by_day] ? @options[:by_day].include?(d.week_day) : d.week_day == start.week_day)
end
else
true
end
end
def check_options(options)
options
end
end
class Date
def week
@week ||= julian_day / 7
end
def week_day
@week_day ||= wday
end
def julian_day
@julian_day ||= mjd
end
end
class Array
def group_by
current_key = nil
results = []
current_set = []
each do |item|
key = yield(item)
if key != current_key
results << current_set unless current_set.empty?
current_set = []
current_key = key
end
current_set << item unless key === false
end
results << current_set unless current_set.empty?
results
end
end
Profiler__::start_profile
e = RecurringEvent.new :start_time => Time.local(2007, 1, 1, 12, 34), :freq => :monthly, :by_day => [1], :by_set_pos => [ 1 ]
e.each(Date.today, Date.today + 365) do |d|
print "#{d.strftime('%I:%M %a %d %b %Y')}\n"
end
Profiler__::stop_profile
Profiler__::print_profile($stdout)