Is there a clever way to achieve the following in Ruby?
hash1 = { "a" => 1, "b" => 2, "d" => 3}
hash2 = { "a" => 4, "b" => 5, "c" => 7}
hash3 = { "a" => 4, "d" => 7, "e" => 9}
puts hash1.csvMerge(hash2).csvMerge(hash3)
with the output being:
{ "a" => "1,4,4",
"b" => "2,5,0",
"c" => "0,7,0",
"d" => "3,0,7",
"e" => "0,0,9" }
What I'm trying to do is merge a bunch of Ruby hashes, in practice I have over a dozen, into a single hash where the values are combined into a comma separated string.
hash1.merge(hash2){|key, oldval, newval| [oldval,newval].join(",")}
=> {"a"=>"1,4", "b"=>"2,5", "d"=>3, "c"=>7}
hashes = [hash1, hash2, hash3]
keys = hashes.inject({}){|hh, h| hh = hh.merge(h); hh}.keys # the set of all keys #
default_hash = keys.inject({}){|d, k| d[k] = 0; d} # hash with value 0 for all keys #
complemented_hashes = hashes.map{|h| default_hash.merge(h)} # missing values filled in #
p Hash[complemented_hashes.map{|h| h.to_a}.flatten(1).group_by{|k, v| k}.
map{|k, v| [k, v.map{|k, v| v}.join(",")]}]
I dont know if this has performance issues
hashes = [hash1, hash2, hash3]
hash_with_all_keys = {}
hashes.each{|hash| hash_with_all_keys.merge!(hash)}
keys = hash_with_all_keys.keys
result_hash_mapping = keys.map do |key|
value = hashes.map{|hash| hash[key].to_i}.join(",")
[key, value]
end
result_hash = Hash[result_hash_mapping]
I could have replaced lines 2 to 4 with keys = hashes.inject({}){|merge_hash, hash| merge_hash.merge(hash)}.keys but I find it hard to read.
hash1 = { "a" => 1, "b" => 2, "d" => 3}
hash2 = { "a" => 4, "b" => 5, "c" => 7}
hash3 = { "a" => 4, "d" => 7, "e" => 9}
hashes = [hash1, hash2, hash3]
hashes.each{|h| h.each{|k,v| h[k] = v.to_s }}
res = hashes.inject{|m,h| m.merge(h){|k,old,new| "#{old},#{new}" }} #merge and glue
p res
#=> {"a"=>"1,4,4", "b"=>"2,5", "d"=>"3,7", "c"=>"7", "e"=>"9"}
Functional approach:
hashes = [hash1, hash2, hash3]
result = Hash[hashes.flat_map(&:keys).uniq.map do |key|
[key, hashes.map { |h| h[key] || 0 }.join(",")]
end]
#=> {"a"=>"1,4,4", "b"=>"2,5,0", "c"=>"0,7,0", "d"=>"3,0,7", "e"=>"0,0,9"}
Related
I have the following Ruby hash
a = {
7 => [1469, 2283],
5 => [1469, 5464],
3 => [7561],
6 => [7952, 8114],
4 => []
}
and would like to get the keys that a number shows up in as a value
b = {
1469 => [7,5],
2283 => [7],
5464 => [5]
...
}
How would I do this? I'm sure there's some super slick way of getting it done.
Given:
a = {
7 => [1469, 2283],
5 => [1469, 5464],
3 => [7561],
6 => [7952, 8114],
4 => [1469, 2283],
2 => []
}
Use a default value of a new array in b:
b=Hash.new {|hsh, key| hsh[key] = [] }
a.each{|k,v| v.each{|n| b[n] << k} }
Or include the object creation with each_with_object:
b=a.each_with_object(Hash.new {|h,k| h[k] = []}) { |(k,v), h|
v.each{ |e| h[e] << k } }
Result b is:
{1469=>[7, 5, 4], 2283=>[7, 4], 5464=>[5], 7561=>[3], 7952=>[6], 8114=>[6]}
Using #each_with_object to iterate over the original hash and the values stored in each array and build up a new hash based on it.
a.each_with_object({}) do |(k, v), h|
v.each do |val|
h[val] ||= []
h[val] << k
end
end
Result:
{
1469 => [7, 5],
2283 => [7],
5464 => [5],
7561 => [3],
7952 => [6],
8114 => [6]
}
#dawg's answer is the way I would go
a.each_with_object(Hash.new {|h,k| h[k] = []}) do |(k,v),obj|
v.each{|e| obj[e] << k}
end
But here are a few other options: (a lot of allocations though)
Array#product and Hash#merge
a.each_with_object({}) do |(k,v),obj|
obj.merge!(v.product([[k]]).to_h) {|_,o,v| [*o,*v]}
end
#map then #reduce
a.map {|k,v| v.product([[k]]).to_h }
.reduce {|memo, h| memo.merge(h) {|_,o,n| o.concat(n)}}
This works too (don't ask questions)
a.map {|k,v| v.each_with_object(k)}
.reduce(&:+)
.group_by(&:shift)
.transform_values(&:flatten)
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"]
let's have this hash:
hash = {"a" => 1, "b" => {"c" => 3}}
hash.get_all_keys
=> ["a", "b", "c"]
how can i get all keys since hash.keys returns just ["a", "b"]
This will give you an array of all the keys for any level of nesting.
def get_em(h)
h.each_with_object([]) do |(k,v),keys|
keys << k
keys.concat(get_em(v)) if v.is_a? Hash
end
end
hash = {"a" => 1, "b" => {"c" => {"d" => 3}}}
get_em(hash) # => ["a", "b", "c", "d"]
I find grep useful here:
def get_keys(hash)
( hash.keys + hash.values.grep(Hash){|sub_hash| get_keys(sub_hash) } ).flatten
end
p get_keys my_nested_hash #=> ["a", "b", "c"]
I like the solution as it is short, yet it reads very nicely.
Version that keeps the hierarchy of the keys
Works with arrays
Works with nested hashes
keys_only.rb
# one-liner
def keys_only(h); h.map { |k, v| v = v.first if v.is_a?(Array); v.is_a?(Hash) ? [k, keys_only(v)] : k }; end
# nicer
def keys_only(h)
h.map do |k, v|
v = v.first if v.is_a?(Array);
if v.is_a?(Hash)
[k, keys_only(v)]
else
k
end
end
end
hash = { a: 1, b: { c: { d: 3 } }, e: [{ f: 3 }, { f: 5 }] }
keys_only(hash)
# => [:a, [:b, [[:c, [:d]]]], [:e, [:f]]]
P.S.: Yes, it looks like a lexer :D
Bonus: Print the keys in a nice nested list
# one-liner
def print_keys(a, n = 0); a.each { |el| el.is_a?(Array) ? el[1] && el[1].class == Array ? print_keys(el, n) : print_keys(el, n + 1) : (puts " " * n + "- #{el}") }; nil; end
# nicer
def print_keys(a, n = 0)
a.each do |el|
if el.is_a?(Array)
if el[1] && el[1].class == Array
print_keys(el, n)
else
print_keys(el, n + 1)
end
else
puts " " * n + "- #{el}"
end
end
nil
end
> print_keys(keys_only(hash))
- a
- b
- c
- d
- e
- f
def get_all_keys(hash)
hash.map do |k, v|
Hash === v ? [k, get_all_keys(v)] : [k]
end.flatten
end
Please take a look of following code:
hash = {"a" => 1, "b" => {"c" => 3}}
keys = hash.keys + hash.select{|_,value|value.is_a?(Hash)}
.map{|_,value| value.keys}.flatten
p keys
result:
["a", "b", "c"]
New solution, considering #Bala's comments.
class Hash
def recursive_keys
if any?{|_,value| value.is_a?(Hash)}
keys + select{|_,value|value.is_a?(Hash)}
.map{|_,value| value.recursive_keys}.flatten
else
keys
end
end
end
hash = {"a" => 1, "b" => {"c" => {"d" => 3}}, "e" => {"f" => 3}}
p hash.recursive_keys
result:
["a", "b", "e", "c", "d", "f"]
Also deal with nested arrays that include hashes
def all_keys(items)
case items
when Hash then items.keys + items.values.flat_map { |v| all_keys(v) }
when Array then items.flat_map { |i| all_keys(i) }
else []
end
end
class Hash
def get_all_keys
[].tap do |result|
result << keys
values.select { |v| v.respond_to?(:get_all_keys) }.each do |value|
result << value.get_all_keys
end
end.flatten
end
end
hash = {"a" => 1, "b" => {"c" => 3}}
puts hash.get_all_keys.inspect # => ["a", "b", "c"]
Here is another approach :
def get_all_keys(h)
h.each_with_object([]){|(k,v),a| v.is_a?(Hash) ? a.push(k,*get_all_keys(v)) : a << k }
end
hash = {"a" => 1, "b" => {"c" => {"d" => 3}}}
p get_all_keys(hash)
# >> ["a", "b", "c", "d"]
I'm sure there is a more elegant solution, but this option works:
blah = {"a" => 1, "b" => {"c" => 3}}
results = []
blah.each do |k,v|
if v.is_a? Hash
results << k
v.each_key {|key| results << key}
else
results << k
end
end
puts results
hash.keys is the simplest one I have seen to return an array of the key values in a hash.
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 }
Write a method, which given an array, returns a hash whose keys are words in the array and whose values are the number of times each word appears.
arr=["A", "man", "a", "plan", "a", "canal","Panama"]
# => {'a' => 3, 'man' => 1, 'canal' => 1, 'panama' => 1, 'plan' => 1}
How do I achieve that? Here's my code:
hash={}
arr.each do |i|
hash.each do |c,v|
hash[c]=v+1
end
end
hash = arr.inject({}) do |hash, element|
element.downcase!
hash[element] ||= 0
hash[element] += 1
hash
end
arr.inject(Hash.new(0)){|h,k| k.downcase!; h[k] += 1; h}
arr = ["A", "man", "a", "plan", "a", "canal","Panama"]
r = {}
arr.each { |e| e.downcase!; r[e] = arr.count(e) if r[e].nil? }
Output
p r
#==> {"a"=>3, "man"=>1, "plan"=>1, "canal"=>1, "panama"=>1}