Як я порівняю два хеші?


108

Я намагаюся порівняти два Ruby хеші, використовуючи наступний код:

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

Вихід на екран - це повний файл з файлу2. Я знаю фактично, що файли різні, але сценарій, схоже, не підбирає їх.


Відповіді:


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. Частина після EDIT дійсна, лише якщо хеші різного розміру.
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

Ви можете спробувати дорогоцінний камінь хешдифа , який дозволяє глибоке порівняння хешів та масивів у хеші.

Наступний приклад:

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 []I тепер отримати вихідний сигнал , який показує , що саме мені потрібно. Ідеально!
davetapley

Нічого собі, HashDiff - приголомшливий. Зробив швидку роботу, намагаючись побачити, що змінилося у величезному вкладеному масиві JSON. Дякую!
Джефф Вігал

Ваш дорогоцінний камінь - приголомшливий! Супер корисно при написанні специфікацій, пов’язаних з маніпуляціями JSON. Дякую.
Ален

2
Мій досвід роботи з HashDiff полягав у тому, що він працює дуже добре для невеликих хешів, але різниця швидкостей, здається, не дуже масштабується. Варто оцінити ваші дзвінки на тему, якщо ви очікуєте, що він може нагодуватись двома великими хешами та переконатися, що час різниці знаходиться у межах вашої толерантності.
Девід Боду

Використання use_lcs: falseпрапора може значно прискорити порівняння на великих хешах:Hashdiff.diff(b, a, use_lcs: false)
Ерік Уокер

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

Rails є протестуючих на 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
helpan

2
Яка особливість! : D
Дейв Морз

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

а як конвертувати обидва хеша 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, коли вони завантажуються.
Олов'яний чоловік
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.