Ruby join two arrays by key value - arrays

I have two arrays, the first array contains field name, type and id.
arr1 = [
{
"n" => "cat",
"t" => 0,
"id" => "42WTd5"
},
{
"n" => "dog",
"t" => 0,
"id" => "1tM5T0"
}
]
Second array contains field, id and value.
arr2 = [
{
"42WTd5"=>"meow",
"1tM5T0"=>"woof"
}
]
How can I join them by id to produce the following result.
cat: meow
dog: woof
Any help is appreciated.

I think you want your result to be a Hash, in which case this would do the job:
def match_animals_to_sounds(animal_array, sound_array)
sounds = sound_array.first
animal_array.map { |animal| [animal['n'], sounds[animal['id']]] }.to_h
end
>> match_animals_to_sounds(arr1, arr2)
=> {"cat"=>"meow", "dog"=>"woof"}
Your arr2 is unusual in that it is an Array of a single element. I'm just calling #first on it to pull out the Hash inside. If you expect some version of this Array to have more than one element in the future, you'll need to rethink the first line of this method.
The second line is standard Ruby Array manipulation. The first part maps each animal to a new Array of two-element Arrays containing each animal's name and sound. At the end, #to_h converts this array of two-element arrays to a single Hash, which is much more useful than an array of strings. I don't know what you intended in your question, but this is probably what you want.
If you prefer to work with Symbols, you can change the second line of the method to:
animal_array.map { |animal| [animal['n'].to_sym, sounds[animal['id']].to_sym] }.to_h
In which case you will get:
>> match_animals_to_sounds(arr1, arr2)
=> {:cat=>:meow, :dog=>:woof}

This is a way to do it.
sounds = arr2[0]
results = arr1.map do |animal|
"#{animal["n"]}: #{sounds[animal["id"]]}"
end
puts results
# => cat: meow
# => dog: woof
Seems like the second array should just be a hash instead. There's no point creating an array if there's only one element in it and that number won't change.
pointless one-liner (don't use this)
puts arr1.map { |x| "#{x["n"]}: #{arr2[0][x["id"]]}" }

You can also get the join result by following code
arr1.collect{ |a| {a["n"] => arr2[0][a["id"]]} }

Related

How to iterate over an array of hashes in Ruby to compare each member with the following members in the array?

So, I have an array of hashes, let's use this as an example
arr = [
{:series=>"GOT", :rating=>"Good", :type=>"Fantasy"},
{:series=>"BB", :rating=>"Great", :type=>"Crime"},
{:series=>"E", :rating=>"Poor", :type=>"Drama"}
]
I'm trying to loop over this array so that I can compare each member with all following members.
E.g. Hash 1 compares with Hash 2 and Hash 3, Hash 2 compares with Hash 3
The actual comparison function I already have written:
output = (data[X].keys & data[Y].keys).select { |k| data[X][k] == data[Y][k] }
X would be the current array and Y is the next element we are comparing to.
EDIT
Here's what I've got so far
for i in 0..data.length
for j in i..data.length
# puts data[j + 1]
output = (data[j].keys & data[j+1].keys).select { |k| data[j][k] == data[j+1][k] }
puts data[j]
puts data[j+1]
puts output
end
puts "*****"
end
My desired output is to print the hash and the other hash we are comparing with, as well as what keys they share a value for.
For example this array:
{:series=>"GOT", :rating=>"Great", :type=>"Fantasy"}
{:series=>"BB", :rating=>"Great", :type=>"Crime"}
Should print this:
{:series=>"GOT", :rating=>"Great", :type=>"Fantasy"}
{:series=>"BB", :rating=>"Great", :type=>"Crime"}
rating
If the key is nil, it should not compare. I think that's why I'm also getting this error when running the above code:
Traceback (most recent call last):
4: from match_users.rb:18:in `<main>'
3: from match_users.rb:18:in `each'
2: from match_users.rb:19:in `block in <main>'
1: from match_users.rb:19:in `each'
match_users.rb:21:in `block (2 levels) in <main>': undefined method `keys' for nil:NilClass (NoMethodError)
Note that you're not using "i" inside your loop, which looks like a bug. Also, your index "j+1" is going off the end of the array, resulting in accessing the nil element. Actually, even "j" goes off the end of the array. Arrays are accessed from 0...length-1, whereas "0..data.length" will access the element at index data.length. I'm guessing you mean something more like:
for i in 0..data.length-2
for j in i+1..data.length-1
output = (data[i].keys & data[j].keys).select { |k| data[i][k] == data[j][k] }
end
end
Create an Iterable Collection
First of all, the data you posted isn't a valid Ruby array of hashes; it's just a sequential list of Hash objects, so you can't iterate over it. You need to wrap your Hash objects into something iterable first, such as an Array. Here's an example:
titles =
[{:series=>"GOT", :rating=>"Good", :type=>"Fantasy"},
{:series=>"BB", :rating=>"Great", :type=>"Crime"},
{:series=>"E", :rating=>"Poor", :type=>"Drama"}]
Relative Comparisons of Consecutive Elements
Now that you have an iterable collection, you can use Enumerable#each_cons (which is already mixed into Array in Ruby's core) in order to iterate over each sequential pair of Hash objects. For demonstration purposes, I've chosen to store the relative comparisons as part of an Array within each title. For example, using the Array of Hash objects stored in titles as above:
STAR_MAPPINGS = {'great' => 5, 'good' => 4, 'fair' => 2,
'poor' => 1, 'unwatchable' => 0}.freeze
COMPARISON_MAP = {
-1 => 'is worse than',
0 => 'the same as',
1 => 'is better than'
}.freeze
def compare_ratings_for title_1, title_2
fmt = '_%s_ %s _%s_'
series_1, series_2 = title_1[:series], title_2[:series]
ratings_1, ratings_2 =
STAR_MAPPINGS[title_1[:rating].downcase],
STAR_MAPPINGS[title_2[:rating].downcase]
comparison_str = COMPARISON_MAP[ratings_1 <=> ratings_2]
format fmt, series_1, comparison_str, series_2
end
titles.each_cons(2).each do |h1, h2|
# Array#| return an ordered, deduplicated union of keys
matching_keys = (h1.keys | h2.keys).flatten.uniq
next if matching_keys.none?
# perform whatever comparisons you want here; this example
# compares ratings by assigning stars to each rating
h1[:comparisons] =
h1.fetch(:comparisons, []) << compare_ratings_for(h1, h2)
h2[:comparisons] =
h2.fetch(:comparisons, []) << compare_ratings_for(h2, h1)
end
titles
The titles variable now holds and returns the following data:
[{:series=>"GOT", :rating=>"Good", :type=>"Fantasy", :comparisons=>["_GOT_ is worse than _BB_"]},
{:series=>"BB", :rating=>"Great", :type=>"Crime", :comparisons=>["_BB_ is better than _GOT_", "_BB_ is better than _E_"]},
{:series=>"E", :rating=>"Poor", :type=>"Drama", :comparisons=>["_E_ is worse than _BB_"]}]
Here's the same data again, but this time titles was pretty-printed with amazing_print for improved readability:
[
{
:series => "GOT",
:rating => "Good",
:type => "Fantasy",
:comparisons => [
"_GOT_ is worse than _BB_"
]
},
{
:series => "BB",
:rating => "Great",
:type => "Crime",
:comparisons => [
"_BB_ is better than _GOT_",
"_BB_ is better than _E_"
]
},
{
:series => "E",
:rating => "Poor",
:type => "Drama",
:comparisons => [
"_E_ is worse than _BB_"
]
}
]
See Also
Array#|
Array#<=>

In Ruby, during iteration of an array, how do I send multiple array elements as values for one specific hash key?

I know how to iterate through an array and I know how to create hash keys and values. I do not know how to create an array for the value and pass it multiple elements. My desired value for hash below is:
{'abc' => [1, 2, 3] , 'def' => [4,5,6,7]}
How would I achieve this hash, while iterating through array a and b below using each?
a = [1,2,3,4,5,6,7]
c = [1,2,3]
b = ['abc', 'def']
hash = {}
From your guidelines given in the comment:
While iterating through array a, if the element of iteration is included in array c, it is passed to the array value within key 'abc'. Otherwise, it is passed to other array value in key 'def'
You can do this:
hash = {}
hash['abc'] = a.select { |x| c.include?(x) }
hash['def'] = a.reject{ |x| c.include?(x) }
See Enumerable#select and Enumerable#reject. Also can take a look at Enumerable#partition which would be another good choice here, where you want to split an array into two arrays based on some condition:
in_a, not_in_a = a.partition { |x| c.include?(x) }
hash = { 'abc' => in_a, 'def' => not_in_a }
You can also do it with regular each if these fancy enumerable methods are bit too much for you:
hash = { 'abc' => [], 'def' => [] }
a.each do |x|
if c.include?(x)
hash['abc'].push(x)
else
hash['def'].push(x)
end
end
Unfortunately this question turned out not to be as interesting as I was hoping. I was hoping that the problem was this:
Knowing the hash key and a value, how can I make sure the key's value is an array and that the given value is appended to that?
For instance, start with h being {}. I have a key name :k and a value 1. I want h[:k], if it doesn't already exist, to be [1]. But if it does already exist, then it's an array and I want to append 1 to that array; for instance, if h[:k] is already [3,2], now it should be [3,2,1].
I can think of various ways to ensure that, but one possibility is this:
(hash[key] ||= []) << value
To see that this works, let's make it a method:
def add_to_hash_array_value(hash:, key:, value:)
(hash[key] ||= []) << value
end
Now I'll just call that a bunch of times:
h = {}
add_to_hash_array_value(hash:h, key:"abc", value:1)
add_to_hash_array_value(hash:h, key:"abc", value:2)
add_to_hash_array_value(hash:h, key:"def", value:4)
add_to_hash_array_value(hash:h, key:"def", value:5)
add_to_hash_array_value(hash:h, key:"abc", value:3)
puts h #=> {"abc"=>[1, 2, 3], "def"=>[4, 5]}
We got the right answer.
This is nice because suppose I have some way of knowing, given a value, what key it should be appended to. I can just repeatedly apply that decision-making process.
However, the trouble with trying to apply that to the original question is that the original question seems not to know exactly how to decide, given a value, what key it should be appended to.

Mapping array values to hash with incrementing key name

I have an array of strings that are hex color codes like so:
["#121427", "#291833", "#4B2E4D", "#5D072F", "#BB2344", "#ED9F90"]
and I want to convert them into a hash with distinct key name where there is a distinct name "color" and then a integer value which increments as it transverses the array adding them like so:
{"color1" => "#121427", "color2" => "#291833", "color3" => "#4B2E4D", "color4" => "#5D072F", "color5" => "#BB2344", "color6" => "#ED9F90"}
The integer value can be 0 based or 1 based, it doesn't matter which ever is cleaner.
I've tried using the map method along with the to_h method, although I cannot figure out how to create an incremental key name as described.
It's not too hard to do this using the each_with_index method which is zero-indexed by default:
Hash[colors.each_with_index.map { |c, i| [ 'color%d' % i, c ] }]
You were close with map, you just needed to expand it into value/index pairs.
Another possible way:
colors.each_with_object({}).with_index(1){|(e, h), i| h["color#{i}"] = e}
It uses:
Enumerable#each_with_object to skip one temporary array and modify the resulting hash directly
Enumerator#with_index to also supply an auto-incrementing integer (it can even start from 1)
If arr is your array, you could do this:
idx = 1.step
# => #<Enumerator: 1:step>
arr.each_with_object({}) { |s,h| h["color#{idx.next}"] = s }
#=> {"color1"=>"#121427", "color2"=>"#291833", "color3"=>"#4B2E4D",
# "color4"=>"#5D072F", "color5"=>"#BB2344", "color6"=>"#ED9F90"}

Add key value pair to Array of Hashes when unique Id's match

I have two arrays of hashes
sent_array = [{:sellersku=>"0421077128", :asin=>"B00ND80WKY"},
{:sellersku=>"0320248102", :asin=>"B00WTEF9FG"},
{:sellersku=>"0324823180", :asin=>"B00HXZLB4E"}]
active_array = [{:price=>39.99, :asin1=>"B00ND80WKY"},
{:price=>7.99, :asin1=>"B00YSN9QOG"},
{:price=>10, :asin1=>"B00HXZLB4E"}]
I want to loop through sent_array, and find where the value in :asin is equal to the value in :asin1 in active_array, then copy the key & value of :price to sent_array. Resulting in this:
final_array = [{:sellersku=>"0421077128", :asin=>"B00ND80WKY", :price=>39.99},
{:sellersku=>"0320248102", :asin=>"B00WTEF9FG"},
{:sellersku=>"0324823180", :asin=>"B00HXZLB4E", :price=>10}]
I tried this, but I get a TypeError - no implicit conversion of Symbol into Integer (TypeError)
sent_array.each do |x|
x.detect { |key, value|
if value == active_array[:asin1]
x[:price] << active_array[:price]
end
}
end
For reasons of both efficiency and readability, it makes sense to first construct a lookup hash on active_array:
h = active_array.each_with_object({}) { |g,h| h[g[:asin1]] = g[:price] }
#=> {"B00ND80WKY"=>39.99, "B00YSN9QOG"=>7.99, "B00HXZLB4E"=>10}
We now merely step through sent_array, updating the hashes:
sent_array.each { |g| g[:price] = h[g[:asin]] if h.key?(g[:asin]) }
#=> [{:sellersku=>"0421077128", :asin=>"B00ND80WKY", :price=>39.99},
# {:sellersku=>"0320248102", :asin=>"B00WTEF9FG"},
# {:sellersku=>"0324823180", :asin=>"B00HXZLB4E", :price=>10}]
Retrieving a key-value pair from a hash (h) is much faster, of course, than searching for a key-value pair in an array of hashes.
This does the trick. Iterate over your sent array and attempt to find a record in your active_array that has that :asin. If you find something, set the price and you are done.
Your code I believe used detect/find incorrectly. What you want out of that method is the hash that matches and then do something with that. You were trying to do everything inside of detect.
sent_array.each do |sent|
item = active_array.find{ |i| i.has_value? sent[:asin] }
sent[:price] = item[:price] if item
end
=> [{:sellersku=>"0421077128", :asin=>"B00ND80WKY", :price=>39.99}, {:sellersku=>"0320248102", :asin=>"B00WTEF9FG"}, {:sellersku=>"0324823180", :asin=>"B00HXZLB4E", :price=>10}]
I am assuming second element of both sent_array and active_array has B00WTEF9FG as asin and asin1 respectively. (seeing your final result)
Now:
a = active_array.group_by{|a| a[:asin1]}
b = sent_array.group_by{|a| a[:asin]}
a.map { |k,v|
v[0].merge(b[k][0])
}
# => [{:price=>39.99, :asin1=>"B00ND80WKY", :sellersku=>"0421077128", :asin=>"B00ND80WKY"}, {:price=>7.99, :asin1=>"B00WTEF9FG", :sellersku=>"0320248102", :asin=>"B00WTEF9FG"}, {:price=>10, :asin1=>"B00HXZLB4E", :sellersku=>"0324823180", :asin=>"B00HXZLB4E"}]
Why were you getting TypeError?
You are doing active_array[:asin1]. Remember active_array itself is an Array. Unless you iterate over it, you cannot look for keys.
Another issue with your approach is, you are using Hash#detect
find is implemented in terms of each. And each, when called on a
Hash, returns key-value pairs in form of arrays with 2 elements
each. That's why find returns an array.
source
Same is true for detect. So x.detect { |key, value| .. } is not going to work as you are expecting it to.
Solution without assumption
a.map { |k,v|
b[k] ? v[0].merge(b[k][0]) : v[0]
}.compact
# => [{:price=>39.99, :asin1=>"B00ND80WKY", :sellersku=>"0421077128", :asin=>"B00ND80WKY"}, {:price=>7.99, :asin1=>"B00YSN9QOG"}, {:price=>10, :asin1=>"B00HXZLB4E", :sellersku=>"0324823180", :asin=>"B00HXZLB4E"}]
Here since asin1 => "B00ND80WKY" has no match, it cannot get sellersku from other hash.

Filling hash of multi-dimensional arrays in perl

Given three scalars, what is the perl syntax to fill a hash in which one of the scalars is the key, another determines which of two arrays is filled, and the third is appended to one of the arrays? For example:
my $weekday = "Monday";
my $kind = "Good";
my $event = "Birthday";
and given only the scalars and not their particular values, obtained inside a loop, I want a hash like:
my %Weekdays = {
'Monday' => [
["Birthday", "Holiday"], # The Good array
["Exam", "Workday"] # The Bad array
]
'Saturday' => [
["RoadTrip", "Concert", "Movie"],
["Yardwork", "VisitMIL"]
]
}
I know how to append a value to an array in a hash, such as if the key is a single array:
push( #{ $Weekdays{$weekday} }, $event);
Used in a loop, that could give me:
%Weekdays = {
'Monday' => [
'Birthday',
'Holiday',
'Exam',
'Workday'
]
}
I suppose the hash key is the particular weekday, and the value should be a two dimensional array. I don't know the perl syntax to, say, push Birthday into the hash as element [0][0] of the weekday array, and the next time through the loop, push another event in as [0][1] or [1][0]. Similarly, I don't know the syntax to access same.
Using your variables, I'd write it like this:
push #{ $Weekdays{ $weekday }[ $kind eq 'Good' ? 0 : 1 ] }, $event;
However, I'd probably just make the Good/Bad specifiers keys as well. And given my druthers:
use autobox::Core;
( $Weekdays{ $weekday }{ $kind } ||= [] )->push( $event );
Note that the way I've written it here, neither expression cares whether or not an array exists before we start.
Is there some reason that
push #{ $Weekdays{Monday}[0] }, "whatever";
isn’t working for you?

Resources