Recursively setting hash keys from an array of keys - arrays

I want a function that can take an array like [:a, :b, :c] and recursively set hash keys, creating what it needs as it goes.
hash = {}
hash_setter(hash, [:a, :b, :c], 'value')
hash #=> {:a => {:b => {:c => 'value' } } }
hash_setter(hash, [:a, :b, :h], 'value2')
hash #=> {:a => {:b => {:c => 'value', :h => 'value2' } } }
I'm aware that Ruby 2.3's dig can be used for getting in this way, though that doesnt quite get you to an answer. If there was a setter equivalent of dig that'd be what I'm looking for.

Code
def nested_hash(keys, v, h={})
return subhash(keys, v) if h.empty?
return h.merge(subhash(keys, v)) if keys.size == 1
keys[0..-2].reduce(h) { |g,k| g[k] }.update(keys[-1]=>v)
h
end
def subhash(keys, v)
*first_keys, last_key = keys
h = { last_key=>v }
return h if first_keys.empty?
first_keys.reverse_each.reduce(h) { |g,k| g = { k=>g } }
end
Examples
h = nested_hash([:a, :b, :c], 14) #=> {:a=>{:b=>{:c=>14}}}
i = nested_hash([:a, :b, :d], 25, h) #=> {:a=>{:b=>{:c=>14, :d=>25}}}
j = nested_hash([:a, :b, :d], 99, i) #=> {:a=>{:b=>{:c=>14, :d=>99}}}
k = nested_hash([:a, :e], 104, j) #=> {:a=>{:b=>{:c=>14, :d=>99}, :e=>104}}
nested_hash([:f], 222, k) #=> {:a=>{:b=>{:c=>14, :d=>99}, :e=>104}, :f=>222}
Observe that the value of :d is overridden in the calculation of j. Also note that:
subhash([:a, :b, :c], 12)
#=> {:a=>{:b=>{:c=>12}}}
This mutates the hash h. If that is not desired one could insert the line
f = Marshal.load(Marshal.dump(h))
after the line return subhash(keys, v) if h.empty? and change subsequent references to h to f. Methods from the Marshal module can be used to create a deep copy of a hash so the original hash is not be mutated.

Solved it with recursion:
def hash_setter(hash, key_arr, val)
key = key_arr.shift
hash[key] = {} unless hash[key].is_a?(Hash)
key_arr.length > 0 ? hash_setter(hash[key], key_arr, val) : hash[key] = val
end

def set_value_for_keypath(initial, keypath, value)
temp = initial
for key in keypath.first(keypath.count - 1)
temp = (temp[key] ||= {})
end
temp[keypath.last] = value
return initial
end
initial = {:a => {:b => {:c => 'value' } } }
set_value_for_keypath(initial, [:a, :b, :h], 'value2')
initial
Or if you prefer something more unreadable:
def set_value_for_keypath(initial, keypath, value)
keypath.first(keypath.count - 1).reduce(initial) { |hash, key| hash[key] ||= {} }[keypath.last] = value
end

Related

ruby: find item with most occurrences in array, if there is

With this code I can find most occurrences of items in an array:
letters.max_by { |i| letters.count(i) }
But this will return 2 for
a = [1, 2, 2, 3, 3]
although 3 has the same occurrence. How can I find out, if there really is an item with most occurrences? I would like to get false if there is no single champion.
This is pretty ugly and in need of refinement, but:
def champion(array)
grouped = array.group_by(&:itself).values.group_by(&:length)
best = grouped[grouped.keys.max]
if (best.length == 1)
best[0][0]
else
false
end
end
I'm not sure there's an easy single-shot solution for this, at least not one that's not O(n^2) or worse, which is unusual.
I guess you could do this if you don't care about performance:
def max_occurrences(arr)
arr.sort.max_by { |v| arr.count(v) } != arr.sort.reverse.max_by { |v| arr.count(v) } ? false : arr.max_by { |v| arr.count(v) }
end
I would do something like this:
def max_occurrences(arr)
counts = Hash.new { |h, k| h[k] = 0 }
grouped_by_count = Hash.new { |h, k| h[k] = [] }
arr.each { |el| counts[el] += 1 } # O(n)
counts.each { |el, count| grouped_by_count[count] << el } # O(n)
max = grouped_by_count.sort { |x, y| y[0] <=> x[0] }.first[1] # O(n log n)
max.length == 1 ? max[0] : false
end
It's no snazzy one-liner, but it's readable and runs in less than O(n log n).
a = [1, 2, 2, 3, 3]
occurrences = a.inject(Hash.new(0)){ |h, el| h[el] += 1; h } # => {1=>1, 2=>2, 3=>2}
max_occurences = occurrences.max_by{ |_, v| v } # => [2, 2]
max_occurences.count > 1 ? false : occurrences.key(max_occurences.first)

RUBY: Combining 2 different arrays with duplicate values into hash

I have 2 Arrays.
product_name = ["Pomegranate", "Raspberry", "Miracle fruit", "Raspberry"]
product_quantity = [2, 4, 5, 5]
I'd like to know how to initialize a hash such that it becomes
product_hash = {"Pomegranate"=>2, "Raspberry"=>9, "Miracle fruit"=>5}
Use each_with_object:
product_name.zip(product_quantity)
.each_with_object({}) {|(k, v), h| h[k] ? h[k] += v : h[k] = v }
#=> {"Pomegranate"=>2, "Raspberry"=>9, "Miracle fruit"=>5}
Or just use hash with default value:
product_name.zip(product_quantity)
.each_with_object(Hash.new(0)) {|(k, v), h| h[k] += v }
#=> {"Pomegranate"=>2, "Raspberry"=>9, "Miracle fruit"=>5}
I would start with something like this:
product_name.zip(product_quantity)
.group_by(&:first)
.map { |k, v| [k, v.map(&:last).inject(:+)] }
.to_h
#=> { "Pomegranate" => 2, "Raspberry" => 9, "Miracle fruit" => 5}
I suggest to lookup each method in the Ruby's docs for Array and Hash and to check in the console what each the intermediate step returns.
This is but a slight variation of #llya's solution #2.
product_name.each_index.with_object(Hash.new(0)) { |i,h|
h[product_name[i]] += h[product_quantity[i]] } .
Couldn't we just do:
product_name.zip(product_quantity).to_h
Seems to return the correct result for me?

Ruby -- Adding the values of sub arrays in an array by their key

So I know how to add all the values in an array.
Example, the sum of [1,2,3,4]...
[1,2,3,4].inject(&:+)
#=> 10
However, I have an array of arrays and would like to add the values that have the same first element of each sub array.
# example
[["A", 10],["A", 5],["B", 5],["B", 5],["C", 15], ["C", 15]]
Desired output:
"(A : 15) - (B : 10) - (C : 30)"
Any help would be appreciated!
arr = [["A", 10],["A", 5],["B", 5],["B", 5],["C", 15], ["C", 15]]
h = arr.each_with_object(Hash.new(0)) { |(f,g),h| h[f] += g }
#=> {"A"=>15, "B"=>10, "C"=>30}
Then
h.map { |pair| "(%s : %s)" % pair }.join(" - ")
#=> "(A : 15) - (B : 10) - (C : 30)"
which you can combine like so:
arr.each_with_object(Hash.new(0)) { |(f,g),h| h[f] += g }.
map { |pair| "(%s : %s)" % pair }.join(" - ")
See Hash::new, especially with regards to the use of a default value (here 0).
Try this
arr = [["A", 10],["A", 5],["B", 5],["B", 5],["C", 15], ["C", 15]]
arr.group_by(&:first).map { |key, group| [key, group.map(&:last).inject(:+)] }
# => [["A", 15], ["B", 10], ["C", 30]]
How does this work?
group_by(&:first) groups the subarrays by first element
map { |key, group| ... } transforms the groups
group.map(&:last).inject(:+) sums up all last elements in a group
a = [["A", 10],["A", 5],["B", 5],["B", 5],["C", 15], ["C", 15]]
result = a.group_by(&:first).each_with_object({}) do |(k, v), h|
h[k] = v.map(&:last).inject(:+)
# if your on Ruby 2.4+ you can write h[k] = v.sum(&:last)
end
#=> {"A"=>15, "B"=>10, "C"=>30}
Another option would be to build the hash from the beginning:
result = a.each_with_object({}) {|(k, v), h| h[k] = h[k].to_i + v }
#=> {"A"=>15, "B"=>10, "C"=>30}
If your desired output is literally a string "(A : 15) - (B : 10) - (C : 30)":
result.map { |k, v| "(#{k} : #{v})" }.join(' - ')
#=> "(A : 15) - (B : 10) - (C : 30)"
There are more elegant ways of doing this, but here is the solution as a block, so you can understand the logic...
What this does is :
convert the array to a hash, when combining
values.
Then it builds the string, one element at a time, storing
each in an array.
And finally, it combines the array of strings into your desired output.
'
my_array = [["A", 10],["A", 5],["B", 5],["B", 5],["C", 15],["C", 15]]
my_hash = {}
output_array = []
my_array.each do |item|
my_hash[item[0]] ||= 0
my_hash[item[0]] += item[1]
end
my_hash.each do |k,v|
output_array.push("(#{k} : #{v})")
end
puts output_array.join(" - ")

Ruby Hash Values is Array, need to convert to string

I have a hash of integers as keys and arrays of strings as values. I need to convert this to a new hash that inverts this relationship with each item from the array of strings in the original hash values becoming a key in the new hash and each original key becoming the associated value. For example:
original = {1 => ['a', 'b', 'c'], 2 => ['g', 'm', 'z']}
new_hash = {'a' => 1, 'b' => 1, 'c' => 1, 'g' => 2, 'm' => 2, 'z' => 2}
I'm struggling to extract the items from the original array values. It's easy enough to do
original.each { |k, v| new_hash[v] = k }
but this keeps the original array as the new key. I've tried doing something like
original.each { |k, v| new_hash[v.each { |i| i }] = k }
but this also returns the original array for some reason.
Another one, via Array#product:
original.flat_map { |k, v| v.product([k]) }.to_h
#=> {"a"=>1, "b"=>1, "c"=>1, "g"=>2, "m"=>2, "z"=>2}
original.flat_map { |k, vs| vs.map { |v| {v => k} } }.reduce(&:merge)
the below snippet will give what you want, but let me think on a more readable and elegant solution.
newhash = {}
original.each do |k,v|
v.each do |v2|
newhash[v2] = k
end
end
#=> {1=>["a", "b", "c"], 2=>["g", "m", "z"]}
newhash
#=> {"a"=>1, "b"=>1, "c"=>1, "g"=>2, "m"=>2, "z"=>2}
Your approach is close. You'll have to iterate each element in the values array when assigning the new key/value pair to the newHash
newHash = {}
original.each { |k, v| v.each {|i| newHash[i] = k}}
original.map { |number, ary| Hash[ary.map { |char| [char, number] }] }.reduce(&:merge)

How to merge two arrays of hashes by the same pair of key and value ruby

I'm new in ruby. I have two hashes:
f = { "server"=>[{ "hostname"=>"a1", "ip"=>"10" }, {"hostname"=>"b1", "ip"=>"10.1" }] }
g = { "admin" =>[{ "name"=>"adam", "mail"=>"any", "hostname"=>"a1" },
{ "name"=>"mike", "mail"=>"id", "hostname"=>"b1"}]}
and I want to get another hash like this:
{ "data" => [{"hostname"=>"a1", "ip"=>"10", "name" =>"adam", "mail"=>"any"},
{"hostname"=>"b1", "ip"=>"10.1", "name" =>"mike", "mail"=>"id"}]}
The pairs "hostname"=>"something" always matches in hashes of both arrays. I have tried something like this:
data = server.merge(admin)
but it isn't so easy and as you expect it doesn't work. Could you help me merge these hashes and explain for the future how you did it?
A quick way that i can think of right now will look like:
servers = { "server" => [{"hostname"=>"a1", "ip"=>"10"}, {"hostname"=>"b1", "ip"=>"10.1"}]}
admins = { "data" => [{"hostname"=>"a1", "ip"=>"10", "name" =>"adam", "mail"=>"any"}, {"hostname"=>"b1", "ip"=>"10.1", "name" =>"mike", "mail"=>"id"}]}
# FYI: you can just use arrays for representing the above data, you don't necessarily need a hash.
list_of_entries = (servers.values + admins.values).flatten
grouped_by_hostname_entries = list_of_entries.group_by { |h| h['hostname'] }
grouped_by_hostname_entries.map { |_, values| values.inject({}, :merge) }
#=> [{"hostname"=>"a1", "ip"=>"10", "name"=>"adam", "mail"=>"any"}, {"hostname"=>"b1", "ip"=>"10.1", "name"=>"mike", "mail"=>"id"}]
As another variant you can try this
h1 = { "server" => [{"hostname"=>"a1", "ip"=>"10"}, {"hostname"=>"b1", "ip"=>"10.1"}]}
h2 = { "admin" => [{"name" =>"adam", "mail"=>"any", "hostname"=>"a1"}, {"name" =>"mike", "mail"=>"id", "hostname"=>"b1"}]}
h1['server'].zip(h2['admin']).map { |ar| ar.first.merge(ar.last) }
#=> [{"hostname"=>"a1", "ip"=>"10", "name"=>"adam", "mail"=>"any"}, {"hostname"=>"b1", "ip"=>"10.1", "name"=>"mike", "mail"=>"id"}]
zip let us iterate through two or more arrays at the same time.
We use map to return result.
In map block ar would be equal
[{"hostname"=>"a1", "ip"=>"10"}, {"name"=>"adam", "mail"=>"any", "hostname"=>"a1"}]
[{"hostname"=>"b1", "ip"=>"10.1"}, {"name"=>"mike", "mail"=>"id", "hostname"=>"b1"}]
So ar.first would be {"hostname"=>"a1", "ip"=>"10"} and the ar.last would be {"name"=>"adam", "mail"=>"any", "hostname"=>"a1"}
Finally we use merge to combine two hashes.
Hope this will help.
Code and example
ff = f["server"].each_with_object({}) { |g,h| h[g["hostname"]] = g }
#=> {"a1"=>{"hostname"=>"a1", "ip"=>"10"}, "b1"=>{"hostname"=>"b1", "ip"=>"10.1"}}
{ "data"=>g["admin"].map { |h| h.merge(ff[h["hostname"]]) } }
#=> {"data"=>[{"name"=>"adam", "mail"=>"any", "hostname"=>"a1", "ip"=>"10"},
# {"name"=>"mike", "mail"=>"id", "hostname"=>"b1", "ip"=>"10.1"}]}
Explanation
We want to produce a hash
{ "data"=>arr }
where
arr #=> [{ "name"=>"adam", "mail"=>"any", "hostname"=>"a1", "ip"=>"10" },
# { "name"=>"mike", "mail"=>"id", "hostname"=>"b1", "ip"=>"10.1" }]
so we need only compute arr.
First, we create the hash
ff = f["server"].each_with_object({}) { |g,h| h[g["hostname"]] = g }
#=> {"a1"=>{"hostname"=>"a1", "ip"=>"10"}, "b1"=>{"hostname"=>"b1", "ip"=>"10.1"}}
We have
enum = f["server"].each_with_object({})
#=> #<Enumerator: [{"hostname"=>"a1", "ip"=>"10"},
# {"hostname"=>"b1", "ip"=>"10.1"}]:each_with_object({})>
We can see the elements that will be generated by this enumerator (and passed to its block) by converting it to an array:
enum.to_a
#=> [[{"hostname"=>"a1", "ip"=>"10"}, {}],
# [{"hostname"=>"b1", "ip"=>"10.1"}, {}]]
Note
enum.each { |g,h| h[g["hostname"]] = g }
#=> {"a1"=>{"hostname"=>"a1", "ip"=>"10"},
# "b1"=>{"hostname"=>"b1", "ip"=>"10.1"}}
each passes the first element of enum and assigns the block variables using parallel assignement (also call multiple assignment):
g,h = enum.next
#=> [{"hostname"=>"a1", "ip"=>"10"}, {}]
g #=> {"hostname"=>"a1", "ip"=>"10"}
h #=> {}
We may now perform the block calculation:
h[g["hostname"]] = g
#=> h["a1"] = {"hostname"=>"a1", "ip"=>"10"}
#=> {"hostname"=>"a1", "ip"=>"10"}
The return value is the new value of the block variable h. The second element of enum is then passed to the block and the block calculation is performed:
g,h = enum.next
#=> [{"hostname"=>"b1", "ip"=>"10.1"}, {"a1"=>{"hostname"=>"a1", "ip"=>"10"}}]
g #=> {"hostname"=>"b1", "ip"=>"10.1"}
h #=> {"a1"=>{"hostname"=>"a1", "ip"=>"10"}}
Notice that the hash h has been updated.
h[g["hostname"]] = g
#=> {"hostname"=>"b1", "ip"=>"10.1"}
So now
h #=> {"a1"=>{"hostname"=>"a1", "ip"=>"10"},
# "b1"=>{"hostname"=>"b1", "ip"=>"10.1"}}
and
ff #=> {"a1"=>{"hostname"=>"a1", "ip"=>"10"}, "b1"=>{"hostname"=>"b1", "ip"=>"10.1"}}
Now we can compute arr:
g["admin"].map { |h| h.merge(ff[h["hostname"]]) }
The first element of g["admin"] is passed to the block and assigned to the block variable:
h = g["admin"][0]
#=> {"name"=>"adam", "mail"=>"any", "hostname"=>"a1"}
and the block calculation is performed:
h.merge(ff[h["hostname"]])
#=> h.merge(ff["a1"])
#=> h.merge({"hostname"=>"a1", "ip"=>"10"})
#=> {"name"=>"adam", "mail"=>"any", "hostname"=>"a1", "ip"=>"10"}
Then
h = g["admin"][1]
#=> {"name"=>"mike", "mail"=>"id", "hostname"=>"b1"}
h.merge(ff[h["hostname"]])
#=> h.merge(ff["b1"])
#=> h.merge({"hostname"=>"a2", "ip"=>"10"})
#=> {"name"=>"mike", "mail"=>"id", "hostname"=>"a2", "ip"=>"10"}
Therefore,
arr
#=> [{"name"=>"adam", "mail"=>"any", "hostname"=>"a1", "ip"=>"10"},
#=> {"name"=>"mike", "mail"=>"id", "hostname"=>"b1", "ip"=>"10.1"}]
is returned by the block and we are finished.
f = { "server"=>[{ "hostname"=>"a1", "ip"=>"10" },
{"hostname"=>"b1", "ip"=>"10.1" }] }
g = { "admin" =>[{ "name"=>"adam", "mail"=>"any", "hostname"=>"a1" },
{ "name"=>"mike", "mail"=>"id", "hostname"=>"b1"}]}
# manual way
host_admin_merge = []
host_admin_merge << f["server"].first.merge(g["admin"].first)
host_admin_merge << f["server"].last.merge(g["admin"].last)
# a bit more automated, iterate, test key's value, append to new array
host_admin_merge = []
f["server"].each do |host|
g["admin"].each do |admin|
if admin[:hostname] == host[:hostname]
host_admin_merge << host.merge(admin)
end
end
end
# assign the array to a hash with "data" as the key
host_admin_hash = {}
host_admin_hash["data"] = host_admin_merge
p host_admin_hash

Resources