CrackedRuby logo

CrackedRuby

Type Conversion Methods

Ruby's type conversion system transforms values between different data types through explicit method calls and implicit coercion protocols.

Core Modules Kernel Module
3.1.2

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