Rails Sort objects by sum of array - ruby-on-rails

Each of my users has an array of integers. The users attend a competition and I would like to sort them by the sum of the first 7 elements of their array. I know how to do each part individually, but I'm not sure how to put it together and display it in the show.
inside the def show
#competition.users.sort_by{|e| e.daily.first(7).sum}
for comp in #competition.users
p comp.name
p comp.daily #Their array
end
This is how I get the sum of the first 7 elements (currently used in my view):
user.daily.first(7).sum
thanks

This would sort the users in the descending order of their sum. User with highest sum will be in the top. Remove the - for ascending sort
#sorted_users = #competition.users.sort_by{|user| -user.daily.first(7).sum}
for comp in #sorted_users
p comp.name
p comp.daily #Their array
end
Or you can use sort_by! which will do inplace sorting
#competition.users.sort_by!{|user| -user.daily.first(7).sum}
for comp in #competition.users
p comp.name
p comp.daily #Their array
end

Your code was right but you didnt store sorted data anywhere
#competition.users.sort_by{|e| e.daily.first(7).sum}.each do |comp|
p comp.name
p comp.daily #Their array
end

Related

Forcing a particular order in Rails model scope

I have a Model M in my rails code. It has a field F which can have 4 values D, J, M and Z
If I use a scope like this, it would sort data by field F alphabetically:
default scope {order (F: :asc)}
I have 2 questions here:
I don't want to sort data alphabetically on F. I would like data to be displayed with this particular order of F instead. I want ALWAYS the records containing value M for field F first, follow by records having value J, D and then Z in this order. How can I achieve this?
Suppose I would like to display records having J first and then sort the rest of the records by alphabetical order of field F, how can I do that?
You can sort with a CASE statement
order("CASE WHEN F = 'M' THEN 0 WHEN F = 'J' THEN 1 WHEN F = 'D' THEN 2 ELSE 3 END")
Alternatively (if it's only "M" that needs to be first and the rest can be alphabetical)
order("CASE WHEN F = 'M' THEN 0 ELSE 1 END, F")

Ruby on Rails - select where ALL ids in array

I'm trying to find the cleanest way to select records based on its associations and a search array.
I have Recipes which have many Ingredients (through a join table)
I have a search form field for an array of Ingredient.ids
To find any recipe which contains any of the ids in the search array, I can use
eg 1.
filtered_meals = Recipe.includes(:ingredients).where("ingredients.id" => ids)
BUT, I want to only match recipes where ALL of it's ingredients are found in the search array.
eg 2.
search_array = [1, 2, 3, 4, 5]
Recipe1 = [1, 4, 5, 6]
Recipe2 = [1, 3, 4]
# results => Recipe2
I am aware that I can use an each loop, something like this;
eg 3.
filtered_meals = []
Recipes.each do |meal|
meal_array = meal.ingredients.ids
variable = meal_array-search_array
if variable.empty?
filtered_meals.push(meal)
end
end
end
return filtered_meals
The problem here is pagination. In the first example I can use .limit() and .offset() to control how many results are shown, but in the third example I would need to add an extra counter, submit that with the results, and then on a page change, re-send the counter and use .drop(counter) on the each.do loop.
This seems way too long winded, is there any better way to do this??
Assuming you are using has_many through & recipe_id, ingredient_id combination are unique.
recipe_ids = RecipeIngredient.select(:recipe_id)
.where(ingredient_id: ids)
.group(:recipe_id)
.having("COUNT(*) >= ?", ids.length)
filtered_meals = Recipe.find recipe_ids
How about
filtered_meals = Recipe.joins(:ingredients)
.group(:recipe_id)
.order("ingredients.id ASC")
.having("array_agg(ingredients.id) = ?", ids)
You'll need to make sure your ids parameter is listed in ascending order so the order of the elements in the arrays will match too.
Ruby on Rails Guide 2.3.3 - Subset Conditions
Recipe.all(:ingredients => { :id => search_array })
Should result in:
SELECT * FROM recipes WHERE (recipes.ingredients IN (1,2,3,4,5))
in SQL.
Would the array & operator work for you here?
Something like:
search_array = [1, 2, 3, 4, 5]
recipe_1 = [1, 4, 5, 6]
recipe_2 = [1, 3, 4]
def contains_all_ingredients?(search_array, recipe)
(search_array & recipe).sort == recipe.sort
end
contains_all_ingredients(search_array, recipe_1) #=> false
contains_all_ingredients(search_array, recipe_2) #=> true
This method compares the arrays and returns only the elements present in both, so if the result of the comparison equals the recipe array, all are present. (And obviously you could have a little refactor to have the method sit in the recipe model.)
You could then do:
Recipes.all.select { |recipe| contains_all_ingredients?(search_array, recipe) }
I'm not sure it passes your example three, but might help on your way? Let me know if that starts off OK, and I'll have more of a think in the meantime / if it's useful :)
I had a similar need and solved it using the pattern below. This is what the method looks like in my Recipe model.
def self.user_has_all_ingredients(ingredient_ids)
# casts ingredient_ids to postgres array syntax
ingredient_ids = '{' + ingredient_ids.join(', ') + '}'
return Recipe.joins(:ingredients)
.group(:id)
.having('array_agg(ingredients.id) <# ?', ingredient_ids)
end
This returns every recipe where all of the required ingredients are included in an ingredients array.
The Postgres '<#' operator was the magic solution. The array_agg function creates an array of each recipe's ingredient ids and then the left-pointing bird operator asks whether all of the unique ids in that array are contained in the array on the right.
Using the array_agg function required me to cast my search_array into Postgres syntax.
My Recipes model has many Ingredients through Portions.
I'd love to know if anyone has any better optimizations or knows how to avoid the casting to Postgres syntax that I needed to do.

How to merge a Redis hash and a Rails record?

For example, I have a shopping cart(redis hash) which contains a product id and a count:
{"165"=>"2", "166"=>"3"}
How do I find all products with these ids?
I think something like that:
1) Product.where(id: hash.keys) , but then I lose the count.
2) I can iterate the hash:
#products = []
hash.each do |id, count|
product = Product.find(id)
#products << product
end
But I don't know how to add count param in the product record and I think this approach is inefficient, because I'll get O(N) queries.
How to solve this problem?
You can get an array of arrays with two elements, where the first is the product and the second is the count:
Product.find(hash.keys).index_by(&:id).values_at(*hash.keys).zip(hash.values)
You can also add .to_h at the end if you want it to be a hash.
I would really not recommend this, but if you absolutely want the count to be added to the Products themselves, you could use the result of the above and eigenclasses:
result.map do |product, count|
product.define_singlethon_method(:count) { count }
product
end

Best way to order records by predetermined list

In order to keep things simple I have avoided using an enum for an attribute, and am instead storing string values.
I have a list of all possible values in a predetermined order in the array: MyTypeEnum.names
And I have an ActiveRecord::Relation list of records in my_recs = MyModel.order(:my_type)
What is the best way to order records in my_recs by their :my_type attribute value in the order specified by the array of values in MyTypeEnum.names ?
I could try this:
my_recs = MyModel.order(:my_type)
ordered_recs = []
MyTypeEnum.names.each do |my_type_ordered|
ordered_recs << my_recs.where(:my_type => my_type_ordered)
end
But am I killing performance by building an array instead of using the ActiveRecord::Relation? Is there a cleaner way? (Note: I may want to have flexibility in the ordering so I don't want to assume the order is hardcoded as above by the order of MyTypeEnum.names)
You are definitely taking a performance hit by doing a separate query for every item in MyTypeEnum. This requires only one query (grabbing all records at once).
ordered_recs = Hash[MyTypeEnum.names.map { |v| [v, []] }]
MyModel.order(:my_type).all.each do |rec|
ordered_recs[rec.my_type] << rec
end
ordered_recs = ordered_recs.values.flatten
If MyTypeEnum contains :a, :b, and :c, ordered_recs is initialized with a Hash of Arrays keyed by each of the above symbols
irb(main):002:0> Hash[MyTypeEnum.names.map { |v| [v, []] }]
=> {:a=>[], :b=>[], :c=>[]}
The records are appended to the proper Array based on it's key in the Hash, and then when all have bene properly grouped, the arrays are concatenated/flattened together into a single list of records.

rails active record traversing rows

I am accessing a database table with the .where() method. This should return many rows. How can I view the first row, or the second row. I know I can use the .each method to traverse all rows but what if I just want to access a certain row. I am new to rails, so sorry for this simple question.
To get the first row, you can just use .first
Model.where(:state => "active").first
.last works the same way:
Model.where(:state => "active").last
To get the nth row, use [] with a 0-based index, as with any other array
Model.where(:state => "active")[1] #second result
Once you have your set of results you can just reference individual rows with the [] method.
http://www.ruby-doc.org/core-1.9.3/Array.html#method-i-5B-5D
results = YourClass.where(:foo => 'bar').all
results[0] # the first result
results[1] # the 2nd
# and so on until
results[results.length - 1] # the last item in the array
results[results.length] # out of bounds, and returns nil
# you can also use negative numbers to count backwards
results[-1] # the last element
results[-2] # the 2nd from last

Resources