---
title: Numeric data types in Ruby and when to use them
teaser: Ruby supports multiple numeric data types in its core and standard libraries.
  What are they made for and how to use them properly?
tags: ruby
author: Rémy Hannequin
published_on: 2024-03-15
---

In programming, we usually deal with numbers daily, sometimes without even
noticing it. There is a nice offer of numeric types in Ruby, each serving a
purpose, supporting features and having different behaviours.

Let's have a look at what these types are, what performance and precision
they provide and how to use them properly in our programs.

## Numbers are `Numeric` objects

The core parent class of all core numeric types is
[`Numeric`](https://ruby-doc.org/3.3.0/Numeric.html), itself inheriting from
`Object`. It includes the `Comparable` module and provides methods for
querying (e.g. `#positive?`), comparing (e.g. `#<=>`) or converting
(e.g. `#floor`).

We don't directly use this class, but those which inherit from it.

Numeric objects from core numeric classes such as `Integer`, are single
immutable objects. A numeric object cannot be instantiated, because only one
instance of each number can exist. They are called
[_immediates_].

[_immediates_]: https://www.ruby-lang.org/en/documentation/faq/6/

```rb
a = 1
b = 1.0

a.object_id == 1.object_id # => true
b.object_id == 1.0.object_id # => true
```

In Ruby, [_almost_ everything is an object]. This means operators such
as `+`, `-` can be defined as methods on core numeric classes. For example, you
can find the definition of the "plus" method in Ruby's source
code: [`Integer#+`], [`Float+`]. Syntactic sugar is added to the language to
make mathematical operations more natural to the human eye:

```rb
1 + 2
# is equivalent to
1.+(2)
# or even
1.public_send(:+, 2)
```

[_almost_ everything is an object]: https://www.honeybadger.io/blog/ruby-object-model/

[`Integer#+`]: https://github.com/ruby/ruby/blob/master/numeric.c#L3990

[`Float+`]: https://github.com/ruby/ruby/blob/master/numeric.c#L1179C14

## List of numeric inheriting classes

### `Integer`

This class deals with any whole number, positive or negative, called
_integers_ in the [classification of numbers].

[classification of numbers]: https://en.wikipedia.org/wiki/List_of_types_of_numbers

If you used Ruby in the past, you may have encountered `Fixnum` and `Bignum`.
They respectively handled small/medium and large integers. While they are
technically still present and used in Ruby's source code, they have been
deprecated from the public API [since Ruby 2.4] in favour of `Integer`.

[since Ruby 2.4]: https://bugs.ruby-lang.org/issues/12005

It is still useful to remember that Ruby will handle small integers
with `Fixnum`, and will dynamically switch to `Bignum` once the number gets
larger and exceeds the capacity of `Fixnum`, which depends on the system Ruby
runs on. `Fixnum` can represent integers directly in memory without the need for
dynamic memory allocation, while `Bignum` can represent larger integers as an
array of arbitrary-precision integers.

In Ruby, there is virtually no size limit for what an integer can be, as
long as there is enough memory to store it.

### `Float`

Floating-point numbers are [real numbers] with a fixed precision,
represented in a similar way as the scientific notation, usually in binary.
`12.34` is represented as 1234 × 10<sup>-2</sup>, where `1234` is an integer
called the _significant_, `10` is the _base_ and <sup>`-2`</sup> is the
_exponent_.

[real numbers]: https://en.wikipedia.org/wiki/Real_number

It is a convenient way of representing decimal numbers. Because only the
significant is, well, significant, floating numbers can technically store a
larger range of numbers than a fixed-point system. The _floating_ notion comes
from the fact the number's radix point is "floating" over the significant as the
exponent is changed.

In Ruby, floats are represented using the native double-precision
floating-point (or just _double_) format defined by
the [IEEE 754 technical standard]. The standard itself provides limits that you
can find in the [Ruby documentation]: `Float::MAX` and `Float::MIN`.

Actual values depend on the platform or system Ruby runs on.

[IEEE 754 technical standard]: https://en.wikipedia.org/wiki/IEEE_754

[Ruby documentation]: https://ruby-doc.org/3.2.2/Float.html

#### Precision of floating-point numbers

In Ruby, `Float` supports decimal numbers up to a precision limit, directly
linked to how floating-point numbers are stored in memory. Floating numbers
are often considered _imprecise_, but that is true only when they are used
outside their precision limit, or in some arithmetic operations.

Because we usually store floating-point numbers in binary within a fixed
structure defined by the IEEE 754, some numbers cannot be accurately stored. In
the same way, the quotient `1÷3` has an infinite decimal
representation (`0.3333333...`, called the _recurring part_), numbers
like `1÷10` have imprecision in binary due to the fact the recurring part is
stopped by the number of digits the computer will store for a float.

As this imprecision (called _rounding error_) happens in binary, the side
effects can be counter-intuitive for humans used to use numbers in base 10.
Thankfully, programming languages are smart enough to hide some of this
imprecision. So, when you define something as `0.1`, the actual stored value
is slightly inaccurate but it doesn't show.

However, when such numbers are combined in arithmetic operations, the rounding
error increases and cannot be hidden anymore. This leads to [famous
_wrong_ results] such as:

```rb
0.1 + 0.2 # => 0.30000000000000004
```

[famous _wrong_ results]: https://0.30000000000000004.com/

### `Rational`

Rational numbers are every number that can be represented as the quotient of
two integers, the _numerator_ and the (non-zero) _denominator_:
`numerator/denominator`.

They are also useful for human comprehension. Our brain is more suited imagining
proportions and doing math with quotients rather than decimal numbers. In a
cooking recipe, it is more convenient to say "one-seventh (`1/7`) of a cup of
flour", especially if you have graduations, than "0.142857 cups of
flour". <small>Actually not the best example, please just use grams for
cooking.</small>

In Ruby, there are three ways to define a rational number:

- using the `r` character on a quotient: `1/3r`
- using `#to_r` defined on `String` and numeric types: `"1/3".to_r`, `1/3.to_r`
- using the class constructor: `Rational(1, 3)`

As for integers, there is virtually no limit to how big a rational number
can get in Ruby, except for the memory limit of the system. The numerator and
denominator are stored as regular Ruby objects.

#### Precision of rational numbers

They are particularly handy for representing _exact_ (or _finite_) numbers
that have an infinite decimal representation, such as 1÷3:

```rb
1 / 3.0
# => 0.3333333333333333, not _exactly_ 1÷3

1 / 3r
# => (1/3), exactly the number 1÷3
```

This precision fixes rounding errors which floating-point numbers suffer. As
we mentioned earlier, the intrinsic imprecision of floats becomes noticeable
in arithmetic operations. This is not the case with rational numbers.

```rb
11.times.inject(0) { |t| t + 0.1 }
# => 1.0999999999999999, not _exactly_ 1.1

11.times.inject(0) { |t| t + 1 / 10r }
# => (11/10), exactly 1.1
```

### `Complex`

Complex (or _imaginary_) numbers are quite different as they are not part of
the _real numbers_ classification. A complex number extends a _real_ number
with an _imaginary unit_ `i`, itself defined so that `i × i = -1`.

This means a complex number can always be expressed in the form `a + b×i`,
where `a` and `b` are _real_ numbers.

For this article's purpose, it is only necessary to understand that a
complex number in Ruby is a set of two numbers, called _coordinates_ and can
be declared in three ways:

- using `::rect` or `::polar` class methods with one or two numeric arguments:
  `Complex.rect(1.5, 1/3r)`
- using `#to_c` defined on `String` and numeric
  types: `"3-4i".to_c`, `1/3r.to_c`
- using the class constructor: `Complex("+1-2i")`

Just like any number, arithmetic can be done between complex numbers and
other types of numbers:

```rb
a = Complex.rect(1, 2) # => (1+2i)
b = 1.2
a + b # => (2.2+2i)
```

In the same way, you can use a float or a rational to represent an integer
(`1.0`, `1r`), any number can be expressed as a complex number even without an
imaginary unit, but there's no real point in doing so. The real and
imaginary parts are stored as Ruby objects, leading to, again, virtually no
size limit.

### `BigDecimal`

Ruby includes `BigDecimal` in the standard library, which provides support for
very large or very accurate floating-point numbers. Because it is part of the
standard library and not the core one, you first need to `require "bigdecimal"`
before using it.

`BigDecimal` enables to use decimal numbers without suffering from rounding
errors of floating-point numbers. As the precision is arbitrary and is
deduced or explicitly defined, `BigDecimal` is way more suited for
calculations than floats:

```rb
11.times.inject(0) { |t| t + 0.1 }
# => 1.0999999999999999, not _exactly_ 1.1

11.times.inject(0) { |t| t + BigDecimal("0.1") }
# => 0.11e1, exactly 1.1
```

The most common way to declare a number with `BigDecimal` is to use the class
constructor. It either takes one or two arguments:

- a number, not specifically a numeric as it accepts strings
- the precision requested, required for floats and rationals as it cannot be
  automatically deduced

Like other numeric types, `BigDecimal` objects can be combined with other
types in arithmetic operations. The `BigMath` module also provides
mathematical functions to increase precision in functions like trigonometry:

```rb
require "bigdecimal/math"

Math::PI
# => 3.141592653589793

BigMath.PI(10)
# => 0.31415926535897932364198143965603e1
```

But we will talk more about precision in the next section.

`BigDecimal` objects are stored in a very similar way as `Bignum`. Therefore,
there is again virtually no limit to how big such a number can get.

### What about irrational numbers?

Irrational numbers are _real_ numbers that are not rational, meaning they
cannot be expressed as the ratio of two integers. Numbers like π or √2 are
irrational.

In Ruby, irrational numbers are the only category of numbers that is not
supported. All other categories are supported by the numeric classes defined
earlier, to some limits like software memory.

Numbers like π or square roots are accessible, but they are floating-point
approximations:

```rb
Math::PI
# => 3.141592653589793, not exactly π

Math.sqrt(2)
# => 1.4142135623730951, not exactly √2
```

Objects like `BigDecimal` will increase the precision of these numbers but
will never reach their exact definition, unlike `Rational` which will represent
exact definitions of numbers as long as we don't try to convert them as floats.

```rb
BigMath.PI(50)
# => 0.314159265358979323846264338
# 32795028841971693993751058209749
# 44592309049629352442819e1
# More accurate, but still not exactly π
```

## Precision

As we have seen in the previous section, precision with numbers is a central
question. Different numeric types in Ruby are meant to deal with precision
at different scales.

`Integer` is precise and can practically deal with any whole number as long
as the computer's memory allows it.

`Float` suffers from some kind of imprecision which is noticeable as soon as
they are involved in calculations.

`Rational` and `Complex` are also precise, as long as they are not converted.

`BigDecimal` drastically improves the precision of some numbers and reduces
rounding errors in calculations, although it is not immune to imprecision.

Precision is part of a tradeoff between range and speed. In Ruby, there is no
numeric type that will perform greatly in these three domains.

### When is precision necessary?

Should you avoid using `Float` at all because you can't do accurate math
with them? The short answer is no. The long answer is: it depends on your
use case. Floating-point numbers are good at speed and they are doing okay at
precision and range.

In some cases, high precision is not necessary. We will never access the
final decimal representation of the number π, but we still use it in
extremely accurate science, thanks to approximations that are precise enough
for the use case.

In Ruby, dividing two integers produces a rounded result. And it's okay in some
use cases like this one: I have a €50 note on me and I would like to buy as many
mangoes of €3 each as possible. `50/3` is not a round number, and Ruby will
return `16`. That's all I need, I can buy 16 mangoes for €3 each with my €50
note, the lost precision isn't important at this moment.

In some cases like currencies or dealing with money in general, exact
numbers are necessary. As usually math is involved when dealing with money,
floating-point numbers are not the right candidate. In programming, we
usually prefer to store currencies as integers. In Ruby, it is useful to use
`BigDecimal` to manipulate currencies as it will accurately store the
provided number. There is also the possibility to use dedicated gems, like
[money].

[money]: https://github.com/RubyMoney/money

## Performance

Performance is another aspect of the tradeoff to make when choosing the
right numeric type. When precision is not that important, performance might be.

Floats have been around for almost as long as computers exist and have
benefited from decades of improvements and tuning. Are we have seen previously,
in Ruby floats are represented using the native _double_ format defined by
IEEE 754. This native representation allows the Ruby interpreter to leverage
low-level operations provided by the underlying hardware, resulting in efficient
computations.

Generally, `Integer` will be the most performant class to use if you only
have to deal with integers. In decimal arithmetic, `Float` is far more
performant than `Rational` and even more than `BigDecimal`. Here is a benchmark
you can reproduce at home if it's cold and you would like to heat the room,
using [`benchable`]:

```rb
require "benchable"
require "bigdecimal"

Benchable.bench(:ips, :memory) do
  bench "Integer" do
    1 + 1
  end

  bench "Float" do
    1 + 0.0001
  end

  bench "Rational" do
    1 + 1 / 1000r
  end

  bench "BigDecimal" do
    1 + BigDecimal("0.0001")
  end
end
```

```
Iterations/speed comparison:

   Integer:  7535799.6 i/s
     Float:  7015345.0 i/s - 1.07x  slower
  Rational:  3329727.4 i/s - 2.26x  slower
Bigdecimal:  1421481.1 i/s - 5.30x  slower


Memory comparison:

   Integer:  80 allocated
     Float:  80 allocated  - same
  Rational:  240 allocated - 3.00x more
Bigdecimal:  432 allocated - 5.40x more
```

[`benchable`]: https://github.com/MatheusRich/benchable

As we can see, even if `BigDecimal` is very convenient, it is also way less
performant than floats. In a context where high accuracy is not important,
favouring `Float` can improve the overall performance of your application.

## Extension

The previously mentioned numeric types are the only ones that are available
in Ruby, natively. It doesn't mean they are the only _objects_ you can use
in a Ruby program when dealing with numbers.

### Inheritance

You can create your own numeric types based on existing classes.

The first method is to use inheritance with the `Numeric` class. You could,
for example, want to implement a numeric class that deals with numbers in
another way than using [Arabic numerals]. It must implement the [`coerce`]
method for interacting with other numeric types. It probably should also
implement arithmetic operators (`#+`, `#-`, `#*`, `#/`) for operations, and
`#<=>` for comparison as `Numeric` includes [`Comparable`].

[Arabic numerals]: https://en.wikipedia.org/wiki/Arabic_numerals

[`coerce`]: https://ruby-doc.org/3.3.0/Numeric.html#method-i-coerce

[`Comparable`]: https://ruby-doc.org/3.3.0/Comparable.html

### Delegation

A second method is to enhance an existing numeric type by delegating the
numeric part, with for example `SimpleDelegator`. This allows to manipulate
the object as a numeric value while having the ability to extend new methods to
it.

```rb

class Meter < SimpleDelegator
  def to_s
    "#{__getobj__} meters"
  end
end

distance = Meter.new(5000)
distance.class # => Meter
"I just ran #{distance}." # => "I just ran 5000 meters"
distance + 1000 # => 6000
```

The downside is it can be confusing to manipulate an object that is
_slightly_ more than a regular numeric, as shown in [this great example by
Avdi Grimm]. For example, arithmetic operations will be cast:

```rb
distance1 = Meter.new(5000)
distance2 = Meter.new(2000)

(distance1 + 1.5).class
# => Float

(distance1 + distance2).class
# => Integer

(distance1 + distance2).to_s
# => "7000"
```

[this great example by Avdi Grimm]: https://avdi.codes/why-you-shouldnt-inherit-from-rubys-core-classes-and-what-to-do-instead/

### Value objects

A more advanced method to create your own numeric type is to use [value
objects].

[value objects]: https://thoughtbot.com/blog/value-object-semantics-in-ruby

Value objects can be complex as everything must be implemented from scratch,
like arithmetic operators, comparison operators, or logic related to dealing
with different types. For example, you could decide to have a `Meter` object
that implements the multiplication operator `#*` to only accept raw numbers.
Or, you could decide to support receiving `Meter` objects and decide if the
returned object should still be a `Meter` or a new `SquareMeter` object.

I recommend watching [Joel's presentation] to have a good understanding of
what a number can be and how to be cautious with them.

[Joel's presentation]: https://www.youtube.com/watch?v=WnTw0z7rD3E

You can find an example of value object implemented from scratch with
the notion of `Angle` in the [astronoby] gem. Ruby 3.2 also introduced the new
core class [`Data`] which helps create simple value-alike objects.

[astronoby]: https://github.com/rhannequin/astronoby

[`Data`]: https://docs.ruby-lang.org/en/3.3/Data.html

## Conclusion

Numeric types are an essential part of computer science. Ruby provides a
large list of types to support all categories of numbers, except irrational
numbers, and different precision levels.

When we manipulate numbers in Ruby, we need to be aware of these types,
features and limitations. Some domains require high precision, while others
need performance. When both are necessary, Ruby offers some tradeoffs.

While Ruby is not known for being heavily used in fundamental science or
data science projects, nothing in the language prevents it from reaching
success in both disciplines. It is a language with incredible flexibility and a
deep care for the developer's happiness that allows for creating great
maintainable software.
