class Dater MAX_INTERVALS = 1000 unless const_defined? :MAX_INTERVALS attr_accessor :start_date, :end_date, :max, :interval, :max_intervals, :type def initialize(options = {}) options[:interval] = 1 unless options.has_key?(:interval) options.each do |key, value| send("#{key}=", value) end end def find unless start_date and interval and (end_date or max) raise "specify a start date, interval, and one of an end date, max dates, or max intervals" end start_date, intervals = self.start_date, 1 returning [] do |matching_dates| loop do result = dates(start_date) matching_dates.concat result.dates start_date = result.next_start_date break if intervals > MAX_INTERVALS break if max_intervals and intervals == max_intervals break if max and matching_dates.size >= max break if end_date and start_date > end_date intervals += 1 end if max matching_dates.pop until matching_dates.size <= max end if end_date and matching_dates.any? matching_dates.pop until (matching_dates.last.nil? || matching_dates.last <= end_date) end end end class << self def method_missing(method_id, *args) subclass_name = "Dater::" + method_id.to_s.camelize if subclasses.include?(subclass_name) subclass_name.constantize.new(*args) else super end end def pattern_names subclasses.map(&:demodulize).map(&:lowercase) end end end class Dater::Result attr_reader :dates attr_accessor :next_start_date def initialize(dates = nil, next_start_date = nil) self.dates = dates || [] self.next_start_date = next_start_date end def dates=(dates) dates = [*dates] unless dates.is_a?(Array) dates.compact! @dates = dates end end class Dater::Daily < Dater def dates(start_date) result = Result.new case type.to_s when 'everyday' result.dates = start_date when 'weekdays' result.dates = start_date if start_date.weekday? else raise "type should be everyday or weekdays" end result.next_start_date = start_date + 1 result end end class Dater::Weekly < Dater attr_accessor :monday, :tuesday, :wednesday, :thursday, :friday, :saturday, :sunday def dates(start_date) result = Result.new case type.to_s when 'week_beginning' result.dates = start_date.start_of_week when 'days' start_date.rest_of_days_in_week.each do |date| if send(Date::DAYNAMES[date.wday].downcase) result.dates << date end end else raise "type should be week_beginning or days" end result.next_start_date = start_date.start_of_next_week(interval) result end end class Dater::Monthly < Dater attr_accessor :day_number attr_accessor :day_occurrence, :day def dates(start_date) result = case type.to_s when 'specific' if start_date.day <= day_number begin start_date.renew(:day => day_number) rescue ArgumentError end end when 'generic' date = start_date.day_occurrences(day_occurrence).find { |date| date.wday == day } date if date >= start_date else raise "type should be specific or generic" end Result.new(result, start_date.start_of_next_month(interval)) end end class Dater::Yearly < Dater attr_accessor :month, :day_number attr_accessor :day_occurrence, :day def dates(start_date) result = case type.to_s when 'specific' begin date = start_date.renew(:month => month, :day => day_number) date if date >= start_date rescue ArgumentError end when 'generic' month_beginning = start_date.renew(:month => month, :day => 1) date = month_beginning.day_occurrences(day_occurrence).find { |date| date.wday == day } date if date >= start_date else raise "type should be specific or generic" end Result.new(result, start_date.start_of_next_year(interval)) end end