Simple Ruby class to print ActiveRecord results as a table

You ever wanted to print a result of records in a fancier way?

With this simple class you can print tables like this:

records = Post.includes(:user).where(user_id: [1,2])
attributes = [:created_at, :title, {user: :name}]

TablePrinter.new(records, attributes).print_table
+-------------------------+-------------------+-----------------+
|  Created at             |  Title            |  User Name      |
+-------------------------+-------------------+-----------------+
| 2020-12-14 02:02:02 UTC | Un titulo 3       | Magdalena Silva |
| 2020-12-14 02:18:25 UTC | ¿Qué es escribir? | Benjamín Silva  |
| 2020-12-20 18:41:46 UTC | Primero para leo  | Magdalena Silva |
| 2021-02-06 14:18:54 UTC | A File            | Benjamín Silva  |
| 2020-12-20 04:09:37 UTC | aasdasd           | Benjamín Silva  |
| 2021-02-12 18:14:34 UTC |                   | Benjamín Silva  |
| 2020-12-14 01:50:20 UTC | Test super title  | Benjamín Silva  |
| 2021-02-06 03:32:02 UTC | lala5             | Benjamín Silva  |
+-------------------------+-------------------+-----------------+

This is an adaptation of this response, stackoverflow.com/a/28685559/1275069 to make it work with ActiveRecord right away.

It supports deeply nested attributes (example: post.user.some_model.some_other.the_attribute), but remember to include them in the query to avoid n+1.

# frozen_string_literal: true

class TablePrinter
  def initialize(records, attrs)
    @records = records
    @attrs = attrs
  end

  # rubocop:disable Rails/Output
  def print_table
    buffer = [divider, header, divider, content, divider].join("\n")
    puts buffer
  end
  # rubocop:enable Rails/Output

  private

  attr_reader :records, :attrs

  def content
    records.map { |row| line(row) }
  end

  def header
    "| #{columns.map { |_, g| g[:label].ljust(g[:width]) }.join(' | ')} |"
  end

  def divider
    "+-#{columns.map { |_, g| '-' * g[:width] }.join('-+-')}-+"
  end

  def line(row)
    str = attrs.map { |attr| row_value(row, attr).ljust(columns[attr][:width]) }.join(' | ')
    "| #{str} |"
  end

  def row_value(row, attr)
    if attr.is_a?(Hash)
      row_value(row.send(attr.keys.first), attr.values.first)
    else
      row ? row.send(attr).to_s : ''
    end
  end

  def columns
    @columns ||= col_labels.each_with_object({}) do |(attr, label), h|
      h[attr] = {
        label: label,
        width: [records.map { |row| row_value(row, attr).to_s.size }.max, label.size].max
      }
    end
  end

  def col_labels
    @col_labels ||= attrs.index_with { |attr| nested_name(attr) }
  end

  def nested_name(attr, memo = '')
    if attr.is_a?(Hash)
      nested_name(attr.values.first, "#{memo} #{attr.keys.first.to_s.humanize}")
    else
      "#{memo} #{attr.to_s.humanize}"
    end
  end
end

Feel free to use it in your projects! Happy Coding.

No Comments Yet