How to fix "String to Integer" error in Ruby - ruby-on-rails

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

Related

Shortening a long if else statement

I have a long if else statement:
rnd = rand(1..1000)
if rnd >= 600
0
elsif rnd < 600 && rnd >= 350
1
elsif rnd < 350 && rnd >= 270
2
elsif rnd < 270 && rnd >= 200
3
elsif rnd < 200 && rnd >= 150
4
elsif rnd < 150 && rnd >= 100
5
elsif rnd < 100 && rnd >= 80
6
elsif rnd < 80 && rnd >= 50
7
elsif rnd < 50 && rnd >= 30
8
else
9
end
I would like to shorten it. Is it possible?
My rubocop swears at this long method.
I would start with something like this:
RANGES = {
(0...30) => 9,
(30...50) => 8,
(50...80) => 7,
# ...
(350...600) => 1,
(600...1000) => 0
}
rnd = rand(1..1000)
RANGES.find { |k, _| k.cover?(rnd) }.last
Great answers already! Just chiming in since I had a suspicion that ruby could handle this with a case statement, and it appears to be able to do so:
rnd = rand(1..1000)
case rnd
when 600.. then 0
when 350...600 then 1
when 270...350 then 2
...
else 9
end
Regardless of the approach taken, you're going to have to specify the ranges somewhere, so I think using something like a case statement is appropriate here (sorry! It doesn't shorten the code more than a few lines). Using a hash would also be a great approach (and might allow you to move the hash elsewhere), as other commenters have already shown.
It's worth mentioning, with ruby ranges, .. means that the range is inclusive and includes the last value (1..10 includes the number 10), and ... means the range is exclusive where it does not include the last value.
The top case 600.. is an endless range, which means it will match anything greater than 600. (That functionality was added in ruby 2.6)
You can simplify your conditions by using only the lower bound.
And you can avoid repeting elsif because it is cumbersome
rnd = rand(1..1000)
lower_bounds = {
600 => 0,
350 => 1,
270 => 2,
200 => 3,
150 => 4,
100 => 5,
80 => 6,
50 => 7,
30 => 8,
0 => 9,
}
lower_bounds.find { |k, _| k <= rnd }.last
MX = 1000
LIMITS = [600, 350, 270, 200, 150, 100, 80, 50, 30, 0]
The required index can be computed as follows.
def doit
rnd = rand(1..MX)
LIMITS.index { |n| n <= rnd }
end
doit
#=> 5
In this example rnd #=> 117.
If this must be repeated many times, and speed is paramount, you could do the following.
LOOK_UP = (1..MX).each_with_object({}) do |m,h|
h[m] = LIMITS.index { |n| n <= m }
end
#=> {1=>9, 2=>9,..., 29=>9,
# 30=>8, 31=>8,..., 49=>8,
# ...
# 600=>0, 601=>0,..., 1000=>0}
Then simply
def doit
LOOK_UP[rand(1..MX)]
end
doit
#=> 3
In this example rand(1..MX) #=> 262.
If speed were paramount but MX were so large that the previous approach would require excessive memory, you could use a binary search.
def doit
rnd = rand(1..MX)
LIMITS.bsearch_index { |n| n <= rnd }
end
doit
#=> 5
In this example rnd #=> 174.
See Array#bsearch_index. bsearch_index returns the correct index in O(log n), n being LIMITS.size). bsearch_index requires the array on which it operates to be ordered.

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

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!

Weighted Average Grade Calculation - hash iteration with nil

I'm having trouble calculating a weighted ratio in Ruby and Rails. As an illustrative scenario, say we have a a weighted average grade calculation for a class in school. I have tests "a", "b" and "c" which have class-level weighting of 0.25, 0.50, and 0.25, respectively. Jenny's scores are 0.95, 0.85 and nil, respectively where she was exempt for the last test. When a student is exempt from the test, the weighting should not count against them. Consider that jenny_grade_scores below is an ActiveRecord Object and TEST_WEIGHTS is a constant in the model.
jenny_grade_scores = { "test_a" => 0.95, "test_b" => 0.85, "test_c" => nil }
TEST_WEIGHTS = { "test_a_weight" => 0.25, "test_b_weight" => 0.50, "test_c_weight" => 0.25 }
What's a more efficient way to calculate the weighted score for this than using:
jenny_test_weights = TEST_WEIGHTS #initialize a student test weight variable and adjust if test score is nil
jenny_test_weights["test_a_weight"] = 0.0 if jenny_grade_scores["test_a"].nil?
jenny_test_weights["test_b_weight"] = 0.0 if jenny_grade_scores["test_b"].nil?
jenny_test_weights["test_c_weight"] = 0.0 if jenny_grade_scores["test_c"].nil?
numerator = (jenny_grade_scores["test_a"] * jenny_test_weights["test_a_weight"] +jenny_grade_scores["test_b"] * jenny_test_weights["test_b_weight"] + jenny_grade_scores["test_c"] * jenny_test_weights["test_c_weight"])
denominator = (jenny_test_weights["test_a_weight"] + jenny_test_weights["test_b_weight"] + jenny_test_weights["test_c_weight"])
final_score = numerator / denominator
In this example, the result should be 0.88333333
What about converting all test scores to float?
>> jenny_grade_scores.inject({}) { |hash, array| hash[array[0]] = array[1].to_f; hash }
=> {"test_a"=>0.95, "test_b"=>0.85, "test_c"=>0.0}
If you don't mind refactoring your code, consider the following:
WEIGHTS = { a: 0.25, b: 0.5, c: 0.25 }
grades = { a: 0.95, b: 0.85 , c: nil }
SUM_OF_WEIGHTS = WEIGHTS.inject(0) do |sum, (test, weight)|
sum += grades[test].nil? ? 0 : weight
end # => 0.75
weighted_score = grades.inject(0) do |w, (test, score)|
w += (score.to_f * WEIGHTS[test])
end # => 0.6625
final_score = weighted_score / SUM_OF_WEIGHTS # => 0.8833333333333333
Footnotes:
Renaming the test keys to same key in all related hashes can simplify the problem.
nil when converted to float becomes 0.0.
inject & reduce can do summation or similar collection related operation elegantly. More over its more idiomatic Ruby, try & learn them if you are not familiar with it.
Updated for non-penalized weight calculation.

Resources