Jak porównać dwa skróty?

108

Próbuję porównać dwa skróty Ruby przy użyciu następującego kodu:

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

Na ekranie wyświetlany jest pełny plik z pliku 2. Wiem na pewno, że pliki są różne, ale skrypt nie wydaje się wychwytywać tego.

dennismonsewicz
źródło
możliwy duplikat Comparing ruby ​​hashes
Geoff Lanotte

Odpowiedzi:

161

Możesz porównać skróty bezpośrednio dla równości:

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


Możesz przekonwertować skróty na tablice, a następnie uzyskać ich różnicę:

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}

Dalsze uproszczenie:

Przypisywanie różnicy poprzez strukturę trójskładnikową:

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

Robi to wszystko w jednej operacji i pozbywa się differencezmiennej:

  Hash[*(
  (hash3.size > hash1.size)    \
      ? hash3.to_a - hash1.to_a \
      : hash1.to_a - hash3.to_a
  ).flatten] 
=> {"c"=>3}
Blaszany Człowiek
źródło
3
Czy w ogóle można uzyskać różnice między nimi?
dennismonsewicz
5
Hashe mogą mieć ten sam rozmiar, ale zawierać różne wartości. W takim przypadku Both hash1.to_a - hash3.to_ai hash3.to_a - hash1.to_amoże jednak zwracać niepuste wartości hash1.size == hash3.size. Część po EDYCJI jest ważna tylko wtedy, gdy skróty mają różne rozmiary.
ohaleck
3
Fajnie, ale powinienem skończyć z wyprzedzeniem. A.size> B.size niekoniecznie oznacza, że ​​A zawiera B. Nadal trzeba wziąć sumę różnic symetrycznych.
Gene
Bezpośrednie porównanie wyników polecenia .to_azakończy się niepowodzeniem, gdy równe skróty mają klucze w innej kolejności: {a:1, b:2} == {b:2, a:1}=> prawda, {a:1, b:2}.to_a == {b:2, a:1}.to_a=> fałsz
aidan
jaki jest cel flatteni *? Dlaczego nie po prostu Hash[A.to_a - B.to_a]?
JeremyKun
34

Możesz wypróbować klejnot hashdiff , który umożliwia dokładne porównanie skrótów i tablic w hashu.

Oto przykład:

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]]
liu fengyun
źródło
4
Miałem dość głębokie skróty, które powodowały niepowodzenia testów. Zastępując got_hash.should eql expected_hashze HashDiff.diff(got_hash, expected_hash).should eql []teraz uzyskać wyjście, które pokazuje dokładnie to, czego potrzebują. Idealny!
davetapley
Wow, HashDiff jest niesamowity. Wykonano szybką pracę, próbując zobaczyć, co się zmieniło w ogromnej zagnieżdżonej tablicy JSON. Dzięki!
Jeff Wigal,
Twój klejnot jest niesamowity! Bardzo pomocny podczas pisania specyfikacji obejmujących manipulacje JSON. Dzięki.
Alain
2
Moje doświadczenie z HashDiff jest takie, że działa on naprawdę dobrze dla małych haszów, ale prędkość różnicowania nie wydaje się dobrze skalować. Warto porównać swoje wywołania do niego, jeśli spodziewasz się, że może otrzymać dwa duże skróty i upewnić się, że czas różnic jest w granicach twojej tolerancji.
David Bodow,
Używanie use_lcs: falseflagi może znacznie przyspieszyć porównania na dużych hasach:Hashdiff.diff(b, a, use_lcs: false)
Eric Walker
15

Jeśli chcesz poznać różnicę między dwoma hashami, możesz to zrobić:

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"}
Guilherme Bernal
źródło
12

Railsy wycofujądiffmetodę.

Aby uzyskać szybką jedną linijkę:

hash1.to_s == hash2.to_s
Evan
źródło
Zawsze o tym zapominam. Istnieje wiele testów równości, które są łatwe w użyciu to_s.
Tin Man
17
Nie powiedzie się, gdy równe skróty mają klucze w innej kolejności: {a:1, b:2} == {b:2, a:1}=> true, {a:1, b:2}.to_s == {b:2, a:1}.to_s=> false
aidan
2
Co to jest funkcja! : D
Dave Morse
5

Możesz użyć prostego przecięcia tablicy, w ten sposób możesz wiedzieć, co różni się w każdym skrócie.

    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
ErvalhouS
źródło
1

Jeśli potrzebujesz szybkiej i brudnej różnicy między hashami, która poprawnie obsługuje zero w wartościach, możesz użyć czegoś takiego

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
dolzenko
źródło
1

Jeśli chcesz ładnie sformatowanego pliku różnicowego, możesz to zrobić:

# Gemfile
gem 'awesome_print' # or gem install awesome_print

A w swoim kodzie:

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

Chodzi o to, aby użyć niesamowitego drukowania do formatowania i porównywania wyników. Różnica nie będzie dokładna, ale jest przydatna do debugowania.

Benjamin Crouzier
źródło
1

... a teraz w formie modułu do zastosowania w różnych klasach kolekcji (między innymi Hash). To nie jest głęboka inspekcja, ale jest prosta.

# 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
Żelazny Zbawiciel
źródło
1

Opracowałem to, aby porównać, czy dwa skróty są równe

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

Użycie:

> 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
Zwycięzca
źródło
0

a co z konwersją obu hash to_json i porównaniem jako ciągiem znaków? ale pamiętaj o tym

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

h1.to_json==h1.to_json
=> true
h1.to_json==h2.to_json
=> false
stbnrivas
źródło
0

Oto algorytm do głębokiego porównania dwóch haszów, który również porówna zagnieżdżone tablice:

    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

Realizacja:

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
Daniel Garmoshka
źródło
-3

Co powiesz na inne, prostsze podejście:

require 'fileutils'
FileUtils.cmp(file1, file2)
Mikrofon
źródło
2
Ma to znaczenie tylko wtedy, gdy chcesz, aby skróty były identyczne na dysku. Dwa pliki, które są różne na dysku, ponieważ elementy hash są w różnej kolejności, nadal mogą zawierać te same elementy i będą równe w przypadku Rubiego po załadowaniu.
Tin Man