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