Reputation: 1399
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
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
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
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
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