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 |