April 2022
Let’s say we need to find the average of some values using Ruby.
Simple enough:
def find_avg(list)
list.sum / list.size
end
find_avg([5,6,8,4])
> 5
Uhh, this doesn’t seem to be working.
We expected 5.75
but we returned 5
.
There seems to be some sort of rounding (down) happening here.
Why would it be rounding down?
It turns out Ruby isn’t actually rounding down - it is automatically converting the result to an integer.
5.75.to_i
> 5
And running the to_i
method appears to round down, but it actually contains no logic relating to rounding.
It is a blunt instrument that simply truncates all the stuff after the decimal, which seems similar to .floor
, but has subtle differences.
Why is it converting the result to an integer?
When performing operations, Ruby will maintain type consistency with the result:
(3 + 4).class
> Integer
(3.4 + 4.1).class
> Float
("string" + "another string").class
> String
In some cases, Ruby will be able to work with mixed types:
(3 + 4.0).class
> Float
But Ruby cannot handle all mixes:
(3 + "string").class
> String cannot be coerced into Integer (TypeError)
Getting back to our earlier issue with the average — both the sum and the count were integers.
And Ruby wants the division of two integers to also be an integer.
And this presents us with some problems.
1 / 2
> 0
999 / 1000
> 0
# our initial example
23 / 4
> 5
This seems bad…?
My initial reaction as well … it seems integer division should return floats, as it feels counterintuitive for (1 / 2) == 0
, even if there’s a reason for it (maintaining similar types).
After digging around a bit, it seems that there have been some proposals to ‘fix’ this issue in newer versions of Ruby, but attempts so far have been rejected, by none other than the creator of Ruby, Matz.
In 2018, in regards to this proposal, Matz had the final word.
Too big incompatibility. Abandoned.
Matz.
I believe the issue stems from backwards compatibility — think of all the code already written that expects integers when dividing, that could potentially break if this issue was resolved.
That said, Python had this same issue, but ‘fixed’ it with version 3.
Can we force a float quotient?
Yep, it’s fairly simple. Just convert one of your values to a float, and the result will be a float.
1 / 2
> 0
1.to_f / 2
> 0.5
1 / 2.to_f
> 0.5
It’s a bit clunky, but it does the trick.
Another option is to use the Ruby method fdiv
:
1.fdiv 2
> 0.5
Last example
The examples above show that we’re clearly working with integers.
But, we don’t necessarily know the type of data we’re working with.
Let’s say we’re creating some sort of Venmo-like app where people can split a bill with friends. We write a simple function split_bill
:
# Split bill
def split_bill(total, count)
total / count
end
Somewhere else in the code, we bill some people based on a split:
total = 5
friend_ids = [4322, 1234, 5674, 1232, 5342, 9048]
per_friend = split_bill(total, friend_ids.count)
def bill(user_id, amount)
# charge user amount
end
friend_ids.each do |f|
bill(f, per_friend)
end
How much will each friend get billed?
0
Since the total was 5
(an integer), and the count was 6
(also an integer), our split method returned 5 / 6
, which is 0
.
We didn’t anticipate total
would be an integer, but it was, and our app is broken.
So, as mentioned earlier, to prevent these type of errors, our split function should look like:
def split_bill(total, count)
total.to_f / count
end
That’s all for now.