Làm cách nào để so sánh hai hàm băm?


108

Tôi đang cố gắng so sánh hai Ruby Hashes bằng cách sử dụng mã sau:

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

Kết quả xuất ra màn hình là tập tin đầy đủ từ file2. Tôi biết thực tế là các tệp khác nhau, nhưng tập lệnh dường như không chọn nó.


có thể trùng lặp của So sánh băm ruby
Geoff Lanotte

Câu trả lời:


161

Bạn có thể so sánh các hàm băm trực tiếp để có sự bình đẳng:

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


Bạn có thể chuyển đổi các băm thành mảng, sau đó nhận được sự khác biệt của chúng:

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}

Đơn giản hóa thêm:

Gán sự khác biệt thông qua cấu trúc bậc ba:

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

Thực hiện tất cả trong một thao tác và loại bỏ differencebiến:

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

3
Có cách nào để có được sự khác biệt giữa hai?
dennismonsewicz

5
Các băm có thể có cùng kích thước, nhưng chứa các giá trị khác nhau. Trong trường hợp đó, cả hai hash1.to_a - hash3.to_ahash3.to_a - hash1.to_acó thể trả về các giá trị khác hash1.size == hash3.size. Phần sau khi CHỈNH SỬA chỉ hợp lệ nếu các hàm băm có kích thước khác nhau.
ohaleck

3
Tốt, nhưng đáng lẽ phải bỏ cuộc khi đang ở phía trước. A.size> B.size không nhất thiết có nghĩa là A bao gồm B. Vẫn cần có sự kết hợp của sự khác biệt đối xứng.
Gene

So sánh trực tiếp sản lượng .to_asẽ thất bại khi băm bằng có phím theo một thứ tự khác nhau: {a:1, b:2} == {b:2, a:1}=> đúng, {a:1, b:2}.to_a == {b:2, a:1}.to_a=> sai
Aidan

mục đích của flattenvà là *gì? Tại sao không chỉ Hash[A.to_a - B.to_a]?
JeremyKun

34

Bạn có thể thử đá quý hashdiff , cho phép so sánh sâu các hàm băm và mảng trong hàm băm.

Sau đây là một ví dụ:

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
Tôi đã có một số băm khá sâu gây ra lỗi thử nghiệm. Bằng cách thay thế got_hash.should eql expected_hashbằng HashDiff.diff(got_hash, expected_hash).should eql []tôi bây giờ nhận được đầu ra hiển thị chính xác những gì tôi cần. Hoàn hảo!
davetapley

Chà, HashDiff thật tuyệt vời. Thực hiện công việc nhanh chóng để cố gắng xem những gì đã thay đổi trong một mảng JSON lồng nhau khổng lồ. Cảm ơn!
Jeff Wigal

Đá quý của bạn thật tuyệt vời! Rất hữu ích khi viết các thông số kỹ thuật liên quan đến các thao tác JSON. Cám ơn.
Alain

2
Kinh nghiệm của tôi với HashDiff là nó hoạt động thực sự tốt đối với các hàm băm nhỏ nhưng tốc độ khác biệt dường như không mở rộng tốt. Đáng giá điểm chuẩn các cuộc gọi của bạn đến nó nếu bạn mong đợi nó có thể nhận được hai hàm băm lớn và đảm bảo rằng thời gian khác biệt nằm trong khả năng chịu đựng của bạn.
David Bodow

Sử dụng use_lcs: falsecờ có thể tăng tốc đáng kể so sánh trên các hàm băm lớn:Hashdiff.diff(b, a, use_lcs: false)
Eric Walker

15

Nếu bạn muốn biết sự khác biệt giữa hai hàm băm là gì, bạn có thể làm như sau:

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

Rails là ti các diffphương pháp.

Để có một lớp lót nhanh chóng:

hash1.to_s == hash2.to_s

Tôi luôn quên điều này. Có rất nhiều kiểm tra bình đẳng được thực hiện dễ dàng to_s.
the Tin Man,

17
Nó sẽ thất bại khi băm bằng có phím theo một thứ tự khác nhau: {a:1, b:2} == {b:2, a:1}=> đúng, {a:1, b:2}.to_s == {b:2, a:1}.to_s=> sai
Aidan

2
Đó là một tính năng! : D
Dave Morse

5

Bạn có thể sử dụng một giao điểm mảng đơn giản, bằng cách này, bạn có thể biết những gì khác nhau trong mỗi hàm băm.

    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

Nếu bạn cần một sự khác biệt nhanh chóng và bẩn thỉu giữa các hàm băm hỗ trợ chính xác các giá trị nil, bạn có thể sử dụng một cái gì đó như

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

Nếu bạn muốn có một khác biệt được định dạng độc đáo, bạn có thể làm như sau:

# Gemfile
gem 'awesome_print' # or gem install awesome_print

Và trong mã của bạn:

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

Ý tưởng là sử dụng bản in tuyệt vời để định dạng và khác biệt đầu ra. Sự khác biệt sẽ không chính xác, nhưng nó hữu ích cho mục đích gỡ lỗi.


1

... và bây giờ ở dạng mô-đun được áp dụng cho nhiều lớp tập hợp khác nhau (Băm trong số đó). Nó không phải là một cuộc kiểm tra sâu, nhưng nó đơn giản.

# 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

Tôi đã phát triển cái này để so sánh xem hai hàm băm có bằng nhau không

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

Việc sử dụng:

> 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

những gì về chuyển đổi cả hash to_json và so sánh dưới dạng chuỗi? nhưng hãy nhớ rằng

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

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

0

Đây là thuật toán để so sánh sâu hai hàm băm, cũng sẽ so sánh các Mảng lồng nhau:

    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

Thực hiệ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

-3

Còn cách tiếp cận khác đơn giản hơn thì sao:

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

2
Điều đó chỉ có ý nghĩa nếu bạn cần các băm giống hệt nhau trên đĩa. Hai tệp khác nhau trên đĩa vì các phần tử băm có thứ tự khác nhau, vẫn có thể chứa các phần tử giống nhau và sẽ bằng nhau theo như Ruby liên quan khi chúng được tải.
Tin Man
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.