How to round Decimals to the First Significant Figure in Ruby - ruby-on-rails

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:
unit_price.match ~= /[^.0]/
unit_price.to_s.split('.').last.size
Any assistance would be most welcome

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]

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

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

Related

How to fix "String to Integer" error in Ruby

I need the shipping cost to be determined by the various rates.
I've tried a hodgepodge of things for the last 5 hours.
If I'm supposed to use .to_s .to_f, I've tried and done so incorrectly.
if (weight < 2)
rate = 0.10
elsif ((weight >= 2) or (weight < 10))
rate = 0.20
elsif ((weight >= 10) or (weight < 40))
rate = 0.30
elsif ((weight >= 40) or (weight < 70))
rate = 0.50
elsif ((weight >= 70) or (weight < 100))
rate = 0.75
else (weight >= 100)
rate = 0.90
end
rate = rate.to_i
ship_cost = weight * price * rate
ship_cost = ship_cost.to_i
The result is supposed to show a shipping cost after the rate is applied. I keep getting to the String to Integer error.
The problem is somehow one or more variables within the multiplication is a string, which results in the TypeError error you're getting, as in:
'a' * 'b' # '*': no implicit conversion of String into Integer (TypeError)
If you want to suppress the error, you can manually convert them into integers or floats. Meaning if the string doesn't have a numeric representation, it'll return 0:
'asd'.to_i # 0
'1'.to_i. # 1
'-9.9'.to_i # -9
'-9.9'.to_f # -9.9
Alternatively, you can handle the rate assignation by using a "dictionary" which holds the min and max value weight can be to return X. Creating a range from min to max and asking if it includes the value of weight you can get assign its value:
dict = {
[-Float::INFINITY, 2] => 0.10,
[2, 10] => 0.20,
[10, 40] => 0.30,
[40, 70] => 0.50,
[70, 100] => 0.75,
[100, Float::INFINITY] => 0.90
}
p dict.find { |(start, finish), _| (start...finish).include?(-42.12) }.last # 0.1
p dict.find { |(start, finish), _| (start...finish).include?(0) }.last # 0.1
p dict.find { |(start, finish), _| (start...finish).include?(1) }.last # 0.1
p dict.find { |(start, finish), _| (start...finish).include?(23) }.last # 0.3
p dict.find { |(start, finish), _| (start...finish).include?(101) }.last # 0.9
A less verbose and more idiomatically correct solution is to use a case statement with ranges:
def shipping_rate(weight)
case weight
when 0...2
0.10
when 2...10
0.20
when 10...40
0.30
when 40...70
0.50
when 70...100
0.75
when 100...Float::INFINITY
0.90
end
end
Declaring a range with ... excludes the end value. So (40...70).cover?(70) == false. That lets us avoid overlap issues.
require "minitest/autorun"
class TestShippingRate < Minitest::Test
def test_correct_rate
assert_equal 0.10, shipping_rate(1)
assert_equal 0.20, shipping_rate(3)
assert_equal 0.30, shipping_rate(39)
assert_equal 0.50, shipping_rate(40)
assert_equal 0.75, shipping_rate(70)
assert_equal 0.90, shipping_rate(101)
end
end
# Finished in 0.002255s, 443.3896 runs/s, 2660.3374 assertions/s.
# 1 runs, 6 assertions, 0 failures, 0 errors, 0 skips
If you want to use a dict like Sebastian Palma suggested you can do it with hash with ranges for keys instead:
def shipping_rate(weight)
{
0...2 => 0.10,
2...10 => 0.20,
10...40 => 0.30,
40...70 => 0.50,
70...100 => 0.75,
100...Float::INFINITY => 0.90
}.find { |k, v| break v if k.cover? weight }
end
Using case is a bit more flexible though as you can add a else condition or handle string arguments:
def shipping_rate(weight)
case weight
when 0...2
0.10
when 2...10
0.20
when 10...40
0.30
when 40...70
0.50
when 70...100
0.75
when 100...Float::INFINITY
0.90
# I'm not saying this is a good idea as the conversion should happen
# upstream. Its just an example of what you can do
when String
shipping_rate(weight.to_f) # recursion
else
raise "Oh noes. This should not happen."
end
end

How to divide Time in float form (hh.mm) by an integer in ruby?

I am trying to divide Time in float form (hh.mm) by an integer.
For example 1.30 by 2 must give 00.45.
Is there any simple way to do this?
Using a float to express h.mm is a bit unusual. You would typically use strings for formatting.
However, I'd start by extracting hours and minutes from the float value. To do so, I would convert the float to a string using format:
time = 1.3
time_str = format('%.2f', time)
#=> "1.30"
Then I would split the string at . to get the hour part and minutes part and call to_i to convert them to actual integers: (I'm using map here, you could also call h = h.to_i / m = m.to_i afterwards)
h, m = time_str.split('.').map(&:to_i)
h #=> 1
m #=> 30
Now that we have the numbers 1 and 30 as integers, we can easily calculate the total duration in minutes:
duration = h * 60 + m
#=> 90
I would then divide the duration by 2 (or whatever value):
duration /= 2
#=> 45
and convert it back to hours and minutes using divmod: (it returns both values at once)
h, m = duration.divmod(60)
h #=> 0
m #=> 45
We can format these as a string:
format('%02d.%02d', h, m)
#=> "00.45"
or convert it back to a float:
time = h + m.fdiv(100)
#=> 0.45
Which can be formatted like this:
format('%05.2f', time)
#=> "00.45"
time = 1.3
divisor = 2
hr, min = (time.fdiv(divisor)).divmod(1)
#=> [0, 0.65]
min = (60 * min).round
#=> 39
"%02d.%02d" % [hr, min]
#=> "00.39"
Another example.
time = 1005
divisor = 5
hr, min = (time.fdiv(divisor)).divmod(1)
#=> [201, 0.0]
"%02d.%02d" % [hr, (60 * min).round]
#=> "201.00"
See Integer#fdiv, Float#fdiv, Integer#divmod and Integer#round. divmod is an extremely useful method that, for reasons I don't understand, seems to be under-used.
Maybe you can split into an array an then:
n = 2
[1, 30].then { |h, m| [h / n, (m + h % n * 60) / n]}
#=> [0, 45]
For splitting:
num = 1.3
('%.2f' % num).split('.').map(&:to_i) #=> [1, 30]
You can try the following :
num = 1.3
splitted_values = ('%.2f' % num).split('.').map(&:to_i) => [1, 30]
((splitted_values[0] * 60) / 2) + (splitted_values[1] / 2 ) => 45

Rails rounding decimal to the nearest power of ten

I'm looking for a rails function that could return the number to the nearest power of ten(10,100,1000), and also need to support number between 0 and 1 (0.1, 0.01, 0.001):
round(9) = 10
round(19) = 10
round(79) = 100
round(812.12) = 1000
round(0.0321) = 0.01
round(0.0921) = 0.1
I've looking on : Round number down to nearest power of ten
the accepted answer using the length of the string, that can't applied to number between 0 and 1.
updated
Round up to nearest power of 10 this one seems great. But I still can't make it work in rails.
I'm not sure about any function which automatically rounds the number to the nearest power of ten. You can achieve it by running the following code:
def rounded_to_nearest_power_of_ten(value)
abs_value = value.abs
power_of_ten = Math.log10(abs_value)
upper_limit = power_of_ten.ceil
lower_limit = power_of_ten.floor
nearest_value = (10**upper_limit - abs_value).abs > (10**lower_limit - abs_value).abs ? 10**lower_limit : 10**upper_limit
value > 0 ? nearest_value : -1*nearest_value
end
Hope this helps.
Let's simplify your problem to the following form - let the input numbers be in the range [0.1, 1), how would rounding of such numbers look like then?
The answer would be simple - for numbers smaller than 0.5 we would return the number 0.1, for larger numbers it would be 1.0.
All we have to do is to make sure that our number will be in that range. We will "move" decimal separator and remember how many moves we made in second variable. This operation is called normalization.
def normalize(fraction)
exponent = 0
while fraction < (1.0/10.0)
fraction *= 10.0
exponent -= 1
end
while fraction >= 1.0
fraction /= 10.0
exponent += 1
end
[fraction, exponent]
end
Using above code you can represent any floating number as a pair of normalized fraction and exponent in base 10. To recreate original number we will move decimal point in opposite direction using formula
original = normalized * base^{exponent}
With data property normalized we can use it in our simple rounding method like that:
def round(number)
fraction, exponent = normalize(number)
if fraction < 0.5
0.1 * 10 ** exponent
else
1.0 * 10 ** exponent
end
end
if the number is >= 1.0, this should work.
10 ** (num.floor.to_s.size - ( num.floor.to_s[0].to_i > 4 ? 0 : 1))
Try this:
def round_tenth(a)
if a.to_f >= 1
return 10 ** (a.floor.to_s.size - ( a.floor.to_s[0].to_i > 4 ? 0 : 1))
end
#a = 0.0392
c = a.to_s[2..a.to_s.length]
b = 0
c.split('').each_with_index do |s, i|
if s.to_i != 0
b = i + 1
break
end
end
arr = Array.new(100, 0)
if c[b-1].to_i > 4
b -= 1
if b == 0
return 1
end
end
arr[b-1] = 1
return ("0." + arr.join()).to_f
end
class Numeric
def name
def helper x, y, z
num = self.abs
r = 1
while true
result = nil
if num.between?(x, y)
if num >= y/2.0
result = y.round(r+1)
else
result = x.round(r)
end
return self.negative? ? -result : result
end
x *= z; y *= z; r += 1
end
end
if self.abs < 1
helper 0.1, 1, 0.1
else
helper 1, 10, 10
end
end
end
Example
-0.049.name # => -0.01
12.name # => 10
and so on, you are welcome!

Opposite of Ruby's number_to_human

Looking to work with a dataset of strings that store money amounts in these formats. For example:
$217.3M
$1.6B
$34M
€1M
€2.8B
I looked at the money gem but it doesn't look like it handles the "M, B, k"'s back to numbers. Looking for a gem that does do that so I can convert exchange rates and compare quantities. I need the opposite of the number_to_human method.
I would start with something like this:
MULTIPLIERS = { 'k' => 10**3, 'm' => 10**6, 'b' => 10**9 }
def human_to_number(human)
number = human[/(\d+\.?)+/].to_f
factor = human[/\w$/].try(:downcase)
number * MULTIPLIERS.fetch(factor, 1)
end
human_to_number('$217.3M') #=> 217300000.0
human_to_number('$1.6B') #=> 1600000000.0
human_to_number('$34M') #=> 34000000.0
human_to_number('€1M') #=> 1000000.0
human_to_number('€2.8B') #=> 2800000000.0
human_to_number('1000') #=> 1000.0
human_to_number('10.88') #=> 10.88
I decided to not be lazy and actually write my own function if anyone else wants this:
def text_to_money(text)
returnarray = []
if (text.count('k') >= 1 || text.count('K') >= 1)
multiplier = 1000
elsif (text.count('M') >= 1 || text.count('m') >= 1)
multiplier = 1000000
elsif (text.count('B') >= 1 || text.count('b') >= 1)
multiplier = 1000000000
else
multiplier = 1
end
num = text.to_s.gsub(/[$,]/,'').to_f
total = num * multiplier
returnarray << [text[0], total]
return returnarray
end
Thanks for the help!

Ruby/Rails while loop not breaking correctly?

I am working on a client's site, and I'm writing an amortization schedule calculator in in ruby on rails. For longer loan term calculations, it doesn't seem to be breaking when the balance reaches 0
Here is my code:
def calculate_amortization_results
p = params[:price].to_i
i = params[:rate].to_d
l = params[:term].to_i
j = i/(12*100)
n = l * 12
m = p * (j / (1 - (1 + j) ** (-1 * n)))
#loanAmount = p
#rateAmount = i
#monthlyAmount = m
#amort = []
#interestAmount = 0
while p > 0
line = Hash.new
h = p*j
c = m-h
p = p-c
line["interest"] = h
line["principal"] = c
if p <= 0
line["balance"] = 0
else
line["balance"] = p
end
line["payment"] = h+c
#amort.push(line)
#interestAmount += h
end
end
And here is the view:
- #amort.each_with_index do |a, i|
%li
.m
= i+1
.i
= number_to_currency(a["interest"], :unit => "$")
.p
= number_to_currency(a["principal"], :unit => "$")
.pp
= number_to_currency(a["payment"], :unit => "$")
.b
= number_to_currency(a["balance"], :unit => "$")
What I am seeing is, in place of $0.00 in the final payment balance, it shows "-$-inf", iterates one more loop, then displays $0.00, but shows "-$-inf" for interest. It should loop until p gets to 0, then stop and set the balance as 0, but it isn't. Any idea what I've done wrong?
The calculator is here. It seems to work fine for shorter terms, like 5 years, but longer terms cause the above error.
Edit:
Changing the while loop to n.times do
and then changing the balance view to
= number_to_currency(a["balance"], :unit => "$", :negative_format => "$0.00")
Is a workaround, but i'd like to know why the while loop wouldn't work correctly
in Ruby the default for numerical values is Fixnum ... e.g.:
> 15 / 4
=> 3
You will see weird rounding errors if you try to use Fixnum values and divide them.
To make sure that you use Floats, at least one of the numbers in the calculation needs to be a Float
> 15.0 / 4
=> 3.75
> 15 / 4.0
=> 3.75
You do two comparisons against 0 , which should be OK if you make sure that p is a Float.
As the other answer suggests, you should use "decimal" type in your database to represent currency.
Please try if this will work:
def calculate_amortization_results
p = params[:price].to_f # instead of to_i
i = params[:rate].to_f # <-- what is to_d ? use to_f
l = params[:term].to_i
j = i/(12*100.0) # instead of 100
n = l * 12
m = p * (j / (1 - (1 + j) ** (-1 * n))) # division by zero if i==0 ==> j==0
#loanAmount = p
#rateAmount = i
#monthlyAmount = m
#amort = []
#interestAmount = 0.0 # instead of 0
while p > 0
line = Hash.new
h = p*j
c = m-h
p = p-c
line["interest"] = h
line["principal"] = c
if p <= 0
line["balance"] = 0
else
line["balance"] = p
end
line["payment"] = h+c
#amort.push(line)
#interestAmount += h
end
end
If you see "inf" in your output, you are doing a division by zero somewhere.. better check the logic of your calculation, and guard against division by zero.
according to Wikipedia the formula is:
http://en.wikipedia.org/wiki/Amortization_calculator
to improve rounding errors, it's probably better to re-structure the formula like this:
m = (p * j) / (1 - (1 + j) ** (-1 * n) # these are two divisions! x**-1 == 1/x
which is equal to:
m = (p * j) + (p * j) / ((1 + j) ** n) - 1.0)
which is equal to: (use this one)
q = p * j # this is much larger than 1 , so fewer rounding errors when dividing it by something
m = q + q / ((1 + j) ** n) - 1.0) # only one division
I think it has something to do with the floating point operations precision. It has already been discussed here: Ruby number precision with simple arithmetic and it would be better to use decimal format for financial purposes.
The answer could be computing the numbers in the loop, but with precomputed number of iterations and from the scratch.

Resources