Merge hashes of arrays based on similar positions with Ruby - arrays

I have the following two hashes with arrays as values.
a = {
"Us" => [["1", ["1", "2"]], ["2", ["1"]]],
"Pa" => [["1", ["1", "3", "5"]], ["4", ["7"]]]
}
b = {
"Us" => [["1", ["F", "O"]], ["2", ["N"]]],
"Pa" => [["1", ["S", "D", "H"]], ["4", ["K"]]]
}
I'm trying to merge the hashes to get a final has like this:
c = {
"Us" => [["1", ["1|F", "2|O"]], ["2", ["1|N"]]],
"Pa" => [["1", ["1|S", "3|D", "5|H"]], ["4", ["7|K"]]]
}
I found the following code with merge, and tried to apply it to my issue, but I got an error:
a.merge(b) {|key, a_val, b_val| a_val.merge b_val }
# >> NoMethodError: undefined method `merge' for [["1", ["1", "2"]], ["2", ["1"]]]:Array
I even got an error with a + b:
a + b
# >> NoMethodError: undefined method `+' for #<Hash:0x0000060078e460>
<<<< UPDATE >>>>
Thanks both Cary and tadman. Outside the original question I show the input file I have and the output I´m tryng to obtain. I show in order for you get an idea why
I generated 2 hashes in that way. In the output I create blocks where fathers are unique values of column 1, below children (unique values in column 2 related with col 1).
Column 3 are subchildren that belong to a value in col2 and column 4 are text contents related with col3.
Probably hash "c" is easier to generate from the beginning.
This is my input file
Main,Stage1,Stage2,Description
Us,1,1,F
Us,1,2,O
Us,2,1,N
Pa,1,1,S
Pa,1,3,D
Pa,1,5,H
Pa,4,7,K
This is the output I almost get.
Main..Stage1..Stage2..Description
Us
......1
..............1.......F
..............2.......O
......2
..............1.......N
Pa
......1
..............1.......S
..............3.......D
..............5.......H
......4
..............7.......K
Then I was able to create this code, but like tadman says, I need to reorder the way I get this to make easier the things, since
I use 4 hashes. After I create hash "a" and "b" I was stuck, since I needed a unique hash to iterate and be able to print in the output structure shown above.
My code before post the question
X = Hash.new{|hsh,key| hsh[key] = [] }
Y = Hash.new{|hsh,key| hsh[key] = [] }
a = Hash.new{|hsh,key| hsh[key] = [] }
b = Hash.new{|hsh,key| hsh[key] = [] }
File.foreach('file.txt').with_index do
|line, line_num|
if line_num > 0
r = line.split(",")
X[r[0] + "°" + r[1]].push r[2]
Y[r[0] + "°" + r[1]].push r[3].strip
end
end
X.each{ |k,v|
lbs = k.split("°")
a[lbs[0]].push [ lbs[1], v] #Here I generate hash a
}
Y.each{ |k,v|
lbs = k.split("°")
b[lbs[0]].push [ lbs[1], v] #Here I generate hash b
}

What you have here is going to require a bit of work to solve because of all the complicated nesting. This would be a lot easier if you did some work to reorder how that data is stored.
Yet you can do this:
a={"Us"=>[["1", ["1", "2"]], ["2", ["1"]]], "Pa"=>[["1", ["1", "3", "5"]], ["4", ["7"]]]}
b={"Us"=>[["1", ["F", "O"]], ["2", ["N"]]], "Pa"=>[["1", ["S", "D", "H"]], ["4", ["K"]]]}
c = a.keys.map do |k|
ah = a[k].to_h
bh = b[k].to_h
[
k,
ah.keys.map do |ka|
[
ka,
ah[ka].zip(bh[ka]).map do |pair|
pair.join('|')
end
]
end
]
end.to_h
# => {"Us"=>[["1", ["1|F", "2|O"]], ["2", ["1|N"]]], "Pa"=>[["1", ["1|S", "3|D", "5|H"]], ["4", ["7|K"]]]}
The key here is rigorous use of map to transform each layer and zip to "zipper" two arrays together into pairs that can then be combined with join into the desired string target. Cast back to a Hash with to_h at the end and you get what you want.
There's an intermediate conversion for each subset to a hash to handle out-of-order situations where one might specify the apparent "keys" in a different sequence.
What you'll want to do is wrap this up in a method with a descriptive name:
def hash_compactor(a,b)
# ... (code) ...
end
That'll help keep it modular. Normally I try and create solutions that handle N arguments by defining it as:
def hash_compactor(*input)
# ...
end
Where input is then an array of various sets in the form you've given. The resulting code is surprisingly a lot more complicated.
Note this makes a lot of assumptions about the input being perfectly matched and will explode if that's not the case.

I suggest you first convert the values of one of the hashes to hashes, as I will explain. Suppose we create a new b.
newbie = b.transform_values(&:to_h)
#=> {"Us"=>{"1"=>["F", "O"], "2"=>["N"]},
# "Pa"=>{"1"=>["S", "D", "H"], "4"=>["K"]}}
We can now use a and newbie to produce the desired return value.
a.each_with_object({}) do |(k,v),h|
h[k] = v.map do |first, arr|
[first, arr.zip(newbie[k][first]).map { |pair| pair.join('|') }]
end
end
#=> {"Us"=>[["1", ["1|F", "2|O"]], ["2", ["1|N"]]],
# "Pa"=>[["1", ["1|S", "3|D", "5|H"]], ["4", ["7|K"]]]}
If a can be mutated it's slightly easier.
a.each do |k,v|
v.map! do |first, arr|
[first, arr.zip(newbie[k][first]).map { |pair| pair.join('|') }]
end
end
The method Hash#trasform_values made its debut in Ruby v2.4. To support older versions, one compute newbie as follows.
newbie = b.each_with_object({}) {|(k,v),h| h[k] = v.to_h }

In this solution we'll keep the original structure.
I've followed your first try but instead of:
a.merge(b) {|key, a_val, b_val| a_val.merge b_val }
Think about use a new custom merge function like:
c = a.merge(b) {|key, a_val, b_val| myMergeArray(a_val, b_val) }
Then the new merge function is a simple recursive one:
def myMergeArray(a,b,sep = '|')
c = a
c.each_with_index { |e, i|
if c[i].is_a? Array
c[i] = myMergeArray(c[i], b[i], sep)
else
c[i] = c[i] + sep + b[i] if c[i] != b[i]
end
}
return c
end
I've assumed that in case of equal elements, just save one, so e.g. "Y" and "Y" yield just "Y" instead of "Y|Y"
Cheers!

Related

Ruby converting Array of Arrays into Array of Hashes

Please I need a help with this.
In Ruby If I have this array of arrays
array = [["a: 1", "b:2"],["a: 3", "b:4"]]
How can I obtain this array of hashes in ruby
aoh = [{:a => "1", :b => "2"},{:a => "3", :b => "4"}]
Note that, like pointed out in the comments, this is most likely an XY-problem and instead of transforming the array the better option is to build the starting array in a better manner.
Nevertheless, you can do this in the following manner:
aoh = array.map { |array| array.to_h { |string| string.split(':').map(&:strip) } }
# => [{"a"=>"1", "b"=>"2"}, {"a"=>"3", "b"=>"4"}]
The above will give you string keys which is the safer option to go with. You can convert them to symbols, but they should only be used for trusted identifiers. When the data comes from an user or external source I would go for the above.
Converting to symbols can be done by adding the following line:
# note that this line will mutate the aoh contents
aoh.each { |hash| hash.transform_keys!(&:to_sym) }
#=> [{:a=>"1", :b=>"2"}, {:a=>"3", :b=>"4"}]
array = [["a: 1", "b:2"], ["a: 3", "b:4"]]
array.map do |a|
Hash[
*a.flat_map { |s| s.split(/: */) }.
map { |s| s.match?(/\A\d+\z/) ? s : s.to_sym }
]
end
#=> [{:a=>"1", :b=>"2"}, {:a=>"3", :b=>"4"}]
The regular expression /: */ reads, "match a colon followed by zero or more (*) spaces". /\A\d+\z/ reads, "match the beginning of the string (\A) followed by one or more (+) digits (\d), followed by the end of the string (\z).
The steps are as follows. The first is for the element arr[0] to be passed to the block, the block variable a assigned its value and the block calculation performed.
a = array[0]
#=> ["a: 1", "b:2"]
b = a.flat_map { |s| s.split(/: */) }
#=> ["a", "1", "b", "2"]
c = b.map { |s| s.match?(/\A\d+\z/) ? s : s.to_sym }
#=> [:a, "1", :b, "2"]
d = Hash[*c]
#=> {:a=>"1", :b=>"2"}
We see the array ["a: 1", "b:2"] is mapped to {:a=>"1", :b=>"2"}. Next the element arr[1] is passed to the block, the block variable a is assigned its value and the block calculation is performed.
a = array[1]
#=> ["a: 3", "b:4"]
b = a.flat_map { |s| s.split(/: */) }
#=> ["a", "3", "b", "4"]
c = b.map { |s| s.match?(/\d/) ? s : s.to_sym }
#=> [:a, "3", :b, "4"]
d = Hash[*c]
#=> {:a=>"3", :b=>"4"}
The splat operator (*) causes Hash[*c] to be evaluated as:
Hash[:a, "3", :b, "4"]
See Hash::[].
Loop through your items, loop through its items, create a new array:
array.map do |items|
items.map do |item|
k,v = item.split(":", 2)
{ k.to_sym => v }
}
}
Note that we're using map instead of each which will return an array.

Working with Transpose functions result in error

consider the following array
arr = [["Locator", "Test1", "string1","string2","string3","string4"],
["$LogicalName", "Create Individual Contact","value1","value2"]]
Desired result:
[Test1=>{"string1"=>"value1","string2"=>"value2","string3"=>"","string4"=>""}]
When I do transpose, it gives me the error by saying second element of the array is not the length of the first element in the array,
Uncaught exception: element size differs (2 should be 4)
so is there any to add empty string in the place where there is no element and can perform the transpose and then create the hash as I have given above? The array may consist of many elements with different length but according to the size of the first element in the array, every other inner array has to change by inserting empty string and then I can do the transpose. Is there any way?
It sounds like you might want Enumerable#zip:
headers, *data_rows = input_data
headers.zip(*data_rows)
# => [["Locator", "$LogicalName"], ["Test1", "Create Individual Contact"],
# ["string1", "value1"], ["string2", "value2"], ["string3", nil], ["string4", nil]]
If you wish to transpose an array of arrays, each element of the array must be the same size. Here you would need to do something like the following.
arr = [["Locator", "Test1", "string1","string2","string3","string4"],
["$LogicalName", "Create Individual Contact","value1","value2"]]
keys, vals = arr
#=> [["Locator", "Test1", "string1", "string2", "string3", "string4"],
# ["$LogicalName", "Create Individual Contact", "value1", "value2"]]
idx = keys.index("Test1") + 1
#=> 2
{ "Test1" => [keys[idx..-1],
vals[idx..-1].
concat(['']*(keys.size - vals.size))].
transpose.
to_h }
#=> {"Test1"=>{"string1"=>"value1", "string2"=>"value2", "string3"=>"", "string4"=>""}}
It is not strictly necessary to define the variables keys and vals, but that avoids the need to create those arrays multiple times. It reads better as well, in my opinion.
The steps are as follows. Note keys.size #=> 6 and vals.size #=> 4.
a = vals[idx..-1]
#=> vals[2..-1]
#=> ["value1", "value2"]
b = [""]*(keys.size - vals.size)
#=> [""]*(4 - 2)
#=> ["", ""]
c = a.concat(b)
#=> ["value1", "value2", "", ""]
d = keys[idx..-1]
#=> ["string1", "string2", "string3", "string4"]
e = [d, c].transpose
#=> [["string1", "value1"], ["string2", "value2"], ["string3", ""], ["string4", ""]]
f = e.to_h
#=> {"string1"=>"value1", "string2"=>"value2", "string3"=>"", "string4"=>""}
f = e.to_h
#=> { "Test1" => f }
Find the longest Element in your Array and make sure every other element has the same length - loop and add maxLength - element(i).length amount of "" elements.

How can I generate a percentage for a regex string match in Ruby?

I'm trying to build a simple method to look at about 100 entries in a database for a last name and pull out all the ones that match above a specific percentage of letters. My current approach is:
Pull all 100 entries from the database into an array
Iterate through them while performing the following action
Split the last name into an array of letters
Subtract that array from another array that contains the letters for the name I am trying to match which leaves only the letters that weren't matched.
Take the size of the result and divide by the original size of the array from step 3 to get a percentage.
If the percentage is above a predefined threshold, push that database object into a results array.
This works, but I feel like there must be some cool ruby/regex/active record method of doing this more efficiently. I have googled quite a bit but can't find anything.
To comment on the merit of the measure you suggested would require speculation, which is out-of-bounds at SO. I therefore will merely demonstrate how you might implement your proposed approach.
Code
First define a helper method:
class Array
def difference(other)
h = other.each_with_object(Hash.new(0)) { |e,h| h[e] += 1 }
reject { |e| h[e] > 0 && h[e] -= 1 }
end
end
In short, if
a = [3,1,2,3,4,3,2,2,4]
b = [2,3,4,4,3,4]
then
a - b #=> [1]
whereas
a.difference(b) #=> [1, 3, 2, 2]
This method is elaborated in my answer to this SO question. I've found so many uses for it that I've proposed it be added to the Ruby Core.
The following method produces a hash whose keys are the elements of names (strings) and whose values are the fractions of the letters in the target string that are contained in each string in names.
def target_fractions(names, target)
target_arr = target.downcase.scan(/[a-z]/)
target_size = target_arr.size
names.each_with_object({}) do |s,h|
s_arr = s.downcase.scan(/[a-z]/)
target_remaining = target_arr.difference(s_arr)
h[s] = (target_size-target_remaining.size)/target_size.to_f
end
end
Example
target = "Jimmy S. Bond"
and the names you are comparing are given by
names = ["Jill Dandy", "Boomer Asad", "Josefine Simbad"]
then
target_fractions(names, target)
#=> {"Jill Dandy"=>0.5, "Boomer Asad"=>0.5, "Josefine Simbad"=>0.8}
Explanation
For the above values of names and target,
target_arr = target.downcase.scan(/[a-z]/)
#=> ["j", "i", "m", "m", "y", "s", "b", "o", "n", "d"]
target_size = target_arr.size
#=> 10
Now consider
s = "Jill Dandy"
h = {}
then
s_arr = s.downcase.scan(/[a-z]/)
#=> ["j", "i", "l", "l", "d", "a", "n", "d", "y"]
target_remaining = target_arr.difference(s_arr)
#=> ["m", "m", "s", "b", "o"]
h[s] = (target_size-target_remaining.size)/target_size.to_f
#=> (10-5)/10.0 => 0.5
h #=> {"Jill Dandy"=>0.5}
The calculations are similar for Boomer and Josefine.

Most efficient way to count duplicated elements between two arrays

As part of a very basic program I am writing in Ruby, I am trying to find the total number of shared elements between two arrays of equal length, but
I need to include repeats.
My current example code for this situation is as follows:
array_a = ["B","A","A","A","B"]
array_b = ["A","B","A","B","B"]
counter = 0
array_a.each_index do |i|
array_a.sort[i] == array_b.sort[i]
counter += 1
end
end
puts counter
I want the return value of this comparison in this instance to be 4, and not 2, as the two arrays share 2 duplicate characters ("A" twice, and "B" twice). This seems to work, but I am wondering if there are any more efficient solutions for this issue. Specifically whether there are any methods you would suggest looking into. I spoke with someone who suggested a different method, inject, but I really don't understand how that applies and would like to understand. I did quite a bit of reading on uses for it, and it still isn't clear to me how it is appropriate. Thank you.
Looking at my code, I have realized that it doesn't seem to work for the situation that I am describing.
Allow me to reiterate and explain what I think the OP's original intent was:
Given arrays of equal size
array_a = ["B","A","A","A","B"]
array_b = ["A","B","A","B","B"]
We need to show the total number of matching pairs of elements between the two arrays. In other words, each B in array_a will "use up" a B in array_b, and the same will be true for each A. As there are two B's in array_a and three in array_b, this leaves us with a count of 2 for B, and following the same logic, 2 for A, for a sum of 4.
(array_a & array_b).map { |e| [array_a.count(e), array_b.count(e)].min }.reduce(:+)
If we get the intersection of the arrays with &, the result is a list of values that exist in both arrays. We then iterate over each match, and select the minimum number of times the element exists in either array --- this is the most number of times the element that can be "used". All that is left is to total the number of paired elements, with reduce(:+)
Changing array_a to ["B", "A", "A", "B", "B"] results in a total of 5, as there are now enough of B to exhaust the supply of B in array_b.
If I understand the question correctly, you could do the following.
Code
def count_shared(arr1, arr2)
arr1.group_by(&:itself).
merge(arr2.group_by(&:itself)) { |_,ov,nv| [ov.size, nv.size].min }.
values.
reduce(0) { |t,o| (o.is_a? Array) ? t : t + o }
end
Examples
arr1 = ["B","A","A","A","B"]
arr2 = ["A","B","A","B","B"]
count_shared(arr1, arr2)
#=> 4 (2 A's + 2 B's)
arr1 = ["B", "A", "C", "C", "A", "A", "B", "D", "E", "A"]
arr2 = ["C", "D", "F", "F", "A", "B", "A", "B", "B", "G"]
count_shared(arr1, arr2)
#=> 6 (2 A's + 2 B's + 1 C + 1 D + 0 E's + 0 F's + 0 G's)
Explanation
The steps are as follows for a slightly modified version of the first example.
arr1 = ["B","A","A","A","B","C","C"]
arr2 = ["A","B","A","B","B","D"]
First apply Enumerable#group_by to both arr1 and arr2:
h0 = arr1.group_by(&:itself)
#=> {"B"=>["B", "B"], "A"=>["A", "A", "A"], "C"=>["C", "C"]}
h1 = arr2.group_by(&:itself)
#=> {"A"=>["A", "A"], "B"=>["B", "B", "B"], "D"=>["D"]}
Prior to Ruby v.2.2, when Object#itself was introduced, you would have to write:
arr.group_by { |e| e }
Continuing,
h2 = h0.merge(h1) { |_,ov,nv| [ov.size, nv.size].min }
#=> {"B"=>2, "A"=>2, "C"=>["C", "C"], "D"=>["D"]}
I will return shortly to explain the above calculation.
a = h2.values
#=> [2, 2, ["C", "C"], ["D"]]
a.reduce(0) { |t,o| (o.is_a? Array) ? t : t + o }
#=> 4
Here Enumerable#reduce (aka inject) merely sums the values of a that are not arrays. The arrays correspond to elements of arr1 that do not appear in arr2 or vise-versa.
As promised, I will now explain how h2 is computed. I've used the form of Hash#merge that employs a block (here { |k,ov,nv| [ov.size, nv.size].min }) to compute the values of keys that are present in both hashes being merged. For example, when the first key-value pair of h1 ("A"=>["A", "A"]) is being merged into h0, since h0 also has a key "A", the array
["A", ["A", "A", "A"], ["A", "A"]]
is passed to the block and the three block variables are assigned values (using "parallel assignment", which is sometimes called "multiple assignment"):
k, ov, nv = ["A", ["A", "A", "A"], ["A", "A"]]
so we have
k #=> "A"
ov #=> ["A", "A", "A"]
nv #=> ["A", "A"]
k is the key, ov ("old value") is the value of "A" in h0 and nv ("new value") is the value of "A" in h1. The block calculation is
[ov.size, nv.size].min
#=> [3,2].min = 2
so the value of "A" is now 2.
Notice that the key, k, is not used in the block calculation (which is very common when using this form of merge). For that reason I've changed the block variable from k to _ (a legitimate local variable), both to reduce the chance of introducing a bug and to signal to the reader that the key is not used in the block. The other elements of h2 that use this block are computed similarly.
Another way
It would be quite simple if we had available an Array method I've proposed be added to the Ruby core:
array_a = ["B","A","A","A","B"]
array_b = ["A","B","A","B","B"]
array_a.size - (array_a.difference(array_b)).size
#=> 4
or
array_a.size - (array_b.difference(array_a)).size
#=> 4
I've cited other applications in my answer here.
This is a perfect job for Enumerable#zip and Enumerable#count:
array_a.zip(array_b).count do |a, b|
a == b
end
# => 2
The zip method pairs up elements, "zippering" them together, and the count method can take a block as to if the element should be counted.
The inject method is very powerful, but it's also the most low-level. Pretty much every other Enumerable method can be created with inject if you work at it, so it's quite flexible, but usually a more special-purpose method is better suited. It's still a useful tool if applied correctly.
In this case zip and count do a much better job and if you know what these methods do, this code is self explanatory.
Update:
If you need to count all overlapping letters regardless of order you need to do some grouping on them. Ruby on Rails provides the handy group_by method in ActiveSupport, but in pure Ruby you need to make your own.
Here's an approach that counts up all the unique letters, grouping them using chunk:
# Convert each array into a map like { "A" => 2, "B" => 3 }
# with a default count of 0.
counts = [ array_a, array_b ].collect do |a|
Hash.new(0).merge(
Hash[a.sort.chunk { |v| v }.collect { |k, a| [ k, a.length ] }]
)
end
# Iterate over one of the maps key by key and count the minimum
# overlap between the two.
counts[0].keys.inject(0) do |sum, key|
sum + [ counts[0][key], counts[1][key] ].min
end

Rejecting hash contents if they are not in array

I have this array:
array = ["1", "2", "3", "4"]
I have this array of hashes:
ah = [
{:id=>"1", :value=>"A"},
{:id=>"2", :value=>"B"},
{:id=>"3", :value=>"C"},
{:id=>"4", :value=>"D"},
{:id=>"5", :value=>"E"},
{:id=>"6", :value=>"F"},
{:id=>"7", :value=>"G"},
{:id=>"8", :value=>"H"},
]
I need to reject any hash in ah whose id is not in array.
What is the best way of achieving this?
You can select the inverse - the hashes whose id is in array by using this code:
ah.select{|el| array.include?(el[:id])}
If you prefer reject, you can use:
ah.reject{|el| !array.include?(el[:id])}
For more info: Array#reject, Array#select.
These methods create a new array, if you want to modify in place use Array#reject! or Array#select!.
For big pieces of data I would go with some preprocessing to avoid O(n*m) lookups.
array = ["1", "2", "3", "4"]
array_hash = array.each_with_object({}){ |i, h| h[i] = true }
ah.select{ |obj| array_hash[obj[:id]] }
I realize there is already an accepted answer but since all the answers here are in O(n*m), I thought I'd propose an alternative in O(n)*.
Here's a rough benchmark if the ah array has 100_000 items and we have 10_000 items in the sub array. I'm including fl00r's answer here and Cary's as we're all trying to avoid the O(n*m) scenario.
user system total real
select with include 34.610000 0.110000 34.720000 ( 34.924679)
reject with include 34.320000 0.100000 34.420000 ( 34.611992)
group and select 0.170000 0.010000 0.180000 ( 0.182358)
select by value 0.040000 0.000000 0.040000 ( 0.041073)
select with set 0.040000 0.000000 0.040000 ( 0.048331)
hashify then values 0.130000 0.010000 0.140000 ( 0.139686)
The code to reproduce this:
require 'benchmark'
require 'set'
list_size = 100_000
sub_list_size = 10_000
ah = Array.new(list_size) { |i| { id: i, value: "A" } }
array = []
sub_list_size.times { array << (0..list_size).to_a.sample }
def group_than_select(ah, array)
grouped = ah.group_by { |x| x[:id] }
good_keys = grouped.keys - array
good_keys.map { |i| grouped[i] }.flatten
end
def select_by_fl00r(ah, array)
array_hash = array.each_with_object({}){ |i, h| h[i] = true }
ah.select{ |obj| array_hash[obj[:id]] }
end
def select_with_set(ah, array)
array_to_set = array.to_set
ah.select { |h| array_to_set.include?(h[:id]) }
end
def hashify_then_values_at(ah, array)
h = ah.each_with_object({}) { |g,h| h.update(g[:id]=>g) }
h.values_at(*(h.keys & array))
end
Benchmark.bm(25) do |x|
x.report("select with include") do
ah.select{|el| array.include?(el[:id])}
end
x.report("reject with include") do
ah.reject{|e| !array.include?(e[:id])}
end
x.report("group and select") do
group_than_select(ah, array)
end
x.report("select by value") do
select_by_fl00r(ah, array)
end
x.report("select with set") do
select_with_set(ah, array)
end
x.report("hashify then values") do
hashify_then_values_at(ah, array)
end
end
Hash maps are typically O(1) search though O(n) worst case is possible.
A better solution than rejecting those ids that are not in the array is to only accept the ones that do:
ah.select { |hash| array.include?(hash[:id]) }
Here are two more possibilities.
array = ["1", "2", "3", "4", "99999999"]
#1
I expect the include? solutions would be considerably faster if array were first converted to a set:
require 'set'
def select_with_set(ah, array)
array_to_set = array.to_set
ah.select { |h| array_to_set.include?(h[:id]) }
end
select_with_set(ah, array)
#=> [{:id=>"1", :value=>"A"}, {:id=>"2", :value=>"B"},
# {:id=>"3", :value=>"C"}, {:id=>"4", :value=>"D"}]
#2
If, as in the example, the hash elements of ah have distinct values for :id, one could do this:
def hashify_then_values_at(ah, array)
h = ah.each_with_object({}) { |g,h| h.update(g[:id]=>g) }
h.values_at(*(h.keys & array))
end
hashify_then_values_at(ah, array)
#=> [{:id=>"1", :value=>"A"}, {:id=>"2", :value=>"B"},
# {:id=>"3", :value=>"C"}, {:id=>"4", :value=>"D"}]

Resources