Ruby comparison with an OR operator - strange behaviour - ruby-on-rails

Below code should be a good example of strange Ruby behaviour when it comes to the OR operator:
def search_by_name_active
if request.path == (name_search_registrants_path \
|| new_registrant_path)
'active'
end
end
And the specs:
describe "#search_by_name_active" do
it "should return active if current page is new_registrant" do
allow(helper.request).to receive(:path).and_return(new_registrant_path)
expect(helper.search_by_name_active).to eq('active')
end
end
Which gives me an error:
Failure/Error: expect(helper.search_by_name_active).to eq('active')
expected: "active"
got: nil
If I remove the brackets:
def search_by_name_active
if request.path == name_search_registrants_path \
|| new_registrant_path
'active'
end
end
The first spec will passed but not the below one:
it "should return nil if current page is not search_by_name" do
allow(helper.request).to receive(:path).and_return(id_search_registrants_path)
expect(helper.search_by_name_active).to be_nil
end
Failure/Error: expect(helper.search_by_name_active).to be_nil
expected: nil
got: "active"
WTF?! Is there any other way to write this logical equation besides an additional if like below?
def search_by_name_active
if request.path == name_search_registrants_path
'active'
elsif request.path == new_registrant_path
'active'
end
end

This behaviour is expected in all programming languages, not just ruby. To simplify your example a little:
x == (a || b)
...is not equivalent to:
(x == a) || (x == b)
The first expression is evaluating (a || b) before comparing it to x. So you're only comparing x to one of the values, not both of them.
The generic way to write this in all programming languages would to instead use the second code sample above. Or in other words, using your specific example:
if request.path == name_search_registrants_path \
|| request.path == new_registrant_path
Or, there are a couple of ruby-specific ways we can shorten this code:
# Works in any ruby code
if [name_search_registrants_path, new_registrant_path].include?(request.path)
# Works in any rails code
if request.path.in? [name_search_registrants_path, new_registrant_path]
The second example is rails-specific, because it's using this extension to the core ruby language.

Related

Refactoring Rubocop Style/GuardClause in a more readable/maintainable way

Rubocop complains: Style/GuardClause: Use a guard clause instead of wrapping the code inside a conditional expression.
if issue_flag == true && issue_notification_sent && !issue_notification_follow_up_sent && send_follow_up
^^
My original code is
if issue_flag == true && issue_notification_sent && !issue_notification_follow_up_sent && send_follow_up
email_address = "sales#test.com"
puts "Emailing Follow Up #{email_address} - #{sales_order}"
UserMailer.issue_notification(self, email_address).deliver_now
update(issue_notification_follow_up_sent: true)
end
and from reading the docs, it seems that I can solve this by implementing the following code instead:
return unless issue_flag == true && issue_notification_sent && !issue_notification_follow_up_sent && send_follow_up
email_address = "sales#test.com"
puts "Emailing Follow Up #{email_address} - #{sales_order}"
UserMailer.issue_notification(self, email_address).deliver_now
update(issue_notification_follow_up_sent: true)
I can see that this essentially breaks early from the method unless the condition is met, but to me, this seems less readable. It also seems less maintainable as further conditionals cannot be added after this code unless they pass the condition on the first line, for instance, to execute something else if issue_flag == true && !issue_notification_sent (anything matching this condition would have already returned on line 1 of the refactored code above).
Is there a better way to refactor this so that more conditions could be added after the code below, without the code returning prematurely?
Thanks.
I think we can do something like below
# issue_flag is boolean so we can directly put it
# create a new method with all the condition and give a proper name
return unless issue_flag && send_follow_up? # change name accourdingly
email_address = "sales#test.com"
puts "Emailing Follow Up #{email_address} - #{sales_order}"
UserMailer.issue_notification(self, email_address).deliver_now
update(issue_notification_follow_up_sent: true)
end
# document its behaviour
def send_follow_up?
issue_notification_sent && !issue_notification_follow_up_sent && send_follow_up
end
The guard structure is used to send the control out of block so if you need change something or do something after the condition then you will not be able to use the guard clause. Have a look at the below code in such scenarios we will not use guard clause
def test
if something_here?
do_something
end
do_something_else
do_something_else1
do_something_else2
end
I would probably extract most part of the method as privates with clear names that tells the intention. Pseudocode implementation would look like this:
def method_name
return unless flag? && issue_notification_sent_with_follow_up
log_follow_up
UserMailer.issue_notification(self, #email_address).deliver_now
update(issue_notification_follow_up_sent: true)
end
private
def flag?
issue_flag == true
end
def issue_notification_sent_with_follow_up
issue_notification_sent && !issue_notification_follow_up_sent && send_follow_up
end
def log_follow_up
#email_address = "sales#test.com"
puts "Emailing Follow Up #{#email_address} - #{sales_order}"
end

variable assigned in conditional is used in same conditional, throws error

I'm trying to do something like this:
if user = User.find_by(email: 'example#example.com') && !user.activated?
# do something
end
but I get an error thrown saying "no method 'activated' for nil:NilClass"
Is there a way for me to accomplish this functionality without using a nested conditional?
You can use the control flow operator and over the logical operator && like so:
if user = User.find_by(email: 'example#example.com') and !user.activated?
# do something
end
Example:
if a = 12 && a.even?
"Working"
end
#=> undefined method `even?' for nil:NilClass
if b = 12 and b.even?
"Working"
end
#=> "Working"
This works because and has a lower precedent than assignment so the assignment will occur prior to the second conditional check.
As long as you don't mind the found = in conditional, should be == warnings.
Second Option:
Your other option is explicit parentheses ie.
if a = 12 && a.even?
"Working"
end
#=> undefined method `even?' for nil:NilClass
if (b = 12) && b.even?
"Working"
end
#=> "Working"
This works because the parentheses () will be evaluated first before the conditional is evaluated so the assignment will occur inside the parens and then be evaluated as part of the conditional.
More on Ruby Operator Precedence
Nope, You can not assign and use the same var in same line, you will need to break it up.
user = User.find_by(email: 'example#example.com')
if user && !user.activated?
# do something
end
Let me know if this helps. You can check user.nil? but above will work as well.
Thats the case when a user is not found. You should add exception handling in condition:
if user = User.find_by(email: 'example#example.com') && !user.try(:activated)?
# do something
end

facing issues about array in rails

I am implementing a program in rails where there is a form and after submitting the form it will check if there is any record with duplicate value for a specific field in database. My database table is students. So my corresponding model name is Student. I am writing this code (what I have just discussed) in my controller.
But I am facing the following error. I am using some arrays for internal operations. When I wrote that particular function in ruby only(not rails) then it was working fine. Moreover I am also facing error due to the use of "length".
My error is:
NoMethodError in StudentsController#create
undefined method `[]' for nil:NilClass
My controller code is:
class StudentsController < ApplicationController
def new
#student=Student.new
#students=Student.all
end
def create
#student=Student.new(u_params)
ret_val=string_check
if ret_val==1
#student.save
redirect_to new_student_path , :notice => "Inserted!!!"
else
redirect_to new_student_path , :notice => "Match,Not inserted!!!"
end
end
def u_params
params.require(:student).permit(:id ,:firstname, :lastname)
end
def u_params_second
params.require(:student).permit(:firstname)
end
def string_check
count =0;
#temp1=:firstname
temp1=params[:firstname]
supplied_val=temp1
puts "Entered in string_check method"
for i in 46..100
temp2=Student.find_by(id:i)
table_val=temp2.firstname
size1=supplied_val.to_s.length
size2=table_val.to_s.length
arr1=Array.new
arr2=Array.new
# arr1[i] ||= []
# arr2[i] ||= []
for i in 0..size1
arr1.push(supplied_val[i])
end
for i in 0..size2
arr2.push(table_val[i])
end
for i in 0..size1
if arr1[i]=="#" || arr1[i]=="#" || arr1[i]=="{" || arr1[i]=="}" || arr1[i]=="(" || arr1[i]==")" || arr1[i]=="[" || arr1[i]=="]" || arr1[i]=="." || arr1[i]==";" || arr1[i]=="," || arr1[i]=="%" || arr1[i]=="&" || arr1[i]=="*" || arr1[i]=="!" || arr1[i]=="?" || arr1[i]=="$" || arr1[i]=="^" || arr1[i]==":" || arr1[i]=="-" || arr1[i]=="/"
count=count+1
# puts count
arr1[i]=""
end
end
# puts arr1
puts arr1.join
final1=arr1.join
for i in 0..size2
if arr2[i]=="#" || arr2[i]=="#" || arr2[i]=="{" || arr2[i]=="}" || arr2[i]=="(" || arr2[i]==")" || arr2[i]=="[" || arr2[i]=="]" || arr2[i]=="." || arr2[i]==";" || arr2[i]=="," || arr2[i]=="%" || arr2[i]=="&" || arr2[i]=="*" || arr2[i]=="!" || arr2[i]=="?" || arr2[i]=="$" || arr2[i]=="^" || arr2[i]==":" || arr2[i]=="-" || arr2[i]=="/"
count=count+1
# puts count
arr2[i]=""
end
end
# puts arr2
puts arr2.join
final2=arr2.join
if final1==final2
flag=0
else
flag=1
end
return flag
end
end
end
The routes.rb file is:
Rails.application.routes.draw do
resources :students
end
My error is: NoMethodError in StudentsController#create
undefined method `[]' for nil:NilClass
It simply means that you are trying to access something as an array that is actually a nil object, and not an array.
To get rid of this error, you can a technique called short-circuit in Ruby.
Let's say your following piece of code is producing the said error:
arr1[i]
You can use an if condition like this:
if arr1
arr1[i]
end
Or use short-circuit technique like this:
arr1 && arr1[i]
If you sure that the relevant code snippet was working for ruby and it's not for rails, the problem is most likely due to variable i used at inner and outer loops both. In any case, this needs to be fixed first or else it will result in unexpected behaviour only.
Outer Loops:
for i in 46..100
Inner Loops:
for i in 0..size1
for i in 0..size2
...
Keep i for outer loop and change the inner loop iterator to j
Hope it Helps : )
Adding to the answers of #harish and #arslan, there may be a case where, temp2=Student.find_by(id:i) may fail because there may not be a student with that id.
So, temp2 may return nil at that time.
for i in 0..size2
arr2.push(table_val[i]) // this may get failed
end
Then arr2.push will not work because table_val[i] is nil, so there are chances of undefined method [] for nil class.

Syntax Error: unexpected $end when using if/else if?

I'm getting an error from one of my controller classes and I can't figure out why. The error is:
SyntaxError in TermsController#show, syntax error, unexpected $end, expecting keyword_end
Here is terms_controller.rb:
class TermsController < ApplicationController
def show
#term = Term.find(params[:id])
if #term.id == 1
#title = "Fall"
else if #term.id == 2
#title = "Winter"
else if #term.id == 3
#title = "Spring"
else if #term.id == 4
#title = "Summer"
end
end
end
My show page currently just consists of:
<h1> <%= #title %> </h1>
It's probably something small that I'm just missing - thanks for your help!
The issue that there are not enough end keywords and it found $end (the token representing the end of the file) before it could find what it was looking for -- another end. (The parser token for the end keyword is either "keyword_end" or "Kend", depending upon ruby version.)
Each if expression requires a matching end keyword.
To get around this, use elsif instead of else if. It is part of the same if construct and does not require a matching end (only the if requires the matching end).
if x == 1
"1"
elsif x == 2
"2"
else
"else"
end
Another option is case which works well if all branches check the same conditional operand (x in this case):
case x
when 1 then "1"
when 2 then "2"
else "else"
end
If you do want to use else if (remember, each if starts a new if conditional construct) then make sure close each block that a if opens. I have indented the code to show this point better.
if x == 1
"1"
else
if x == 2
"2"
else
"else"
end
end
Happy coding.
For the pedantic: there is also another form of if, which is expr if cond, which doesn't have a matching end as part of the syntax and the rules talked about above do not apply to it.
In addition, if and case are just expressions in Ruby, so it might be more idiomatically written like this
#term = Term.find(params[:id])
#title = case #term.id
when 1 then "Fall"
when 2 then "Winter"
when 3 then "Spring"
when 4 then "Summer"
else "Invalid Term"
end
The if/elsif/end syntax can be used in the same way, but using case avoids the repeated mentioning of #term.id. Another option is to use a Hash to perform this sort of simple mapping -- or the mapping can be encapsulated in a separate method -- but that is covered elsewhere ;-)
Instead of else if use elsif.
Why not just do this:
class TermsController < ApplicationController
##seasons = { 1 => "Fall", 2 => "Winter", 3 => "Spring", 4 => "Summer"}
def show
#term = Term.find(params[:id])
#title = ##seasons[params[:id]] || "Invalid Season"
end
end

comparing params in rails

I am doing
if params[:type] = "Type A"
# do something
end
if params[:type] = "Type B"
# do something
end
But I think that is wrong. I should be using ==
However that gives me error:
You have nil object when you didn't expect it
What is the best way to do this in rails?
All I am doing is getting a radio button variable from a form and doing logic based on its value (either Type A or Type B)
Preamble
class Hash
def try(arg)
self[arg] rescue nil
end
end
Your way
if params.try(:type) == "Type A"
# do
elsif params.try(:type) == "Type B"
# do
end
DRY
case params.try(:type)
when "Type A"
# do
when "Type B"
# do
else
# default do
end
You're sure it should be params[:type]? First, check your logs to see what is inside params before you access action in controller.
To check multiple choices you can use switch construct:
case params[:type]
when "Type A"
# do sth
when "Type B"
# do sth
else # not A nor B, can be nil
# do nothing
end
And if you need to deeper inside params then you can use if/else:
if params[:type] && params[:type][:value] == "sth"
# do sth
elsif params[:type] && params[:type][:value] == "..."
# do sth
end
And check where you get your error from, because in Ruby you can easily compare nil and String, so it's not because of using == in your example.

Resources