Flatten deep nested hash to array for sha1 hashing - ruby

I want to compute an unique sha1 hash from a ruby hash. I thought about
(Deep) Converting the Hash into an array
Sorting the array
Join array by empty string
calculate sha1
Consider the following hash:
hash = {
foo: "test",
bar: [1,2,3]
hello: {
world: "world",
arrays: [
{foo: "bar"}
]
}
}
How can I get this kind of nested hash into an array like
[:foo, "test", :bar, 1, 2, 3, :hello, :world, "earth", :arrays, :my, "example"]
I would then sort the array, join it with array.join("") and compute the sha1 hash like this:
require 'digest/sha1'
Digest::SHA1.hexdigest hash_string
How could I flatten the hash like I described above?
Is there already a gem for this?
Is there a quicker / easier way to solve this? I have a large amount of objects to convert (~700k), so performance does matter.
EDIT
Another problem that I figured out by the answers below are this two hashes:
a = {a: "a", b: "b"}
b = {a: "b", b: "a"}
When flattening the hash and sorting it, this two hashes produce the same output, even when a == b => false.
EDIT 2
The use case for this whole thing is product data comparison. The product data is stored inside a hash, then serialized and sent to a service that creates / updates the product data.
I want to check if anything has changed inside the product data, so I generate a hash from the product content and store it in a database. The next time the same product is loaded, I calculate the hash again, compare it to the one in the DB and decide wether the product needs an update or not.

EDIT : As you detailed, two hashes with keys in different order should give the same string. I would reopen the Hash class to add my new custom flatten method :
class Hash
def custom_flatten()
self.sort.map{|pair| ["key: #{pair[0]}", pair[1]]}.flatten.map{ |elem| elem.is_a?(Hash) ? elem.custom_flatten : elem }.flatten
end
end
Explanation :
sort converts the hash to a sorted array of pairs (for the comparison of hashes with different keys order)
.map{|pair| ["key: #{pair[0]}", pair[1]]} is a trick to differentiate keys from values in the final flatten array, to avoid the problem of {a: {b: {c: :d}}}.custom_flatten == {a: :b, c: :d}.custom_flatten
flatten converts an array of arrays into a single array of values
map{ |elem| elem.is_a?(Hash) ? elem.custom_flatten : elem } calls back fully_flatten on any sub-hash left.
Then you just need to use :
require 'digest/sha1'
Digest::SHA1.hexdigest hash.custom_flatten.to_s

I am not aware of a gem that does something like what you are looking for. There is a Hash#flatten method in ruby, but it does not flatten nested hashes recursively. Here is a straight forward recursive function that will flatten in the way that you requested in your question:
def completely_flatten(hsh)
hsh.flatten(-1).map{|el| el.is_a?(Hash) ? completely_flatten(el) : el}.flatten
end
This will yield
hash = {
foo: "test",
bar: [1,2,3]
hello: {
world: "earth",
arrays: [
{my: "example"}
]
}
}
completely_flatten(hash)
#=> [:foo, "test", :bar, 1, 2, 3, :hello, :world, "earth", :arrays, :my, "example"]
To get the string representation you are looking for (before making the sha1 hash) convert everything in the array to a string before sorting so that all of the elements can be meaningfully compared or else you will get an error:
hash_string = completely_flatten(hash).map(&:to_s).sort.join
#=> "123arraysbarearthexamplefoohellomytestworld"

The question is how to "flatten" a hash. There is a second, implicit, question concerning sha1, but, by SO rules, that needs to be addressed in a separate question. You can "flatten" any hash or array as follows.
Code
def crush(obj)
recurse(obj).flatten
end
def recurse(obj)
case obj
when Array then obj.map { |e| recurse e }
when Hash then obj.map { |k,v| [k, recurse(v)] }
else obj
end
end
Example
crush({
foo: "test",
bar: [1,2,3],
hello: {
world: "earth",
arrays: [{my: "example"}]
}
})
#=> [:foo, "test", :bar, 1, 2, 3, :hello, :world, "earth", :arrays, :my, "example"]
crush([[{ a:1, b:2 }, "cat", [3,4]], "dog", { c: [5,6] }])
#=> [:a, 1, :b, 2, "cat", 3, 4, "dog", :c, 5, 6]

Use Marshal for Fast Serialization
You haven't articulated a useful reason to change your data structure before hashing. Therefore, you should consider marshaling for speed unless your data structures contain unsupported objects like bindings or procs. For example, using your hash variable with the syntax corrected:
require 'digest/sha1'
hash = {
foo: "test",
bar: [1,2,3],
hello: {
world: "world",
arrays: [
{foo: "bar"}
]
}
}
Digest::SHA1.hexdigest Marshal.dump(hash)
#=> "f50bc3ceb514ae074a5ab9672ae5081251ae00ca"
Marshal is generally faster than other serialization options. If all you need is speed, that will be your best bet. However, you may find that JSON, YAML, or a simple #to_s or #inspect meet your needs better for other reasons. As long as you are comparing similar representations of your object, the internal format of the hashed object is largely irrelevant to ensuring you have a unique or unmodified object.

Any solution based on flattening the hash will fail for nested hashes. A robust solution is to explicitly sort the keys of each hash recursively (from ruby 1.9.x onwards, hash keys order is preserved), and then serialize it as a string and digest it.
def canonize_hash(h)
r = h.map { |k, v| [k, v.is_a?(Hash) ? canonize_hash(v) : v] }
Hash[r.sort]
end
def digest_hash(hash)
Digest::SHA1.hexdigest canonize_hash(hash).to_s
end
digest_hash({ foo: "foo", bar: "bar" })
# => "ea1154f35b34c518fda993e8bb0fe4dbb54ae74a"
digest_hash({ bar: "bar", foo: "foo" })
# => "ea1154f35b34c518fda993e8bb0fe4dbb54ae74a"

Related

Ruby Hash: type casting

I’m trying to get a better grasp on writing in Ruby and working with Hash tables and their values.
1. Say you have a hash:
‘FOO’= {‘baz’ => [1,2,3,4,5]}
Goal: convert each value into a string in the ‘Ruby’ way.
I’ve come across multiple examples of using .each eg.
FOO.each = { |k,v| FOO[k] = v.to_s }
However this renders an array encapsulated in a string. Eg. "[1,2,3,4,5]" where it should be ["1", "2", "3", "4", "5"].
2. When type casting is performed on a Hash that’s holds an array of values, is the result a new array? Or simply a change in type of value (eg. 1 becomes “1” when .to_s is applied (say the value was placed through a each enumerator like above).
An explanation is greatly appreciated. New to Ruby.
In the each block, k and v are the key value pair. In your case, 'baz' is key and [1,2,3,4,5] is value. Since you're doing v.to_s, it converts the whole array to string and not the individual values.
You can do something like this to achieve what you want.
foo = { 'baz' => [1,2,3,4,5] }
foo.each { |k, v| foo[k] = v.map(&:to_s) }
You can use Hash#transform_values:
foo = { 'baz' => [1, 2, 3, 4, 5] }
foo.transform_values { |v| v.map(&:to_s) } #=> {"baz"=>["1", "2", "3", "4", "5"]}

Apply something to certain value of a hash, return the whole hash

What is the proper way of doing so in Ruby in a functional and immutable way:
a = { price: 100, foo: :bar, bar: :baz }
def reduced_price(row)
row.merge(price: row[:price] / 2)
end
reduced_price(a) # => { price: 50, foo: :bar, bar: :baz }
I don't want to mutate anything and I don't like the consctruction row.merge(key: row[:key]) because it repeats the :key and refers to row twice. If there would be something like:
{a: 1, b: 2}.apply_to_key(:a) { |x| x * 10 } # => {a: 10, b: 2}
it would be great.
To sum up, I want a method that, when given a key, updates a single value of a hash by that key using the previous value, and then returns the whole hash.
And while I was writing the question, I have found the answer. Since SO suggests sharing my knowledge Q&A-style, here it is:
{a: 1, b: 2}.merge(a: nil) { |_, v| v * 10 } # => {a: 10, b: 2}
When you pass a block to hash.merge(other_hash), it will be executed for each pair of other_hash with parameters key, oldval, newval and should return resulting value. In a case above, my other_hash contains only one key I wanted to alter, and from the three params I care only for the second, old_val, to use it in my block.
It's perfectly chainable and doesn't mutate a thing. It has a bit of redundancy (having to pass nil or any other value to other_hash, having to ignore first parameter of the block), but I guess it's the closest I could get.
If you can suggest better answer, I will consider accepting it.

Is there a quick and easy way to create a checksum from Ruby's basic data structures?

I have a data structure (Hash) that looks something like this:
{
foo: "Test string",
bar: [475934759, 5619827847]
}
I'm trying to create a checksum from that Hash to check for equality in the future. I tried using the hash method of the Hash, which resulted in a satisfyingly nice-looking hash, but it turns out that the same Hash will produce a different hash after the interpreter has been restarted.
I really just want to be able to create a ~128 bit checksum from a Hash, String or Array instance.
Is this possible?
You could calculate your own hash based on the object's Marshal dump or JSON representation.
This calculates the MD5 hash of a Marshal dump:
require 'digest/md5'
hash = {
foo: "Test string",
bar: [475934759, 5619827847]
}
Marshal::dump(hash)
#=> "\x04\b{\a:\bfooI\"\x10Test string\x06:\x06ET:\bbar[\ai\x04'0^\x1Cl+\b\x87\xC4\xF7N\x01\x00"
Digest::MD5.hexdigest(Marshal::dump(hash))
#=> "1b6308abdd8f5f6290e2825a078a1a02"
Update
You can implement your own strategy, although I would not recommend to change core functionality:
class Hash
def _dump(depth)
# this doesn't cause a recursion because sort returns an array
Marshal::dump(self.sort, depth)
end
def self._load(marshaled_hash)
Hash[Marshal::load(marshaled_hash)]
end
end
Marshal::dump({foo:1, bar:2})
#=> "\x04\bu:\tHash\e\x04\b[\a[\a:\bbari\a[\a:\bfooi\x06"
Marshal::dump({bar:2, foo:1})
#=> "\x04\bu:\tHash\e\x04\b[\a[\a:\bbari\a[\a:\bfooi\x06"
Marshal::load(Marshal::dump({foo:1, bar:2}))
#=> {:bar=>2, :foo=>1}
To build on #Stefan's answer above, if order of the hash is important, sort the output before pushing it through Mashall.
require 'digest/md5'
hash = {
'foo'=> "Test string",
'bar'=> [475934759, 5619827847]
}
puts Digest::MD5.hexdigest(Marshal::dump(hash.collect{|k,v| [k,v]}.sort{|a,b| a[0] <=> b[0]}))
# 8509c564c0ae8dcb6c2b9b564ba6a03f
hash = {
'bar'=> [475934759, 5619827847],
'foo'=> "Test string"
}
puts Digest::MD5.hexdigest(Marshal::dump(hash.collect{|k,v| [k,v]}.sort{|a,b| a[0] <=> b[0]}))
# 8509c564c0ae8dcb6c2b9b564ba6a03f
If you need to generate the checksum for the content of the hash, whatever the order of the data, using Marshal or sort or other techniques won't work.
The only solid way I found so far is the following:
require 'digest/md5'
hash1 = { "a" => 1, "b" => "2", c: { d: "3" } }
hash2 = { c: { d: "3" }, "a" => 1, "b" => "2" }
Digest::MD5.hexdigest(Marshal.dump(hash1)) # => "5def3b2cbdddd3aa6730b6d0527c2d79"
Digest::MD5.hexdigest(Marshal.dump(hash2)) # => "8155698ccfb05b8db01490e9b9634fd9"
Digest::MD5.hexdigest(hash1.to_s.chars.sort.join) # => "812bb65d65380fc1e620a9596806cc35"
Digest::MD5.hexdigest(hash2.to_s.chars.sort.join) # => "812bb65d65380fc1e620a9596806cc35"

How to output sorted hash in ruby template

I'm building a config file for one of our inline apps. Its essentially a json file. I'm having a lot of trouble getting puppet/ruby 1.8 to output the hash/json the same way each time.
I'm currently using
<%= require "json"; JSON.pretty_generate data %>
But while outputting human readable content, it doesn't guarantee the same order each time. Which means that puppet will send out change notifications often for the same data.
I've also tried
<%= require "json"; JSON.pretty_generate Hash[*data.sort.flatten] %>
Which will generate the same data/order each time. The problem comes when data has a nested array.
data => { beanstalkd => [ "server1", ] }
becomes
"beanstalkd": "server1",
instead of
"beanstalkd": ["server1"],
I've been fighting with this for a few days on and off now, so would like some help
Since hashes in Ruby are ordered, and the question is tagged with ruby, here's a method that will sort a hash recursively (without affecting ordering of arrays):
def sort_hash(h)
{}.tap do |h2|
h.sort.each do |k,v|
h2[k] = v.is_a?(Hash) ? sort_hash(v) : v
end
end
end
h = {a:9, d:[3,1,2], c:{b:17, a:42}, b:2 }
p sort_hash(h)
#=> {:a=>9, :b=>2, :c=>{:a=>42, :b=>17}, :d=>[3, 1, 2]}
require 'json'
puts sort_hash(h).to_json
#=> {"a":9,"b":2,"c":{"a":42,"b":17},"d":[3,1,2]}
Note that this will fail catastrophically if your hash has keys that cannot be compared. (If your data comes from JSON, this will not be the case, since all keys will be strings.)
Hash is an unordered data structure. In some languages (ruby, for example) there's an ordered version of hash, but in most cases in most languages you shouldn't rely on any specific order in a hash.
If order is important to you, you should use an array. So, your hash
{a: 1, b: 2}
becomes this
[{a: 1}, {b: 2}]
I think, it doesn't force too many changes in your code.
Workaround to your situation
Try this:
data = {beanstalkId: ['server1'], ccc: 2, aaa: 3}
data2 = data.keys.sort.map {|k| [k, data[k]]}
puts Hash[data2]
#=> {:aaa=>3, :beanstalkId=>["server1"], :ccc=>2}

Convert array-of-hashes to a hash-of-hashes, indexed by an attribute of the hashes

I've got an array of hashes representing objects as a response to an API call. I need to pull data from some of the hashes, and one particular key serves as an id for the hash object. I would like to convert the array into a hash with the keys as the ids, and the values as the original hash with that id.
Here's what I'm talking about:
api_response = [
{ :id => 1, :foo => 'bar' },
{ :id => 2, :foo => 'another bar' },
# ..
]
ideal_response = {
1 => { :id => 1, :foo => 'bar' },
2 => { :id => 2, :foo => 'another bar' },
# ..
}
There are two ways I could think of doing this.
Map the data to the ideal_response (below)
Use api_response.find { |x| x[:id] == i } for each record I need to access.
A method I'm unaware of, possibly involving a way of using map to build a hash, natively.
My method of mapping:
keys = data.map { |x| x[:id] }
mapped = Hash[*keys.zip(data).flatten]
I can't help but feel like there is a more performant, tidier way of doing this. Option 2 is very performant when there are a very minimal number of records that need to be accessed. Mapping excels here, but it starts to break down when there are a lot of records in the response. Thankfully, I don't expect there to be more than 50-100 records, so mapping is sufficient.
Is there a smarter, tidier, or more performant way of doing this in Ruby?
Ruby <= 2.0
> Hash[api_response.map { |r| [r[:id], r] }]
#=> {1=>{:id=>1, :foo=>"bar"}, 2=>{:id=>2, :foo=>"another bar"}}
However, Hash::[] is pretty ugly and breaks the usual left-to-right OOP flow. That's why Facets proposed Enumerable#mash:
> require 'facets'
> api_response.mash { |r| [r[:id], r] }
#=> {1=>{:id=>1, :foo=>"bar"}, 2=>{:id=>2, :foo=>"another bar"}}
This basic abstraction (convert enumerables to hashes) was asked to be included in Ruby long ago, alas, without luck.
Note that your use case is covered by Active Support: Enumerable#index_by
Ruby >= 2.1
[UPDATE] Still no love for Enumerable#mash, but now we have Array#to_h. It creates an intermediate array, but it's better than nothing:
> object = api_response.map { |r| [r[:id], r] }.to_h
Something like:
ideal_response = api_response.group_by{|i| i[:id]}
#=> {1=>[{:id=>1, :foo=>"bar"}], 2=>[{:id=>2, :foo=>"another bar"}]}
It uses Enumerable's group_by, which works on collections, returning matches for whatever key value you want. Because it expects to find multiple occurrences of matching key-value hits it appends them to arrays, so you end up with a hash of arrays of hashes. You could peel back the internal arrays if you wanted but could run a risk of overwriting content if two of your hash IDs collided. group_by avoids that with the inner array.
Accessing a particular element is easy:
ideal_response[1][0] #=> {:id=>1, :foo=>"bar"}
ideal_response[1][0][:foo] #=> "bar"
The way you show at the end of the question is another valid way of doing it. Both are reasonably fast and elegant.
For this I'd probably just go:
ideal_response = api_response.each_with_object(Hash.new) { |o, h| h[o[:id]] = o }
Not super pretty with the multiple brackets in the block but it does the trick with just a single iteration of the api_response.

Resources