af83/chouette-core

View on GitHub
app/models/control/service_count_trend.rb

Summary

Maintainability
A
15 mins
Test Coverage
module Control
  class ServiceCountTrend < Control::Base
    module Options
      extend ActiveSupport::Concern

      included do
        option :weeks_before
        option :weeks_after
        option :maximum_difference

        validates(
          :weeks_before,
          :weeks_after,
          :maximum_difference,
          numericality: { only_integer: true, greater_than: 0, allow_nil: false }
        )
      end
    end
    include Options

    class Run < Control::Base::Run
      include Options

      def run
        analysis.anomalies.each do |anomaly|
          control_messages.create({
            message_attributes: {
              date: anomaly.date,
              line: anomaly.line_id
            },
            criticity: criticity,
            source_id: anomaly.line_id,
            source_type: 'Chouette::Line',
            message_key: :service_count_trend
          })
        end
      end

      def analysis
        @analysis ||= Analysis.new(
          context,
          {
            weeks_before: weeks_before,
            weeks_after: weeks_after,
            maximum_difference: maximum_difference
          }
        )
      end

      class Analysis

        def initialize(context, options)
          @context = context
          options.each { |k,v| send "#{k}=", v }
        end
        attr_accessor :context, :weeks_before, :weeks_after, :maximum_difference

        def anomalies
          PostgreSQLCursor::Cursor.new(query).map { |attributes| Anomaly.new(attributes) }
        end

        def query
          <<~SQL
            SELECT
              percentage_difference_table.line_id,
              percentage_difference_table.date,
              percentage_difference_table.sum_count,
              percentage_difference_table.avg_sum,
              percentage_difference_table.percentage_difference
            FROM (
              SELECT
                sum_and_avg_table.line_id,
                sum_and_avg_table.date,
                sum_and_avg_table.sum_count,
                sum_and_avg_table.avg_sum,
                ABS((sum_and_avg_table.sum_count - sum_and_avg_table.avg_sum) / sum_and_avg_table.sum_count) * 100 AS percentage_difference
              FROM (
                SELECT
                  A.line_id, A.date,
                  SUM(A.count) AS sum_count,
                  (
                    SELECT
                      avg_table.avg_sum
                    FROM (
                      SELECT
                        sum_table.line_id,
                        AVG(sum_table.sum_count) AS avg_sum
                      FROM ( #{sum_table_query} ) AS sum_table
                      WHERE (EXTRACT(dow from sum_table.date) = EXTRACT(dow from A.date))
                      GROUP BY sum_table.line_id
                    ) AS avg_table
                    WHERE (avg_table.line_id = A.line_id)
                  ) AS avg_sum
                FROM service_counts A
                WHERE A.date BETWEEN (A.date - #{days_before}) AND (A.date + #{days_after})
                GROUP BY A.line_id, A.date
              ) AS sum_and_avg_table
              WHERE sum_and_avg_table.sum_count > 0
            ) AS percentage_difference_table
            WHERE percentage_difference_table.percentage_difference > #{maximum_difference}
          SQL
        end

        class Anomaly
          def initialize(attributes)
            attributes.each { |k,v| send "#{k}=", v if respond_to?(k) }
          end
          attr_accessor :line_id, :date
        end

        private

        def sum_table_query
          context
            .service_counts
            .group(:line_id, :date)
            .select('SUM(count) AS sum_count', :line_id, :date)
            .where("date BETWEEN (date - ?) AND (date + ?)", days_before, days_after)
            .to_sql
        end

        def days_before
          7 * weeks_before
        end

        def days_after
          7 * weeks_after
        end
      end
    end
  end
end