Class: Tabulo::Table

Inherits:
Object
  • Object
show all
Includes:
Enumerable
Defined in:
lib/tabulo/table.rb

Overview

Represents a table primarily intended for "pretty-printing" in a fixed-width font.

A Table is also an Enumerable, of which each element is a Row.

Instance Method Summary collapse

Constructor Details

#initialize(sources, columns: [], column_width: nil, header_frequency: :start, wrap_header_cells_to: nil, wrap_body_cells_to: nil) {|_self| ... } ⇒ Table

Returns a new Table.

Parameters:

  • sources (Enumerable)

    the underlying Enumerable from which the table will derive its data

  • columns (Array[Symbol]) (defaults to: [])

    Specifies the initial columns. The Symbols provided must be unique. Each element of the Array will be used to create a column whose content is created by calling the corresponding method on each element of sources. Note the #add_column method is a much more flexible way to set up columns on the table.

  • column_width (Integer, nil) (defaults to: nil)

    The default column width for columns in this table, not excluding padding. If nil, then DEFAULT_COLUMN_WIDTH will be used.

  • header_frequency (:start, nil, Integer) (defaults to: :start)

    Controls the display of column headers. If passed :start, headers will be shown at the top of the table only. If passed nil, headers will not be shown. If passed an Integer N (> 0), headers will be shown at the top of the table, then repeated every N rows.

  • wrap_header_cells_to (nil, Integer) (defaults to: nil)

    Controls wrapping behaviour for header cells if the content thereof is longer than the column's fixed width. If passed nil (default), content will be wrapped for as many rows as required to accommodate it. If passed an Integer N (> 0), content will be wrapped up to N rows and then truncated thereafter.

  • wrap_body_cells_to (nil, Integer) (defaults to: nil)

    Controls wrapping behaviour for table cells (excluding headers), if their content is longer than the column's fixed width. If passed nil, content will be wrapped for as many rows as required to accommodate it. If passed an Integer N (> 0), content will be wrapped up to N rows and then truncated thereafter.

Yields:

  • (_self)

Yield Parameters:

  • _self (Tabulo::Table)

    the object that the method was called on

Raises:



51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
# File 'lib/tabulo/table.rb', line 51

def initialize(sources, columns: [], column_width: nil, header_frequency: :start,
  wrap_header_cells_to: nil, wrap_body_cells_to: nil)

  @sources = sources
  @header_frequency = header_frequency
  @wrap_header_cells_to = wrap_header_cells_to
  @wrap_body_cells_to = wrap_body_cells_to

  @default_column_width = (column_width || DEFAULT_COLUMN_WIDTH)

  @column_registry = { }
  columns.each { |item| add_column(item) }

  yield self if block_given?
end

Instance Method Details

#add_column(label, header: nil, align_header: :center, align_body: nil, width: nil, formatter: :to_s.to_proc, &extractor) ⇒ Object

Adds a column to the Table.

Parameters:

  • label (Symbol, String)

    A unique identifier for this column, which by default will also be used as the column header text (see also the header param). If the extractor argument is not also provided, then the label argument should correspond to a method to be called on each item in the table sources to provide the content for this column.

  • header (nil, #to_s) (defaults to: nil)

    (nil) Text to be displayed in the column header. If passed nil, the column's label will also be used as its header text.

  • align_header (:left, :center, :right) (defaults to: :center)

    (:center) Specifies how the header text should be aligned.

  • align_body (:left, :center, :right, nil) (defaults to: nil)

    (nil) Specifies how the cell body contents should be aligned. Possible If nil is passed, then the alignment is determined by the type of the cell value, with numbers aligned right, booleans center-aligned, and other values left-aligned. Note header text alignment is configured separately using the :align_header param.

  • width (Integer) (defaults to: nil)

    (nil) Specifies the width of the column, excluding padding. If nil, then the column will take the width provided by the column_width param with which the Table was initialized.

  • formatter (#to_proc) (defaults to: :to_s.to_proc)

    (:to_s.to_proc) A lambda or other callable object that will be passed the calculated value of each cell to determine how it should be displayed. This is distinct from the extractor (see below). For example, if the extractor for this column generates a Date, then the formatter might format that Date in a particular way. If no formatter is provided, then .to_s will be called on the extracted value of each cell to determine its displayed content.

  • extractor (#to_proc)

    A block or other callable that will be passed each of the Table sources to determine the value in each cell of this column. If this is not provided, then the column label will be treated as a method to be called on each source item to determine each cell's value.

Raises:

  • (InvalidColumnLabelError)

    if label has already been used for another column in this Table. (This is case-sensitive, but is insensitive to whether a String or Symbol is passed to the label parameter.)



99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
# File 'lib/tabulo/table.rb', line 99

def add_column(label, header: nil, align_header: :center, align_body: nil,
  width: nil, formatter: :to_s.to_proc, &extractor)

  column_label = label.to_sym

  if column_registry.include?(column_label)
    raise InvalidColumnLabelError, "Column label already used in this table."
  end

  @column_registry[column_label] =
    Column.new(
      header: (header || label).to_s,
      align_header: align_header,
      align_body: align_body,
      width: (width || @default_column_width),
      formatter: formatter,
      extractor: (extractor || label.to_proc)
    )
end

#eachObject

Calls the given block once for each Row in the Table, passing that Row as parameter.

Note that when printed, the first row will visually include the headers (assuming these were not disabled when the Table was initialized).

Examples:

table.each do |row|
  puts row
end


138
139
140
141
142
143
144
145
146
147
148
149
150
151
# File 'lib/tabulo/table.rb', line 138

def each
  @sources.each_with_index do |source, index|
    include_header =
      case @header_frequency
      when :start
        index == 0
      when Integer
        index % @header_frequency == 0
      else
        @header_frequency
      end
    yield body_row(source, with_header: include_header)
  end
end

#formatted_headerString

Returns an "ASCII" graphical representation of the Table column headers.

Returns:

  • (String)

    an "ASCII" graphical representation of the Table column headers.



154
155
156
157
# File 'lib/tabulo/table.rb', line 154

def formatted_header
  cells = column_registry.map { |_, column| column.header_subcells }
  format_row(cells, @wrap_header_cells_to)
end

#horizontal_ruleString

Returns an "ASCII" graphical representation of a horizontal dividing line suitable for printing at any point in the table.

Examples:

Print a horizontal divider after every row:

table.each do |row|
  puts row
  puts table.horizontal_rule
end

Returns:

  • (String)

    an "ASCII" graphical representation of a horizontal dividing line suitable for printing at any point in the table.



167
168
169
170
171
172
# File 'lib/tabulo/table.rb', line 167

def horizontal_rule
  inner = column_registry.map do |_, column|
    surround(column.horizontal_rule, HORIZONTAL_RULE_CHARACTER)
  end
  surround_join(inner, CORNER_CHARACTER)
end

#shrinkwrap!(max_table_width: nil) ⇒ Table

Reset all the column widths so that each column is just wide enough to accommodate its header text as well as the formatted content of each its cells for the entire collection, together with a single character of padding on either side of the column, without any wrapping.

Note that calling this method will cause the entire source Enumerable to be traversed and all the column extractors and formatters to be applied in order to calculate the required widths.

Note also that this method causes column widths to be fixed as appropriate to the formatted cell contents given the state of the source Enumerable at the point it is called. If the source Enumerable changes between that point, and the point when the Table is printed, then columns will not be resized yet again on printing.

Parameters:

  • max_table_width (nil, Numeric) (defaults to: nil)

    (nil) If provided, stops the total table width (including padding and borders) from expanding beyond this number of characters. Width is deducted from columns if required to achieve this, with one character progressively deducted from the width of the widest column until the target is reached. When the table is printed, wrapping or truncation will then occur in these columns as required (depending on how they were configured). Note that regardless of the value passed to max_table_width, the table will always be left wide enough to accommodate at least 1 character's width of content, 1 character of left padding and 1 character of right padding in each column, together with border characters (1 on each side of the table and 1 between adjacent columns). I.e. there is a certain width below width the Table will refuse to shrink itself.

Returns:

  • (Table)

    the Table itself



200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
# File 'lib/tabulo/table.rb', line 200

def shrinkwrap!(max_table_width: nil)
  return self if column_registry.none?
  columns = column_registry.values

  columns.each do |column|
    column.width = wrapped_width(column.header)
  end

  @sources.each do |source|
    columns.each do |column|
      width = wrapped_width(column.formatted_cell_content(source))
      column.width = width if width > column.width
    end
  end

  if max_table_width
    total_columns_width = columns.inject(0) { |sum, column| sum + column.width }
    total_padding = column_registry.count * 2
    total_borders = column_registry.count + 1
    unadjusted_table_width = total_columns_width + total_padding + total_borders

    # Ensure max table width is at least wide enough to accommodate table borders and padding
    # and one character of content.
    min_table_width = total_padding + total_borders + column_registry.count
    max_table_width = min_table_width if min_table_width > max_table_width

    required_reduction = [unadjusted_table_width - max_table_width, 0].max

    required_reduction.times do
      widest_column = columns.inject(columns.first) do |widest, column|
        column.width >= widest.width ? column : widest
      end

      widest_column.width -= 1
    end
  end

  self
end

#to_sString

Returns a graphical "ASCII" representation of the Table, suitable for display in a fixed-width font.

Returns:

  • (String)

    a graphical "ASCII" representation of the Table, suitable for display in a fixed-width font.



121
122
123
124
125
126
127
# File 'lib/tabulo/table.rb', line 121

def to_s
  if column_registry.any?
    join_lines(map(&:to_s))
  else
    ""
  end
end