Overview
Ruby provides extensive type conversion capabilities through both explicit conversion methods and implicit coercion mechanisms. The language defines two primary conversion categories: explicit conversions using methods like to_s
, to_i
, and to_a
, and implicit conversions through coercion methods such as to_str
, to_int
, and to_ary
.
Explicit conversion methods perform best-effort transformations, applying reasonable defaults when conversion fails or encounters ambiguous input. Implicit conversion methods enforce strict type compatibility, raising exceptions when objects cannot properly substitute for the target type.
# Explicit conversion - permissive
"123abc".to_i
# => 123
# Implicit conversion - strict
def process_string(str)
str.to_str.upcase # Requires string-like object
end
process_string("hello") # => "HELLO"
process_string(42) # => NoMethodError
Ruby's conversion system supports transformations between all core data types including strings, numbers, arrays, hashes, symbols, and booleans. The language also provides global conversion functions like String()
, Integer()
, and Float()
that offer controlled conversion with exception handling.
# Global conversion functions
String(123) # => "123"
Integer("456") # => 456
Float("78.9") # => 78.9
# Method-based conversions
123.to_s # => "123"
"456".to_i # => 456
"78.9".to_f # => 78.9
Basic Usage
The most common conversion methods transform values between strings, numbers, and collections. Each core class provides conversion methods that handle typical transformation scenarios with sensible fallback behavior.
String conversion using to_s
works with any Ruby object, invoking the object's string representation method. Numbers, arrays, and hashes all define meaningful string conversions.
# String conversions
42.to_s # => "42"
[1, 2, 3].to_s # => "[1, 2, 3]"
{a: 1}.to_s # => "{:a=>1}"
nil.to_s # => ""
Numeric conversion methods to_i
and to_f
parse string representations and truncate or round other numeric types. String parsing stops at the first non-numeric character, returning zero for completely non-numeric strings.
# Integer conversions
"123".to_i # => 123
"123.45".to_i # => 123
"123abc".to_i # => 123
"abc".to_i # => 0
3.14.to_i # => 3
# Float conversions
"123.45".to_f # => 123.45
"123".to_f # => 123.0
"abc".to_f # => 0.0
42.to_f # => 42.0
Array conversion using to_a
transforms collections into arrays, with specific behavior for different source types. Hashes convert to nested arrays of key-value pairs, while ranges expand into arrays of values.
# Array conversions
{a: 1, b: 2}.to_a # => [[:a, 1], [:b, 2]]
(1..5).to_a # => [1, 2, 3, 4, 5]
"hello".to_a # => ["hello"] (Ruby 1.8), deprecated
Hash conversion with to_h
creates hashes from arrays of pairs or other enumerable collections that yield key-value pairs.
# Hash conversions
[[:a, 1], [:b, 2]].to_h # => {:a=>1, :b=>2}
{a: 1, b: 2}.to_h # => {:a=>1, :b=>2} (identity)
# From enumerable with block
%w[apple banana cherry].map.with_index.to_h
# => {"apple"=>0, "banana"=>1, "cherry"=>2}
Advanced Usage
Complex conversion scenarios involve chained transformations, conditional conversion logic, and custom conversion protocols for user-defined classes. Ruby's duck typing system enables sophisticated conversion patterns through protocol implementation.
Custom classes implement conversion methods to integrate with Ruby's type system. Defining to_s
, to_i
, and collection conversion methods enables seamless integration with existing code expecting these types.
class Temperature
def initialize(celsius)
@celsius = celsius
end
def to_f
(@celsius * 9.0 / 5.0) + 32
end
def to_i
@celsius.to_i
end
def to_s
"#{@celsius}°C"
end
def to_a
[@celsius, to_f]
end
end
temp = Temperature.new(25)
temp.to_s # => "25°C"
temp.to_f # => 77.0
temp.to_i # => 25
temp.to_a # => [25, 77.0]
Conditional conversion patterns handle multiple possible input types through case statements or method presence checking. This approach creates flexible APIs accepting diverse input formats.
def normalize_input(value)
case value
when String
value.strip.downcase
when Symbol
value.to_s.downcase
when Numeric
value.to_s
when Array
value.map { |v| normalize_input(v) }
else
value.respond_to?(:to_s) ? value.to_s : value.inspect
end
end
normalize_input(" Hello ") # => "hello"
normalize_input(:WORLD) # => "world"
normalize_input(42) # => "42"
normalize_input([1, :TWO, "3"]) # => ["1", "two", "3"]
Conversion pipelines chain multiple transformations to achieve complex data restructuring. Method chaining enables readable transformation sequences that handle edge cases gracefully.
class DataProcessor
def self.process_csv_line(line)
line
.split(',')
.map(&:strip)
.map { |field| numeric?(field) ? field.to_f : field }
.map { |field| field == "" ? nil : field }
.each_with_index
.to_h { |value, index| ["field_#{index}".to_sym, value] }
end
def self.numeric?(string)
string.match?(/\A-?\d+(\.\d+)?\z/)
end
end
DataProcessor.process_csv_line("John, 25, 75.5, ")
# => {:field_0=>"John", :field_1=>25.0, :field_2=>75.5, :field_3=>nil}
Common Pitfalls
Type conversion contains numerous edge cases that catch developers off-guard. Understanding these behaviors prevents subtle bugs in production code.
String to integer conversion stops parsing at the first non-numeric character, which can mask data quality issues. Empty strings and pure alphabetic strings convert to zero, potentially hiding validation problems.
# Unexpected parsing behavior
"123abc".to_i # => 123 (not an error!)
"abc123".to_i # => 0 (starts with non-numeric)
" 123 ".to_i # => 123 (ignores leading whitespace)
"".to_i # => 0 (empty string)
"0xFF".to_i # => 0 (not hexadecimal parsing)
# Better validation approach
def safe_to_i(str)
return nil if str.nil? || str.strip.empty?
cleaned = str.strip
return nil unless cleaned.match?(/\A-?\d+\z/)
cleaned.to_i
end
safe_to_i("123abc") # => nil
safe_to_i("123") # => 123
Float conversion exhibits similar behavior but with additional complexity around decimal parsing and scientific notation. Invalid inputs silently convert to zero rather than raising errors.
# Float parsing edge cases
"1.23.45".to_f # => 1.23 (stops at second decimal)
"1.23e2".to_f # => 123.0 (scientific notation works)
"1.23e".to_f # => 1.23 (incomplete scientific notation)
"Infinity".to_f # => 0.0 (not parsed as infinity)
"NaN".to_f # => 0.0 (not parsed as NaN)
Array and hash conversions have asymmetric behavior that causes data loss in round-trip scenarios. Hash to array conversion creates nested arrays, but the reverse transformation requires specific input structure.
# Asymmetric array/hash conversion
original = {a: 1, b: 2, c: 3}
as_array = original.to_a # => [[:a, 1], [:b, 2], [:c, 3]]
back_to_hash = as_array.to_h # => {:a=>1, :b=>2, :c=>3}
# This works, but structure matters
broken_array = [1, 2, 3, 4, 5]
broken_array.to_h # => ArgumentError: invalid value for Hash
Nil conversion behavior varies significantly across different target types, creating inconsistent handling patterns throughout applications.
# Inconsistent nil conversion
nil.to_s # => ""
nil.to_i # => 0
nil.to_f # => 0.0
nil.to_a # => []
# But these raise errors
nil.to_h # => NoMethodError
nil.to_sym # => NoMethodError
Error Handling & Debugging
Proper error handling in type conversion requires understanding when methods raise exceptions versus returning default values. The global conversion functions provide stricter error handling than their method counterparts.
Global conversion functions like Integer()
and Float()
raise ArgumentError
for invalid input, while method-based conversions like to_i
and to_f
return default values. This difference enables more robust validation strategies.
# Method conversions - lenient
def lenient_conversion(value)
{
as_integer: value.to_i,
as_float: value.to_f,
as_string: value.to_s
}
end
lenient_conversion("abc")
# => {:as_integer=>0, :as_float=>0.0, :as_string=>"abc"}
# Function conversions - strict
def strict_conversion(value)
{
as_integer: Integer(value),
as_float: Float(value),
as_string: String(value)
}
rescue ArgumentError => e
{ error: e.message }
end
strict_conversion("abc")
# => {:error=>"invalid value for Integer(): \"abc\""}
Custom validation wrappers combine the flexibility of method conversion with the safety of function conversion. These patterns enable application-specific error handling policies.
module SafeConversion
class ConversionError < StandardError; end
def self.to_integer(value, allow_partial: false)
return value if value.is_a?(Integer)
return nil if value.nil?
string_value = value.to_s.strip
return nil if string_value.empty?
# Check for complete numeric string unless partial allowed
unless allow_partial || string_value.match?(/\A-?\d+\z/)
raise ConversionError, "Invalid integer format: #{value.inspect}"
end
result = string_value.to_i
# Detect zero from invalid input vs actual zero
if result == 0 && !string_value.match?(/\A-?0+\z/)
raise ConversionError, "Conversion resulted in unexpected zero: #{value.inspect}"
end
result
end
end
SafeConversion.to_integer("123") # => 123
SafeConversion.to_integer("123abc") # => ConversionError
SafeConversion.to_integer("123abc", allow_partial: true) # => 123
Debugging conversion issues requires logging both input values and conversion results. Complex data pipelines benefit from intermediate value inspection to identify where conversions fail.
class ConversionLogger
def self.debug_conversion(value, target_type)
puts "Converting #{value.inspect} (#{value.class}) to #{target_type}"
result = case target_type
when :integer
value.to_i
when :float
value.to_f
when :string
value.to_s
when :array
value.to_a
end
puts "Result: #{result.inspect} (#{result.class})"
result
rescue => e
puts "Conversion failed: #{e.message}"
nil
end
end
# Usage in debugging scenarios
data = ["123", "45.6", "abc", nil, [1, 2]]
converted = data.map { |item| ConversionLogger.debug_conversion(item, :integer) }
Performance & Memory
Type conversion performance varies dramatically based on input size, conversion complexity, and target type. Understanding these characteristics helps optimize conversion-heavy code paths.
String conversion generally performs well but creates new string objects for each operation. Numeric conversions involve parsing overhead that scales with string length and complexity.
require 'benchmark'
# String conversion performance comparison
test_data = Array.new(100_000) { rand(1..1_000_000) }
Benchmark.bm(15) do |x|
x.report("to_s method:") do
test_data.each { |n| n.to_s }
end
x.report("String() func:") do
test_data.each { |n| String(n) }
end
x.report("interpolation:") do
test_data.each { |n| "#{n}" }
end
end
# Results show to_s and String() perform similarly,
# string interpolation adds slight overhead
Numeric conversion performance degrades with string length and complexity. Leading/trailing whitespace, scientific notation, and long decimal strings impact parsing speed.
# Performance test for numeric conversions
simple_numbers = Array.new(50_000) { rand(1..999).to_s }
complex_numbers = Array.new(50_000) { " #{rand * 1000}.#{rand(100000)}e#{rand(-5..5)} " }
Benchmark.bm(20) do |x|
x.report("simple to_i:") { simple_numbers.each(&:to_i) }
x.report("complex to_i:") { complex_numbers.each(&:to_i) }
x.report("simple to_f:") { simple_numbers.each(&:to_f) }
x.report("complex to_f:") { complex_numbers.each(&:to_f) }
end
Collection conversion creates new objects and can trigger garbage collection pressure with large datasets. In-place transformation often provides better memory characteristics.
# Memory-efficient conversion patterns
class EfficientConverter
def self.transform_numeric_strings(array)
# Modify in place when possible
array.map! do |item|
case item
when String
item.match?(/\A\d+\z/) ? item.to_i : item
else
item
end
end
end
def self.batch_convert_with_gc(large_array, batch_size = 10_000)
large_array.each_slice(batch_size).flat_map do |batch|
converted = batch.map(&:to_i)
GC.start if batch_size > 5_000 # Force GC between large batches
converted
end
end
end
Reference
Core Conversion Methods
Method | Parameters | Returns | Description |
---|---|---|---|
#to_s |
None | String |
Converts object to string representation |
#to_i |
base=10 (optional) |
Integer |
Parses integer from string or truncates number |
#to_f |
None | Float |
Parses float from string or converts number |
#to_a |
None | Array |
Converts enumerable to array |
#to_h |
None or block | Hash |
Converts array of pairs or enumerable to hash |
#to_sym |
None | Symbol |
Converts string to symbol |
#to_c |
None | Complex |
Converts number to complex number |
#to_r |
None | Rational |
Converts number to rational number |
Global Conversion Functions
Function | Parameters | Returns | Description |
---|---|---|---|
String(obj) |
Object | String |
Strict string conversion, raises on failure |
Integer(obj, base=10) |
Object, base | Integer |
Strict integer conversion with base support |
Float(obj) |
Object | Float |
Strict float conversion, raises on invalid input |
Array(obj) |
Object | Array |
Converts to array or wraps in array |
Hash(obj) |
Object | Hash |
Strict hash conversion, raises on failure |
Complex(real, imag=0) |
Numbers | Complex |
Creates complex number |
Rational(num, den=1) |
Numbers | Rational |
Creates rational number |
Conversion Base Support
Base | Format | Example Input | to_i Result |
---|---|---|---|
2 | Binary | "1010" |
10 |
8 | Octal | "755" |
493 |
10 | Decimal | "123" |
123 |
16 | Hexadecimal | "ff" |
255 |
Implicit Conversion Methods
Method | Purpose | Raises | Used By |
---|---|---|---|
#to_str |
String duck-typing | NoMethodError if undefined |
String concatenation, regex matching |
#to_int |
Integer duck-typing | NoMethodError if undefined |
Array indexing, numeric operations |
#to_ary |
Array duck-typing | NoMethodError if undefined |
Multiple assignment, splatting |
#to_hash |
Hash duck-typing | NoMethodError if undefined |
Keyword argument expansion |
#to_proc |
Proc duck-typing | NoMethodError if undefined |
Block parameter conversion |
Common Conversion Patterns
# Safe numeric conversion with validation
def parse_number(input, type: :integer)
cleaned = input.to_s.strip
return nil if cleaned.empty?
case type
when :integer
cleaned.match?(/\A-?\d+\z/) ? cleaned.to_i : nil
when :float
cleaned.match?(/\A-?\d*\.?\d+([eE][+-]?\d+)?\z/) ? cleaned.to_f : nil
end
end
# Conditional conversion with fallbacks
def flexible_to_array(obj)
case obj
when Array then obj
when Hash then obj.to_a
when Range then obj.to_a
when String then obj.chars
else [obj]
end
end
# Type-preserving conversion
def smart_convert(value)
return value unless value.is_a?(String)
case value.strip
when /\A-?\d+\z/ then value.to_i
when /\A-?\d*\.\d+\z/ then value.to_f
when 'true' then true
when 'false' then false
when 'nil' then nil
else value
end
end