Come si confrontano due hash?


108

Sto cercando di confrontare due Ruby Hash utilizzando il seguente codice:

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

L'output sullo schermo è il file completo da file2. So per certo che i file sono diversi, ma lo script non sembra raccoglierlo.


possibile duplicato di Comparing ruby ​​hashes
Geoff Lanotte

Risposte:


161

Puoi confrontare gli hash direttamente per l'uguaglianza:

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


Puoi convertire gli hash in array, quindi ottenere la loro differenza:

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}

Semplificando ulteriormente:

Assegnazione della differenza tramite una struttura ternaria:

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

Facendo tutto in una sola operazione e sbarazzandosi della differencevariabile:

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

3
C'è comunque per ottenere le differenze tra i due?
dennismonsewicz

5
Gli hash possono essere della stessa dimensione, ma contenere valori diversi. In tal caso, entrambi hash1.to_a - hash3.to_ae hash3.to_a - hash1.to_apotrebbero restituire valori non vuoti hash1.size == hash3.size. La parte dopo EDIT è valida solo se gli hash sono di dimensioni diverse.
ohaleck

3
Bello, ma avrei dovuto smettere prima. A.size> B.size non significa necessariamente che A include B. È ancora necessario prendere l'unione delle differenze simmetriche.
Gene

Il confronto diretto dell'output di .to_afallirà quando hash uguali hanno chiavi in ​​un ordine diverso: {a:1, b:2} == {b:2, a:1}=> true, {a:1, b:2}.to_a == {b:2, a:1}.to_a=> false
aidan

qual è lo scopo di flattene *? Perché non solo Hash[A.to_a - B.to_a]?
JeremyKun

34

Puoi provare la gemma hashdiff , che consente un confronto approfondito di hash e array nell'hash.

Quanto segue è un esempio:

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
Ho avuto alcuni hash abbastanza profondi che hanno causato errori nei test. Sostituendo il got_hash.should eql expected_hashcon HashDiff.diff(got_hash, expected_hash).should eql []ora ottengo l'output che mostra esattamente ciò di cui ho bisogno. Perfetto!
davetapley

Wow, HashDiff è fantastico. Ha fatto un rapido lavoro per cercare di vedere cosa è cambiato in un enorme array JSON nidificato. Grazie!
Jeff Wigal

La tua gemma è fantastica! Super utile quando si scrivono specifiche che coinvolgono manipolazioni JSON. Grazie.
Alain

2
La mia esperienza con HashDiff è stata che funziona molto bene per piccoli hash ma la velocità del diff non sembra scalare bene. Vale la pena confrontare le tue chiamate se ti aspetti che possa essere alimentato con due hash di grandi dimensioni e assicurandoti che il tempo di diff sia entro la tua tolleranza.
David Bodow

L'utilizzo del use_lcs: falseflag può velocizzare notevolmente i confronti su hash di grandi dimensioni:Hashdiff.diff(b, a, use_lcs: false)
Eric Walker

15

Se vuoi ottenere qual è la differenza tra due hash, puoi farlo:

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 sta deprecando il diffmetodo.

Per una breve battuta:

hash1.to_s == hash2.to_s

Me ne dimentico sempre. Ci sono molti controlli di uguaglianza che sono facili da usare to_s.
Tin Man

17
{a:1, b:2} == {b:2, a:1}{a:1, b:2}.to_s == {b:2, a:1}.to_s
Fallirà

2
Qual è una caratteristica! : D
Dave Morse

5

Potresti usare una semplice intersezione di array, in questo modo puoi sapere cosa differisce in ogni 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

Se hai bisogno di un diff veloce e sporco tra hash che supporti correttamente zero nei valori, puoi usare qualcosa di simile

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

Se vuoi un diff ben formattato, puoi farlo:

# Gemfile
gem 'awesome_print' # or gem install awesome_print

E nel tuo codice:

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

L'idea è di utilizzare una stampa fantastica per formattare e diffondere l'output. La differenza non sarà esatta, ma è utile per scopi di debug.


1

... e ora in modulo modulo per essere applicata ad una varietà di classi di insiemi (Hash fra loro). Non è un'ispezione approfondita, ma è semplice.

# 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

L'ho sviluppato per confrontare se due hash sono uguali

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

L'utilizzo:

> 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

che dire di convertire entrambi gli hash in_json e confrontare come stringa? ma tenendolo a mente

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

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

0

Ecco l'algoritmo per confrontare in profondità due hash, che confronteranno anche gli array annidati:

    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

Implementazione:

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

Che ne dici di un altro approccio più semplice:

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

2
Ciò è significativo solo se è necessario che gli hash siano identici sul disco. Due file che sono diversi su disco perché gli elementi hash sono in ordini diversi, possono ancora contenere gli stessi elementi e saranno uguali per Ruby una volta caricati.
Tin Man
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.