Bagaimana cara membandingkan dua hash?


108

Saya mencoba membandingkan dua Ruby Hashes menggunakan kode berikut:

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

Output ke layar adalah file lengkap dari file2. Saya tahu pasti bahwa file-file itu berbeda, tetapi skrip sepertinya tidak mengambilnya.


kemungkinan duplikat dari Membandingkan hash ruby
Geoff Lanotte

Jawaban:


161

Anda dapat membandingkan hash secara langsung untuk persamaan:

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


Anda dapat mengonversi hash menjadi array, lalu dapatkan perbedaannya:

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}

Menyederhanakan lebih lanjut:

Menetapkan perbedaan melalui struktur terner:

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

Melakukan semuanya dalam satu operasi dan menyingkirkan differencevariabel:

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

3
Apakah ada cara untuk mendapatkan perbedaan di antara keduanya?
dennismonsewicz

5
Hash boleh berukuran sama, tetapi berisi nilai yang berbeda. Dalam kasus seperti itu Baik hash1.to_a - hash3.to_adan hash3.to_a - hash1.to_adapat mengembalikan nilai tidak kosong hash1.size == hash3.size. Bagian setelah EDIT hanya valid jika hash memiliki ukuran yang berbeda.
ohaleck

3
Bagus, tapi harus berhenti dulu. A. size> B. size tidak selalu berarti A termasuk B. Masih perlu mengambil gabungan perbedaan simetris.
Gene

Membandingkan langsung output dari .to_aakan gagal ketika hash yang sama memiliki kunci dalam urutan yang berbeda: {a:1, b:2} == {b:2, a:1}=> true, {a:1, b:2}.to_a == {b:2, a:1}.to_a=> false
aidan

apa tujuan flattendan *? Kenapa tidak Hash[A.to_a - B.to_a]?
JeremyKun

34

Anda dapat mencoba permata hashdiff , yang memungkinkan perbandingan mendalam antara hash dan array dalam hash.

Berikut ini adalah contohnya:

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]]

4
Saya memiliki beberapa hash yang cukup dalam yang menyebabkan kegagalan pengujian. Dengan mengganti got_hash.should eql expected_hashdengan HashDiff.diff(got_hash, expected_hash).should eql []saya sekarang mendapatkan keluaran yang menunjukkan dengan tepat apa yang saya butuhkan. Sempurna!
davetapley

Wow, HashDiff luar biasa. Bekerja cepat dengan mencoba melihat apa yang telah berubah dalam larik JSON bersarang yang besar. Terima kasih!
Jeff Wigal

Permata Anda luar biasa! Sangat membantu saat menulis spesifikasi yang melibatkan manipulasi JSON. Terima kasih.
Alain

2
Pengalaman saya dengan HashDiff adalah ia bekerja sangat baik untuk hash kecil tetapi kecepatan diff tampaknya tidak berskala dengan baik. Layak untuk membandingkan panggilan Anda ke sana jika Anda berharap itu mungkin mendapatkan dua hash besar dan memastikan bahwa waktu yang berbeda berada dalam toleransi Anda.
David Bodow

Menggunakan use_lcs: falsebendera secara signifikan dapat mempercepat perbandingan pada hash besar:Hashdiff.diff(b, a, use_lcs: false)
Eric Walker

15

Jika Anda ingin mendapatkan perbedaan antara dua hash, Anda dapat melakukan ini:

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

12

Rel yang mencela paradiff metode.

Untuk satu baris cepat:

hash1.to_s == hash2.to_s

Saya selalu melupakan ini. Ada banyak pemeriksaan kesetaraan yang mudah digunakan to_s.
Manusia Timah

17
Ini akan gagal ketika hash yang sama memiliki kunci dalam urutan yang berbeda: {a:1, b:2} == {b:2, a:1}=> true, {a:1, b:2}.to_s == {b:2, a:1}.to_s=> false
aidan

2
Yang merupakan fitur! : D
Dave Morse

5

Anda bisa menggunakan perpotongan array sederhana, dengan cara ini Anda bisa mengetahui apa yang berbeda di setiap 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


1

Jika Anda membutuhkan perbedaan cepat dan kotor antara hash yang dengan benar mendukung nil dalam nilai, Anda dapat menggunakan sesuatu seperti

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

1

Jika Anda menginginkan diff berformat bagus, Anda dapat melakukan ini:

# Gemfile
gem 'awesome_print' # or gem install awesome_print

Dan di kode Anda:

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

Idenya adalah menggunakan cetakan yang luar biasa untuk memformat, dan membedakan hasilnya. Perbedaannya tidak persis sama, tetapi berguna untuk tujuan debugging.


1

... dan sekarang dalam bentuk modul untuk diterapkan ke berbagai kelas koleksi (ada hash di antara mereka). Ini bukan pemeriksaan yang mendalam, tapi sederhana.

# 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

1

Saya mengembangkan ini untuk membandingkan jika dua hash sama

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

Penggunaan:

> 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


0

bagaimana dengan mengonversi hash to_json dan membandingkannya sebagai string? tapi ingatlah itu

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

h1.to_json==h1.to_json
=> true
h1.to_json==h2.to_json
=> false

0

Berikut adalah algoritma untuk membandingkan dua Hash secara mendalam, yang juga akan membandingkan Array bersarang:

    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

Penerapan:

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

-3

Bagaimana dengan pendekatan lain yang lebih sederhana:

require 'fileutils'
FileUtils.cmp(file1, file2)

2
Itu hanya berarti jika Anda membutuhkan hash yang identik pada disk. Dua file yang berbeda pada disk karena elemen hash berada dalam urutan yang berbeda, masih dapat berisi elemen yang sama, dan akan sama sejauh menyangkut Ruby setelah mereka dimuat.
the Tin Man
Dengan menggunakan situs kami, Anda mengakui telah membaca dan memahami Kebijakan Cookie dan Kebijakan Privasi kami.
Licensed under cc by-sa 3.0 with attribution required.