hummingbird-me/kitsu-server

View on GitHub
app/models/concerns/stat/amount_consumed.rb

Summary

Maintainability
A
1 hr
Test Coverage
B
83%
class Stat < ApplicationRecord
  # A common base for both the anime and manga amount-consumed stats.  In future, as we add more
  # media types, this is gonna be handy.
  module AmountConsumed
    extend ActiveSupport::Concern

    # The default stats_data values, automatically handled by the Stat superclass
    def default_data
      { 'media' => 0, 'units' => 0, 'time' => 0, 'completed' => 0 }
    end

    # Override #stats_data to find the percentile for each stat
    def stats_data
      data = super || default_data
      # Generate percentile data
      if global_stat
        data['percentiles'] = %w[media units time].each_with_object({}) do |key, out|
          # Find the first percentile with a value above our own
          percentiles = global_stat.stats_data.dig('percentiles', key)
          next unless percentiles && data[key]

          out[key] = percentiles.find_index { |val| val > data[key] }.to_f / 100
        end

        data['averageDiffs'] = %w[media units time].each_with_object({}) do |key, out|
          # Find the difference from average
          average = global_stat.stats_data.dig('average', key)
          next unless average && data[key] && average.positive? && data[key].positive?

          out[key] = (data[key] - average) / average
        end
      end
      data
    end

    # Recalculate this entire statistic from scratch
    # @return [self]
    def recalculate!
      entries = user.library_entries.by_kind(media_kind)

      reconsume_units = entries.joins(media_kind)
                               .sum("COALESCE(#{unit_count}, 0) * reconsume_count")

      self.stats_data = {}
      stats_data['media'] = entries.count
      stats_data['units'] = reconsume_units + entries.sum(:progress)
      stats_data['time'] = entries.sum(:time_spent)
      stats_data['completed'] = entries.completed_at_least_once.count

      self.recalculated_at = Time.now

      save!
    end

    # @param entry [LibraryEntry] an entry that was created
    # @return [void]
    def on_create(entry)
      stats_data['media'] += 1
      on_update(entry)
    end

    # @param entry [LibraryEntry] an entry that was removed
    # @return [void]
    def on_destroy(entry)
      stats_data['media'] -= 1
      stats_data['units'] -= entry.progress
      if entry.media
        stats_data['units'] -= entry.reconsume_count * (entry.media.send(unit_count) || 0)
      end
      stats_data['time'] -= entry.time_spent
      stats_data['completed'] -= 1 if entry.completed_at_least_once?

      save_or_recalculate!
    end

    # @param entry [LibraryEntry] an entry that was updated
    # @return [void]
    def on_update(entry)
      diff = LibraryEntryDiff.new(entry)
      stats_data['units'] += diff.progress_diff
      stats_data['units'] += diff.reconsume_diff * (entry.media.send(unit_count) || 0)
      stats_data['time'] += diff.time_diff

      stats_data['completed'] += if diff.became_uncompleted? then -1
                                 elsif diff.became_completed? then +1
                                 else 0
                                 end

      save_or_recalculate!
    end

    private

    def save_or_recalculate!
      if should_recalculate?
        recalculate!
      else
        save!
      end
    end

    def should_recalculate?
      %w[units time media].any? { |k| stats_data[k].negative? }
    end

    # @return [String] the column for the media unit count
    def unit_count
      "#{unit_kind}_count"
    end
  end
end