Parsing & Formatting in Ruby
Why Master Text Parsing & Formatting?
Parsing and formatting are essential skills for professional software development. They bridge the gap between raw data and human-readable output:
- Professional Output: Create reports, invoices, and documents with precise formatting
- Data Presentation: Format numbers, currencies, dates, and percentages consistently
- User Interfaces: Display data in tables, lists, and forms with proper alignment
- API Integration: Parse incoming data and format outgoing responses
- Business Logic: Generate contracts, statements, and regulatory reports
Problems Parsing & Formatting Solves
1. Consistent Data Presentation
Eliminate manual formatting errors by using consistent, programmatic formatting rules. Ensure numbers, dates, and currency always appear in the correct format across your application.
2. Professional Reports & Documents
Generate professional-looking reports, invoices, statements, and documents with proper alignment, padding, and formatting that meets business standards and regulatory requirements.
3. Data Parsing & Validation
Parse structured data from files, APIs, and user input reliably. Extract meaningful information from text and validate it meets expected formats and ranges.
Learning Path
1. sprintf and printf Fundamentals
Understanding sprintf vs printf
sprintf returns a formatted string, while printf prints directly to stdout.
Both use the same format specifiers, but sprintf is more commonly used in applications
since you can store, modify, or further process the result.
# Basic sprintf usage
name = "Alice"
age = 30
score = 87.5
# String formatting with sprintf
formatted = sprintf("Name: %s, Age: %d, Score: %.1f", name, age, score)
puts formatted # => "Name: Alice, Age: 30, Score: 87.5"
# Alternative syntax (% operator)
formatted = "Name: %s, Age: %d, Score: %.1f" % [name, age, score]
puts formatted # => "Name: Alice, Age: 30, Score: 87.5"
# printf prints directly
printf("Name: %s, Age: %d, Score: %.1f\n", name, age, score)
# Output: Name: Alice, Age: 30, Score: 87.5
# Named parameters (more readable for complex formatting)
formatted = sprintf("Name: %{name}s, Age: %{age}d, Score: %{score}.1f",
name: name, age: age, score: score)
puts formatted # => "Name: Alice, Age: 30, Score: 87.5"
# Using format method (alias for sprintf)
formatted = format("Name: %s, Age: %d, Score: %.1f", name, age, score)
puts formatted # => "Name: Alice, Age: 30, Score: 87.5"
💡 When to Use Each Method
- sprintf/format: When you need to store, modify, or return the formatted string
- printf: For direct output, debugging, or simple console applications
- % operator: Quick formatting when you have an array of values
- Named parameters: Complex formatting with many variables for readability
2. Format Specifiers and Precision Control
Basic Format Specifiers
Format specifiers control how values are converted to strings. Understanding these gives you precise control over output appearance, crucial for reports, financial data, and user interfaces.
# Basic format specifiers
number = 42
decimal = 123.456789
text = "Ruby"
# %d - Integer (decimal)
puts sprintf("%d", number) # => "42"
puts sprintf("%5d", number) # => " 42" (right-aligned in 5 chars)
puts sprintf("%05d", number) # => "00042" (zero-padded)
puts sprintf("%-5d", number) # => "42 " (left-aligned)
# %f - Floating point
puts sprintf("%f", decimal) # => "123.456789"
puts sprintf("%.2f", decimal) # => "123.46" (2 decimal places)
puts sprintf("%8.2f", decimal) # => " 123.46" (8 chars total, 2 decimal)
puts sprintf("%08.2f", decimal) # => "00123.46" (zero-padded)
# %s - String
puts sprintf("%s", text) # => "Ruby"
puts sprintf("%10s", text) # => " Ruby" (right-aligned in 10 chars)
puts sprintf("%-10s", text) # => "Ruby " (left-aligned)
puts sprintf("%.2s", text) # => "Ru" (truncated to 2 chars)
# %x - Hexadecimal
puts sprintf("%x", 255) # => "ff"
puts sprintf("%X", 255) # => "FF" (uppercase)
puts sprintf("%#x", 255) # => "0xff" (with prefix)
# %o - Octal
puts sprintf("%o", 64) # => "100"
# %e/%E - Scientific notation
puts sprintf("%e", 1234.56) # => "1.234560e+03"
puts sprintf("%.2E", 1234.56) # => "1.23E+03"
# %g/%G - General format (auto-chooses %f or %e)
puts sprintf("%g", 123.456) # => "123.456"
puts sprintf("%g", 0.000123) # => "0.000123"
puts sprintf("%g", 123456789.0) # => "1.23457e+08"
Advanced Formatting Options
Advanced formatting includes flags, width specifiers, and precision controls that give you fine-grained control over output appearance.
# Advanced formatting flags and options
# + flag: Always show sign
puts sprintf("%+d", 42) # => "+42"
puts sprintf("%+d", -42) # => "-42"
puts sprintf("%+.2f", 123.45) # => "+123.45"
# Space flag: Space for positive numbers
puts sprintf("% d", 42) # => " 42"
puts sprintf("% d", -42) # => "-42"
# # flag: Alternative form
puts sprintf("%#x", 255) # => "0xff"
puts sprintf("%#o", 64) # => "0100"
puts sprintf("%#.2f", 123.0) # => "123.00" (force decimal point)
# Complex width and precision
price = 1234.567
puts sprintf("%10.2f", price) # => " 1234.57" (10 chars total, 2 decimal)
puts sprintf("%-10.2f", price) # => "1234.57 " (left-aligned)
puts sprintf("%010.2f", price) # => "0001234.57" (zero-padded)
# Dynamic width and precision using *
width = 15
precision = 3
puts sprintf("%*.*f", width, precision, price) # => " 1234.567"
# Multiple values with different formats
data = [1, "Product A", 29.99, 5]
formatted = sprintf("%3d | %-12s | %8.2f | %2d", *data)
puts formatted # => " 1 | Product A | 29.99 | 5"
# Currency formatting example
amount = 1234.56
formatted = sprintf("$%,.2f", amount)
puts formatted # Note: Ruby doesn't have built-in comma separator
# For commas, we need a custom solution:
def format_currency(amount)
sprintf("$%.2f", amount).gsub(/(\d)(?=(\d{3})+\.)/,'\1,')
end
puts format_currency(1234567.89) # => "$1,234,567.89"
3. Number and Currency Formatting
Professional Number Formatting
Professional applications require consistent number formatting for financial data, scientific calculations, and business reports. Ruby provides tools to format numbers with appropriate precision, separators, and prefixes.
# Professional number formatting helpers
class NumberFormatter
# Format currency with commas and dollar sign
def self.currency(amount, currency_symbol = "$")
formatted = sprintf("%.2f", amount.abs)
with_commas = formatted.gsub(/(\d)(?=(\d{3})+\.)/,'\1,')
sign = amount < 0 ? "-" : ""
"#{sign}#{currency_symbol}#{with_commas}"
end
# Format large numbers with commas
def self.with_commas(number)
sprintf("%.0f", number).gsub(/(\d)(?=(\d{3})+$)/,'\1,')
end
# Format percentages
def self.percentage(decimal, precision = 1)
sprintf("%.#{precision}f%%", decimal * 100)
end
# Format file sizes
def self.file_size(bytes)
units = ['B', 'KB', 'MB', 'GB', 'TB']
return "0 B" if bytes == 0
exp = (Math.log(bytes) / Math.log(1024)).floor
size = bytes / (1024.0 ** exp)
sprintf("%.1f %s", size, units[exp])
end
# Format scientific notation for readability
def self.scientific(number, precision = 2)
sprintf("%.#{precision}e", number)
end
end
# Examples
puts NumberFormatter.currency(1234567.89) # => "$1,234,567.89"
puts NumberFormatter.currency(-567.8) # => "-$567.80"
puts NumberFormatter.with_commas(1000000) # => "1,000,000"
puts NumberFormatter.percentage(0.856) # => "85.6%"
puts NumberFormatter.percentage(0.856, 2) # => "85.60%"
puts NumberFormatter.file_size(1536) # => "1.5 KB"
puts NumberFormatter.file_size(1073741824) # => "1.0 GB"
puts NumberFormatter.scientific(0.000000123) # => "1.23e-07"
# Financial formatting with different currencies
prices = [1234.56, 567.89, 12.34]
currencies = ["$", "€", "£"]
prices.zip(currencies).each do |price, symbol|
puts NumberFormatter.currency(price, symbol)
end
# Output:
# $1,234.56
# €567.89
# £12.34
Business-Specific Formatting
Different business contexts require specific formatting conventions. Here are common patterns for invoices, reports, and financial statements.
# Business formatting patterns
# Invoice line item formatting
def format_invoice_line(description, quantity, unit_price)
total = quantity * unit_price
sprintf("%-30s %8.2f x %12s = %12s",
description.slice(0, 30),
quantity,
NumberFormatter.currency(unit_price),
NumberFormatter.currency(total))
end
# Financial statement formatting
def format_financial_line(label, amount, width = 40)
if amount >= 0
sprintf("%-#{width-15}s %15s", label, NumberFormatter.currency(amount))
else
# Show negative amounts in parentheses
formatted_amount = NumberFormatter.currency(amount.abs)
sprintf("%-#{width-15}s (%13s)", label, formatted_amount)
end
end
# Percentage change formatting
def format_change(old_value, new_value)
change = new_value - old_value
percent_change = (change / old_value.abs) * 100
direction = change >= 0 ? "↑" : "↓"
color_code = change >= 0 ? "32" : "31" # Green for positive, red for negative
sprintf("%s %.1f%% (\e[#{color_code}m%s%s\e[0m)",
NumberFormatter.currency(change),
percent_change.abs,
direction,
NumberFormatter.currency(change.abs))
end
# Examples
puts format_invoice_line("Ruby Development Services", 40, 125.00)
puts format_invoice_line("Code Review", 8, 75.50)
puts format_invoice_line("Documentation Writing", 12, 60.00)
puts
puts format_financial_line("Revenue", 125000.50)
puts format_financial_line("Expenses", -89000.75)
puts format_financial_line("Net Income", 35999.75)
puts
puts "Sales change: #{format_change(100000, 125000)}"
puts "Costs change: #{format_change(50000, 45000)}"
# Output:
# Ruby Development Services 40.00 x $125.00 = $5,000.00
# Code Review 8.00 x $75.50 = $604.00
# Documentation Writing 12.00 x $60.00 = $720.00
#
# Revenue $125,000.50
# Expenses ($89,000.75)
# Net Income $35,999.75
#
# Sales change: $25,000.00 25.0% (↑$25,000.00)
# Costs change: -$5,000.00 10.0% (↓$5,000.00)
4. Table and Report Generation
Professional Table Formatting
Tables are essential for presenting structured data. Professional table formatting includes proper alignment, consistent spacing, headers, separators, and totals.
# Professional table generator
class TableFormatter
def initialize(headers, column_widths = nil)
@headers = headers
@rows = []
@column_widths = column_widths || calculate_auto_widths
end
def add_row(row)
@rows << row
recalculate_widths if @column_widths.nil?
end
def add_separator
@rows << :separator
end
def to_s
result = []
# Header
result << format_row(@headers, bold: true)
result << separator_line
# Rows
@rows.each do |row|
if row == :separator
result << separator_line
else
result << format_row(row)
end
end
result.join("\n")
end
private
def format_row(row, bold: false)
formatted_cells = row.each_with_index.map do |cell, index|
width = @column_widths[index]
# Format based on content type
if cell.is_a?(Numeric)
sprintf("%#{width}.2f", cell)
else
sprintf("%-#{width}s", cell.to_s)
end
end
line = "| #{formatted_cells.join(' | ')} |"
bold ? "\e[1m#{line}\e[0m" : line
end
def separator_line
widths = @column_widths.map { |w| "-" * w }
"+#{widths.map { |w| "-#{w}-" }.join('+')}+"
end
def calculate_auto_widths
all_data = [@headers] + @rows.reject { |r| r == :separator }
(0...@headers.length).map do |col_index|
all_data.map { |row| row[col_index].to_s.length }.max + 2
end
end
end
# Example: Sales report
sales_table = TableFormatter.new(
["Product", "Q1", "Q2", "Q3", "Q4", "Total"],
[20, 12, 12, 12, 12, 15]
)
sales_data = [
["Ruby Services", 125000, 145000, 135000, 155000],
["Rails Consulting", 89000, 95000, 102000, 98000],
["Training", 45000, 52000, 48000, 61000]
]
sales_data.each do |product, q1, q2, q3, q4|
total = q1 + q2 + q3 + q4
sales_table.add_row([product, q1, q2, q3, q4, total])
end
sales_table.add_separator
# Add totals row
totals = sales_data.reduce([0, 0, 0, 0]) do |acc, (_, q1, q2, q3, q4)|
[acc[0] + q1, acc[1] + q2, acc[2] + q3, acc[3] + q4]
end
total_all = totals.sum
sales_table.add_row(["TOTAL", *totals, total_all])
puts sales_table.to_s
Report Generation
Complete reports combine multiple formatting techniques to create professional documents with headers, data sections, calculations, and summaries.
# Comprehensive report generator
class ReportGenerator
def initialize(title, company_name = nil)
@title = title
@company_name = company_name
@sections = []
@created_at = Time.now
end
def add_section(title, content)
@sections << { title: title, content: content }
end
def generate
report = []
# Header
report << "=" * 80
report << @company_name.center(80) if @company_name
report << @title.center(80)
report << "Generated: #{@created_at.strftime('%B %d, %Y at %I:%M %p')}".center(80)
report << "=" * 80
report << ""
# Sections
@sections.each_with_index do |section, index|
report << "#{index + 1}. #{section[:title]}"
report << "-" * (section[:title].length + 3)
report << ""
if section[:content].is_a?(String)
report << section[:content]
elsif section[:content].respond_to?(:to_s)
report << section[:content].to_s
end
report << ""
end
# Footer
report << "=" * 80
report << "End of Report".center(80)
report << "=" * 80
report.join("\n")
end
end
# Example: Monthly financial report
def generate_monthly_report(month, year)
report = ReportGenerator.new(
"Monthly Financial Report - #{Date::MONTHNAMES[month]} #{year}",
"Ruby Development Company"
)
# Revenue breakdown
revenue_data = [
["Development Services", 85000],
["Consulting", 42000],
["Training", 18000],
["Support", 12000]
]
revenue_table = TableFormatter.new(
["Service", "Revenue"],
[30, 15]
)
total_revenue = 0
revenue_data.each do |service, amount|
revenue_table.add_row([service, NumberFormatter.currency(amount)])
total_revenue += amount
end
revenue_table.add_separator
revenue_table.add_row(["TOTAL REVENUE", NumberFormatter.currency(total_revenue)])
report.add_section("Revenue Breakdown", revenue_table)
# Expense summary
expenses = {
"Salaries" => 45000,
"Office Rent" => 8000,
"Software Licenses" => 3500,
"Marketing" => 6200,
"Other" => 2800
}
expense_content = expenses.map do |category, amount|
sprintf("%-20s %15s", category, NumberFormatter.currency(amount))
end.join("\n")
expense_content += "\n" + "-" * 35
total_expenses = expenses.values.sum
expense_content += "\n" + sprintf("%-20s %15s", "TOTAL EXPENSES", NumberFormatter.currency(total_expenses))
report.add_section("Expense Summary", expense_content)
# Bottom line
net_income = total_revenue - total_expenses
margin = (net_income.to_f / total_revenue) * 100
summary = sprintf("Total Revenue: %s\n", NumberFormatter.currency(total_revenue))
summary += sprintf("Total Expenses: %s\n", NumberFormatter.currency(total_expenses))
summary += sprintf("Net Income: %s\n", NumberFormatter.currency(net_income))
summary += sprintf("Profit Margin: %.1f%%", margin)
report.add_section("Financial Summary", summary)
report.generate
end
# Generate and display the report
puts generate_monthly_report(3, 2024)
5. Advanced Data Parsing and Validation
Parsing Structured Data
Real-world applications often need to parse data from various sources like log files, configuration files, APIs, and user input. Robust parsing includes validation and error handling.
# Advanced data parsing with validation
class DataParser
# Parse currency strings to numbers
def self.parse_currency(currency_string)
# Remove currency symbols and commas
cleaned = currency_string.gsub(/[$,€£¥]/, '').strip
# Handle negative values in parentheses
if cleaned.match(/^\((.+)\)$/)
-Float(cleaned.gsub(/[()]/, ''))
else
Float(cleaned)
end
rescue ArgumentError
raise "Invalid currency format: #{currency_string}"
end
# Parse percentage strings
def self.parse_percentage(percent_string)
cleaned = percent_string.gsub(/%/, '').strip
Float(cleaned) / 100.0
rescue ArgumentError
raise "Invalid percentage format: #{percent_string}"
end
# Parse date strings in various formats
def self.parse_flexible_date(date_string)
formats = [
'%Y-%m-%d', # 2024-03-15
'%m/%d/%Y', # 03/15/2024
'%d/%m/%Y', # 15/03/2024
'%B %d, %Y', # March 15, 2024
'%d %B %Y', # 15 March 2024
'%Y%m%d' # 20240315
]
formats.each do |format|
begin
return Date.strptime(date_string.strip, format)
rescue Date::Error
next
end
end
raise "Unable to parse date: #{date_string}"
end
# Parse financial data from text
def self.parse_financial_line(line)
# Example: "Revenue Q1 2024: $125,000.50 (15% increase)"
pattern = /^(.+?):\s*\$?([\d,.-]+)\s*(?:\((.+?)\))?/
match = line.match(pattern)
if match
label = match[1].strip
amount = parse_currency("$#{match[2]}")
note = match[3]&.strip
{ label: label, amount: amount, note: note }
else
raise "Unable to parse financial line: #{line}"
end
end
end
# Examples
puts DataParser.parse_currency("$1,234.56") # => 1234.56
puts DataParser.parse_currency("(567.89)") # => -567.89
puts DataParser.parse_currency("€1.234,56") # => 1234.56
puts DataParser.parse_percentage("25.5%") # => 0.255
puts DataParser.parse_percentage("100%") # => 1.0
puts DataParser.parse_flexible_date("2024-03-15") # => 2024-03-15
puts DataParser.parse_flexible_date("March 15, 2024") # => 2024-03-15
financial_line = "Revenue Q1 2024: $125,000.50 (15% increase)"
parsed = DataParser.parse_financial_line(financial_line)
puts parsed # => {:label=>"Revenue Q1 2024", :amount=>125000.5, :note=>"15% increase"}
Configuration and Template Processing
Applications often need to process configuration files and templates. This includes variable substitution, conditional formatting, and template rendering.
# Simple template processor with variable substitution
class SimpleTemplate
def initialize(template)
@template = template
end
def render(variables = {})
result = @template.dup
# Replace variable placeholders {{variable_name}}
variables.each do |key, value|
placeholder = "{{#{key}}}"
formatted_value = format_value(value)
result.gsub!(placeholder, formatted_value)
end
# Process conditional blocks {{#if condition}}...{{/if}}
result = process_conditionals(result, variables)
result
end
private
def format_value(value)
case value
when Float, BigDecimal
if value.abs >= 1000000
NumberFormatter.with_commas(value.round(0))
elsif value.abs >= 1000
NumberFormatter.with_commas(value.round(2))
else
sprintf("%.2f", value)
end
when Integer
value.abs >= 1000 ? NumberFormatter.with_commas(value) : value.to_s
when Date, Time
value.strftime("%B %d, %Y")
else
value.to_s
end
end
def process_conditionals(text, variables)
# Simple conditional processing: {{#if variable}}content{{/if}}
text.gsub(/\{\{#if\s+(\w+)\}\}(.*?)\{\{\/if\}\}/m) do |match|
condition = $1
content = $2
if variables[condition.to_sym] || variables[condition]
content
else
""
end
end
end
end
# Example: Invoice template
invoice_template = <<~TEMPLATE
INVOICE #{{invoice_number}}
Date: {{invoice_date}}
Due Date: {{due_date}}
Bill To:
{{client_name}}
{{client_address}}
{{#if rush_job}}
*** RUSH JOB - EXPEDITED PROCESSING ***
{{/if}}
Services:
{{service_description}}
Subtotal: ${{subtotal}}
{{#if tax_rate}}
Tax ({{tax_rate}}%): ${{tax_amount}}
{{/if}}
TOTAL: ${{total_amount}}
{{#if payment_terms}}
Payment Terms: {{payment_terms}}
{{/if}}
TEMPLATE
# Generate invoice
template = SimpleTemplate.new(invoice_template)
invoice_data = {
invoice_number: "INV-2024-001",
invoice_date: Date.new(2024, 3, 15),
due_date: Date.new(2024, 4, 15),
client_name: "ABC Corporation",
client_address: "123 Business St, Suite 100\nBusiness City, BC 12345",
service_description: "Ruby Development Services (40 hours @ $125/hr)",
subtotal: 5000.00,
tax_rate: 8.5,
tax_amount: 425.00,
total_amount: 5425.00,
rush_job: true,
payment_terms: "Net 30 days"
}
puts template.render(invoice_data)
Best Practices and Performance Tips
🎯 Best Practices
- Use named parameters: For complex formatting, named parameters improve readability
- Validate input data: Always validate data before formatting to prevent errors
- Create reusable formatters: Build utility classes for consistent formatting across your app
- Consider localization: Different regions have different number and date formats
- Cache formatted values: For expensive formatting operations, consider caching results
⚠️ Common Pitfalls
Floating Point Precision
Be careful with floating point arithmetic for financial calculations. Consider using BigDecimal for precise currency calculations.
Format Specifier Mismatches
Ensure your format specifiers match the data types. Using %d with a string will cause errors.
Performance with Large Datasets
String formatting can be expensive. For large reports, consider streaming output or batch processing.
🚀 Next Steps
With parsing and formatting skills, you can create professional applications with polished output. Continue your learning with:
- Time & Date Processing: Learn to format and parse dates, times, and durations
- Internationalization: Adapt formatting for different locales and languages
- Template Engines: Explore ERB, Mustache, or other templating systems
- Business Applications: Apply these skills to invoicing, reporting, and document generation