Passing sort_by an array of sort fields - ruby

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

Related

How can ONE Ruby-symbol map to MULTIPLE values?

From what I have read so far I know, that a symbol is pointing always to the same value. Unlike a string, which is mutable in Ruby. Means a string might not have the same value all the time.
But if I create a list which hashes like e.g. this one:
persons = [
{first_name: "John", last_name: "Doe"},
{first_name: "Lisa", last_name: "Meyer"}
]
If I now do:
persons[1][:first_name]
Then I get the correct value "Lisa".
But how does that work?
If a symbol can point to only one exact value:
How can it differentiate between the "first_name"-symbol of the first hash and the "first_name"-symbol of the second, third ... hash?
persons = [
{ first_name: "John", last_name: "Doe" },
{ first_name: "Lisa", last_name: "Meyer"}
]
persons is an array of hashes. Arrays can be accessed by their indexes.
So, the index 0 in persons is { first_name: "John", last_name: "Doe" }.
The index 1 in persons is { first_name: "Lisa", last_name: "Meyer"} and so on:
p persons[0] # {:first_name=>"John", :last_name=>"Doe"}
p persons[1] # {:first_name=>"Lisa", :last_name=>"Meyer"}
Don't get confused in mutability here. It's just you're referring to the wrong data type.
If you want, you can check every hash key from persons and see that they have the same object_id (first_name and last_name). That's because symbols are immutables, allowing Ruby to create only one instance of them as long as your code is running:
persons.map { |e| e.map { |key, _| [key, key.object_id] }.to_h }
# [{:first_name=>1016988, :last_name=>1017308}, {:first_name=>1016988, :last_name=>1017308}]
As per the comment question; no, it doesn't create a new object depending on the scope, e.g.:
p :first_name.object_id
# 1016668
def persons
[{ first_name: "John", last_name: "Doe" },
{ first_name: "Lisa", last_name: "Meyer"}]
end
p persons.map { |e| e.map { |key, _| [key, key.object_id] }.to_h }
# [{:first_name=>1016668, :last_name=>1017308}, {:first_name=>1016668, :last_name=>1017308}]
:first_name.object_id is defined outside the scope of the persons method, but while inside the method, it keeps pointing to the same object.
In your example you select the object at index 1 of your array.
{first_name: "Lisa", last_name: "Meyer"}
How could the value of the first_name key be different than 'Lisa'?

How can I achieve certain values of a hash?

In the following nested hash,
hash = {a: 2, b: 4, c: {name: "John", id: 12, age: 33}}
I want to return the values that are hash:
{:name => "John", :id => 12, :age => 33}
I want to returned a hash. I thought the following code will do the job:
hash.select! {|_k, v| v.is_a?(Hash)}
# => {:c => {:name => "John", :id => 12, :age => 33}}
but I get both k/v pairs. Did I miss anything on the code? How can I achieve the return value as mentioned?
I would do something like:
hash.values.find(&Hash.method(:===))
#=> {:name=>"John", :id=>12, :age=>33}
select returns the key and value that matched. Add .values to get just the values without the keys:
hash.select! { |_k, v| v.is_a?(Hash) }.values
This will return an array of the values that were matched by select:
[{:name=>"John", :id=>12, :age=>33}]
If you know there will only ever be one result, you can get the desired value by calling first:
hash.select! { |_k, v| v.is_a?(Hash) }.values.first
{:name=>"John", :id=>12, :age=>33}

sort a hash by a property of the value

I have a hash that has "Song" objects as values. These song objects have a property 'updated_at' that I want to sort them by. I tried something like:
#data.values.updated_at.sort
but no luck. Any ideas?
You want to check the documentation of Enumerable#sort_by
e.g.
[{name: 'ryan', number: 3},
{name: 'john', number: 1},
{name: 'june', number: 2}].sort_by{|item| item[:number]}
Hopefully that is enough to get you going.
It's simply:
#data.values.sort_by(&:updated_at)
For example:
class A
attr_reader :updated_at
def initialize(updated_at)
#updated_at = updated_at
end
end
#data = { "Bob"=>A.new(3), "Trixi"=>A.new(1), "Bubba"=>A.new(2) }
#=> {"Bob"=>#<A:0x007ffa191935a0 #updated_at=3>,
# "Trixi"=>#<A:0x007ffa19193550 #updated_at=1>,
# "Bubba"=>#<A:0x007ffa19193528 #updated_at=2>}
#data.values.sort_by(&:updated_at)
#=> [#<A:0x007ffa19193550 #updated_at=1>,
# #<A:0x007ffa19193528 #updated_at=2>,
# #<A:0x007ffa191935a0 #updated_at=3>]

A more elegant way to map an array of hashes in ruby

I've got an array of hashes: hashes = [{field: 'one'}, {field: 'two'}]
And I want to get the list of fields from it: ['one', 'two']
hashes.map(&:field) doesn't work, clearly, and hashes.map { |hash| hash[field] } feels a bit clunky to me.
Is there a more elegant way?
EDIT: I should clarify, I do only want the value of 'fields' in my response.
So,
hashes = [{field: 'one', another: 'three'}, {field: 'two'}].do_the_thing should be ['one', 'two']
Perhaps something like the following would be more pleasing to the eye:
hashes = [{field: 'one', another: 'three'}, {field: 'two'}]
fields = lambda { |hash| hash[:field] }
hashes.collect(&fields)
Look at flat_map:
hashes = [{field: 'one'}, {field: 'two'}]
hashes.flat_map(&:values) # => ["one", "two"]
Not sure if this is any better, but perhaps it reads a little more clearly:
hashes.map { |hash| hash.values }.flatten

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