Convert Array of objects to Hash with a field as the key - ruby

I have an Array of objects:
[
#<User id: 1, name: "Kostas">,
#<User id: 2, name: "Moufa">,
...
]
And I want to convert this into an Hash with the id as the keys and the objects as the values. Right now I do it like so but I know there is a better way:
users = User.all.reduce({}) do |hash, user|
hash[user.id] = user
hash
end
The expected output:
{
1 => #<User id: 1, name: "Kostas">,
2 => #<User id: 2, name: "Moufa">,
...
}

users_by_id = User.all.map { |user| [user.id, user] }.to_h
If you are using Rails, ActiveSupport provides Enumerable#index_by:
users_by_id = User.all.index_by(&:id)

You'll get a slightly better code by using each_with_object instead of reduce.
users = User.all.each_with_object({}) do |user, hash|
hash[user.id] = user
end

You can simply do (using the Ruby 3 syntax of _1)
users_by_id = User.all.to_h { [_1.id, _1] }

Related

Ruby function to get specific item in array and dynamically get nested data

If I have an array of hashes that looks like this
array = [{
name: 'Stan',
surname: 'Smith',
address: {
street: 'Some street',
postcode: '98877',
#...
}
}, {
#...
}]
can you write a function to get a specific item in an array, iterate over it and dynamically retrieve subsequently nested data?
This example doesn't work, but hopefully better explains my question:
def getDataFromFirstItem(someVal)
array(0).each{ |k, v| v["#{ someVal }"] }
end
puts getDataFromFirstItem('name')
# Expected output: 'Stan'
For context, I'm trying to create a Middleman helper so that I don't have to loop through a specific array that only has one item each time I use it in my template. The item (a hash) contains a load of global site variables. The data is coming from Contentful, within which everything is an array of entries.
Starting in ruby 2.3 and greater, you can use Array#dig and Hash#dig which both
Extracts the nested value specified by the sequence of idx objects by calling dig at each step, returning nil if any intermediate step is nil.
array = [{
name: 'Stan',
surname: 'Smith',
address: {
street: 'Some Street',
postcode: '98877'
}
}, {
}]
array.dig(0, :name) # => "Stan"
array.dig(0, :address, :postcode) # => "98877"
array.dig(0, :address, :city) # => nil
array.dig(1, :address, :postcode) # => nil
array.dig(2, :address, :postcode) # => nil
Please try this
array = [{
name: 'Stan',
surname: 'Smith',
address: {
street: 'Some street',
postcode: '98877',
#...
}
},
{
name: 'Nimish',
surname: 'Gupta',
address: {
street: 'Some street',
postcode: '98877',
#...
}
}
]
def getDataFromFirstItem(array, someVal)
array[0][someVal]
end
#Run this command
getDataFromFirstItem(array, :name) # => 'Stan'
#Please notice I send name as a symbol instead of string because the hash you declared consists of symbol keys
#Also if you want to make a dynamic program that works on all indexes of an array and not on a specific index then you can try this
def getDataFromItem(array, index, someVal)
if array[index]
array[index][someVal]
end
end
getDataFromItem(array, 0, :name) # => Stan
getDataFromItem(array, 1, :name) # => Nimish
getDataFromItem(array, 2, :name) # => nil
Hope this works, Please let me know if you still faces any issues

Passing sort_by an array of sort fields

If I have a array of hashes
collection = [
{ first_name: 'john', last_name: 'smith', middle: 'c'},
{ first_name: 'john', last_name: 'foo', middle: 'a'}
]
And an array of keys I want to sort by:
sort_keys = ['first_name', 'last_name']
How can I pass these keys to sort_by given that the keys will always match the keys in the collection?
I've tried
collection.sort_by { |v| sort_keys.map(&:v) }
but this doesn't work. I believe I'll need to use a proc but I'm not sure how to implement it. Would appreciate any help!
Using Ruby 2.2.1
If you change your sort_keys to contain symbols:
sort_keys = [:first_name, :last_name]
You can use values_at to retrieve the values:
collection.sort_by { |h| h.values_at(*sort_keys) }
#=> [{:first_name=>"john", :last_name=>"foo", :middle=>"a"}, {:first_name=>"john", :last_name=>"smith", :middle=>"c"}]
The array that is used to sort the hashes looks like this:
collection.map { |h| h.values_at(*sort_keys) }
#=> [["john", "smith"], ["john", "foo"]]

Sort array with custom order

I have an array of ids order say
order = [5,2,8,6]
and another array of hash
[{id: 2,name: name2},{id: 5,name: name5}, {id: 6,name: name6}, {id: 8,name: name8}]
I want it sorted as
[{id: 5,name: name5},{id: 2,name: name2}, {id: 8,name: name8}, {id: 6,name: name6}]
What could be best way to implement this? I can implement this with iterating both and pushing it to new array but looking for better solution.
Try this
arr = [
{:id=>2, :name=>"name2"}, {:id=>5, :name=>"name5"},
{:id=>6, :name=>"name6"}, {:id=>8, :name=>"name8"}
]
order = [5,2,8,6]
arr.sort_by { |a| order.index(a[:id]) }
# => [{:id=>5, :name=>"name5"}, {:id=>2, :name=>"name2"},
#{:id=>8, :name=>"name8"}, {:id=>6, :name=>"name6"}]
Enumerable#in_order_of (Rails 7+)
Starting from Rails 7, there is a new method Enumerable#in_order_of.
A quote right from the official Rails docs:
in_order_of(key, series)
Returns a new Array where the order has been set to that provided in the series, based on the key of the objects in the original enumerable.
[ Person.find(5), Person.find(3), Person.find(1) ].in_order_of(:id, [ 1, 5, 3 ])
=> [ Person.find(1), Person.find(5), Person.find(3) ]
If the series include keys that have no corresponding element in the Enumerable, these are ignored. If the Enumerable has additional elements that aren't named in the series, these are not included in the result.
It is not perfect in a case of hashes, but you can consider something like:
require 'ostruct'
items = [{ id: 2, name: 'name2' }, { id: 5, name: 'name5' }, { id: 6, name: 'name6' }, { id: 8, name: 'name8' }]
items.map(&OpenStruct.method(:new)).in_order_of(:id, [5,2,8,6]).map(&:to_h)
# => [{:id=>5, :name=>"name5"}, {:id=>2, :name=>"name2"}, {:id=>8, :name=>"name8"}, {:id=>6, :name=>"name6"}]
Sources:
Official docs - Enumerable#in_order_of.
PR - Enumerable#in_order_of #41333.
Rails 7 adds Enumerable#in_order_of.

Is there a clean way to access hash values in array of hashes?

In this code:
arr = [ { id: 1, body: 'foo'}, { id: 2, body: 'bar' }, { id: 3, body: 'foobar' }]
arr.map { |h| h[:id] } # => [1, 2, 3]
Is there a cleaner way to get the values out of an array of hashes like this?
Underscore.js has pluck, I'm wondering if there is a Ruby equivalent.
If you don't mind monkey-patching, you can go pluck yourself:
arr = [{ id: 1, body: 'foo'}, { id: 2, body: 'bar' }, { id: 3, body: 'foobar' }]
class Array
def pluck(key)
map { |h| h[key] }
end
end
arr.pluck(:id)
=> [1, 2, 3]
arr.pluck(:body)
=> ["foo", "bar", "foobar"]
Furthermore, it looks like someone has already generalised this for Enumerables, and someone else for a more general solution.
Now rails support Array.pluck out of the box. It has been implemented by this PR
It is implemented as:
def pluck(key)
map { |element| element[key] }
end
So there is no need to define it anymore :)
Unpopular opinion maybe, but I wouldn't recommend using pluck on Array in Rails projects since it is also implemented by ActiveRecord and it behaves quite differently in the ORM context (it changes the select statement) :
User.all.pluck(:name) # Changes select statement to only load names
User.all.to_a.pluck(:name) # Loads the whole objects, converts to array, then filters the name out
Therefore, to avoid confusion, I'd recommend using the shorten map(&:attr) syntax on arrays:
arr.map(&:name)

Removing hashes that have identical values for particular keys

I have an Array of Hashes with the same keys, storing people's data.
I want to remove the hashes that have the same values for the keys :name and :surname. The rest of the values can differ, so calling uniq! on array won't work.
Is there a simple solution for this?
You can pass a block to uniq or uniq!, the value returned by the block is used to compare two entries for equality:
irb> people = [{name: 'foo', surname: 'bar', age: 10},
{name: 'foo', surname: 'bar' age: 11}]
irb> people.uniq { |p| [p[:name], p[:surname]] }
=> [{:name=>"foo", :surname=>"bar", :age=>10}]
arr=[{name: 'john', surname: 'smith', phone:123456789},
{name: 'thomas', surname: 'hardy', phone: 671234992},
{name: 'john', surname: 'smith', phone: 666777888}]
# [{:name=>"john", :surname=>"smith", :phone=>123456789},
# {:name=>"thomas", :surname=>"hardy", :phone=>671234992},
# {:name=>"john", :surname=>"smith", :phone=>666777888}]
arr.uniq {|h| [h[:name], h[:surname]]}
# [{:name=>"john", :surname=>"smith", :phone=>123456789},
# {:name=>"thomas", :surname=>"hardy", :phone=>671234992}]
unique_people = {}
person_array.each do |person|
unique_people["#{person[:name]} #{person[:surname]}"] = person
end
array_of_unique_people = unique_people.values
This should do the trick.
a.delete_if do |h|
a.select{|i| i[:name] == h[:name] and i[:surname] == h[:surname] }.count > 1
end

Resources