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`

, 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*.

```
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:

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

## List of numeric inheriting classes

###
`Integer`

This class deals with any whole number, positive or negative, called
*integers* in the classification 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`

.

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^{-2}, where `1234`

is an integer
called the *significant*, `10`

is the *base* and ^{-2} is the
*exponent*.

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.

#### 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:

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

###
`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”. Actually not the best example, please just use grams for
cooking.

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:

```
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.

```
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:

```
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:

```
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:

```
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:

```
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.

```
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.

## 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`

:

```
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
```

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`

.

### 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.

```
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:

```
distance1 = Meter.new(5000)
distance2 = Meter.new(2000)
(distance1 + 1.5).class
# => Float
(distance1 + distance2).class
# => Integer
(distance1 + distance2).to_s
# => "7000"
```

### Value objects

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

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.

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.

## 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.