Sorting a multidimensional array with ruby - ruby

Im having trouble sorting a multidimensional array in ruby and can't find any question similar to my problem. I have an array/hash or both? (excuse me as im coming from a c/php/java background and this is my first time using Ruby)
user['shapeshifter'] = {age => '25', country => 'Australia'}
user['user2'] = {age => '29', country => 'Australia'}
user['user3'] = {age => '21', country => 'Russia'}
i want to sort the user array based on age.

You need a hash of hashes, and ruby 1.9.2 for sorted hashes, IIRC. This was covered in Sort hash by key, return hash in Ruby
Assuming your test case, fixed so it is valid:
user = {}
user['shapeshifter'] = {:age => 25, :country => 'Australia'}
user['user2'] = {:age => 29, :country => 'Australia'}
user['user3'] = {:age => 21, :country => 'Russia'}
All it takes is:
user.sort_by {|key,value| value[:age]}

Currently ruby 1.9 has ordered hash but still does not exist reordering function.
You can try sort pairs of arrays and than make a new Hash.
Like this
user = {}
user['shapeshifter'] = {:age => '25', :country => 'Australia'}
user['user2'] = {:age => '29', :country => 'Australia'}
user['user3'] = {:age => '21', :country => 'Russia'}
result1 = user.sort { |user1, user2|
user1[1][:key] <=> user2[1][:key] # user1,2 = [key, value] from hash
}
puts Hash[result1].inspect
or this
result2 = user.sort_by { |user_key, user_val|
user_val[:key]
}
puts Hash[result2].inspect

Related

I need to convert a string to an integer within a hash

I have a nested hash and I need to return the inside hash, which is a value of the key. The problem is one of the values is a string and it needs to be returned as an integer.
def player_stats(player_name)
game_hash.keys.each do |data|
if game_hash[data][:players].keys.include?(player_name)
return game_hash[data][:players][player_name]
end
end
end
game_hash = {
:home => {team_name:"Brooklyn Nets", colors:["Black", "White"],
:players => {"Alan Anderson" => {:number => 0, :shoe => "16",
:points => 22, :rebounds => 12, :assists =>12, :steals => 3,
:blocks => 1, :slam_dunks => 1},
The code is correct, the only thing is that I need to input a line that will convert the string**(:shoe)** to an integer.
game_hash = {
:home => {:team_name =>"Brooklyn Nets",
:colors => ["Black", "White"],
:players => {
"Alan Anderson" => {
:number => 0,
:shoe => "16",
:points => 22,
}
}
}
}
In this case, assuming you do not want to mutate (modify) the original hash, it's easiest to first make a deep copy of game_hash:
h = Marshal.load(Marshal.dump(game_hash))
#=> {:home=>{:team_name=>"Brooklyn Nets", :colors=>["Black", "White"],
# :players=>{"Alan Anderson"=>{:number=>0, :shoe=>"16", :points=>22}}}}
See Marshall::load and Marshall::dump.
Then just modify the value of interest:
h[:home][:players]["Alan Anderson"][:shoe] =
h[:home][:players]["Alan Anderson"][:shoe].to_i
h #=> {:home=>{:team_name=>"Brooklyn Nets", :colors=>["Black", "White"],
# :players=>{"Alan Anderson"=>{:number=>0, :shoe=>16, :points=>22}}}}
Lastly, let's confirm the original hash was not mutated:
game_hash
#=> {:home=>{:team_name=>"Brooklyn Nets", :colors=>["Black", "White"],
# :players=>{"Alan Anderson"=>{:number=>0, :shoe=>"16", :points=>22}}}}
Following your code, just a couple of changes (see inline comments in the option below).
Given the game_hash, which could have integers as string somewhere:
game_hash = {:home => { team_name:"Brooklyn Nets", colors:["Black", "White"],:players => {"Alan Anderson" => {:number => "0", :shoe => "16", :points => 22, :rebounds => 12, :assists =>12, :steals => 3, :blocks => 1, :slam_dunks => 1},"Reggie Evans" => {:number => "30",:shoe => "14",:points => 12, :rebounds => 12, :assists => 12, :steals => 12, :blocks => 12, :slam_dunks => 7}}}}
This is the option:
def player_stats(player_name, game_hash) # <-- pass game_hash as parameter
game_hash.keys.each do |data|
if game_hash[data][:players].keys.include?(player_name)
return game_hash[data][:players][player_name].transform_values { |v| v.to_i } # <-- transform values to integer
end
end
end
Then, you can call:
player_stats("Reggie Evans", game_hash)
#=> {:number=>30, :shoe=>14, :points=>12, :rebounds=>12, :assists=>12, :steals=>12, :blocks=>12, :slam_dunks=>7}
There are a few "issues" that I would like to point out in your #player_stats method, but I will focus on your problem with converting the player hash values to integers. To start off, I am making the following assumptions:
There is no consistency in whether the values are formatted as a string or integer.
All values are of integer form, in either the Integer type, or String type.
One thing I would like to point out is that shoe size could be 9.5 or 10.5, etc. This could be the reason why the shoe value is formatted as a string. If this is the case, then you would need to take this into account and convert to a float using #to_f instead of #to_i. But since your question is asking for it to be an integer, I will use #to_i in my examples below.
Given my assumptions, you would simply loop over all the values in the player hash and call #to_i on the value to convert it to an integer. This can be done using the following method:
def convert_hash_values_to_int(hash)
hash.each do |key, value|
hash[key] = value.to_i
end
end
You can call this method inside your #player_stats method. Note that this method is mutating the original hash values in place without creating a new hash. If this is not desirable, then you should use #inject:
def convert_hash_values_to_int(hash)
hash.inject({}) do |result, (key, value)|
result[key] = value.to_i
result
end
end
This creates a new hash with the transformed values, and returns it.
This can be simplified even further using #each_with_object, which also creates a new hash instead of mutating the original hash:
def convert_hash_values_to_int(hash)
hash.each_with_object({}) do |(key, value), result|
result[key] = value.to_i
end
end
This approach does not require that you return result in each loop. Note that the arguments are switched for #each_with_object - |(key, value), result| instead of |result, (key, value)|.
There is one more approach that you could use which is the most succinct, but it is only available in Ruby 2.4+:
def convert_hash_values_to_int(hash)
hash.transform_values do |value|
value.to_i
end
end
Which can be even more succinct:
def convert_hash_values_to_int(hash)
hash.transform_values(&:to_i)
end
#transform_values does not mutate the original hash. If you would like to mutate the original hash, you would need to use a bang:
def convert_hash_values_to_int(hash)
hash.transform_values!(&:to_i)
end

Ruby converting an array to a hash

I have the following string and I would like to convert it to a hash printing the below result
string = "Cow, Bill, Phone, Flour"
hash = string.split(",")
>> {:animal => "Cow", :person: "Bill", :gadget => "Phone",
:grocery => "Flour"}
hash = Hash[[:animal, :person, :gadget, :grocery].zip(string.split(/,\s*/))]
The answer by #Max is quite nice. You might understand it better as:
def string_to_hash(str)
values = str.split(/,\s*/)
names = [:animal, :person, :gadget, :grocery]
Hash[names.zip(values)]
end
Here is a less sophisticated approach:
def string_to_hash(str)
parts = str.split(/,\s*/)
Hash[
:animal => parts[0],
:person => parts[1],
:gadget => parts[2],
:grocery => parts[3],
]
end

New hash from array of hashes

The objective of the code below is to produce a hash with the keys being the :id field of
the hashes in original_array, and the values being all elements in original_array which have that :id.
original_array = [
{:id => '123', :name => 'test'},
{:id => '123', :name => 'another test'},
{:id => '456', :name => 'yet another test'}
]
new_hash = {}
original_array.each do |a|
new_hash[a[:id]] = original_array.select {|x| x[:id] == a[:id]}
end
My code does that, but there must be some better way to do it, ideally where the hash can be created in one step. If anyone can suggest and explain one (in the hope that I might improve my understanding of this sort of thing), then it would be appreciated.
This should do it
new_hash = original_array.group_by{|h| h[:id]}
Documentation: Enumerable#group_by.

Merge hashes based on particular key/value pair in ruby

I am trying to merge an array of hashes based on a particular key/value pair.
array = [ {:id => '1', :value => '2'}, {:id => '1', :value => '5'} ]
I would want the output to be
{:id => '1', :value => '7'}
As patru stated, in sql terms this would be equivalent to:
SELECT SUM(value) FROM Hashes GROUP BY id
In other words, I have an array of hashes that contains records. I would like to obtain the sum of a particular field, but the sum would grouped by key/value pairs. In other words, if my selection criteria is :id as in the example above, then it would seperate the hashes into groups where the id was the same and the sum the other keys.
I apologize for any confusion due to the typo earlier.
Edit: The question has been clarified since I first posted my answer. As a result, I have revised my answer substantially.
Here are two "standard" ways of addressing this problem. Both use Enumerable#select to first extract the elements from the array (hashes) that contain the given key/value pair.
#1
The first method uses Hash#merge! to sequentially merge each array element (hashes) into a hash that is initially empty.
Code
def doit(arr, target_key, target_value)
qualified = arr.select {|h|h.key?(target_key) && h[target_key]==target_value}
return nil if qualified.empty?
qualified.each_with_object({}) {|h,g|
g.merge!(h) {|k,gv,hv| k == target_key ? gv : (gv.to_i + hv.to_i).to_s}}
end
Example
arr = [{:id => '1', :value => '2'}, {:id => '2', :value => '3'},
{:id => '1', :chips => '4'}, {:zd => '1', :value => '8'},
{:cat => '2', :value => '3'}, {:id => '1', :value => '5'}]
doit(arr, :id, '1')
#=> {:id=>"1", :value=>"7", :chips=>"4"}
Explanation
The key here is to use the version of Hash#merge! that uses a block to determine the value for each key/value pair whose key appears in both of the hashes being merged. The two values for that key are represented above by the block variables hv and gv. We simply want to add them together. Note that g is the (initially empty) hash object created by each_with_object, and returned by doit.
target_key = :id
target_value = '1'
qualified = arr.select {|h|h.key?(target_key) && h[target_key]==target_value}
#=> [{:id=>"1", :value=>"2"},{:id=>"1", :chips=>"4"},{:id=>"1", :value=>"5"}]
qualified.empty?
#=> false
qualified.each_with_object({}) {|h,g|
g.merge!(h) {|k,gv,hv| k == target_key ? gv : (gv.to_i + hv.to_i).to_s}}
#=> {:id=>"1", :value=>"7", :chips=>"4"}
#2
The other common way to do this kind of calculation is to use Enumerable#flat_map, followed by Enumerable#group_by.
Code
def doit(arr, target_key, target_value)
qualified = arr.select {|h|h.key?(target_key) && h[target_key]==target_value}
return nil if qualified.empty?
qualified.flat_map(&:to_a)
.group_by(&:first)
.values.map { |a| a.first.first == target_key ? a.first :
[a.first.first, a.reduce(0) {|tot,s| tot + s.last}]}.to_h
end
Explanation
This may look complex, but it's not so bad if you break it down into steps. Here's what's happening. (The calculation of qualified is the same as in #1.)
target_key = :id
target_value = '1'
c = qualified.flat_map(&:to_a)
#=> [[:id,"1"],[:value,"2"],[:id,"1"],[:chips,"4"],[:id,"1"],[:value,"5"]]
d = c.group_by(&:first)
#=> {:id=>[[:id, "1"], [:id, "1"], [:id, "1"]],
# :value=>[[:value, "2"], [:value, "5"]],
# :chips=>[[:chips, "4"]]}
e = d.values
#=> [[[:id, "1"], [:id, "1"], [:id, "1"]],
# [[:value, "2"], [:value, "5"]],
# [[:chips, "4"]]]
f = e.map { |a| a.first.first == target_key ? a.first :
[a.first.first, a.reduce(0) {|tot,s| tot + s.last}] }
#=> [[:id, "1"], [:value, "7"], [:chips, "4"]]
f.to_h => {:id=>"1", :value=>"7", :chips=>"4"}
#=> {:id=>"1", :value=>"7", :chips=>"4"}
Comment
You may wish to consider makin the values in the hashes integers and exclude the target_key/target_value pairs from qualified:
arr = [{:id => 1, :value => 2}, {:id => 2, :value => 3},
{:id => 1, :chips => 4}, {:zd => 1, :value => 8},
{:cat => 2, :value => 3}, {:id => 1, :value => 5}]
target_key = :id
target_value = 1
qualified = arr.select { |h| h.key?(target_key) && h[target_key]==target_value}
.each { |h| h.delete(target_key) }
#=> [{:value=>2}, {:chips=>4}, {:value=>5}]
return nil if qualified.empty?
Then either
qualified.each_with_object({}) {|h,g| g.merge!(h) { |k,gv,hv| gv + hv } }
#=> {:value=>7, :chips=>4}
or
qualified.flat_map(&:to_a)
.group_by(&:first)
.values
.map { |a| [a.first.first, a.reduce(0) {|tot,s| tot + s.last}] }.to_h
#=> {:value=>7, :chips=>4}

How do I add values from two different arrays of hashes together?

I have two arrays of hashes. The keys for the hashes are different:
player_scores1 = [{:first_name=>"Bruce", :score => 43, :time => 50},
{:first_name=>"Clark", :score => 45, :minutes => 20}]
player_scores2 = [{:last_name=>"Wayne", :points => 13, :time => 40},
{:last_name=>"Kent", :points => 3, :minutes => 20}]
I'd like to create a new array of hashes which adds up :score and :points together and assign it to a key called :score. I'd also like to combine the :first_name and :last_name and assign it to a key called :full_name. I want to discard any other keys.
This would result in this array:
all_players = [{:full_name => "Bruce Wayne", :score => 56},
{:full_name => "Clark Kent", :score => 48}]
Is there an elegant way to do this?
Something like this:
player_scores1.zip(player_scores2).map { |a,b|
{
:full_name => a[:first_name]+' '+b[:last_name],
:score => a[:score]+b[:points]
}
}
The code you're looking for is:
final = []
player_scores1.each_index do |index|
entry_1 = player_scores1.values(index)
entry_2 = player_scores2.values(index)[:first_name]
score = entry_1[:score] + entry_2[:points]
final << {:full_name => "#{entry_1[:first_name]} #{entry_2[:last_name]}", :score => score }
end
Any suggestions on tightening this up would be much appreciated!
This works. I don't if that's elegant enough though.
player_scores1 = [{:first_name=>"Bruce", :score => 43, :time => 50},
{:first_name=>"Clark", :score => 45, :minutes => 20}]
player_scores2 = [{:last_name=>"Wayne", :points => 13, :time => 40},
{:last_name=>"Kent", :points => 3, :minutes => 20}]
p (0...[player_scores1.length, player_scores2.length].min).map {|i| {
:full_name => player_scores1[i][:first_name] + " " + player_scores2[i][:last_name],
:score => player_scores1[i][:score] + player_scores2[i][:points]
}}
This example on Codepad.
This uses zip with a block to loop over the hashes, joining the names and summarizing:
all_players = []
player_scores1.zip(player_scores2) { |a, b|
all_players << {
:full_name => a[:first_name] + ' ' + b[:last_name],
:score => a[:score] + b[:points]
}
}
all_players # => [{:full_name=>"Bruce Wayne", :score=>56}, {:full_name=>"Clark Kent", :score=>48}]

Resources