# coding: utf-8 require 'equatable' require 'forwardable' require 'necromancer' require_relative 'table/columns' require_relative 'table/header' require_relative 'table/orientation' require_relative 'table/row' require_relative 'table/renderer' require_relative 'table/transformation' require_relative 'table/validatable' module TTY # A core class intended for storing data in a structured, tabular form. # Once the data is stored in a TTY::Table various operations can be performed # before the information is dumped into a stdout. class Table include Comparable, Enumerable, Validatable, Equatable extend Forwardable # The table header # # @return [Enumerable] # # @api public attr_reader :header # The table rows # # @return [Enumerable] # # @api private attr_reader :rows # private :rows # The table orientation out of :horizontal and :vertical # # @reutrn [TTY::Table::Orientation] # # @api public attr_reader :orientation # The table original row count # # @reutrn [Integer] # # @api public attr_reader :original_rows # The table original column count # # @reutrn [Integer] # # @api public attr_reader :original_columns # Subset of safe methods that both Array and Hash implement def_delegators(:data, :assoc, :flatten, :include?, :index, :length, :select, :to_a, :values_at, :pretty_print, :rassoc) # Create a new Table where each argument is a row # # @example # table = TTY::Table[['a1', 'a2'], ['b1', 'b2']] # # @api public def self.[](*rows) new(rows: rows) end # Instantiate a new Table # # @example of no header # table = Table.new [['a1', 'a2'], ['b1', 'b2']] # # @example of direct parameters # rows = [['a1', 'a2'], ['b1', 'b2']] # table = Table.new ['Header 1', 'Header 2'], rows # # @example of parameters passed as options # rows = [['a1', 'a2'], ['b1', 'b2']] # table = Table.new header: ['Header 1', 'Header 2'], rows: rows # # @example of parameters passed as hash # Table.new [{'Header1' => ['a1','a2'], 'Header2' => ['b1', 'b2'] }]} # # @param [Array[Symbol], Hash] *args # # @api public def self.new(*args, &block) options = args.last.respond_to?(:to_hash) ? args.pop : {} if args.size.nonzero? super(Transformation.extract_tuples(args).merge(options), &block) else super(options, &block) end end # Initialize a Table # # @param [Hash] options # the options to create the table with # @option options [String] :header # column names to be displayed # @option options [String] :rows # Array of Arrays expressing the rows # @option options [Symbol] :orientation # used to transform table orientation # # @return [TTY::Table] # # @api private def initialize(options = {}, &block) validate_options! options @converter = Necromancer.new @header = (value = options[:header]) ? Header.new(value) : nil @rows = coerce(options.fetch(:rows) { Row.new([]) }) @rotated = false self.orientation = options.fetch(:orientation) { :horizontal } assert_row_sizes @rows orientation.transform(self) yield_or_eval(&block) if block_given? end # Provides access to all table data # # @return [Array] # # @api public def data (header && !header.empty?) ? [header] + rows : rows end # Sets table orientation # # @param [String,Symbol] value # # @api public def orientation=(value) @orientation = Orientation.coerce(value) end # Marks this table as rotated # # @return [Boolean] # # @api public def rotated? @rotated end # Rotate the table between vertical and horizontal orientation # # @return [self] # # @api private def rotate orientation.transform(self) self end # Rotate the table vertically # # @api private def rotate_vertical @original_columns = columns_size @original_rows = rows_size @rows = orientation.slice(self) @header = [] if header @rotated = true end # Rotate the table horizontally # # @api private def rotate_horizontal return unless rotated? head, body = orientation.slice(self) if header && header.empty? @header = head[0] @rows = body.map { |row| to_row(row, @header) } else @rows = body.map { |row| to_row(row) } end end # Lookup element of the table given a row(i) and column(j) # # @param [Integer] row_index # @param [Integer] column_index # # @example # table = TTY::Table.new [['a1','a2'], ['b1','b2']] # table[0] # => ['a1','a2'] # table[0,0] # => 'a1' # table[-1] # => ['b1','b2'] # # @api public def [](row_index, column_index = false) return row(row_index) unless column_index if row_index >= 0 && column_index >= 0 rows.fetch(row_index) { return nil }[column_index] else fail TTY::Table::TupleMissing.new(row_index, column_index) end end alias_method :at, :[] alias_method :element, :[] alias_method :component, :[] # Set table value at row(i) and column(j) # # @api private def []=(row_index, column_index, val) @rows[row_index][column_index] = val end private :[]= # Return a row number at the index of the table as an Array. # When a block is given, the elements of that Array are iterated over. # # @example # rows = [['a1', 'a2'], ['b1', 'b2']] # table = TTY::Table.new rows: rows # table.row(1) { |row| ... } # # @param [Integer] index # # @yield [] # optional block to execute in the iteration operation # # @return [self] # # @api public def row(index, &block) if block_given? rows.fetch(index) { return self }.each(&block) self else rows.fetch(index) { return nil } end end # Return a column number at the index of the table as an Array. # If the table has a header then column can be searched by header name. # When a block is given, the elements of that Array are iterated over. # # @example # header = [:h1, :h2] # rows = [ ['a1', 'a2'], ['b1', 'b2'] ] # table = TTY::Table.new :rows => rows, :header => header # table.column(1) # table.column(1) { |element| ... } # table.column(:h1) # table.column(:h1) { |element| ... } # # @param [Integer, String, Symbol] index # # @yield [] # optional block to execute in the iteration operation # # @return [self] # # @api public def column(index) index_unknown = index.is_a?(Integer) && (index >= columns_size || index < 0) if block_given? return self if index_unknown rows.map { |row| yield row[index] } else return nil if index_unknown rows.map { |row| row[index] }.compact end end # Add row to table # # @param [Array] row # # @return [self] # # @api public def <<(row) rows_copy = rows.dup assert_row_sizes rows_copy << row rows << to_row(row) self end # Iterate over each tuple in the set # # @example # table = TTY::Table.new(header, tuples) # table.each { |row| ... } # # @yield [Array[Array]] # # @return [self] # # @api public def each return to_enum unless block_given? data.each { |row| yield row } self end # Same as #each but passes the index of the row with the row itself # # @example # table = TTY::Table.new(header, tuples) # table.each_with_index { |row, index| # puts "#{row} at #{index}" # } # # @return self # # @api public def each_with_index return to_enum unless block_given? start_index = -1 data.each do |row| yield row.to_a, start_index += 1 end self end # Return the number of columns # # @example # table.columns_size # => 5 # # @return [Integer] # # @api public def columns_size rows.size > 0 ? rows[0].size : 0 end # Return the number of rows # # @example # table.row_size # => 5 # # @return [Integer] # # @api public def rows_size rows.size end # Return the number of rows and columns # # @example # table.size # => [3,5] # # @return [Array] row x columns # # @api public def size [rows_size, columns_size] end # Check table width # # @return [Integer] width # # @api public def width Columns.new(self).total_width end # Return true if this is an empty table, i.e. if the number of # rows or the number of columns is 0 # # @return [Boolean] # # @api public def empty? columns_size == 0 || rows_size == 0 end # Return string representation of table using basic renderer. # # @return [String] # # @api public def to_s render(:basic) end # Return renderer for this table # # @param [Symbol] type # the renderer type # # @param [Hash] options # the renderer options # def renderer(type = :basic, options = {}) @renderer ||= Renderer.select(type).new(self, options) end # Render a given table. This method takes options which will be passed # to the renderer prior to rendering, which allows the caller to set any # table rendering variables. # # @param [Symbol] renderer_type # the renderer to be used # # @param [Hash] options # # @yield [renderer] # # @yieldparam [TTY::Table::Renderer] renderer # the renderer for the table # # @return [String] # # @api public def render(*args, &block) render_with(nil, *args, &block) end # Render a given table using custom border class. # # @param [TTY::Table::Border] # # @param [Symbol] renderer_type # # @yield [renderer] # # @yieldparam [TTY::Table::Renderer] renderer # the renderer for the table # # @return [String] # # @api public def render_with(border_class, renderer_type=(not_set=true), options={}, &block) unless not_set if renderer_type.respond_to?(:to_hash) options = renderer_type else options[:renderer] = renderer_type end end Renderer.render_with(border_class, self, options, &block) end # Coerce an Enumerable into a Table # This coercion mechanism is used by Table to handle Enumerable types # and force them into array type. # # @param [Enumerable] object # the object to coerce # # @return [Array] # # @api public def coerce(rows) rows = @converter.convert(rows).to(:array) rows.map { |row| to_row(row, header) } end private # Evaluate block # # @return [Table] # # @api private def yield_or_eval(&block) return unless block block.arity > 0 ? yield(self) : instance_eval(&block) end end # Table end # TTY