How to count occurrences of strings in objects - ruby-on-rails

I'm looking to group, sort and count the occurrences of titles of a selection articles. I have selected my group of articles as follows:
#articles = Article.find(:all, :where("distance < ?", specified_distance))
Here out, I would like to group, sort and count the article titles that are included in #articles (not of all articles) for use in a view (as follows):
New York Yankees (3 Articles)
Boston Red Sox (1 Article)
Chicago Cubs (8 Articles)
This is not through usage of a many-to-many, it strictly string comparison, grouping, counting. I'm not sure how it is to be done. Is there a standard practice for doing this in Rails?
This is similar to what this user is asking for, but slightly different:
rails sorting blog posts by title
EDIT/
I must use #articles only (and not the physical query displayed above) because the query will often change and is much more complicated beyond that. So, my solution must refer to/from #articles.

In model:
scope :articles_up_to, lambda { |distance| where("distance < ?", distance) }
scope :article_counts_up_to, lambda { |distance|
articles_up_to(distance)
.select("COUNT(*) AS count, title")
.group("title")
.order("count DESC")
}
In controller:
#article_counts = Article.article_counts_up_to(specified_distance)
Edit:
This article on scopes may be helpful: http://edgerails.info/articles/what-s-new-in-edge-rails/2010/02/23/the-skinny-on-scopes-formerly-named-scope/index.html

Ok, I understand you want to use #articles, so briefly, here's a (really messy) solution which doesn't involve changing the model, or building new SQL queries:
#article_counts = #articles
.group_by(&:title)
.map { |a| {:title => a[0], :count => a[1].size} }
.sort { |a1,a2| a2[:count] <=> a1[:count] }

There's a group by method that you can use:
http://railscasts.com/episodes/29-group-by-month

Related

Rails the best way to scope vars

i have a 'Course' model that has the following attributes;
Course
Price - float
Featured - boolean
My question would be the following, I need 4 lists in my controller, recent courses, paid courses, free courses and featured courses.
It would be good practice to write my controller as follows?
def index
#courses = Course.order(created_at: :desc)
#free_courses = []
#courses.map {|c| #free_courses << c if c.price == 0}
#premium_courses = []
#courses.map {|c| #premium_courses << c if c.price> 0}
#featured_courses = []
#courses.map {|c| #featured_courses << c if c.featured}
end
Or do the consultations separately?
def index
#courses = Course.order(created_at: :desc)
#free_courses = Course.where("price == 0")
#premium_courses = Course.where("price > 0")
#featured_courses = Course.where(featured: true)
end
I checked through the logs that the first option is more performance but I am in doubt if it is an anti partner.
Thanks for all!
The second approach will become faster than the first as the size of the Course table increases. The first approach has to iterate over every record in the table 4 times. The second approach creates a Relation of only the records that match the where clause, so it does less work.
Also, the second approach has the advantage of laziness. Each query is only run at the time it is used, so it can be changed further along the code path. It's more flexible.
Note that it would be an improvement to the second approach to create scopes on the Course model that handles the logic. For example, one each for courses, free_courses, premium_courses and featured courses. This has the advantage of putting database logic in the model instead of the controller, where it can more easily be reused and maintained.
The second approach is better because when you use the .where() method, you are arranging the query in database itself rather than by the controller.
It is generally bad practice to iterate over all records in the database in Rails (i.e. Course.map or Course.all) both for performance and memory usage. As your database grows this becomes exponentially problematic. It's much better to use Course.where() methods. You'll probably want a default sort order so you can add with one line in your model.
default_scope { order(created_at: :desc) }
Then you can just do this in controller and they'll have the sort by default:
#courses = Course.all
I would also suggest adding scopes to your model for easier access.
So in your course.rb file
scope :free -> { where("price == 0") }
scope :premium -> { where("price > 0") }
scope :featured -> { where(featured: true) }
Then in your controller you can just do:
#courses = Course.all
#free_courses = Course.free
#premium_courses = Course.premium
#featured_courses = Course.featured
These scopes can also be chained if you need to combine those so you could do things like:
#mixed_courses = Course.premium.featured
As others have explained, Model.where() executes the selection of data by passing sql inside where("Write Pure SQL QUERIES HERE") where as regular ruby enumerable methods (.map) iterate over array which must be instantiated as ruby objects. That's where the memory / performance issues take the hit. It's ok if you're working with small data sets, but anything with data volume will get ugly.

Rails searching with multiple conditions (if values are not empty)

Let's say I have a model Book with a field word_count, amongst potentially many other similar fields.
What is a good way for me to string together conditions in an "advanced search" of the database? In the above example, I'd have a search form with boxes for "word count between ___ and ___". If a user fills in the first box, then I want to return all books with word count greater than that value; likewise, if the user fills in the second box, then I want to return all books with word count less than that value. If both values are filled in, then I want to return word counts within that range.
Obviously if I do
Book.where(:word_count => <first value>..<second value>)
then this will break if only one of the fields was filled in. Is there any way to handle this problem elegantly? Keep in mind that there may be many similar search conditions, so I don't want to build separate queries for every possible combination.
Sorry if this question has been asked before, but searching the site hasn't yielded any useful results yet.
How about something like:
#books = Book
#books = #books.where("word_count >= ?", values[0]) if values[0].present?
#books = #books.where("word_count <= ?", values[1]) if values[1].present?
ActiveRecord will chain the where clauses
The only problem is that if values[0] && values[1] the query would not return anything if values[0] was greater than values[1].
For our advanced searching we create a filter object which encapsulates the activerecord queries into simple methods. It was originally based on this Thoughtbot post
A book filter could look something like this:
class BookFilter
def initialize
#relation = Book.scoped
end
def restrict(r)
minimum_word_count!(r[:first]) if r[:first].present?
maximum_word_count!(r[:second]) if r[:second].present?
recent! if r.try(:[], :recent) == '1'
#relation
end
protected
def recent!
where('created_at > ? ', 1.week.ago)
end
def minimum_word_count!(count)
where('word_count >= ? ', count)
end
def maximum_word_count!(count)
where('word_count <= ?', count)
end
def where(*a)
#relation = #relation.where(*a)
end
end
#to use
books = BookFilter.new.restrict(params)
Take a look at the ransack gem, which is the successor to the meta_search gem, which still seems to have the better documentation.
If you do want to roll your own, there's nothing preventing you from chaining clauses using the same attribute:
scope = Book
scope = scope.where("word_count >= ?", params[:first]) if params[:first]
scope = scope.where("word_count <= ?", params[:last]) if params[:last]
But it's really not necessary to roll your own search, there are plenty of ready solutions available as in the gems above.

DRY instance variables for similar find queries

Perhaps this question may have already been answered, but I am not sure what to search for.
I have a page with an A to Z list of clothing brands, which has an each block to iterate through them all. I would like to split this list out by letter, and have an A to Z row of links at the top, where each letter jumps down the page to their letter in the list. In order to do this however, I can only think of making an each loop for each letter, with <A NAME="A"> etc. next to it, and an instance variable for each one.
My question is, how do I avoid having 26 different instance variables in my controller?
#Abrands = Product.where('brand LIKE ?', "A%")
#Bbrands = Product.where('brand LIKE ?', "B%")
#Cbrands = Product.where('brand LIKE ?', "C%")
etc.
This is clearly not very DRY, is there a better way I could do this? I am still finding my feet with rails, any help would be much appreciated!
Would something like this work for you?
#products = Product.all.group_by{|product| product.brand.slice(0,1)}
This is a nice one-liner that will only issue 1 query. It will result in a hash similar to other users' suggestions.
# Hash initialization to empty arrays
#brands = Hash.new { |h,k| h[k] = [] }
Product.all.each do |product|
#brands[product.brand[0].upcase.to_sym] << product
end
which returns a hash like this:
{:A => [products for brands A*], :B => [products for brands B*], ...}
This method has the advantage of doing only one query instead of 26.
Does this openrailscasts episode help you?
Non Active Record (Products by Letter)

How do I calculate the most popular combination of a order lines? (or any similar order/order lines db arrangement)

I'm using Ruby on Rails. I have a couple of models which fit the normal order/order lines arrangement, i.e.
class Order
has_many :order_lines
end
class OrderLines
belongs_to :order
belongs_to :product
end
class Product
has_many :order_lines
end
(greatly simplified from my real model!)
It's fairly straightforward to work out the most popular individual products via order line, but what magical ruby-fu could I use to calculate the most popular combination(s) of products ordered.
Cheers,
Graeme
My suggestion is to create an array a of Product.id numbers for each order and then do the equivalent of
h = Hash.new(0)
# for each a
h[a.sort.hash] += 1
You will naturally need to consider the scale of your operation and how much you are willing to approximate the results.
External Solution
Create a "Combination" model and index the table by the hash, then each order could increment a counter field. Another field would record exactly which combination that hash value referred to.
In-memory Solution
Look at the last 100 orders and recompute the order popularity in memory when you need it. Hash#sort will give you a sorted list of popularity hashes. You could either make a composite object that remembered what order combination was being counted, or just scan the original data looking for the hash value.
Thanks for the tip digitalross. I followed the external solution idea and did the following. It varies slightly from the suggestion as it keeps a record of individual order_combos, rather than storing a counter so it's possible to query by date as well e.g. most popular top 10 orders in the last week.
I created a method in my order which converts the list of order items to a comma separated string.
def to_s
order_lines.sort.map { |ol| ol.id }.join(",")
end
I then added a filter so the combo is created every time an order is placed.
after_save :create_order_combo
def create_order_combo
oc = OrderCombo.create(:user => user, :combo => self.to_s)
end
And finally my OrderCombo class looks something like below. I've also included a cached version of the method.
class OrderCombo
belongs_to :user
scope :by_user, lambda{ |user| where(:user_id => user.id) }
def self.top_n_orders_by_user(user,count=10)
OrderCombo.by_user(user).count(:group => :combo).sort { |a,b| a[1] <=> b[1] }.reverse[0..count-1]
end
def self.cached_top_orders_by_user(user,count=10)
Rails.cache.fetch("order_combo_#{user.id.to_s}_#{count.to_s}", :expiry => 10.minutes) { OrderCombo.top_n_orders_by_user(user, count) }
end
end
It's not perfect as it doesn't take into account increased popularity when someone orders more of one item in an order.

How to filter results by multiple fields?

I am working on a survey application in ruby on rails and on the results page I want to let users filter the answers by a bunch of demographic questions I asked at the start of the survey.
For example I asked users what their gender and career was. So I was thinking of having dropdowns for gender and career. Both dropdowns would default to all but if a user selected female and marketer then my results page would so only answers from female marketers.
I think the right way of doing this is to use named_scopes where I have a named_scope for every one of my demographic questions, in this example gender and career, which would take in a sanitized value from the dropdown to use at the conditional but i'm unsure on how to dynamically create the named_scope chain since I have like 5 demographic questions and presumably some of them are going to be set to all.
You can chain named scopes together:
def index
#results = Results.scoped
#results = #results.gender(params[:gender]) unless params[:gender].blank?
#results = #results.career(params[:career]) unless params[:career].blank?
end
I prefer however to use the has_scope gem:
has_scope :gender
has_scope :career
def index
#results = apply_scopes(Results).all
end
If you use has_scope with inherited_resources, you don't even need to define the index action.
named_scope :gender,lambda { |*args|
unless args.first.blank?
{ :conditions => [ "gender = ?", args.first] }
end
}
If you write named scopes in this way, you can have all them chained, and if one of your params will be blank wont breaks.
Result.gender("Male") will return male results.
Result.gender("") will return male and female too.
And you can chain all of your methods like this. Finally as a filtering you can have like:
Result.age(16).gender("male").career("beginer")
Result.age(nil).gender("").career("advanced") - will return results with advanced career, etc.
Try some like this:
VistaFact.where( if active then {:field => :vista2} else {} end)
Or like this:
VistaFact.where(!data.blank? ? {:field=>data.strip} : {}).where(some? ? {:field2 => :data2} : {}).where ...
That work for me very nice!

Resources