How to refactor huge lookup table in Ruby - ruby-on-rails

I have a method:
def assign_value
...
#obj.value = find_value
end
and a huge lookup table:
def find_value
if #var > 0 && #var <= 30
0.4
elsif #var > 30 && #var <= 50
0.7
elsif #var > 50 && #var <= 70
1.1
elsif #var > 70 && #var <= 100
1.5
elsif #var > 100 && #var <= 140
2.10
elsif #var > 140 && #var <= 200
2.95
elsif #var > 200 && #var <= 300
4.35
elsif #var > 300 && #var <= 400
6.15
elsif #var > 400 && #var <= 500
7.85
elsif #var > 500 && #var <= 600
9.65
...
end
and so on for 1800 lines.
Needless to say, it's for the tax department of an unnamed country. Right now, it's written manually (all 1800 lines of it) to account the varying length of the integer ranges and decimal return values.
Without rewriting an entire country's tax code, how would I refactor this?
The ranges and values do change on a yearly basis, and we need to maintain backwards compatibility. A database table would simplify matters, but because we're dealing with a number of different countries, each with different requirements and tax codes, creating a database table isn't as straightforward as it sounds.

#MilesStanfield's answer has pretty much the same impact as yours. As said in my comment, I would use a hash to get rid of all conditions (or cases):
COUNTRY_TAX_RATES = {
0..30 => 0.4,
31..50 => 0.7,
51..70 => 1.1
}
COUNTRY_TAX_RATES[0..30] # 0.4
COUNTRY_TAX_RATES.select { |code| code === 10 }.values.first # 0.4
If you don't change the codes often you could use this. If it does change frequently you might consider using another approach (for example, store in db and manage through an user interface).
UPDATE: Now I'm no longer on my mobile I want to expand this answer a little bit further. It seems you are disregarding #Wand Maker's proposal, but I don't think you should.
Imagine you use my hash. It is convenient, without any conditionals and easy to adjust. However, as Wand Maker points out, everytime either the range or decimal changes you need a developer to update the code. This might be ok, because you only have to do it on a yearly basis, but it is not the cleanest approach.
You want accountants to be able to update the tax rates and codes, instead of developers. So you should probably create a table which contains these attributes. I'm not sure what the ranges and decimals stand for in your example, but I hope you get the idea:
Tax (ActiveRecord) with range_column (Give this an explicit, explanatory name. You could also use min and max range columns), code, rate and country_id
class Tax < ActiveRecord::Base
serialize :range_column
belongs_to :country
end
Country has_many :taxes
If you want to know the tax rate for malta (country_id: 30), with tax code 40 (whatever this might mean) you could do something like this:
Tax.select(:tax_rate).joins(:country).find_by(country_id: 30, range_column: 40).tax_rate # you might need a between statement instead, can't check right now if querying a serialized hash works like this
Now an accountant can update the ranges, decimals or any other attribute when these change (of course you have to build a CRUD for it first).
P.s. Don't mind the naming, not sure what these numbers represent. :)

I would use a case statement with ranges
case #var
when 0..30 then 0.4
when 31..50 then 0.7
when 51..70 then 1.1
...
end

Suppose inc is income and arr is an array of pairs [bp, tax], where
arr[0][0] = 0, meaning bp (income "breakpoint") for the first element of arr is zero;
arr[i][0] < arr[i+1][0], meaning breakpoints are increasing;
tax arr[-1][1] is payable if inc >= arr[-1][0]; and
tax arr[i][1] is payable if arr[i][0] <= inc < arr[i+1][0], 0 <= i <= arr.size-2.
Tax can then be computed
arr.find { |bp,_| inc >= bp }.last
since the breakpoints are increasing.

Related

Most elegant way to add 1 to an integer if a boolean is true in Ruby?

This will be an easy question for you Ruby experts: What is the most elegant way to add the value of 1 to an integer if a boolean is true?
For example, extra_unit is a boolean, and I want to add 1 to the total only if extra_unit is true.
total = unit_price * (units + (extra_unit ? 1 : 0 ))
Is that the most elegant solution, where, by "elegant", I mean in terms of compact but still readable code?
Given the caveat in my comment to the question above, and if I wanted it on one line and didn't care what it did to the mind of a coworker or my future self, I'd do something like this:
total = unit_price * (bonus ? units + 1 : units)
But, really, it could be written more verbosely without affecting the speed, which would increase the readability:
unit_price = 1.00
units = 1
bonus = true
unit_price * (units + (bonus ? 1 : 0 )) # => 2.0
unit_price * (bonus ? units + 1 : units) # => 2.0
multiplier = if bonus
units + 1
else
units
end
unit_price * multiplier # => 2.0
Those all return the same value so they're equivalent in result.
Running some benchmarks:
require 'fruity'
compare do
t1 {unit_price * (units + (bonus ? 1 : 0 ))}
t2 {unit_price * (bonus ? units + 1 : units)}
t3 {
multiplier = if bonus
units + 1
else
units
end
unit_price * multiplier
}
end
# >> Running each test 65536 times. Test will take about 2 seconds.
# >> t2 is similar to t1
# >> t1 is similar to t3
I ran the benchmarks multiple times, and the positions would swap, with t3 being marginally slower twice, but not enough that Fruity would consistently flag it as such.
So, as far as elegance goes, insisting that it being cryptic or as small as possible doesn't necessarily buy us anything useful, so instead go with readable and still fast. From experience, benchmarks are essential if you're going for elegant; Too often I've been surprised when I thought something would be faster because it was concise, when in comparison another more verbose expression ran circles around it.

Ruby operators - formula

I'm trying to make a formula in my project.rb model in my rails 4 app.
I have an attribute in an preference table, called delay. I want to calculate whether the tolerance on one user's part, is close to the delay required by another user.
In my project.rb, I've tried to do this as follows:
def publication_delay_variance
if #current_user.profile.organisation.preference.delay >= #project.profile.organisation.preference.delay
'No problems here'
elsif #current_user.profile.organisation.preference.delay * 90% >= #project.profile.organisation.preference.delay
"Close, but not quite there"
else #current_user.profile.organisation.preference.delay * 50% >= #project.profile.organisation.preference.delay
"We're not in alignment here"
end
end
the current user is the current user who is currently logged in and interacting with the page. The other user is the user who created the project. Each user has an organisation. Each organisation has preferences. I'm trying to compare them.
Can anyone see what I'm doing wrong? I don't have much experience with this. My current attempt generates this error:
syntax error, unexpected >=
...ence.publication_delay * 90% >= #project.profile.organisatio...
..
The problem is that 90% isn't valid in Ruby. You probably meant to use 0.9 instead. Also, your last else should be an elsif:
def publication_delay_variance
if #current_user.profile.organisation.preference.delay >= #project.profile.organisation.preference.delay
'No problems here'
elsif #current_user.profile.organisation.preference.delay * 0.9 >= #project.profile.organisation.preference.delay
"Close, but not quite there"
elsif #current_user.profile.organisation.preference.delay * 0.5 >= #project.profile.organisation.preference.delay
"We're not in alignment here"
end
end
Of course, without an else you don't have a default case, so you should consider what behavior you want if none of those three conditions is true.
P.S. You can make this a lot more readable by assigning those values to local variables with shorter names:
def publication_delay_variance
user_delay = #current_user.profile.organisation.preference.delay
project_delay = #project.profile.organisation.preference.delay
if user_delay >= project_delay
"No problems here"
elsif user_delay * 0.9 >= project_delay
"Close, but not quite there"
elsif user_delay * 0.5 >= project_delay
"We're not in alignment here"
end
end
P.P.S. 0.9 and 0.5 are magic numbers. Consider moving their values into constants.
In Ruby, % is the modulus operator, which takes two arguments x % y and returns the remainder of x / y. It doesn't make sense to have >= immediately after it, which is what the error message is telling you. To represent a percentage in Ruby, use a decimal number, eg 0.9.

BigDecimal to Currency with -0.0

I am working on reports for a website and I am currently thinking of what would be the best way to handle BigDecimal -0.0's.
The database I'm working with has a lot of them. When these -0.0's are put through number_to_currency(), I get "$-0.00". My format for negative numbers is actually "-$x.xx", so note that number_to_currency is not formatting it as a negative number (otherwise there would also be a negative sign in front of the dollar sign), but for some reason the negative sign is being translated along with the 0.
Right now my solution is to do this every time I get an amount from the database:
amount *= -1 if amount == 0 && amount.sign == -1
This changes the -0.0 to a 0.0. It's simple enough, but I can't help but wonder if there is a better solution, or something on BigDecimals or number_to_currency to handle this situation that I'm just not finding.
That is so because the number is converted into a string to be displayed. And:
# to_d converts to BigDecimal, just FYI
"-0".to_d.to_s #=> "-0.0"
Therefore you will have to make it a 0 yourself. But the sign-checks are redundant - a simple comparison with 0 will do the trick:
bdn = "-0".to_d # or BigDecimal.new("-0")
value = bdn.zero? ? 0 : bdn
number_to_currency(value, other_options)
However, you wouldn't want to manually add this check everywhere you're calling number_to_currency. It would be more convenient to create your own modified_number_to_currency method, in your ApplicationHelper, like so:
def modified_number_to_currency( number, options )
value = number.zero? ? 0 : number
number_to_currency(value, options)
end
And then use modified_number_to_currency instead of number_to_currency.
Alternatively, you could overwrite number_to_currency and have it call super in the end. That might also work but I'm not 100% certain.
Coming to your check specifically:
amount *= -1 if amount == 0 && amount.sign == -1
It should simply be:
amount = 0.to_d if amount.zero? # the to_d might or might not be required

How to count the number of decimal places in a Float?

I am using Ruby 1.8.7 and Rails 2.3.5.
If I have a float like 12.525, how can a get the number of digits past the decimal place? In this case I expect to get a '3' back.
Something like that, I guess:
n = 12.525
n.to_s.split('.').last.size
You should be very careful with what you want. Floating point numbers are excellent for scientific purposes and mostly work for daily use, but they fall apart pretty badly when you want to know something like "how many digits past the decimal place" -- if only because they have about 16 digits total, not all of which will contain accurate data for your computation. (Or, some libraries might actually throw away accurate data towards the end of the number when formatting a number for output, on the grounds that "rounded numbers are more friendly". Which, while often true, means it can be a bit dangerous to rely upon formatted output.)
If you can replace the standard floating point numbers with the BigDecimal class to provide arbitrary-precision floating point numbers, then you can inspect the "raw" number:
> require 'bigdecimal'
=> true
> def digits_after_decimal_point(f)
> sign, digits, base, exponent = f.split
> return digits.length - exponent
> end
> l = %w{1.0, 1.1, 1000000000.1, 1.0000000001}
=> ["1.0,", "1.1,", "1000000000.1,", "1.0000000001"]
> list = l.map { |n| BigDecimal(n) }
=> [#<BigDecimal:7f7a56aa8f70,'0.1E1',9(18)>, #<BigDecimal:7f7a56aa8ef8,'0.11E1',18(18)>, #<BigDecimal:7f7a56aa8ea8,'0.1000000000 1E10',27(27)>, #<BigDecimal:7f7a56aa8e58,'0.1000000000 1E1',27(27)>]
> list.map { |i| digits_after_decimal_point(i) }
=> [0, 1, 1, 10]
Of course, if moving to BigDecimal makes your application too slow or is patently too powerful for what you need, this might overly complicate your code for no real benefit. You'll have to decide what is most important for your application.
Here is a very simple approach. Keep track of how many times you have to multiple the number by 10 before it equals its equivalent integer:
def decimals(a)
num = 0
while(a != a.to_i)
num += 1
a *= 10
end
num
end
decimals(1.234) # -> 3
decimals(10/3.0) # -> 16
Like This:
theFloat.to_s.split(".")[1].length
It is not very pretty, but you can insert it as a method for Float:
class Float
def decimalPlaces
self.to_s.split(".")[1].length
end
end
Can you subtract the floor and then just count how many characters left?
(12.525 -( 12.52­5.floor )).to­_s.length-­2
=> 3
edit: nope this doesnt work for a bunch of reasons, negatives and 0.99999 issues
Olexandr's answer doesn't work for integer. Can try the following:
def decimals(num)
if num
arr = num.to_s.split('.')
case arr.size
when 1
0
when 2
arr.last.size
else
nil
end
else
nil
end
end
You can use this approach
def digits_after_decimal_point(n)
splitted = n.to_s.split(".")
if splitted.count > 1
return 0 if splitted[1].to_f == 0
return splitted[1].length
else
return 0
end
end
# Examples
digits_after_decimal_point("1") #=> 0
digits_after_decimal_point("1.0") #=> 0
digits_after_decimal_point("1.01") #=> 2
digits_after_decimal_point("1.00000") #=> 0
digits_after_decimal_point("1.000001") #=> 6
digits_after_decimal_point(nil) #=> 0

after_update callback issues

I'm trying to recalculate percentages in an after_update callback of my model.
def update_percentages
if self.likes_changed? or self.dislikes_changed?
total = self.likes + self.dislikes
self.likes_percent = (self.likes / total) * 100
self.dislikes_percent = (self.dislikes / total) * 100
self.save
end
end
This doesn't work. The percentage always comes out as a 100 or 0, which completely wrecks everything.
Where am I slipping up? I guarantee that self.likes and self.dislikes are being incremented correctly.
The Problem
When you divide an integer by an integer (aka integer division), most programming languages, including Ruby, assume you want your result to be an Integer. This is mostly due to History, because with lower level representations of numbers, an integer is very different than a number with a decimal point, and division with integers is much faster. So your percentage, a number between 0 and 1, has its decimal truncated, and so becomes either 0 or 1. When multiplied by 100, becomes either 0 or 100.
A General Solution
If any of the numbers in the division are not integers, then integer division will not be performed. The alternative is a number with a decimal point. There are several types of numbers like this, but typically they are referred to as floating point numbers, and in Ruby, the most typical floating point number is of the class Float.
1.0.class.ancestors
# => [Float, Precision, Numeric, Comparable, Object, Kernel]
1.class.ancestors
# => [Fixnum, Integer, Precision, Numeric, Comparable, Object, Kernel]
In Rails' models, floats are represented with the Ruby Float class, and decimal with the Ruby BigDecimal class. The difference is that BigDecimals are much more accurate (ie can be used for money).
Typically, you can "typecaste" your number to a float, which means that you will not be doing integer division any more. Then, you can convert it back to an integer after your calculations if necessary.
x = 20 # => 20
y = 30 # => 30
y.to_f # => 30.0
x.class # => Fixnum
y.class # => Fixnum
y.to_f.class # => Float
20 / 30 # => 0
20 / 30.0 # => 0.666666666666667
x / y # => 0
x / y.to_f # => 0.666666666666667
(x / y.to_f).round # => 1
A Solution For You
In your case, assuming you are wanting integer results (ie 42 for 42%) I think the easiest way to do this would be to multiply by 100 before you divide. That pushes your decimal point as far out to the right as it will ever go, before the division, which means that your number is as accurate as it will ever get.
before_save :update_percentages
def update_percentages
total = likes + dislikes
self.likes_percent = 100 * likes / total
self.dislikes_percent = 100 * dislikes / total
end
Notes:
I removed implicit self you only need them on assignment to disambiguate from creating a local variable, and when you have a local variable to disambiguate that you wish to invoke the method rather than reference the variable
As suggested by egarcia, I moved it to a callback that happens before the save (I selected before_save because I don't know why you would need to calculate this percentage on an update but not a create, and I feel like it should happen after you validate that the numbers are correct -- ie within range, and integers or decimal or whatever)
Because it is done before saving, we remove the call to save in the code, that is already going to happen
Because we are not explicitly saving in the callback, we do not risk an infinite loop, and thus do not need to check if the numbers have been updated. We just calculate the percentages every time we save.
Because likes/dislikes is an integer value and integer/integer = integer.
so you can do one of two things, convert to Float or change your order of operations.
self.likes_percent = (self.likes.to_f/total.to_f) * 100
Or, to keep everything integers
self.likes_percent = (self.likes * 100)/total
I'm not sure that this is the only problem that you have, but after_update gets called after the object is saved.
Try changing the update_percentages before - on a before_update or a before_validate instead. Also, remove the self.save line - it will be called automatically later on if you use one of those callbacks.

Resources