lib/term_utils/tab.rb in term_utils-0.3.0 vs lib/term_utils/tab.rb in term_utils-0.3.1
- old
+ new
@@ -14,36 +14,156 @@
# 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}
+ 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}
+ 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)
+ target[:offset] = source[:offset]
+ end
+ if (source.has_key? :column_separator_width) && (source[:column_separator_width].is_a? Integer) && (source[:column_separator_width] > 0)
+ 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)
+ target[:width] = source[:width]
+ end
+ if (source.has_key? :align) && %i[left right].index(source[:align])
+ target[:align] = source[:align]
+ end
+ if (source.has_key? :fixed) && (!!source[:fixed] == source[:fixed])
+ target[:fixed] = source[:fixed]
+ end
+ if (source.has_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))
+ 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)]
+ else
+ res = "%s%s" % [src[0..(width - (ellipsis.length + 1))], ellipsis]
+ end
+ else
+ res = "%-*s" % [width, src]
+ end
+ elsif align == :right
+ # Align right
+ if fixed && (src.length > width)
+ if ellipsis.length >= width
+ res = ellipsis[0..(width - 1)]
+ else
+ res = "%s%s" % [ellipsis, src[(src.length - width + ellipsis.length)..(src.length - 1)]]
+ end
+ else
+ res = "%*s" % [width, src]
+ end
+ end
+ res
+ end
# Represents a table.
class Table
# @return [Symbol]
attr_accessor :id
+ # @return [Integer]
+ attr_accessor :offset
+ # @return [Integer]
+ 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
@columns = []
end
+ # Returns the properties of this one.
+ # @return [Hash]
+ def props
+ {: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
+ # @option opts [Boolean] :fixed
+ # @option opts [String] :ellipsis
+ # @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
col.validate
else
opts[:id] = id
opts[:index] = @columns.length
- col = Column.new(opts)
+ col = Column.new(@column_defaults.merge(opts))
block.call(col) if block
col.validate
@columns << col
end
col
@@ -53,83 +173,85 @@
# @return [Tab::Column, nil]
def find_column(id)
@columns.find { |c| c.id == id }
end
# Creates a new table printer.
- # @param io [IO]
+ # @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, opts)
+ ptr = Printer.new(self, io, props.merge(opts))
block.call(ptr) if block
ptr
end
# Prints a header row.
- # @param io [IO]
+ # @param io [#puts]
# @param values [Array<Object>, Hash<Symbol, Object>]
# @param opts [Hash]
# @option opts [Integer] :offset
# @option opts [Integer] :column_separator_width
# @return [nil]
+ # @raise [TermUtils::Tab::TableError]
def print_header(io, values = nil, opts = {})
vals = values
if values.nil?
- vals = @columns.map { |col| col.id.to_s }
+ vals = @columns.map { |col| col.header.title }
elsif values.is_a? Hash
vals = []
@columns.each do |col|
vals << values[col.id]
end
end
- raise "wrong values (not array)" unless vals.is_a? Array
- offset = opts.fetch(:offset, 0)
- column_separator_width = opts.fetch(:column_separator_width, 2)
+ 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
@columns.each do |col|
sb << " " * column_separator_width if col.index > 0
sb << col.render_header(vals[col.index])
end
io.puts sb.string
end
# Prints a data row.
- # @param io [IO]
+ # @param io [#puts]
# @param values [Array<Object>, Hash<Symbol, Object>]
# @param opts [Hash]
# @option opts [Integer] :offset
# @option opts [Integer] :column_separator_width
# @return [nil]
+ # @raise [TermUtils::Tab::TableError]
def print_data(io, values, opts = {})
vals = values
if values.is_a? Hash
vals = []
@columns.each do |col|
vals << values[col.id]
end
end
- raise "wrong values (not array)" unless vals.is_a? Array
- offset = opts.fetch(:offset, 0)
- column_separator_width = opts.fetch(:column_separator_width, 2)
+ 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
@columns.each do |col|
sb << " " * column_separator_width if col.index > 0
sb << col.render_data(vals[col.index])
end
io.puts sb.string
end
# Prints a separator row.
- # @param io [IO]
+ # @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, 0)
- column_separator_width = opts.fetch(:column_separator_width, 2)
+ offset = opts.fetch(:offset)
+ column_separator_width = opts.fetch(:column_separator_width)
sb = StringIO.new
sb << " " * offset if offset > 0
@columns.each do |col|
sb << " " * column_separator_width if col.index > 0
sb << "-" * col.width
@@ -160,10 +282,12 @@
attr_accessor :fixed
# @return [String]
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
@@ -176,68 +300,71 @@
@width = opts.fetch(:width, 8)
@align = opts.fetch(:align, :left)
@fixed = opts.fetch(:fixed, false)
@ellipsis = opts.fetch(:ellipsis, "?")
@format = opts.fetch(:format, nil)
+ @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 "missing column id (nil)" if @id.nil?
- raise "missing column index (nil)" if @index.nil?
- raise "wrong column index (not integer)" unless @index.is_a? Integer
- raise "wrong column index (not >= 0)" if @index < 0
- raise "missing column width (nil)" if @width.nil?
- raise "wrong column width (not integer)" unless @width.is_a? Integer
- raise "wrong column width (not > 0)" if @width <= 0
- raise "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 < 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)
+ @header.validate
end
- # Aligns and cuts a given string.
- # @param str [String]
- # @return [String]
- def align_cut(str)
- if @align == :left
- # Align left
- if @fixed and (str.length > @width)
- str = "#{str[0..(@width - (@ellipsis.length + 1))]}#{@ellipsis}"
- else
- str = "%-*s" % [@width, str]
- end
- else
- # Align right
- if @fixed and (str.length > @width)
- str = "#{@ellipsis}#{str[(str.length - @width + @ellipsis.length)..(str.length - 1)]}"
- else
- str = "%*s" % [@width, str]
- end
- end
- str
- end
# Renders a given header.
- # @param v [Object]
+ # @param val [Object]
# return [String]
- def render_header(v)
- str = v
- str = str.to_s unless str.is_a? String
- align_cut str
+ def render_header(val)
+ 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 v [Object]
+ # @param val [Object]
# return [String]
- def render_data(v)
- str = v
- if v
+ def render_data(val)
+ src = val
+ if val
if @format.is_a? Proc
- str = @format.call(v)
+ src = @format.call(val)
elsif @format.is_a? String
- str = @format % v
+ src = @format % val
end
end
- str = str.to_s unless str.is_a? String
- align_cut str
+ 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)
+ end
+ end
# Represents a table printer.
class Printer
# @return [Tab::Table]
attr_accessor :table
# @return [IO]
@@ -260,47 +387,84 @@
# @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 = {})
- @table.print_header(@io, values, @options.merge(opts))
+ 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 = {})
- @table.print_data(@io, values, @options.merge(opts))
+ 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 = {})
- @table.print_separator(@io, @options.merge(opts))
+ def separator(opts = nil)
+ @table.print_separator(@io, opts ? @options.merge(opts) : @options)
end
end
# Represents a holder of tables.
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 = {})
+ @table_defaults = TermUtils::Tab.init_table_props
+ @column_defaults = TermUtils::Tab.init_column_props
@tables = {}
end
- # Defines a table.
+ # 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
+ 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
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
@tables[id] = new_tab
end
@tables[id]
@@ -321,23 +485,46 @@
def printer(id, io, opts = {}, &block)
find_table(id).printer(io, opts, &block)
end
end
@@default_holder = Holder.new
- # Defines a table.
+ # 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)
+ @@default_holder.define_table(id, opts, &block)
end
- # Finds a table.
+ # 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 table printer.
+ # Creates a new Printer for a registered Table.
# @param id [Symbol]
# @param io [IO]
# @param opts [Hash]
# @option opts [Integer] :offset
# @option opts [Integer] :column_separator_width