# Copyright (C) 2007-2011, 2013 Rocky Bernstein # # # Part of Columnize to format in either direction module Columnize class Columnizer ARRANGE_ARRAY_OPTS = {:array_prefix => '[', :line_prefix => ' ', :line_suffix => ',', :array_suffix => ']', :colsep => ', ', :arrange_vertical => false} OLD_AND_NEW_KEYS = {:lineprefix => :line_prefix, :linesuffix => :line_suffix} # TODO: change colfmt to cell_format; change colsep to something else ATTRS = [:arrange_vertical, :array_prefix, :array_suffix, :line_prefix, :line_suffix, :colfmt, :colsep, :displaywidth, :ljust] attr_reader :list, :opts def initialize(list=[], opts={}) self.list = list self.opts = DEFAULT_OPTS.merge(opts) end def list=(list) @list = list if @list.is_a? Array @short_circuit = @list.empty? ? "\n" : nil else @short_circuit = '' @list = [] end end # TODO: freeze @opts def opts=(opts) @opts = opts OLD_AND_NEW_KEYS.each {|old, new| @opts[new] = @opts.delete(old) if @opts.keys.include?(old) and !@opts.keys.include?(new) } @opts.merge!(ARRANGE_ARRAY_OPTS) if @opts[:arrange_array] set_attrs_from_opts end def update_opts(opts) self.opts = @opts.merge(opts) end def columnize return @short_circuit if @short_circuit rows, colwidths = min_rows_and_colwidths ncols = colwidths.length justify = lambda {|t, c| @ljust ? t.ljust(colwidths[c]) : t.rjust(colwidths[c]) } textify = lambda do |row| row.map!.with_index(&justify) unless ncols == 1 && @ljust "#{@line_prefix}#{row.join(@colsep)}#{@line_suffix}" end text = rows.map(&textify) text.first.sub!(/^#{@line_prefix}/, @array_prefix) unless @array_prefix.empty? text.last.sub!(/#{@line_suffix}$/, @array_suffix) unless @array_suffix.empty? text.join("\n") # + "\n" # if we want extra separation end # TODO: make this a method, rather than a function (?) # compute the smallest number of rows and the max widths for each column def min_rows_and_colwidths list = @list.map(&@stringify) cell_widths = list.map(&@term_adjuster).map(&:size) # Set default arrangement: one atom per row cell_width_max = cell_widths.max result = [arrange_by_row(list, list.size, 1), [cell_width_max]] # If any atom > @displaywidth, stop and use one atom per row. return result if cell_width_max > @displaywidth # For horizontal arrangement, we want to *maximize* the number # of columns. Thus the candidate number of rows (+sizes+) starts # at the minumum number of rows, 1, and increases. # For vertical arrangement, we want to *minimize* the number of # rows. So here the candidate number of columns (+sizes+) starts # at the maximum number of columns, list.length, and # decreases. Also the roles of columns and rows are reversed # from horizontal arrangement. # Loop from most compact arrangement to least compact, stopping # at the first successful packing. The below code is tricky, # but very cool. # # FIXME: In the below code could be DRY'd. (The duplication got # introduced when I revised the code - rocky) if @arrange_vertical (1..list.length).each do |size| other_size = (list.size + size - 1) / size colwidths = arrange_by_row(cell_widths, other_size, size).map(&:max) totwidth = colwidths.inject(&:+) + ((colwidths.length-1) * @colsep.length) return [arrange_by_column(list, other_size, size), colwidths] if totwidth <= @displaywidth end else list.length.downto(1).each do |size| other_size = (list.size + size - 1) / size colwidths = arrange_by_column(cell_widths, other_size, size).map(&:max) totwidth = colwidths.inject(&:+) + ((colwidths.length-1) * @colsep.length) return [arrange_by_row(list, other_size, size), colwidths] if totwidth <= @displaywidth end end result end # Given +list+, +ncols+, +nrows+, arrange the one-dimensional # array into a 2-dimensional lists of lists organized by rows. # # In either horizontal or vertical arrangement, we will need to # access this for the list data or for the width # information. # # Here is an example: # arrange_by_row((1..5).to_a, 3, 2) => # [[1,2], [3,4], [5]], def arrange_by_row(list, nrows, ncols) (0...nrows).map {|r| list[r*ncols, ncols] }.compact end # Given +list+, +ncols+, +nrows+, arrange the one-dimensional # array into a 2-dimensional lists of lists organized by columns. # # In either horizontal or vertical arrangement, we will need to # access this for the list data or for the width # information. # # Here is an example: # arrange_by_column((1..5).to_a, 2, 3) => # [[1,3,5], [2,4]] def arrange_by_column(list, nrows, ncols) (0...ncols).map do |i| (0..nrows-1).inject([]) do |row, j| k = i + (j * ncols) k < list.length ? row << list[k] : row end end end def set_attrs_from_opts ATTRS.each {|attr| self.instance_variable_set "@#{attr}", @opts[attr] } @ljust = !@list.all? {|datum| datum.kind_of?(Numeric)} if @ljust == :auto @displaywidth -= @line_prefix.length @displaywidth = @line_prefix.length + 4 if @displaywidth < 4 @stringify = @colfmt ? lambda {|li| @colfmt % li } : lambda {|li| li.to_s } @term_adjuster = @opts[:term_adjust] ? lambda {|c| c.gsub(/\e\[.*?m/, '') } : lambda {|c| c } end end end # Demo if __FILE__ == $0 Columnize::DEFAULT_OPTS = {:line_prefix => '', :displaywidth => 80} puts Columnize::Columnizer.new.arrange_by_row((1..5).to_a, 2, 3).inspect puts Columnize::Columnizer.new.arrange_by_column((1..5).to_a, 2, 3).inspect end