Closed. This question needs to be more focused. It is not currently accepting answers.
Want to improve this question? Update the question so it focuses on one problem only by editing this post.
Closed 7 months ago.
Improve this question
I have a table that I'd like to sort by letter grade. The issue is that it sorts letters before letters with symbols. For example, it sorts: A, A+, A-, B, B+, B-, etc., but I would like it to sort A+, A, A-, B+, B, B-, etc. Is there a way to set do this?
As idea you can check last letter of grade and depending on its value add some number to grade
You can add scope to your model
scope :order_by_grade, -> do
sql = <<~SQL
CASE RIGHT(grade, 1)
WHEN '-' THEN grade || 3
WHEN '+' THEN grade || 1
ELSE grade || 2
END
SQL
order(sql)
end
And then apply to your model Work.order_by_grade
As another idea you can define some constant in the model with all variants and use index
And may be better sort from the worst to the best as ASC and from the best to the worst as DESC -- it's your choice
GRADES = %w[A+ A A- B+ ...]
scope :order_by_grade, -> do
sql = 'CASE grade '
GRADES.reverse_each.with_index do |grade, index|
sql << sanitize_sql_array(['WHEN ? THEN ?', grade, index])
end
sql << ' END'
order(sql)
end
And then apply to your model Work.order_by_grade or even Work.order_by_grade.reverse_order
You may write the following.
arr = ["A", "B-", "B+", "A-", "B", "A+"]
order = { '+' => 0, nil => 1, '-' => 2 }
arr.sort_by { |s| [s[0], order[s[1]]] }
#=> ["A+", "A", "A-", "B+", "B", "B-"]
Two arrays are compared for ordering with the method Array#<=>. See especially the third paragraph at the link.
Suppose
s1 = 'A'
s2 = 'A+'
are being compared. For s1 we compute the array
[s[0], order[s[1]]]
#=> ['A', order[nil]]
#=> ['A', 1]
For s2
[s[0], order[s[1]]]
#=> ['A', order['+']]
#=> ['A', 0]
As
['A', 0] <=> ['A', 1]
#=> -1
'A+' is ordered before 'A'.
Related
I have Rails 5.2 project with three models:
class Post
has_many :post_tags
has_many :tags, through: :post_tags
end
class PostTags
belongs_to :post
belongs_to :tag
end
class Tags
has_many :post_tags
has_many :posts, through: :post_tags
end
I have the a number of arrays of tag ids, e.g:
array_1 = [3, 4, 5]
array_2 = [5, 6, 8]
array_3 = [9, 11, 13]
I want a query that will return posts that are tagged with at least one tag with an id from each of the arrays.
For instance, imagine I have a post with the following tag ids:
> post = Post.find(1)
> post.tag_ids
> [4, 8]
If I ran the query with array_1 and array_2 it would return this post. However if I ran it with array_1, array_2 and array_3 it would not return this post.
I attempted this with the following query:
Post.joins(:tags).where('tags.id IN (?) AND tags.id IN (?)', array_1, array_2)
But this does not return the post.
What should the query be to return the post?
Any help would be greatly appreciated!
Since you've tagged this question with postgresql you can perform the query you want using the intersect keyword. Unfortunately, activerecord doesn't natively support intersect so you'll have to build sql to use this method.
array_1 = [3, 4, 5]
array_2 = [5, 6, 8]
query = [array_1, array_2].map do |tag_ids|
Post.joins(:tags).where(tags: { id: tag_ids }).to_sql
end.join(' intersect ')
Post.find_by_sql(query)
Edit:
We can use subqueries to return the posts and maintain the activerecord relation.
array_1 = [3, 4, 5]
array_2 = [5, 6, 8]
Post
.where(post_tags: PostTag.where(tag_id: array_1))
.where(post_tags: PostTag.where(tag_id: array_2))
For bonus points, you can turn where(post_tag: PostTag.where(tag_id: array_1)) into a scope on Posts and chain as many of them as you'd like.
As mentioned by #NikhilVengal. You should be able to use intersection of 3 scoped queries like so
scopes = [array_1,array_2,array_3].map do |arr|
Post.joins(:post_tags).where(PostTag.arel_table[:tag_id].in(arr)).arel
end
subquery = scopes.reduce do |memo,scope|
# Arel::Nodes::Intersect.new(memo,scope)
memo.intersect(scope)
end
Post.from(Arel::Nodes::As.new(subquery,Post.arel_table))
This should return Post objects that are the intersection of the 3 queries.
Alternatively we can create 3 joins
joins = [array_1,array_2,array_3].map.with_index do |arr,idx|
alias = PostTag.arel_table.alias("#{PostTag.arel_table.name}_#{idx}")
Arel::Nodes::InnerJoin.new(
alias,
Arel::Nodes::On.new(
Post.arel_table[:id].eq(alias.arel_table[:post_id])
.and(alias.arel_table[:tag_id].in(arr))
)
)
end
Post.joins(joins).distinct
This will create 3 Inner joins with the Post table each being with the PostTag table filtered to the specific tag_ids ensuring that the Post will only show up if it exists in all 3 lists.
The use of AND in your where condition is checking for values that intersect (both arrays contain the same values).
array_1 = [3, 4, 5]
array_2 = [5, 6, 8]
And will return results with id: 5 since it's in both arrays.
Using an OR will get you what you need. Either one of these should work for you:
Post.joins(:tags).where('tags.id IN (?) OR tags.id IN (?)', array_1, array_2)
OR
Post.joins(:tags).where(tags: { id: array_1 + array_2 })
my idea is that you could group by posts.id and sum all its tags position in the input array position, suppose you query with 3 group_tags then you have the result like this:
post_id group_tags_1 group_tags_2 group_tags_3 ....
1 2 0 0
2 1 1 1
so the final result is the Post with id 2 since it has at least one tag from each group.
def self.by_group_tags(group_tags)
having_enough_tags = \
group_tags.map do |tags|
sanitize_sql_array(["SUM(array_position(ARRAY[?], tags.id::integer)) > 0", tags])
end
Post
.joins(:tags)
.group("posts.id")
.having(
having_enough_tags.join(" AND ")
)
end
# Post.by_group_tags([[1,2], [3,4]])
# Post.by_group_tags([[1,2], [3,4], [5,6,7]])
update:
if you want to go to further chain and should not be effected by group, then just simple return a Relation that wrap all post ids you query from by_group_tags, such as a where as below
class Post
def self.by_group_tags(group_tags)
# ...
end
def self.scope_by_group_tags(group_tags)
post_ids = Post.by_group_tags(group_tags).pluck(:id)
Post.where(id: post_ids)
end
end
# Post.scope_by_group_tags([[1,2], [3,4]]).first(10)
the drawback: call query the same Posts twice.
I am trying to sort #users by name alphabetically. How would I do that?
#users.person.name
EDIT:
Here is how I solved this:
#users.sort! { |a, b| a.person.name <=> b.person.name }
Thank you num8er
One observations on your solution. If you use #sort! instead of #sort you'll want to have a good reason why; #sort is preferable unless you need #sort!.
Consider this code:
1 test = %w(c a b)
2 p test.sort # => ['a', 'b', 'c']
3 p test # => ["c", "a", "b"]
4 p test.sort! # => ['a', 'b', 'c']
5 p test # => ['a', 'b', 'c']
If you don't know why line 5 shows a different value from line 3, then I would suggest that you avoid using #sort! altogether until you do. Otherwise, you could create some very difficult-to-find bugs.
You can use sort, but that requires first fetching all the Users. If there are many Users this can eat a lot of memory.
Instead, you can do the sorting in the database and fetch them in order on demand.
#users = User
.joins(:person) # join by the model name
.order("people.name") # order by the table name
# find_each is more efficient than each for large sets
#users.find_each do |user|
...do something with each user...
end
And you can put it in a scope to use anywhere.
class User
scope :by_name, ->() { joins(:person).order("people.name") }
end
# Find the first 10 users, ordered by their name.
#users = User.by_name.limit(10)
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 an ActiveRecord object that has four String columns. I'd like to make a validation that verifies that a certain value is unique across all four columns. For example, assuming the four columns in question are named a, b, c, and d:
FooObject.new( a: 'bar' ).save!
should succeed, but
FooObject.new( b: 'bar' ).save!
should fail because there is already a FooObject whose value of either a, b, c, or d matches the value entered for b. Is there a neat, clean way to accomplish this validation on the object? Thank you!
arr = ["a", "b"]
arr.uniq{|x| x}.size //2
arr << "b"
arr.uniq{|x| x}.size // 2
2 == 2
hence this element already exists in the column
arr represents a temp copy of one row of FoObject
You can try a custom method:
validate :uniqueness_across_columns
def uniqueness_across_columns
cols = [:a, :b, :c, :d]
conditions = cols.flat_map{|x| cols.map{|i| arel_table[x].eq(self.try(i)) if self.try(i).present? }}
!self.class.exists? conditions.compact.reduce(:or)
end
I have 2 multi selectors in extJS (respondents groups and jsut respondents). Erery respondent have a group (ONLY 1).
here, how i selected my ids ...
I get respondent_group id, for example = 1
respondent_ids = params[:selected_respondents].split(/,/).collect {|r| r.to_i}
here i get respondents ids: 3,4,1
respondent_pure_ids = params[:selected_alone_respondents].split(/,/).collect {|r| r.to_i}
here i find respondents in group (for example i have 10 groups, every group has 1-3 respondents).
respondents = Respondent.find(:all, :conditions => ["respondent_group_id in (?) AND id NOT IN (?)", respondent_ids, session[:user].id])
I find respondents .
respondents_alone = Respondent.find(:all, :conditions => ["id in (?) AND id NOT IN (?)", respondent_pure_ids, session[:user].id])
here i found respondents (i find id where respondent_group = ?) and send them email.
respondents.each do |r|
Notifier.deliver_inquiry_notification()
end
What I want?
I get respondents and respondents_alone id's.
For example respondents = 3 , 4 , 6
respondents_ alone = 3, 5, 6, 8
I have 3 and 6 ids in both. I dont want to dublicate my data . How to check: if respondents ids DOES NOT equals respondent_alone ids I SEND EMAIL else error!
You can use ruby array arithmetic to get the difference between both arrays or to get every entry only once:
a = [3, 4, 6]
b = [3, 5, 6, 8]
a - b # [4] (only a without any element of b, difference)
b - a # [5, 8] (only b without any element of a, difference)
a | b # [3, 4, 6, 5, 8] (complete set of all ids, union)
a & b # [3, 6] (ids same in both arrays, intersection)
With this you can check if some ids are only in one array or in both, e.g. difference is empty or a|b==a&b => both are equal
cf. http://www.ruby-doc.org/core/classes/Array.html