I'm trying to sort an array of objects based upon different attributes. Some of those attributes I would like to sort in ascending order and some in descending order. I have been able to sort by ascending or descending but have been unable to combine the two.
Here is the simple class I am working with:
class Dog
attr_reader :name, :gender
DOGS = []
def initialize(name, gender)
#name = name
#gender = gender
DOGS << self
end
def self.all
DOGS
end
def self.sort_all_by_gender_then_name
self.all.sort_by { |d| [d.gender, d.name] }
end
end
I can then instantiate some dogs to be sorted later.
#rover = Dog.new("Rover", "Male")
#max = Dog.new("Max", "Male")
#fluffy = Dog.new("Fluffy", "Female")
#cocoa = Dog.new("Cocoa", "Female")
I can then use the sort_all_by_gender_then_name method.
Dog.sort_all_by_gender_then_name
=> [#cocoa, #fluffy, #max, #rover]
The array it returns includes females first, then males, all sorted by name in ascending order.
But what if I want to have gender be descending, and then name ascending, so that it would be males first and then sorted by name ascending. In this case:
=> [#max, #rover, #cocoa, #fluffy]
Or, if I wanted it by gender ascending, but name descending:
=> [#fluffy, #cocoa, #rover, #max]
When sorting numerical values, you can prepend a - to make it sort in reverse. However, I have been unable to find a way to do this with strings. Any help or ideas would be appreciated. Thanks.
Here's one way to do it using .sort instead of .sort_by:
dogs = [
{ name: "Rover", gender: "Male" },
{ name: "Max", gender: "Male" },
{ name: "Fluffy", gender: "Female" },
{ name: "Cocoa", gender: "Female" }
]
# gender asc, name asc
p(dogs.sort do |a, b|
[a[:gender], a[:name]] <=> [b[:gender], b[:name]]
end)
# gender desc, name asc
p(dogs.sort do |a, b|
[b[:gender], a[:name]] <=> [a[:gender], b[:name]]
end)
# gender asc, name desc
p(dogs.sort do |a, b|
[a[:gender], b[:name]] <=> [b[:gender], a[:name]]
end)
Output:
[{:name=>"Cocoa", :gender=>"Female"}, {:name=>"Fluffy", :gender=>"Female"}, {:name=>"Max", :gender=>"Male"}, {:name=>"Rover", :gender=>"Male"}]
[{:name=>"Max", :gender=>"Male"}, {:name=>"Rover", :gender=>"Male"}, {:name=>"Cocoa", :gender=>"Female"}, {:name=>"Fluffy", :gender=>"Female"}]
[{:name=>"Fluffy", :gender=>"Female"}, {:name=>"Cocoa", :gender=>"Female"}, {:name=>"Rover", :gender=>"Male"}, {:name=>"Max", :gender=>"Male"}]
Basically, this is doing something similar to negating numbers (as you mentioned in the question), by swapping the property to the other element if it needs to be sorted in descending order.
This ReversedOrder mixin can help you accomplish the mixed direction sorts on seperate attributes, using sort_by:
module ReversedOrder
def <=>(other)
- super
end
end
Use example:
dogs = [
{ name: "Rover", gender: "Male" },
{ name: "Max", gender: "Male" },
{ name: "Fluffy", gender: "Female" },
{ name: "Cocoa", gender: "Female" }
]
dogs.sort_by {|e| [e[:gender], e[:name]] }
=> [{:name=>"Cocoa", :gender=>"Female"},
{:name=>"Fluffy", :gender=>"Female"},
{:name=>"Max", :gender=>"Male"},
{:name=>"Rover", :gender=>"Male"}]
dogs.sort_by {|e| [e[:gender].dup.extend(ReversedOrder), e[:name]] }
=> [{:name=>"Max", :gender=>"Male"},
{:name=>"Rover", :gender=>"Male"},
{:name=>"Cocoa", :gender=>"Female"},
{:name=>"Fluffy", :gender=>"Female"}]
Note: Be careful to dup the reversed element. Without that, you will mixin the comparison inverter to the actual object instead of just the key being made for sort_by and it will forever produce reversed comparisons.
Related
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'?
For example, I have
array = [ {name: 'robert', nationality: 'asian', age: 10},
{name: 'robert', nationality: 'asian', age: 5},
{name: 'sira', nationality: 'african', age: 15} ]
I want to get the result as
array = [ {name: 'robert', nationality: 'asian', age: 15},
{name: 'sira', nationality: 'african', age: 15} ]
since there are 2 Robert's with the same nationality.
Any help would be much appreciated.
I have tried Array.uniq! {|e| e[:name] && e[:nationality] } but I want to add both numbers in the two hashes which is 10 + 5
P.S: Array can have n number of hashes.
I would start with something like this:
array = [
{ name: 'robert', nationality: 'asian', age: 10 },
{ name: 'robert', nationality: 'asian', age: 5 },
{ name: 'sira', nationality: 'african', age: 15 }
]
array.group_by { |e| e.values_at(:name, :nationality) }
.map { |_, vs| vs.first.merge(age: vs.sum { |v| v[:age] }) }
#=> [
# {
# :name => "robert",
# :nationality => "asian",
# :age => 15
# }, {
# :name => "sira",
# :nationality => "african",
# :age => 15
# }
# ]
Let's take a look at what you want to accomplish and go from there. You have a list of some objects, and you want to merge certain objects together if they have the same ethnicity and name. So we have a key by which we will merge. Let's put that in programming terms.
key = proc { |x| [x[:name], x[:nationality]] }
We've defined a procedure which takes a hash and returns its "key" value. If this procedure returns the same value (according to eql?) for two hashes, then those two hashes need to be merged together. Now, what do we mean by "merge"? You want to add the ages together, so let's write a merge function.
merge = proc { |x, y| x.dup.tap { |x1| x1[:age] += y[:age] } }
If we have two values x and y such that key[x] and key[y] are the same, we want to merge them by making a copy of x and adding y's age to it. That's exactly what this procedure does. Now that we have our building blocks, we can write the algorithm.
We want to produce an array at the end, after merging using the key procedure we've written. Fortunately, Ruby has a handy function called each_with_object which will do something very nice for us. The method each_with_object will execute its block for each element of the array, passing in a predetermined value as the other argument. This will come in handy here.
result = array.each_with_object({}) do |x, hsh|
# ...
end.values
Since we're using keys and values to do the merge, the most efficient way to do this is going to be with a hash. Hence, we pass in an empty hash as the extra object, which we'll modify to accumulate the merge results. At the end, we don't care about the keys anymore, so we write .values to get just the objects themselves. Now for the final pieces.
if hsh.include? key[x]
hsh[ key[x] ] = merge.call hsh[ key[x] ], x
else
hsh[ key[x] ] = x
end
Let's break this down. If the hash already includes key[x], which is the key for the object x that we're looking at, then we want to merge x with the value that is currently at key[x]. This is where we add the ages together. This approach only works if the merge function is what mathematicians call a semigroup, which is a fancy way of saying that the operation is associative. You don't need to worry too much about that; addition is a very good example of a semigroup, so it works here.
Anyway, if the key doesn't exist in the hash, we want to put the current value in the hash at the key position. The resulting hash from merging is returned, and then we can get the values out of it to get the result you wanted.
key = proc { |x| [x[:name], x[:nationality]] }
merge = proc { |x, y| x.dup.tap { |x1| x1[:age] += y[:age] } }
result = array.each_with_object({}) do |x, hsh|
if hsh.include? key[x]
hsh[ key[x] ] = merge.call hsh[ key[x] ], x
else
hsh[ key[x] ] = x
end
end.values
Now, my complexity theory is a bit rusty, but if Ruby implements its hash type efficiently (which I'm fairly certain it does), then this merge algorithm is O(n), which means it will take a linear amount of time to finish, given the problem size as input.
array.each_with_object(Hash.new(0)) { |g,h| h[[g[:name], g[:nationality]]] += g[:age] }.
map { |(name, nationality),age| { name:name, nationality:nationality, age:age } }
[{ :name=>"robert", :nationality=>"asian", :age=>15 },
{ :name=>"sira", :nationality=>"african", :age=>15 }]
The two steps are as follows.
a = array.each_with_object(Hash.new(0)) { |g,h| h[[g[:name], g[:nationality]]] += g[:age] }
#=> { ["robert", "asian"]=>15, ["sira", "african"]=>15 }
This uses the class method Hash::new to create a hash with a default value of zero (represented by the block variable h). Once this hash heen obtained it is a simple matter to construct the desired hash:
a.map { |(name, nationality),age| { name:name, nationality:nationality, age:age } }
I am trying to create a nested hash from an array that has several elements saved to it. I've tried experimenting with each_with_object, each_with_index, each and map.
class Person
attr_reader :name, :city, :state, :zip, :hobby
def initialize(name, hobby, city, state, zip)
#name = name
#hobby = hobby
#city = city
#state = state
#zip = zip
end
end
steve = Person.new("Steve", "basketball","Dallas", "Texas", 75444)
chris = Person.new("Chris", "piano","Phoenix", "Arizona", 75218)
larry = Person.new("Larry", "hunting","Austin", "Texas", 78735)
adam = Person.new("Adam", "swimming","Waco", "Texas", 76715)
people = [steve, chris, larry, adam]
people_array = people.map do |person|
person = person.name, person.hobby, person.city, person.state, person.zip
end
Now I just need to turn it into a hash. One issue I am having is, when I'm experimenting with other methods, I can turn it into a hash, but the array is still inside the hash. The expected output is just a nested hash with no arrays inside of it.
# Expected output ... create the following hash from the peeps array:
#
# people_hash = {
# "Steve" => {
# "hobby" => "golf",
# "address" => {
# "city" => "Dallas",
# "state" => "Texas",
# "zip" => 75444
# }
# # etc, etc
Any hints on making sure the hash is a nested hash with no arrays?
This works:
person_hash = Hash[peeps_array.map do |user|
[user[0], Hash['hobby', user[1], 'address', Hash['city', user[2], 'state', user[3], 'zip', user[4]]]]
end]
Basically just use the ruby Hash [] method to convert each of the sub-arrays into an hash
Why not just pass people?
people.each_with_object({}) do |instance, h|
h[instance.name] = { "hobby" => instance.hobby,
"address" => { "city" => instance.city,
"state" => instance.state,
"zip" => instance.zip } }
end
arr = [{name: 'one'}, {name: 'two'}, {name: 'one'}, {name: 'three'}]
How can I get an array of string from it, ["one", "two", "three"]. If each item in arr is considered a row, what I want is a simple ruby way of doing what following sql statement does:
select name from test group by name order by count(*) desc
This is the way I do in Rails currently, but I think that is too much coding.
tags_hash = arr.inject(Hash.new(0)) {|h,t| h[t.name]+=1; h}
descending = -1
tags_array = tags_hash.sort_by {|k,v| v * descending}
#tags = tags_array.collect {|item| item[0]}
arr.group_by { |i| i[:name] }.sort_by { |name, items| items.length }.reverse.map(&:first)
Group by groups the elements by name:
arr.group_by { |i| i[:name] }
# => {"one"=>[{:name=>"one"}, {:name=>"one"}], "two"=>[{:name=>"two"}], "three"=>[{:name=>"three"}]}
Sort by orders them by the number of found items in each, reverse makes it descending order.
The map extracts the actual name of each group.
# => ["one", "two", "three"]
If you want to get ["one", "two", "three"] sorted from that array you can write something like this:
counts = Hash.new(0)
arr.map{|i| i[:name]}.each { |name| counts[name] += 1 }
counts.sort{|a,b| b[1] <=> a[1]}.map(&:first)
[
{
"name": "John Doe",
"location": {
"name": "New York, New York",
"id": 12746342329
},
"hometown": {
"name": "Brooklyn, New York",
"id": 43453644
}
},
{
"name": "Jane Doe",
"location": {
"name": "Miami, Florida",
"id": 12746342329
},
"hometown": {
"name": "Queens, New York",
"id": 12746329
}
}
]
Given this piece of JSON, how would I be able to loop through and pull out all of the "hometown" and "location" keys and see which people had the value of New York?
My issue is I can Array.each through these items, but I don't know how to traverse both location && hometown with my criteria ("New York").
people.select {|person|
person.any? {|k, v|
%w[location hometown].include?(k) && /New York/ =~ v['name']
}}
This basically says the following: select all entries in the array for which the following condition is true. The condition is: is it true for any of the key-value pairs that the key is either 'hometown' or 'location' and the name property of the value belonging to that key matches the Regexp /New York/?
However, your object model seems to be in a serious need of refactoring. In fact, the main problem is that your object model isn't even an object model, it's a hash and array model.
Here's what I mean by a proper object model:
class Person
attr_reader :name, :location, :hometown
def initialize(name, location=nil, hometown=nil)
#name, #location, #hometown = name, location, hometown
end
def cities
return #location, #hometown
end
end
class City
attr_reader :id, :name
def initialize(id, name)
#id, #name = id, name
end
def =~(other)
name =~ other
end
end
nyc = City.new(12746342329, 'New York, New York')
brooklyn = City.new(43453644, 'Brooklyn, New York')
miami = City.new(12746342329, 'Miami, Florida')
queens = City.new(12746329, 'Queens, New York')
john = Person.new('John Doe', nyc, brooklyn)
jane = Person.new('Jane Doe', miami, queens)
people = [john, jane]
If you have such a proper object model, your code becomes much cleaner, because instead of teasing apart the nuts of bults of a nested maze of hashes and arrays, you have nice little objects that you can simply ask some questions:
people.select {|person| person.cities.any? {|city| city =~ /New York/ }}
You can almost read this like English: from the array select all people for which any of their cities matches the Regexp /New York/.
If we improve the object model further, it gets even better:
class Person
def lived_in?(pattern)
cities.any? {|city| city =~ pattern }
end
end
people.select {|person| person.lived_in?(/New York/) }
This basically says "From the people, select the ones which at one time lived in New York". That's much better than "from the people select all for which the first element of the key value pair is either the string 'hometown' or the string 'location' and the second element of the key value pair matches the Regexp /New York/".
I think Jörg's solution has a minor bug - 'location' and 'hometown' are not used, so, for example, following "person" would pass the test:
{
'name' => 'Foo Bar',
'favourite movie' => {
name => 'New York, New York!'
}
}
Here's a shot at correcting it, along with comments:
ary.select {|person| # get me every person satisfying following condition
%w[location hometown].any? {|key| # check if for any of the strings 'location' and 'hometown'
# person should have that key, and its 'name' should contain /New York/ regexp
person[key] && person[key]['name'] && /New York/ =~ person[key]['name']
}}