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