Wie vergleiche ich zwei Hashes?

108

Ich versuche zwei Ruby Hashes mit dem folgenden Code zu vergleichen:

#!/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

Die Ausgabe auf dem Bildschirm ist die vollständige Datei aus Datei2. Ich weiß sicher, dass die Dateien unterschiedlich sind, aber das Skript scheint es nicht aufzunehmen.

dennismonsewicz
quelle
mögliches Duplikat des Vergleichens von Rubin-Hashes
Geoff Lanotte

Antworten:

161

Sie können Hashes direkt auf Gleichheit vergleichen:

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


Sie können die Hashes in Arrays konvertieren und dann ihren Unterschied ermitteln:

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}

Weiter vereinfachen:

Differenz über eine ternäre Struktur zuweisen:

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

Alles in einer Operation erledigen und die differenceVariable loswerden :

  Hash[*(
  (hash3.size > hash1.size)    \
      ? hash3.to_a - hash1.to_a \
      : hash1.to_a - hash3.to_a
  ).flatten] 
=> {"c"=>3}
der Blechmann
quelle
3
Gibt es überhaupt die Unterschiede zwischen den beiden zu bekommen?
Dennismonsewicz
5
Hashes können gleich groß sein, aber unterschiedliche Werte enthalten. In diesem Fall geben Both hash1.to_a - hash3.to_aund hash3.to_a - hash1.to_amöglicherweise nicht leere Werte zurück hash1.size == hash3.size. Der Teil nach EDIT ist nur gültig, wenn Hashes unterschiedlicher Größe sind.
Ohaleck
3
Schön, hätte aber schon vorher aufhören sollen. A.size> B.size bedeutet nicht unbedingt, dass A B enthält. Es muss noch die Vereinigung symmetrischer Differenzen vorgenommen werden.
Gene
Der direkte Vergleich der Ausgabe von .to_awird fehlschlagen, wenn gleiche Hashes Schlüssel in einer anderen Reihenfolge haben: {a:1, b:2} == {b:2, a:1}=> wahr, {a:1, b:2}.to_a == {b:2, a:1}.to_a=> falsch
aidan
Was ist der Zweck von flattenund *? Warum nicht einfach Hash[A.to_a - B.to_a]?
JeremyKun
34

Sie können den Hashdiff- Edelstein ausprobieren , der einen umfassenden Vergleich von Hashes und Arrays im Hash ermöglicht.

Das Folgende ist ein Beispiel:

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
quelle
4
Ich hatte einige ziemlich tiefe Hashes, die zu Testfehlern führten. Durch das Ersetzen der got_hash.should eql expected_hashmit HashDiff.diff(got_hash, expected_hash).should eql []mir jetzt Ausgang, zeigt genau das, was ich brauche. Perfekt!
Davetapley
Wow, HashDiff ist großartig. Wir haben schnell versucht zu sehen, was sich in einem riesigen verschachtelten JSON-Array geändert hat. Vielen Dank!
Jeff Wigal
Dein Juwel ist großartig! Super hilfreich beim Schreiben von Spezifikationen mit JSON-Manipulationen. Vielen Dank.
Alain
2
Meine Erfahrung mit HashDiff hat gezeigt, dass es für kleine Hashes sehr gut funktioniert, aber die Diff-Geschwindigkeit scheint nicht gut zu skalieren. Es lohnt sich, Ihre Anrufe zu vergleichen, wenn Sie erwarten, dass zwei große Hashes eingespeist werden, und sicherzustellen, dass die Differenzzeit innerhalb Ihrer Toleranz liegt.
David Bodow
Die Verwendung der use_lcs: falseFlagge kann Vergleiche mit großen Hashes erheblich beschleunigen:Hashdiff.diff(b, a, use_lcs: false)
Eric Walker
15

Wenn Sie den Unterschied zwischen zwei Hashes ermitteln möchten, können Sie Folgendes tun:

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

Schienen deprecating das diffVerfahren.

Für einen schnellen Einzeiler:

hash1.to_s == hash2.to_s
Evan
quelle
Ich vergesse das immer. Es gibt viele Gleichstellungsprüfungen, die einfach zu handhaben sind to_s.
der Blechmann
17
Es wird fehlschlagen, wenn gleiche Hashes Schlüssel in einer anderen Reihenfolge haben: {a:1, b:2} == {b:2, a:1}=> wahr, {a:1, b:2}.to_s == {b:2, a:1}.to_s=> falsch
aidan
2
Welches ist eine Funktion! : D
Dave Morse
5

Sie können einen einfachen Array-Schnittpunkt verwenden, um zu erkennen, was sich in den einzelnen Hashs unterscheidet.

    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
quelle
1

Wenn Sie einen schnellen und schmutzigen Unterschied zwischen Hashes benötigen, der Nullwerte korrekt unterstützt, können Sie so etwas wie verwenden

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
quelle
1

Wenn Sie ein schön formatiertes Diff wünschen, können Sie dies tun:

# Gemfile
gem 'awesome_print' # or gem install awesome_print

Und in Ihrem 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})

Die Idee ist, großartigen Druck zu verwenden, um die Ausgabe zu formatieren und zu differenzieren. Der Unterschied ist nicht genau, aber für Debugging-Zwecke nützlich.

Benjamin Crouzier
quelle
1

... und jetzt in Modulform für eine Vielzahl von Sammlungsklassen (darunter auch Hash). Es ist keine gründliche Inspektion, aber es ist einfach.

# 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
Eisenretter
quelle
1

Ich habe dies entwickelt, um zu vergleichen, ob zwei Hashes gleich sind

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

Die Verwendung:

> 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
Sieger
quelle
0

Was ist mit dem Konvertieren von Hash in_json und dem Vergleichen als Zeichenfolge? aber denken Sie daran

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

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

Hier ist ein Algorithmus zum gründlichen Vergleichen von zwei Hashes, mit dem auch verschachtelte Arrays verglichen werden:

    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

Implementierung:

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

Wie wäre es mit einem anderen, einfacheren Ansatz:

require 'fileutils'
FileUtils.cmp(file1, file2)
Mike
quelle
2
Dies ist nur dann sinnvoll, wenn die Hashes auf der Festplatte identisch sein müssen. Zwei Dateien, die sich auf der Festplatte unterscheiden, weil die Hash-Elemente in unterschiedlicher Reihenfolge vorliegen, immer noch dieselben Elemente enthalten können und nach dem Laden für Ruby gleich sind.
der Blechmann