Eli Sadoff
Eli Sadoff

Reputation: 7308

What can be coerced into an integer in Ruby?

In Ruby, if you try to do 2 + nil, for example, you get

TypeError: nil can't be coerced into Integer

but, this made me wonder if there is anything that can be coerced into an Integer. A lot of times Integers are coerced to other numeric types, like here

(2 + Rational('1/2')).class 
# => Rational
(2 + 0.5).class
# => Float
(2 + 1i).class
# => Complex

And, Booleans in Ruby don't get coerced to Integer. So, what does get coerced into an Integer?

Upvotes: 5

Views: 5647

Answers (3)

Jörg W Mittag
Jörg W Mittag

Reputation: 369498

There are four protocols for types coercion/conversion in Ruby, three general ones, and one specific to arithmetic operations on numbers.

one-letter type conversion

When objects can meaningfully be represented as an instance of another class, then they respond to one-letter type conversion methods such as to_s, to_i, to_f, etc. The bar for responding to one of those is very low: if there is any way that an object can be represented as, say, an integer that is not completely bogus, then it can respond to to_i. E.g. it kinda-sorta makes sense to represent nil as the integer 0, therefore nil responds to to_i. Likewise, it kinda-sorta makes sense to represent nil as the empty string, therefore it responds to to_s.

multi-letter type conversion

This is much stricter. Only when an object can be reasonably be interpreted as being of the same type as an instance of a different class, is it allowed to respond to to_str, to_int, etc. For example, there are 10 implementations of to_i in the Ruby core library, but only 4 for to_int.

In pure OO, it shouldn't matter what class an object is an instance of. If an object behaves like an integer, then it is, for all intents and purposes, an integer, no matter what class it is an instance of. Unfortunately, Ruby is not purely OO. For example, on YARV, Array#[] is implemented by indexing into a C array using a machine integer. (On JRuby, it is indexing into a Java array, and similarly on Opal, MRuby, Rubinius, IronRuby, MagLev, …) This means that only objects that are actual instances of the Integer class can be used for array indexing, because the Ruby implementation only knows how to extract a machine integer (Java integer, …) from an Integer, but not from an arbitrary object that behaves like an integer; it knows what the internal memory structure of an Integer looks like, but it doesn't know what the internal memory structure of your MyInteger looks like.

But, as an escape hatch, Array#[] will try to convert the object to an integer using to_int first. So, it's not fully OO, but at least at gives you the option to participate in array indexing by converting yourself to an integer.

Likewise, the & unary ampersand prefix "convert-to-block" operator theoretically only works with Procs, but it will call to_proc first, to give the object a chance to convert itself to a Proc.

capital letter type conversion

There are a couple of factory methods defined in Kernel whose name matches a core class, e.g. Kernel#Integer, Kernel#Float, etc. Those are intended to convert a wide range of objects to their respective classes. They are in some sense much more loose, in that they accept a much wider range of objects than the multi-letter type conversions. But in some sense they are also stricter, since they are allowed to raise exceptions. The to_ methods aren't allowed to raise exceptions: they either exist, and then they must work, or they don't exist at all.

Kernel#Integer accepts a string, for example, and will try to parse it as an integer.

arithmetic operations coercion

This protocol is specific to numbers, and to arithmetic operations. When you send a number a message for an arithmetic operation, and pass an argument that the number doesn't know how to deal with, it will ask that argument to coerce the pair of numbers to a more general pair of types.

So, let's say, you have a + b, and a doesn't know what to do with b, then within a's + method, it will call b.coerce(self), and b will respond with [a_converted_to_a_more_general_type, b_converted_to_a_more_general_type], after which a will retry calling a_converted_to_a_more_general_type + b_converted_to_a_more_general_type:

class Roman < Numeric
  def initialize(str)
    # dummy implementation for the sake of this example
    @num = 5 if str == 'V'
  end

  def coerce(other)
    puts "`coerce` called with #{other.inspect}, which is a #{other.class}"
    [other, @num]
  end

  def to_int
    puts '`to_int` called'
    @num
  end
end

2 + Roman.new('V')
# `coerce` called with 2, which is a Integer
#=> 7

('a'..'g').to_a[Roman.new('V')]
# `to_int` called
#=> 'f'

Upvotes: 6

the Tin Man
the Tin Man

Reputation: 160571

If a string contains digits only it can be coerced to an Integer:

Integer('1') # => 1
Integer('1.0') # => ArgumentError: invalid value for Integer(): "1.0"

This can be useful if you're trying to make sure that a value that was input was truly an integer instead of a float or whether it had any other "junk" in it:

Float('1.0') # => 1.0

If you want to coerce values to an integer, and are not trying to validate input, then you probably want to use to_i instead, which gives you a better chance of getting a usable result without an exception occurring:

1.to_i  # => 1
'1'.to_i  # => 1
Time.now.to_i # => 1485640961
''.to_i # => 0
nil.to_i  # => 0

1.to_i + nil.to_i # => 1

Upvotes: -1

lorefnon
lorefnon

Reputation: 13105

In ruby arbitrary classes can choose to be coercible to other types by defining a coerce method. This is described in detail here.

Upvotes: 8

Related Questions