CrackedRuby logo

CrackedRuby

Format Strings

Overview

Ruby provides several mechanisms for format strings: string interpolation with #{}, printf-style formatting with the % operator, the String#% method, sprintf function, and String.format. Each approach offers different capabilities for controlling output formatting, numeric precision, padding, and alignment.

String interpolation directly embeds Ruby expressions within double-quoted strings. The #{} syntax evaluates the enclosed expression and converts the result to a string using the object's to_s method.

name = "Alice"
age = 30
"Hello, #{name}. You are #{age} years old."
# => "Hello, Alice. You are 30 years old."

Printf-style formatting uses format specifiers with the % operator. This approach provides precise control over numeric formatting, padding, and alignment through format directives.

"Name: %s, Score: %d" % ["Bob", 95]
# => "Name: Bob, Score: 95"

The sprintf function and String#% method offer identical functionality to the % operator but with different calling conventions. Ruby's format specifiers support positional arguments, named placeholders, and various formatting options for different data types.

Basic Usage

String interpolation handles most formatting needs with direct expression evaluation. Ruby converts interpolated values using their to_s methods, making interpolation work with any object that responds to to_s.

user = { name: "Charlie", points: 1250 }
balance = 45.67

message = "User #{user[:name]} has #{user[:points]} points and $#{balance} balance"
# => "User Charlie has 1250 points and $45.67 balance"

# Method calls work within interpolation
products = ["laptop", "mouse", "keyboard"]
"Found #{products.length} products: #{products.join(', ')}"
# => "Found 3 products: laptop, mouse, keyboard"

Printf-style formatting uses the % operator with format specifiers. The basic syntax places format directives in the string and provides values in an array.

# Basic string and integer formatting
"Product: %s, Quantity: %d" % ["Monitor", 5]
# => "Product: Monitor, Quantity: 5"

# Multiple arguments with different types
"Price: $%.2f, Tax: %.1f%%, Total: $%.2f" % [29.99, 8.5, 32.54]
# => "Price: $29.99, Tax: 8.5%, Total: $32.54"

The sprintf function provides the same formatting capabilities with function call syntax instead of operator syntax.

sprintf("Order #%06d processed at %s", 147, Time.now.strftime("%H:%M"))
# => "Order #000147 processed at 14:30"

# Equivalent using String#%
"Order #%06d processed at %s" % [147, Time.now.strftime("%H:%M")]
# => "Order #000147 processed at 14:30"

Named format specifiers use hash arguments instead of positional arrays, making complex formatting more readable.

template = "Welcome %{name}! Your account balance is $%{balance}.2f"
data = { name: "Diana", balance: 156.78 }

template % data
# => "Welcome Diana! Your account balance is $156.78"

# Same result with sprintf
sprintf("Welcome %{name}! Your account balance is $%{balance}.2f", data)
# => "Welcome Diana! Your account balance is $156.78"

Advanced Usage

Positional argument references allow reordering and reusing format arguments. Ruby supports both numbered positions and named placeholders for complex formatting scenarios.

# Positional references with numbers
template = "Error %2$s occurred in %1$s at line %3$d"
sprintf(template, "parser.rb", "SyntaxError", 42)
# => "Error SyntaxError occurred in parser.rb at line 42"

# Reusing arguments with position references
"RGB: %1$d, %1$d, %1$d creates grayscale %1$d" % [128]
# => "RGB: 128, 128, 128 creates grayscale 128"

Advanced numeric formatting controls precision, padding, and alignment through format flags. Multiple flags can combine to create complex formatting behaviors.

# Left-aligned with minimum width
"%-10s|" % ["left"]
# => "left      |"

# Zero-padded numbers with signs
"%+08d" % [42]
# => "+0000042"

# Scientific notation with precision
"%.3e" % [1234.56789]
# => "1.235e+03"

# Hexadecimal formatting with prefix
"%#x" % [255]
# => "0xff"

Binary and octal formatting provides alternative numeric representations with optional prefixes and case control.

number = 42

# Binary representations
"%b" % number    # => "101010"
"%#b" % number   # => "0b101010"

# Octal with prefix
"%#o" % number   # => "052"

# Hexadecimal case variations
"%x" % number    # => "2a"
"%X" % number    # => "2A"
"%#X" % number   # => "0X2A"

Complex nested interpolation combines string interpolation with format strings for dynamic template generation.

def format_report(title, data, precision = 2)
  template = "#{title}: %{value}.#{precision}f (%{change:+.1f}%%)"
  
  data.map do |item|
    template % {
      value: item[:current],
      change: item[:percent_change]
    }
  end.join("\n")
end

metrics = [
  { current: 45.678, percent_change: 12.5 },
  { current: 78.234, percent_change: -3.2 }
]

format_report("Revenue", metrics)
# => "Revenue: 45.68 (+12.5%)
#     Revenue: 78.23 (-3.2%)"

Custom formatting classes can implement to_s or to_str methods to control interpolation behavior, or define formatting methods for printf-style operations.

class Currency
  def initialize(amount, symbol = "$")
    @amount = amount
    @symbol = symbol
  end
  
  def to_s
    "#{@symbol}#{@amount}"
  end
  
  def to_f
    @amount.to_f
  end
end

price = Currency.new(49.99, "")

# Interpolation uses to_s
"Price: #{price}"
# => "Price: €49.99"

# Printf-style can use numeric conversion
"Amount: %.2f" % price.to_f
# => "Amount: 49.99"

Error Handling & Debugging

Format string errors typically manifest as ArgumentError exceptions when argument counts mismatch format specifiers, or TypeError when arguments cannot convert to expected types.

begin
  "%s %d %f" % ["text", 42]  # Missing third argument
rescue ArgumentError => e
  puts e.message
  # => "too few arguments"
end

begin
  "%d" % ["not_a_number"]    # String cannot convert to integer
rescue ArgumentError => e
  puts e.message
  # => "invalid value for Integer(): \"not_a_number\""
end

Named placeholder errors occur when hash keys don't match format specifier names or when mixing positional and named formats inappropriately.

template = "Name: %{first} %{last}"
data = { first: "John" }  # Missing 'last' key

begin
  result = template % data
rescue KeyError => e
  puts "Missing format key: #{e.message}"
  # => "Missing format key: key{last} not found"
end

# Defensive formatting with default values
template = "Name: %{first} %{last}"
safe_data = { first: "John", last: "Unknown" }.merge(data)
template % safe_data
# => "Name: John Unknown"

Type conversion debugging requires understanding Ruby's conversion hierarchy. Format specifiers attempt implicit conversion through to_i, to_f, or to_s methods depending on the format type.

class DebuggingNumber
  def initialize(value)
    @value = value
  end
  
  def to_i
    puts "Converting #{@value} to integer"
    @value.to_i
  end
  
  def to_f
    puts "Converting #{@value} to float"
    @value.to_f
  end
  
  def to_s
    puts "Converting #{@value} to string"
    @value.to_s
  end
end

num = DebuggingNumber.new("42.7")

"%d" % num
# Output: Converting 42.7 to integer
# => "42"

"%.2f" % num
# Output: Converting 42.7 to float
# => "42.70"

Interpolation errors can occur when expressions within #{} raise exceptions or when objects lack proper to_s implementations.

# Safe interpolation with rescue
def safe_interpolate(template, **vars)
  vars.transform_values do |value|
    value.to_s
  rescue NoMethodError
    "[Object:#{value.class}]"
  rescue => e
    "[Error:#{e.class}]"
  end
  
  template % vars
end

class ProblematicObject
  def to_s
    raise "Conversion failed"
  end
end

result = safe_interpolate(
  "Status: %{status}, Object: %{obj}",
  status: "OK",
  obj: ProblematicObject.new
)
# => "Status: OK, Object: [Error:RuntimeError]"

Performance & Memory

String interpolation generally performs better than concatenation for simple cases but creates new string objects for each interpolation operation. Understanding memory allocation patterns helps optimize format-heavy code.

# String interpolation creates new strings
def measure_interpolation(iterations)
  GC.start
  start_memory = GC.stat(:heap_allocated_pages)
  
  iterations.times do |i|
    result = "Item #{i}: #{i * 2}"
  end
  
  end_memory = GC.stat(:heap_allocated_pages)
  end_memory - start_memory
end

# Printf-style formatting also creates new strings
def measure_printf(iterations)
  GC.start
  start_memory = GC.stat(:heap_allocated_pages)
  
  iterations.times do |i|
    result = "Item %d: %d" % [i, i * 2]
  end
  
  end_memory = GC.stat(:heap_allocated_pages)
  end_memory - start_memory
end

puts "Interpolation pages: #{measure_interpolation(10000)}"
puts "Printf pages: #{measure_printf(10000)}"

Format string reuse improves performance when the same template formats multiple data sets. Storing format strings as constants or instance variables reduces parsing overhead.

class ReportGenerator
  TEMPLATE = "Report: %{title} - Score: %{score:.2f} (%{grade})"
  
  def format_results(results)
    # Reusing template reduces format parsing
    results.map { |data| TEMPLATE % data }
  end
  
  def format_with_interpolation(results)
    # Each interpolation parses template syntax
    results.map do |data|
      "Report: #{data[:title]} - Score: #{data[:score]:.2f} (#{data[:grade]})"
    end
  end
end

generator = ReportGenerator.new
data = [
  { title: "Math", score: 95.67, grade: "A" },
  { title: "Science", score: 87.23, grade: "B" }
]

# Template reuse performs better with many iterations
generator.format_results(data)

Complex format strings with many specifiers create parsing overhead. Simplifying formats or pre-computing values can improve performance in tight loops.

# Expensive: complex formatting in loop
def format_complex_slow(items)
  items.map do |item|
    "%10s | %8.2f | %+6.1f%% | %#8x | %12.3e" % [
      item[:name],
      item[:price],
      item[:change],
      item[:id],
      item[:volume]
    ]
  end
end

# Faster: pre-format complex values
def format_complex_fast(items)
  template = "%s | %s | %s | %s | %s"
  
  items.map do |item|
    template % [
      item[:name].ljust(10),
      sprintf("%8.2f", item[:price]),
      sprintf("%+6.1f%%", item[:change]),
      sprintf("%#8x", item[:id]),
      sprintf("%12.3e", item[:volume])
    ]
  end
end

Memory-conscious formatting avoids creating intermediate string objects when processing large datasets. String mutation and buffer reuse can reduce allocation pressure.

require 'stringio'

class BufferedFormatter
  def initialize
    @buffer = StringIO.new
  end
  
  def format_batch(template, data_array)
    @buffer.rewind
    @buffer.truncate(0)
    
    data_array.each do |data|
      @buffer << (template % data)
      @buffer << "\n"
    end
    
    @buffer.string
  end
end

formatter = BufferedFormatter.new
results = formatter.format_batch(
  "ID: %06d, Status: %s",
  1000.times.map { |i| [i, "active"] }
)

Common Pitfalls

Argument order mismatches between format specifiers and provided values create subtle bugs that may not surface immediately during development.

# Easy to mix up argument order
user_id = 12345
score = 87.5

# Wrong: score formats as integer, id as float
"User %d scored %.1f points" % [score, user_id]
# => "User 87 scored 12345.0 points"

# Correct: match argument types to specifiers
"User %d scored %.1f points" % [user_id, score]
# => "User 12345 scored 87.5 points"

# Safer: use named placeholders
"User %{id}d scored %{score}.1f points" % { id: user_id, score: score }
# => "User 12345 scored 87.5 points"

Type conversion surprises occur when Ruby's implicit conversion doesn't match expectations, particularly with numeric formatting and string inputs.

# String to integer conversion strips non-numeric characters
"%d" % ["42abc"]
# => ArgumentError: invalid value for Integer(): "42abc"

# But leading digits work with to_i method directly
"42abc".to_i
# => 42

# Nil converts unexpectedly
"%s" % [nil]
# => ""

"%d" % [nil]
# => ArgumentError: can't convert nil into Integer

Security vulnerabilities can arise when user input controls format strings, allowing format string injection attacks similar to SQL injection.

# Dangerous: user controls format string
def format_user_data(format_string, data)
  format_string % data
rescue => e
  "Format error: #{e.message}"
end

# Attacker could crash the application or expose data
user_format = "%{password}s revealed!"  # Tries to access password key
user_data = { name: "Alice" }

format_user_data(user_format, user_data)
# => "Format error: key{password} not found"

# Safe: validate allowed format strings
ALLOWED_FORMATS = {
  "name_only" => "Name: %{name}s",
  "name_score" => "Name: %{name}s, Score: %{score}d"
}

def safe_format_user_data(format_key, data)
  template = ALLOWED_FORMATS[format_key]
  return "Invalid format" unless template
  
  template % data.slice(*template.scan(/%\{(\w+)\}/).flatten.map(&:to_sym))
end

Encoding issues surface when format strings and arguments have different encodings, particularly with binary data or user input from different sources.

# ASCII format string with UTF-8 argument
ascii_template = "Name: %s".force_encoding('ASCII')
utf8_name = "José".force_encoding('UTF-8')

begin
  result = ascii_template % [utf8_name]
rescue Encoding::CompatibilityError => e
  puts "Encoding error: #{e.message}"
  # Handle by forcing compatible encodings
  result = ascii_template.encode('UTF-8') % [utf8_name]
end

Precision and overflow behaviors with numeric formatting can produce unexpected results when values exceed format specifications or precision limits.

# Precision truncation doesn't round
"%.2f" % [3.999]
# => "4.00"

# Large numbers may overflow format width
"%3d" % [12345]
# => "12345"  # Exceeds width specification

# Negative numbers need extra space for sign
"%4d" % [-123]
# => "-123"   # Uses full width

# Zero-padding with negative numbers
"%05d" % [-123]
# => "-0123"  # Padding after sign

Reference

String Interpolation

String interpolation embeds Ruby expressions directly within double-quoted strings using #{expression} syntax. Single-quoted strings do not support interpolation.

"Current time: #{Time.now}"
"Calculation: #{2 + 3}"
"Method result: #{array.length}"

Format Specifiers

Specifier Type Description Example
%s String String representation "%s" % ["hello"]"hello"
%d, %i Integer Decimal integer "%d" % [42]"42"
%o Integer Octal representation "%o" % [64]"100"
%x, %X Integer Hexadecimal (lowercase/uppercase) "%x" % [255]"ff"
%b Integer Binary representation "%b" % [8]"1000"
%f Float Fixed-point notation "%f" % [3.14]"3.140000"
%e, %E Float Scientific notation "%e" % [1000]"1.000000e+03"
%g, %G Float Shortest float representation "%g" % [1000.0]"1000"
%c Integer/String Character representation "%c" % [65]"A"
%% Literal Percent sign "%%""%"

Format Flags

Flag Effect Example
- Left-justify within field width "%-5s" % ["hi"]"hi "
+ Show sign for positive numbers "%+d" % [42]"+42"
Space for positive numbers "% d" % [42]" 42"
# Alternative form (prefix for octal/hex) "%#x" % [255]"0xff"
0 Zero-pad numbers "%05d" % [42]"00042"

Width and Precision

Format Description Example
%10s Minimum width of 10 characters "%10s" % ["hi"]" hi"
%.3s Maximum 3 characters "%.3s" % ["hello"]"hel"
%10.3s Width 10, max 3 characters "%10.3s" % ["hello"]" hel"
%.2f 2 decimal places "%.2f" % [3.14159]"3.14"
%8.2f Width 8, 2 decimal places "%8.2f" % [3.14]" 3.14"

Named Placeholders

Named placeholders use %{name}specifier syntax with hash arguments for better readability and flexibility.

template = "Hello %{name}s, you have %{count}d messages"
data = { name: "Alice", count: 5 }
template % data  # => "Hello Alice, you have 5 messages"

Positional References

Positional references allow argument reordering and reuse with %n$specifier syntax where n is the 1-based argument position.

"%2$s owns %1$d cats" % [3, "Bob"]  # => "Bob owns 3 cats"
"%1$d + %1$d = %2$d" % [5, 10]      # => "5 + 5 = 10"

Method Reference

Method Parameters Returns Description
String#% Array or Hash String Format string with arguments
sprintf(format, *args) String, arguments String C-style string formatting
String.format Same as sprintf String Alias for sprintf
Kernel#sprintf String, arguments String Global sprintf function

Common Error Types

Error Cause Solution
ArgumentError: too few arguments Missing format arguments Provide all required arguments
ArgumentError: too many arguments Extra unused arguments Remove excess arguments or add specifiers
ArgumentError: invalid value for Integer() Non-numeric string for %d Validate input or use string conversion
KeyError: key{name} not found Missing hash key for named placeholder Include all required keys in argument hash
TypeError: no implicit conversion Incompatible argument type Convert arguments to expected types
Encoding::CompatibilityError Encoding mismatch Ensure compatible encodings between format and arguments