<commit_before>require 'version'

module LaserLemon
  module VestalVersions
    def self.included(base)
      base.extend ClassMethods
    end

    module ClassMethods
      def versioned
        has_many :versions, :as => :versioned, :order => 'versions.number ASC', :dependent => :destroy do
          def at(value)
            case value
              when Version then value
              when Numeric then find_by_number(value.floor)
              when Symbol then respond_to?(value) ? send(value) : nil
              when Date, Time then last(:conditions => ['versions.created_at <= ?', value.to_time])
            end
          end

          def between(from_value, to_value)
            from, to = number_at(from_value), number_at(to_value)
            return [] if from.nil? || to.nil? || (from == to)
            all(
              :conditions => {:number => Range.new(*[from, to].sort)},
              :order => "versions.number #{(from > to) ? 'DESC' : 'ASC'}"
            )
          end

          private

            def number_at(value)
              case value
                when Version then value.number
                when Numeric then value.floor
                when Symbol, Date, Time then at(value).try(:number)
              end
            end
        end

        after_save :create_version, :if => :needs_version?

        include InstanceMethods
      end
    end

    module InstanceMethods
      private

        def needs_version?
          !changed.empty?
        end

        def reset_version(new_version = nil)
          @last_version = nil if new_version.nil?
          @version = new_version
        end

        def create_version
          if versions.empty?
            versions.create(:changes => attributes, :number => 1)
          else
            reset_version
            versions.create(:changes => changes, :number => (version.to_i + 1))
          end

          reset_version
        end

      public

        def version
          @version ||= last_version
        end

        def last_version
          @last_version ||= versions.maximum(:number)
        end

        def revert_to(value)
          chain = versions.between(version, value)
          return version unless chain.size > 1

          new_version = chain.last.number
          backward = chain.first > chain.last
          backward ? chain.pop : chain.shift

          timestamps = %w(created_at created_on updated_at updated_on)

          chain.each do |version|
            version.changes.except(*timestamps).each do |attribute, change|
              new_value = backward ? change.first : change.last
              write_attribute(attribute, new_value)
            end
          end

          reset_version(new_version)
        end

        def revert_to!(value)
          revert_to(value)
          reset_version if saved = save
          saved<commit_msg>Added a #reverted? method in preparation for some clever save handling for reverted instances.<commit_after>require 'version'

module LaserLemon
  module VestalVersions
    def self.included(base)
      base.extend ClassMethods
    end

    module ClassMethods
      def versioned
        has_many :versions, :as => :versioned, :order => 'versions.number ASC', :dependent => :destroy do
          def at(value)
            case value
              when Version then value
              when Numeric then find_by_number(value.floor)
              when Symbol then respond_to?(value) ? send(value) : nil
              when Date, Time then last(:conditions => ['versions.created_at <= ?', value.to_time])
            end
          end

          def between(from_value, to_value)
            from, to = number_at(from_value), number_at(to_value)
            return [] if from.nil? || to.nil? || (from == to)
            all(
              :conditions => {:number => Range.new(*[from, to].sort)},
              :order => "versions.number #{(from > to) ? 'DESC' : 'ASC'}"
            )
          end

          private

            def number_at(value)
              case value
                when Version then value.number
                when Numeric then value.floor
                when Symbol, Date, Time then at(value).try(:number)
              end
            end
        end

        after_save :create_version, :if => :needs_version?

        include InstanceMethods
      end
    end

    module InstanceMethods
      private

        def needs_version?
          !changed.empty?
        end

        def reset_version(new_version = nil)
          @last_version = nil if new_version.nil?
          @version = new_version
        end

        def create_version
          if versions.empty?
            versions.create(:changes => attributes, :number => 1)
          else
            reset_version
            versions.create(:changes => changes, :number => (version.to_i + 1))
          end

          reset_version
        end

      public

        def version
          @version ||= last_version
        end

        def last_version
          @last_version ||= versions.maximum(:number)
        end

        def reverted?
          version != last_version
        end

        def revert_to(value)
          chain = versions.between(version, value)
          return version unless chain.size > 1

          new_version = chain.last.number
          backward = chain.first > chain.last
          backward ? chain.pop : chain.shift

          timestamps = %w(created_at created_on updated_at updated_on)

          chain.each do |version|
            version.changes.except(*timestamps).each do |attribute, change|
              new_value = backward ? change.first : change.last
              write_attribute(attribute, new_value)
            end
          end

          reset_version(new_version)
        end

        def revert_to!(value)
          revert_to(value)
          reset_version if saved = save
          saved
{'fraction_non_alphanumeric': 0.081473968897904, 'fraction_numerical': 0.0010141987829614604, 'mean_word_length': 1.6533632286995517, 'pattern_counts': {'":': 0, '<': 5, '<?xml version=': 0, '>': 33, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '3673328', 'n_tokens_mistral': 1561, 'n_tokens_neox': 1501, 'n_words': 419}