Arif
Arif

Reputation: 1399

Rails Rounding float with different options

I have a form where user enters a decimal value and a drop down with four options: Dollar (.00), Quarter (.00, .25, .50, .75), Dime (.10, .20, .30 .. .90) and Penny (.01, .02, .03 ... .99). Also there is an option to select either round UP or DOWN.

These options are used to round the value entered by the user. I monkey patched the Float class and added round_to_quarter that works fine:

class Float
  def round_to_quarter
    (self * 4).round / 4.0
  end

  def round_to_dime
    #TODO
  end

  def round_to_penny
    #TODO
  end

  def round_to_dollar
    #TODO
  end  
end

9.22.round_to_quarter #=> 9.25

How do I round the value for Dime(.10, .20, .30 .. .90) and Penny (.01, .02, .03 ... .99) options and round up and down?

The Ruby version is 2.2.3

Upvotes: 1

Views: 902

Answers (4)

engineersmnky
engineersmnky

Reputation: 29308

One more way to handle the situation:

require 'bigdecimal'
class Rounder
  DENOMS = {penny: 0.01, nickel: 0.05, dime: 0.1, quarter: 0.25, half_dollar: 0.5, dollar: 1.0}
  DENOMS.each do |denom,val|
    define_method("round_to_#{denom}") do |direction: :nearest|
        self.send(direction, self.send(:step_def, val))
    end     
  end
  def initialize(v)
    @v = BigDecimal(v.to_s)
    @enum_range = (@[email protected] + 1)
  end

  def round_to(denom,direction: :nearest)
    check_denom(denom)
    if denom.is_a?(Numeric)
      self.send(direction, self.send(:step_def, denom))
    else
      self.public_send("round_to_#{denom}",direction: direction) 
    end    
  end


  private 
    def down(enum)
        enum.reverse_each.detect {|f| f <= @v }
    end
    def up(enum) 
        enum.detect {|f| f >= @v }
    end
    def nearest(enum)
      [up(enum),down(enum)].min_by {|n| (n - @v).abs}
    end
    def step_def(val)
      @enum_range.step(val)
    end  
    def check_denom(denom)
      if denom.is_a?(Numeric)
        raise ArgumentError, "Numeric denom must be greater than 0 and less than or equal to 1" if (denom > 1 || denom <= 0)
      elsif denom.respond_to?(:to_sym)
        raise ArgumentError, "expecting one of #{DENOMS.keys} got :#{denom.to_sym}" unless DENOMS.keys.include?(denom.to_sym)
      else
        raise ArgumentError,"expected Numeric, Symbol or String got #{denom.class}"
      end
    end    
end

This allows flexible implementation for predefined denominations as well as rounding to any precision desired. Obviously this could be optimized a bit for longer precision's by scaling down the @enum_range.

You could patch this in to Numeric to allow direct access:

class Numeric
  def round_to(denom,direction: :nearest)
    Rounder.new(self).round_to(denom,direction: direction)
  end
end

Then usage as such

r = Rounder.new(9.22)
r.round_to_quarter
#=> 9.25
r.round_to_dime(direction: :up)
#=> 9.3
r.round_to(:nickel)
#=> 9.2
r.round_to(0.45, direction: :up)
#=> 9.45
r.round_to({})
#=> ArgumentError: expected Numeric, Symbol or String got Hash
r.round_to(:pound)
#=> ArgumentError: expecting one of [:penny, :nickel, :dime, :quarter,
#     :half_dollar, :dollar] got :pound
77.43.round_to(:quarter)
#=> 77.5
Rounder.new("123.0000001").round_to_half_dollar(direction: :up)
#=> 123.5
#Obviously a Fixnum is already precise but it does still work 
[:down,:up].each do |dir|
  puts "#{dir} => #{12.round_to(:quarter, direction: dir)}"
end
# down => 12.0
# up => 12.0

Upvotes: 1

gwcodes
gwcodes

Reputation: 5690

Here's a generic way to do it for any precision:

class Float
  def round_currency(precision: 1, direction: :none)
    round_method = case direction
      when :none then :round
      when :up   then :ceil
      when :down then :floor
    end

    integer_value = (self * 100).round
    ((integer_value / precision.to_f).send(round_method) * precision / 100.0)
  end
end


# USAGE
9.37.round_currency(direction: :none, precision: 10)
# => 9.4

9.37.round_currency(direction: :up, precision: 25)
# => 9.5

9.37.round_currency(direction: :none)
# => 9.37

# Precision is defined in pennies: 10 dime, 25 quarter, 100 dollar. 1 penny is default

This code converts the float into an integer first to ensure accuracy. Be wary using ceil and floor with floating number arithmetic - due to accuracy errors you could get odd results e.g. 9.37 * 100 = 936.9999999999999. If you floor the result, you'll end up rounding to 9.36

Upvotes: 4

moyinho20
moyinho20

Reputation: 624

Round to penny should work fine just like quarters

def round_to_penny
  ((self * 100).round/100.0)
end 

Round to dime would however reduce to 1 decimal place since you are rounding to 1 decimal place. You could change it to 2 decimal places when displaying the value.

def round_to_dime
  ((self * 10).round/10.0)
end

You could use '%.2f' however:

'%.2f' % 9.25.round_to_dime => "9.30"

Upvotes: 0

Vamsi Krishna
Vamsi Krishna

Reputation: 3782

I think you can try the following overrides...

Ceil goes with UP and Floor goes with DOWN

class Float
  def ceil_to_quarter
    (self * 4).ceil / 4.0
  end

  def floor_to_quarter
    (self * 4).floor / 4.0
  end

  def ceil_to_dime
    (self * 10).ceil / 10.0
  end

  def floor_to_dime
    (self * 10).floor / 10.0
  end

  def ceil_to_penny
    (self * 10).ceil / 10.0
  end 

  def floor_to_penny
    (self * 100).floor / 100.0
  end 
end

Upvotes: 0

Related Questions