Rubocop claims for too many 'if' statements - ruby

I am measuring my code for a Rails app using Rubocop. This is my code:
def ratings_treatment
if #rating.advertise_id
advertise = Advertise.find(advertise_id)
rating_average(advertise)
elsif #rating.product_id
product = Product.find(product_id)
rating_average(product)
elsif #rating.combo_id
combo = Combo.find(combo_id)
rating_average(combo)
else
establishment = Establishment.find(#rating.establishment_id)
rating_average(establishment)
end
end
It does not pass a test regarding if. It is claimed that there are too many if statements.
I thought of splitting the code into some checker methods, but was wondering if there is some better way to get good metrics and still write good code.

I think you could use a switch statement to assign the argument for rating_average:
def ratings_treatment
average = case
when #rating.advertise_id then Advertise.find(advertise_id)
when #rating.product_id then Product.find(product_id)
when #rating.combo_id then Combo.find(combo_id)
else Establishment.find(#rating.establishment_id)
end
rating_average average
end
Although Rubocop will complain with
Style/EmptyCaseCondition: Do not use empty case condition, instead use
an if expression
You could also simplify your if expression:
def ratings_treatment
average = if #rating.advertise_id
Advertise.find advertise_id
elsif #rating.product_id
Product.find product_id
elsif #rating.combo_id
Combo.find combo_id
else
Establishment.find #rating.establishment_id
end
rating_average average
end

If your #rating has associations set up then you could say:
def ratings_treatment
obj = #rating.advertise || #rating.product || #rating.combo || #rating.establishment
rating_average(obj)
end
or even:
def ratings_treatment
rating_average(#rating.advertise || #rating.product || #rating.combo || #rating.establishment)
end
or perhaps something fancy (and overkill for only four branches):
def ratings_treatment
rating_average(
%w[advertise product combo establishment].lazy.map { |m| #rating.send(m) }.find(&:present?)
)
end
or maybe even:
def rated_object
to_result = ->(m) { #rating.send(m) }
%w[advertise product combo establishment]
.lazy
.map(&to_result)
.find(&:present?)
end
def ratings_treatment
rating_average(rated_object)
end
I'd go so far as to suggest that something like the above rated_object method really should be a method on #rating. You should be able to ask a rating for the thing that was rated without having to manually spin through all the possibilities.
You could even patch that lazy.map... stuff into Enumerable if you wanted something in Enumerable that behaved like e.m1 || e.m2 || e.m3 || ... (been there, done that) all the way down to short circuiting.
I have no idea what Rubocop would make of any of these shenanigans though.

This is a classic case of a style violation that has a deeper underlying cause, i.e. a modelling problem. In your case, it looks like what you're actually trying to achieve is a polymorphic association. If you change your Rating to:
# app/models/rating.rb
class Rating < ApplicationRecord
belongs_to :rateable, polymorphic: true
end
you can have the Rating relate to any number of things, by simply adding this to the other end of the association:
# app/models/product.rb
class Product < ApplicationRecord
has_many :pictures, as: :rateable
end
Your original method, if it is even needed anymore, becomes:
def ratings_treatment
rating_average(#rating.rateable)
end

Related

How to define an inner method whose name finish with a question mark and that has access to the outer method variables?

Here is the target specification:
the method ends with ?
the method can reach the outer block variables, without relying on class attribute shared scope.
Actually I did find a way to respond to this specification:
def resource
user = try_fetch_current_user
define_singleton_method(:access_anyway?) do user&.admin? || false end
#display = OpenStruct.new(
'resource1?': user&.role1? || access_anyway?,
'resource2?': user&.role2? || access_anyway?
)
end
Note: Here define_singleton_method was chose over define_method as the latter was not defined in the context of use.
So that does the trick, but is rather ugly and doesn't highlight well, compared to :
def resource
#user = try_fetch_current_user
def access_anyway? do #user&.admin? || false end
#display = OpenStruct.new(
'resource1?': #user&.role1? || access_anyway?,
'resource2?': #user&.role2? || access_anyway?
)
end
or even
def resource
user = try_fetch_current_user
access_anyway = user&.admin? || false
#display = OpenStruct.new(
'resource1?': user&.role1? || access_anyway,
'resource2?': user&.role2? || access_anyway
)
end
and lastly:
def resource
def access_anyway? do try_fetch_current_user&.admin? || false end
user = try_fetch_current_user
#display = OpenStruct.new(
'resource1?': user&.role1? || access_anyway?,
'resource2?': user&.role2? || access_anyway?
)
end
With the second solution, #user will unnecessarily leak into the view (when used as a RoR controller method). With the third solution, which is far more clean in my opinion, the question mark is lost which makes the intention far less clear at a first glance. And the last one is less cluttered than the first, but not as clean as the third, and will possibly duplicate heavy operations with a second call to try_fetch_current_user.
So, is there a way to express things as cleanly as the third solution, while keeping the question mark, and without calling the try_fetch_current_user twice?
How to define an inner method whose name finish with a question mark and that has access to the outer method variables?
Ruby doesn't have inner methods, so you cannot define one.

Overlap method for Hotel Reservation system Ruby

I am refactoring code for a hotel reservation system, specifically a method to list all the vacancies in the hotel, for a school project. Our instructor provided his solution and unfortunately he and the rest of our school is on break until Monday (when our refactoring assignment is due). StackOverflow, I could utilize your help.
Essentially, my question boils down to why was bang (!) used in the following code.
def overlap?(other)
return !(other.checkout <= #checkin || other.checkin >= #checkout)
end
This code is a method within Module Hotel and class DateRange. It is called in another class to check for vacancies for a given date range, see below.
overlap_reservations = #reservations.select do |res|
res.overlaps(dates)
end
In my mind, I would have not utilized bang. But I am a newbie and am blind to the interplay here. Any help you can provide is appreciated.
This code suffers several severe code smells/glitches, that should never appear in the good ruby code.
def overlap?(other)
return !(other.checkout <= #checkin || other.checkin >= #checkout)
end
one should never use explicit return in the last line of ruby method. Ruby will return the value, the last line was evaluated to, automatically.
one should try to simplify the conditions as possible, since the human brain is vulnerable to double/triple conditions with negations.
one should not mix ! negation with a disjunction, since it is expanded to a conjunction.
I doubt the code above is even correct, if we are talking about commonly used “checkins” and “checkouts.”
The summing up:
def overlap?(other)
# boolean for whether other checkout overlaps
co = other.checkout >= #checkin && other.checkout <= #checkout
# boolean for whether other checkin overlaps
ci = other.checkin >= #checkin && other.checkin <= #checkout
# return a result
ci || co
end
or, using the whole power of ruby:
def overlap?(other)
ci_co_range = #checkin..#checkout
ci_co_range.cover?(other.checkout) || ci_co_range.cover?(other.checkin)
end
or (from your forthcoming Christmas lesson :)
def overlap?(other)
[other.checkout, other.checkin].any? do |co_ci|
(#checkin..#checkout).cover?(co_ci)
end
end
Documentation: Range, Enumerable#any?.

Ruby - Populate and Array with returned method values

So, pretend we have the following three methods that check a grid to determine if there is a winner, and will return true if there is.
def win_diagonal?
# Code here to check for diagonal win.
end
def win_horizontal?
# Code here to check for horizontal win.
end
def win_vertical?
# Code here to check for vertical win.
end
I would like to push the returned values of each method into an Array instead of literally using the method names. Is this possible?
def game_status
check_wins = [win_vertical?, win_diagonal?, win_horizontal?]
if check_wins.uniq.length != 1 # When we don't have only false returns from methods
return :game_over
end
end
What you are looking for will indeed work in ruby.
def hello_world?
"hello world!"
end
a = [hello_world?]
Prints out
=> ["hello world!"]
Hope that helps. IRB is your friend when you wonder if something is possible in Ruby :-)
Simpler way (and very readable) yet:
def game_status
win_vertical? || win_diagonal? || win_horizontal?
end
If, for example, win_vertical? returns true, the other algorithms won't even need to run. You return immediately.
Or, if you need to know in which way the user won, I mean, if you need to preserve the results of all methods after they ran, you can use a hash, like:
{:vertical => win_vertical?, :diagonal => win_diagonal?, :horizontal => win_horizontal?}
This solution, like the array one, is worse than the first one above for it runs all algorithms all the time. If they are complex, you may have a problem. =)
You can do something like this when you really want to store all return values in an array:
def game_status
check_wins = [win_vertical?, win_diagonal?, win_horizontal?]
return :game_over if check_wins.any?
end
For readability I would prefer:
def game_status
return :game_over if win_vertical? || win_diagonal? || win_horizontal?
end

Dynamic methods using define_method and eval

I've put together two sample classes implemented in a couple of different ways which pretty well mirrors what I want to do in my Rails model. My concern is that I don't know what, if any are the concerns of using either method. And I've only found posts which explain how to implement them or a general warning to avoid/ be careful when using them. What I have not found is a clear explanation of how to accomplish this safely, and what I'm being careful of or why I should avoid this pattern.
class X
attr_accessor :yn_sc, :um_sc
def initialize
#yn_sc = 0
#um_sc = 0
end
types = %w(yn um)
types.each do |t|
define_method("#{t}_add") do |val|
val = ActiveRecord::Base.send(:sanitize_sql_array, ["%s", val])
eval("##{t}_sc += #{val}")
end
end
end
class X
attr_accessor :yn_sc, :um_sc
def initialize
#yn_sc = 0
#um_sc = 0
end
types = %w(yn um)
types.each do |t|
# eval <<-EVAL also works
self.class_eval <<-EVAL
def #{t}_add(val)
##{t}_sc += val
end
EVAL
end
end
x = X.new
x.yn_add(1) #=> x.yn_sc == 1 for both
Well, your code looks realy safe. But imagine a code based on user input. It might be look something like
puts 'Give me an order, sir!'
order = gets.chomp
eval(order)
What will happen if our captain will go wild and order us to 'rm -rf ~/'? Sad things for sure!
So take a little lesson. eval is not safe because it evaluates every string it receives.
But there's another reason not to use eval. Sometimes it evaluates slower than alternatives. Look here if interested.

How to define a fixed-width constraint in parslet

I am looking into parslet to write alot of data import code. Overall, the library looks good, but I'm struggling with one thing. Alot of our input files are fixed width, and the widths differ between formats, even if the actual field doesn't. For example, we might get a file that has a 9-character currency, and another that has 11-characters (or whatever). Does anyone know how to define a fixed width constraint on a parslet atom?
Ideally, I would like to be able to define an atom that understands currency (with optional dollar signs, thousand separators, etc...) And then I would be able to, on the fly, create a new atom based on the old one that is exactly equivalent, except that it parses exactly N characters.
Does such a combinator exist in parslet? If not, would it be possible/difficult to write one myself?
What about something like this...
class MyParser < Parslet::Parser
def initialize(widths)
#widths = widths
super
end
rule(:currency) {...}
rule(:fixed_c) {currency.fixed(#widths[:currency])}
rule(:fixed_str) {str("bob").fixed(4)}
end
puts MyParser.new.fixed_str.parse("bob").inspect
This will fail with:
"Expected 'bob' to be 4 long at line 1 char 1"
Here's how you do it:
require 'parslet'
class Parslet::Atoms::FixedLength < Parslet::Atoms::Base
attr_reader :len, :parslet
def initialize(parslet, len, tag=:length)
super()
raise ArgumentError,
"Asking for zero length of a parslet. (#{parslet.inspect} length #{len})" \
if len == 0
#parslet = parslet
#len = len
#tag = tag
#error_msgs = {
:lenrep => "Expected #{parslet.inspect} to be #{len} long",
:unconsumed => "Extra input after last repetition"
}
end
def try(source, context, consume_all)
start_pos = source.pos
success, value = parslet.apply(source, context, false)
return succ(value) if success && value.str.length == #len
context.err_at(
self,
source,
#error_msgs[:lenrep],
start_pos,
[value])
end
precedence REPETITION
def to_s_inner(prec)
parslet.to_s(prec) + "{len:#{#len}}"
end
end
module Parslet::Atoms::DSL
def fixed(len)
Parslet::Atoms::FixedLength.new(self, len)
end
end
Methods in parser classes are basically generators for parslet atoms. The simplest form these methods come in are 'rule's, methods that just return the same atoms every time they are called. It is just as easy to create your own generators that are not such simple beasts. Please look at http://kschiess.github.com/parslet/tricks.html for an illustration of this trick (Matching strings case insensitive).
It seems to me that your currency parser is a parser with only a few parameters and that you could probably create a method (def ... end) that returns currency parsers tailored to your liking. Maybe even use initialize and constructor arguments? (ie: MoneyParser.new(4,5))
For more help, please address your questions to the mailing list. Such questions are often easier to answer if you illustrate it with code.
Maybe my partial solution will help to clarify what I meant in the question.
Let's say you have a somewhat non-trivial parser:
class MyParser < Parslet::Parser
rule(:dollars) {
match('[0-9]').repeat(1).as(:dollars)
}
rule(:comma_separated_dollars) {
match('[0-9]').repeat(1, 3).as(:dollars) >> ( match(',') >> match('[0-9]').repeat(3, 3).as(:dollars) ).repeat(1)
}
rule(:cents) {
match('[0-9]').repeat(2, 2).as(:cents)
}
rule(:currency) {
(str('$') >> (comma_separated_dollars | dollars) >> str('.') >> cents).as(:currency)
# order is important in (comma_separated_dollars | dollars)
}
end
Now if we want to parse a fixed-width Currency string; this isn't the easiest thing to do. Of course, you could figure out exactly how to express the repeat expressions in terms of the final width, but it gets really unnecessarily tricky, especially in the comma separated case. Also, in my use case, currency is really just one example. I want to be able to have an easy way to come up with fixed-width definitions for adresses, zip codes, etc....
This seems like something that should be handle-able by a PEG. I managed to write a prototype version, using Lookahead as a template:
class FixedWidth < Parslet::Atoms::Base
attr_reader :bound_parslet
attr_reader :width
def initialize(width, bound_parslet) # :nodoc:
super()
#width = width
#bound_parslet = bound_parslet
#error_msgs = {
:premature => "Premature end of input (expected #{width} characters)",
:failed => "Failed fixed width",
}
end
def try(source, context) # :nodoc:
pos = source.pos
teststring = source.read(width).to_s
if (not teststring) || teststring.size != width
return error(source, #error_msgs[:premature]) #if not teststring && teststring.size == width
end
fakesource = Parslet::Source.new(teststring)
value = bound_parslet.apply(fakesource, context)
return value if not value.error?
source.pos = pos
return error(source, #error_msgs[:failed])
end
def to_s_inner(prec) # :nodoc:
"FIXED-WIDTH(#{width}, #{bound_parslet.to_s(prec)})"
end
def error_tree # :nodoc:
Parslet::ErrorTree.new(self, bound_parslet.error_tree)
end
end
# now we can easily define a fixed-width currency rule:
class SHPParser
rule(:currency15) {
FixedWidth.new(15, currency >> str(' ').repeat)
}
end
Of course, this is a pretty hacked solution. Among other things, line numbers and error messages are not good inside of a fixed width constraint. I would love to see this idea implemented in a better fashion.

Resources