How do I merge two hashes into a hash of arrays? - ruby

http://codepad.org/WmYzQLiS
I'd like to merge these two arrays:
a = { :a => 'a', :b => 'b', :d => 'd' }
b = { :a => '1', :b => 'b', :c => 'c' }
Into a hash that looks like this:
{:a => ["a", "1"], :b => ["b", "b"], :c => [nil, "c"], :d => ["d", nil] }
This obviously doesn't work:
p a.merge(b) { |k, v1, v2| [v1, v2] } # {:c=>"c", :a=>["a", "1"], :b=>["b", "b"], :d=>"d"}

It's because Hash#merge call the block only for the duplicate keys.
a = { :a => 'a', :b => 'b', :d => 'd' }
b = { :a => '1', :b => 'b', :c => 'c' }
Hash[(a.keys|b.keys).map { |key|
[key, [a.fetch(key, nil), b.fetch(key, nil)]]
}]
# => {:a=>["a", "1"], :b=>["b", "b"], :d=>["d", nil], :c=>[nil, "c"]}
# In Ruby 2.1+
(a.keys|b.keys).map { |key|
[key, [a.fetch(key, nil), b.fetch(key, nil)]]
}.to_h

(a.keys | b.keys).each_with_object({}) { |k,h| h[k] = [a[k], b[k]] }
#=> {:a=>["a", "1"], :b=>["b", "b"], :d=>["d", nil], :c=>[nil, "c"]}
This is easy to generalize for an arbitrary number of hashes.
arr = [{ :a => 'a', :b => 'b', :d => 'd' },
{ :a => '1', :b => 'b', :c => 'c' },
{ :b => '0', :c => 'b', :d => 'c' }]
arr.reduce([]) { |ar,h| ar | h.keys }
.each_with_object({}) { |k,h| h[k] = arr.map { |g| g[k] } }
#=> {:a=>["a", "1", nil], :b=>["b", "b", "0"],
# :d=>["d", nil, "c"], :c=>[nil, "c", "b"]}

Related

Return min values of hash with duplicate values

For example, if I have the hash {"a" => 1, "b" => 2, "c" => 1}, I want ["a", "c"].
I can do hash.min_by{|k,v| v}, but that only returns the first match ("a"=>1).
How do I get it to recognize duplicates and return {"a"=> 1, "c"=> 1}?
That operation is a bit unusual for a hash, so it’s not very neat:
min_value = hash.values.min
min_pairs = hash.select { |k, v| v == min_value }
{"a" => 1, "b" => 2, "c" => 1}.group_by(&:last).min.last.map(&:first)
# => ["a", "c"]
or
{"a" => 1, "b" => 2, "c" => 1}.group_by(&:last).min.last.to_h.keys
# => ["a", "c"]
You can write FORTRAN in any language! :)
It has the advantage of only requiring 1 pass :
hash = {"a" => 1, "b" => 2, "c" => 1}
min = Float::INFINITY
values_for_min = []
hash.each do |key, value|
case value <=> min
when 0
values_for_min << key
when -1
min = value
values_for_min = [key]
end
end
p min
#=> 1
p values_for_min
#=> ["a", "c"]

Concatenating tree key names [duplicate]

This question is the inverse of this question.
Given a nested hash like
{
:a => {
:b => {:c => 1, :d => 2},
:e => 3,
},
:f => 4,
}
what is the best way to convert it into a flat hash like
{
[:a, :b, :c] => 1,
[:a, :b, :d] => 2,
[:a, :e] => 3,
[:f] => 4,
}
Another way:
def flat_hash(h,f=[],g={})
return g.update({ f=>h }) unless h.is_a? Hash
h.each { |k,r| flat_hash(r,f+[k],g) }
g
end
h = { :a => { :b => { :c => 1,
:d => 2 },
:e => 3 },
:f => 4 }
flat_hash(h) #=> {[:a, :b, :c]=>1, [:a, :b, :d]=>2, [:a, :e]=>3, [:f]=>4}
Very similar to Adiel Mittmann's solution
def flat_hash(h, k = [])
new_hash = {}
h.each_pair do |key, val|
if val.is_a?(Hash)
new_hash.merge!(flat_hash(val, k + [key]))
else
new_hash[k + [key]] = val
end
end
new_hash
end
Edit: Refactored for elegance. Should be almost as fast.
def flat_hash(hash, k = [])
return {k => hash} unless hash.is_a?(Hash)
hash.inject({}){ |h, v| h.merge! flat_hash(v[-1], k + [v[0]]) }
end
My attempt:
def flatten_hash(h)
return { [] => h } unless h.is_a?(Hash)
Hash[h.map { |a,v1| flatten_hash(v1).map { |b,v2| [[a] + b, v2] } }.flatten(1)]
end
Sorry for the bad variables names, had to fit it in one line.
This is not an attempt to give you the best way to do it, but it is a way :P
def flatten(hash)
return {[] => hash} if !hash.is_a?(Hash)
map = {}
hash.each_pair do |key1, value1|
flatten(value1).each_pair do |key2, value2|
map[[key1] + key2] = value2
end
end
return map
end
It works for your example, producing this result:
{[:a, :b, :c]=>1, [:a, :b, :d]=>2, [:a, :e]=>3, [:f]=>4}
It may not produce the result you expect if there are empty hashes.
A functional approach (see the history for an alternative implementations):
def recursive_flatten(hash)
hash.flat_map do |key, value|
if value.is_a?(Hash)
recursive_flatten(value).map { |ks, v| [[key] + ks, v] }
else
[[[key], value]]
end
end.to_h
end
Inspired by #cary-swoveland way, but in Hash class :
class Hash
def deep_flatten(previous_key=[])
flat_hash = {}
self.each do |key, value|
next_key = previous_key+[key]
flat_hash.update(value.is_a?(Hash) ? value.deep_flatten(next_key) : {next_key=>value})
end
return flat_hash
end
end
h = { :a => { :b => { :c => 1, :d => 2 }, :e => 3 }, :f => 4 }
h.deep_flatten #=> {[:a, :b, :c]=>1, [:a, :b, :d]=>2, [:a, :e]=>3, [:f]=>4}
A declarative solution using DeepEnumerable:
require 'deep_enumerable'
h = { :a => { :b => { :c => 1, :d => 2 }, :e => 3 }, :f => 4 }
h.deep_each.map do |k, v|
[DeepEnumerable.deep_key_to_array(k), v]
end.to_h
or, for those who prefer point-free style
h.deep_each.to_h.shallow_map_keys(&DeepEnumerable.method(:deep_key_to_array))
Array support / readable names / no update for speed / stringified results keys
def flat_hash(input, base = nil, all = {})
if input.is_a?(Array)
input = input.each_with_index.to_a.each(&:reverse!)
end
if input.is_a?(Hash) || input.is_a?(Array)
input.each do |k, v|
flat_hash(v, base ? "#{base}.#{k}" : k, all)
end
else
all[base] = input
end
all
end

ruby: how to find non-unique elements in array and print each with number of occurrences?

I have
a = ["a", "d", "c", "b", "b", "c", "c"]
and need to print something like (sorted descending by number of occurrences):
c:3
b:2
I understand first part (finding NON-unique) is:
b = a.select{ |e| a.count(e) > 1 }
=> ["c", "b", "b", "c", "c"]
or
puts b.select{|e, c| [e, a.count(e)] }.uniq
c
b
How to output each non-unique with number of occurrences sorted backwards?
puts a.uniq.
map { | e | [a.count(e), e] }.
select { | c, _ | c > 1 }.
sort.reverse.
map { | c, e | "#{e}:#{c}" }
The group_by method is used for this often:
a.group_by{ |i| i }
{
"a" => [
[0] "a"
],
"d" => [
[0] "d"
],
"c" => [
[0] "c",
[1] "c",
[2] "c"
],
"b" => [
[0] "b",
[1] "b"
]
}
I like:
a.group_by{ |i| i }.each_with_object({}) { |(k,v), h| h[k] = v.size }
{
"a" => 1,
"d" => 1,
"c" => 3,
"b" => 2
}
Or:
Hash[a.group_by{ |i| i }.map{ |k,v| [k, v.size] }]
{
"a" => 1,
"d" => 1,
"c" => 3,
"b" => 2
}
One of those might scratch your itch. From there you can reduce the result using a little test:
Hash[a.group_by{ |i| i }.map{ |k,v| v.size > 1 && [k, v.size] }]
{
"c" => 3,
"b" => 2
}
If you just want to print the information use:
puts a.group_by{ |i| i }.map{ |k,v| "#{k}: #{v.size}" }
a: 1
d: 1
c: 3
b: 2
From Ruby 2.7, you can utilise Enumerable#tally and numbered block arguments:
a = ["a", "d", "c", "b", "b", "c", "c"]
puts a.tally.filter { _2 > 1 }.sort_by { -_2 }.map &:first
Here, Enumerable#tally returns a hash like { 'a' => 1, 'b' => 2, ... }, which you then have to filter and sort. After sorting, the hash would've collapsed to a nested array, e.g. [['b', 2], ...]. The last step is to take the first argument of each array element, using &:first.
How about:
a.sort.chunk{|x| a.count(x)}.sort.reverse.each do |n, v|
puts "#{v[0]}:#{n}" if n > 1
end
I personally like this solution:
a.inject({}) {|hash, val| hash[val] ||= 0; hash[val] += 1; hash}.
reject{|key, value| value == 1}.sort.reverse.
each_pair{|k,v| puts("#{k}:#{v}")}
a.reduce(Hash.new(0)) { |memo,x| memo[x] += 1; memo } # Frequency count.
.select { |_,count| count > 1 } # Choose non-unique items.
.sort_by { |x| -x[1] } # Sort by number of occurrences descending.
# => [["c", 3], ["b", 2]]
Also:
a.group_by{|x|x}.map{|k,v|[k,v.size]}.select{|x|x[1]>1}.sort_by{|x|-x[1]}
# => [["c", 3], ["b", 2]]
This will give you a hash with element => occurrences:
b.reduce(Hash.new(0)) do |hash, element|
hash[element] += 1
hash
end
puts a.uniq.
map { |e| a.count(e) > 1 ? [e, a.count(e)] : nil }.compact.
sort { |a, b| b.last <=> a.last }

Converting a nested hash into a flat hash

This question is the inverse of this question.
Given a nested hash like
{
:a => {
:b => {:c => 1, :d => 2},
:e => 3,
},
:f => 4,
}
what is the best way to convert it into a flat hash like
{
[:a, :b, :c] => 1,
[:a, :b, :d] => 2,
[:a, :e] => 3,
[:f] => 4,
}
Another way:
def flat_hash(h,f=[],g={})
return g.update({ f=>h }) unless h.is_a? Hash
h.each { |k,r| flat_hash(r,f+[k],g) }
g
end
h = { :a => { :b => { :c => 1,
:d => 2 },
:e => 3 },
:f => 4 }
flat_hash(h) #=> {[:a, :b, :c]=>1, [:a, :b, :d]=>2, [:a, :e]=>3, [:f]=>4}
Very similar to Adiel Mittmann's solution
def flat_hash(h, k = [])
new_hash = {}
h.each_pair do |key, val|
if val.is_a?(Hash)
new_hash.merge!(flat_hash(val, k + [key]))
else
new_hash[k + [key]] = val
end
end
new_hash
end
Edit: Refactored for elegance. Should be almost as fast.
def flat_hash(hash, k = [])
return {k => hash} unless hash.is_a?(Hash)
hash.inject({}){ |h, v| h.merge! flat_hash(v[-1], k + [v[0]]) }
end
My attempt:
def flatten_hash(h)
return { [] => h } unless h.is_a?(Hash)
Hash[h.map { |a,v1| flatten_hash(v1).map { |b,v2| [[a] + b, v2] } }.flatten(1)]
end
Sorry for the bad variables names, had to fit it in one line.
This is not an attempt to give you the best way to do it, but it is a way :P
def flatten(hash)
return {[] => hash} if !hash.is_a?(Hash)
map = {}
hash.each_pair do |key1, value1|
flatten(value1).each_pair do |key2, value2|
map[[key1] + key2] = value2
end
end
return map
end
It works for your example, producing this result:
{[:a, :b, :c]=>1, [:a, :b, :d]=>2, [:a, :e]=>3, [:f]=>4}
It may not produce the result you expect if there are empty hashes.
A functional approach (see the history for an alternative implementations):
def recursive_flatten(hash)
hash.flat_map do |key, value|
if value.is_a?(Hash)
recursive_flatten(value).map { |ks, v| [[key] + ks, v] }
else
[[[key], value]]
end
end.to_h
end
Inspired by #cary-swoveland way, but in Hash class :
class Hash
def deep_flatten(previous_key=[])
flat_hash = {}
self.each do |key, value|
next_key = previous_key+[key]
flat_hash.update(value.is_a?(Hash) ? value.deep_flatten(next_key) : {next_key=>value})
end
return flat_hash
end
end
h = { :a => { :b => { :c => 1, :d => 2 }, :e => 3 }, :f => 4 }
h.deep_flatten #=> {[:a, :b, :c]=>1, [:a, :b, :d]=>2, [:a, :e]=>3, [:f]=>4}
A declarative solution using DeepEnumerable:
require 'deep_enumerable'
h = { :a => { :b => { :c => 1, :d => 2 }, :e => 3 }, :f => 4 }
h.deep_each.map do |k, v|
[DeepEnumerable.deep_key_to_array(k), v]
end.to_h
or, for those who prefer point-free style
h.deep_each.to_h.shallow_map_keys(&DeepEnumerable.method(:deep_key_to_array))
Array support / readable names / no update for speed / stringified results keys
def flat_hash(input, base = nil, all = {})
if input.is_a?(Array)
input = input.each_with_index.to_a.each(&:reverse!)
end
if input.is_a?(Hash) || input.is_a?(Array)
input.each do |k, v|
flat_hash(v, base ? "#{base}.#{k}" : k, all)
end
else
all[base] = input
end
all
end

Sum 2 hashes attributes with the same key

I have 2 hashes, for example:
{'a' => 30, 'b' => 14}
{'a' => 4, 'b' => 23, 'c' => 7}
where a, b and c are objects. How can I sum those hashes' keys to get a new hash like:
{'a' => 34, 'b' => 37, 'c' => 7}
a_hash = {'a' => 30, 'b' => 14}
b_hash = {'a' => 4, 'b' => 23, 'c' => 7}
a_hash.merge(b_hash){ |k, a_value, b_value| a_value + b_value }
=> {"a"=>34, "b"=>37, "c"=>7}
b_hash.merge(a_hash){ |k, b_value, a_value| a_value + b_value }
=> {"a"=>34, "b"=>37, "c"=>7}
If some one looking to add more than 2 hashes, use this
#sample array with any number of hashes
sample_arr = [{:a=>2, :b=>4, :c=>8, :d=>20, :e=>5},
{:a=>1, :b=>2, :c=>4, :d=>10, :e=>5, :r=>7},
{:a=>1, :b=>2, :c=>4, :d=>10},
{:a=>2, :b=>4, :c=>8, :d=>20, :e=>5},
{:a=>1, :b=>2, :c=>4, :d=>10, :e=>5, :r=>7},
{:a=>1, :b=>2, :c=>4, :d=>10}]
sample_arr.inject { |acc, next_obj| acc.merge(next_obj) { |key,arg1,arg2| arg1+arg2 } }
# => {:a=>8, :b=>16, :c=>32, :d=>80, :e=>20, :r=>14}
In case of heterogeneous hash (containing both String and Number). For adding only integers.
#resultant_visit_hash = arr.inject { |acc, next_obj| acc.merge(next_obj) { |key,arg1,arg2| arg1+arg2 if (arg1.class == Integer && arg2.class == Integer) } }
Code is self explanatory.

Resources