13

Given that I'd like to do the following calculation:

total = subtotal - discount

Because discount might be greater than subtotal, there is code like the following:

class Calculator
  def initialize(subtotal: subtotal, discount: discount)
    @subtotal = subtotal
    @discount = discount
  end

  def total
    [subtotal - discount, 0].max
  end

  private

  def subtotal
    @subtotal
  end

  def discount
    @discount
  end
end

When seeing the [subtotal - discount, 0].max part or any similar code, I often have to pause and think.

Are there more elegant ways to handle this kind of calculation?

Domon
  • 6,753
  • 1
  • 25
  • 29

5 Answers5

28

I think your solution is essentially correct, and probably the most readable besides a small refactor. I might change it slightly like so:

  def total
    final_total = subtotal - discount
    [final_total, 0].max
  end

The ruby expression [final_total, 0].max is essentially the traditional solution in mathematics for the same function: max {final_total, 0}. The difference is just notation and context. Once you see this max expression once or twice you can read it as follows: "final_total, but at least zero".

Perhaps if you use this expression more than once you can add another at_least_zero method or something like in Shiko's solution.

Rex Butler
  • 1,030
  • 2
  • 13
  • 24
2

Thinking we can extend the Numeric class?

class Numeric                                                                  
  def non_negative                                                             
    self > 0 ? self : 0                                                                      
  end                                                                          
end                                                                            

class Calculator
  def initialize(subtotal: subtotal, discount: discount)
    @subtotal = subtotal
    @discount = discount
  end

  def total
    (@subtotal - @discount).non_negative
  end
end
songyy
  • 4,323
  • 6
  • 41
  • 63
2

A plain if statement might be easier to understand:

def total
  if discount > subtotal
    0
  else
    subtotal - discount
  end
end
Stefan
  • 109,145
  • 14
  • 143
  • 218
2

Some performance numbers:

                        user     system      total        real
[i, 0.0].max        0.806408   0.001779   0.808187 (  0.810676)
0.0 if i < 0.0      0.643962   0.001077   0.645039 (  0.646368)
0.0 if i.negative?  0.625610   0.001680   0.627290 (  0.629439)

Code:

require 'benchmark'

n = 10_000_000
Benchmark.bm do |benchmark|
  benchmark.report('[value, 0.0].max'.ljust(18)) do
    n.times do |i|
      a = [-1*i, 0.0].max
    end
  end

  benchmark.report('0.0 if value < 0.0'.ljust(18)) do
    n.times do |i|
       a = 0.0 if -1*i < 0.0 
    end
  end

  benchmark.report('0.0 if value.negative?'.ljust(18)) do
    n.times do |i|
      a = 0.0 if (-1*i).negative?
    end
  end
end
1

Just to clarify more, we need to add classes to be extended in core_ext.rb . file :
1) Create core_ext.rb file under config\initializers folder in your project.
2) Paste below as mentioned by @songyy in his answer:

class Numeric                                                                  
  def non_negative                                                             
    self > 0 ? self : 0                                                                      
  end                                                                          
end    

Reference:
https://guides.rubyonrails.org/plugins.html#extending-core-classes

Shiko
  • 2,448
  • 1
  • 24
  • 31