I have 3 array of hashes:
a = [{name: 'Identifier', value: 500}, {name: 'Identifier2', value: 50 }]
b = [{name: 'Identifier', value: 500}, {name: 'Identifier2', value: 50 }]
c = [{name: 'Identifier', value: 500}, {name: 'Identifier2', value: 50 }]
and I have to merge them into one, based on the name prop of each identifier, so the result will be:
d = [{name: 'Identifier', value: 1500 }, {name: 'Identifier2', value: 150}]
Is there a smart ruby way of doing this, or do I have to do create another hash where the keys are the identifiers, the values the values and then transform it into an array?
Thank you.
When the values of a single key in a collection of hashes are to be totaled I usually begin by constructing a counting hash:
h = (a+b+c).each_with_object({}) do |g,h|
h[g[:name]] = (h[g[:name]] || 0) + g[:value]
end
#=> {"Identifier"=>1500, "Identifier2"=>150}
Note that if h does not have a key g[:name], h[g[:name]] #=> nil, so:
h[g[:name]] = (h[g[:name]] || 0) + g[:value]
= (nil || 0) + g[:value]
= 0 + g[:value]
= g[:value]
We may now easily obtain the desired result:
h.map { |(name,value)| { name: name, value: value } }
#=> [{:name=>"Identifier", :value=>1500},
# {:name=>"Identifier2", :value=>150}]
If desired these two expressions can be chained:
(a+b+c).each_with_object({}) do |g,h|
h[g[:name]] = (h[g[:name]] || 0) + g[:value]
end.map { |(name,value)| { name: name, value: value } }
#=> [{:name=>"Identifier", :value=>1500},
# {:name=>"Identifier2", :value=>150}]
Sometimes you might see:
h[k1] = (h[k1] || 0) + g[k2]
written:
(h[k1] ||= 0) + g[k2]
which expands to the same thing.
Another way to calculate h, which I would say is more "Ruby-like", is the following.
h = (a+b+c).each_with_object(Hash.new(0)) do |g,h|
h[g[:name]] += g[:value]
end
This creates the hash represented by the block variable h using the form of Hash::new that takes an argument called the default value:
h = Hash.new(0)
All this means is that if h does not have a key k, h[k] returns the default value, here 0. Note that
h[g[:name]] += g[:value]
expands to:
h[g[:name]] = h[g[:name]] + g[:value]
so if h does not have a key g[:name] this reduces to:
h[g[:name]] = 0 + g[:value]
If you were wondering why h[g[:name]] on the left of the equality was not replaced by 0, it is because that part of the expression employs the method Hash#[]=, whereas the method Hash#[] is used on he right. Hash::new with a default value only concerns Hash#[].
You can do everything in ruby !
Here is a solution to your problem :
d = (a+b+c).group_by { |e| e[:name] }.map { |f| f[1][0].merge(value: f[1].sum { |g| g[:value] }) }
I encourage you to check the Array Ruby doc for more information: https://ruby-doc.org/core-2.7.0/Array.html
I am assuming that the order of identifiers in all arrays is the same. That is {name: 'Identifier', value: ...} always the first element in all 3 arrays, {name: 'Identifier2', value: ... } always the second, etc. In this simple case, a simple each_with_index is a simple and clear solution:
d = []
a.each_with_index do |hash, idx|
d[idx] = {name: hash[:name], value: a[idx][:value] + b[idx][:value] + c[idx][:value] }
end
# Or a more clear version using map:
a.each_with_index do |hash, idx|
d[idx] = {name: hash[:name], value: [a, b, c].map { |h| h[idx][:value] }.sum }
end
A couple different ways, avoiding any finicky array-indexing and the like, (also functionally, since you've added the tag):
grouped = (a + b + c).group_by { _1[:name] }
name_sums = grouped.transform_values { |hashes| hashes.map { _1[:value] }.sum }
name_vals = (a + b + c).map { Hash[*_1.values_at(:name, :value)] }
name_sums = name_vals.reduce { |l, r| l.merge(r) { |k, lval, rval| lval + rval } }
in either case, finish it off with:
name_sums.map { |name, value| { name: name, value: value } }
Related
I'm supposed to write a method that takes a nested hash as input and returns that hash with added "depth" keys. So, for example, the following input:
tree = {
a: 1,
b: 2,
c: { d: { e: 3 } }
}
would yield the following return value:
{
a: 1,
b: 2,
c: {
d: {
e: 3,
depth: 2
},
depth: 1
},
depth: 0
}
If the input is not a hash, then the function should return nil.
This is what I came up with:
def depth(hash)
num = 0
hash.each do |key, value|
if value.class == Hash
num += 1
v[:depth] = num
value.each do |k, v|
if v.class == Hash
num += 1
v[:depth] = num
v.each do |ky, val|
if val.class == Hash
num += 1
v[:depth] = num
val.each do |ke, vl|
if vl.class == Hash
num += 1
v[:depth] = num
end
end
end
end
end
end
end
num = 0
end
end
but it's limited to hash depth of 4, and I can't just keep making the method bigger.
Try this.
def depth(h, i=0)
h.each_with_object(depth: i) { |(k,v),g| g[k] = v.is_a?(Hash) ? depth(v, i+1) : v }
end
depth { a: 1, b: 2, c: { d: { e: 3 } }
#=> {:depth=>0, :a=>1, :b=>2, :c=>{:depth=>1, :d=>{:depth=>2, :e=>3}}}
I need to filter a nested hash to return items for a particular combination of attributes. If the attribute is present it returns that hash, if the attribute is not present it returns the default. If the attribute is set to 'none' it returns nothing. Consider the following hash:
{
"size"=>{
"default"=>{
"jeans"=>"boyfriend"
},
"blue"=>"none"
},
"style"=>{
"default"=>{
"shoes"=>"boots"
},
"blue"=>{
"jeans"=>"jeggings"
}
}
}
if the color is 'black', then
{
"size"=>{
"jeans"=>"boyfriend"
},
"style"=>{
"shoes"=>"boots"
}
}
or if the color is 'blue', then
{
"size"=>{
},
"style"=>{
"jeans"=>"jeggings"
}
}
What is best way to do this? I have tried various combinations of select and delete but either end up with an array or a hash with the color key included.
Letting h be the hash given in the question, the following method will return the desired hash if my understanding of the question is correct.
def doit(h, color)
h.each_with_object({}) do |(k,f),g|
c,v = f.find { |kk,_| kk != "default" }
if c == color
g[k] = v.is_a?(Hash) ? v : {}
else
g[k] = f["default"]
end
end
end
doit(h, 'black')
#=> {"size"=>{"jeans"=>"boyfriend"}, "style"=>{"shoes"=>"boots"}}
doit(h, 'blue')
#=> {"size"=>{}, "style"=>{"jeans"=>"jeggings"}}
The steps for the second example are as follows.
color = 'blue'
enum = h.each_with_object({})
#=> #<Enumerator: {"size"=>{"default"=>{"jeans"=>"boyfriend"},
# "blue"=>"none"}, "style"=>{"default"=>{"shoes"=>"boots"},
# "blue"=>{"jeans"=>"jeggings"}}}:each_with_object({})>
The first value of this enumerator is generated:
x = enum.next
#=> [["size", {"default"=>{"jeans"=>"boyfriend"}, "blue"=>"none"}], {}]
and passed to the block. The block variables are set equal to x and their values are determined by "disambiguation":
(k,f),g = x
k #=> "size"
f ##=> {"default"=>{"jeans"=>"boyfriend"}, "blue"=>"none"}
g #=> {}
The block calculation is now performed.
c,v = f.find { |kk,_| kk != "default" }
#=> ["blue", "none"]
c #=> "blue"
v #=> "none"
As
c == color
#=> "blue" == "blue" => true
we compute
v.is_a?(Hash)
#=> false
and therefore perform the assignment
g[k] = {}
#=> {}
so that now
g #=> {"size"=>{}}
The second and last element of h is now generated and passed to the block.
x = enum.next
#=> [["style", {"default"=>{"shoes"=>"boots"},
# "blue"=>{"jeans"=>"jeggings"}}], {"style"=>{"jeans"=>"jeggings"}}]
(k,f),g = x
k #=> "style"
f #=> {"default"=>{"shoes"=>"boots"}, "blue"=>{"jeans"=>"jeggings"}}
g #=> {"size"=>"none"}
c,v = f.find { |kk,_| kk != "default" }
#=> ["blue", {"jeans"=>"jeggings"}]
c #=> "blue"
v #=> {"jeans"=>"jeggings"}
c == color
# "blue" == "blue" => true
v.is_a?(Hash)
#=> true
g[k] = v
#=> {"jeans"=>"jeggings"}
g #=> {"size"=>"none", "style"=>{"jeans"=>"jeggings"}}
and g is returned.
Below is what I ended up with after some refactoring. It works and tests all pass. Could do with more refactoring.
class Filterer
def self.filter(facets, color)
acc = {}
facets.each do |k, facets|
facets.each do |_, facet|
acc[k] = color_facets(color, facets)
end
end
acc
end
def self.color_facets(color, facets)
return {} if no_facets?(color, facets)
facets[color] ? facets[color] : facets['default']
end
def self.no_facets?(color, facets)
facets[color] && facets[color] == 'no facet'
end
end
I have an array of pairs like this:
arr = [
{lat: 44.456, lng: 33.222},
{lat: 42.456, lng: 31.222},
{lat: 44.456, lng: 33.222},
{lat: 44.456, lng: 33.222},
{lat: 42.456, lng: 31.222}
]
There are some geographical coordinates of some places. I want to get an array with these coordinates grouped and sorted by frequency. The result should look like this:
[
{h: {lat: 44.456, lng: 33.222}, fr: 3},
{h: {lat: 42.456, lng: 31.222}, fr: 2},
]
How can I do this?
The standard ways of approaching this problem are to use Enumerable#group_by or a counting hash. As others have posted answers using the former, I'll go with the latter.
arr.each_with_object(Hash.new(0)) { |f,g| g[f] += 1 }.map { |k,v| { h: k, fr: v } }
#=> [{:h=>{:lat=>44.456, :lng=>33.222}, :fr=>3},
# {:h=>{:lat=>42.456, :lng=>31.222}, :fr=>2}]
First, count instances of the hashes:
counts = arr.each_with_object(Hash.new(0)) { |f,g| g[f] += 1 }
#=> {{:lat=>44.456, :lng=>33.222}=>3,
# {:lat=>42.456, :lng=>31.222}=>2}
Then construct the array of hashes:
counts.map { |k,v| { h: k, fr: v } }
#=> [{:h=>{:lat=>44.456, :lng=>33.222}, :fr=>3},
# {:h=>{:lat=>42.456, :lng=>31.222}, :fr=>2}]
g = Hash.new(0) creates an empty hash with a default value of zero. That means that if g does not have a key k, g[k] returns zero. (The hash is not altered.) g[k] += 1 is first expanded to g[k] = g[k] + 1. If g does not have a key k, g[k] on the right side returns zero, so the expression becomes:
g[k] = 1.
Alternatively, you could write:
counts = arr.each_with_object({}) { |f,g| g[f] = (g[f] ||= 0) + 1 }
If you want the elements (hashes) of the array returned to be in decreasing order of the value of :fr (here it's coincidental), tack on Enumerable#sort_by:
arr.each_with_object(Hash.new(0)) { |f,g| g[f] += 1 }.
map { |k,v| { h: k, fr: v } }.
sort_by { |h| -h[:fr] }
arr.group_by(&:itself).map{|k, v| {h: k, fr: v.length}}.sort_by{|h| h[:fr]}.reverse
# =>
# [
# {:h=>{:lat=>44.456, :lng=>33.222}, :fr=>3},
# {:h=>{:lat=>42.456, :lng=>31.222}, :fr=>2}
# ]
arr.group_by{|i| i.hash}.map{|k, v| {h: v[0], fr: v.size}
#=> [{:h=>{:lat=>44.456, :lng=>33.222}, :fr=>3}, {:h=>{:lat=>42.456, :lng=>31.222}, :fr=>2}]
Is there a way to determine if a hash is a nested hash in ruby ?
For example
a = { a: 1, b: 2, c: 2 }
should return false
a = { a: {a1: 1, a2: 2}, b: {b1: 1}, c: 2 }
should return true
a.any? { |_, v| v.is_a?(Hash) }
You can check it by iterating over your hash values with Hash#values method:
a.values.any? { |v| v.is_a?(Hash) }
Let's say I have a Ruby Hash where at least two keys are identical, except for their case, for instance:
{ 'Foo' => 1, 'foo' => 2, 'bar' => 3 }
Is there a way I can combine like keys (except for their case) such that the resulting Hash might look like this?
{ 'foo' => 3, 'bar' => 3 }
Thank you!
You can build a new hash:
new_hash = Hash.new(0)
old_hash.each_pair { |k, v| new_hash[k.downcase] += v }
You can use inject to loop all the hash items and build a new hash.
hash = { 'Foo' => 1, 'foo' => 2, 'bar' => 3 }
hash.inject({}) do |result, (key, value)|
key = key.downcase
result[key] = result[key] ? result[key] + value : value
result
end
Here is one more way of doing this
h = { 'Foo' => 1, 'foo' => 2, 'bar' => 3 }
p h.collect{|k, v| {k.downcase => v}}.reduce { |a, v| a.update(v) {|k, o, n| o + n } }
#=> {"foo"=>3, "bar"=>3}
h = { 'Foo' => 1, 'foo' => 2, 'bar' => 3 }
h.each_with_object({}) { |(k,v),g| g[k.downcase] = g[k.downcase].to_i + v }
#=> {"foo"=>3, "bar"=>3}
This makes use of the fact that if g does not have a key e, g[e] on the right side will equal nil and nil.to_i #=> 0. On the other hand, if g has a key e, h[e].to_i will equal h[e].
Another way:
h.each_with_object({}) { |(k,v),g| g.update(k.downcase=>v) { |_,o,v| o+v } }
#=> {"foo"=>3, "bar"=>3}