1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159
|
# Copyright (C) 2007-2011, 2013 Rocky Bernstein
# <rockyb@rubyforge.net>
#
# 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? ? "<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
|