AshNaz87
AshNaz87

Reputation: 434

How to round Decimals to the First Significant Figure in Ruby

I am attempting to solve an edge case to a task related to a personal project.

It is to determine the unit price of a service and is made up of the total_amount and cost.

Examples include:

# 1
unit_price = 300 / 1000 # = 0.3

# 2
unit_price = 600 / 800 # = 0.75

# 3
unit_price = 500 / 1600 # = 0.3125

For 1 and 2, the unit_prices can stay as they are. For 3, rounding to 2 decimal places will be sufficient, e.g. (500 / 1600).round(2)

The issue arises when the float becomes long:

# 4
unit_price = 400 / 56000 # = 0.007142857142857143

What's apparent is that the float is rather long. Rounding to the first significant figure is the aim in such instances.

I've thought about using a regular expression to match the first non-zero decimal, or to find the length of the second part and apply some logic:

Any assistance would be most welcome

Upvotes: 3

Views: 551

Answers (3)

Cary Swoveland
Cary Swoveland

Reputation: 110665

def my_round(f)
  int = f.to_i
  f -= int
  coeff, exp = ("%e" % f).split('e')
  "#{coeff.to_f.round}e#{exp}".to_f + int
end

my_round(0.3125)
  #=> 0.3 
my_round(-0.3125)
  #=> -0.3 
my_round(0.0003625)
  #=> 0.0004 
my_round(-0.0003625)
  #=> -0.0004 
my_round(42.0031)
  #=> 42.003 
my_round(-42.0031)
  #=> -42.003 

The steps are as follows.

f = -42.0031
int = f.to_i
  #=> -42 
f -= int
  #=> -0.0031000000000034333 
s = "%e" % f
  #=> "-3.100000e-03" 
coeff, exp = s.split('e')
  #=> ["-3.100000", "-03"] 
c = coeff.to_f.round
  #=> -3 
d = "#{c}e#{exp}"
  #=> "-3e-03" 
e = d.to_f
  #=> -0.003 
e + int 
  #=> -42.003 

To instead keep only the most significant digit after rounding, change the method to the following.

def my_round(f)
  coeff, exp = ("%e" % f).split('e')
  "#{coeff.to_f.round}e#{exp}".to_f
end

If f <= 0 this returns the same as the earlier method. Here is an example when f > 0:

my_round(-42.0031)
  #=> -40.0

Upvotes: 0

mrzasa
mrzasa

Reputation: 23307

You can detect the length of the zeros string with regex. It's a bit ugly, but it works:

def significant_round(number, places)
  match = number.to_s.match(/\.(0+)/)
  return number unless match
  zeros = number.to_s.match(/\.(0+)/)[1].size
  number.round(zeros+places)
end  

pry(main)> significant_round(3.14, 1)
=> 3.14
pry(main)> significant_round(3.00014, 1)
=> 3.0001

Upvotes: 1

Aleksei Matiushkin
Aleksei Matiushkin

Reputation: 121000

One should use BigDecimal for this kind of computation.

require 'bigdecimal'

bd = BigDecimal((400.0 / 56000).to_s)
#⇒ 0.7142857142857143e-2
bd.exponent
#⇒ -2

Example:

[10_000.0 / 1_000, 300.0 / 1_000, 600.0 / 800,
                   500.0 / 1_600, 400.0 / 56_000].
  map { |bd| BigDecimal(bd.to_s) }.
  map do |bd|
    additional = bd.exponent >= 0 ? 0 : bd.exponent + 1 
    bd.round(2 - additional) # THIS
  end.
  map(&:to_f)
#⇒ [10.0, 0.3, 0.75, 0.31, 0.007]

Upvotes: 3

Related Questions