More ruby way to count objects that match criteria - ruby-on-rails

Here is what I am doing now:
def get_counts
products = Product.all
a_count, b_count, c_count = 0, 0, 0
products.collect{ |p| a_count+=1 if p.some_attribute == 'a' }
products.collect{ |p| b_count+=1 if p.some_attribute == 'b' }
products.collect{ |p| c_count+=1 if p.some_attribute == 'c' }
return a_count, b_count, c_count
end
This feels horribly scripty to me. I tried using inject but couldn't get it to work how I wanted. Does anyone have a better way to do this?

To improve on #xdazz's answer
def get_counts
Product.where(some_attribute: ['a','b','c']).
count(group: "some_attribute")
end
This will return a hash in the form:
{'a' => 3, 'b' => 4, 'c' => 5}

def get_counts
return Product.where(:some_attribute => 'a').count, Product.where(:some_attribute => 'b').count, Product.where(:some_attribute => 'c').count
end
If you want only one query, then use group by.

Related

Convert group count from activerecord into hash with multiple group stages

I'm trying to get some statistics.
Model.where(status:#statuses).group(:sub_group, :status).count
This returns me
{
["sub_group1", "status1"] => 3},
["sub_group2", "status3"] => 7,
["sub_group1", "status2"] => 5, ....etc }
I want to merge them so each element has a unique subgroup.
e.g. a hash like:
{
"sub_group1" => {"status1" => 3, "status2" => 5,
"sub_group2" => {"status3" => 7},
}
or an array
[
["subgroup1", {"status1" = 3, "status2" => 5}],
["subgroup2".....
]
i.e. I want all those terms to be merged with sub_group as the primary header so that I can get the results for subgroup in one item.
Brain not working today....
You can try with merge!:
result = Model.where(status: #statuses).group(:sub_group, :status).count
result.reduce({}) do |collection, (attributes, count)|
collection.merge!(attributes[0] => { attributes[1] => count }) do |_, prev_value, next_value|
prev_value.merge!(next_value)
end
end
Demonstration
I suppose you could do something like this:
res = Model.where(status:#statuses).group(:sub_group, :status).count
nice_hash = {}
res.each do |key, count|
nice_hash[key[0]] = {} unless nice_hash[key[0]]
nice_hash[key[0]][key] = count
end
After this nice_hash should be on the desired format
Please try with below code.
a = Model.where(status:#statuses).group(:sub_group, :status).count
res = {}
a.each do |k,v|
c={}
if res.has_key?(k[0])
c[k[1]]=v
res[k[0]]=res[k[0]].merge(c)
else
c[k[1]]=v
res[k[0]]=c
end
end
I believe the answer to your question should be this or this should guide in the right direction:
Model.where(status:#statuses).group_by(&:sub_group)
If you need the only the statuses as you mentioned, you could do:
Model.where(status:#statuses).select(:status, :sub_group).group_by(&:sub_group)

How to construct where clause in ruby using if

I am finding something like below. Constructing a where clause using condition. Is it possible in ruby? or I need to separate it into two where clause?
Post
.where(tag: "A") if condition A
.where(tag: "B") if condition B
.where(user_id: 1)
.order(....)
Actually, my case is like this. Is there any way to handle?
def this_function
#questions = Question.joins(:comment_threads)
.tagged_with(tag_variable, wild: true, any: true) if tag_variable.present?
.where(index_where_clause)
.where("questions.created_at < ?", query_from_date_time)
.order(created_at: :desc).limit(5)
end
def index_where_clause
where_clause = {}
where_clause[:user_detail_id] = current_user_detail.id if params[:type] == "my_question"
where_clause[:comments] = {user_detail_id: current_user_detail.id} if params[:type] == "my_answer"
where_clause[:wine_question_score_id] = params[:wine_question_score_id] if params[:wine_question_score_id].present?
where_clause
end
The methods you're using return relations so you can say things like this:
#questions = Question.joins(:comment_threads)
#questions = #questions.where("questions.created_at < ?", query_from_date_time)
#questions = #questions.tagged_with(tag_variable, wild: true, any: true) if tag_variable.present?
#questions = #questions.where(:user_detail_id => current_user_detail.id) if params[:type] == "my_question"
#questions = #questions.where(:comments => { user_detail_id: current_user_detail.id}) if params[:type] == "my_answer"
#questions = #questions.where(:wine_question_score_id => params[:wine_question_score_id]) if params[:wine_question_score_id].present?
#questions = #questions.order(created_at: :desc).limit(5)
and build the query piece by piece depending on what you have in params.
I'd probably break it down a little more:
def whatever
#questions = Question.joins(:comment_threads)
#questions = #questions.where("questions.created_at < ?", query_from_date_time)
#questions = with_tag(#questions, tag_variable)
#...
#questions = #questions.order(created_at: :desc).limit(5)
end
private
def with_tag(q, tag)
if tag.present?
q.tagged_with(tag, wild: true, any: true)
else
q
end
end
#...
and bury all the noisy bits in little methods to make things cleaner and easier to read. If you're doing this more than once then you could use scopes to hide the noise in the model class and re-use it as needed.
#tap can be helpful for modifying an object in place to apply conditional logic, in this case the object would be your .where conditions:
Post
.where(
{ user_id: 1 }
.tap do |conditions|
conditions[:tag] = 'A' if condition A
conditions[:tag] = 'B' if condition B
end
)
.order(...)
Or, perhaps it's a little cleaner if you create a helper method:
def specific_conditions
{ user_id: 1 }.tap do |conditions|
conditions[:tag] = 'A' if condition A
conditions[:tag] = 'B' if condition B
end
end
Post.where(specific_conditions).order(...)
But as a side note, if there's a case where condition A and condition B can both be true, the second conditions[:tag] = ... line will override the first. If there is not a case where both can be true, you might try to use some kind of collection to look up the proper value for tag.
CONDITION_TAGS = {
a: 'A'.freeze,
b: 'B'.freeze,
}.freeze
def specific_conditions
{ user_id: 1 }
.tap do |conditions|
conditions[:tag] = CONDITION_TAGS[condition_value] if condition_value
end
end
Post.where(specific_conditions).order(...)
#in Question class
scope :with_user_detail, -> (user_detail_id, flag=true) do
where("user_detail_id = ?", user_detail_id) if flag
end
scope :with_user_detail_comments, -> (user_detail_id, flag=true) do
joins(:comment_threads).where("comments.user_detail_id = ?", user_detail_id) if flag
end
scope :with_wine_question_score, -> (wine_question_score_id) do
where("wine_question_score_id = ?", wine_question_score_id) if wine_question_score_id.present?
end
scope :tagged_with_condition, -> (tag_variable, wild, any) do
tagged_with(tag_variable, wild, any) if tag_variable.present?
end
def this_function
my_question_flag = params[:type] == "my_question"
my_answer_flag = params[:type] == "my_answer"
Question.with_user_detail(current_user_detail.id, my_question_flag)
.tagged_with_condition(tag_variable, wild: true, any: true)
.with_user_detail_comments(current_user_detail.id, my_answer_flag)
.with_wine_question_score(params[:wine_question_score_id])
.order(created_at: :desc).limit(5)
end
You can do the following:
condition = {:tag => "A"} if condition A
condition = {:tag => "B"} if condition B
Post
.where(condition)
.where(:user_id => 1)
.order(....)
you have to use scope :
scope :my_scope, -> (variable) { where(some: vatiable) if my_condition }

Rails: each for multiple and one object data

I'm new in rails and need to clear one question:
for example my method return such data:
#<Article ART_ID: 1151754, ART_ARTICLE_NR: "0 281 002 757", ART_SUP_ID: 30, ART_DES_ID: nil, ART_COMPLETE_DES_ID: 62395, ART_CTM: nil, ART_PACK_SELFSERVICE: 0, ART_MATERIAL_MARK: 0, ART_REPLACEMENT: 0, ART_ACCESSORY: 0, ART_BATCH_SIZE1: nil, ART_BATCH_SIZE2: nil, datetime_of_update: "2012-09-25 17:49:18">
or array, not only one object: how could use each func then?
for example:
articles = ArtLookup.search_strong_any_kind_without_brand(params[:article_nr].gsub(/[^0-9A-Za-z]/, ''))
binding.pry
if articles.present?
articles.each do |a|
#all_parts_result <<
{
analogue_manufacturer_name: a.supplier.SUP_BRAND,
analogue_code: a.ART_ARTICLE_NR,
delivery_time_min: '',
delivery_time_max: '',
min_quantity: '',
product_name: a.art_name,
quantity: '',
price: '',
distributor_id: '',
link_to_tecdoc: a.ART_ID
}
end
end
now i get errors like
`undefined method `each' for `#<Article:0x007f6554701640>
i think it is becouse i have sometimes one object, sometimes 10, and sometime 0.
how is it beatifull and right to do in rails?
Your search_strong_any_kind_without_brand method is looping through your articles based on the search condition. If the article matches then you are setting #art_concret to the match and then returning the match. However, you're not finding all matches, just the last one.
.
loop
#art_concret = art
end
.
return #art_concret
If you set the #art_concret as an array and inject results into this instance variable, then you will have the resulting search in array form. However, keep in mind that this does kind of break the ActiveRecord ORM as you would be returning a simple array and not an ActiveRecord Relation array.
def self.search_strong_any_kind_without_brand(search)
search_condition = search.upcase
#art_concret = []
#search = find(:all, :conditions => ['MATCH (ARL_SEARCH_NUMBER) AGAINST(? IN BOOLEAN MODE)', search_condition])
#articles = Article.find(:all, :conditions => ["ART_ID in (?)", #search.map(&:ARL_ART_ID)])
#binding.pry
#articles.each do |art|
if art.ART_ARTICLE_NR.gsub(/[^0-9A-Za-z]/, '') == search
#art_concret << art
end
end
return #art_concret
end
If you want to keep the code a bit cleaner then use select on your matching condition instead of looping through each article in #articles.
def self.search_strong_any_kind_without_brand(search)
search_condition = search.upcase
#search = find(:all, :conditions => ['MATCH (ARL_SEARCH_NUMBER) AGAINST(? IN BOOLEAN MODE)', search_condition])
#articles = Article.find(:all, :conditions => ["ART_ID in (?)", #search.map(&:ARL_ART_ID)])
#binding.pry
return #articles.select { |art| art.ART_ARTICLE_NR.gsub(/[^0-9A-Za-z]/, '') == search }
end
Unrelated: is there a reason why you're using instance variables in search_strong_any_kind_without_brand?
I think the right thing to do is to make sure your method always returns an array (or enumerable).
looking at the code you posted in to pastebin I would recommend you use Array#select in your method
for example you might be able to just return this:
#articles.select { |art| art.ART_ARTICLE_NR.gsub(/[^0-9A-Za-z]/, '') == search }
assuming #articles is an array or collection you will always get an array back, even if it is 0, or 1 element
This answer would be a bit offtopic, but I would like to mention a splat operator:
[*val]
will produce array, consisting of either single val value whether it’s not an array, or the array itself whether val is an array:
▶ def array_or_single param
▷ [*param].reduce &:+ # HERE WE GO
▷ end
=> :array_or_single
▶ array_or_single [1,2,3]
=> 6
▶ array_or_single 5
=> 5
That said, you code would work with this tiny improvement:
- articles.each do |a|
+ [*articles].each do |a|
Hope it gives a hint on how one might handle the data, coming from the 3rd party. As an answer to your particular question, please follow the advises in the other answers here.

Create key if it doesn't exist in nested hashes

I've been trying to figure out how to write this ruby code more eloquently. Does someone have a better solution?
a[:new] = {} if a[:new].nil?
a[:new].merge!( { new_key => new_value } )
is there a way to write this in a more elegant way? I come across this a lot when dealing with nested hashes that need to check whether an key exist and if not, create it.
Write it as below taking the help of Hash#to_h and NilClass#to_h
a[:new] = a[:new].to_h.merge( { new_key => new_value } )
Example :
hsh1[:a] # => nil
hsh1[:a] = hsh1[:a].to_h.merge({1=>2})
hsh1[:a] # => {1=>2}
hsh2 = {:a => {'k' => 2}}
hsh2[:a] # => {"k"=>2}
hsh2[:a] = hsh2[:a].to_h.merge({1=>2})
hsh2 # => {:a=>{"k"=>2, 1=>2}}
Do this at the beginning:
a = Hash.new{|h, k| h[k] = {}}
then, without caring whether a has a key :new or not, do
a[:new].merge!(new_key => new_value)
or
a[:new][new_key] = new_value

How to compare two Hashes so to return true if both Hashes have same keys/values?

I am using Ruby on Rails 3.2.2 and Ruby 1.9.3. I would like to compare two Hashes (A and B) so to return true if a Hash (A) include all keys/values of the other Hash (B).
For example, given I have
params.inspect
# => { "action"=>"...", "controller"=>"...", "key_param1"=>"value_param1", , "key_param2"=>"value_param2", "key_param3"=>"value_param3", ... }
my_hash1.inspect
# => { "key_param1"=>"value_param1", "key_param2"=>"value_param2" }
my_hash2.inspect
# => { "key_param4"=>"value_param4", "key_param1"=>"value_param1" }
my_hash3.inspect
# => {}
Then I am looking for a method (or something like that) in order to make
params.has_same_keys_and_values_as?(my_hash1)
# => true
params.has_same_keys_and_values_as?(my_hash2)
# => false
params.has_same_keys_and_values_as?(my_hash3)
# => true
Assuming that Hash#keys and Hash#values return values in the same order:
params.values_at(*my_hash.keys) == my_hash.values
I think you can use:
a.slice(*b.keys) == b
where a and b are your hashes. note that slice is a rails method and not ruby.
in plain ruby you can write:
a.keep_if{|k, v| b[k]} == b
class Hash
def >=(b)
eq = true
b.each { |k, v| eq &= !(self.include? k) ? false : ( ( ((self[k]&&v).is_a? Hash) && !((v||self[k]).empty?) ) ? self[k]>=v : true)}
return eq
end
end
params = { "action"=>"...", "controller"=>"...", "key_param1"=>"value_param1", "key_param2"=>"value_param2", "key_param3"=>"value_param3" }
my_hash1 = { "key_param1"=>"value_param1", "key_param2"=>"value_param2" }
my_hash2 = { "key_param4"=>"value_param4", "key_param1"=>"value_param1" }
my_hash3 = {}
p params >= my_hash1 #true
p params >= my_hash2 #false
p params >= my_hash3 #true
It'll work with "deep" hashes as well:
b = {1 => {2 => {} }, 4 => {} }
a = {1 => {2 => {3 => {} }}, 4 => {}, 5 => "123" }
p a >= b #true
p b >= a #false
P.S.
Whether one hash includes another hash
EDIT: This is assuming that the values/keys are not in the same order in both hashes.
You could iterate over each key in hash1 and use has_key? on hash2. Keep in mind this is naive solution that could be slow for large datasets.
Checkout has_key? and has_value? here: http://www.ruby-doc.org/core-1.9.3/Hash.html#method-i-has_key-3F
You could loop as follows:
hash1.each_key { |key|
if hash2.has_key?(key)
do whatever
endif
}
better way, there's an active support method for this, hash.diff, wrap it with .empty? to check if they are the same
{:one => 1}.diff({:one => 1}).empty?
=> true
{:one => 1}.diff({:one => 2}).empty?
=> false
http://as.rubyonrails.org/classes/ActiveSupport/CoreExtensions/Hash/Diff.html

Resources