Numeric types in Ruby
14 June 2022
I recently hit an issue with an existing application which was experiencing bugs
comparing two numbers—one stored as a
Float, the other as a
had some existing idea this was bug-prone and not good practice, but the details
evaded me so I thought I’d spend some time looking into the representation of
numeric types in Ruby.
Why do we even need to worry about how our numbers are being stored? Ruby isn’t a strongly typed language, can’t we just let it do its thing and forget about it? In many cases, yes, but in some—as I discovered—no.
If we just present a number to Ruby, it will attempt a best guess at what class to use to represent it. Often this is appropriate:
But other times, not so much:
What are these issues?
This isn’t the place to get into the intricacies of how floating point numbers are represented internally, suffice to say they are performant but inaccurate; sometimes this matters, sometimes it doesn’t. If you want to understand what’s going on, this is a great resource.
Numeric and its subclasses
All numbers in Ruby are represented using one of the subclasses of Numeric. The
most important of these are
BigDecimal, and below I take a look at each of them. It’s worth noting you
cannot instantiate any of these. They are implemented as immediates, so are
immutable. There cannot be more than one instance of 5, for example.
There are a wide number of methods and checks they all support. Some of the most
useful (and self-apparent) are
Probably the simplest of the subclasses,
Integers represent whole numbers.
Historically, the two subclasses of
Bignum), were used
depending on the size of the integer—the former if it could be represented in a
native machine word, otherwise the latter. This is no longer the
As we saw above,
Floats are Ruby’s default for representing numbers with a
fractional part. They’re fast and often adequate but have their shortcomings:
primarily their use can lead to inaccuracies and unexpected results.
If you’re unaware of the mathematical concept of complex numbers, it probably means you’re unlikely to need to use them and don’t need to worry too much about how they’re implemented in Ruby. Basically, a complex number has two parts: one of them “imaginary”. We can deal with them as follows:
Be aware of the possibly counter-intuitive nature of the
real? method when
Complex objects however—it will return
false even if the number
has no imaginary part.
Rational numbers are those represented by a numerator and denominator, and Ruby is happy to handle them:
Rational numbers are exact, but be cautious when comparing them to inexact data types.
BigDecimal provides support for very large or very accurate
representation of numbers with a fractional part. They are the recommended way
of representing financial data.
BigDecimal comes from the standard library, so you will need to
'bigdecimal' in your project.
A few things to be aware of when working with the class:
- It will return infinity in some cases, such as division by zero.
- In others, it will return
NaN(Not a Number), such as 0/0.
NaNis never equal to anything, including itself.
- Numbers too small to be represented using the currently defined precision will return zero.
BigDecimal uses is defined when you create the object: the
second argument to
new is the number of significant digits. There’s a good
description of how this works over on Stack