module Xcodeproj::Differ

Computes the recursive diff of Hashes, Array and other objects.

Useful to compare two projects. Inspired from ‘active_support/core_ext/hash/diff’.

@example

h1 = { :common => 'value', :changed => 'v1' }
h2 = { :common => 'value', :changed => 'v2', :addition => 'new_value' }
h1.recursive_diff(h2) == {
  :changed => {
    :self  => 'v1',
    :other => 'v2'
  },
  :addition => {
    :self  => nil,
    :other => 'new_value'
  }
} #=> true

Public Class Methods

array_diff(value_1, value_2, options) click to toggle source

Returns the recursive diff of two arrays.

@see diff

# File lib/xcodeproj/differ.rb, line 111
def self.array_diff(value_1, value_2, options)
  ensure_class(value_1, Array)
  ensure_class(value_2, Array)
  return nil if value_1 == value_2

  new_objects_value_1 = array_non_unique_diff(value_1, value_2)
  new_objects_value_2 = array_non_unique_diff(value_2, value_1)
  return nil if value_1.empty? && value_2.empty?

  matched_diff = {}
  if id_key = options[:id_key]
    matched_value_1 = []
    matched_value_2 = []
    new_objects_value_1.each do |entry_value_1|
      if entry_value_1.is_a?(Hash)
        id_value = entry_value_1[id_key]
        entry_value_2 = new_objects_value_2.find do |entry|
          entry[id_key] == id_value
        end
        if entry_value_2
          matched_value_1 << entry_value_1
          matched_value_2 << entry_value_2
          diff = diff(entry_value_1, entry_value_2, options)
          matched_diff[id_value] = diff if diff
        end
      end
    end

    new_objects_value_1 -= matched_value_1
    new_objects_value_2 -= matched_value_2
  end

  if new_objects_value_1.empty? && new_objects_value_2.empty?
    if matched_diff.empty?
      nil
    else
      matched_diff
    end
  else
    result = {}
    result[options[:key_1]] = new_objects_value_1 unless new_objects_value_1.empty?
    result[options[:key_2]] = new_objects_value_2 unless new_objects_value_2.empty?
    result[:diff] = matched_diff unless matched_diff.empty?
    result
  end
end
array_non_unique_diff(value_1, value_2) click to toggle source

Returns the difference between two arrays, taking into account the number of times an element repeats in both arrays.

@param [Array] value_1

First array to the difference operation.

@param [Array] value_2

Second array to the difference operation.

@return [Array]

# File lib/xcodeproj/differ.rb, line 248
def self.array_non_unique_diff(value_1, value_2)
  value_2_elements_by_count = value_2.reduce({}) do |hash, element|
    updated_element_hash = hash.key?(element) ? { element => hash[element] + 1 } : { element => 1 }
    hash.merge(updated_element_hash)
  end

  value_1_elements_by_deletions =
    value_1.to_set.map do |element|
      times_to_delete_element = value_2_elements_by_count[element] || 0
      next [element, times_to_delete_element]
    end.to_h

  value_1.select do |element|
    if value_1_elements_by_deletions[element] > 0
      value_1_elements_by_deletions[element] -= 1
      next false
    end
    next true
  end
end
clean_hash(hash, key) click to toggle source

Returns a copy of the hash where the given key is removed recursively.

@param [Hash] hash

The hash to clean

@param [Object] key

The key to remove.

@return [Hash] A copy of the hash without the key.

# File lib/xcodeproj/differ.rb, line 187
def self.clean_hash(hash, key)
  new_hash = hash.dup
  self.clean_hash!(new_hash, key)
  new_hash
end
clean_hash!(hash, key) click to toggle source

Recursively cleans a key from the given hash.

@param [Hash] hash

The hash to clean

@param [Object] key

The key to remove.

@return [void]

# File lib/xcodeproj/differ.rb, line 203
def self.clean_hash!(hash, key)
  hash.delete(key)
  hash.each do |_, value|
    case value
    when Hash
      clean_hash!(value, key)
    when Array
      value.each { |entry| clean_hash!(entry, key) if entry.is_a?(Hash) }
    end
  end
end
diff(value_1, value_2, options = {}) click to toggle source

Computes the recursive difference of two given values.

@param [Object] value_1

The first value to compare.

@param [Object] value_2

The second value to compare.

@param [Object] key_1

The key for the diff of value_1.

@param [Object] key_2

The key for the diff of value_2.

@param [Object] id_key

The key used to identify correspondent hashes in an array.

@return [Hash] The diff @return [Nil] if the given values are equal.

# File lib/xcodeproj/differ.rb, line 45
def self.diff(value_1, value_2, options = {})
  options[:key_1] ||= 'value_1'
  options[:key_2] ||= 'value_2'
  options[:id_key] ||= nil

  method = if value_1.class == value_2.class
             case value_1
             when Hash  then :hash_diff
             when Array then :array_diff
             else :generic_diff
             end
           else
             :generic_diff
           end
  send(method, value_1, value_2, options)
end
ensure_class(object, klass) click to toggle source

Ensures that the given object belongs to the given class.

@param [Object] object

The object to check.

@param [Class] klass

the expected class of the object.

@raise If the object doesn’t belong to the given class.

@return [void]

# File lib/xcodeproj/differ.rb, line 233
def self.ensure_class(object, klass)
  raise "Wrong type `#{object.inspect}`" unless object.is_a?(klass)
end
generic_diff(value_1, value_2, options) click to toggle source

Returns the diff of two generic objects.

@see diff

# File lib/xcodeproj/differ.rb, line 162
def self.generic_diff(value_1, value_2, options)
  return nil if value_1 == value_2

  {
    options[:key_1] => value_1,
    options[:key_2] => value_2,
  }
end
hash_diff(value_1, value_2, options) click to toggle source

Computes the recursive difference of two hashes.

@see diff

# File lib/xcodeproj/differ.rb, line 85
def self.hash_diff(value_1, value_2, options)
  ensure_class(value_1, Hash)
  ensure_class(value_2, Hash)
  return nil if value_1 == value_2

  result = {}
  all_keys = (value_1.keys + value_2.keys).uniq
  all_keys.each do |key|
    key_value_1 = value_1[key]
    key_value_2 = value_2[key]
    diff = diff(key_value_1, key_value_2, options)
    if diff
      result[key] = diff if diff
    end
  end
  if result.empty?
    nil
  else
    result
  end
end
project_diff(project_1, project_2, key_1 = 'project_1', key_2 = 'project_2') click to toggle source

Optimized for reducing the noise from the tree hash of projects

# File lib/xcodeproj/differ.rb, line 64
def self.project_diff(project_1, project_2, key_1 = 'project_1', key_2 = 'project_2')
  project_1 = project_1.to_tree_hash unless project_1.is_a?(Hash)
  project_2 = project_2.to_tree_hash unless project_2.is_a?(Hash)
  options = {
    :key_1  => key_1,
    :key_2  => key_2,
    :id_key => 'displayName',
  }
  diff(project_1, project_2, options)
end