lib/term_utils/tab.rb in term_utils-0.3.2 vs lib/term_utils/tab.rb in term_utils-0.4.0
- old
+ new
@@ -1,6 +1,8 @@
-# Copyright (C) 2019 Thomas Baron
+# frozen-string-literal: true
+
+# Copyright (C) 2020 Thomas Baron
#
# This file is part of term_utils.
#
# term_utils is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
@@ -11,98 +13,103 @@
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with term_utils. If not, see <https://www.gnu.org/licenses/>.
+
module TermUtils
# The tab module provides a way to print formatted tables.
module Tab
# Represents a table error.
class TableError < StandardError
def initialize(msg)
super
end
end
+
# Creates initial column properties.
# @return [Hash] `:offset`, `:column_separator_width`.
def self.init_table_props
- {:offset => 0, :column_separator_width => 2}
+ { offset: 0, column_separator_width: 2 }
end
+
# Creates initial column properties.
# @return [Hash] `:width`, `:align`, `:fixed`, `:ellipsis`, `:format`.
def self.init_column_props
- {:width => 8, :align => :left, :fixed => false, :ellipsis => "?", :format => nil}
+ { width: 8, align: :left, fixed: false, ellipsis: '?', format: nil }
end
+
# Assigns table properties.
# @param target [Hash]
# @param source [Hash] `:offset`, `:column_separator_width`.
# @return [Hash]
def self.assign_table_props(target, source)
- if (source.has_key? :offset) && (source[:offset].is_a? Integer) && (source[:offset] >= 0)
+ if (source.key? :offset) && (source[:offset].is_a? Integer) && (source[:offset] >= 0)
target[:offset] = source[:offset]
end
- if (source.has_key? :column_separator_width) && (source[:column_separator_width].is_a? Integer) && (source[:column_separator_width] > 0)
+ if (source.key? :column_separator_width) && (source[:column_separator_width].is_a? Integer) && source[:column_separator_width].positive?
target[:column_separator_width] = source[:column_separator_width]
end
target
end
+
# Assigns column properties.
# @param target [Hash]
# @param source [Hash] `:width`, `:align`, `:fixed`, `:ellipsis`, `:format`.
# @return [Hash]
def self.assign_column_props(target, source)
- if (source.has_key? :width) && (source[:width].is_a? Integer) && (source[:width] > 0)
+ if (source.key? :width) && (source[:width].is_a? Integer) && source[:width].positive?
target[:width] = source[:width]
end
- if (source.has_key? :align) && %i[left right].index(source[:align])
+ if (source.key? :align) && %i[left right].index(source[:align])
target[:align] = source[:align]
end
- if (source.has_key? :fixed) && (!!source[:fixed] == source[:fixed])
+ if (source.key? :fixed) && (!!source[:fixed] == source[:fixed])
target[:fixed] = source[:fixed]
end
- if (source.has_key? :ellipsis) && (source[:ellipsis].is_a? String)
+ if (source.key? :ellipsis) && (source[:ellipsis].is_a? String)
target[:ellipsis] = source[:ellipsis]
end
- if (source.has_key? :format) && ((source[:ellipsis] == nil) || (source[:ellipsis].is_a? Proc) || (source[:ellipsis].is_a? String))
+ if (source.key? :format) && (source[:ellipsis].nil? || (source[:ellipsis].is_a? Proc) || (source[:ellipsis].is_a? String))
target[:format] = source[:format]
end
target
end
+
# Aligns and cuts a given string.
# @param src [String]
# @param align [Symbol] `:left`, `:right`.
# @param fixed [Boolean] Whether the column width is fixed.
# @param width [Integer] The column width.
# @param ellipsis [String] The ellipsis when not fixed.
# @return [String]
def self.align_cut(src, align, fixed, width, ellipsis)
- res = src
if align == :left
# Align left
if fixed && (src.length > width)
if ellipsis.length >= width
- res = ellipsis[0..(width - 1)]
+ ellipsis[0..(width - 1)]
else
- res = "%s%s" % [src[0..(width - (ellipsis.length + 1))], ellipsis]
+ format '%<value>s%<ellipsis>s', value: src[0..(width - (ellipsis.length + 1))], ellipsis: ellipsis
end
else
- res = "%-*s" % [width, src]
+ src.ljust(width)
end
elsif align == :right
# Align right
if fixed && (src.length > width)
if ellipsis.length >= width
- res = ellipsis[0..(width - 1)]
+ ellipsis[0..(width - 1)]
else
- res = "%s%s" % [ellipsis, src[(src.length - width + ellipsis.length)..(src.length - 1)]]
+ format '%<ellipsis>s%<value>s', ellipsis: ellipsis, value: src[(src.length - width + ellipsis.length)..(src.length - 1)]
end
else
- res = "%*s" % [width, src]
+ src.rjust(width)
end
end
- res
end
+
# Represents a table.
class Table
# @return [Symbol]
attr_accessor :id
# @return [Integer]
@@ -111,41 +118,41 @@
attr_accessor :column_separator_width
# @return [Hash] `:width`, `:align`, `:fixed`, `:ellipsis`, `:format`.
attr_accessor :column_defaults
# @return [Array<Tab::Column>]
attr_accessor :columns
+
# @param opts [Hash]
# @option opts [Symbol] :id
# @option opts [Integer] :offset
# @option opts [Hash] :column_defaults
def initialize(opts = {})
opts = TermUtils::Tab.init_table_props.merge(opts)
@id = opts.fetch(:id, nil)
@offset = opts.fetch(:offset)
@column_separator_width = opts.fetch(:column_separator_width)
- if opts.has_key? :column_defaults
- @column_defaults = opts[:column_defaults].dup
- else
- @column_defaults = TermUtils::Tab.default_column_props
- end
+ @column_defaults = opts.key?(:column_defaults) ? opts[:column_defaults].dup : TermUtils::Tab.default_column_props
@columns = []
end
+
# Returns the properties of this one.
# @return [Hash]
def props
- {:offset => @offset, :column_separator_width => @column_separator_width}
+ { offset: @offset, column_separator_width: @column_separator_width }
end
+
# Sets column default properties.
# @param opts [Hash]
# @option opts [Integer] :width
# @option opts [Symbol] :align
# @option opts [Boolean] :fixed
# @option opts [String] :ellipsis
# @option opts [Proc, String, nil] :format
def set_column_defaults(opts = {})
TermUtils::Tab.assign_column_props(@column_defaults, opts)
end
+
# Defines a column.
# @param id [Symbol]
# @param opts [Hash]
# @option opts [Integer] :width
# @option opts [Symbol] :align
@@ -154,39 +161,42 @@
# @option opts [Proc, String, nil] :format
# @return [Tab::Column]
def define_column(id, opts = {}, &block)
col = @columns.find { |c| c.id == id }
if col
- block.call(col) if block
+ block&.call(col)
col.validate
else
opts[:id] = id
opts[:index] = @columns.length
col = Column.new(@column_defaults.merge(opts))
- block.call(col) if block
+ block&.call(col)
col.validate
@columns << col
end
col
end
+
# Finds a column.
# @param id [Symbol]
# @return [Tab::Column, nil]
def find_column(id)
@columns.find { |c| c.id == id }
end
+
# Creates a new table printer.
# @param io [#puts]
# @param opts [Hash]
# @option opts [Integer] :offset
# @option opts [Integer] :column_separator_width
# @return [Tab::Printer]
def printer(io, opts = {}, &block)
ptr = Printer.new(self, io, props.merge(opts))
- block.call(ptr) if block
+ block&.call(ptr)
ptr
end
+
# Prints a header row.
# @param io [#puts]
# @param values [Array<Object>, Hash<Symbol, Object>]
# @param opts [Hash]
# @option opts [Integer] :offset
@@ -201,21 +211,23 @@
vals = []
@columns.each do |col|
vals << values[col.id]
end
end
- raise TermUtils::Tab::TableError, "wrong values (not array)" unless vals.is_a? Array
+ raise TermUtils::Tab::TableError, 'wrong values (not array)' unless vals.is_a? Array
+
offset = opts.fetch(:offset)
column_separator_width = opts.fetch(:column_separator_width)
sb = StringIO.new
- sb << " " * offset if offset > 0
+ sb << ' ' * offset if offset.positive?
@columns.each do |col|
- sb << " " * column_separator_width if col.index > 0
+ sb << ' ' * column_separator_width if col.index.positive?
sb << col.render_header(vals[col.index])
end
io.puts sb.string
end
+
# Prints a data row.
# @param io [#puts]
# @param values [Array<Object>, Hash<Symbol, Object>]
# @param opts [Hash]
# @option opts [Integer] :offset
@@ -228,48 +240,52 @@
vals = []
@columns.each do |col|
vals << values[col.id]
end
end
- raise TermUtils::Tab::TableError, "wrong values (not array)" unless vals.is_a? Array
+ raise TermUtils::Tab::TableError, 'wrong values (not array)' unless vals.is_a? Array
+
offset = opts.fetch(:offset)
column_separator_width = opts.fetch(:column_separator_width)
sb = StringIO.new
- sb << " " * offset if offset > 0
+ sb << ' ' * offset if offset.positive?
@columns.each do |col|
- sb << " " * column_separator_width if col.index > 0
+ sb << ' ' * column_separator_width if col.index.positive?
sb << col.render_data(vals[col.index])
end
io.puts sb.string
end
+
# Prints a separator row.
# @param io [#puts]
# @param opts [Hash]
# @option opts [Integer] :offset
# @option opts [Integer] :column_separator_width
# @return [nil]
def print_separator(io, opts = {})
offset = opts.fetch(:offset)
column_separator_width = opts.fetch(:column_separator_width)
sb = StringIO.new
- sb << " " * offset if offset > 0
+ sb << ' ' * offset if offset.positive?
@columns.each do |col|
- sb << " " * column_separator_width if col.index > 0
- sb << "-" * col.width
+ sb << ' ' * column_separator_width if col.index.positive?
+ sb << '-' * col.width
end
io.puts sb.string
end
+
# Returns column titles.
# @return [Hash<Symbol, String>]
def titles
h = {}
@columns.each do |col|
h[col.id] = col.id.to_s
end
h
end
end
+
# Represents a table column.
class Column
# @return [Symbol]
attr_accessor :id
# @return [Integer]
@@ -284,10 +300,11 @@
attr_accessor :ellipsis
# @return [Proc, String, nil]
attr_accessor :format
# @return [TermUtils::Tab::Header]
attr_accessor :header
+
# @param opts [Hash]
# @option opts [Symbol] :id
# @option opts [Integer] :index
# @option opts [Integer] :width
# @option opts [Symbol] :align
@@ -298,35 +315,39 @@
@id = opts.fetch(:id)
@index = opts.fetch(:index)
@width = opts.fetch(:width, 8)
@align = opts.fetch(:align, :left)
@fixed = opts.fetch(:fixed, false)
- @ellipsis = opts.fetch(:ellipsis, "?")
+ @ellipsis = opts.fetch(:ellipsis, '?')
@format = opts.fetch(:format, nil)
- @header = TermUtils::Tab::Header.new(:title => @id.to_s, :align => @align)
+ @header = TermUtils::Tab::Header.new(title: @id.to_s, align: @align)
end
+
# Validates the column represented by this one.
# @return [nil]
# @raise [TermUtils::Tab::TableError]
def validate
- raise TermUtils::Tab::TableError, "missing column id (nil)" if @id.nil?
- raise TermUtils::Tab::TableError, "missing column index (nil)" if @index.nil?
- raise TermUtils::Tab::TableError, "wrong column index (not integer)" unless @index.is_a? Integer
- raise TermUtils::Tab::TableError, "wrong column index (not >= 0)" if @index < 0
- raise TermUtils::Tab::TableError, "missing column width (nil)" if @width.nil?
- raise TermUtils::Tab::TableError, "wrong column width (not integer)" unless @width.is_a? Integer
- raise TermUtils::Tab::TableError, "wrong column width (not > 0)" if @width <= 0
- raise TermUtils::Tab::TableError, "wrong column align (not :left or :right)" unless %i[left right].index(@align)
+ raise TermUtils::Tab::TableError, 'missing column id (nil)' if @id.nil?
+ raise TermUtils::Tab::TableError, 'missing column index (nil)' if @index.nil?
+ raise TermUtils::Tab::TableError, 'wrong column index (not integer)' unless @index.is_a? Integer
+ raise TermUtils::Tab::TableError, 'wrong column index (not >= 0)' if @index.negative?
+ raise TermUtils::Tab::TableError, 'missing column width (nil)' if @width.nil?
+ raise TermUtils::Tab::TableError, 'wrong column width (not integer)' unless @width.is_a? Integer
+ raise TermUtils::Tab::TableError, 'wrong column width (not > 0)' if @width <= 0
+ raise TermUtils::Tab::TableError, 'wrong column align (not :left or :right)' unless %i[left right].index(@align)
+
@header.validate
end
+
# Renders a given header.
# @param val [Object]
# return [String]
def render_header(val)
- src = (val.is_a? String) ? val : val.to_s
+ src = val.is_a?(String) ? val : val.to_s
TermUtils::Tab.align_cut(src, @header.align, @fixed, @width, @ellipsis)
end
+
# Renders a given value.
# @param val [Object]
# return [String]
def render_data(val)
src = val
@@ -335,148 +356,164 @@
src = @format.call(val)
elsif @format.is_a? String
src = @format % val
end
end
- src = (src.is_a? String) ? src : src.to_s
+ src = src.is_a?(String) ? src : src.to_s
TermUtils::Tab.align_cut(src, @align, @fixed, @width, @ellipsis)
end
end
+
# Represents a column header.
class Header
# @return [String]
attr_accessor :title
# @return [Symbol] `:left`, `:right`.
attr_accessor :align
+
# Constructs a new Header.
# @param opts [Hash]
# @option opts [String] :title
# @option opts [Symbol] :align
def initialize(opts = {})
@title = opts.fetch(:title)
@align = opts.fetch(:align, :left)
end
+
# Validates the column represented by this one.
# @return [nil]
# @raise [TermUtils::Tab::TableError]
def validate
- raise TermUtils::Tab::TableError, "missing header title (nil)" if @title.nil?
- raise TermUtils::Tab::TableError, "wrong header align (not :left or :right)" unless %i[left right].index(@align)
+ raise TermUtils::Tab::TableError, 'missing header title (nil)' if @title.nil?
+ raise TermUtils::Tab::TableError, 'wrong header align (not :left or :right)' unless %i[left right].index(@align)
end
end
# Represents a table printer.
class Printer
# @return [Tab::Table]
attr_accessor :table
# @return [IO]
attr_accessor :io
# @return [Hash]
attr_accessor :options
+
# @param table [Tab::Table]
# @param io [IO]
# @param options [Hash]
def initialize(table, io, options)
@table = table
@io = io
@options = options
end
+
# Prints an empty line.
def line
- @io.puts ""
+ @io.puts
end
+
# Prints a header row.
# @param values [Array<Object>, Hash<Symbol, Object>]
# @param opts [Hash]
# @option opts [Integer] :offset
# @option opts [Integer] :column_separator_width
# @return [nil]
def header(values = nil, opts = nil)
@table.print_header(@io, values, opts ? @options.merge(opts) : @options)
end
+
# Prints a data row.
# @param values [Array<Object>, Hash<Symbol, Object>]
# @param opts [Hash]
# @option opts [Integer] :offset
# @option opts [Integer] :column_separator_width
# @return [nil]
def data(values, opts = nil)
@table.print_data(@io, values, opts ? @options.merge(opts) : @options)
end
+
# Prints a separator.
# @param opts [Hash]
# @option opts [Integer] :offset
# @option opts [Integer] :column_separator_width
# @return [nil]
def separator(opts = nil)
@table.print_separator(@io, opts ? @options.merge(opts) : @options)
end
end
- # Represents a holder of tables.
+ # Represents a Holder of Table(s).
class Holder
# @return [Hash] `:offset`, `:column_separator_width`.
attr_accessor :table_defaults
# @return [Hash] `:width`, `:align`, `:fixed`, `:ellipsis`, `:format`.
attr_accessor :column_defaults
# @return [Hash<Symbol, Tab::Table>]
attr_accessor :tables
- def initialize(opts = {})
+
+ # Creates a new Holder.
+ def initialize
@table_defaults = TermUtils::Tab.init_table_props
@column_defaults = TermUtils::Tab.init_column_props
@tables = {}
end
+
# Sets table default properties.
# @param opts [Hash]
# @option opts [Integer] :offset
# @option opts [Symbol] :column_separator_width
def set_table_defaults(opts = {})
TermUtils::Tab.assign_table_props(@table_defaults, opts)
end
+
# Sets column default properties.
# @param opts [Hash]
# @option opts [Integer] :width
# @option opts [Symbol] :align
# @option opts [Boolean] :fixed
# @option opts [String] :ellipsis
# @option opts [Proc, String, nil] :format
def set_column_defaults(opts = {})
TermUtils::Tab.assign_column_props(@column_defaults, opts)
end
+
# Creates a new table, using default properties, without registering it.
# @param opts [Hash]
# @return [Tab::Table]
def create_table(opts = {}, &block)
opts[:offset] = @table_defaults.fetch(:offset)
opts[:column_separator_width] = @table_defaults.fetch(:column_separator_width)
opts[:column_defaults] = @column_defaults.dup
new_tab = Table.new(opts)
- block.call(new_tab) if block
+ block&.call(new_tab)
new_tab
end
+
# Defines a table, using default properties.
# @param id [Symbol]
# @param opts [Hash]
# @return [Tab::Table]
def define_table(id, opts = {}, &block)
- if @tables.has_key? id
- block.call(@tables[id]) if block
+ if @tables.key? id
+ block&.call(@tables[id])
else
opts[:id] = id
opts[:offset] = @table_defaults.fetch(:offset)
opts[:column_separator_width] = @table_defaults.fetch(:column_separator_width)
opts[:column_defaults] = @column_defaults.dup
new_tab = Table.new(opts)
- block.call(new_tab) if block
+ block&.call(new_tab)
@tables[id] = new_tab
end
@tables[id]
end
+
# Finds a table.
# @param id [Symbol]
# @return [Tab::Table, nil]
def find_table(id)
@tables[id]
end
+
# Creates a new table printer.
# @param id [Symbol]
# @param io [IO]
# @param opts [Hash]
# @option opts [Integer] :offset
@@ -484,46 +521,53 @@
# @return [Tab::Printer]
def printer(id, io, opts = {}, &block)
find_table(id).printer(io, opts, &block)
end
end
- @@default_holder = Holder.new
+
+ @@default_holder = Holder.new # rubocop:disable Style/ClassVars
+
# Sets table default properties.
# @param opts [Hash]
# @option opts [Integer] :offset
# @option opts [Symbol] :column_separator_width
def self.set_table_defaults(opts = {})
@@default_holder.set_table_defaults(opts)
end
+
# Sets column default properties.
# @param opts [Hash]
# @option opts [Integer] :width
# @option opts [Symbol] :align
# @option opts [Boolean] :fixed
# @option opts [String] :ellipsis
# @option opts [Proc, String, nil] :format
def self.set_column_defaults(opts = {})
@@default_holder.set_column_defaults(opts)
end
+
# Creates a new Table, using default properties, without registering it.
# @param opts [Hash]
# @return [Tab::Table]
def self.create_table(opts = {}, &block)
@@default_holder.create_table(opts, &block)
end
+
# Defines a new Table, using default properties, and registers it.
# @param id [Symbol]
# @param opts [Hash]
# @return [Tab::Table]
def self.define_table(id, opts = {}, &block)
@@default_holder.define_table(id, opts, &block)
end
+
# Finds a registered table.
# @param id [Symbol]
# @return [Tab::Table, nil]
def self.find_table(id)
@@default_holder.find_table(id)
end
+
# Creates a new Printer for a registered Table.
# @param id [Symbol]
# @param io [IO]
# @param opts [Hash]
# @option opts [Integer] :offset