Como posso comparar dois hashes?

108

Estou tentando comparar dois Ruby Hashes usando o seguinte código:

#!/usr/bin/env ruby

require "yaml"
require "active_support"

file1 = YAML::load(File.open('./en_20110207.yml'))
file2 = YAML::load(File.open('./locales/en.yml'))

arr = []

file1.select { |k,v|
  file2.select { |k2, v2|
    arr << "#{v2}" if "#{v}" != "#{v2}"
  }
}

puts arr

A saída para a tela é o arquivo completo do arquivo2. Sei com certeza que os arquivos são diferentes, mas o script não parece detectá-los.

Dennismonsewicz
fonte
possível duplicata de Comparing ruby ​​hashes
Geoff Lanotte

Respostas:

161

Você pode comparar hashes diretamente para igualdade:

hash1 = {'a' => 1, 'b' => 2}
hash2 = {'a' => 1, 'b' => 2}
hash3 = {'a' => 1, 'b' => 2, 'c' => 3}

hash1 == hash2 # => true
hash1 == hash3 # => false

hash1.to_a == hash2.to_a # => true
hash1.to_a == hash3.to_a # => false


Você pode converter os hashes em matrizes e obter a diferença:

hash3.to_a - hash1.to_a # => [["c", 3]]

if (hash3.size > hash1.size)
  difference = hash3.to_a - hash1.to_a
else
  difference = hash1.to_a - hash3.to_a
end
Hash[*difference.flatten] # => {"c"=>3}

Simplificando ainda mais:

Atribuição de diferença por meio de uma estrutura ternária:

  difference = (hash3.size > hash1.size) \
                ? hash3.to_a - hash1.to_a \
                : hash1.to_a - hash3.to_a
=> [["c", 3]]
  Hash[*difference.flatten] 
=> {"c"=>3}

Fazer tudo em uma operação e se livrar da differencevariável:

  Hash[*(
  (hash3.size > hash1.size)    \
      ? hash3.to_a - hash1.to_a \
      : hash1.to_a - hash3.to_a
  ).flatten] 
=> {"c"=>3}
o homem de lata
fonte
3
Existe alguma maneira de obter as diferenças entre os dois?
dennismonsewicz
5
Hashes podem ter o mesmo tamanho, mas conter valores diferentes. Nesse caso, hash1.to_a - hash3.to_ae hash3.to_a - hash1.to_apodem retornar valores não vazios hash1.size == hash3.size. A parte após EDIT é válida apenas se os hashes forem de tamanhos diferentes.
ohaleck
3
Legal, mas deveria ter desistido antes. A.size> B.size não significa necessariamente que A inclui B. Ainda é necessário fazer a união das diferenças simétricas.
Gene
A comparação direta da saída de .to_afalhará quando os hashes iguais tiverem chaves em uma ordem diferente: {a:1, b:2} == {b:2, a:1}=> verdadeiro, {a:1, b:2}.to_a == {b:2, a:1}.to_a=> falso
aidan
qual é o propósito de flattene *? Por que não apenas Hash[A.to_a - B.to_a]?
JeremyKun
34

Você pode tentar a gema hashdiff , que permite uma comparação profunda de hashes e matrizes no hash.

O seguinte é um exemplo:

a = {a:{x:2, y:3, z:4}, b:{x:3, z:45}}
b = {a:{y:3}, b:{y:3, z:30}}

diff = HashDiff.diff(a, b)
diff.should == [['-', 'a.x', 2], ['-', 'a.z', 4], ['-', 'b.x', 3], ['~', 'b.z', 45, 30], ['+', 'b.y', 3]]
liu fengyun
fonte
4
Tive alguns hashes bastante profundos causando falhas de teste. Ao substituir o got_hash.should eql expected_hashpor HashDiff.diff(got_hash, expected_hash).should eql [], agora obtenho uma saída que mostra exatamente o que preciso. Perfeito!
davetapley
Uau, HashDiff é incrível. Rapidamente tentou ver o que mudou em uma enorme matriz JSON aninhada. Obrigado!
Jeff Wigal
Sua joia é incrível! Muito útil ao escrever especificações envolvendo manipulações JSON. THX.
Alain
2
Minha experiência com o HashDiff é que ele funciona muito bem para hashes pequenos, mas a velocidade do diff não parece escalar bem. Vale a pena comparar suas chamadas a ele se você espera que ele seja alimentado com dois hashes grandes e certifique-se de que o tempo de diferença está dentro de sua tolerância.
David Bodow
Usar o use_lcs: falsesinalizador pode acelerar significativamente as comparações em hashes grandes:Hashdiff.diff(b, a, use_lcs: false)
Eric Walker
15

Se quiser saber qual é a diferença entre dois hashes, você pode fazer o seguinte:

h1 = {:a => 20, :b => 10, :c => 44}
h2 = {:a => 2, :b => 10, :c => "44"}
result = {}
h1.each {|k, v| result[k] = h2[k] if h2[k] != v }
p result #=> {:a => 2, :c => "44"}
Guilherme Bernal
fonte
12

Rails está descontinuando o diffmétodo.

Para uma linha rápida:

hash1.to_s == hash2.to_s
Evan
fonte
Eu sempre esqueço disso. Existem muitas verificações de igualdade que são fáceis de usar to_s.
The Tin Man de
17
Ele falhará quando hashes iguais tiverem chaves em uma ordem diferente: {a:1, b:2} == {b:2, a:1}=> true, {a:1, b:2}.to_s == {b:2, a:1}.to_s=> false
aidan
2
O que é um recurso! : D
Dave Morse
5

Você poderia usar uma interseção de matriz simples, dessa forma você pode saber o que difere em cada hash.

    hash1 = { a: 1 , b: 2 }
    hash2 = { a: 2 , b: 2 }

    overlapping_elements = hash1.to_a & hash2.to_a

    exclusive_elements_from_hash1 = hash1.to_a - overlapping_elements
    exclusive_elements_from_hash2 = hash2.to_a - overlapping_elements
ErvalhouS
fonte
1

Se você precisa de uma diferença rápida e suja entre os hashes que suporta corretamente nulo em valores você pode usar algo como

def diff(one, other)
  (one.keys + other.keys).uniq.inject({}) do |memo, key|
    unless one.key?(key) && other.key?(key) && one[key] == other[key]
      memo[key] = [one.key?(key) ? one[key] : :_no_key, other.key?(key) ? other[key] : :_no_key]
    end
    memo
  end
end
Dolzenko
fonte
1

Se você quiser um diff bem formatado, pode fazer o seguinte:

# Gemfile
gem 'awesome_print' # or gem install awesome_print

E em seu código:

require 'ap'

def my_diff(a, b)
  as = a.ai(plain: true).split("\n").map(&:strip)
  bs = b.ai(plain: true).split("\n").map(&:strip)
  ((as - bs) + (bs - as)).join("\n")
end

puts my_diff({foo: :bar, nested: {val1: 1, val2: 2}, end: :v},
             {foo: :bar, n2: {nested: {val1: 1, val2: 3}}, end: :v})

A ideia é usar uma impressão incrível para formatar e diferenciar a saída. O diff não será exato, mas é útil para fins de depuração.

Benjamin Crouzier
fonte
1

... e agora na forma de módulo para ser aplicado a uma variedade de classes de coleção (Hash entre elas). Não é uma inspeção profunda, mas é simples.

# Enable "diffing" and two-way transformations between collection objects
module Diffable
  # Calculates the changes required to transform self to the given collection.
  # @param b [Enumerable] The other collection object
  # @return [Array] The Diff: A two-element change set representing items to exclude and items to include
  def diff( b )
    a, b = to_a, b.to_a
    [a - b, b - a]
  end

  # Consume return value of Diffable#diff to produce a collection equal to the one used to produce the given diff.
  # @param to_drop [Enumerable] items to exclude from the target collection
  # @param to_add  [Enumerable] items to include in the target collection
  # @return [Array] New transformed collection equal to the one used to create the given change set
  def apply_diff( to_drop, to_add )
    to_a - to_drop + to_add
  end
end

if __FILE__ == $0
  # Demo: Hashes with overlapping keys and somewhat random values.
  Hash.send :include, Diffable
  rng = Random.new
  a = (:a..:q).to_a.reduce(Hash[]){|h,k| h.merge! Hash[k, rng.rand(2)] }
  b = (:i..:z).to_a.reduce(Hash[]){|h,k| h.merge! Hash[k, rng.rand(2)] }
  raise unless a == Hash[ b.apply_diff(*b.diff(a)) ] # change b to a
  raise unless b == Hash[ a.apply_diff(*a.diff(b)) ] # change a to b
  raise unless a == Hash[ a.apply_diff(*a.diff(a)) ] # change a to a
  raise unless b == Hash[ b.apply_diff(*b.diff(b)) ] # change b to b
end
Salvador de ferro
fonte
1

Eu desenvolvi isso para comparar se dois hashes são iguais

def hash_equal?(hash1, hash2)
  array1 = hash1.to_a
  array2 = hash2.to_a
  (array1 - array2 | array2 - array1) == []
end

O uso:

> hash_equal?({a: 4}, {a: 4})
=> true
> hash_equal?({a: 4}, {b: 4})
=> false

> hash_equal?({a: {b: 3}}, {a: {b: 3}})
=> true
> hash_equal?({a: {b: 3}}, {a: {b: 4}})
=> false

> hash_equal?({a: {b: {c: {d: {e: {f: {g: {h: 1}}}}}}}}, {a: {b: {c: {d: {e: {f: {g: {h: 1}}}}}}}})
=> true
> hash_equal?({a: {b: {c: {d: {e: {f: {g: {marino: 1}}}}}}}}, {a: {b: {c: {d: {e: {f: {g: {h: 2}}}}}}}})
=> false
Vencedor
fonte
0

que tal converter o hash para_json e comparar como string? mas tendo em mente que

require "json"
h1 = {a: 20}
h2 = {a: "20"}

h1.to_json==h1.to_json
=> true
h1.to_json==h2.to_json
=> false
Stbnrivas
fonte
0

Aqui está um algoritmo para comparar profundamente dois Hashes, que também comparará matrizes aninhadas:

    HashDiff.new(
      {val: 1, nested: [{a:1}, {b: [1, 2]}] },
      {val: 2, nested: [{a:1}, {b: [1]}] }
    ).report
# Output:
val:
- 1
+ 2
nested > 1 > b > 1:
- 2

Implementação:

class HashDiff

  attr_reader :left, :right

  def initialize(left, right, config = {}, path = nil)
    @left  = left
    @right = right
    @config = config
    @path = path
    @conformity = 0
  end

  def conformity
    find_differences
    @conformity
  end

  def report
    @config[:report] = true
    find_differences
  end

  def find_differences
    if hash?(left) && hash?(right)
      compare_hashes_keys
    elsif left.is_a?(Array) && right.is_a?(Array)
      compare_arrays
    else
      report_diff
    end
  end

  def compare_hashes_keys
    combined_keys.each do |key|
      l = value_with_default(left, key)
      r = value_with_default(right, key)
      if l == r
        @conformity += 100
      else
        compare_sub_items l, r, key
      end
    end
  end

  private

  def compare_sub_items(l, r, key)
    diff = self.class.new(l, r, @config, path(key))
    @conformity += diff.conformity
  end

  def report_diff
    return unless @config[:report]

    puts "#{@path}:"
    puts "- #{left}" unless left == NO_VALUE
    puts "+ #{right}" unless right == NO_VALUE
  end

  def combined_keys
    (left.keys + right.keys).uniq
  end

  def hash?(value)
    value.is_a?(Hash)
  end

  def compare_arrays
    l, r = left.clone, right.clone
    l.each_with_index do |l_item, l_index|
      max_item_index = nil
      max_conformity = 0
      r.each_with_index do |r_item, i|
        if l_item == r_item
          @conformity += 1
          r[i] = TAKEN
          break
        end

        diff = self.class.new(l_item, r_item, {})
        c = diff.conformity
        if c > max_conformity
          max_conformity = c
          max_item_index = i
        end
      end or next

      if max_item_index
        key = l_index == max_item_index ? l_index : "#{l_index}/#{max_item_index}"
        compare_sub_items l_item, r[max_item_index], key
        r[max_item_index] = TAKEN
      else
        compare_sub_items l_item, NO_VALUE, l_index
      end
    end

    r.each_with_index do |item, index|
      compare_sub_items NO_VALUE, item, index unless item == TAKEN
    end
  end

  def path(key)
    p = "#{@path} > " if @path
    "#{p}#{key}"
  end

  def value_with_default(obj, key)
    obj.fetch(key, NO_VALUE)
  end

  module NO_VALUE; end
  module TAKEN; end

end
Daniel Garmoshka
fonte
-3

Que tal outra abordagem mais simples:

require 'fileutils'
FileUtils.cmp(file1, file2)
Mike
fonte
2
Isso só é significativo se você precisar que os hashes sejam idênticos no disco. Dois arquivos que são diferentes no disco porque os elementos hash estão em ordens diferentes, ainda podem conter os mesmos elementos e serão iguais no que diz respeito ao Ruby depois de carregados.
o Homem de Lata de