Ranking results with complex conditions using Rails and Squeel - ruby-on-rails

I'm probably doing something silly here - and I'm open to other ways of doing - but I'm trying to order my results set based on a computed field:
Client.select{['clients.*',
(cast((surname == matching_surname).as int) * 10 +
cast((given_names == matching_given_names).as int) +
cast((date_of_birth == matching_date_of_birth).as int).as(ranking)]}.
where{(surname =~ matching_surname) |
(given_names =~ matching_given_names) |
(date_of_birth == matching_date_of_birth)}.
order{`ranking`.desc}
My problem is that date_of_birth could be nil. This causes the cast((...).as int) call to return three different values - 1 if the expression evaluated to true; 0 if the expression evaluated to false; and nil if the underlying column value was nil.
The nil values from the expressions cause the whole ranking to evaluate to NIL - which means that even if I have a record that matches exactly on surname and given_names, if the date_of_birth column is nil, the ranking for the record is nil.
I have tried to use a complex expression in the cast that checks if not nil or the matching_value, but it fails with a Squeel exception using | and ruby evaluates it when using || and or.
I've also tried to use a predicates in the order for aliased columns:
order{[`ranking` != nil, `ranking`.desc]}
but that throws an ActiveRecordexception complaining that the column ranking does not exist.
I'm at the end of my rope... any ideas?

After a bit of a dance, I was able to calculate the ranking using a series of outer joins to other scopes as follows:
def self.weighted_by_any (client)
scope =
select{[`clients.*`,
[
((cast((`not rank_A.id is null`).as int) * 100) if client[:social_insurance_number].present?),
((cast((`not rank_B.id is null`).as int) * 10) if client[:surname].present?),
((cast((`not rank_C.id is null`).as int) * 1) if client[:given_names].present?),
((cast((`not rank_D.id is null`).as int) * 1) if client[:date_of_birth].present?)
].compact.reduce(:+).as(`ranking`)
]}.by_any(client)
scope = scope.joins{"left join (" + Client.weigh_social_insurance_number(client).to_sql + ") AS rank_A ON rank_A.id = clients.id"} if client[:social_insurance_number].present?
scope = scope.joins{"left join (" + Client.weigh_surname(client).to_sql + ") AS rank_B on rank_B.id = clients.id"} if client[:surname].present?
scope = scope.joins{"left join (" + Client.weigh_given_names(client).to_sql + ") AS rank_C on rank_C.id = clients.id"} if client[:given_names].present?
scope = scope.joins{"left join (" + Client.weigh_date_of_birth(client).to_sql + ") AS rank_D on rank_D.id = clients.id"} if client[:date_of_birth].present?
scope.order{`ranking`.desc}
end
where Client.weigh_<attribute>(client) is another scope that looks like the following:
def self.weigh_social_insurance_number (client)
select{[:id]}.where{social_insurance_number == client[:social_insurance_number]}
end
This allowed me to break out the comparison of the value from the check for nil and so removed the third value in my boolean calculation (TRUE => 1, FALSE => 0).
Clean? Efficient? Elegant? Maybe not... but working. :)
EDIT base on new information
I've refactored this into something much more beautiful, thanks to Bigxiang's answer. Here's what I've come up with:
First, i replaced the weigh_<attribute>(client) scopes with sifters. I'd previously discovered that you can use sifters in the select{} portion of the scope - which we will be using in a minute.
sifter :weigh_social_insurance_number do |token|
# check if the token is present - we don't want to match on nil, but we want the column in the results
# cast the comparison of the token to the column to an integer -> nil = nil, true = 1, false = 0
# use coalesce to replace the nil value with `0` (for no match)
(token.present? ? coalesce(cast((social_insurance_number == token).as int), `0`) : `0`).as(weight_social_insurance_number)
end
sifter :weigh_surname do |token|
(token.present? ? coalesce(cast((surname == token).as int), `0`) :`0`).as(weight_surname)
end
sifter :weigh_given_names do |token|
(token.present? ? coalesce(cast((given_names == token).as int), `0`) : `0`).as(weight_given_names)
end
sifter :weigh_date_of_birth do |token|
(token.present? ? coalesce(cast((date_of_birth == token).as int), `0`) : `0`).as(weight_date_of_birth)
end
So, let's create a scope using the sifters to weigh all our criteria:
def self.weigh_criteria (client)
select{[`*`,
sift(weigh_social_insurance_number, client[:social_insurance_number]),
sift(weigh_surname, client[:surname]),
sift(weigh_given_names, client[:given_names]),
sift(weigh_date_of_birth, client[:date_of_birth])
]}
end
Now that we can determine if the criteria provided match the column value, we compute our ranking using another sifter:
sifter :ranking do
(weight_social_insurance_number * 100 + weight_surname * 10 + weight_date_of_birth * 5 + weight_given_names).as(ranking)
end
And adding it all together to make our scope that includes all the model attributes and our computed attributes:
def self.weighted_by_any (client)
# check if the date is valid
begin
client[:date_of_birth] = Date.parse(client[:date_of_birth])
rescue => e
client.delete(:date_of_birth)
end
select{[`*`, sift(ranking)]}.from("(#{weigh_criteria(client).by_any(client).to_sql}) clients").order{`ranking`.desc}
end
So, I can now search for a client and have the results ranked by how closely they match the provided criteria with:
irb(main): Client.weighted_by_any(client)
Client Load (8.9ms) SELECT *,
"clients"."weight_social_insurance_number" * 100 +
"clients"."weight_surname" * 10 +
"clients"."weight_date_of_birth" * 5 +
"clients"."weight_given_names" AS ranking
FROM (
SELECT *,
coalesce(cast("clients"."social_insurance_number" = '<sin>' AS int), 0) AS weight_social_insurance_number,
coalesce(cast("clients"."surname" = '<surname>' AS int), 0) AS weight_surname,
coalesce(cast("clients"."given_names" = '<given_names>' AS int), 0) AS weight_given_names, 0 AS weight_date_of_birth
FROM "clients"
WHERE ((("clients"."social_insurance_number" = '<sin>'
OR "clients"."surname" ILIKE '<surname>%')
OR "clients"."given_names" ILIKE '<given_names>%'))
) clients
ORDER BY ranking DESC
Cleaner, more elegant, and working better!

Related

WITH Queries by ActiveRecord::QueryMethods And Return ActiveRecord_Relation rails

I'm using select in with query by find_by_sql:
scope :error_percentage, -> (user_obj) {
find_by_sql("WITH products_boost_sum AS (SELECT *, (CASE WHEN (city_id =#{user_obj.user.city_id || -1}) THEN 1 ELSE 0 END) + (CASE WHEN (country_id =#{user_obj.country_id || -1}) THEN 1 ELSE 0 END) AS boost_sum FROM products) SELECT *, (CASE WHEN boost_sum = 2 THEN 2 ELSE 0 END) AS error_percentage FROM products_boost_sum")
}
The problem this scope is used by other scopes. So I need to return Active_Relation Object not an array.
I checked the QueryMethods of ActiveRecord, but I can't find a method for with Query.
How can I use with Query to return ActiveRelation Object?
If there no queryMethod for it, Is that available by select Method?
CTE queries are not supported by Active Record out of the box, but there is a gem that adds support https://github.com/DavyJonesLocker/postgres_ext.
Then you can do queries like
Score.with(my_games: Game.where(id: 1)).joins('JOIN my_games ON scores.game_id = my_games.id')

How to Find the Middle Character(s) in Ruby?

I'm trying to write a method in Ruby that:
if the string length is even numbers it will return the middle two characters and,
if the string length is odd it will return only the middle character
i put together this code, but it is not working:
def the_middle(s)
if s.length % 2 == 0
return s.index(string.length/2-1) && s.index(string.length/2)
else
return s.index(string.length/2).round
end
end
I think the problem is in the syntax, not the logic, and I was hoping someone could identify where the syntax error might be.
I really appreciate your help!
Actually you have both syntax errors and logic (semantic) errors in that code.
First of all it seems you have misunderstood how the index method on string works. It does not return the character at the given index but the index of a given substring or regex as can be seen in the documentation:
Returns the index of the first occurrence of the given substring or pattern (regexp) in str.
You're also using the wrong operator to concatenate the two middle characters when the string length is even. && is the logical and operator. It's usually used for conditions and not assigments - for example in an if statement if s.length.even? && s.length > 2. The operator you want to use is + which concatenates strings.
Finally, you're using string.length but string is not defined anywhere. What you mean is probably s.length (the input parameter).
The correct solution would be more like the following:
def the_middle(s)
if s.length.even?
return s[s.length/2-1] + s[s.length/2]
else
return s[s.length/2]
end
end
I have taken the liberty to replace s.length % 2 == 0 with s.length.even? as it's more intention revealing and really the ruby way of finding out whether an integer is even or odd.
You can solve this without a conditional using String#[].
Using a range with a negative end:
def the_middle(s)
i = (s.length - 1) / 2
s[i..-i.succ]
end
Or start and length:
def the_middle(s)
a, b = (s.length - 1).divmod(2)
s[a, b + 1]
end
Both return the same results:
the_middle("a") #=> "a"
the_middle("aba") #=> "b"
the_middle("abcba") #=> "c"
the_middle("abcdcda") #=> "d"
# ^
the_middle("abba") #=> "bb"
the_middle("abccba") #=> "cc"
the_middle("abcddcda") #=> "dd"
# ^^
Try this:
def get_middle(s)
x = (s.length/2)
s.length.even? ? s[x-1..x] : s[x]
end
Since olerass already answered your doubt about the syntax, i will suggest you a less verbose solution for the question in the title:
def the_middle(s)
return s[s.length/2] if s.length.odd?
s[s.length/2-1] + s[s.length/2]
end
Same answer the syntax is just consolidated.
Format (logic result) ? ( if true this is the result) : (if false this is the result)
def get_middle(s)
num = s.length
num.even? ? ( s[num/2-1] + s[num/2]) : (s[num/2])
end

Activerecord where array with less than condition

I have an array of conditions i'm passing to where(), with the conditions being added one at a time such as
conditions[:key] = values[:key]
...
search = ModelName.where(conditions)
which works fine for all those that i want to compare with '=', however I want to add a '<=' condition to the array instead of '=' such as
conditions[:key <=] = values[:key]
which of course won't work. Is there a way to make this work so it i can combine '=' clauses with '<=' clauses in the same condition array?
One way of doing it:
You could use <= in a where clause like this:
User.where('`users`.`age` <= ?', 20)
This will generate the following SQL:
SELECT `users`.* FROM `users` WHERE (`users`.`age` <= 20)
Update_1:
For multiple conditions, you could do this:
User.where('`users`.`age` <= ?', 20).where('`users`.`name` = ?', 'Rakib')
Update_2:
Here is another way for multiple conditions in where clause:
User.where('(id >= ?) AND (name= ?)', 1, 'Rakib')
You can add any amount of AND OR conditions like this in your ActiveRecord where clause. I just showed with 2 to keep it simple.
See Ruby on Rails Official Documentation for Array Conditions for more information.
Update_3:
Another slight variation of how to use Array Conditions in where clause:
conditions_array = ["(id >= ?) AND (name = ?)", 1, "Rakib"]
User.where(conditions_array)
I think, this one will fit your exact requirement.
You could use arel.
conditions = {x: [:eq, 1], y: [:gt, 2]}
model_names = ModelName.where(nil)
conditions.each do |field, options|
condition = ModelName.arel_table[field].send(*options)
model_names = model_names.where(condition)
end
model_names.to_sql --> 'SELECT * FROM model_names WHERE x = 1 and y > 2'

ActiveRecord query w/ check for empty field

I am generating the condition for an ActiveRecord where as follows:
query = {:status => status}
if (limit)
query[:limit] = #vals['limit'].to_i
end
if (offset && limit)
query[:offset] = (offset - 1) * limit
end
rows = Review.all(query)
This works just fine. I filter on 'status' of a review and I fill in limit and offset if passed in. Problem is now that I need to add a check for 'not null' on the reviews content field.
I.E.
AND review.content != '' && review.content != nil
I have read you can do something like
Review.were("review <> ''")
Which by itself works but I am not sure how to incorporate that into my above command. Or change the above command to work with a where statement rather than an 'all' statement.
I would write that code something like
query = Review.where("status = ?", status).where("review <> '' AND review IS NOT NULL")
if limit.present?
query = query.limit(limit)
if offset.present?
query = query.offset((offset - 1) * limit)
end
end
rows = query.all
rails query object does lazy evaluation, so you can build up the query, no sql is issued to the database until you begin to iterate over the rows
alternate to .where("review <> '' AND review IS NOT NULL")
.where("COALESCE(review, '') <> ''")

ruby looping question

I want to make a loop on a variable that can be altered inside of the loop.
first_var.sort.each do |first_id, first_value|
second_var.sort.each do |second_id, second_value_value|
difference = first_value - second_value
if difference >= 0
second_var.delete(second_id)
else
second_var[second_id] += first_value
if second_var[second_id] == 0
second_var.delete(second_id)
end
first_var.delete(first_id)
end
end
end
The idea behind this code is that I want to use it for calculating how much money a certain user is going to give some other user. Both of the variables contain hashes. The first_var is containing the users that will get money, and the second_var is containing the users that are going to pay. The loop is supposed to "fill up" a user that should get money, and when a user gets full, or a user is out of money, to just take it out of the loop, and continue filling up the rest of the users.
How do I do this, because this doesn't work?
Okay. What it looks like you have is two hashes, hence the "id, value" split.
If you are looping through arrays and you want to use the index of the array, you would want to use Array.each_index.
If you are looping through an Array of objects, and 'id' and 'value' are attributes, you only need to call some arbitrary block variable, not two.
Lets assume these are two hashes, H1 and H2, of equal length, with common keys. You want to do the following: if H1[key]value is > than H2[key]:value, remove key from H2, else, sum H1:value to H2:value and put the result in H2[key].
H1.each_key do |k|
if H1[k] > H2[k] then
H2.delete(k)
else
H2[k] = H2[k]+H1[k]
end
end
Assume you are looping through two arrays, and you want to sort them by value, and then if the value in A1[x] is greater than the value in A2[x], remove A2[x]. Else, sum A1[x] with A2[x].
b = a2.sort
a1.sort.each_index do |k|
if a1[k] > b[k]
b[k] = nil
else
b[k] = a1[k] + b[k]
end
end
a2 = b.compact
Based on the new info: you have a hash for payees and a hash for payers. Lets call them ees and ers just for convenience. The difficult part of this is that as you modify the ers hash, you might confuse the loop. One way to do this--poorly--is as follows.
e_keys = ees.keys
r_keys = ers.keys
e = 0
r = 0
until e == e_keys.length or r == r_keys.length
ees[e_keys[e]] = ees[e_keys[e]] + ers[r_keys[r]]
x = max_value - ees[e_keys[e]]
ers[r_keys[r]] = x >= 0 ? 0 : x.abs
ees[e_keys[e]] = [ees[e_keys[e]], max_value].min
if ers[r_keys[r]] == 0 then r+= 1 end
if ees[e_keys[e]] == max_value then e+=1 end
end
The reason I say that this is not a great solution is that I think there is a more "ruby" way to do this, but I'm not sure what it is. This does avoid any problems that modifying the hash you are iterating through might cause, however.
Do you mean?
some_value = 5
arrarr = [[],[1,2,5],[5,3],[2,5,7],[5,6,2,5]]
arrarr.each do |a|
a.delete(some_value)
end
arrarr now has the value [[], [1, 2], [3], [2, 7], [6, 2]]
I think you can sort of alter a variable inside such a loop but I would highly recommend against it. I'm guessing it's undefined behaviour.
here is what happened when I tried it
a.each do |x|
p x
a = []
end
prints
1
2
3
4
5
and a is [] at the end
while
a.each do |x|
p x
a = []
end
prints nothing
and a is [] at the end
If you can I'd try using
each/map/filter/select.ect. otherwise make a new array and looping through list a normally.
Or loop over numbers from x to y
1.upto(5).each do |n|
do_stuff_with(arr[n])
end
Assuming:
some_var = [1,2,3,4]
delete_if sounds like a viable candidate for this:
some_var.delete_if { |a| a == 1 }
p some_var
=> [2,3,4]

Resources