How to collapse a multi-dimensional array of hashes in Ruby? - arrays

Background:
Hey all, I am experimenting with external APIs and am trying to pull in all of the followers of a User from a site and apply some sorting.
I have refactored a lot of the code, HOWEVER, there is one part that is giving me a really tough time. I am convinced there is an easier way to implement this than what I have included and would be really grateful on any tips to do this in a much more eloquent way.
My goal is simple. I want to collapse an array of arrays of hashes (I hope that is the correct way to explain it) into one array of hashes.
Problem Description:
I have an array named f_collectionswhich has 5 elements. Each element is an array of size 200. Each sub-element of these arrays is a hash of about 10 key-value pairs. My best representation of this is as follows:
f_collections = [ collection1, collection2, ..., collection5 ]
collection1 = [ hash1, hash2, ..., hash200]
hash1 = { user_id: 1, user_name: "bob", ...}
I am trying to collapse this multi-dimensional array into one array of hashes. Since there are five collection arrays, this means the results array would have 1000 elements - all of which would be hashes.
followers = [hash1, hash2, ..., hash1000]
Code (i.e. my attempt which I do not want to keep):
I have gotten this to work with a very ugly piece of code (see below), with nested if statements, blocks, for loops, etc... This thing is a nightmare to read and I have tried my hardest to research ways to do this in a simpler way, I just cannot figure out how. I have tried flatten but it doesn't seem to work.
I am mostly just including this code to show I have tried very hard to solve this problem, and while yes I solved it, there must be a better way!
Note: I have simplified some variables to integers in the code below to make it more readable.
for n in 1..5 do
if n < 5
(0..199).each do |j|
if n == 1
nj = j
else
nj = (n - 1) * 200 + j
end
#followers[nj] = #f_collections[n-1].collection[j]
end
else
(0..199).each do |jj|
njj = (4) * 200 + jj
#followers[njj] = #f_collections[n-1].collection[jj]
end
end
end

Oh... so It is not an array objects that hold collections of hashes. Kind of. Lets give it another try:
flat = f_collection.map do |col|
col.collection
end.flatten
which can be shortened (and is more performant) to:
flat = f_collection.flat_map do |col|
col.collection
end
This works because the items in the f_collection array are objects that have a collection attribute, which in turn is an array.
So it is "array of things that have an array that contains hashes"
Old Answer follows below. I leave it here for documentation purpose. It was based on the assumption that the data structure is an array of array of hashes.
Just use #flatten (or #flatten! if you want this to be "inline")
flat = f_collections.flatten
Example
sub1 = [{a: 1}, {a: 2}]
sub2 = [{a: 3}, {a: 4}]
collection = [sub1, sub2]
flat = collection.flatten # returns a new collection
puts flat #> [{:a=>1}, {:a=>2}, {:a=>3}, {:a=>4}]
# or use the "inplace"/"destructive" version
collection.flatten! # modifies existing collection
puts collection #> [{:a=>1}, {:a=>2}, {:a=>3}, {:a=>4}]
Some recommendations for your existing code:
Do not use for n in 1..5, use Ruby-Style enumeration:
["some", "values"].each do |value|
puts value
end
Like this you do not need to hardcode the length (5) of the array (did not realize you removed the variables that specify these magic numbers). If you you want to detect the last iteration you can use each_with_index:
a = ["some", "home", "rome"]
a.each_with_index do |value, index|
if index == a.length - 1
puts "Last value is #{value}"
else
puts "Values before last: #{value}"
end
end
While #flatten will solve your problem you might want to see how DIY-solution could look like:
def flatten_recursive(collection, target = [])
collection.each do |item|
if item.is_a?(Array)
flatten_recursive(item, target)
else
target << item
end
end
target
end
Or an iterative solution (that is limited to two levels):
def flatten_iterative(collection)
target = []
collection.each do |sub|
sub.each do |item|
target << item
end
end
target
end

Related

How to find a specific value in a nested array?

I'm trying to figure out how to place a value into one of three arrays and then shuffle those arrays and have the program output the index location of the value.
Here is what I have so far:
# The purpose of this program is to randomly place the name Zac
# in one of three arrays and return the array number and position of
# Zac
A1 = ["John","Steve","Frank","Charles"]
A2 = ["Sam","Clint","Stuart","James"]
A3 = ["Vic","Jim","Bill","David"]
n = [A1,A2,A3]
name = "Zac"
def placename(title, namelist)
mix = rand(2)
namelist[mix] << title
namelist.shuffle
return namelist
end
allnames = [] << placename(name, n)
def findname(allnames, key)
allnames.each do |i|
until allnames[i].include?(key) == true
i+=1
end
location = allnames[i].find_index(key)
puts "The location and value of #{key} is #{location}"
end
end
findname(allnames, name)
At the moment I'm getting a "undefined method for Nil Class" error (no method error)
Can someone please clarify what I'm doing wrong with this or if there is a more effective way of going about this? Thanks in advance!!
Your approach assumes that in the block starting...
allnames.each do |i|
... that i will contain the index of the allnames element. This isn't true. i will contain the VALUE (contents) of the element.
What you could try as an alternative is...
allnames.each_with_index do |_value, i|
or, you can do...
allnames.each do |value|
and then replace all references to allnames[i] with value
another problem is that...
allnames = [] << placename(name, n)
puts the returned array of arrays inside ANOTHER array. I think what you want to do is..
allnames = placename(name, n)
I modified the last fewlines. I hope this is what you wanted
allnames = placename(name, n)
def findname allnames, key
r = allnames.map.with_index{|x,i|x.include?(key) ? i : p}-[p]
puts "The location of value #{key} is array number #{r[0]} and item number #{allnames[r[0]].index(key)}"
end
findname(allnames, name)
Edit: Randomization
To get randomized array number and item number you have to do the following
def placename(title, namelist)
mix = rand(3) # Since the number of arrays (nested within) is 3 we can use 3 instead of 2
namelist[mix] << title
namelist.map!{|x|x.shuffle}.shuffle! # Shuffling each item and the whole array in place.
return namelist
end
Assuming you want to modify the array in place, I'd do it like this:
# insert name into random subarray
def insert_name name
subarray_idx = rand #name_arrays.size
subarray = #name_arrays[subarray_idx]
insertion_idx = rand subarray.size
#name_arrays[subarray_idx].insert insertion_idx, name
sprintf '"%s" inserted at #name_arrays[%d][%d]',
name, subarray_idx, insertion_idx
end
# define starting array, then print & return the
# message for further parsing if needed
#name_arrays = [
%w[John Steve Frank Charles],
%w[Sam Clint Stuart James],
%w[Vic Jim Bill David],
]
p(insert_name 'Zac')
This has a few benefits:
You can inspect #name_arrays to validate that things look the way you expect.
The message can be parsed with String#scan if desired.
You can modify #insert_name to return your indexes, rather than having to search for the name directly.
If you don't capture the insertion index as a return value, or don't want to parse it from your message String, you can search for it by leveraging Enumerable#each_with_index and Array#index. For example:
# for demonstration only, set this so you can get the same
# results since the insertion index was randomized
#name_arrays =
[["John", "Steve", "Frank", "Charles"],
["Sam", "Clint", "Stuart", "James"],
["Vic", "Jim", "Zac", "Bill", "David"]]
# return indices of nested match
def find_name_idx name
#name_arrays.each_with_index
.map { [_2, _1.index(name)] }
.reject { _1.any? nil }
.pop
end
# use Array#dig to retrieve item at nested index
#name_arrays.dig *find_name_idx('Zac')

How to merge 2 arrays of equal length into a single dictionary with key:value pairs in Godot?

I have been trying to randomize the values in an ordered array (ex:[0,1,2,3]) in Godot. There is supposed to be a shuffle() method for arrays, but it seems to be broken and always returns "null". I have found a workaround that uses a Fisher-Yates shuffle, but the resulting array is considered "unsorted" by the engine, and therefore when I try to use methods such as bsearch() to find a value by it's position, the results are unreliable at best.
My solution was to create a dictionary, comprised of an array containing the random values I have obtained, merged with a second array of equal length with (sorted) numbers (in numerical order) which I can then use as keys to access specific array positions when needed.
Question made simple...
In GDScript, how would you take 2 arrays..
ex: ARRAY1 = [0,1,2,3]
ARRAY2 = [a,b,c,d]
..and merge them to form a dictionary that looks like this:
MergedDictionary = {0:a, 1:b, 2:c, 3:d}
Any help would be greatly appreciated.
Godot does not support "zip" methodology for merging arrays such as Python does, so I am stuck merging them manually. However... there is little to no documentation about how to do this in GDScript, despite my many hours of searching.
Try this:
var a = [1, 2, 3]
var b = ["a", "b", "c"]
var c = {}
if a.size() == b.size():
var i = 0
for element in a:
c[element] = b[i]
i += 1
print("Dictionary c: ", c)
If you want to add elements to a dictionary, you can assign values to the keys like existing keys.

Ruby array intersection returning a blank array

I'm very new to Ruby so please go easy on me. I have this small function that doesn't want to perform an intersection command. If I go into irb and enter the arrays then set the intersection command like: third_array = array1 & array2, third_array returns the common element. But when I run this snippet through irb, it just returns [ ]. Any suggestions?
class String
define_method(:antigrams) do |word2|
array1 = []
array2 = []
array1.push(self.split(""))
array2.push(word2.split(""))
third_array = array1 & array2
third_array
end
end
After looking at what you have, I think your code boils down to this:
class String
def antigrams(word)
self.chars & word.chars
end
end
"flurry".antigrams("flagrant")
# => ["f", "l", "r"]
If you're calling split('') on a word that's effectively the same as chars, though a lot less efficient. Another mistake was pushing a whole array into an array, which creates a nested array of the form [ [ 'f', 'l', ... ] ]. Since the two resulting array-of-arrays have nothing in common, their inner arrays are different, the & operation returns an empty array.
What you meant was to concatenate the one array to the other, something that can be done with += for example.
Whenever you're curious what's happening, either use irb to try out chunks of code, or p to debug at different points in your method.

Find duplicated values in an Array of Hashes

I am looking for a way to select only duplicate entries from multiple Arrays of Hashes.
Say I have a project with an attribute called "exchange_rate":
project.exchange_rate #=>
[{"name"=>"USD", "rate"=>1.0},
{"name"=>"EUR", "rate"=>0.91},
{"name"=>"CNY", "rate"=>6.51},
{"name"=>"NOK", "rate"=>1},
{"name"=>"DKK", "rate"=>1},
{"name"=>"JPY", "rate"=>113.24}]
Now I have multiple projects which have the same construct, just with a little more/less entries in the Array. The "rate" within the Hash isn't important at all. I just need to iterate over all projects and their exchange_rates and find those entries that are in each and every of the Arrays.
So to speak, if I had the following project_2:
project_2.exchange_rate #=>
[{"name"=>"USD", "rate"=>1.0},
{"name"=>"GBP", "rate"=>0.7},
{"name"=>"SGD", "rate"=>1.38},
{"name"=>"HKD", "rate"=>7.76},
{"name"=>"CNY", "rate"=>0.94},
{"name"=>"DE", "rate"=>0.86},
{"name"=>"JPY", "rate"=>113.24}]
After comparing these two entries, I'd like to end up with an Array that looks like so:
# => ["USD", "CNY", "JPY"]
Because these three names are in both of the projects. This should, of course, be dynamic and work with whatever number of projects and exchange_rates.
I can't seem to find a way of doing this.
I tried the following already:
er = projects.map { |e| e[:exchange_rate] }.inject(:+)
founds = er.find_all { |x| er.count(x) > 1 }.uniq
But it comes up with a huge Array that includes all kind of values, not just duplicates.
TL;DR:
I need to iterate over all projects and their exchange_rates
I need to find all duplicated entries of these
I need to end up with just the "name" value of these
I have an unknown amount of projects as well as exchange_rates bound to each project
Thank you very much in advance!
I figured this isn't exactly what I need, so I changed my mind and did it differently.
Still, the question might be viable for others to get answered. If you have an answer, go ahead and post it :)
My (completely off-topic) result:
names = projects.map{|p| p[:exchange_rates].map{|er| er["name"] } }
final = names.flatten.uniq
# from => [["USD", "EUR", "GBR"], [], ["MYR", "GBR"], ["USD"], ...]
# to ["USD", "EUR", "GBR", "MYR"]
you can simply use project_1.exchange_rate & project_2.exchange_rate
, which gives you [{"name"=>"USD", "rate"=>1.0}, {"name"=>"JPY", "rate"=>113.24}], i.e common entries from both the arrays whose key and value match in both arrays.
But if you're looking for finding only the common elements in terms of keys of the hashes in the two arrays, you can try something like this
project_1.exchange_rate.map {|e| e["name"]} &
project_2.exchange_rate.map {|e| e["name"]}
#=> ["USD", "CNY", "JPY"]
If you have multiple arrays like you said, try something like this:
def get_duplicate_keys(*rates)
all_rates = rates.inject([]) { |s, e| s + e }
temp = all_rates.group_by { |e| e["name"] }
temp.select { |k,v| v.count > 1 }.keys
end
r1 = [{"name"=>"USD", "rate"=>1.0},
{"name"=>"EUR", "rate"=>0.91},
{"name"=>"CNY", "rate"=>6.51},
{"name"=>"NOK", "rate"=>1},
{"name"=>"DKK", "rate"=>1},
{"name"=>"JPY", "rate"=>113.24}]
r2 = [{"name"=>"USD", "rate"=>1.0},
{"name"=>"GBP", "rate"=>0.7},
{"name"=>"SGD", "rate"=>1.38},
{"name"=>"HKD", "rate"=>7.76},
{"name"=>"CNY", "rate"=>0.94},
{"name"=>"DE", "rate"=>0.86},
{"name"=>"JPY", "rate"=>113.24}]
r3 = [{"name"=>"GBP", "rate"=>0.7},
{"name"=>"SGD", "rate"=>1.38}]
p get_duplicate_keys(r1 + r2 + r3)
#=> ["USD", "CNY", "JPY", "GBP", "SGD"]
You can try this solution,
duplicates = project.exchange_rate & project_2.exchange_rate
and then
duplicates.map{|er| er["name"]}
This returns result
=> ["USD", "CNY", "JPY"]
OR You can try below solution.....
Firstly you find array of of names for both projects
proj1_names = []
project.exchange_rates.each{ |er| proj1_names << er["name"] }
proj2_names = []
project_2.exchange_rates.each{ |er| proj2_names << er["name"]}
this gives result like
proj1_names = ["USD","EUR","CNY","NOK","DKK","JPY"]
proj2_names = ["USD","GBP","SGD","HKD","CNY","DE","JPY"]
and then try below method
proj1_names.select{|name| proj2_names.include?(name)}
this returns duplicate names as result
i.e => ["USD", "CNY", "JPY"]
May this helps you..

Modify hashes in an array based on another array

I have two arrays like this:
a = [{'one'=>1, 'two'=>2},{'uno'=>1, 'dos'=>2}]
b = ['english', 'spanish']
I need to add a key-value pair to each hash in a to get this:
a = [{'one'=>1, 'two'=>2, 'language'=>'english'},{'uno'=>1, 'dos'=>2, 'language'=>'spanish'}]
I attempted this:
(0..a.length).each {|c| a[c]['language']=b[c]}
and it does not work. With this:
a[1]['language']=b[1]
(0..a.length).each {|c| puts c}
an error is shown:
NoMethodError (undefined method '[]=' for nil:NilClass)
How can I fix this?
a.zip(b){|h, v| h["language"] = v}
a # => [
# {"one"=>1, "two"=>2, "language"=>"english"},
# {"uno"=>1, "dos"=>2, "language"=>"spanish"}
# ]
When the each iterator over your Range reaches the last element (i.e. a.length), you will attempt to access a nonexisting element of a.
In your example, a.length is 2, so on the last iteration of your each, you will attempt to access a[2], which doesn't exist. (a only contains 2 elements wich indices 0 and 1.) a[2] evaluates to nil, so you will now attempt to call nil['language']=b[2], which is syntactic sugar for nil.[]=('language', b[2]), and since nil doesn't have a []= method, you get a NoMethodError.
The immediate fix is to not iterate off the end of a, by using an exclusive Range:
(0...a.length).each {|c| a[c]['language'] = b[c] }
By the way, the code you posted:
(0..a.length).each {|c| puts c }
should clearly have shown you that you iterate till 2 instead of 1.
That's only the immediate fix, however. The real fix is to simply never iterate over a datastructure manually. That's what iterators are for.
Something like this, where Ruby will keep track of the index for you:
a.each_with_index do |hsh, i| hsh['language'] = b[i] end
Or, without fiddling with indices at all:
a.zip(b.zip(['language'].cycle).map(&:reverse).map(&Array.method(:[])).map(&:to_h)).map {|x, y| x.merge!(y) }
[Note: this last one doesn't mutate the original Arrays and Hashes unlike the other ones.]
The problem you're having is that your (0..a.length) is inclusive. a.length = 2 so you want to modify it to be 0...a.length which is exclusive.
On a side note, you could use Array#each_with_index like this so you don't have to worry about the length and so on.
a.each_with_index do |hash, index|
hash['language'] = b[index]
end
Here is another method you could use
b.each_with_index.with_object(a) do |(lang,i),obj|
obj[i]["language"] = lang
obj
end
#=>[
{"one"=>1, "two"=>2, "language"=>"english"},
{"uno"=>1, "dos"=>2, "language"=>"spanish"}
]
What this does is creates an Enumerator for b with [element,index] then it calls with_object using a as the object. It then iterates over the Enumerator passing in each language and its index along with the a object. It then uses the index from b to find the proper index in a and adds a language key to the hash that is equal to the language.
Please know this is a destructive method where the objects in a will mutate during the process. You could make it non destructive using with_object(a.map(&:dup)) this will dup the hashes in a and the originals will remain untouched.
All that being said I think YAML would be better suited for a task like this but I am not sure what your constraints are. As an example:
yml = <<YML
-
one: 1
two: 2
language: "english"
-
uno: 1
dos: 2
language: "spanish"
YML
require 'yaml'
YAML.load(yml)
#=>[
{"one"=>1, "two"=>2, "language"=>"english"},
{"uno"=>1, "dos"=>2, "language"=>"spanish"}
]
Although using YAML I would change the structure for numbers to be more like language => Array of numbers by index e.g. {"english" => ["zero","one","two"]}. That way you can can access them like ["english"][0] #=> "zero"

Resources