~tsvallender

My name is Trevor, I’m a software engineer specialising in Ruby on Rails. I’m also a dad, geek and tabletop gamer.

Profile photo of Trevor Vallender

Numeric types in Ruby

14 June 2022 Updated on

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 BigDecimal. I 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 worry?

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:

> 86.class
=> Integer
> 4.9.class
=> Float
> (2+1i).class
=> Complex

But other times, not so much:

> 4/8
=> 0
> 0.1+0.2
=> 0.30000000000000004

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 Integer, Float, Complex, Rational and 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 finite?, integer?, negative?, nonzero?, positive?, real? and zero?.

Integer (docs)

Probably the simplest of the subclasses, Integers represent whole numbers. Historically, the two subclasses of Integer (Fixnum and 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 case.

Float (docs)

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.

Complex (docs)

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:

> 2+1i
=> (2+1i)
> Complex(5)
=> (5+0i)
> Complex(3,8)
=> (3+8i)
> 8.to_c
=> (8+0i)

Be aware of the possibly counter-intuitive nature of the real? method when applied to Complex objects however—it will return false even if the number has no imaginary part.

Rational (docs)

Rational numbers are those represented by a numerator and denominator, and Ruby is happy to handle them:

> Rational(5)
=> (5/1)
> Rational(-7, 8)
=> (-7/8)
> 6.to_r
=> (6/1)
> 2/8r
=> (1/4)

Rational numbers are exact, but be cautious when comparing them to inexact data types.

BigDecimal (docs)

Finally, 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 require 'bigdecimal' in your project.

A few things to be aware of when working with the class:

The precision 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 Overflow.