Sort array with custom order - ruby

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.

Related

Append increment number to duplicate item in array of objects [closed]

Closed. This question needs to be more focused. It is not currently accepting answers.
Want to improve this question? Update the question so it focuses on one problem only by editing this post.
Closed 2 years ago.
This post was edited and submitted for review 12 days ago.
Improve this question
There are duplicate items in the array. I want to append an incremental number to such items. For example:
Input:
[
{id: 1, name: "john"},
{id: 2, name: "Man"},
{id: 3, name: "Man"},
{id: 4, name: "john"},
{id: 5, name: "kind"},
{id: 6, name: "nova"},
{id: 7, name: "kind"},
{id: 8, name: "fred"},
{id: 9, name: "fred"},
{id: 10, name: "john"}
]
Expected output:
[
{id: 1, name: "john-1"},
{id: 2, name: "Man-1"},
{id: 3, name: "Man-2"},
{id: 4, name: "john-2"},
{id: 5, name: "kind-1"},
{id: 6, name: "nova"},
{id: 7, name: "kind-2"},
{id: 8, name: "fred"},
{id: 9, name: "monk"},
{id: 10, name: "john-3"}
]
the input data can be huge, is there any way to mutate the same object and find only duplicate to reduce the latency and memory
Something like this would work:
input
.group_by { |item| item[:name] }
.reject { |_name, entries| entries.count == 1 }
.each do |_name, entries|
entries.each.with_index(1) do |entry, index|
entry[:name] << "-#{index}"
end
end
It groups the items by their :name key and rejects those with only 1 entry. It then traverses the remaining groups (i.e. with 2 or more entries) and appends the 1-bases index to each entry's name.
Afterwards, input will be:
[{:id=>1, :name=>"john-1"},
{:id=>2, :name=>"Man-1"},
{:id=>3, :name=>"Man-2"},
{:id=>4, :name=>"john-2"},
{:id=>5, :name=>"kind-1"},
{:id=>6, :name=>"nova"},
{:id=>7, :name=>"kind-2"},
{:id=>8, :name=>"fred-1"},
{:id=>9, :name=>"fred-2"},
{:id=>10, :name=>"john-3"}]
This is some quick example with given input:
input
.group_by { |value| value[:name] }
.values
.select { |x| x.map.with_index(1) { |y, i| p y[:name] = "#{y[:name]}-#{i}" } }
.flatten
And thats the output:
[{:id=>1, :name=>"john-1"},
{:id=>4, :name=>"john-2"},
{:id=>10, :name=>"john-3"},
{:id=>2, :name=>"Man-1"},
{:id=>3, :name=>"Man-2"},
{:id=>5, :name=>"kind-1"},
{:id=>7, :name=>"kind-2"},
{:id=>6, :name=>"nova-1"},
{:id=>8, :name=>"fred-1"},
{:id=>9, :name=>"fred-2"}]
Group by will group input by duplicated names. I don't know if you tried anything but this is something that you can start work with and improve.
Tried to mutate the same input and incremented the values in name string
orders = [{:id=>2, :name=>"Man"}, {:id=>3, :name=>"Man"}, {:id=>8, :name=>"fred"}, {:id=>9, :name=>"fred"},{:id=>5, :name=>"kindaa"}, {:id=>1, :name=>"john"}, {:id=>4, :name=>"john"}, {:id=>10, :name=>"john"}, {:id=>5, :name=>"kinda"}, {:id=>5, :name=>"kind"}, {:id=>7, :name=>"kind"}, {:id=>6, :name=>"nova"}, {:id=>11, :name=>"nova"}]
counter = 1
orders.each_with_index do |order, index|
if index < orders.length - 1
if order[:name] == orders[index + 1][:name]
orders[index][:name] = orders[index][:name].to_s + "-#{counter}"
counter = counter + 1
else
orders[index][:name] = orders[index][:name].to_s + "-#{counter}" if counter > 1
counter = 1
end
else
orders[index][:name] = orders[index][:name].to_s + "-#{counter}" if counter > 1
end
end
puts orders
Output:
{:id=>2, :name=>"Man-1"}
{:id=>3, :name=>"Man-2"}
{:id=>8, :name=>"fred-1"}
{:id=>9, :name=>"fred-2"}
{:id=>5, :name=>"kindaa"}
{:id=>1, :name=>"john-1"}
{:id=>4, :name=>"john-2"}
{:id=>10, :name=>"john-3"}
{:id=>5, :name=>"kinda"}
{:id=>5, :name=>"kind-1"}
{:id=>7, :name=>"kind-2"}
{:id=>6, :name=>"nova-1"}
{:id=>11, :name=>"nova-2"}

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"]]

Simulate join between Hashes

I have two Arrays of Hashes which simulate two tables in a database, with one key in the first hash referencing a separately-named key in the second hash, example below:
cars = [ { id: 1, color: 'red', owner_id: 1 }, { id: 2, color: 'black', owner_id: 1 } ]
owners = [ { id: 1, name: 'Alice' }, { id: 2, name: 'Bob' } ]
I'd like to try to accomplish a "join" on these two hashes, resulting in a new Array of Hashes, so that the keys and values from owners will be merged into any of the cars hashes where the cars' :owner_id matches an owner's :id. So in the above example, the result would look like this:
[ { id: 1, color: 'red', owner_id: 1, name: 'Alice' }, { id: 2, color: 'black', owner_id: 1, name: 'Alice' } ]
Anyone have any thoughts on how I could achieve this? Thank you!
[EDIT] Updated to clarify that I would like the results to be placed in a new Array of Hashes, rather than mutating either of the original Arrays.
def join(referers, referees, on_referer, on_referee)
referers.map do |record|
referees.find do |referee_record|
record[on_referer] == referee_record[on_referee]
end.merge(record)
end
end
cars = [ { id: 1, color: 'red', owner_id: 1 }, { id: 2, color: 'black', owner_id: 1 } ]
owners = [ { id: 1, name: 'Alice' }, { id: 2, name: 'Bob' } ]
join(cars, owners, :owner_id, :id)
# => [{:id=>1, :name=>"Alice", :color=>"red", :owner_id=>1},
# {:id=>2, :name=>"Alice", :color=>"black", :owner_id=>1}]
Edit: I just noticed that it is the key :owner_id in cars that is to be matched with the :id in owners. I assumed the key :id in cars was to be matched. I will leave my answer as is, considering that the modification is trivial and that it may be easier to follow if the match is to be on the same key names.
Assuming that:
you want to modify (mutate) cars; and
for each element h of owners there is an element g of cars for which h[:id] == g[:id],
it's just
owners.each { |h| cars.find { |g| g[:id] == h[:id] }.update(h) }
cars #=> [{:id=>1, :color=>"red", :owner_id=>1, :name=>"Alice"},
# {:id=>2, :color=>"black", :owner_id=>1, :name=>"Bob"}]
On the other hand, if:
you do not wish to mutate cars or
for a given element h of owners there may be no element g of cars for which h[:id]==g[:id] or
you just want to improve efficiency,
you could first create a hash for cars or owners whose keys are values of :id.
Suppose:
owners = [ { id: 3, name: 'Alice' }, { id: 2, name: 'Bob' } ]
We could create a hash for owners:
owners_by_id = owners.each_with_object({}) { |g,h| h.update(g[:id]=>g) }
#=> {3=>{:id=>3, :name=>"Alice"}, 2=>{:id=>2, :name=>"Bob"}}
and then write:
cars.map do |h|
g = {}.merge(h)
id = g[:id]
g.update(owners_by_id[id]) if owners_by_id.key?(id)
g
end
#=> [{:id=>1, :color=>"red", :owner_id=>1},
# {:id=>2, :color=>"black", :owner_id=>1, :name=>"Bob"}]
Assuming that the hashes at the same position in the arrays correspond:
[cars, owners].transpose.map{|h1, h2| h1.merge(h2)}
Otherwise, your example is bad.

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)

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

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] }

Resources