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.

Constant Summary collapse

DEFAULT_COLUMN_WIDTH =
12
DEFAULT_COLUMN_PADDING =
1
DEFAULT_HORIZONTAL_RULE_CHARACTER =
"-"
DEFAULT_VERTICAL_RULE_CHARACTER =
"|"
DEFAULT_INTERSECTION_CHARACTER =
"+"
DEFAULT_TRUNCATION_INDICATOR =
"~"

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(sources, *cols, columns: [], column_width: nil, column_padding: nil, header_frequency: :start, wrap_header_cells_to: nil, wrap_body_cells_to: nil, horizontal_rule_character: nil, vertical_rule_character: nil, intersection_character: nil, truncation_indicator: nil, align_header: :center, align_body: :auto, border_styler: nil) {|_self| ... } ⇒ Table

Returns a new Tabulo::Table.

Parameters:

  • sources (Enumerable)

    the underlying Enumerable from which the table will derive its data

  • cols (Array[Symbol])

    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.

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

    DEPRECATED Use cols instead.

  • 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. 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.

  • horizontal_rule_character (nil, String) (defaults to: nil)

    Determines the character used to draw horizontal lines where required in the table. If omitted or passed nil, defaults to DEFAULT_HORIZONTAL_RULE_CHARACTER. If passed something other than nil or a single-character String, raises InvalidHorizontalRuleCharacterError.

  • vertical_rule_character (nil, String) (defaults to: nil)

    Determines the character used to draw vertical lines where required in the table. If omitted or passed nil, defaults to DEFAULT_VERTICAL_RULE_CHARACTER. If passed something other than nil or a single-character String, raises InvalidVerticalRuleCharacterError.

  • intersection_character (nil, String) (defaults to: nil)

    Determines the character used to draw line intersections and corners where required in the table. If omitted or passed nil, defaults to DEFAULT_INTERSECTION_CHARACTER. If passed something other than nil or a single-character String, raises InvalidIntersectionCharacterError.

  • truncation_indicator (nil, String) (defaults to: nil)

    Determines the character used to indicate that a cell's content has been truncated. If omitted or passed nil, defaults to DEFAULT_TRUNCATION_INDICATOR. If passed something other than nil or a single-character String, raises InvalidTruncationIndicatorError.

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

    Determines the amount of blank space with which to pad either of each column. Defaults to 1.

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

    (:center) Determines the alignment of header text for columns in this Table. Can be overridden for individual columns using the align_header option passed to #add_column

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

    (:auto) Determines the alignment of body cell (i.e. non-header) content within columns in this Table. Can be overridden for individual columns using the align_body option passed to #add_column. If passed :auto, alignment is determined by cell content, with numbers aligned right, booleans center-aligned, and other values left-aligned.

  • border_styler (nil, #to_proc) (defaults to: nil)

    (nil) A lambda or other callable object taking a single parameter, representing a section of the table's borders (which for this purpose include any horizontal and vertical lines inside the table). If passed nil, then no additional styling will be applied to borders. If passed a callable, then that callable will be called for each border section, with the resulting string rendered in place of that border. The extra width of the string returned by the border_styler is not taken into consideration by the internal table rendering calculations Thus it can be used to apply ANSI escape codes to border characters, to colour the borders for example, without breaking the table formatting.

Yields:

  • (_self)

Yield Parameters:

  • _self (Tabulo::Table)

    the object that the method was called on

Raises:



101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
# File 'lib/tabulo/table.rb', line 101

def initialize(sources, *cols, columns: [], column_width: nil, column_padding: nil, header_frequency: :start,
  wrap_header_cells_to: nil, wrap_body_cells_to: nil, horizontal_rule_character: nil,
  vertical_rule_character: nil, intersection_character: nil, truncation_indicator: nil,
  align_header: :center, align_body: :auto, border_styler: nil)

  if columns.any?
    Deprecation.warn("`columns' option to Tabulo::Table#initialize", "the variable length parameter `cols'", 2)
  end

  @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_padding = (column_padding || DEFAULT_COLUMN_PADDING)
  @align_header = align_header
  @align_body = align_body
  @border_styler = border_styler

  @horizontal_rule_character = validate_character(horizontal_rule_character,
    DEFAULT_HORIZONTAL_RULE_CHARACTER, InvalidHorizontalRuleCharacterError, "horizontal rule character")
  @vertical_rule_character = validate_character(vertical_rule_character,
    DEFAULT_VERTICAL_RULE_CHARACTER, InvalidVerticalRuleCharacterError, "vertical rule character")
  @intersection_character = validate_character(intersection_character,
    DEFAULT_INTERSECTION_CHARACTER, InvalidIntersectionCharacterError, "intersection character")
  @truncation_indicator = validate_character(truncation_indicator,
    DEFAULT_TRUNCATION_INDICATOR, InvalidTruncationIndicatorError, "truncation indicator")

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

  yield self if block_given?
end

Instance Attribute Details

#sourcesEnumerable

Returns the underlying enumerable from which the table derives its data.

Returns:

  • (Enumerable)

    the underlying enumerable from which the table derives its data



37
38
39
# File 'lib/tabulo/table.rb', line 37

def sources
  @sources
end

Instance Method Details

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

Adds a column to the Table.

Parameters:

  • label (Symbol, String, Integer)

    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. If a String is passed as the label, then it will be converted to a Symbol for the purpose of serving as this label.

  • 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, nil) (defaults to: nil)

    (nil) Specifies how the header text should be aligned. If nil is passed, then the alignment is determined by the Table-level setting passed to the align_header (which itself defaults to :center). Otherwise, this option determines the alignment of the header content for this column.

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

    (nil) Specifies how the cell body contents should be aligned. If nil is passed, then the alignment is determined by the Table-level setting passed to the align_body option on Table initialization (which itself defaults to :auto). Otherwise this option determines the alignment of this column. If :auto is passed, 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.

  • styler (nil, #to_proc) (defaults to: nil)

    (nil) A lambda or other callable object that will be passed two arguments: the calculated value of the cell (prior to the formatter being applied); and a string representing a single formatted line within the cell. For example, if the cell content is wrapped over three lines, then for that cell, the styler will be called three times, once for each line of content within the cell. If passed nil, then no additional styling will be applied to the cell content (other than what was already applied by the formatter). If passed a callable, then that callable will be called for each line of content within the cell, and the resulting string rendered in place of that line. The styler option differs from the formatter option in that the extra width of the string returned by styler is not taken into consideration by the internal table and cell width calculations involved in rendering the table. Thus it can be used to apply ANSI escape codes to cell content, to colour the cell content for example, without breaking the table formatting. Note that if the content of a cell is truncated, then the whatever styling is applied by the styler to the cell content will also be applied to the truncation indicator character.

  • header_styler (nil, #to_proc) (defaults to: nil)

    (nil) A lambda or other callable object taking a single parameter, representing a single line of within the header content for this column. For example, if the header cell content is wrapped over three lines, then the header_styler will be called once for each line. If passed nil, then no additional styling will be applied to the header cell content. If passed a callable, then that callable will be called for each line of content within the header cell, and the resulting string rendered in place of that line. The extra width of the string returned by the header_styler is not taken into consideration by the internal table and cell width calculations involved in rendering the table. Thus it can be used to apply ANSI escape codes to header cell content, to colour the cell content for example, without breaking the table formatting. Note that if the header content is truncated, then any header_styler will be applied to the truncation indicator character as well as to the truncated 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.)



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
# File 'lib/tabulo/table.rb', line 202

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

  column_label =
    case label
    when Integer, Symbol
      label
    when String
      label.to_sym
    end

  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_header,
      align_body: align_body || @align_body,
      width: (width || @default_column_width),
      formatter: formatter,
      extractor: (extractor || label.to_proc),
      styler: styler,
      header_styler: header_styler,
      truncation_indicator: @truncation_indicator,
      padding_character: PADDING_CHARACTER,
    )
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


251
252
253
254
255
256
257
258
259
260
261
262
263
264
# File 'lib/tabulo/table.rb', line 251

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.



267
268
269
270
# File 'lib/tabulo/table.rb', line 267

def formatted_header
  cells = column_registry.map { |_, column| column.header_cell }
  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.



280
281
282
283
284
285
286
# File 'lib/tabulo/table.rb', line 280

def horizontal_rule
  inner = column_registry.map do |_, column|
    @horizontal_rule_character * (column.width + @column_padding * 2)
  end

  styled_border(surround_join(inner, @intersection_character))
end

#pack(max_table_width: :auto) ⇒ 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: :auto)

    (:auto) With no args, or if passed :auto, stops the total table width (including padding and borders) from expanding beyond the bounds of the terminal screen. If passed nil, the table width will not be capped. 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



316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
# File 'lib/tabulo/table.rb', line 316

def pack(max_table_width: :auto)
  return self if column_registry.none?
  columns = column_registry.values

  columns.each { |column| column.width = wrapped_width(column.header) }

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

  if max_table_width
    max_table_width = TTY::Screen.width if max_table_width == :auto
    shrink_to(max_table_width)
  end

  self
end

#shrinkwrap!(max_table_width: nil) ⇒ Table

Deprecated.

Use #pack instead.

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. If passed :auto, the table width will automatically be capped at the current terminal width. 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



444
445
446
447
# File 'lib/tabulo/table.rb', line 444

def shrinkwrap!(max_table_width: nil)
  Deprecation.warn("`Tabulo::Table#shrinkwrap!'", "`#pack'")
  pack(max_table_width: max_table_width)
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.



234
235
236
237
238
239
240
# File 'lib/tabulo/table.rb', line 234

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

#transpose(opts = {}) ⇒ Table

Creates a new Tabulo::Table from the current Table, transposed, that is rotated 90 degrees, relative to the current Table, so that the header names of the current Table form the content of left-most column of the new Table, and each column thereafter corresponds to one of the elements of the current Table's sources, with the header of that column being the String value of that element.

Examples:

puts Tabulo::Table.new(-1..1, :even?, :odd?, :abs).transpose
  # => +-------+--------------+--------------+--------------+
  #    |       |      -1      |       0      |       1      |
  #    +-------+--------------+--------------+--------------+
  #    | even? |     false    |     true     |     false    |
  #    |  odd? |     true     |     false    |     true     |
  #    |   abs |            1 |            0 |            1 |

Parameters:

  • opts (Hash) (defaults to: {})

    Options for configuring the new, transposed Tabulo::Table. The following options are the same as the keyword params for the #initialize method for Tabulo::Table: column_width, column_padding, header_frequency, wrap_header_cells_to, wrap_body_cells_to, horizontal_rule_character, vertical_rule_character, intersection_character, truncation_indicator, align_header, align_body. These are applied in the same way as documented for #initialize, when creating the new, transposed Table. Any options not specified explicitly in the call to #transpose will inherit their values from the original Tabulo::Table (with the exception of settings for the left-most column, containing the field names, which are determined as described below). In addition, the following options also apply to #transpose:

Options Hash (opts):

  • :field_names_width (nil, Integer)

    Determines the width of the left-most column of the new Table, which contains the names of "fields" (corresponding to the original Table's column headings). If this is not provided, then by default this column will be made just wide enough to accommodate its contents.

  • :field_names_header (String) — default: ""

    By default the left-most column will have a blank header; but this can be overridden by passing a String to this option.

  • :field_names_header_alignment (:left, :center, :right) — default: :right

    Specifies how the header text of the left-most column (if it has header text) should be aligned.

  • :field_names_body_alignment (:left, :center, :right) — default: :right

    Specifies how the body text of the left-most column should be aligned.

  • :headers (#to_proc) — default: :to_s.to_proc

    A lambda or other callable object that will be passed in turn each of the elements of the current Table's sources Enumerable, to determine the text to be displayed in the header of each column of the new Table (other than the left-most column's header, which is determined as described above).

Returns:

Raises:



381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
# File 'lib/tabulo/table.rb', line 381

def transpose(opts = {})
  default_opts = [:column_width, :column_padding, :header_frequency, :wrap_header_cells_to,
    :wrap_body_cells_to, :horizontal_rule_character, :vertical_rule_character,
    :intersection_character, :truncation_indicator, :align_header, :align_body].map do |sym|
    [sym, instance_variable_get("@#{sym}")]
  end.to_h

  initializer_opts = default_opts.merge(Util.slice_hash(opts, *default_opts.keys))
  default_extra_opts = { field_names_width: nil, field_names_header: "",
    field_names_body_alignment: :right, field_names_header_alignment: :right, headers: :to_s.to_proc }
  extra_opts = default_extra_opts.merge(Util.slice_hash(opts, *default_extra_opts.keys))

  # The underlying enumerable for the new table, is the columns of the original table.
  fields = column_registry.values

  Table.new(fields, **initializer_opts) do |t|

    # Left hand column of new table, containing field names
    width_opt = extra_opts[:field_names_width]
    field_names_width = (width_opt.nil? ? fields.map { |f| f.header.length }.max : width_opt)

    t.add_column(:dummy, header: extra_opts[:field_names_header], width: field_names_width, align_header:
      extra_opts[:field_names_header_alignment], align_body: extra_opts[:field_names_body_alignment], &:header)

    # Add a column to the new table for each of the original table's sources
    sources.each_with_index do |source, i|
      t.add_column(i, header: extra_opts[:headers].call(source)) do |original_column|
        original_column.body_cell_value(source)
      end
    end
  end
end