Simple Ruby class to print ActiveRecord results as a table
Benjamín Silva
Posted on March 5, 2021
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, https://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.
💖 💪 🙅 🚩
Benjamín Silva
Posted on March 5, 2021
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.