ฉันจะเปรียบเทียบแฮชสองอันได้อย่างไร


108

ฉันกำลังพยายามเปรียบเทียบ Ruby Hashes สองตัวโดยใช้รหัสต่อไปนี้:

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

ผลลัพธ์ไปยังหน้าจอคือไฟล์แบบเต็มจาก file2 ฉันรู้ว่าไฟล์ต่างกัน แต่ดูเหมือนสคริปต์จะไม่รับ


อาจซ้ำกันได้ของการเปรียบเทียบแฮชทับทิม
Geoff Lanotte

คำตอบ:


161

คุณสามารถเปรียบเทียบแฮชโดยตรงเพื่อความเท่าเทียมกัน:

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


คุณสามารถแปลงแฮชเป็นอาร์เรย์จากนั้นรับความแตกต่าง:

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}

ทำให้ง่ายขึ้น:

การกำหนดความแตกต่างผ่านโครงสร้างด้านท้าย:

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

ดำเนินการทั้งหมดในการดำเนินการเดียวและกำจัดdifferenceตัวแปร:

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

3
จะมีความแตกต่างระหว่างทั้งสองหรือไม่?
dennismonsewicz

5
แฮชอาจมีขนาดเท่ากัน แต่มีค่าต่างกัน ในกรณีเช่นนี้ทั้งคู่hash1.to_a - hash3.to_aและ hash3.to_a - hash1.to_aอาจส่งคืนค่าที่ไม่ว่างhash1.size == hash3.sizeเปล่าแม้ว่า ส่วนหลังจากแก้ไขจะใช้ได้ก็ต่อเมื่อแฮชมีขนาดต่างกัน
ohaleck

3
ดี แต่ควรเลิกไปก่อน A.size> B.size ไม่จำเป็นต้องหมายถึง A รวมถึง B. ยังคงต้องใช้การรวมกันของความแตกต่างแบบสมมาตร
ยีน

การเปรียบเทียบผลลัพธ์ของ.to_aจะล้มเหลวโดยตรงเมื่อแฮชที่เท่ากันมีคีย์ในลำดับที่แตกต่างกัน: {a:1, b:2} == {b:2, a:1}=> true, {a:1, b:2}.to_a == {b:2, a:1}.to_a=> false
aidan

จุดประสงค์ของอะไรflattenและ*? ทำไมไม่เพียงHash[A.to_a - B.to_a]?
JeremyKun

34

คุณสามารถลองอัญมณีhashdiffซึ่งช่วยให้สามารถเปรียบเทียบแฮชและอาร์เรย์ในแฮชได้อย่างลึกซึ้ง

ต่อไปนี้เป็นตัวอย่าง:

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
ฉันมีแฮชที่ค่อนข้างลึกทำให้การทดสอบล้มเหลว โดยการแทนที่got_hash.should eql expected_hashด้วยHashDiff.diff(got_hash, expected_hash).should eql []ฉันจะได้ผลลัพธ์ซึ่งแสดงให้เห็นว่าฉันต้องการอะไร สมบูรณ์แบบ!
davetapley

ว้าว HashDiff สุดยอดมาก ทำงานอย่างรวดเร็วเพื่อพยายามดูว่ามีอะไรเปลี่ยนแปลงไปบ้างในอาร์เรย์ JSON ที่ซ้อนกันขนาดใหญ่ ขอบคุณ!
Jeff Wigal

อัญมณีของคุณยอดเยี่ยมมาก! มีประโยชน์มากเมื่อเขียนข้อกำหนดที่เกี่ยวข้องกับการปรับแต่ง JSON ขอบคุณ.
Alain

2
ประสบการณ์ของฉันกับ HashDiff พบว่ามันใช้งานได้ดีกับแฮชขนาดเล็ก แต่ความเร็วในการแพร่กระจายดูเหมือนจะไม่ปรับขนาดได้ดี คุ้มค่าที่จะเปรียบเทียบการโทรของคุณหากคุณคาดหวังว่ามันอาจได้รับแฮชขนาดใหญ่สองอันและตรวจสอบให้แน่ใจว่าเวลาที่แตกต่างอยู่ในความอดทนของคุณ
David Bodow

การใช้use_lcs: falseธงสามารถเร่งการเปรียบเทียบกับแฮชขนาดใหญ่ได้อย่างมีนัยสำคัญ:Hashdiff.diff(b, a, use_lcs: false)
Eric Walker

15

หากคุณต้องการให้แฮชสองตัวแตกต่างกันอย่างไรคุณสามารถทำได้:

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

ทางรถไฟถูกตำหนิdiffวิธี

สำหรับซับเดียวอย่างรวดเร็ว:

hash1.to_s == hash2.to_s

ฉันมักจะลืมเรื่องนี้ to_sมีจำนวนมากของการตรวจสอบความเสมอภาคที่ทำง่ายใช้เป็น
คนดีบุก

17
มันจะล้มเหลวเมื่อแฮชเท่ากันมีคีย์ในลำดับที่แตกต่างกัน: {a:1, b:2} == {b:2, a:1}=> true, {a:1, b:2}.to_s == {b:2, a:1}.to_s=> false
aidan

2
ซึ่งเป็นคุณสมบัติ! : D
Dave Morse

5

คุณสามารถใช้จุดตัดอาร์เรย์แบบธรรมดาวิธีนี้จะทำให้คุณทราบได้ว่าแฮชแต่ละตัวแตกต่างกันอย่างไร

    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

หากคุณต้องการความแตกต่างที่รวดเร็วและสกปรกระหว่างแฮชที่รองรับค่าศูนย์อย่างถูกต้องคุณสามารถใช้สิ่งต่างๆเช่น

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

หากคุณต้องการความแตกต่างในรูปแบบที่สวยงามคุณสามารถทำได้:

# Gemfile
gem 'awesome_print' # or gem install awesome_print

และในรหัสของคุณ:

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

แนวคิดคือการใช้การพิมพ์ที่ยอดเยี่ยมในการจัดรูปแบบและทำให้ผลลัพธ์แตกต่างกัน ความแตกต่างจะไม่แน่นอน แต่มีประโยชน์สำหรับการดีบัก


1

... และตอนนี้อยู่ในรูปแบบโมดูลที่จะนำไปใช้กับคลาสคอลเลกชันที่หลากหลาย (แฮชในหมู่พวกเขา) ไม่ใช่การตรวจสอบอย่างละเอียด แต่เป็นเรื่องง่าย

# 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

ฉันพัฒนาสิ่งนี้เพื่อเปรียบเทียบว่าแฮชสองอันเท่ากันหรือไม่

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

การใช้งาน:

> 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

สิ่งที่เกี่ยวกับการแปลงทั้ง hash to_json และเปรียบเทียบเป็นสตริง? แต่จำไว้ว่า

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

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

0

นี่คืออัลกอริทึมในการเปรียบเทียบแฮชสองอันอย่างลึกซึ้งซึ่งจะเปรียบเทียบอาร์เรย์ที่ซ้อนกัน:

    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

การนำไปใช้:

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

วิธีอื่นที่ง่ายกว่า:

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

2
นั่นจะมีความหมายก็ต่อเมื่อคุณต้องการให้แฮชเหมือนกันบนดิสก์ ไฟล์สองไฟล์ที่แตกต่างกันบนดิสก์เนื่องจากองค์ประกอบแฮชอยู่ในลำดับที่ต่างกันยังสามารถมีองค์ประกอบเดียวกันได้และจะเท่ากันเท่าที่ Ruby เกี่ยวข้องเมื่อโหลดแล้ว
The Tin Man
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.