Overview
Ruby provides multiple approaches to string formatting, ranging from simple interpolation to sophisticated format specifications. The primary methods include String#%
, Kernel#sprintf
, Kernel#printf
, and string interpolation with #{}
. These tools handle numeric formatting, alignment, padding, precision control, and type conversion.
The String#%
operator and sprintf
method use format specifiers similar to C's printf family, supporting flags, width, precision, and conversion specifiers. Ruby extends this with additional features for object inspection and more flexible argument handling.
"Hello %s, you have %d messages" % ["Alice", 5]
# => "Hello Alice, you have 5 messages"
sprintf("%.2f%% complete", 87.459)
# => "87.46% complete"
name, count = "Bob", 12
"Welcome #{name}! (#{count} items)"
# => "Welcome Bob! (12 items)"
String interpolation with #{}
evaluates Ruby expressions directly within double-quoted strings, offering the most readable syntax for simple cases. Format strings excel when precise control over output appearance is required, particularly for numeric data, tabular output, and internationalization scenarios.
Ruby's formatting system handles type conversion automatically, converting objects using to_s
, to_i
, to_f
, or inspect
as appropriate for each format specifier. The system supports positional and named arguments, making it suitable for both simple templates and complex localization systems.
Basic Usage
The String#%
method applies format specifiers to arguments provided as an array or single value. Format specifiers begin with %
followed by optional flags, width, precision, and a conversion character.
# Basic substitution
"Name: %s, Age: %d" % ["Carol", 28]
# => "Name: Carol, Age: 28"
# Single argument doesn't require array
"Progress: %d%%" % 75
# => "Progress: 75%"
# Multiple arguments with positioning
"Item %d: %s costs $%.2f" % [1, "Laptop", 999.99]
# => "Item 1: Laptop costs $999.99"
The sprintf
method provides identical functionality with different syntax, often preferred when the format string comes from a variable or when building complex formatting functions.
template = "User %s logged in at %s"
sprintf(template, "admin", Time.now.strftime("%H:%M"))
# => "User admin logged in at 14:23"
# Equivalent using String#%
template % ["admin", Time.now.strftime("%H:%M")]
Numeric formatting controls decimal places, padding, and alignment. The precision specifier .n
controls decimal places for floating-point numbers and maximum characters for strings.
# Floating-point precision
"Temperature: %.1f°C" % 23.847
# => "Temperature: 23.8°C"
# String truncation
"Comment: %.20s" % "This is a very long comment that will be truncated"
# => "Comment: This is a very long "
# Integer padding with zeros
"Order #%05d" % 42
# => "Order #00042"
Width specifiers control field width and alignment. Positive width right-aligns content, while negative width left-aligns. The alignment applies padding with spaces by default.
# Right-aligned in 10-character field
"Status: '%10s'" % "OK"
# => "Status: ' OK'"
# Left-aligned in 10-character field
"Status: '%-10s'" % "ERROR"
# => "Status: 'ERROR '"
# Numeric padding
"%8.2f" % 123.4
# => " 123.40"
Advanced Usage
Ruby supports positional argument references, allowing format strings to reuse arguments or present them in different orders. The syntax %n$
references the nth argument (1-indexed).
# Argument reordering
"Last: %2$s, First: %1$s" % ["John", "Doe"]
# => "Last: Doe, First: John"
# Argument reuse
"Coordinates: X=%1$d, Y=%2$d, Distance from origin: %1$d,%2$d" % [10, 20]
# => "Coordinates: X=10, Y=20, Distance from origin: 10,20"
# Complex positioning with formatting
"Date: %3$02d/%2$02d/%1$04d" % [2024, 3, 15]
# => "Date: 15/03/2024"
Hash-based formatting provides named placeholders, improving readability and maintainability for templates with many substitutions.
"Hello %(name)s, you have %(count)d messages in %(folder)s" % {
name: "Alice",
count: 7,
folder: "Inbox"
}
# => "Hello Alice, you have 7 messages in Inbox"
# Combining with complex formatting
"Balance: %(currency)s%(amount)8.2f" % {
currency: "$",
amount: 1234.56
}
# => "Balance: $ 1234.56"
The flag characters modify formatting behavior. Common flags include +
for explicit signs, #
for alternate forms, 0
for zero padding, and space for positive number padding.
# Explicit sign display
"%+d, %+d" % [42, -17]
# => "+42, -17"
# Alternate form for hex and octal
"%#x, %#o" % [255, 64]
# => "0xff, 0100"
# Zero padding vs space padding
"%08d vs %8d" % [42, 42]
# => "00000042 vs 42"
# Space padding for positive numbers
"% d, % d" % [42, -17]
# => " 42, -17"
Ruby extends standard format specifiers with %p
for inspect
output, useful for debugging and displaying object representations that preserve type information.
values = ["string", 42, :symbol, nil, [1, 2, 3]]
values.each_with_index do |val, i|
puts "%d: %p (%s)" % [i, val, val.class]
end
# Output:
# 0: "string" (String)
# 1: 42 (Integer)
# 2: :symbol (Symbol)
# 3: nil (NilClass)
# 4: [1, 2, 3] (Array)
Method chaining enables building complex formatting pipelines, particularly useful when processing collections or building formatted reports.
data = [
{ name: "Product A", price: 19.99, stock: 150 },
{ name: "Product B", price: 45.00, stock: 23 },
{ name: "Product C", price: 8.50, stock: 0 }
]
formatted_report = data.map.with_index do |item, i|
"%-3d %-12s $%7.2f %4s" % [
i + 1,
item[:name],
item[:price],
item[:stock] > 0 ? item[:stock] : "OUT"
]
end.join("\n")
puts "ID Product Price Stock"
puts "=" * 30
puts formatted_report
Error Handling & Debugging
Invalid format specifiers raise ArgumentError
exceptions with descriptive messages indicating the problematic format sequence. These errors occur at runtime when the format string is processed.
begin
"Invalid format: %q" % "test"
rescue ArgumentError => e
puts "Format error: #{e.message}"
end
# => Format error: invalid format character 'q'
begin
"Missing precision: %." % 42
rescue ArgumentError => e
puts "Format error: #{e.message}"
end
# => Format error: incomplete format specifier; use %% to print a %
Argument count mismatches produce different error types depending on the mismatch direction. Too few arguments raise ArgumentError
, while extra arguments are silently ignored.
# Too few arguments
begin
"%s %s %s" % ["one", "two"]
rescue ArgumentError => e
puts "Error: #{e.message}"
end
# => Error: too few arguments
# Extra arguments ignored (no error)
"%s %s" % ["one", "two", "three", "four"]
# => "one two"
Hash-based formatting raises KeyError
when referenced keys don't exist in the provided hash, making missing data explicit rather than producing cryptic output.
begin
"Hello %(name)s, %(missing)s" % { name: "Alice" }
rescue KeyError => e
puts "Missing key: #{e.message}"
end
# => Missing key: key not found: "missing"
# Defensive programming with default values
template = "Hello %(name)s, %(status)s"
data = { name: "Alice" }
safe_data = { status: "unknown" }.merge(data)
template % safe_data
# => "Hello Alice, unknown"
Type conversion errors surface when arguments cannot be converted to the expected type for format specifiers. Ruby attempts automatic conversion but fails for incompatible types.
begin
"%d" % "not_a_number"
rescue ArgumentError => e
puts "Conversion error: #{e.message}"
end
# => Conversion error: invalid value for Integer(): "not_a_number"
# Safe conversion with validation
def safe_format(template, *args)
template % args
rescue ArgumentError, TypeError => e
"FORMAT_ERROR: #{e.message}"
end
safe_format("%d items", "abc")
# => "FORMAT_ERROR: invalid value for Integer(): \"abc\""
Debugging complex format strings benefits from systematic testing of each component. Break complex formats into smaller parts and validate arguments before formatting.
def debug_format(template, args)
puts "Template: #{template.inspect}"
puts "Arguments: #{args.inspect}"
# Extract format specifiers
specifiers = template.scan(/%[^%\s]*/)
puts "Specifiers: #{specifiers}"
# Check argument count
expected = specifiers.length
actual = args.is_a?(Array) ? args.length : 1
puts "Expected #{expected} args, got #{actual}"
result = template % args
puts "Result: #{result.inspect}"
result
rescue => e
puts "Error: #{e.class} - #{e.message}"
nil
end
debug_format("User %s has %d points", ["Alice", 100])
Production Patterns
Logging systems frequently use string formatting to create structured, readable log entries with consistent formatting across different message types and severity levels.
class Logger
LOG_FORMAT = "[%s] %5s: %s"
TIMESTAMP_FORMAT = "%Y-%m-%d %H:%M:%S"
def log(level, message, **context)
timestamp = Time.now.strftime(TIMESTAMP_FORMAT)
formatted_message = LOG_FORMAT % [timestamp, level.upcase, message]
unless context.empty?
context_str = context.map { |k, v| "#{k}=#{v}" }.join(" ")
formatted_message += " (#{context_str})"
end
puts formatted_message
end
def info(message, **context)
log(:info, message, **context)
end
def error(message, **context)
log(:error, message, **context)
end
end
logger = Logger.new
logger.info("User login successful", user_id: 12345, ip: "192.168.1.1")
logger.error("Database connection failed", timeout: "30s", retry_count: 3)
# Output:
# [2024-03-15 14:30:15] INFO: User login successful (user_id=12345 ip=192.168.1.1)
# [2024-03-15 14:30:16] ERROR: Database connection failed (timeout=30s retry_count=3)
Financial applications require precise numeric formatting with proper decimal handling, currency symbols, and thousands separators for user-facing displays.
class CurrencyFormatter
def self.format_currency(amount, currency = "USD", precision = 2)
# Handle negative amounts
sign = amount < 0 ? "-" : ""
abs_amount = amount.abs
# Format with precision
formatted = "%.#{precision}f" % abs_amount
# Add thousands separators
parts = formatted.split(".")
parts[0] = parts[0].reverse.gsub(/(\d{3})(?=\d)/, '\1,').reverse
# Combine with currency symbol
case currency.upcase
when "USD" then "#{sign}$#{parts.join('.')}"
when "EUR" then "#{sign}€#{parts.join('.')}"
when "GBP" then "#{sign}£#{parts.join('.')}"
else "#{sign}#{currency} #{parts.join('.')}"
end
end
def self.format_percentage(value, precision = 1)
"%+.#{precision}f%%" % value
end
end
# Usage in financial reports
portfolio = [
{ symbol: "AAPL", shares: 100, price: 150.75, change: 2.3 },
{ symbol: "GOOGL", shares: 50, price: 2750.80, change: -1.2 },
{ symbol: "TSLA", shares: 25, price: 890.45, change: 5.7 }
]
puts "%-8s %6s %12s %12s %8s" % ["Symbol", "Shares", "Price", "Value", "Change"]
puts "-" * 55
portfolio.each do |holding|
value = holding[:shares] * holding[:price]
puts "%-8s %6d %12s %12s %8s" % [
holding[:symbol],
holding[:shares],
CurrencyFormatter.format_currency(holding[:price]),
CurrencyFormatter.format_currency(value),
CurrencyFormatter.format_percentage(holding[:change])
]
end
Report generation and data export systems use formatting to create fixed-width files, CSV exports, and human-readable reports with proper alignment and spacing.
class ReportGenerator
COLUMN_FORMATS = {
string: "%-*s", # Left-aligned string
number: "%*d", # Right-aligned integer
decimal: "%*.2f", # Right-aligned float with 2 decimals
currency: "%*s", # Right-aligned formatted currency
date: "%-*s" # Left-aligned date string
}
def initialize(columns)
@columns = columns
@data = []
end
def add_row(row_data)
@data << row_data
end
def generate_fixed_width
# Calculate column widths based on data
widths = calculate_widths
# Generate header
header = @columns.map.with_index do |col, i|
COLUMN_FORMATS[col[:type]] % [widths[i], col[:name]]
end.join(" ")
# Generate separator
separator = widths.map { |w| "-" * w }.join("-")
# Generate data rows
rows = @data.map do |row|
row.map.with_index do |value, i|
col = @columns[i]
formatted_value = format_value(value, col[:type])
COLUMN_FORMATS[col[:type]] % [widths[i], formatted_value]
end.join(" ")
end
[header, separator, *rows].join("\n")
end
private
def calculate_widths
@columns.map.with_index do |col, i|
header_width = col[:name].length
data_width = @data.map { |row| format_value(row[i], col[:type]).length }.max || 0
[header_width, data_width, col[:min_width] || 0].max
end
end
def format_value(value, type)
case type
when :currency
value.is_a?(Numeric) ? "$%.2f" % value : value.to_s
when :decimal
value.is_a?(Numeric) ? "%.2f" % value : value.to_s
when :date
value.respond_to?(:strftime) ? value.strftime("%Y-%m-%d") : value.to_s
else
value.to_s
end
end
end
# Generate sales report
report = ReportGenerator.new([
{ name: "Product", type: :string, min_width: 12 },
{ name: "Units", type: :number, min_width: 6 },
{ name: "Price", type: :currency, min_width: 10 },
{ name: "Revenue", type: :currency, min_width: 12 },
{ name: "Date", type: :date, min_width: 10 }
])
report.add_row(["Widget Pro", 150, 29.99, 4498.50, Date.new(2024, 3, 15)])
report.add_row(["Gadget Ultra", 89, 149.95, 13345.55, Date.new(2024, 3, 15)])
puts report.generate_fixed_width
Common Pitfalls
Encoding issues arise when format strings and arguments have different encodings, particularly with non-ASCII characters and internationalization scenarios. Ruby's string formatting preserves the encoding of the format string by default.
# Encoding mismatch can cause issues
ascii_format = "Name: %s".encode("ASCII")
utf8_name = "José".encode("UTF-8")
begin
result = ascii_format % utf8_name
rescue Encoding::CompatibilityError => e
puts "Encoding error: #{e.message}"
end
# Solution: ensure consistent encoding
utf8_format = ascii_format.encode("UTF-8")
result = utf8_format % utf8_name
# => "Name: José"
# Or force encoding compatibility
result = ascii_format % utf8_name.encode("ASCII", undef: :replace, invalid: :replace)
# => "Name: Jos?"
Precision specifiers behave differently for strings versus numbers, leading to unexpected truncation when mixing data types in generic formatting functions.
# String truncation with precision
"%.5s" % "This is a long string"
# => "This " (truncated to 5 characters)
# Numeric precision
"%.5f" % 3.14159265
# => "3.14159" (5 decimal places)
# Mixing types can surprise developers
values = ["Short", "This is a very long string", 3.14159]
values.each { |v| puts "Value: %.5s" % v }
# Output:
# Value: Short
# Value: This (truncated!)
# Value: 3.141 (converted to string first)
Width and precision interactions create confusion when both are specified. Width applies to the entire formatted result, including precision-controlled decimal places.
# Width vs precision for numbers
"'%8.2f'" % 123.456
# => " 123.46" (8 total width, 2 decimal places)
# Common mistake: expecting width to be integer part only
"'%8.2f'" % 12345.67
# => "12345.67" (width ignored when number is wider)
# Left-alignment with precision
"'%-8.2f'" % 123.456
# => "123.46 " (left-aligned in 8-character field)
Zero-padding flags interact unexpectedly with negative numbers and different numeric types, particularly with hexadecimal and octal representations.
# Zero padding with negative numbers
"%08d" % -42
# => "-0000042" (sign preserved, padding added)
# Hex formatting with zero padding and signs
"%+08x" % 255
# => "+0000ff" (plus sign with hex)
# Octal padding pitfalls
"%08o" % 64
# => "00000100" (octal representation padded)
# Mixing flags can produce unexpected results
"%+#08x" % 255
# => "+0x000ff" (alternate form + sign + padding)
Positional argument references become error-prone in complex templates, especially when arguments are reordered or removed during template maintenance.
# Fragile positional references
template = "User %2$s (ID: %1$d) has %3$d messages from %4$s to %5$s"
args = [12345, "Alice", 7, "2024-01-01", "2024-03-15"]
# Easy to break when modifying
# If we remove argument 4, all subsequent references break
broken_template = "User %2$s (ID: %1$d) has %3$d messages until %4$s"
broken_args = [12345, "Alice", 7, "2024-03-15"] # Missing date argument
begin
broken_template % broken_args
rescue ArgumentError => e
puts "Broken reference: #{e.message}"
end
# Better approach: use hash-based formatting for complex templates
safe_template = "User %(name)s (ID: %(id)d) has %(count)d messages until %(end_date)s"
safe_args = { name: "Alice", id: 12345, count: 7, end_date: "2024-03-15" }
safe_template % safe_args
Type coercion failures occur silently in some cases and loudly in others, creating inconsistent behavior patterns that are difficult to debug in production systems.
# Silent coercion that may surprise
"%d" % "123" # => "123" (string converted to integer)
"%d" % "123.45" # => ArgumentError (can't convert float string)
"%f" % "123" # => "123.000000" (string converted to float)
"%s" % 123 # => "123" (number converted to string)
# Nil handling varies by format specifier
"%s" % nil # => "" (nil becomes empty string)
"%d" % nil # => TypeError (nil can't be converted to integer)
# Object conversion depends on method availability
class CustomObject
def initialize(value)
@value = value
end
def to_s
"Custom(#{@value})"
end
def to_i
@value.to_i
end
end
obj = CustomObject.new("42")
"%s" % obj # => "Custom(42)" (uses to_s)
"%d" % obj # => 42 (uses to_i)
"%f" % obj # => TypeError (no to_f method)
Reference
Format Specifiers
Specifier | Type | Description | Example |
---|---|---|---|
%s |
String | String representation via to_s |
"Hello %s" % "World" → "Hello World" |
%d , %i |
Integer | Decimal integer via to_i |
"%d" % 42.7 → "42" |
%o |
Integer | Octal representation | "%o" % 64 → "100" |
%x |
Integer | Lowercase hexadecimal | "%x" % 255 → "ff" |
%X |
Integer | Uppercase hexadecimal | "%X" % 255 → "FF" |
%f |
Float | Fixed-point decimal via to_f |
"%.2f" % 3.14159 → "3.14" |
%e |
Float | Scientific notation (lowercase) | "%e" % 1234.5 → "1.234500e+03" |
%E |
Float | Scientific notation (uppercase) | "%E" % 1234.5 → "1.234500E+03" |
%g |
Float | General format (shorter of %f or %e) | "%g" % 1234.5 → "1234.5" |
%G |
Float | General format (shorter of %f or %E) | "%G" % 0.0001 → "0.0001" |
%c |
Integer | Character with given codepoint | "%c" % 65 → "A" |
%p |
Any | Object inspection via inspect |
"%p" % [1,2,3] → "[1, 2, 3]" |
%% |
Literal | Literal percent character | "100%% complete" → "100% complete" |
Flags
Flag | Description | Example |
---|---|---|
+ |
Always show sign for numbers | "%+d" % 42 → "+42" |
- |
Left-align within field width | "%-5s" % "hi" → "hi " |
0 |
Pad with zeros instead of spaces | "%05d" % 42 → "00042" |
(space) |
Prefix positive numbers with space | "% d" % 42 → " 42" |
# |
Use alternate form | "%#x" % 255 → "0xff" |
Width and Precision
Format | Description | Example |
---|---|---|
%10s |
Right-align in 10-character field | "%10s" % "hello" → " hello" |
%-10s |
Left-align in 10-character field | "%-10s" % "hello" → "hello " |
%.5s |
Truncate string to 5 characters | "%.5s" % "hello world" → "hello" |
%10.2f |
10-character field, 2 decimal places | "%10.2f" % 3.14159 → " 3.14" |
%*s |
Width from next argument | "%*s" % [8, "hello"] → " hello" |
%.*f |
Precision from next argument | "%.*f" % [3, 3.14159] → "3.142" |
Positional Arguments
Format | Description | Example |
---|---|---|
%1$s |
Use first argument | "%2$s %1$s" % ["World", "Hello"] → "Hello World" |
%2$d |
Use second argument | "%1$s: %2$d" % ["Count", 5] → "Count: 5" |
%(name)s |
Use hash key 'name' | "%(greeting)s %(name)s" % {name: "Alice", greeting: "Hi"} → "Hi Alice" |
Methods
Method | Parameters | Returns | Description |
---|---|---|---|
String#% |
args (Object or Array) |
String |
Apply format string to arguments |
Kernel#sprintf |
format (String), *args |
String |
Create formatted string |
Kernel#printf |
format (String), *args |
nil |
Print formatted string to stdout |
IO#printf |
format (String), *args |
IO |
Print formatted string to IO object |
Common Error Types
Error | Cause | Example |
---|---|---|
ArgumentError |
Invalid format specifier | "%q" % "test" |
ArgumentError |
Too few arguments | "%s %s" % ["only_one"] |
ArgumentError |
Type conversion failure | "%d" % "not_a_number" |
KeyError |
Missing hash key | "%(missing)s" % {} |
TypeError |
Incompatible argument type | "%f" % Object.new |
Encoding::CompatibilityError |
Encoding mismatch | ASCII format with UTF-8 args |