Conversions and Coercion
|
This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production. The reference material consulted while preparing these pages is listed in the section bibliography. |
This page is written against Ruby 4.0 (4.0.7, checked on 2026-10-18); features newer than Ruby 3.0 name the version that introduced them.
Ruby never turns text into a number behind your back the way a weakly typed language does, and it never refuses to
convert a value the way a strictly typed one does. It has four separate mechanisms instead, and knowing which one is at work explains
almost every TypeError and every surprising 0 that a program produces: explicit conversion methods that you call
(to_s, to_i), implicit conversion methods that core methods call on their arguments (to_str, to_ary),
the strict Kernel functions with capitalised names (Integer(), Array()), and coerce, the protocol that lets
1 + fine work when fine is not a number. This page covers all four, the exact error messages they produce, how to
implement them in your own classes, and the traps that come with method_missing.
The literal syntax for numbers, strings and collections is in Literals,
and the classes themselves are in Numbers,
Strings, Symbols,
Arrays and Hashes.
The splat and double splat at a call site, which use to_a and to_hash, are in
Calling Methods and Argument Forwarding;
the & that calls to_proc is in Blocks, Procs and Lambdas;
multiple assignment, which uses to_ary, is in
Assignment and Multiple Assignment.
How ==, eql? and <=> relate to the types of their operands is in
Equality, Hashing and Comparison, the exception
classes that conversions raise are in
Exceptions and Error Handling, and the dynamic
dispatch that makes the method_missing trap possible is in
Dynamic Dispatch and Method Definition.
The four mechanisms
| Mechanism | Names | Who calls it | Strictness |
|---|---|---|---|
Explicit conversion method |
|
You, in your own code. |
Lenient: |
Implicit conversion method |
|
The core library, on an argument that must be a String, Array, Hash, Integer, Proc, IO, path or Regexp. |
Strict: the object must really stand in for the target type, or the call raises |
Conversion function |
|
You, when input must be valid. |
Strict for text, forgiving for objects; |
Coercion |
|
The numeric operators and comparisons, on the right operand. |
Strict: the pair returned must be usable with the operator. |
The difference between the first two is the idea to keep: to_s says "give me a representation of yourself", and
nearly everything has one; to_str says that the object is a string without being an instance of String, and almost nothing
does. The core library calls the implicit names on arguments, and the explicit names never, which is why
"total: " + 5 is a TypeError while "total: #{5}" works: interpolation asks for a representation
(to_s), and String#+ asks for a String (to_str).
The documented rules for the implicit family are the same for all four target classes. An object is convertible to
a class when it has the conversion method, the method accepts no arguments, and it returns an instance of that class
(kind_of? is true). If the method is missing the call raises TypeError (no implicit conversion of X into Y),
if the method takes arguments it raises ArgumentError, and if the method returns the wrong class it raises
TypeError with the message can't convert X to Y (X#to_y gives Z).
Explicit conversions
The to_x methods are ordinary methods named after the thing they return. They exist on whichever classes find them
useful, they never raise for a value they understand, and most of them are deliberately forgiving about input they do not.
| Method | Where it is defined | Behaviour |
|---|---|---|
|
|
A String describing the object. Called by interpolation and |
|
|
Parse as much leading text as is numeric and stop; |
|
the same, with |
Exact Rational (a Float converts with its binary error) and Complex. |
|
|
A new Array of the elements. |
|
|
A Hash. |
|
|
The Symbol with that name. Any String is allowed: |
|
|
A Proc, so that |
|
|
A |
|
|
Convert between a code point and a one-character String. |
|
|
Convert to and from text in a base from 2 to 36; an invalid base raises |
|
|
Convert to Integer with a stated rounding rule, which is usually what you meant by |
# frozen_string_literal: true
# Explicit conversions are lenient: they read what they can and stop.
"12abc".to_i # => 12
"abc".to_i # => 0
" 42 ".to_i # => 42
"1_000".to_i # => 1000
"3.7kg".to_f # => 3.7
"0x1A".to_i # => 0
"0x1A".hex # => 26
"ff".to_i(16) # => 255
"9".to_i(2) # => 0
"0.1".to_r # => (1/10)
"abc".to_r # => (0/1)
"1+2i".to_c # => (1+2i)
# nil converts to the "empty" value of each type.
nil.to_s # => ""
nil.to_i # => 0
nil.to_a # => []
nil.to_h # => {}
# Numbers convert by rule, not by parsing.
3.7.to_i # => 3
-3.7.truncate # => -3
3.7.floor # => 3
3.2.ceil # => 4
1.5.to_r # => (3/2)
0.1.to_r # => (3602879701896397/36028797018963968)
0.1.rationalize # => (1/10)
255.to_s(16) # => "ff"
65.chr # => "A"
"A".ord # => 65
# Collections convert in both directions.
{ a: 1 }.to_a # => [[:a, 1]]
[[:a, 1]].to_h # => {a: 1}
[1, 2].to_h { [it, it * 2] } # => {1 => 2, 2 => 4}
[1, 2].to_h # raises TypeError: wrong element type Integer at 0 (expected array)
(1..3).to_a # => [1, 2, 3]
name = "title"
name.to_sym # => :title
phrase = "with space"
phrase.to_sym # => :"with space"
Source: String#to_i, String#hex, String#to_r, String#to_c, Array#to_h, Float#to_r, Float#rationalize and NilClass#to_a
Four rules cover most of what goes wrong with these methods:
-
to_iandto_fnever complain."12abc".to_iis12and"abc".to_iis0, so a typo in user input becomes a valid but wrong number. Use them when a best-effort value is the point (reading a leading number from"3.7kg"), and useInteger()andFloat()when the input must be valid. -
to_iignores prefixes unless you give the base."0x1A".to_iis0and"0x1A".hexis26.Integer("0x1A")understands the prefix by itself. -
A Float converts to a Rational exactly, including its error.
0.1.to_ris(3602879701896397/36028797018963968);0.1.rationalizefinds the simplest nearby Rational,(1/10), andRational("0.1")is exact because it starts from text. -
nilhas a conversion method for most targets (to_s,to_i,to_f,to_a,to_h,to_r,to_c) that returns the empty or zero value, which is whynil.to_ais[]and whyvalue.to_sis a common way to normalise optional input.nil.to_sreturns a frozen empty String, and Ruby 4.0 changed the splat*nilso that it no longer callsnil.to_a(the result, an empty list of arguments, is the same).
Money and measurements are the usual reason to leave Float behind: 0.1 + 0.2 is 0.30000000000000004, while two
Rationals add exactly. The bundled gem bigdecimal (a bundled gem since Ruby 3.4, so a Gemfile entry is needed under
Bundler) offers decimal arithmetic with BigDecimal("0.1"); its to_f, to_r and to_i follow the same conventions.
to_s, inspect and interpolation
to_s is the conversion for people, and inspect is the conversion for programmers. String interpolation, puts,
print and Kernel#String call to_s; p, irb and Array#inspect (which formats each element) call inspect. The two
are independent: a class that overrides only to_s still prints the default #<Class:0x...> form from inspect, which
lists the instance variables, and a class that overrides only inspect has the default to_s. Override to_s for the text
a user would read (Shelf::Isbn returns its digits) and inspect for a debugging view; Data and Struct already
generate a good inspect.
The result of to_s must be a String. When it is not, interpolation does not raise: it falls back to the default
description, as the last lines of the listing in to_str show.
to_proc
&object in an argument list calls object.to_proc and passes the resulting Proc as the block. Symbol#to_proc
(&:upcase), Method#to_proc (&method(:Integer)), Hash#to_proc (the Proc looks the argument up in the Hash) and Proc#to_proc
(itself) are built in. Any class may define to_proc, which is how a predicate object or a query object becomes
usable wherever a block is. It is covered with the other implicit conversions below, because & requires it in the same way
that + requires to_str.
Implicit conversions
A method that needs an Array, a Hash, an Integer or a String is, almost always, written to accept anything that can be one, by calling the matching implicit method on its argument. The implicit methods are the library’s duck-typing hooks: defining one is a promise that the object can be used anywhere the target class can.
| Method | Target | Called by (core examples) |
|---|---|---|
|
String |
|
|
Array |
Multiple assignment ( |
|
Hash |
The double splat |
|
Integer |
Any index, count or size argument: |
|
Proc |
The block-pass |
|
IO |
|
|
path String |
|
|
Regexp |
|
Arrays hold one exception to the rule that to_ary means "is an Array": the splat *obj calls to_a, not to_ary, and treats
an object without to_a as a one-element list. The two methods look alike and are used in different places, which is
the cause of most Array conversion surprises (Pitfalls).
to_str
to_str is the strictest promise. Only String defines it in the core library; define it on your own class only when
an instance can be substituted for a String everywhere, as a path, a pathname-like value or a string wrapper can. The
next listing makes a Shelf::Slug that behaves that way, next to Shelf::Isbn, which only has to_s.
# frozen_string_literal: true
require "shelf"
module Shelf
# A URL-friendly name that can stand in for a String.
class Slug
def initialize(text) = @text = text.downcase.tr(" ", "-")
# Implicit: "I am a String", used when a method needs one.
def to_str = @text
# Explicit: how to display me.
def to_s = "Slug(#{@text})"
end
end
slug = Shelf::Slug.new("Moby Dick")
prefix = "title: "
p prefix + slug # => "title: moby-dick"
p "slug is #{slug}" # => "slug is Slug(moby-dick)"
String(slug) # => "moby-dick"
File.join("books", slug) # => "books/moby-dick"
["books", slug].join("/") # => "books/moby-dick"
"moby-dick-2".start_with?(slug) # => true
"moby-dick".include?(slug) # => true
{ "moby-dick" => 1 }[slug] # => nil
text = "moby-dick"
p text == slug # => false
text.eql?(slug) # => false
# Shelf::Isbn has to_s but deliberately no to_str.
isbn = Shelf::Isbn.new(digits: "978-0-306-40615-7")
p "ISBN #{isbn}" # => "ISBN 9780306406157"
prefix = "ISBN "
p prefix + isbn # raises TypeError: no implicit conversion of Shelf::Isbn into String
p prefix + :symbol # raises TypeError: no implicit conversion of Symbol into String
# A to_s that returns a non-String is ignored, without an error.
broken = Object.new
def broken.to_s = 42
"value: #{broken}".sub(/0x\h+/, "0x...") # => "value: #<Object:0x...>"
Source: String#to_str, String#plus, File.join and Array#join
What the listing shows:
-
to_stris used by methods that require a String. Interpolation andKernel#Stringprefer the representation, andArray#joinandString(slug)chooseto_strfirst, soto_sandto_strcan disagree and different methods can pick different answers. Keeping the two equal is the safest design when both exist. -
A Hash lookup is not a conversion:
{ "moby-dick" => 1 }[slug]isnilbecause the lookup compares keys witheql?andhash, which do not convert. Neither doeseql?. -
String#==is the one place where the conversion goes in the other direction: when the argument is not a String but responds toto_str, the result isargument == string. WithSlugnot defining==, the answer isfalse. Define==as well if instances must compare equal to Strings (see Equality, Hashing and Comparison). -
Shelf::Isbndefinesto_s, so"ISBN #{isbn}"works, and deliberately notto_str, so"ISBN " + isbnraisesTypeError. That is the right design for a value object: it can be printed but is not a String. -
A
to_sthat returns something other than a String is not an error in interpolation; Ruby silently prints#<Object:0x...>instead. This is a strong reason to keepto_strivial.
to_ary and to_hash
to_ary and to_hash make an object destructure like an Array or splat like a Hash. They are rarely worth defining
outside collection wrappers, and the listing shows how much of the language they reach.
# frozen_string_literal: true
module Shelf
# Behaves like an Array of titles wherever Ruby asks for one.
class ReadingList
def initialize(*titles) = @titles = titles
def to_ary = @titles
end
# Behaves like a Hash of search options wherever Ruby asks for one.
class Filters
def initialize(**options) = @options = options
def to_hash = @options
end
end
list = Shelf::ReadingList.new("Dune", "Emma")
first, second = list
[first, second] # => ["Dune", "Emma"]
p ["Ulysses"] + list # => ["Ulysses", "Dune", "Emma"]
[list, ["Ulysses"]].flatten # => ["Dune", "Emma", "Ulysses"]
[list].map { |a, b| "#{a}/#{b}" } # => ["Dune/Emma"]
%w[a b].zip(list) # => [["a", "Dune"], ["b", "Emma"]]
Array(list) # => ["Dune", "Emma"]
list.to_a # raises NoMethodError: undefined method 'to_a' for an instance of Shelf::ReadingList
[*list].size # => 1
def search(author: nil, available: true) = [author, available]
filters = Shelf::Filters.new(author: "Le Guin")
search(**filters) # => ["Le Guin", true]
{ available: false }.merge(filters) # => {available: false, author: "Le Guin"}
search(filters) # raises ArgumentError: wrong number of arguments (given 1, expected 0)
Source: Array#to_ary, Hash#to_hash, Array#flatten and Hash#merge
The points to take from it:
-
Multiple assignment, block parameters and method parameters in parentheses all use
to_aryto destructure. The splat*listdoes not: it needsto_a, so[*list]has one element (the list itself) when onlyto_aryexists. Defineto_aas well, usually as an alias, if the object should splat. -
Array#+,zip,flattenandArray()convert withto_ary.flattenis the dangerous one: it callsto_aryon every element, so a wrapper that answersto_aryis flattened away (below). -
The double splat
**filterscallsto_hash, and so doesHash#merge. Since Ruby 3.0 a positional argument is never turned into keywords automatically, sosearch(filters)is anArgumentError: pass**filters. This is the separation described in Methods and Parameters. -
Structhasto_aandto_hbut noto_ary, andDatahasto_honly, so neither destructures in an assignment. Pattern matching usesdeconstructanddeconstruct_keysfor that (see Pattern Matching).
to_int and to_proc
to_int is how the library accepts "something integral". Float implements it by truncation, so [1, 2, 3][1.9] is
2, and a class that wants to be an index, a count or a size does the same. to_int does not make an object a number
for arithmetic: 3 + copies is a TypeError, because the + operator uses coerce (below), not to_int.
# frozen_string_literal: true
require "shelf"
module Shelf
# A number of copies that can be used where Ruby asks for an Integer.
class Copies
def initialize(count) = @count = count
def to_int = @count
end
# A reusable predicate: &by_author calls to_proc on it.
class ByAuthor
def initialize(name) = @name = name
def to_proc = ->(book) { book.author.name == @name }
end
end
copies = Shelf::Copies.new(2)
%w[a b c][copies] # => "c"
Array.new(copies, "Dune") # => ["Dune", "Dune"]
p "ab" * copies # => "abab"
Integer(copies) # => 2
[1, 2, 3].first(copies) # => [1, 2]
p 3 + copies # raises TypeError: Shelf::Copies can't be coerced into Integer
def book(isbn, title, author) = Shelf::Book.new(Shelf::Isbn.new(digits: isbn), title, Shelf::Author.new(author))
books = [
book("978-0-306-40615-7", "Notes", "Ada Lovelace"),
book("978-0-13-235088-4", "Engines", "Charles Babbage")
]
books.select(&Shelf::ByAuthor.new("Ada Lovelace")).map(&:title) # => ["Notes"]
# The built-in implementers of to_proc.
%w[a b].map(&:upcase) # => ["A", "B"]
%w[1 2].map(&method(:Integer)) # => [1, 2]
%i[a b].map(&{ a: 1, b: 2 }) # => [1, 2]
:upcase.to_proc.lambda? # => true
[1, 2].each(&5) # raises TypeError: no implicit conversion of Integer into Proc
Source: Integer#to_int, Float#to_int, Symbol#to_proc, Hash#to_proc, Method#to_proc and Proc#to_proc
ByAuthor is the useful shape of to_proc: a small object that carries configuration and is passed with & wherever
a block is expected. The Proc it returns is a lambda here, which is the usual choice because it checks its arity. Passing
something that cannot be converted, such as &5, raises TypeError with the message no implicit conversion of Integer into Proc.
to_path, to_io and the other implicit names
File.open(obj) and the other file-name methods accept anything with to_path before they try to_str, which is why a
Pathname works as a file name although it is not a String. A class that represents a location, such as a store file, should implement to_path and leave to_str alone, so it cannot be added to a String by
accident. IO.select and IO.try_convert use to_io the same way, so a wrapper around a socket or a file works
where a stream is required once it defines to_io. Regexp.union and Regexp.try_convert use to_regexp.
try_convert: conversion that reports instead of raises
String, Array, Hash, Integer, Regexp and IO have a class method named try_convert that applies the protocol
and returns nil when the object does not take part. It is the tool to write a method that accepts several shapes.
Integer.try_convert was added in Ruby 3.1; the others are older.
# frozen_string_literal: true
tag = Object.new
def tag.to_str = "fiction"
tag2 = Object.new
def tag2.to_int = 3
String.try_convert(tag) # => "fiction"
String.try_convert(:fiction) # => nil
Array.try_convert([1]) # => [1]
Array.try_convert(1..2) # => nil
Hash.try_convert([[1, 2]]) # => nil
Integer.try_convert(tag2) # => 3
Integer.try_convert(1.9) # => 1
Integer.try_convert("1") # => nil
Regexp.try_convert("a+") # => nil
IO.try_convert($stdin).equal?($stdin) # => true
# The idiom behind Kernel#Array, written by hand: a value or a list of values.
def titles_of(value) = Array.try_convert(value) || [value]
titles_of("Dune") # => ["Dune"]
titles_of(%w[Dune Emma]) # => ["Dune", "Emma"]
titles_of(Struct.new(:x, :y).new(1, 2)).size # => 1
Array(Struct.new(:x, :y).new(1, 2)).size # => 2
# respond_to? is the duck-typing check; it is true for both protocols.
[tag.respond_to?(:to_str), :fiction.respond_to?(:to_str), tag2.respond_to?(:to_int)] # => [true, false, true]
Source: Array.try_convert, String.try_convert, Hash.try_convert, Integer.try_convert, Regexp.try_convert and IO.try_convert
titles_of is what Kernel#Array does minus the second step: it tests with to_ary only, so it treats a Struct as one
value, while Array() goes on to to_a and turns a Struct into its members. respond_to?(:to_str) is the usual duck-typing
test when a try_convert is not available, but try_convert also checks that the result has the right class and
is the version the library itself uses.
When to implement an implicit conversion
-
Implement
to_str,to_ary,to_hashorto_intonly for classes that are substitutes for the target: wrappers, proxies, lazily-loaded collections, path-like values. A value object, a model or a domain type almost never qualifies. -
Make the result a real instance of the target class, and never mutate or raise inside the conversion.
-
Prefer an explicit method with a meaningful name (
to_h,to_a,to_path,to_proc) over an implicit one when callers can use it. Implicit conversions are invoked by code you did not write, with no visible call site. -
Do not implement
to_aryto make destructuring work by accident. Usedeconstructanddeconstruct_keys, which pattern matching andcase/inuse and which flatten andputsignore. -
Return
selfor a copy deliberately:to_aryandto_hashof the core classes returnself, so the callee may mutate the argument.
Conversion functions
The capitalised Kernel methods are the strict, input-validating converters. Their names deliberately match the
class they return, which is why they are one of the few places where a method name starts with a capital letter, and
they are conventionally written with parentheses: Integer("42"). They take any value, apply a documented conversion order, and
raise when the value is not convertible.
| Function | Order of attempts | Raises |
|---|---|---|
|
A String is parsed (prefixes, |
|
|
A String is parsed (decimal text, exponents, |
|
|
|
|
|
|
Nothing for ordinary values. |
|
|
|
|
Text such as |
|
|
Text such as |
|
The keyword exception: false is accepted by Integer(), Float(), Rational() and Complex() and turns the
ArgumentError and TypeError failures into nil. It does not exist on String(), Array() or Hash(), which
raise ArgumentError for it because they have no text to reject. These functions are also what format("%d", value) and
"%d" % value call, so the same messages appear there.
Source: Kernel#Integer, Kernel#Float, Kernel#String, Kernel#Array, Kernel#Hash, Kernel#Rational and Kernel#Complex
Integer and Float
# frozen_string_literal: true
# Integer(): strict parsing of Strings, truncation of numbers.
Integer("42") # => 42
Integer(" 42\n") # => 42
Integer("12_000") # => 12000
Integer("-0b11") # => -3
Integer("0x1A") # => 26
Integer("0o17") # => 15
Integer("017") # => 15
Integer("ff", 16) # => 255
Integer("0x1A", 16) # => 26
Integer("zz", 36) # => 1295
Integer("08", 10) # => 8
Integer(3.99) # => 3
Integer(-3.99) # => -3
Integer("12abc") # raises ArgumentError: invalid value for Integer(): "12abc"
Integer("08") # raises ArgumentError: invalid value for Integer(): "08"
Integer("1e3") # raises ArgumentError: invalid value for Integer(): "1e3"
Integer("4 2") # raises ArgumentError: invalid value for Integer(): "4 2"
Integer("") # raises ArgumentError: invalid value for Integer(): ""
Integer("12", 1) # raises ArgumentError: invalid radix 1
Integer(nil) # raises TypeError: can't convert nil into Integer
Integer([1]) # raises TypeError: can't convert Array into Integer
Integer(Float::NAN) # raises FloatDomainError: NaN
Integer(Float::INFINITY) # raises FloatDomainError: Infinity
# exception: false turns the failures above into nil.
Integer("abc", exception: false) # => nil
Integer(nil, exception: false) # => nil
Integer("12", exception: false) # => 12
Source: Kernel#Integer
What deserves attention in Integer():
-
The prefix decides the base.
0xis hexadecimal,0bbinary,0oand a bare leading0octal, soInteger("017")is15andInteger("08")is an error, because8is not an octal digit. Reading a user-supplied number such as a zero-padded day, a postcode or"08"therefore needs the base:Integer("08", 10)is8. -
A base given with a prefix must agree.
Integer("0x1A", 16)is26;Integer("0b11", 8)is an error. Giving a base for a non-String (Integer(10, 2)) is anArgumentError(base specified for non string value). The base must be between 2 and 36 or the result isinvalid radix. -
Underscores between digits are accepted (
"12_000"), whitespace around the number is accepted (" 42\n"), and anything else is not: no inner space, no exponent ("1e3"), no decimals ("0.5"), no empty string. -
Non-String arguments are converted, not parsed. A Float is truncated toward zero (
Integer(-3.99)is-3), and NaN and Infinity raiseFloatDomainError; this is one of the failuresexception: falsealso turns intonil.nil,trueand Symbols raiseTypeError. -
Integer()accepts any object withto_int, thento_i. That makes it the right gate for "an integer or something integral", whereInteger.try_convertwould only checkto_int.
Float() is the same idea for decimal text.
# frozen_string_literal: true
# Float(): the same strictness for decimal text.
Float("1e3") # => 1000.0
Float("1_000.5") # => 1000.5
Float(" 2.5 ") # => 2.5
Float(".5") # => 0.5
Float("1.") # => 1.0
Float("0x1p3") # => 8.0
Float(3) # => 3.0
Float("3.7kg") # raises ArgumentError: invalid value for Float(): "3.7kg"
Float("") # raises ArgumentError: invalid value for Float(): ""
Float(nil) # raises TypeError: can't convert nil into Float
Float("abc", exception: false) # => nil
# String(): to_str first, then to_s. It never fails for ordinary objects.
String(nil) # => ""
String(12) # => "12"
String(:title) # => "title"
String([1, 2]) # => "[1, 2]"
# Array(): to_ary first, then to_a, otherwise a one-element Array.
Array(nil) # => []
list = [1, 2]
Array(list).equal?(list) # => true
Array(1) # => [1]
Array(1..3) # => [1, 2, 3]
Array("a\nb") # => ["a\nb"]
Array({ a: 1 }) # => [[:a, 1]]
Array(Struct.new(:x, :y).new(1, 2)) # => [1, 2]
# Hash(): nil and [] become {}, a Hash stays, everything else is an error.
Hash(nil) # => {}
empty = []
Hash(empty) # => {}
Hash({ a: 1 }) # => {a: 1}
Hash([[1, 2]]) # raises TypeError: can't convert Array into Hash
Hash(1) # raises TypeError: can't convert Integer into Hash
Hash([[1, 2]], exception: false) # raises ArgumentError: wrong number of arguments (given 2, expected 1)
Source: Kernel#Float, Kernel#String, Kernel#Array and Kernel#Hash
Notes on the listing:
-
Float("1.")is1.0since Ruby 3.4; earlier versions raisedArgumentErrorfor a decimal point with no digits after it.Float("NaN")andFloat("Infinity")are errors: text never produces them, thoughFloat(Float::NAN)returns NaN. -
Array()is the guard-clause conversion for "one value or a list":Array(nil)is[],Array([1])is the same Array, and anything else becomes a one-element list, except objects that answerto_a. AHashbecomes its pairs, aRangeits members, aStructits values, and aTimeits ten calendar fields, andArray(file)reads every line of an open file. UseArray.try_convert(value) || [value]or[value].flatten(1)when only real Arrays should expand. -
String()is rarely needed because interpolation does the same, but it never returnsniland never raises for an ordinary object (String(nil)is""), which makes it the neutral choice for "convert whatever this is for display". -
Hash()is the narrowest:nil,[], a Hash or an object withto_hash. It does not turn an Array of pairs into a Hash; useto_hfor that.
Rational and Complex
# frozen_string_literal: true
# Rational() and Complex() parse text and combine integers.
Rational("1/3") # => (1/3)
Rational("0.75") # => (3/4)
Rational(" 1/3 ") # => (1/3)
Rational(1, 3) # => (1/3)
Rational("abc") # raises ArgumentError: invalid value for convert(): "abc"
Rational(nil) # raises TypeError: can't convert nil into Rational
Rational(1, 0) # raises ZeroDivisionError: divided by 0
Rational("abc", exception: false) # => nil
Rational(nil, exception: false) # => nil
Complex("1+2i") # => (1+2i)
Complex("x", exception: false) # => nil
# A Float carries its binary error into the Rational; text does not.
Rational(0.1) # => (3602879701896397/36028797018963968)
Rational("0.1") # => (1/10)
[0.1 + 0.2, Rational("0.1") + Rational("0.2")] # => [0.30000000000000004, (3/10)]
Source: Kernel#Rational, Kernel#Complex, Rational and Complex
Rational("0.1") is (1/10) because the text is read as a decimal fraction; Rational(0.1) is the huge fraction
that the binary Float really holds. Prefer the text form for money, rates and anything typed by a person, and see
Numbers for the full numeric tower, round(half:) and BigDecimal.
Strict or lenient: a decision per input
Pick the conversion by what a bad input should do. The shelf gem reads a loan period from the environment:
# frozen_string_literal: true
# Reading a loan period from the environment: a default for absent input,
# an error for input that is present but wrong.
DEFAULT_LOAN_DAYS = 14
def loan_days_lenient(text) = text.to_i.nonzero? || DEFAULT_LOAN_DAYS
def loan_days_strict(text)
return DEFAULT_LOAN_DAYS if text.nil? || text.strip.empty?
days = Integer(text, 10, exception: false)
raise ArgumentError, "SHELF_LOAN_DAYS must be a positive whole number, got #{text.inspect}" unless days&.positive?
days
end
loan_days_lenient("21") # => 21
loan_days_lenient("3 weeks") # => 3
loan_days_lenient("three") # => 14
loan_days_strict("21") # => 21
loan_days_strict("08") # => 8
loan_days_strict(nil) # => 14
loan_days_strict(" ") # => 14
loan_days_strict("3 weeks") # raises ArgumentError: SHELF_LOAN_DAYS must be a positive whole number, got "3 weeks"
loan_days_strict("-2") # raises ArgumentError: SHELF_LOAN_DAYS must be a positive whole number, got "-2"
loan_days_strict(ENV.fetch("SHELF_LOAN_DAYS", nil)) # => 14
Source: Kernel#Integer, String#to_i, Numeric#nonzero? and ENV.fetch
loan_days_lenient reads "3 weeks" as 3 days and "three" as 14 without a sound; both are wrong in a way nobody
notices. loan_days_strict distinguishes absent input (use the default), valid input, and present-but-invalid input
(fail fast, naming the variable and the value). The pattern is general:
| Situation | Use | Reason |
|---|---|---|
Input from a user, file or environment that must be a number |
|
Rejects |
Optional input with a documented default |
|
|
Reading a number from the front of a longer text |
|
Lenient on purpose. |
A value that is one thing or a list of things |
|
Normalises |
Internal values of a known class |
no conversion |
Converting everything defensively hides bugs; validate at the edge only. |
Guard clauses: class, capability or conversion
A method that needs a number of days can check its argument in three ways, and the choice decides which callers succeed.
# frozen_string_literal: true
module Shelf
# Three ways to guard a parameter that must be a number of days.
module Guards
# By class: strict, rejects 14.0 and every duck.
def self.by_class(days)
raise ArgumentError, "days must be an Integer, got #{days.inspect}" unless days.is_a?(Integer)
days
end
# By capability: accepts anything with an implicit integer conversion.
def self.by_duck(days)
raise ArgumentError, "days must respond to to_int, got #{days.inspect}" unless days.respond_to?(:to_int)
days.to_int
end
# By conversion: accepts text as well and lets Ruby produce the error.
def self.by_conversion(days) = Integer(days, exception: false) || raise(ArgumentError, "bad days: #{days.inspect}")
end
end
Shelf::Guards.by_class(14) # => 14
Shelf::Guards.by_class(14.0) # raises ArgumentError: days must be an Integer, got 14.0
Shelf::Guards.by_duck(14.9) # => 14
Shelf::Guards.by_duck("14") # raises ArgumentError: days must respond to to_int, got "14"
Shelf::Guards.by_conversion("14") # => 14
Shelf::Guards.by_conversion(14.9) # => 14
Shelf::Guards.by_conversion("two") # raises ArgumentError: bad days: "two"
Shelf::Guards.by_conversion(nil) # raises ArgumentError: bad days: nil
Source: Object#respond_to?, Kernel#Integer and ArgumentError
is_a? is the strictest and the least Ruby-like: it refuses a Float and any user-defined integral type. respond_to?(:to_int)
accepts everything the core library itself would accept as an integer, and its message names the missing capability.
Integer() is the most permissive, since it also parses text, so it belongs at the edge of the program where input is
text, not in the middle of it. In all three the guard raises ArgumentError with the offending value, because a
wrong value for the right kind of parameter is an ArgumentError; the TypeError of the implicit conversions is reserved
for a wrong class (see Error messages).
Conversions in your own classes
A class chooses which conversions to offer, and the convention is the same as in the core library.
-
to_sfor the user-facing text,inspectfor the debugging view,to_hfor a Hash view,to_afor a list view,to_procfor a block-compatible object. Name them like the core methods or callers will not find them. -
DataandStructalready provide them.Datahasto_h(anddeconstruct_keys),Structhasto_aandto_h; aDatahas noto_a, soArray(data)is a one-element list holding the object, and it will not splat. See Struct, Data and Value Objects. -
A converting constructor accepts the type itself, converts what it understands and raises
TypeErrorfor the rest. The core library names these with a capital letter (Integer(value)); RuboCop’sNaming/MethodNameflags that spelling in application code, so the usual name isfrom,parseorcoerce, and theKernel-style name is kept for libraries that deliberately mimic it.
# frozen_string_literal: true
require "shelf"
module Shelf
class Isbn
# A converting constructor: accept what is already an Isbn, convert what can
# be converted, and refuse the rest with a TypeError.
def self.from(value)
case value
when Isbn then value
when String, Integer then new(digits: value)
else raise TypeError, "can't convert #{value.class} into Shelf::Isbn"
end
end
end
# One book or many; nil means none.
def self.add_all(catalog, books)
Array(books).each { |book| catalog.add(book) }
catalog
end
end
Shelf::Isbn.from("978-0-306-40615-7") # => #<data Shelf::Isbn digits="9780306406157">
Shelf::Isbn.from(9_780_306_406_157).to_s # => "9780306406157"
Shelf::Isbn.from(nil) # raises TypeError: can't convert NilClass into Shelf::Isbn
Shelf::Isbn.from("978-0-306-40615-8") # raises ArgumentError: invalid ISBN: 978-0-306-40615-8
book = Shelf::Book.new(Shelf::Isbn.from("978-0-306-40615-7"), "Notes", Shelf::Author.new("Ada Lovelace"))
Shelf.add_all(Shelf::Catalog.new(store: Shelf::Store::Memory.new), book).count # => 1
Shelf.add_all(Shelf::Catalog.new(store: Shelf::Store::Memory.new), [book, book]).count # => 2
Shelf.add_all(Shelf::Catalog.new(store: Shelf::Store::Memory.new), nil).count # => 0
# Data and Struct publish their state through different conversions.
author = Shelf::Author.new("Ada Lovelace")
author.to_h # => {name: "Ada Lovelace"}
author.respond_to?(:to_a) # => false
Struct.new(:name).new("Ada Lovelace").to_a # => ["Ada Lovelace"]
Array(author) # => [#<data Shelf::Author name="Ada Lovelace">]
Source: Data#to_h, Struct#to_a, Kernel#Array and TypeError
Shelf.add_all accepts one book, a list of books or nothing, which is the Array() guard clause: the caller can
pass whatever it has, and the method has one code path. The Isbn.from method follows the three-part shape of a
converting constructor: return an instance unchanged, convert the types it understands (a String, an Integer), and
raise TypeError with the class name for the rest. Invalid values still raise ArgumentError from Isbn.new, which
keeps the two failure kinds apart: a wrong class is a TypeError, a wrong value is an ArgumentError.
Coercion
Arithmetic and comparison operators are methods of their left operand. 1 + x is 1.+(x), and Integer#+ knows only
how to add another Integer, a Float, a Rational or a Complex. For anything else it asks the right operand to do the
work: it calls x.coerce(1), expects a two-element Array [a, b] back, and evaluates a + b. The same protocol is
used by -, *, /, **, %, divmod and the relational operators <, <=, > and >= of the numeric classes.
# frozen_string_literal: true
module Shelf
# A late fee in cents that takes part in Integer arithmetic.
class Fine
include Comparable
attr_reader :cents
def initialize(cents) = @cents = cents
def +(other) = Fine.new(cents + cents_of(other))
def -(other) = Fine.new(cents - cents_of(other))
def *(other)
raise TypeError, "a Fine cannot be multiplied by a Fine" if other.is_a?(Fine)
Fine.new(cents * other)
end
def <=>(other) = other.is_a?(Fine) ? cents <=> other.cents : nil
# Integer#+ and friends call this when the right operand is a Fine.
# It returns a pair [left, right] of objects that know how to combine.
def coerce(number) = [Fine.new(number), self]
def to_s = format("%<euros>d.%<rest>02d", euros: cents / 100, rest: cents % 100)
private
def cents_of(other) = other.is_a?(Fine) ? other.cents : Integer(other)
end
end
fine = Shelf::Fine.new(250)
(fine + 100).to_s # => "3.50"
(100 + fine).to_s # => "3.50"
(1000 - fine).to_s # => "7.50"
(fine * 3).to_s # => "7.50"
low = 100
high = 300
exact = 250
[low < fine, high < fine, exact == fine] # => [true, false, false]
floor = Shelf::Fine.new(200)
ceiling = Shelf::Fine.new(400)
[100, 250, 900].map { Shelf::Fine.new(it).clamp(floor, ceiling).to_s } # => ["2.00", "2.50", "4.00"]
p 3 * fine # raises TypeError: a Fine cannot be multiplied by a Fine
word = "x"
p fine + word # raises ArgumentError: invalid value for Integer(): "x"
p 1 + Object.new # raises TypeError: Object can't be coerced into Integer
one = 1
p one < Object.new # raises ArgumentError: comparison of Integer with Object failed
p 1 <=> Object.new # => nil
# What Numeric itself does.
1.coerce(2.5) # => [2.5, 1.0]
1.5.coerce(2) # => [2.0, 1.5]
Rational(1, 2).coerce(2) # => [(2/1), (1/2)]
1.coerce("2") # => [2.0, 1.0]
[1 + 1r, 1r + 1.0, 1.0 + 1i] # => [(2/1), 2.0, (1.0+1i)]
Source: Numeric#coerce, Integer#coerce, Float#coerce, Comparable and Comparable#clamp
What the listing demonstrates:
-
coerce(number)returns a pair whose first element will be the new left operand. Returning[Fine.new(number), self]means that100 + finebecomesFine.new(100) + fine, and theFineoperators do the rest. This is also why1000 - fineis correct without any special case: the pair keeps the operand order. -
Relational operators use it too.
Integer#<coerces and then compares the pair, so100 < fineworks as long asFinehas<(here fromComparable). The relational methods raiseArgumentError(comparison of Integer with Object failed) when the other side cannot take part, where the arithmetic ones raiseTypeError. -
==does not coerce.250 == fineisfalse, becauseInteger#==asksfine == 250andFine#<=>returnsnilfor a non-Fine.<=>also returnsnilfor an operand it cannot compare instead of raising, which is howsortfinds out that two things are not comparable. -
Coercion is a contract about every operator, not only about
+.3 * finebecomesFine.new(3) * fine, soFine#*receives aFineand the listing rejects it: a class that supportsnumber * objectmust choose itscoerceresult to fit every operator it supports. -
The numeric classes define the generality ladder.
Integercoerces toRationaltoFloattoComplex:1 + 1ris(2/1),1r + 1.0is2.0,1.0 + 1iis(1.0+1i).Integer#coercewith a non-numeric argument converts both sides withFloat(), so1.coerce("2")is[2.0, 1.0]; that never helps1 + "2", because a String has nocoerceof its own and the operator raises first.
A class derived from Numeric (the base of Integer, Float, Rational and Complex) inherits zero?, positive?
and, through Comparable, clamp and between?, and must define its own coerce: the inherited Numeric#coerce tries
to turn both sides into Floats and raises TypeError for an object that cannot become one. Keep a class that is not really
a number, like a late fee, outside the hierarchy and give it coerce as above.
The commutative shortcut and its trap
A frequent first attempt at coerce returns [self, number], so that number + object becomes object + number.
That is right for + and *, whose operands commute, and wrong for -, /, ** and <, whose operands do not.
# frozen_string_literal: true
# A coerce that returns [self, number] is only right for commutative operators.
Quantity = Struct.new(:value) do
def -(other) = value - other
def coerce(number) = [self, number]
end
quantity = Quantity.new(3)
p quantity - 10 # => -7
[10 - quantity] # => [-7]
Source: Numeric#coerce
10 - quantity should be 7 and the program prints -7 without any error. Return a pair of objects of the same class
([Quantity.new(number), self]) and implement every operator for that class, or return [number, value] if the object is
really a wrapper around a plain number.
What a failed coercion looks like
# frozen_string_literal: true
# What Integer#+ does with a right operand that has a broken coerce.
not_a_pair = Object.new
def not_a_pair.coerce(_number) = :nope
wrong_size = Object.new
def wrong_size.coerce(number) = [number]
raises = Object.new
def raises.coerce(_number) = raise(ArgumentError, "no way")
returns_nil = Object.new
def returns_nil.coerce(_number) = nil
p 1 + not_a_pair # raises TypeError: coerce must return [x, y]
p 1 + wrong_size # raises TypeError: coerce must return [x, y]
p 1 + raises # raises ArgumentError: no way
p 1 + returns_nil # raises TypeError: coerce must return [x, y]
p 1 + Object.new # raises TypeError: Object can't be coerced into Integer
Source: Numeric#coerce and TypeError
coerce must return exactly two elements, nil is not accepted, and an exception raised inside it is not swallowed.
When the right operand has no coerce at all, the message names its class: Object can't be coerced into Integer,
nil can't be coerced into Integer and String can't be coerced into Float.
Error messages
The messages are stable enough to recognise on sight. They name the class of the failing value in current Ruby (a
nil receiver is called nil, not NilClass), and they separate the three kinds of failure.
# frozen_string_literal: true
def show
yield
rescue StandardError => e
puts "#{e.class}: #{e.message}"
end
text = "a"
digit = "2"
# Implicit conversion failures: a wrong class, never a wrong value.
show { text + :b }
show { [1] + nil }
show { Array.new("3") }
show { [3, 1].first("1") }
show { [1, 2, 3][Object.new] }
show { {}.merge([[1, 2]]) }
show { File.basename(42) }
# Coercion failures: the right operand cannot take part in the arithmetic.
show { 1 + digit }
show { 1 + nil }
show { 1 * Object.new }
show { 1.5 - "x" }
show { 1 < "2" }
show { 1.0 >= nil }
# Explicit parsing failures.
show { Integer("12abc") }
show { Integer(nil) }
show { Float("") }
show { format("%d", "1x") }
show { format("%d", nil) }
show { nil + 1 }
show { "5".to_int }
# Non-raising results.
p 1 <=> "2"
p 1 == "1"
TypeError: no implicit conversion of Symbol into String
TypeError: no implicit conversion of nil into Array
TypeError: no implicit conversion of String into Integer
TypeError: no implicit conversion of String into Integer
TypeError: no implicit conversion of Object into Integer
TypeError: no implicit conversion of Array into Hash
TypeError: no implicit conversion of Integer into String
TypeError: String can't be coerced into Integer
TypeError: nil can't be coerced into Integer
TypeError: Object can't be coerced into Integer
TypeError: String can't be coerced into Float
ArgumentError: comparison of Integer with String failed
ArgumentError: comparison of Float with nil failed
ArgumentError: invalid value for Integer(): "12abc"
TypeError: can't convert nil into Integer
ArgumentError: invalid value for Float(): ""
ArgumentError: invalid value for Integer(): "1x"
TypeError: can't convert nil into Integer
NoMethodError: undefined method '+' for nil
NoMethodError: undefined method 'to_int' for an instance of String
nil
false
Source: TypeError, ArgumentError and NoMethodError
| Message | Exception | Meaning and fix |
|---|---|---|
|
|
A method that needs a Y received an X that has no |
|
|
The object has the implicit method but it returned a Z. Fix the method. |
|
|
An arithmetic operator found a right operand without |
|
|
A relational operator, |
|
|
Text that is not a valid number. Validate or use |
|
|
|
|
|
A conversion method (or any method) called on |
|
|
|
|
|
A conversion method that takes arguments, or a Hash passed where keywords were expected (since Ruby 3.0). |
method_missing and conversion names
The core library calls implicit conversions through a probe: it first asks whether the object responds to to_ary or
to_str, and only then calls it. An object that answers respond_to? for every name, which is what a dynamic proxy
that implements method_missing and respond_to_missing? for everything does, looks convertible to everything. The
effect is not an error but wrong data: flatten and puts quietly drop the proxy.
# frozen_string_literal: true
# A catch-all method_missing that also answers respond_to? for every to_* name.
class Ghost
def method_missing(name, *) = name.start_with?("to_") ? [] : super
def respond_to_missing?(name, include_private = false) = name.start_with?("to_") || super
end
ghost = Ghost.new
[ghost, 1].flatten # => [1]
prefix = "x"
p prefix + ghost # raises TypeError: can't convert Ghost to String (Ghost#to_str gives Array)
Source: BasicObject#method_missing and respond_to_missing?
The Ghost returned [] for to_ary, so flatten replaced it with that empty list and the Array lost an element
without a warning, and to_str returned an Array instead of a String, which raised the TypeError shown. The fix is to
answer only the names the proxy really supports and to hand every other name to super, which raises NoMethodError;
the conversion probes treat that as "not convertible".
# frozen_string_literal: true
# A proxy that forwards only the names it really knows.
class Careful
FORWARDED = %i[title pages].freeze
def initialize(target) = @target = target
def method_missing(name, *, &) = FORWARDED.include?(name) ? @target.public_send(name, *, &) : super
def respond_to_missing?(name, include_private = false) = FORWARDED.include?(name) || super
end
book = Struct.new(:title, :pages).new("Dune", 612)
careful = Careful.new(book)
careful.title # => "Dune"
careful.respond_to?(:to_ary) # => false
[careful, 1].flatten.size # => 2
prefix = "x"
p prefix + careful # raises TypeError: no implicit conversion of Careful into String
Careful forwards title and pages, reports so through respond_to_missing?, and stays a single element in a
flatten. Rules for any method_missing: always define respond_to_missing? with the same list, never answer
names that begin with to_ unless you mean the conversion, and call super for the rest. The details of
method_missing itself are in
Dynamic Dispatch and Method Definition.
Truthiness is not a conversion
There is no to_bool. A condition tests the object itself: only nil and false are falsy, and everything else, including
0, "", [] and {}, is truthy, with no method called. !!value produces true or false by applying ! twice and
value.nil? or value.empty? ask the specific question. Code ported from languages where 0 or "" is false is a
frequent source of bugs; see
Control Expressions.
Guidelines
-
Convert at the edge of the program (command line, files, HTTP, the environment) with the strict functions, and keep the internals typed; do not sprinkle
to_sandto_ithrough the logic. -
Use
Integer(text, 10)for user-visible numbers,Float(text)for decimals,Rational(text)orBigDecimalfor money, andexception: falsewith||for optional input. -
Use
Array()for "one or many",Hash()for "a Hash or nothing", and know thatArray()expands every object withto_a. -
Implement
to_sandinspectfor every class that people will print; implementto_str,to_ary,to_hashorto_intonly for true substitutes; implementto_pathandto_procfreely. -
Keep
==,eql?andhashconsistent with your conversions; a class that converts to a String should usually also compare equal to it. -
Raise
TypeErrorfor a wrong class andArgumentErrorfor a wrong value, with the offending value in the message. -
For operators on your own types, define
coercereturning a pair of your own class, and testnumber op objectfor every operator, not only+. -
Do not override
method_missingfor names that start withto_without an explicit allow list.
RuboCop cops for conversions
| Cop | What it reports |
|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
A converting method named like a class, |
|
An operator method ( |
Source: RuboCop Style cops, Lint cops and Naming cops
Pitfalls
-
to_ihides bad input."12abc".to_i,"abc".to_iandnil.to_ireturn numbers. UseInteger()when the input must be valid. -
The octal trap.
Integer("08")andInteger("09")fail because a leading0selects octal. Pass the base:Integer("08", 10)."08".to_iis8. -
Array(object)is not "wrap in an Array". It expands a Hash into pairs, a Struct into values, a Time into its ten fields, a Range into members, and an open file into its lines.Array(nil)is[], which silently changes meaning. -
[*obj]andArray(obj)differ. The splat usesto_aonly;Array()triesto_aryfirst. -
to_sthat does not return a String is ignored by interpolation and gives#<Class:0x...>. -
Defining
to_strfor convenience. It lets the object be added to Strings, joined, used as a file name and compared throughString#==, so mistakes such as passing the object where a path or a name was meant succeed silently. Define it only when the object is a String stand-in. -
to_aryon a wrapper makesflatten,putsand destructuring consume it. -
method_missingthat answers everyto_*(see the section above). -
A Hash is not keywords. Since Ruby 3.0,
foo(hash)never turns a Hash into keyword arguments. Pass**hash. -
Float to Integer is truncation, not rounding.
Integer(3.99)is3; useround,ceilorfloorto choose. -
==andeql?ignore conversion.1 == 1.0istrueand1.eql?(1.0)isfalse; Hash keys useeql?andhash, so{ 1 => :a }[1.0]isnil. -
coercereturning[self, number]is only right for commutative operators. -
Rescuing
TypeErrorto detect the type of an argument also hides genuine conversion bugs inside the call. Test withrespond_to?ortry_convertfirst. -
Integer("1e3")fails but"1e3".to_fis1000.0. Parsing a number written with an exponent needsFloat()and then an explicit conversion withto_iorround. -
Locale and Unicode digits.
Integer()andto_iread ASCII digits only; Arabic-Indic digits such as"١٢"are anArgumentErrorinInteger()and0into_i. There is no locale-aware parsing in the core library.
Comparison with other languages
| Language | Model | Difference from Ruby |
|---|---|---|
|
|
|
Automatic juggling between strings and numbers ( |
Ruby never converts a String to a number for you; |
|
TypeScript and JavaScript |
Loose coercion ( |
|
|
|
|
Explicit conversion |
|
|
|
|
|
|
|
Where the books are outdated or wrong
The conversion idioms in older books are mostly still valid; these details changed or were never right.
| Area | Older material shows | Current approach (Ruby 4.0) |
|---|---|---|
|
Advice to wrap "one or many" arguments with |
Right for |
Error messages |
|
|
Hash in output |
|
|
Fixnum and Bignum |
|
One class, |
Last-argument Hash |
A Hash passed as the last argument is converted to keywords. |
Never since Ruby 3.0; |
|
|
|
Number parsing |
|
Accepted since Ruby 3.4 ( |
|
The splat calls |
Not since Ruby 4.0; the result is the same empty list. |
|
Not covered. |
Added in Ruby 3.1. |
Version notes
| Version | Change |
|---|---|
3.0 |
Positional and keyword arguments are separated: a Hash is not converted to keywords, and |
3.1 |
|
3.2 |
|
3.3 |
Error messages name the receiver by class ("for an instance of"). |
3.4 |
|
4.0 |
|
References
-
Kernel#Integer, Float, String, Array, Hash, Rational and Complex
-
Numeric#coerce, Integer, Float, Rational and Complex
-
Array.try_convert, String.try_convert, Hash.try_convert, Integer.try_convert, Regexp.try_convert and IO.try_convert
-
Struct, Data, Comparable, TypeError, ArgumentError and FloatDomainError