Array manipulation (summing one column by certain group, keeping others the same) - ruby-on-rails

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

Related

Rails query, require all conditions in array

I have two models, both associated with each other through has_many through.
I can query the model and filter based on its associated records:
Car.includes(:equipment).where(equipment: { id: [1, 2, 3] })
The problem is that I want to require all of those records, rather than requiring just one of them.
Is there a way to build a query that requires all of the values in the array (the [1, 2, 3] from the above example).
In other words, I'd like to query for all cars that have all three equipment (ids of 1, 2 and 3).
Assuming you have an id_ary like [1,2,3], how about something like:
id_ary.each_with_object(Car.includes(:equipment)) do |id, scope|
scope.where(equipment: {id: id})
end
Looping like that should and your where conditions, I believe.

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.

In a rails 3.2 app, how can I populate an array in a controller with a collection defined in a model?

In a view have a collection for a pull down menu (using the simple form gem):
<%= f.input :item, :collection => MyModel::CATEGORY %>
The collection is populated in my model:
CATEGORY = [["Category", 5], ["Another", 4], ["This", 3], ["That", 2], ["Foo", 1]]
Rather than recreating it, I'd like to use it also in my controller:
#category = []
#category << ProductDevelopment::CATEGORY
What I would really like is an array populated like this:
#category[1] = "Foo"
#category[2] = "That"
#category[3] = "This"
#category[4] = "Another"
#category[5] = "Category"
Searched but can't seem to locate the proper syntax to do this. Thanks for your help.
#category = MyModel::CATEGORY.sort{|x,y| x[1] <=> y[1]}.map{|c| c[0]}
The sort part simply sorts your categories based on the number associated to them, then the map creates a new array only containing the category names.
I suggest you do not do this.
Use a enumeration gem like those: https://www.ruby-toolbox.com/categories/Active_Record_Enumerations
IMO is more testable, reusable and maintainable.
Given CATEGORIES = [ ['a', 1], ['b', 2], ... ] with items in any order where the elements represent categories with elements [category, index], you have two choices of data structure from which to select.
An Array where you can look up categories by index
Be careful with this choice. As you'll necessarily have a 0 index for which you'll need to account out of the box.
# Mix things up for kicks
categories = CATEGORIES.shuffle
categories.inject([]){|memo,cat| memo[cat[1]] = cat[0]; memo }
# => [nil, 'a', 'b']
You'll also need to worry about nil entries if you ever choose to remove a category. Say you delete ['d', 4]. You'd now see the following:
# => [nil, 'a', 'b', 'c', nil, 'e', ...]
A Hash where you can look up the categories by key
This will avoid the whole issue with nil values will occur when using arrays:
# Mix things up for kicks
categories = CATEGORIES.shuffle
categories.inject({}){ |memo,cat| memo.merge!({cat[1]=>cat[0]}) }
# => {2=>'a', 1=>'b'}
I'd personally recommend the Hash, and as far as I know, hashes play well with collections in most Rails form builders.
#category = MyModel::CATEGORY.map{ |arr| arr[0] }
**Not tested

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.

Get sorted most common objects from an array of hashmaps in Ruby on Rails

I'm looking to get the sorted most common results from an array containing hashmaps. The hashmap data is non-numerical so:
line_value = {'date' => date, 'name' => name, 'url' => url }
where I can grab the most common urls. I considered using SQL to grab the counts, sort them and be done with it, but I think there is probably a faster way to do it in straight ruby since the array and hashmaps are not in a database and would need to be put there to begin with.
So I'm looking for non-SQL methods to do this. Note, I'm not just looking for the most common result (singular) but the top 5 or 10 common results.
How about
most_common_urls = line_value['url'].sort[0..9]
Change
[0..5]
to whatever number you need.
The first thing to do is to build up a count of the unique urls in your array. I much prefer each_with_object to inject for this (you don't have to return the hash at each step):
url_count = items.each_with_object(Hash.new(0)) do |item, count|
count[item['url']] += 1
end
# => {'example.com' => 1, 'facebook.com' => 4, 'twitter.com' => 2, ...}
Then you want to turn this into is an array of the keys, sorted by the values. Using Array#sort_by will do quite nicely, but it sorts in ascending order. You could take the last N items, and reverse them:
top_urls = url_count.keys.sort_by!{|url| url_count[url]}.last(5).reverse!
or you could negate the count so that the highest numbers are sorted to the front:
top_urls = url_count.keys.sort_by!{|url| -url_count[url]}.first(5)
urls.map {|u| u["url"]}.inject(Hash.new(0)) {|k,v| k[v] += 1; k}.rank.sort_by {|k,v| v}.last(5).reverse
Or:
urls.group_by {|k|{ :u => k["url"], :q => 0}}.map {|k,v| k[:q] = v.count; k}.sort_by {|k| k[:q]}.last(5).reverse

Resources