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
Related
I have a hash like this:
t={"4nM"=>"Triangle", "I40"=>"Triangle", "123"=>"Square"}
And I want to turn it into a hash like:
{"Triangle" => ["4nM", "I40"], "Square" => ["123"]}
What is the best way to do this?
I start with group_by but then the code gets to be a bit convoluted....
This is what I did:
t.group_by { |k, v| v }.map { |type, group| {type => group.flatten.reject { |x| x == type } } }
h = { "4nM"=>"Triangle", "I40"=>"Triangle", "123"=>"Square" }
h.each_with_object({}) { |(k,v),h| (h[v] ||= []) << k }
#=> {"Triangle"=>["4nM", "I40"], "Square"=>["123"]}
The expression
(h[v] ||= []) << k
expands to
(h[v] = h[v] || []) << k
If h has a key v, h[k] will be truthy, so the expression above reduces to
(h[v] = h[v]) << k
and then
h[v] << k
If h does not have a key v, h[k] #=> nil, so the expression above reduces to
(h[v] = []) << k
resulting in
h[v] #=> [k]
Alternatively, we could write
h.each_with_object(Hash.new { |h,k| h[k] = [] }) { |(k,v),h| h[v] << k }
#=> {"Triangle"=>["4nM", "I40"], "Square"=>["123"]}
See Hash::new for an explanation of the use of a block for returning the default values of keys that are not present in the hash.
This is the shortest I could write :
t.group_by(&:last).map{|k,v|[k,v.map(&:first)]}.to_h
Still 4 characters longer than #Cary Swoveland's answer.
Note that in Rails, Hash#transform_values makes it a bit easier :
t.group_by{|_,v| v }.transform_values{|v| v.map(&:first) }
You can cut it down a little bit by doing this
t.group_by {|k,v| v}.map{|k,v| {k => v.map(&:first)}}
but your original implementation was already pretty concise.
t={"4nM"=>"Triangle", "I40"=>"Triangle", "123"=>"Square"}
h = Hash.new{[]}
t.each{|k,v| h[v] <<= k}
I can use hashes as keys and values:
a = {}
b = {}
a[b] = b
a #=> {{}=>{}}
a[b] == b #=> true
I can even put a hash inside itself as a value:
a[:a] = a
a #=> {{}=>{}, :a=>{...}}
a == a[:a] #=> true
But I can't put a hash inside itself as a key:
a[a] = a
a #=> {{}=>{}, {...}=>{...}}
a[a] #=> nil
a[a] == a #=> false
I would expect a == a[a] #=> true in this case.
Why does this happen? I don't have a use case for this, I'm just curious about why a hash can't be used as it's own key.
It is not that you cannot. You just need to rehash after you modify a mutable key in the hash.
a = {}
b = {}
a[b] = b
a[:a] = a
a[a] = a
a.rehash
# => {{}=>{}, :a=>{...}, {...}=>{...}}
a[a] == a
# => true
The a is different before and after a[a] = a. So you needed to update the a key of a.
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}
How to merge array of hash based on the same keys in ruby?
example :
a = [{:a=>1},{:a=>10},{:b=>8},{:c=>7},{:c=>2}]
How to get result like this?
a = [{:a=>[1, 10]},{:b=>8},{:c=>[7, 2]}]
Try
a.flat_map(&:entries)
.group_by(&:first)
.map{|k,v| Hash[k, v.map(&:last)]}
Another alternative:
a = [{:a=>1},{:a=>10},{:b=>8},{:c=>7},{:c=>2}]
p a.each_with_object({}) { |h, o| h.each { |k,v| (o[k] ||= []) << v } }
# => {:a=>[1, 10], :b=>[8], :c=>[7, 2]}
It also works when the Hashes have multiple key/value combinations, e.g:
b = [{:a=>1, :b=>5, :x=>10},{:a=>10, :y=>2},{:b=>8},{:c=>7},{:c=>2}]
p b.each_with_object({}) { |h, o| h.each { |k,v| (o[k] ||= []) << v } }
# => {:a=>[1, 10], :b=>[5, 8], :x=>[10], :y=>[2], :c=>[7, 2]}
Minor addition to answer by Arie Shaw to match required answer:
a.flat_map(&:entries)
.group_by(&:first)
.map{|k,v| Hash[k, v.size.eql?(1) ? v.last.last : v.map(&:last) ]}
#=> [{:a=>[1, 10]}, {:b=>8}, {:c=>[7, 2]}]
I'd do :
a = [{:a=>1},{:a=>10},{:b=>8},{:c=>7},{:c=>2}]
merged_hash = a.each_with_object({}) do |item,hsh|
k,v = item.shift
hsh[k] = hsh.has_key?(k) ? [ *Array( v ), hsh[k] ] : v
end
merged_hash.map { |k,v| { k => v } }
# => [{:a=>[10, 1]}, {:b=>8}, {:c=>[2, 7]}]
update
A better taste :
a = [{:a=>1},{:a=>10},{:b=>8},{:c=>7},{:c=>2}]
merged_hash = a.each_with_object({}) do |item,hsh|
k,v = item.shift
(hsh[k] ||= []) << v
end
merged_hash.map { |k,v| { k => v } }
# => [{:a=>[10, 1]}, {:b=>8}, {:c=>[2, 7]}]
I have an array:
values = [["branding", "color", "blue"],
["cust_info", "customer_code", "some_customer"],
["branding", "text", "custom text"]]
I am having trouble tranforming it to hash as follow:
{
"branding" => {"color"=>"blue", "text"=>"custom text"},
"cust_info" => {"customer_code"=>"some customer"}
}
You can use default hash values to create something more legible than inject:
h = Hash.new {|hsh, key| hsh[key] = {}}
values.each {|a, b, c| h[a][b] = c}
Obviously, you should replace the h and a, b, c variables with your domain terms.
Bonus: If you find yourself needing to go N levels deep, check out autovivification:
fun = Hash.new { |h,k| h[k] = Hash.new(&h.default_proc) }
fun[:a][:b][:c][:d] = :e
# fun == {:a=>{:b=>{:c=>{:d=>:e}}}}
Or an overly-clever one-liner using each_with_object:
silly = values.each_with_object(Hash.new {|hsh, key| hsh[key] = {}}) {|(a, b, c), h| h[a][b] = c}
Here is an example using Enumerable#inject:
values = [["branding", "color", "blue"],
["cust_info", "customer_code", "some_customer"],
["branding", "text", "custom text"]]
# r is the value we are are "injecting" and v represents each
# value in turn from the enumerable; here we create
# a new hash which will be the result hash (res == r)
res = values.inject({}) do |r, v|
group, key, value = v # array decomposition
r[group] ||= {} # make sure group exists
r[group][key] = value # set key/value in group
r # return value for next iteration (same hash)
end
There are several different ways to write this; I think the above is relatively simple. See extracting from 2 dimensional array and creating a hash with array values for using a Hash (i.e. grouper) with "auto vivification".
Less elegant but easier to understand:
hash = {}
values.each do |value|
if hash[value[0]]
hash[value[0]][value[1]] = value[2]
else
hash[value[0]] = {value[1] => value[2]}
end
end
values.inject({}) { |m, (k1, k2, v)| m[k1] = { k2 => v }.merge m[k1] || {}; m }