Merge two collections in Ruby to simplify a code - ruby

Here is a code in Ruby. I'm just trying to simplify it. Do you have any ideas?
def foo
points = some_collection.map { |item| {:key1 => item.key1, :key2 => item.key2, :key3 => item.key3, :key4=> item.key4} }
some_collection2.each do |item2|
points << {:key1 => item2.key1, :key2 => item2.key2, :key3 => item2.key3, :key4=> item2.key4}
end
points
end

If I understand correctly the mapping function is the same for both collections in your case.
points = (some_collection + some_collection2).map do |item|
{:key1 => item.method1, :key2 => item.method2, :key3 => item.key4, :key5=> item.key5}
end

I would store transformed results of the two collections into two arrays and then just added them together.
def foo
a1 = some_collection.map do |item|
{:key1 => item.method1, :key2 => item.method2, :key3 => item.key4, :key5=> item.key5}
end
a2 = some_collection2.map do |item|
{:key1 => item.method1, :key2 => item.method2, :key3 => item.key4, :key5=> item.key5}
end
a1 + a2
end
Or, if you want, it can even become this
def foo
some_collection.map do |item|
{:key1 => item.method1, :key2 => item.method2, :key3 => item.key4, :key5=> item.key5}
end + some_collection2.map do |item2|
{:key1 => item.method1, :key2 => item.method2, :key3 => item.key4, :key5=> item.key5}
end
end

You can even extract a list of keys in a separate array, so you would have:
keys = [:key1, :key2, :key3, :key4]
points = (some_collection + some_collection2).map do |item|
Hash[keys.map{|key| [key, item.send(key)]}]
end

Related

Merge hashes based on particular key/value pair in ruby

I am trying to merge an array of hashes based on a particular key/value pair.
array = [ {:id => '1', :value => '2'}, {:id => '1', :value => '5'} ]
I would want the output to be
{:id => '1', :value => '7'}
As patru stated, in sql terms this would be equivalent to:
SELECT SUM(value) FROM Hashes GROUP BY id
In other words, I have an array of hashes that contains records. I would like to obtain the sum of a particular field, but the sum would grouped by key/value pairs. In other words, if my selection criteria is :id as in the example above, then it would seperate the hashes into groups where the id was the same and the sum the other keys.
I apologize for any confusion due to the typo earlier.
Edit: The question has been clarified since I first posted my answer. As a result, I have revised my answer substantially.
Here are two "standard" ways of addressing this problem. Both use Enumerable#select to first extract the elements from the array (hashes) that contain the given key/value pair.
#1
The first method uses Hash#merge! to sequentially merge each array element (hashes) into a hash that is initially empty.
Code
def doit(arr, target_key, target_value)
qualified = arr.select {|h|h.key?(target_key) && h[target_key]==target_value}
return nil if qualified.empty?
qualified.each_with_object({}) {|h,g|
g.merge!(h) {|k,gv,hv| k == target_key ? gv : (gv.to_i + hv.to_i).to_s}}
end
Example
arr = [{:id => '1', :value => '2'}, {:id => '2', :value => '3'},
{:id => '1', :chips => '4'}, {:zd => '1', :value => '8'},
{:cat => '2', :value => '3'}, {:id => '1', :value => '5'}]
doit(arr, :id, '1')
#=> {:id=>"1", :value=>"7", :chips=>"4"}
Explanation
The key here is to use the version of Hash#merge! that uses a block to determine the value for each key/value pair whose key appears in both of the hashes being merged. The two values for that key are represented above by the block variables hv and gv. We simply want to add them together. Note that g is the (initially empty) hash object created by each_with_object, and returned by doit.
target_key = :id
target_value = '1'
qualified = arr.select {|h|h.key?(target_key) && h[target_key]==target_value}
#=> [{:id=>"1", :value=>"2"},{:id=>"1", :chips=>"4"},{:id=>"1", :value=>"5"}]
qualified.empty?
#=> false
qualified.each_with_object({}) {|h,g|
g.merge!(h) {|k,gv,hv| k == target_key ? gv : (gv.to_i + hv.to_i).to_s}}
#=> {:id=>"1", :value=>"7", :chips=>"4"}
#2
The other common way to do this kind of calculation is to use Enumerable#flat_map, followed by Enumerable#group_by.
Code
def doit(arr, target_key, target_value)
qualified = arr.select {|h|h.key?(target_key) && h[target_key]==target_value}
return nil if qualified.empty?
qualified.flat_map(&:to_a)
.group_by(&:first)
.values.map { |a| a.first.first == target_key ? a.first :
[a.first.first, a.reduce(0) {|tot,s| tot + s.last}]}.to_h
end
Explanation
This may look complex, but it's not so bad if you break it down into steps. Here's what's happening. (The calculation of qualified is the same as in #1.)
target_key = :id
target_value = '1'
c = qualified.flat_map(&:to_a)
#=> [[:id,"1"],[:value,"2"],[:id,"1"],[:chips,"4"],[:id,"1"],[:value,"5"]]
d = c.group_by(&:first)
#=> {:id=>[[:id, "1"], [:id, "1"], [:id, "1"]],
# :value=>[[:value, "2"], [:value, "5"]],
# :chips=>[[:chips, "4"]]}
e = d.values
#=> [[[:id, "1"], [:id, "1"], [:id, "1"]],
# [[:value, "2"], [:value, "5"]],
# [[:chips, "4"]]]
f = e.map { |a| a.first.first == target_key ? a.first :
[a.first.first, a.reduce(0) {|tot,s| tot + s.last}] }
#=> [[:id, "1"], [:value, "7"], [:chips, "4"]]
f.to_h => {:id=>"1", :value=>"7", :chips=>"4"}
#=> {:id=>"1", :value=>"7", :chips=>"4"}
Comment
You may wish to consider makin the values in the hashes integers and exclude the target_key/target_value pairs from qualified:
arr = [{:id => 1, :value => 2}, {:id => 2, :value => 3},
{:id => 1, :chips => 4}, {:zd => 1, :value => 8},
{:cat => 2, :value => 3}, {:id => 1, :value => 5}]
target_key = :id
target_value = 1
qualified = arr.select { |h| h.key?(target_key) && h[target_key]==target_value}
.each { |h| h.delete(target_key) }
#=> [{:value=>2}, {:chips=>4}, {:value=>5}]
return nil if qualified.empty?
Then either
qualified.each_with_object({}) {|h,g| g.merge!(h) { |k,gv,hv| gv + hv } }
#=> {:value=>7, :chips=>4}
or
qualified.flat_map(&:to_a)
.group_by(&:first)
.values
.map { |a| [a.first.first, a.reduce(0) {|tot,s| tot + s.last}] }.to_h
#=> {:value=>7, :chips=>4}

select and delete from hash in ruby

In a method I get a list of options passed in. Some are related to a particular scope.
I want to store those special keys in another hash to be able to pass it to a different method, and delete them from the original hash.
(I'm actually writing a rails simple_form custom input, but that doesn't matter)
I have the following code:
all_options = { :key1 => 1, :key2 => 2, :something_else => 42 }
my_keys = [:key1, :key2, :key3, :key4]
my_options = all_options.select {|k,v| my_keys.include?(k)}
all_options.delete_if {|k,v| my_keys.include?(k)}
# expecting
my_options == { :key1 => 1, :key2 => 2 }
all_options == { :something_else => 42 }
Now my question is there a better, i.e. smarter way of doing it?
Maybe it's just sugar, but I want to know.
all_options = { :key1 => 1, :key2 => 2, :something_else => 42 }
my_keys = [:key1, :key2, :key3, :key4]
my_options = my_keys.inject({}) {|h,k| h[k] = all_options.delete(k) if all_options.key?(k);h}
all_options
# => {:something_else=>42}
my_options
# => {:key1=>1, :key2=>2}
here's a way to improve Ju Liu's answer:
all_options = { :key1 => 1, :key2 => 2, :something_else => 42 }
my_keys = [:key1, :key2, :key3, :key4]
my_options = all_options.extract!(*my_keys).keep_if {|k,v| v}
all_options
# => {:something_else=>42}
my_options
# => {:key1=>1, :key2=>2}
however you'll lose your options if any key in a all_options hash has an actual value of nil or false (don't know if you need to keep them):
all_options = { :key1 => 1, :key2 => nil, :something_else => 42 }
here's a way to keep false's
my_options = all_options.extract!(*my_keys).keep_if {|k,v| !v.nil?}
p.s. it would be possible to keep all values including nils if you store the keys from all_options:
all_options = { :key1 => 1, :key2 => 2, :something_else => 42 }
all_keys = all_options.keys
my_keys = [:key1, :key2, :key3, :key4]
my_options = all_options.extract!(*my_keys).keep_if {|k,v| all_keys.include?(k)}
all_options
# => {:something_else=>42}
my_options
# => {:key1=>1, :key2=>2}
Maybe the extract! method in active_support could work?
I know only Ruby. So here my Ruby approach :
all_options = { :key1 => 1, :key2 => 2, :something_else => 42 }
my_keys = [:key1, :key2, :key3, :key4]
#below statement is your my_options
Hash[my_keys.map{|i| [i,all_options.delete(i)] if all_options.has_key? i }.compact]
# => {:key1=>1, :key2=>2}
all_options
# => {:something_else=>42}
all_options = { key1: 1, key2: 2, something_else: 42 }
my_keys = [:key1, :key2, :key3, :key4]
my_options = my_keys.each_with_object({}) do |key, hash|
hash[key] = all_options.delete(key) if all_options.key?(key)
end

Convert Nested Array into Nested Hash in Ruby

Without knowing the dimension of array, how do I convert an array to a nested hash?
For example:
[["Message", "hello"]]
to:
{{:message => "Hello"}}
Or:
[["Memory", [["Internal Memory", "32 GB"], ["Card Type", "MicroSD"]]]]
to:
{{:memory => {:internal_memroy => "32 GB", :card_type => "MicroSD"}}}
or:
[["Memory", [["Internal Memory", "32 GB"], ["Card Type", "MicroSD"]]], ["Size", [["Width", "12cm"], ["height", "20cm"]]]]
to:
{ {:memory => {:internal_memroy => "32 GB", :card_type => "MicroSD"}, {:size => {:width => "12cm", :height => "20cm" } } }
Considering your format of nested arrays of pairs, that following function transforms it into the hash you'd like
def nested_arrays_of_pairs_to_hash(array)
result = {}
array.each do |elem|
second = if elem.last.is_a?(Array)
nested_arrays_to_hash(elem.last)
else
elem.last
end
result.merge!({elem.first.to_sym => second})
end
result
end
A shorter version
def nested_arrays_to_hash(array)
return array unless array.is_a? Array
array.inject({}) do |result, (key, value)|
result.merge!(key.to_sym => nested_arrays_to_hash(value))
end
end
> [:Message => "hello"]
=> [{:Message=>"hello"}]
Thus:
> [:Message => "hello"][0]
=> {:Message=>"hello"}

Construct a hash with other hashes

I want to join 2 or more hashes like this.
h1 = { :es => { :hello => "You" } }
h2 = { :es => { :bye => "Man" } }
How can I get this?
h1 + h2 = { :es => { :hello => "you", :bye => "Man" } }
Thanks.
irb(main):001:0> h1 = {:es => {:hello => "You"}}
=> {:es=>{:hello=>"You"}}
irb(main):002:0> h2 = {:es => {:bye => "Man"}}
=> {:es=>{:bye=>"Man"}}
irb(main):003:0> h1.each_key {|x| h1[x].merge! h2[x]}
=> {:es=>{:bye=>"Man", :hello=>"You"}}
What you want is the deep_merge method. Does exactly what you want.
ruby-1.9.2-p136 :001 > {:es => {:hello => "You" } }.deep_merge({:es => {:bye => "Man"}})
=> {:es=>{:hello=>"You", :bye=>"Man"}}
http://apidock.com/rails/ActiveSupport/CoreExtensions/Hash/DeepMerge/deep_merge
Similar to activesupport's deep_merge, but with a functional approach. Works recursively:
class Hash
def inner_merge(other_hash)
other_hash.inject(self) do |acc, (key, value)|
if (acc_value = acc[key]) && acc_value.is_a?(Hash) && value.is_a?(Hash)
acc.merge(key => acc_value.inner_merge(value))
else
acc.merge(key => value)
end
end
end
end
h1.inner_merge(h2) #=> {:es=>{:hello=>"You", :bye=>"Man"}}
If you don't use ActiveSupport, this Proc will perform a deep merge. 1.8.7 & 1.9.2 compatible.
dm = lambda {|l,r| l.merge(r) {|k,ov,nv| l[k] = ov.is_a?(Hash) ? dm[ov, nv || {}] : nv} }
dm[h1,h2]
# => {:es=>{:hello=>"You", :bye=>"Man"}}

How to change format of nested hashes

I'm looking for a solution how to write the format function which will take a string or nested hash as an argument and return the flatten version of it with the path as a key.
arg = "foo"
format(arg) # => { "hash[keys]" => "foo" }
arg = {:a => "foo", :b => { :c => "bar", :d => "baz" }}
format(arg) # => { "hash[keys][a]" => "foo", "hash[keys][b][c]" => "bar", "hash[keys][b][d]" => "baz" }
def hash_flatten h
h.inject({}) do |a,(k,v)|
if v.is_a?(Hash)
hash_flatten(v).each do |sk, sv|
a[[k]+sk] = sv
end
else
k = k ? [k] : []
a[k] = v
end
a
end
end
def format h
if h.is_a?(Hash)
a = hash_flatten(h).map do |k,v|
key = k.map{|e| "[#{e}]"}.join
"\"event[actor]#{key}\" => \"#{v}\""
end.join(', ')
else
format({nil => h})
end
end
arg = "sth"
puts format(arg)
# => "event[actor]" => "sth"
arg = {:a => "sth", :b => { :c => "sth else", :d => "trololo" }}
puts format(arg)
# => "event[actor][a]" => "sth", "event[actor][b][c]" => "sth else", "event[actor][b][d]" => "trololo"

Resources