I created an array in a rails runner(this is not a model and has no attributes) like:
name_1 = 5
name_2 = 14
name_3 = 26
name_4 = 3
...
#names = [name_1, name_2, name_3, name_4, name_5]
Each "name_x" has an integer stored as its value.
How can I order the array so that it orders the output [highest => lowest] by the current values, and also shows the "name" [key, value] ?.
Currently,
puts #names
shows only the values with no order. Tks for pointing me in the right direction, theres many posts relating arrays but most asume its a model with attributes where you can say to order by the attribute. how do you order when you have no specific attributes like in this scenario?
If you want names (keys) and values, use a Hash.
#names = {name_1: 5, name_2: 14, name_3: 26, name_4: 3}
A Hash is Enumerable, so you can sort it. However sorting results in an array, but its easy to make that a Hash again, and hashes in Ruby maintain the order of insertion.
#names.sort_by{|k,v| v}
[[:name_4, 3], [:name_1, 5], [:name_2, 14], [:name_3, 26]]
#names.sort_by{|k,v| v}.to_h
{:name_4=>3, :name_1=>5, :name_2=>14, :name_3=>26}
The natural sort order is ascending (smallest to largest), but you can just negate the sort_by value, or reverse the resulting array.
#names.sort_by{|k,v| -v}.to_h
{:name_3=>26, :name_2=>14, :name_1=>5, :name_4=>3}
#names.sort_by{|k,v| v}.reverse.to_h
{:name_3=>26, :name_2=>14, :name_1=>5, :name_4=>3}
Related
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.
I have a Meal model with an ingredients attribute. The ingredients attribute is a multidimensional array eg. ingredients: => [[ 1, 5, "g"], [ 2, 5,"ml"], [3, 10,"tsp"]]. The first value in each inner array is an ingredient id.
My search params give an array of ingredient ids, eg. ingredients"=>["3", "67"]
I know I can iterate through all the meals and compare with the search parameters with .each do etc, but I would like to use .where so I can better limit results. Is this possible?
I'm not sure how to select the first item of each inner array to compare with the search parameters. I have tried a few variations but either get formatting errors or no results.
If search = ["3", "67"] (example ingredient ids)
Meal.where('ingredients IN (?)', "#{search}") (No results)
Meal.where('ingredients[][0] IN (?)', "#{search}") (This gives an error)
I would like to sort objects, but i want this sorting to not be based on the direct value i have stored in the database.
In the database there are integer values, 1,2,3... but there is also a hash, that specifies, what those values mean.
{1 => "a", 2 => "za", 3 => "xa"}.
So if an instance has value 3, it should be sorted as "xa". Can I achieve this goal with order() method? It is important to not use arrays, but rather ActiveRecord Relations
For the making of a temp_table and populate it with the values of your hash, this should work.
sqlQuery1 = ActiveRecord::Base.connection.execute("CREATE TEMP TABLE IF NOT EXISTS hash_tmp(id integer, hash_values integer)")
sqlQuery2 = ActiveRecord::Base.connection.execute(TRUNCATE hash_tmp)
Your_hash.each do |id, value|
sqlQuery3 = ActiveRecord::Base.connection.execute("INSERT INTO hash_tmp(id, hash_values) VALUES (#{id}, '#{value}')")
end
You should have a model for that temp table you created, then you can get all the values (filtered or not) and order by the new field:
#YourTemp = Yourtemp_model.all(:order=> "hash_values")
This is assuming that to this point your hash holds all the possible values which means you can iterate it and populate your temp table with them.
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.
I have a table of Logs with the columns name, duration, type, ref_id.
I update the table every so often so perhaps it will look like a col of ['bill', 'bob', 'bob', 'jill'] for names, and [3, 5, 6, 2] for duration, and ['man', boy', 'boy', 'girl'] for type, and [1, 2, 2, 3] for ref_id.
I would like to manipulate my table so that I can add all the durations so that I get a hash or something that looks like this:
{'name' => ['bill', 'bob', 'jill'], 'duration' => [3, 11, 2], 'type' => ['man', 'boy', 'girl'], ref_id => [1, 2, 3]}
How can I do this?
(for more info--currently I'm doing Log.sum(:duration, :group => 'name') which gives me the names themselves as the keys (bill, bob, jill) instead of the column name, with the correct duration sums as their values (3, 11, 2). but then I lose the rest of the data...)
I guess I could manually go through each log and add the name/type/ref_id if it's not in the hash, then add onto the duration. If so what's the best way to do that?
If you know of good sources on rails array manipulation/commonly used idioms, that would be great too!
Couple of notes first.
Your table is not properly normalized. You should split this table into (at least) two: users, and durations. You should do this for lots of reasons, that's relational databases 101.
Also, the hash you want as a result also doesn't look right, it suggests that you are pre-grouping data to suit your presentation. It's usually more logical to put these results in an array of hashes, than in a hash of arrays.
Now on to the answer:
With your table, you can simply do GROUP BY:
SELECT name, type, ref_id, SUM(duration) as duration
FROM logs
GROUP BY name, type, ref_id
or, using AR:
durations = Log.find(:all,
:select => 'name, type, ref_id, SUM(duration) as duration',
:group => 'name, type, ref_id'
)
In order to convert this to a hash of arrays, you'd use something like:
Hash[
%w{name, type, ref_id, duration}.map{|f|
[f, durations.map{|h|
h.attributes[f]
}]
}
]
Maybe all you need is something like this that spins through all the log entries and collects the results:
# Define attributes we're interested in
operate_on = %w[ name duration type ref_id ]
# Create a new hash with placeholder hashes to collect instances
summary = Hash[operate_on.map { |k| [ k, { } ] }]
Log.all.collect do |log|
operate_on.each do |attr|
# Flag this attribute/value pair as having been seen
summary[attr][log.send(attr)] = true
end
end
# Extract only the keys, return these as a hash
summary = Hash[summary.map { |key, value| [ key, value.keys ] }]
A more efficient method would be to do this as several SELECT DISTINCT(x) calls instead of instancing so many models.
Didn't quite understand if you want to save records from your hash, or you want to query the table and get results back in this form. If you want to get a hash back, then this should work:
Log.all.inject({}) do |hash, l|
hash['name'] ||= []
hash['duration'] ||= []
hash['type'] ||= []
hash['ref_id'] ||= []
hash['name'] << l.name
hash['duration'] << l.duration
hash['type'] << l.type
hash['ref_id'] << l.ref_id
hash
end