boblail/hiccup

View on GitHub
lib/hiccup/inferable/guesser.rb

Summary

Maintainability
B
5 hrs
Test Coverage
require 'hiccup/inferable/scorer'

module Hiccup
  module Inferable
    class Guesser

      def initialize(klass, options={})
        @klass = klass
        @verbose = options.fetch(:verbose, false)
        @allow_skips = options.fetch(:allow_skips, true)
        @max_complexity = options.fetch(:max_complexity, 3)
      end

      attr_reader :max_complexity

      def allow_skips?
        @allow_skips
      end



      def generate_guesses(dates)
        @start_date = dates.first
        @end_date = dates.last
        [].tap do |guesses|
          guesses.concat generate_yearly_guesses(dates)
          guesses.concat generate_monthly_guesses(dates)
          guesses.concat generate_weekly_guesses(dates)
        end
      end

      def generate_yearly_guesses(dates)
        histogram_of_patterns = dates.to_histogram do |date|
          [date.month, date.day]
        end
        patterns_by_popularity = histogram_of_patterns.flip # => {1 => [...], 2 => [...], 5 => [a, b]}
        highest_popularity = patterns_by_popularity.keys.max # => 5
        most_popular = patterns_by_popularity[highest_popularity].first # => a
        start_date = Date.new(@start_date.year, *most_popular)

        [].tap do |guesses|
          skip_range.each do |skip|
            guesses << @klass.new.tap do |schedule|
              schedule.kind = :annually
              schedule.start_date = start_date
              schedule.end_date = @end_date
              schedule.skip = skip
            end
          end
        end
      end

      def generate_monthly_guesses(dates)
        histogram_of_patterns = dates.to_histogram do |date|
          [date.get_nth_wday_of_month, Date::DAYNAMES[date.wday]]
        end
        patterns_by_popularity = histogram_of_patterns.flip

        histogram_of_days = dates.to_histogram(&:day)
        days_by_popularity = histogram_of_days.flip

        if @verbose
          puts "",
               "  monthly analysis:",
               "    input: #{dates.inspect}",
               "    histogram (weekday): #{histogram_of_patterns.inspect}",
               "    by_popularity (weekday): #{patterns_by_popularity.inspect}",
               "    histogram (day): #{histogram_of_days.inspect}",
               "    by_popularity (day): #{days_by_popularity.inspect}"
        end

        [].tap do |guesses|
          skip_range.each do |skip|
            enumerate_by_popularity(days_by_popularity) do |days|
              next if days.length > max_complexity
              guesses << @klass.new.tap do |schedule|
                schedule.kind = :monthly
                schedule.start_date = @start_date
                schedule.end_date = @end_date
                schedule.skip = skip
                schedule.monthly_pattern = days
              end
            end

            enumerate_by_popularity(patterns_by_popularity) do |patterns|
              next if patterns.length > max_complexity
              guesses << @klass.new.tap do |schedule|
                schedule.kind = :monthly
                schedule.start_date = @start_date
                schedule.end_date = @end_date
                schedule.skip = skip
                schedule.monthly_pattern = patterns
              end
            end
          end
        end
      end

      def generate_weekly_guesses(dates)
        [].tap do |guesses|
          histogram_of_wdays = dates.to_histogram do |date|
            Date::DAYNAMES[date.wday]
          end
          wdays_by_popularity = histogram_of_wdays.flip

          if @verbose
            puts "",
                 "  weekly analysis:",
                 "    input: #{dates.inspect}",
                 "    histogram: #{histogram_of_wdays.inspect}",
                 "    by_popularity: #{wdays_by_popularity.inspect}"
          end

          skip_range.each do |skip|
            enumerate_by_popularity(wdays_by_popularity) do |wdays|
              next if wdays.length > max_complexity
              guesses << @klass.new.tap do |schedule|
                schedule.kind = :weekly
                schedule.start_date = @start_date
                schedule.end_date = @end_date
                schedule.skip = skip
                schedule.weekly_pattern = wdays
              end
            end
          end
        end
      end

      def skip_range
        return 1..1 unless allow_skips?
        1...5
      end



      # Expects a hash of values grouped by popularity
      # Yields the most popular values first, and then
      # increasingly less popular values
      def enumerate_by_popularity(values_by_popularity)
        popularities = values_by_popularity.keys.sort.reverse
        popularities.length.times do |i|
          at_popularities = popularities.take(i + 1)
          yield values_by_popularity.values_at(*at_popularities).flatten(1)
        end
      end

    end
  end
end