İki karmayı nasıl karşılaştırırım?


108

Aşağıdaki kodu kullanarak iki Ruby Hash'ı karşılaştırmaya çalışıyorum:

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

Ekrandaki çıktı dosya2'den tam dosyadır. Dosyaların farklı olduğunu biliyorum, ama komut dosyası onu almıyor.


Yanıtlar:


161

Karmaları doğrudan eşitlik için karşılaştırabilirsiniz:

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


Karmaları dizilere dönüştürebilir, sonra farklarını elde edebilirsiniz:

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}

Daha da basitleştirme:

Üçlü bir yapı aracılığıyla fark atama:

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

Hepsini tek bir işlemde yapmak ve differencedeğişkenden kurtulmak :

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

3
İkisi arasındaki farkları anlamanın bir yolu var mı?
dennismonsewicz

5
Hash'ler aynı boyutta olabilir ancak farklı değerler içerebilir. Böyle bir durumda Both hash1.to_a - hash3.to_ave hash3.to_a - hash1.to_ayine de boş olmayan değerler döndürebilir hash1.size == hash3.size. DÜZENLE'den sonraki bölüm yalnızca karmalar farklı boyuttaysa geçerlidir.
ohaleck

3
Güzel, ama ileride bırakmalıydım. A. boyut> B. boyut, mutlaka A'nın B'yi içerdiği anlamına gelmez. Yine de simetrik farklılıkların birleşimini almamız gerekir.
Gene

.to_aEşit karmalar farklı sıradaki anahtarlara sahip olduğunda will çıktısının doğrudan karşılaştırılması başarısız: {a:1, b:2} == {b:2, a:1}=> true, {a:1, b:2}.to_a == {b:2, a:1}.to_a=> false
aidan

amacı nedir flattenve *? Neden sadece değil Hash[A.to_a - B.to_a]?
JeremyKun

34

Hashdiff gemini deneyebilirsiniz , bu karma hash'lerin ve dizilerin derinlemesine karşılaştırılmasına olanak tanır.

Aşağıda bir örnek verilmiştir:

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
Test hatalarına neden olan oldukça derin karmalarım vardı. Değiştirerek got_hash.should eql expected_hashile HashDiff.diff(got_hash, expected_hash).should eql []I şimdi tam olarak neye ihtiyacım gösterileri çıkışını olsun. Mükemmel!
davetapley

Vay canına, HashDiff harika. İç içe geçmiş büyük bir JSON dizisinde nelerin değiştiğini görmeye çalışmakla hızlı bir çalışma yapıldı. Teşekkürler!
Jeff Wigal

Cevheriniz harika! JSON manipülasyonlarını içeren özellikler yazarken süper yardımcı olur. Teşekkürler.
Alain

2
HashDiff ile yaşadığım deneyim, küçük karmalar için gerçekten iyi çalıştığı, ancak fark hızının iyi ölçeklenmediği görülüyor. İki büyük karma ile beslenmesini bekliyorsanız ve fark süresinin toleransınız dahilinde olduğundan emin olmak istiyorsanız, çağrılarınızı karşılaştırmaya değer.
David Bodow

use_lcs: falseBayrağı kullanmak, büyük karmalar üzerinde karşılaştırmaları önemli ölçüde hızlandırabilir:Hashdiff.diff(b, a, use_lcs: false)
Eric Walker

15

İki karma arasındaki farkın ne olduğunu öğrenmek istiyorsanız, bunu yapabilirsiniz:

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

Raylar edilir kaldırıyordiff yöntem.

Hızlı bir tek astar için:

hash1.to_s == hash2.to_s

Bunu hep unutuyorum. Kullanımı kolay birçok eşitlik kontrolü vardır to_s.
The Tin Man

17
Eşit karmalar farklı bir sırada anahtarlara sahipse başarısız olur: {a:1, b:2} == {b:2, a:1}=> true, {a:1, b:2}.to_s == {b:2, a:1}.to_s=> false
aidan

2
Bu bir özelliktir! : D
Dave Morse

5

Basit bir dizi kesişimini kullanabilirsiniz, bu şekilde her karmada neyin farklı olduğunu bilebilirsiniz.

    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

Değerlerde nil'i doğru şekilde destekleyen hash'ler arasında hızlı ve kirli bir farka ihtiyacınız varsa,

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

Güzel biçimlendirilmiş bir farklılık istiyorsanız, bunu yapabilirsiniz:

# Gemfile
gem 'awesome_print' # or gem install awesome_print

Ve kodunuzda:

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

Buradaki fikir, çıktıyı biçimlendirmek ve farklılaştırmak için harika bir baskı kullanmaktır. Fark kesin olmayacaktır, ancak hata ayıklama amacıyla kullanışlıdır.


1

... ve şimdi çeşitli koleksiyon sınıflarına uygulanacak modül formunda (aralarında Hash). Derin bir inceleme değil, ama basit.

# 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

Bunu, iki karmanın eşit olup olmadığını karşılaştırmak için geliştirdim

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

Kullanım:

> 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

hem hash'i_json'a dönüştürüp hem de dizge olarak karşılaştırmaya ne dersiniz? ama şunu aklınızda bulundurun

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

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

0

Burada, iki Hash'ı derinlemesine karşılaştırmak için bir algoritma var ve bu da iç içe dizileri de karşılaştıracak:

    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

Uygulama:

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

Başka, daha basit bir yaklaşıma ne dersiniz?

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

2
Bu, yalnızca hash'lerin diskte aynı olmasına ihtiyacınız varsa anlamlıdır. Hash öğeleri farklı sıralarda olduğu için diskte farklı olan iki dosya, yine aynı öğeleri içerebilir ve yüklendiklerinde Ruby söz konusu olduğunda eşit olacaktır.
The Tin Man
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.