Reputation: 1701
I have something that should be really simple, but it's killing me.
l = LineItem.first
#<LineItem id: 5, product_id: 1, quantity: 1, price: #<BigDecimal:7f7fdb51a3f8,'0.999E3',9(18)>, cart_id: 5, discount_percentage: 10, discount_amount: nil, discount_active: true, created_at: "2012-01-12 16:17:41", updated_at: "2012-01-12 16:17:41">
I have
l.discount_percentage.blank?
=> false
So, I have the following method:
def total_price
discount_amount = 0 if discount_amount.blank?
discount_percentage = 0 if discount_percentage.blank?
discounted_amount_from_percent = price*(discount_percentage.to_f/100)
applicable_discount = [discount_amount,discounted_amount_from_percent].max
return (price-applicable_discount)
end
But when I do this:
l.total_price
Instead of returning 899, it returns 999 (meaning that the if discount_percentage.blank? didn't work at all!)
Or the syntax WHATEVER_HERE if true/false only work in the View on Rails??
Upvotes: 2
Views: 276
Reputation: 67900
Here lays the problem:
discount_amount = 0 if discount_amount.blank?
discount_percentage = 0 if discount_percentage.blank?
Ruby "sees" variables from top to bottom and from left to right, so in that line he first sees a local variable (discount_amount =
) so he decides this discount_amount
thing in discount_mount.blank?
is that same local variable (and not the instance method. You think the variable is not defined yet, but Ruby has already spotted it). Not having any value yet, discount_amount
it set to default value nil
, so nil.blank?
succeeds and the assignment discount_percentage = 0
is made. Ditto for discount_percentage
. Here's a demo snippet:
class ExampleClass
def run
x = "it works as expected" if x == "x"
x
end
def run2
if x == "x"
x = "it works as expected"
end
x
end
def run3
xy = "it works as expected" if x == "x"
xy
end
def x; "x"; end
end
p ExampleClass.new.run #=> nil
p ExampleClass.new.run2 #=> "it works as expected"
p ExampleClass.new.run3 #=> "it works as expected"
Step 1: don't use the same names for local variables and instance methods. That's usually a bad idea anyway because you lose track of which one you are using, but in this case it has really bitten you.
Step 2: Do not write imperative code when you're doing math calculations! Really, maths (9X % of the things you do in a typical application, (10-X)% being unavoidable side-effects) play well with expressions, not with statements. I'd write:
def total_price
final_discount_amount = discount_amount || 0
final_discount_percentage = discount_percentage || 0
discounted_amount_from_percent = price * (final_discount_percentage.to_f/100)
applicable_discount = [final_discount_amount, discounted_amount_from_percent].max
price - applicable_discount
end
Upvotes: 6
Reputation: 47951
When using attribute writers (e.g foo = ...
), you should use self
explicitly. This is nicely explained here.
So your code should be like this:
def total_price
self.discount_amount = 0 if discount_amount.blank?
self.discount_percentage = 0 if discount_percentage.blank?
# local var, self not necessary
discounted_amount_from_percent = price*(discount_percentage.to_f/100)
# local var, self not necessary
applicable_discount = [discount_amount,discounted_amount_from_percent].max
return (price-applicable_discount)
end
This is also explained in the Programming Ruby book:
Why did we write
self.leftChannel
in the example on page 74? Well, there's a hidden gotcha with writable attributes. Normally, methods within a class can invoke other methods in the same class and its superclasses in functional form (that is, with an implicit receiver ofself
). However, this doesn't work with attribute writers. Ruby sees the assignment and decides that the name on the left must be a local variable, not a method call to an attribute writer.
Upvotes: 1