Comment comparer deux hachages?

108

J'essaie de comparer deux Ruby Hashes en utilisant le code suivant:

#!/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 sortie à l'écran est le fichier complet de file2. Je sais pertinemment que les fichiers sont différents, mais le script ne semble pas le saisir.

dennismonsewicz
la source
doublon possible de Comparing ruby ​​hashes
Geoff Lanotte

Réponses:

161

Vous pouvez comparer les hachages directement pour l'égalité:

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


Vous pouvez convertir les hachages en tableaux, puis obtenir leur différence:

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}

Simplifier davantage:

Attribuer la différence via une structure ternaire:

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

Tout faire en une seule opération et supprimer la differencevariable:

  Hash[*(
  (hash3.size > hash1.size)    \
      ? hash3.to_a - hash1.to_a \
      : hash1.to_a - hash3.to_a
  ).flatten] 
=> {"c"=>3}
l'homme d'étain
la source
3
Y a-t-il de toute façon pour obtenir les différences entre les deux?
dennismonsewicz
5
Les hachages peuvent être de la même taille, mais contenir des valeurs différentes. Dans ce cas, Both hash1.to_a - hash3.to_aet hash3.to_a - hash1.to_apeut renvoyer des valeurs non vides hash1.size == hash3.size. La partie après EDIT n'est valide que si les hachages sont de taille différente.
ohaleck
3
Bien, mais j'aurais dû arrêter de fumer. A.size> B.size ne signifie pas nécessairement que A inclut B. Il faut encore prendre l'union des différences symétriques.
Gene
La comparaison directe de la sortie de .to_aéchouera lorsque les hachages égaux ont des clés dans un ordre différent: {a:1, b:2} == {b:2, a:1}=> true, {a:1, b:2}.to_a == {b:2, a:1}.to_a=> false
aidan
quel est le but de flattenet *? Pourquoi pas juste Hash[A.to_a - B.to_a]?
JeremyKun
34

Vous pouvez essayer la gemme hashdiff , qui permet une comparaison approfondie des hachages et des tableaux dans le hachage.

Ce qui suit est un exemple:

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
la source
4
J'ai eu des hachages assez profonds provoquant des échecs de test. En remplaçant le got_hash.should eql expected_hashpar, HashDiff.diff(got_hash, expected_hash).should eql []j'obtiens maintenant une sortie qui montre exactement ce dont j'ai besoin. Parfait!
davetapley
Wow, HashDiff est génial. Nous avons rapidement essayé de voir ce qui a changé dans un énorme tableau JSON imbriqué. Merci!
Jeff Wigal
Votre bijou est génial! Super utile lors de l'écriture de spécifications impliquant des manipulations JSON. THX.
Alain
2
Mon expérience avec HashDiff a été que cela fonctionne très bien pour les petits hachages, mais la vitesse de différence ne semble pas bien évoluer. Cela vaut la peine de comparer vos appels si vous pensez qu'il peut recevoir deux gros hachages et vous assurer que le temps de différence est dans votre tolérance.
David Bodow
L'utilisation de l' use_lcs: falseindicateur peut considérablement accélérer les comparaisons sur les hachages volumineux:Hashdiff.diff(b, a, use_lcs: false)
Eric Walker
15

Si vous souhaitez connaître la différence entre deux hachages, vous pouvez le faire:

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
la source
12

Rails désapprouve la diffméthode.

Pour un one-liner rapide:

hash1.to_s == hash2.to_s
Evan
la source
J'oublie toujours ça. Il existe de nombreux contrôles d'égalité qui sont faciles à utiliser to_s.
the Tin Man
17
Il échouera lorsque les hachages égaux auront des clés dans un ordre différent: {a:1, b:2} == {b:2, a:1}=> true, {a:1, b:2}.to_s == {b:2, a:1}.to_s=> false
aidan
2
Ce qui est une fonctionnalité! : D
Dave Morse
5

Vous pouvez utiliser une simple intersection de tableau, de cette façon, vous pouvez savoir ce qui diffère dans chaque hachage.

    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
la source
1

Si vous avez besoin d'une différence rapide et sale entre les hachages qui prend correctement en charge nil dans les valeurs, vous pouvez utiliser quelque chose comme

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
la source
1

Si vous voulez un diff bien formaté, vous pouvez le faire:

# Gemfile
gem 'awesome_print' # or gem install awesome_print

Et dans votre code:

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})

L'idée est d'utiliser une impression impressionnante pour formater et de différencier la sortie. Le diff ne sera pas exact, mais il est utile à des fins de débogage.

Benjamin Crouzier
la source
1

... et maintenant sous forme de module à appliquer à une variété de classes de collection (Hash parmi elles). Ce n'est pas une inspection approfondie, mais c'est 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
Sauveur de fer
la source
1

J'ai développé ceci pour comparer si deux hachages sont égaux

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

L'usage:

> 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
Victor
la source
0

qu'en est-il de convertir les deux hachage en_json et de les comparer en tant que chaîne? mais en gardant à l'esprit que

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

h1.to_json==h1.to_json
=> true
h1.to_json==h2.to_json
=> false
stbnrivas
la source
0

Voici un algorithme pour comparer en profondeur deux hachages, qui comparera également des tableaux imbriqués:

    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

La mise en oeuvre:

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
la source
-3

Que diriez-vous d'une autre approche plus simple:

require 'fileutils'
FileUtils.cmp(file1, file2)
Mike
la source
2
Cela n'a de sens que si vous avez besoin que les hachages soient identiques sur le disque. Deux fichiers qui sont différents sur le disque car les éléments de hachage sont dans des ordres différents, peuvent toujours contenir les mêmes éléments, et seront égaux en ce qui concerne Ruby une fois qu'ils seront chargés.
The Tin Man