ruby hash searching for two criterias - ruby-on-rails

grades = [
{:student=>"James", :age=>19, :score=>85},
{:student=>"Kate", :age=>19, :score=>92},
{:student=>"Sara", :age=>20, :score=>74},
{:student=>"Riley", :age=>20, :score=>85},
{:student=>"patrick", :age=>20, :score=>96},
{:student=>"luke", :age=>21, :score=>88},
{:student=>"susie", :age=>21, :score=>90}
]
I am trying to get the student with the highest score that is the age 20 but only can sort the highest by all the students. Does any one know how to limit the max_by to only students that are 20 from the above hash ?

filter for people with age 20
find the maximum score
grades.select { |person| person[:age] == 20 }.max_by { |person| person[:score] }

My objective is to make a single pass through the hashes.
Initially I posted the following, but it was incorrect because it returns the name of a student who is not 20 if there are no students of age 20.
grades.max { |h| h[:age] == 20 ? h[:score] :
-Float::INFINITY }[:student]
Here is my revised answer:
def best20(grades)
student = nil
highest = -Float::INFINITY
grades.each do |h|
if h[:age] == 20 && h[:score] > highest
highest = h[:score]
student = h[:student]
end
end
student
end
best20 grades
#=> "patrick"
best20 [{:student=>"James", :age=>19, :score=>85}]
#=> nil

Just as idea.
Suppose we have several 20-year-old students with the same maximum score.
In this case variants of Cary and Ursus return only first person.
Therefore, such a thought occurred to me:
grades = [
{:student=>"James", :age=>19, :score=>85},
{:student=>"Kate", :age=>19, :score=>92},
{:student=>"Sara", :age=>20, :score=>74},
{:student=>"Riley", :age=>20, :score=>85},
{:student=>"Patrick", :age=>20, :score=>96},
{:student=>"Vladimir", :age=>20, :score=>96},
{:student=>"Luke", :age=>21, :score=>88},
{:student=>"Susie", :age=>21, :score=>90}
]
def best20(grades)
students20 = grades.select { |r| r[:age] == 20 }
return nil if students20.empty?
max_score20 = students20.max_by { |r| r[:score] }[:score]
students20.select { |r| r[:score] == max_score20 }.map { |r| r[:student] }
end
best20 grades
# => ["Patrick", "Vladimir"]
best20 [{:student=>"James", :age=>19, :score=>85}]
# => nil
Do not judge strictly, this is just an idea.

Related

build a new array of hash from multiple array of hashes

I have following three array of hashes.
customer_mapping = [
{:customer_id=>"a", :customer_order_id=>"g1"},
{:customer_id=>"b", :customer_order_id=>"g2"},
{:customer_id=>"c", :customer_order_id=>"g3"},
{:customer_id=>"d", :customer_order_id=>"g4"},
{:customer_id=>"e", :customer_order_id=>"g5"}
]
customer_with_products = [
{:customer_order_id=>"g1", :product_order_id=>"a1"},
{:customer_order_id=>"g2", :product_order_id=>"a2"},
{:customer_order_id=>"g3", :product_order_id=>"a3"},
{:customer_order_id=>"g4", :product_order_id=>"a4"},
{:customer_order_id=>"g5", :product_order_id=>"a5"}
]
product_mapping = [
{:product_id=>"j", :product_order_id=>"a1"},
{:product_id=>"k", :product_order_id=>"a2"},
{:product_id=>"l", :product_order_id=>"a3"}
]
What i want is a new hash with only customer_id and product_id
{:product_id=>"j", :customer_id=>"a"},
{:product_id=>"k", :customer_id=>"b"},
{:product_id=>"l", :customer_id=>"c"}
I tried to loop over product_mapping and select the customer_order_id that match product_order_id in customer_with_products and then thought of looping over customer_mapping but not able to get desired output from the first step.
How can i achieve this?
Using
def merge_by(a,b, key)
(a+b).group_by { |h| h[key] }
.each_value.map { |arr| arr.inject(:merge) }
end
merge_by(
merge_by(customer_mapping, customer_with_products, :customer_order_id),
product_mapping,
:product_order_id
).select { |h| h[:product_id] }.map { |h| h.slice(:product_id, :customer_id) }
#=>[{:product_id=>"j", :customer_id=>"a"},
# {:product_id=>"k", :customer_id=>"b"},
# {:product_id=>"l", :customer_id=>"c"}]
Definitely not the cleanest solution, if your initial arrays come from SQL queries, I think those queries could be modified to aggregate your data properly.
merge_by(customer_mapping, customer_with_products, :customer_order_id)
# => [{:customer_id=>"a", :customer_order_id=>"g1", :product_order_id=>"a1"},
# {:customer_id=>"b", :customer_order_id=>"g2", :product_order_id=>"a2"},
# {:customer_id=>"c", :customer_order_id=>"g3", :product_order_id=>"a3"},
# {:customer_id=>"d", :customer_order_id=>"g4", :product_order_id=>"a4"},
# {:customer_id=>"e", :customer_order_id=>"g5", :product_order_id=>"a5"}]
Then merge it similarly with your last array and cleanup the result selecting only the elements for which :product_id was found, slicing wanted keys.
Alternatively, a much more readable solution, depending on your array sizes might be slower as it keeps iterating over the hashes:
product_mapping.map do |hc|
b_match = customer_with_products.detect { |hb| hb[:product_order_id] == hc[:product_order_id] }
a_match = customer_mapping.detect { |ha| ha[:customer_order_id] == b_match[:customer_order_id] }
[hc, a_match, b_match].inject(:merge)
end.map { |h| h.slice(:product_id, :customer_id) }
Following your handling of the problem the solution would be the following:
result_hash_array = product_mapping.map do |product_mapping_entry|
customer_receipt = customer_with_products.find do |customer_with_products_entry|
product_mapping_entry[:product_order_id] == customer_with_products_entry[:product_order_id]
end
customer_id = customer_mapping.find do |customer_mapping_entry|
customer_receipt[:customer_order_id] == customer_mapping_entry[:customer_order_id]
end[:customer_id]
{product_id: product_mapping_entry[:product_id], customer_id: customer_id}
end
Output
results_hash_array => [{:product_id=>"j", :customer_id=>"a"},
{:product_id=>"k", :customer_id=>"b"},
{:product_id=>"l", :customer_id=>"c"}]
Other option, starting from customer_mapping, one liner (but quite wide):
customer_mapping.map { |e| {customer_id: e[:customer_id], product_id: (product_mapping.detect { |k| k[:product_order_id] == (customer_with_products.detect{ |h| h[:customer_order_id] == e[:customer_order_id] } || {} )[:product_order_id] } || {} )[:product_id] } }
#=> [{:customer_id=>"a", :product_id=>"j"},
# {:customer_id=>"b", :product_id=>"k"},
# {:customer_id=>"c", :product_id=>"l"},
# {:customer_id=>"d", :product_id=>nil},
# {:customer_id=>"e", :product_id=>nil}]
cust_order_id_to_cust_id =
customer_mapping.each_with_object({}) do |g,h|
h[g[:customer_order_id]] = g[:customer_id]
end
#=> {"g1"=>"a", "g2"=>"b", "g3"=>"c", "g4"=>"d", "g5"=>"e"}
prod_order_id_to_cust_order_id =
customer_with_products.each_with_object({}) do |g,h|
h[g[:product_order_id]] = g[:customer_order_id]
end
#=> {"a1"=>"g1", "a2"=>"g2", "a3"=>"g3", "a4"=>"g4", "a5"=>"g5"}
product_mapping.map do |h|
{ product_id: h[:product_id], customer_id:
cust_order_id_to_cust_id[prod_order_id_to_cust_order_id[h[:product_order_id]]] }
end
#=> [{:product_id=>"j", :customer_id=>"a"},
# {:product_id=>"k", :customer_id=>"b"},
# {:product_id=>"l", :customer_id=>"c"}]
This formulation is particularly easy to test. (It's so straightforward that no debugging was needed).
I would recommended to rather take a longer but more readable solution which you also understand in some months from now by looking at it. Use full names for the hash keys instead of hiding them behind k, v for more complexe lookups (maybe its just my personal preference).
I would suggest somethink like:
result = product_mapping.map do |mapping|
customer_id = customer_mapping.find do |hash|
hash[:customer_order_id] == customer_with_products.find do |hash|
hash[:product_order_id] == mapping[:product_order_id]
end[:customer_order_id]
end[:customer_id]
{ product_id: mapping[:product_id], customer_id: customer_id }
end

Simplify code that adds dictionary value with highest count to an array

I have an array of individual animals, and an array of relevant species. I want to add the higher of cats and dogs to my array of relevant species. The array of individual animals may not have the requested species, in which case species_count returns {}.
species_count takes in an array of animals and groups them by species. Example:
animals = ['chihuaha', 'german_shepherd', 'golden_retriever', 'tabby cat', 'siamese cat'}
species_count(animals, DOG) = { species: 'dog', count: 3 }
species_count(animals, CAT) = { species: 'cat', count: 3 }
species_count(animals, MOUSE) = {}
The below can be improved, I think. Ruby has all sorts of magical methods that surprise me.
dogs = species_count(animals, DOG)
dog_count = dogs.fetch(:count, 0)
cats = species_count(animals, CAT)
cat_count = cats.fetch(:count, 0)
if dog_count >= cat_count && dog_count >= 3
relevant_species << dogs
elsif cat_count >= 3
relevant_species << cats
end
Something like this will probably be the way to do it:
Simplified code:
relevant_species = [DOG, CAT, MOUSE]
.map { |animal| species_count(animals, animal) }
.sort { |a, b| a[:count].to_i <=> b[:count].to_i }
.last
OR step by step:
# returns array of [{ species: 'dog', count: 3 }, ... ]
species_counts = [DOG, CAT, MOUSE].map { |animal| species_count(animals, animal) }
# sorts the array based on the count value. to_i is to account for nils
sorted_species_counts = species_counts.sort { |a, b| a[:count].to_i <=> b[:count].to_i }
# returns the last element (with the highest count value) to be assigned to relevant species
relevant_species = sorted_species_counts.last

Ruby on rails How to find first item in array that matches items in another array

I have a leaderboard array that looks something like this:
[{:member=>"1", :score=>7.0, :rank=>1}, {:member=>"5", :score=>6.0, :rank=>2}, {:member=>"4", :score=>5.0, :rank=>3}, {:member=>"3", :score=>4.0, :rank=>4}, {:member=>"2", :score=>3.0, :rank=>5}]
I also have an array of active user ids [3,5].
How can I get the member number of the highest ranked active user and assign that to a variable? The leaderboard array will always be in order of rank.
One method would be to reduce your array to only entries who's :member is also in the active user IDs array, and then take the first element of that array:
leaderboard = [...]
active_user_ids = [3,5]
leaderboard.take_while{ |m| active_user_ids.include?(m[:member].to_i) }.first
leaderboard = [{:member=>"1", :score=>7.0, :rank=>1},
{:member=>"5", :score=>6.0, :rank=>2},
{:member=>"4", :score=>5.0, :rank=>3},
{:member=>"3", :score=>4.0, :rank=>4},
{:member=>"2", :score=>3.0, :rank=>5}]
active_members = [3,5]
highest_ranked_active_member = leaderboard.
select { |h| active_members.include? h[:member].to_i }.
min_by { |h| h[:rank] }[:member]
#=> "5"
This is a case where I love creating reusable lambdas to reuse code when querying the data.
active_members = [3,5]
active_member = -> member { active_members.include? member[:member].to_i }
member_score = -> member { member[:score] }
leader_board = ...
# Find first active_member:
p leader_board.find(&active_member) #=> {:member=>"5", :score=>6.0, :rank=>2}
# Find active member with lowest and highest score:
p leader_board.select(&active_member).minmax_by(&member_score) #=> [{:member=>"3", :score=>4.0, :rank=>4}, {:member=>"5", :score=>6.0, :rank=>2}]
list = [
{:member=>"1", :score=>7.0, :rank=>1},
{:member=>"5", :score=>6.0, :rank=>2},
{:member=>"4", :score=>5.0, :rank=>3},
{:member=>"3", :score=>4.0, :rank=>4},
{:member=>"2", :score=>3.0, :rank=>5}
]
list.select { |item| [3,5].include? item[:member].to_i }.max { |item| item[:rank] }[:member] => "3"

how to group by in a array of hashes

I have a array of hashes
and I need to be able to group them by users.
order = Order.find(options[:order_id])
shipments = order.shipments.select {|shipment| shipment["roundtrip_shipment"] == nil }.collect {|o| {
id: o.id,
user_id: o.user_id,
...
} }
I am having some problems in making a group by in the array.
I have tried to do shipments = order.shipments.group_by { |shipment| shipment[:user_id] }.count
but this always returns 1 when I know that have 2 users here
Thanks for all the help
You can simply do as follow:
order = Order.find(options[:order_id])
shipments = order.shipments.where.not(roundtrip_shipment: nil).group_by(&:user_id).collect {|x,y| {x => y.map(&:id)} }

Searching and comparing ActiveRecord attributes to find largest value

I have a model that would look something like:
my_diet = Diet.new
my_diet.food_type_1 = "beef"
my_diet.food_type_1_percentage = 40
my_diet.food_type_2 = "carrots"
my_diet.food_type_2_percentage = 50
my_diet.food_type_3 = "beans"
my_diet.food_type_3_percentage = 5
my_diet.food_type_4 = "chicken"
my_diet.food_type_4_percentage = 5
I need to find which food_type has the highest percentage. So far I've tried creating a hash out of the attibutes and percentages then sorting the hash (see below) but it feels like there must be a cleaner way to do it.
food_type_percentages = { :food_type_1 => my_diet.foo_type_percentage_1_percentage.nil? ? 0 : my_dient.food_type_1_percentage,
:food_type_2 => my_diet.foo_type_percentage_2_percentage.nil? ? 0 : my_dient.food_type_2_percentage,
:food_type_3 => my_diet.foo_type_percentage_3_percentage.nil? ? 0 : my_dient.food_type_3_percentage,
:food_type_4 => my_diet.foo_type_percentage_4_percentage.nil? ? 0 : my_dient.food_type_4_percentage
}
food_type_percentages.sort {|a,b| a[1]<=>b[1]}.last
Any ideas?
Thanks!
To find the max value amongst columns of an existent row in the DB, do the following:
d = Diet.first(:select => "*, GREATEST(
food_type_1_percentage,
food_type_2_percentage,
food_type_3_percentage,
food_type_4_percentage) AS top_food_type_percentage,
CASE GREATEST(
food_type_1_percentage,
food_type_2_percentage,
food_type_3_percentage,
food_type_4_percentage)
WHEN food_type_1_percentage THEN food_type_1
WHEN food_type_2_percentage THEN food_type_2
WHEN food_type_3_percentage THEN food_type_3
WHEN food_type_4_percentage THEN food_type_4
END AS top_food_type")
d.top_food_type # carrots
d.top_food_type_percentage # 50
If you are trying to find the top food type in the current model instance then
class Diet < ActiveRecord::Base
def top_food_type
send(top_food_type_col)
end
def top_food_type_percentage
send("#{top_food_type_col}_percentage")
end
FOOD_TYPE_COL = %w(food_type_1 food_type_2 food_type_3 food_type_4)
def top_food_type_col
#top_food_type_col ||= FOOD_TYPE_COL.sort do |a, b|
send("#{a}_percentage") <=> send("#{b}_percentage")
end.last
end
end
Now you can do the following:
d = Diet.new
....
....
....
d.top_food_type # carrots
d.top_food_type_percentage # 50
I assume food_percentage is the column
if you just want to find out ref this
Diet.maximum('food_percentage') # gives 50
OR you want complete record use this
Diet.find(:first, :order=> 'food_percentage DESC', :limit=>1)

Resources