Ruby Array Having Hash Pairs? - ruby-on-rails

I have the ruby array as following :
array = [{"id"=>8, "book_id"=>14238}, {"id"=>5, "book_id"=>14238}, {"id"=>7, "book_id"=>10743}, {"id"=>9, "book_id"=>10743}]
I want a new array combining the result of ids having same book_id.
Expected Result:
array = [{"book_id"=>14238, "id"=>[8,5]}, {"book_id"=>10743, "id"=>[7,9]}]

I can't say that this is easy to understand, but it is concise:
array.group_by {|item| item["book_id"] }.map do |k, v|
{ "book_id" => k, "id" => v.map {|item| item["id"] } }
end
=> [{"book_id"=>14238, "id"=>[8, 5]}, {"book_id"=>10743, "id"=>[7, 9]}]
The first transformation done by group_by rearranges your array so that items with the same book_id are grouped together:
array.group_by {|item| item["book_id"] }
=> {14238=>[{"id"=>8, "book_id"=>14238}, {"id"=>5, "book_id"=>14238}], 10743=>[{"id"=>7, "book_id"=>10743}, {"id"=>9, "book_id"=>10743}]}
The second transformation (map) reformats the hash produced by the group_by into a list of hashes, and the second map collects the id's into a list.

You can also do this using the form of Hash#update (a.k.a. merge!) that employs a block to resolve the values of keys that are contained in both of the hashes being merged.
Code
def aggregate(arr)
arr.each_with_object({}) do |g,h|
f = { g["book_id"]=>{ "id"=>[g["id"]], "book_id"=>g["book_id"] } }
h.update(f) do |_,ov,nv|
ov["id"] << nv["id"].first
ov
end
end.values
end
Example
arr = [{"id"=>8, "book_id"=>14238}, {"id"=>5, "book_id"=>14238},
{"id"=>7, "book_id"=>10743}, {"id"=>9, "book_id"=>10743},
{"id"=>6, "book_id"=>10511}]
aggregate(arr)
#=> [{"id"=>[8, 5], "book_id"=>14238},
# {"id"=>[7, 9], "book_id"=>10743},
# {"id"=>[6], "book_id"=>10511}]
Alternative output
Depending on your requirements, you might consider building a single hash instead of another array of hashes:
def aggregate(arr)
arr.each_with_object({}) { |g,h|
h.update({ g["book_id"]=>[g["id"]] }) { |_,ov,nv| ov+nv } }
end
aggregate(arr)
#=> {14238=>[8, 5], 10743=>[7, 9], 10511=>[6]}

I'd use a hash for the output for easier lookups and/or reuse:
array = [{"id"=>8, "book_id"=>14238}, {"id"=>5, "book_id"=>14238}, {"id"=>7, "book_id"=>10743}, {"id"=>9, "book_id"=>10743}]
hash = array.group_by{ |h| h['book_id'] }.map{ |k, v| [k, v.flat_map{ |h| h['id'] }]}.to_h
# => {14238=>[8, 5], 10743=>[7, 9]}
The keys are the book_id values, and the associated array contains the id values.
The expected result of
array = [{"book_id"=>14238, "id"=>[8,5]}, {"book_id"=>10743, "id"=>[7,9]}]
isn't a good structure if you're going to do any sort of lookups in it. Imagine having hundreds or thousands of elements and needing to find "book_id" == 10743 in the array, especially if it's not a sorted list; The array would have to be walked until the desired entry was found. That is a slow process.
Instead, simplify the structure to a simple hash, allowing you to easily locate a value using a simple Hash lookup:
hash[10743]
The lookup will never slow down.
If the resulting data is to be iterated in order by sorting, use
sorted_keys = hash.keys.sort
and
hash.values_at(*sorted_keys)
to extract the values in the sorted order. Or iterate over the hash if the key/values need to be extracted, perhaps for insertion into a database.

Related

ruby delete hash of hash based on identical values for different keys

I have a hash of hashes like this:
authors = {"7"=> {"id"=>"60"} , "0"=> {"id"=>"60"} , "1"=> {"id"=>"99"}, "8"=> {"id"=>"99"}, "15"=> {"id"=>"19"} }
I want to merge each hash where the id of the hash in that hash is duplicated (or remove each second hash with same hash of hash id).
In this case, I want to end up with
authors = {"7"=> {"id"=>"60"} , "1"=> {"id"=>"99"}, "15"=> {"id"=>"19"}}
There are quite a few questions on sorting hashes of hashes, and I've been trying to get my head around this, but I don't see how to achieve this.
Here are two ways.
#1
require 'set'
st = Set.new
authors.select { |_,v| st.add?(v) }
#=> {"7"=>{"id"=>"60"}, "1"=>{"id"=>"99"}, "15"=>{"id"=>"19"}}
#2
authors.reverse_each.with_object({}) { |(k,v),h| h[v] = k }.
reverse_each.with_object({}) { |(k,v),h| h[v] = [k] }
#=> {"7"=>[{"id"=>"60"}], "1"=>[{"id"=>"99"}], "15"=>[{"id"=>"19"}]}
or
authors.reverse_each.to_h.invert.invert.reverse_each.to_h
Try this one
authors.to_a.uniq { |item| item.last["id"] }.to_h
=> {"7"=>{"id"=>"60"}, "1"=>{"id"=>"99"}, "15"=>{"id"=>"19"}}
uniq method with a block can do the work

Remove specific key set which is an array from a hash

I am trying to remove the elements in an array from a hash and the array forms part of the 'keys' in a hash. Here is an illustration.
hash = {'a' = 1, 'b' = 2, 'c' =3, 'd' = 4}
arr = ["a","d"] #Now I need to remove the elements from this array from the above hash
Resultant hash should be as below
new_hash = {'b' = 2,'c' =3}
This is what I tried unfortunately it doesn't seem to work
for i in 0..hash.length-1
arr.each do |key_to_del|
hash.delete key_to_del unless h.nil?
end
end
Your hash isn't correct format. It should be like this.
hash={"a"=>1, "b"=>2, "c"=>3, "d"=>4}
arr=["a","d"]
Solution 1
hash.reject! {|k, v| arr.include? k }
Solution 2
arr.each{|v| hash.delete(v)}

Ruby array of hash. group_by and modify in one line

I have an array of hashes, something like
[ {:type=>"Meat", :name=>"one"},
{:type=>"Meat", :name=>"two"},
{:type=>"Fruit", :name=>"four"} ]
and I want to convert it to this
{ "Meat" => ["one", "two"], "Fruit" => ["Four"]}
I tried group_by but then i got this
{ "Meat" => [{:type=>"Meat", :name=>"one"}, {:type=>"Meat", :name=>"two"}],
"Fruit" => [{:type=>"Fruit", :name=>"four"}] }
and then I can't modify it to leave just the name and not the full hash. I need to do this in one line because is for a grouped_options_for_select on a Rails form.
array.group_by{|h| h[:type]}.each{|_, v| v.replace(v.map{|h| h[:name]})}
# => {"Meat"=>["one", "two"], "Fruit"=>["four"]}
Following steenslag's suggestion:
array.group_by{|h| h[:type]}.each{|_, v| v.map!{|h| h[:name]}}
# => {"Meat"=>["one", "two"], "Fruit"=>["four"]}
In a single iteration over initial array:
arry.inject(Hash.new([])) { |h, a| h[a[:type]] += [a[:name]]; h }
Using ActiveSuport's Hash#transform_values:
array.group_by{ |h| h[:type] }.transform_values{ |hs| hs.map{ |h| h[:name] } }
#=> {"Meat"=>["one", "two"], "Fruit"=>["four"]}
array = [{:type=>"Meat", :name=>"one"}, {:type=>"Meat", :name=>"two"}, {:type=>"Fruit", :name=>"four"}]
array.inject({}) {|memo, value| (memo[value[:type]] ||= []) << value[:name]; memo}
I would do as below :
hsh =[{:type=>"Meat", :name=>"one"}, {:type=>"Meat", :name=>"two"}, {:type=>"Fruit", :name=>"four"}]
p Hash[hsh.group_by{|h| h[:type] }.map{|k,v| [k,v.map{|h|h[:name]}]}]
# >> {"Meat"=>["one", "two"], "Fruit"=>["four"]}
#ArupRakshit answer, slightly modified (the function has been added for sake of clarity in the final example):
def group(list, by, at)
list.group_by { |h| h[by] }.map { |k,v| [ k , v.map {|h| h[at]} ] }.to_h
end
sample =[
{:type=>"Meat", :name=>"one", :size=>"big" },
{:type=>"Meat", :name=>"two", :size=>"small" },
{:type=>"Fruit", :name=>"four", :size=>"small" }
]
group(sample, :type, :name) # => {"Meat"=>["one", "two"], "Fruit"=>["four"]}
group(sample, :size, :name) # => {"big"=>["one"], "small"=>["two", "four"]}
Please, notice that, although not mentioned in the question, you may want to preserve the original sample as it is. Some answers kept provision on this, others not as.
After grouping (list.group_by {...}) the part that does the transformation (without modifying the original sample's values) is:
.map { |k,v| [ k , v.map {|h| h[at]} ] }.to_h
Some hints:
iterating the pairs of the Hash of groups (first map), where
for each iteration, we receive |group_key, array] and return an Array of [group_key, new_array] (outer block),
and finally to_h transforms the Array of Arrays into the Hash (this [[gk1,arr1],[gk2,arr2]...] into this { gk1 => arr1, gk2 => arr2, ...})
There is one missing step not explained at step (2) above. new_array is made by v.map {|h| h[at]}, which justs casts the value at of each original Hash (h) element of the array (so we move from Array of Hashes to an Array of elements).
Hope that helps others to understand the example.

Split an Array of arrays into 2 separate arrays in Ruby

I have a hash with some key value pairs as below:
#level2 = #l2.inject(Hash.new(0)) { |hash,element|
hash[element] +=1
hash }
I perform some sorting on the hash based on the keys.
#level2 = #level2.sort_by { |x, _| x }.reverse
Now I assume that the sort_by gives me an Array of Arrays. I want to split this into 2 arrays such that my first array should contain all keys and second array should contain all values.
The hash#keys and hash#values are not accessible after sorting the hash. So that does not work in this case.
Regardless of how you make the hash it will will have a Hash#keys method and a Hash#values. They both return arrays that are just what you seem to want.
keys_array = #level2.keys
values_array = #level2.values
You could iterate over the array of arrays and add each element to a new array. This would keep the order of the elements.
keys_array = []
values_array = []
#level2.each do |key, value|
keys_array << key
values_array << value
end

Ruby on Rails: Array to Hash with (key, array of values)

Lets say I have an Array of content_categories (content_categories = user.content_categories)
I now want to add every element belonging to a certain categorie to content_categories with the category as a key and the the content-item IDs as elements of a set
In PHP something like this is possible:
foreach ($content_categories as $key => $category) {
$contentsByCategoryIDArray = Category.getContents($category[id])
$content_categories[$key][$contentsByCategoryIDArray]
}
Is there an easy way in rails to do this?
Greets,
Nico
Your question isn't really a Rails question, it's a general Ruby programming question.
Your description isn't very clear, but from what I understand, you want to group IDs for common categories using a Hash. There are various other ways of doing this, but this is easy to understand::
ary = [
'cat1', {:id => 1},
'cat2', {:id => 2},
'cat1', {:id => 3}
]
hsh = {}
ary.each_slice(2) { |a|
key,category = a
hsh[key] ? hsh[key] << category[:id] : hsh[key] = [category[:id]]
}
hsh # => {"cat1"=>[1, 3], "cat2"=>[2]}
I'm using a simple Array with a category, followed by a simple hash representing some object instance, because it makes it easy to visualize. If you have a more complex object, replace the hash entries with those objects, and tweak how you access the ID in the ternary (?:) line.
Using Enumerable.inject():
hsh = ary.each_slice(2).inject({}) { |h,a|
key,category = a
h[key] ? h[key] << category[:id] : h[key] = [category[:id]]
h
}
hsh # => {"cat1"=>[1, 3], "cat2"=>[2]}
Enumerable.group_by() could probably shrink it even more, but my brain is fading.
I'd use Enumerable#inject
content_categories = content_categories_array.inject({}){ |memo, category| memo[category] = Category.get_contents(category); memo }
Hash[content_categories.map{|cat|
[cat, Category.get_contents(cat)]
}]
Not really the right answer, because you want IDs in your array, but I post it anyway, because it's nice and short, and you might actually get away with it:
content_categories.group_by(&:category)
content_categories.each do |k,v|
content_categories[k] = Category.getContents(v)
end
I suppose it's works
If i understand correctly, content_categories is an array of categories, which needs to be turned into a hash of categories, and their elements.
content_categories_array = content_categories
content_categories_hash = {}
content_categories_array.each do |category|
content_categories_hash[category] = Category.get_contents(category)
end
content_categories = content_categories_hash
That is the long version, which you can also write like
content_categories = {}.tap do |hash|
content_categories.each { |category| hash[category] = Category.get_contents(category) }
end
For this solution, content_categories must be a hash, not an array as you describe. Otherwise not sure where you're getting the key.
contents_by_categories = Hash[*content_categories.map{|k, v| [k, Category.getContents(v.id)]}]

Resources