class Terminal::Table

Constants

VERSION

Attributes

headings[R]
title[R]

Public Class Methods

new(options = {}) click to toggle source

Generates a ASCII/Unicode table with the given options.

# File lib/terminal-table/table.rb, line 12
def initialize options = {}, &block
  @elaborated = false
  @headings = []
  @rows = []
  @column_widths = []
  self.style = options.fetch :style, {}
  self.headings = options.fetch :headings, []
  self.rows = options.fetch :rows, []
  self.title = options.fetch :title, nil
  yield_or_eval(&block) if block

  style.on_change(:width) { require_column_widths_recalc }
end

Public Instance Methods

<<(array)
Alias for: add_row
==(other) click to toggle source

Check if other is equal to self. other is considered equal if it contains the same headings and rows.

# File lib/terminal-table/table.rb, line 204
def == other
  if other.respond_to? :render and other.respond_to? :rows
    self.headings == other.headings and self.rows == other.rows
  end
end
add_row(array) click to toggle source

Add a row.

# File lib/terminal-table/table.rb, line 39
def add_row array
  row = array == :separator ? Separator.new(self) : Row.new(self, array)
  @rows << row
  require_column_widths_recalc unless row.is_a?(Separator)
end
Also aliased as: <<
add_separator(border_type: :div) click to toggle source

Add a separator.

# File lib/terminal-table/table.rb, line 49
def add_separator(border_type: :div)
  @rows << Separator.new(self, border_type: border_type)
end
align_column(n, alignment) click to toggle source

Align column n to the given alignment of :center, :left, or :right.

# File lib/terminal-table/table.rb, line 29
def align_column n, alignment
  # nil forces the column method to return the cell itself
  column(n, nil).each do |cell|
    cell.alignment = alignment unless cell.alignment?
  end
end
cell_padding() click to toggle source
# File lib/terminal-table/table.rb, line 57
def cell_padding
  style.padding_left + style.padding_right
end
cell_spacing() click to toggle source
# File lib/terminal-table/table.rb, line 53
def cell_spacing
  cell_padding + style.border_y_width
end
column(n, method = :value, array = rows) click to toggle source

Return column n.

# File lib/terminal-table/table.rb, line 64
def column n, method = :value, array = rows
  array.map { |row|
    # for each cells in a row, find the column with index
    # just greater than the required one, and go back one.
    index = col = 0
    row.cells.each do |cell|
      break if index > n
      index += cell.colspan
      col += 1
    end
    cell = row[col - 1]
    cell && method ? cell.__send__(method) : cell
  }.compact
end
column_width(n) click to toggle source

Return length of column n.

# File lib/terminal-table/table.rb, line 96
def column_width n
  column_widths[n] || 0
end
Also aliased as: length_of_column
column_with_headings(n, method = :value) click to toggle source

Return n column including headings.

# File lib/terminal-table/table.rb, line 82
def column_with_headings n, method = :value
  column n, method, headings_with_rows
end
columns() click to toggle source

Return columns.

# File lib/terminal-table/table.rb, line 89
def columns
  (0...number_of_columns).map { |n| column n }
end
elaborate_rows() click to toggle source

Elaborate rows to form an Array of Rows and Separators with adjacency properties added.

This is separated from the String rendering so that certain features may be tweaked before the String is built.

# File lib/terminal-table/table.rb, line 126
def elaborate_rows

  buffer = style.border_top ? [Separator.new(self, border_type: :top, implicit: true)] : []
  unless @title.nil?
    buffer << Row.new(self, [title_cell_options])
    buffer << Separator.new(self, implicit: true)
  end
  @headings.each do |row|
    unless row.cells.empty?
      buffer << row
      buffer << Separator.new(self, border_type: :double, implicit: true)
    end
  end
  if style.all_separators
    @rows.each_with_index do |row, idx|
      # last separator is bottom, others are :div
      border_type = (idx == @rows.size - 1) ? :bot : :div
      buffer << row
      buffer << Separator.new(self, border_type: border_type, implicit: true)
    end
  else
    buffer += @rows
    buffer << Separator.new(self, border_type: :bot, implicit: true) if style.border_bottom
  end

  # After all implicit Separators are inserted we need to save off the
  # adjacent rows so that we can decide what type of intersections to use
  # based on column spans in the adjacent row(s).
  buffer.each_with_index do |r, idx|
    if r.is_a?(Separator)
      prev_row = idx > 0 ? buffer[idx - 1] : nil
      next_row = buffer.fetch(idx + 1, nil)
      r.save_adjacent_rows(prev_row, next_row)
    end
  end
  
  @elaborated = true
  @rows = buffer
end
headings=(arrays) click to toggle source

Set the headings

# File lib/terminal-table/table.rb, line 111
def headings= arrays
  arrays = [arrays] unless arrays.first.is_a?(Array)
  @headings = arrays.map do |array|
    row = Row.new(self, array)
    require_column_widths_recalc
    row
  end
end
length_of_column(n)
Alias for: column_width
number_of_columns() click to toggle source

Return total number of columns available.

# File lib/terminal-table/table.rb, line 104
def number_of_columns
  headings_with_rows.map { |r| r.number_of_columns }.max || 0
end
render() click to toggle source

Render the table.

# File lib/terminal-table/table.rb, line 169
def render
  elaborate_rows unless @elaborated
  @rows.map { |r| style.margin_left + r.render.rstrip }.join("\n")
end
Also aliased as: to_s
rows() click to toggle source

Return rows without separator rows.

# File lib/terminal-table/table.rb, line 178
def rows
  @rows.reject { |row| row.is_a? Separator }
end
rows=(array) click to toggle source
# File lib/terminal-table/table.rb, line 182
def rows= array
  @rows = []
  array.each { |arr| self << arr }
end
style() click to toggle source
# File lib/terminal-table/table.rb, line 191
def style
  @style ||= Style.new
end
style=(options) click to toggle source
# File lib/terminal-table/table.rb, line 187
def style=(options)
  style.apply options
end
title=(title) click to toggle source
# File lib/terminal-table/table.rb, line 195
def title=(title)
  @title = title
  require_column_widths_recalc
end
to_s()
Alias for: render

Private Instance Methods

column_widths() click to toggle source
# File lib/terminal-table/table.rb, line 367
def column_widths
  recalc_column_widths if @require_column_widths_recalc
  @column_widths
end
columns_width() click to toggle source
# File lib/terminal-table/table.rb, line 212
def columns_width
  column_widths.inject(0) { |s, i| s + i + cell_spacing } + style.border_y_width
end
headings_with_rows() click to toggle source

Return headings combined with rows.

# File lib/terminal-table/table.rb, line 346
def headings_with_rows
  @headings + rows
end
recalc_column_widths() click to toggle source
# File lib/terminal-table/table.rb, line 216
def recalc_column_widths
  @require_column_widths_recalc = false
  n_cols = number_of_columns
  space_width = cell_spacing
  return if n_cols == 0

  # prepare rows
  all_rows = headings_with_rows
  all_rows << Row.new(self, [title_cell_options]) unless @title.nil?

  # DP states, dp[colspan][index][split_offset] => column_width.
  dp = []

  # prepare initial value for DP.
  all_rows.each do |row|
    index = 0
    row.cells.each do |cell|
      cell_value = cell.value_for_column_width_recalc
      cell_width = Unicode::DisplayWidth.of(cell_value.to_s)
      colspan = cell.colspan

      # find column width from each single cell.
      dp[colspan] ||= []
      dp[colspan][index] ||= [0]        # add a fake cell with length 0.
      dp[colspan][index][colspan] ||= 0 # initialize column length to 0.

      # the last index `colspan` means width of the single column (split
      # at end of each column), not a width made up of multiple columns.
      single_column_length = [cell_width, dp[colspan][index][colspan]].max
      dp[colspan][index][colspan] = single_column_length

      index += colspan
    end
  end

  # run DP.
  (1..n_cols).each do |colspan|
    dp[colspan] ||= []
    (0..n_cols-colspan).each do |index|
      dp[colspan][index] ||= [1]
      (1...colspan).each do |offset|
        # processed level became reverse map from width => [offset, ...].
        left_colspan = offset
        left_index = index
        left_width = dp[left_colspan][left_index].keys.first

        right_colspan = colspan - left_colspan
        right_index = index + offset
        right_width = dp[right_colspan][right_index].keys.first

        dp[colspan][index][offset] = left_width + right_width + space_width
      end

      # reverse map it for resolution (max width and short offset first).
      rmap = {}
      dp[colspan][index].each_with_index do |width, offset|
        rmap[width] ||= []
        rmap[width] << offset
      end

      # sort reversely and store it back.
      dp[colspan][index] = Hash[rmap.sort.reverse]
    end
  end

  resolve = lambda do |colspan, full_width, index = 0|
    # stop if reaches the bottom level.
    return @column_widths[index] = full_width if colspan == 1

    # choose best split offset for partition, or second best result
    # if first one is not dividable.
    candidate_offsets = dp[colspan][index].collect(&:last).flatten
    offset = candidate_offsets[0]
    offset = candidate_offsets[1] if offset == colspan

    # prepare for next round.
    left_colspan = offset
    left_index = index
    left_width = dp[left_colspan][left_index].keys.first

    right_colspan = colspan - left_colspan
    right_index = index + offset
    right_width = dp[right_colspan][right_index].keys.first

    # calculate reference column width, give remaining spaces to left.
    total_non_space_width = full_width - (colspan - 1) * space_width
    ref_column_width = total_non_space_width / colspan
    remainder = total_non_space_width % colspan
    rem_left_width = [remainder, left_colspan].min
    rem_right_width = remainder - rem_left_width
    ref_left_width = ref_column_width * left_colspan +
                     (left_colspan - 1) * space_width + rem_left_width
    ref_right_width = ref_column_width * right_colspan +
                      (right_colspan - 1) * space_width + rem_right_width

    # at most one width can be greater than the reference width.
    if left_width <= ref_left_width and right_width <= ref_right_width
      # use refernce width (evenly partition).
      left_width = ref_left_width
      right_width = ref_right_width
    else
      # the wider one takes its value, shorter one takes the rest.
      if left_width > ref_left_width
        right_width = full_width - left_width - space_width
      else
        left_width = full_width - right_width - space_width
      end
    end

    # run next round.
    resolve.call(left_colspan, left_width, left_index)
    resolve.call(right_colspan, right_width, right_index)
  end

  full_width = dp[n_cols][0].keys.first
  unless style.width.nil?
    new_width = style.width - space_width - style.border_y_width
    if new_width < full_width
      raise "Table width exceeds wanted width " +
            "of #{style.width} characters."
    end
    full_width = new_width
  end

  resolve.call(n_cols, full_width)
end
require_column_widths_recalc() click to toggle source
# File lib/terminal-table/table.rb, line 363
def require_column_widths_recalc
  @require_column_widths_recalc = true
end
title_cell_options() click to toggle source
# File lib/terminal-table/table.rb, line 359
def title_cell_options
  {:value => @title, :alignment => :center, :colspan => number_of_columns}
end
yield_or_eval() { |self| ... } click to toggle source
# File lib/terminal-table/table.rb, line 350
def yield_or_eval &block
  return unless block
  if block.arity > 0
    yield self
  else
    self.instance_eval(&block)
  end
end