¿Cómo comparo dos hashes?

108

Estoy tratando de comparar dos Ruby Hashes usando el siguiente 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

La salida a la pantalla es el archivo completo de file2. Sé a ciencia cierta que los archivos son diferentes, pero el script no parece captarlo.

dennismonsewicz
fuente
posible duplicado de Comparación de hashes de rubí
Geoff Lanotte

Respuestas:

161

Puede comparar hashes directamente para la igualdad:

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


Puede convertir los hash en matrices y luego obtener su diferencia:

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 aún más:

Asignar diferencia a través de una estructura ternaria:

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

Haciéndolo todo en una sola operación y deshaciéndose de la differencevariable:

  Hash[*(
  (hash3.size > hash1.size)    \
      ? hash3.to_a - hash1.to_a \
      : hash1.to_a - hash3.to_a
  ).flatten] 
=> {"c"=>3}
el hombre de hojalata
fuente
3
¿Hay alguna forma de obtener las diferencias entre los dos?
dennismonsewicz
5
Los hash pueden ser del mismo tamaño, pero contener valores diferentes. En tal caso, ambos hash1.to_a - hash3.to_ay hash3.to_a - hash1.to_apueden devolver valores no vacíos hash1.size == hash3.size. La parte posterior a EDIT solo es válida si los hashes son de diferente tamaño.
ohaleck
3
Bien, pero debería haberlo dejado antes. A. tamaño> B. tamaño no significa necesariamente que A incluye B. Aún es necesario tomar la unión de diferencias simétricas.
Gene
La comparación directa de la salida de .to_afallará cuando los hash iguales tengan claves en un orden diferente: {a:1, b:2} == {b:2, a:1}=> verdadero, {a:1, b:2}.to_a == {b:2, a:1}.to_a=> falso
aidan
¿cuál es el propósito de flatteny *? ¿Por qué no simplemente Hash[A.to_a - B.to_a]?
JeremyKun
34

Puede probar la gema hashdiff , que permite una comparación profunda de hashes y matrices en el hash.

Lo siguiente es un ejemplo:

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
fuente
4
Tuve algunos hash bastante profundos que causaron fallas en las pruebas. Al reemplazar el got_hash.should eql expected_hashcon HashDiff.diff(got_hash, expected_hash).should eql [], ahora obtengo un resultado que muestra exactamente lo que necesito. ¡Perfecto!
davetapley
Vaya, HashDiff es increíble. Hizo un trabajo rápido al intentar ver qué ha cambiado en una enorme matriz JSON anidada. ¡Gracias!
Jeff Wigal
¡Tu joya es increíble! Muy útil al escribir especificaciones que involucren manipulaciones JSON. Gracias.
Alain
2
Mi experiencia con HashDiff ha sido que funciona muy bien para pequeños hashes, pero la velocidad de diferencia no parece escalar bien. Vale la pena comparar sus llamadas si espera que se alimenten con dos hashes grandes y asegurarse de que el tiempo de diferencia esté dentro de su tolerancia.
David Bodow
El uso de la use_lcs: falsebandera puede acelerar significativamente las comparaciones en hash grandes:Hashdiff.diff(b, a, use_lcs: false)
Eric Walker
15

Si desea obtener cuál es la diferencia entre dos hashes, puede hacer esto:

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
fuente
12

Rails está desaprobando el diffmétodo.

Para una línea rápida:

hash1.to_s == hash2.to_s
Evan
fuente
Siempre me olvido de esto. Hay muchos controles de igualdad que son fáciles de usar to_s.
The Tin Man
17
Fallará cuando los hash iguales tengan claves en un orden diferente: {a:1, b:2} == {b:2, a:1}=> verdadero, {a:1, b:2}.to_s == {b:2, a:1}.to_s=> falso
aidan
2
¡Qué es una característica! : D
Dave Morse
5

Puede usar una intersección de matriz simple, de esta manera puede saber qué difiere en 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
fuente
1

Si necesita una diferencia rápida y sucia entre hashes que admita correctamente valores nulos, puede 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
fuente
1

Si desea una diferencia con un formato agradable, puede hacer esto:

# Gemfile
gem 'awesome_print' # or gem install awesome_print

Y en tu 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})

La idea es utilizar una impresión impresionante para formatear y diferenciar la salida. La diferencia no será exacta, pero es útil para depurar.

Benjamin Crouzier
fuente
1

... y ahora en forma de módulo para ser aplicado a una variedad de clases de colección (Hash entre ellas). No es una inspección profunda, pero es simple.

# 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 hierro
fuente
1

Desarrollé esto para comparar si dos hashes son iguales

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

El 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
Víctor
fuente
0

¿qué hay de convertir hash to_json y comparar como cadena? pero teniendo en cuenta que

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

h1.to_json==h1.to_json
=> true
h1.to_json==h2.to_json
=> false
stbnrivas
fuente
0

Aquí hay un algoritmo para comparar en profundidad dos Hashes, que también compararán matrices anidadas:

    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

Implementación:

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
fuente
-3

¿Qué tal otro enfoque más simple?

require 'fileutils'
FileUtils.cmp(file1, file2)
Miguel
fuente
2
Eso solo es significativo si necesita que los hash sean idénticos en el disco. Dos archivos que son diferentes en el disco porque los elementos hash están en diferentes órdenes, aún pueden contener los mismos elementos y serán iguales en lo que respecta a Ruby una vez que se carguen.
The Tin Man