Ruby Logo

Parsing & Formatting

Master sprintf/printf formatting, number formatting, and advanced text parsing techniques.

Home Ruby Parsing & Formatting

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
2 Format specifiers and precision control
3 Number and currency formatting
4 Table and report generation
5 Advanced parsing and business applications

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

Quick Navigation

Related Topics

Video Tutorial

Watch and learn parsing & formatting

Pro Tip: After reading through the content above, watch this video to reinforce your understanding and see the concepts in action!