tabmani 0.0.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: c4354a2b6bde336a87c348be716782869b8bdeb5
4
+ data.tar.gz: 756757244859d56327254b3cd0a542a8a8dcca44
5
+ SHA512:
6
+ metadata.gz: 399da63041ae6721161cb2a6ba40630526d40457dba97d35cd6832c472e67fe154c69daae92328ee2974d40dccedee31b656b270be0fdf7d0952e3398fc323d4
7
+ data.tar.gz: 40e65f68834e5247001a699fbf23f578cc1b9120c21b3b3d4cb104b495470848bc16dba1d8c2772c3c0011f31765ed77b060c939d84f50b4af444352e95a8032
data/.document ADDED
@@ -0,0 +1,5 @@
1
+ lib/**/*.rb
2
+ bin/*
3
+ -
4
+ features/**/*.feature
5
+ LICENSE.txt
data/CHANGES ADDED
@@ -0,0 +1,7 @@
1
+ = tabmani changelog
2
+
3
+ == Master (for 0.0.1)
4
+
5
+ == Version 0.0.0 [2018-09-25]
6
+
7
+ - Initial release
data/Gemfile ADDED
@@ -0,0 +1,16 @@
1
+ source "https://rubygems.org"
2
+ # Add dependencies required to use your gem here.
3
+ # Example:
4
+ # gem "activesupport", ">= 2.3.5"
5
+
6
+ # Add dependencies to develop your gem here.
7
+ # Include everything needed to run rake, tests, features, etc.
8
+ group :development do
9
+ gem "rdoc", "~> 6.0.4"
10
+ gem "bundler", "~> 1.16"
11
+ gem "jeweler", "~> 2.3"
12
+ gem "simplecov", ">= 0"
13
+ gem "test-unit", "~> 3.2.4"
14
+ gem "tefil", '~> 1.0'
15
+ gem "builtinextension", '~> 0.1'
16
+ end
data/LICENSE.txt ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2018 ippei94da
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.rdoc ADDED
@@ -0,0 +1,19 @@
1
+ = tabmani
2
+
3
+ This GEM provide tabmani command, which help you manipulate table with text format.
4
+
5
+ == Contributing to tabmani
6
+
7
+ * Check out the latest master to make sure the feature hasn't been implemented or the bug hasn't been fixed yet.
8
+ * Check out the issue tracker to make sure someone already hasn't requested it and/or contributed it.
9
+ * Fork the project.
10
+ * Start a feature/bugfix branch.
11
+ * Commit and push until you are happy with your contribution.
12
+ * Make sure to add tests for it. This is important so I don't break it in a future version unintentionally.
13
+ * Please try not to mess with the Rakefile, version, or history. If you want to have your own version, or is otherwise necessary, that is fine, but please isolate to its own commit so I can cherry-pick around it.
14
+
15
+ == Copyright
16
+
17
+ Copyright (c) 2018 ippei94da. See LICENSE.txt for
18
+ further details.
19
+
data/Rakefile ADDED
@@ -0,0 +1,51 @@
1
+ # encoding: utf-8
2
+
3
+ require 'rubygems'
4
+ require 'bundler'
5
+ begin
6
+ Bundler.setup(:default, :development)
7
+ rescue Bundler::BundlerError => e
8
+ $stderr.puts e.message
9
+ $stderr.puts "Run `bundle install` to install missing gems"
10
+ exit e.status_code
11
+ end
12
+ require 'rake'
13
+
14
+ require 'jeweler'
15
+ Jeweler::Tasks.new do |gem|
16
+ # gem is a Gem::Specification... see http://guides.rubygems.org/specification-reference/ for more options
17
+ gem.name = "tabmani"
18
+ gem.homepage = "http://github.com/ippei94da/tabmani"
19
+ gem.license = "MIT"
20
+ gem.summary = %Q{Table manipulator}
21
+ gem.description = %Q{This GEM provide tabmani command, which help you manipulate table with text format.}
22
+ gem.email = "ippei94da@gmail.com"
23
+ gem.authors = ["ippei94da"]
24
+ # dependencies defined in Gemfile
25
+ end
26
+ Jeweler::RubygemsDotOrgTasks.new
27
+
28
+ require 'rake/testtask'
29
+ Rake::TestTask.new(:test) do |test|
30
+ test.libs << 'lib' << 'test'
31
+ test.pattern = 'test/**/test_*.rb'
32
+ test.verbose = true
33
+ end
34
+
35
+ desc "Code coverage detail"
36
+ task :simplecov do
37
+ ENV['COVERAGE'] = "true"
38
+ Rake::Task['test'].execute
39
+ end
40
+
41
+ task :default => :test
42
+
43
+ require 'rdoc/task'
44
+ Rake::RDocTask.new do |rdoc|
45
+ version = File.exist?('VERSION') ? File.read('VERSION') : ""
46
+
47
+ rdoc.rdoc_dir = 'rdoc'
48
+ rdoc.title = "tabmani #{version}"
49
+ rdoc.rdoc_files.include('README*')
50
+ rdoc.rdoc_files.include('lib/**/*.rb')
51
+ end
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 0.0.0
data/bin/tabmani ADDED
@@ -0,0 +1,81 @@
1
+ #! /usr/bin/env ruby
2
+ # coding: utf-8
3
+
4
+ require "pp"
5
+ require "optparse"
6
+ require "tabmani"
7
+ require 'thor'
8
+
9
+ USAGE = <<HERE
10
+ Usage: #{File.basename(__FILE__)} [options] [key=val ...] [key]
11
+ HERE
12
+
13
+ ## option analysis
14
+ options = {}
15
+ op = OptionParser.new
16
+ op.banner = USAGE
17
+ op.on("-r" , "--right" ,"justify to right" ){ options[:just ] = :right}
18
+ op.on("-t" , "--transpose" ,"transpose matrix" ){ options[:transpose] = true}
19
+ op.on("-k" , "--show-key" ,"show key charcters" ){ options[:key] = true}
20
+ op.on( "--sum=key" ,"indicate output format" ){|v|options[:sum ] = v}
21
+ op.on("-a" , "--analyze" ,"show analysis report" ){
22
+ options[:analyze ] = true
23
+ options[:key ] = true
24
+ }
25
+
26
+ op.on("-c", "--input-csv" ,"input style as CSV" ){options[:input] = :csv }
27
+ op.on("-b", "--input-blank" ,"input style as blanks" ){options[:input] = :blank }
28
+ op.on( "--input-column" ,"input style as column" ){options[:input] = :column }
29
+ op.on("-s char", "--input-separator=char","input style as indicated character separated" ){ |v|
30
+ options[:input] = :separator
31
+ options[:in_separator] = v
32
+ }
33
+
34
+ op.on("-C", "--output-csv" ,"output style as CSV" ){options[:output] = :csv }
35
+ op.on( "--output-column" ,"output style as column" ){options[:output] = :column}
36
+ op.on( "--output-mds" ,"output style as markdown simple" ){options[:output] = :mds}
37
+ op.on( "--output-tex" ,"output style as latex" ){options[:output] = :tex}
38
+ op.on("-S char", "--output-separator=char","output style as indicated character separated" ){ |v|
39
+ options[:output] = :column
40
+ options[:out_separator] = v
41
+ }
42
+
43
+ op.on( "--reform" ,"reform key 1, 2, 3 as x, y, value." ){options[:reform] = true}
44
+ op.on( "--title" ,"use the first line as title" ){ options[:title] = true}
45
+ op.parse!(ARGV)
46
+
47
+ # default settings
48
+ options[ :just ] ||= :left
49
+ options[ :input ] ||= :blank
50
+ options[ :output ] ||= :column
51
+ options[ :out_separator ] ||= ' '
52
+
53
+ ## TODO: Argument check
54
+
55
+ table = Tabmani::Table.parse(io: $stdin, style: options[:input], separator: options[:in_separator])
56
+
57
+ table.set_title if options[:title]
58
+
59
+ filter_conds = {}
60
+ ARGV.select{|v| v.include? '='}.each do |str|
61
+ key, val = str.split('=')
62
+ filter_conds[key] = val
63
+ end
64
+ filter_conds.each { |key, val| table.filter!(key, val) }
65
+
66
+ table.transpose! if options[:transpose]
67
+ table.add_sum(options[:sum]) if options[:sum]
68
+ begin
69
+ table.reform(ARGV[0], ARGV[1], ARGV[2]) if options[:reform]
70
+ rescue Tabmani::Table::DuplicateCellError => message
71
+ puts message
72
+ end
73
+
74
+ table.dump(style: options[:output],
75
+ io: $stdout,
76
+ just: options[:just],
77
+ separator: options[:out_separator]
78
+ )
79
+
80
+ table.show_keys if options[:key]
81
+ table.analyze(io: $stdout, keys: ARGV) if options[:analyze ]
data/lib/tabmani.rb ADDED
@@ -0,0 +1,6 @@
1
+
2
+ class Tabmani; end
3
+
4
+ require 'tefil'
5
+
6
+ require 'tabmani/table.rb'
@@ -0,0 +1,446 @@
1
+ #! /usr/bin/env ruby
2
+ # coding: utf-8
3
+
4
+ #INPUT_SEPARATOR = /\s+/
5
+
6
+ #
7
+ #
8
+ #
9
+ class Tabmani::Table < Tefil::TextFilterBase
10
+
11
+ attr_reader :matrix , :titles, :indent
12
+
13
+ class DuplicateCellError < StandardError; end
14
+ class ParseError < StandardError; end
15
+ class SymbolMismatchError < StandardError; end
16
+
17
+ def initialize(matrix: , indent: 0)
18
+ @matrix = matrix
19
+ @titles = nil
20
+ @indent = indent
21
+ @hlines = []
22
+ end
23
+
24
+ ## Wrapper for various parse method.
25
+ # style: :csv, :blank, or :column
26
+ # separator: option for separator style
27
+ def self.parse(io: , style: , separator: ',')
28
+ case style
29
+ when :csv ; self.parse_csv(io)
30
+ when :blank ; self.parse_blanks_separated_value(io)
31
+ when :column ; self.parse_column_based_value(io)
32
+ when :separator ; self.parse_separator(io, separator)
33
+ else;
34
+ raise ParseError, "Unknown style: #{style}"
35
+ end
36
+ end
37
+
38
+ ## CSV
39
+ def self.parse_csv(io)
40
+ self.new(matrix: CSV.parse(io))
41
+ end
42
+
43
+ def self.parse_separator(io, separator)
44
+
45
+ lines = io.readlines
46
+ matrix = lines.map { |line| line.chomp.split(separator) }
47
+ self.new(matrix: matrix)
48
+ end
49
+
50
+ ## blanks_separated_value
51
+ def self.parse_blanks_separated_value(io)
52
+ lines = io.readlines
53
+ indent = lines.map{|line| /^(\s*)/ =~ line; $1.length}.min
54
+ matrix = lines.map { |line| line.strip.split(/\s+/) }
55
+
56
+ lines.map { |line| line.strip.split(/\s+/) }
57
+
58
+ self.new(matrix: matrix, indent: indent)
59
+ end
60
+
61
+ ## column_based_value
62
+ # 縦に貫通する空白列を区切りにする。
63
+ # 各要素の空白は strip する。
64
+ def self.parse_column_based_value(io)
65
+ lines = io.readlines
66
+ return if lines.empty?
67
+ lines.map! { |line| line.chomp }
68
+ lines.delete_if { |line| line.empty? } # delete line consist of linefeed only.
69
+ ranges = self.get_ranges(self.projection_ary(lines))
70
+ matrix = lines.map { |line| ranges.map { |range| line[range].to_s.strip} }
71
+ self.new(matrix: matrix)
72
+ end
73
+
74
+ def self.dump_column_format(matrix:,
75
+ hlines: [],
76
+ io: $stdout,
77
+ indent: 0,
78
+ titles: nil,
79
+ just: :left,
80
+ separator: ' ')
81
+ matrix = [titles, * matrix] if titles
82
+ maxima = self.max_lengths(matrix)
83
+ lines = matrix.map do |row|
84
+ self.line_str(items: row,
85
+ lengths: maxima,
86
+ separator: separator,
87
+ just: just,
88
+ indent: indent)
89
+ end
90
+ hlines.sort.reverse.each { |index| lines.insert(index, '-' * whole_length(matrix)) }
91
+ io.puts lines.join("\n")
92
+ end
93
+
94
+ def self.print_size(string)
95
+ string.each_char.map{|c| c.bytesize == 1 ? 1 : 2}.reduce(0, &:+)
96
+ end
97
+
98
+ # true の範囲を示す二重配列を返す。
99
+ # 各要素は 始点..終点 の各インデックスで出来た範囲。
100
+ ## 各要素は[始点, 終点] の各インデックス。
101
+ def self.get_ranges(ary)
102
+ results = []
103
+ start = nil
104
+ prev = false
105
+ ary << false # for true in final item
106
+ ary.each_with_index do |cur, i|
107
+ if prev == false && cur == true
108
+ start = i
109
+ prev = cur
110
+ elsif prev == true && cur == false
111
+ results << (start..(i - 1))
112
+ prev = cur
113
+ else
114
+ next
115
+ end
116
+ end
117
+ ary.pop
118
+ results
119
+ end
120
+
121
+ # 全ての文字列の最大長を要素数とする配列で、
122
+ # 空白文字以外があれば true, 全て空白文字ならば false にした配列。
123
+ def self.projection_ary(lines)
124
+ return [] if lines.empty?
125
+ max_length = lines.max_by{|line| line.size}.size
126
+ results = Array.new(max_length).fill(false)
127
+ lines.each do |line|
128
+ line.chomp.size.times do |i|
129
+ c = line[i]
130
+ next if results[i] == true
131
+ if c == ' '
132
+ next
133
+ else
134
+ results[i] = true
135
+ end
136
+ end
137
+ end
138
+ results
139
+ end
140
+
141
+ def self.padding(str: , width: , padding: ' ', place: :left)
142
+ output_width = str.each_char.map{|c| c.bytesize == 1 ? 1 : 2}.reduce(0, &:+)
143
+ padding_size = [0, width - output_width].max
144
+
145
+ case place
146
+ when :left ; left = 0 ; right = padding_size
147
+ when :right ; left = padding_size ; right = 0
148
+ when :center ; left = padding_size / 2 ; right = padding_size - left
149
+ else
150
+ raise SymbolMismatchError, place
151
+ end
152
+ (padding * left) + str + (padding * right)
153
+ end
154
+
155
+ def self.whole_length(matrix)
156
+ result = 0
157
+ self.max_lengths(matrix).each { |l| result += l}
158
+ result += num_columns(matrix) - 1
159
+ end
160
+
161
+ def self.num_columns(matrix)
162
+ matrix.map{|items| items.size}.max
163
+ end
164
+
165
+ def self.line_str(items: ,
166
+ lengths: ,
167
+ lineend: nil,
168
+ just: :left,
169
+ indent: 0,
170
+ separator: ' ')
171
+
172
+ new_items = []
173
+ lengths.each_with_index do |length, index|
174
+ item = items[index].to_s
175
+ new_items[index] = self.padding(str: item, width: lengths[index], place: just)
176
+ end
177
+ str = " " * indent
178
+ str += new_items.join(separator)
179
+ if lineend
180
+ str += lineend
181
+ else
182
+ str.sub!(/ +$/, "")
183
+ end
184
+ str
185
+ end
186
+
187
+ # return array of max size of item at index
188
+ def self.max_lengths(matrix)
189
+ results = []
190
+ matrix.each do |row|
191
+ row.each_with_index do |item, index|
192
+ item = item.to_s
193
+ results[index] ||= 0
194
+ size = Tabmani::Table.print_size(item)
195
+ results[index] = size if results[index] < size
196
+ end
197
+ end
198
+ results
199
+ end
200
+
201
+ ## Wrapper for various dump method.
202
+ def dump(io: , style: , just: :left, separator: ' ')
203
+ case style
204
+ when :column ; dump_column_format(io: io, just: just, separator: separator )
205
+ when :csv ; dump_csv(io: io)
206
+ when :mds ; dump_md_simple(io: io, just: just )
207
+ when :tex ; dump_tex(io: io, just: just )
208
+ else
209
+ raise ParseError, "Unknown style: #{style}"
210
+ end
211
+ end
212
+
213
+ def dump_column_format(io: $stdout, just: :left, separator: ' ')
214
+ self.class.dump_column_format(matrix: @matrix,
215
+ hlines: @hlines,
216
+ titles: @titles,
217
+ io: io,
218
+ indent: @indent,
219
+ just: just,
220
+ separator: separator)
221
+ end
222
+
223
+ def dump_csv(io: )
224
+ matrix = @matrix
225
+ matrix = [@titles, * @matrix] if @titles
226
+ csv_string = CSV.generate do |csv|
227
+ matrix.each { |items| csv << items }
228
+ end
229
+ io.print csv_string
230
+ end
231
+
232
+ ## markdown simple table style
233
+ def dump_md_simple(io: $stdout, just: :left)
234
+ if @titles
235
+ matrix = [@titles, * @matrix] if @titles
236
+ else
237
+ matrix = @matrix.clone
238
+ end
239
+ matrix.insert(1, line_row)
240
+ self.class.dump_column_format(io: io, matrix: matrix, just: just)
241
+ end
242
+
243
+ def dump_tex(io: $stdout, just: :left)
244
+ case just
245
+ when :left ; just_char = 'l'
246
+ when :right ; just_char = 'r'
247
+ when :center ; just_char = 'c'
248
+ end
249
+
250
+ maxima = max_lengths
251
+ h_size = @matrix.map{|row| row.size }.max
252
+
253
+ io.puts "\\begin{tabular}{#{just_char * h_size }}"
254
+
255
+ lines = []
256
+ if @titles
257
+ lines << self.class.line_str(items: @titles,
258
+ lengths: maxima,
259
+ separator: " & ",
260
+ just: just,
261
+ lineend: ' \\\\',
262
+ indent: 2)
263
+ @hlines << 1
264
+ end
265
+
266
+ @matrix.each do |row|
267
+ lines << self.class.line_str(items: row,
268
+ lengths: maxima,
269
+ separator: " & ",
270
+ just: just,
271
+ lineend: ' \\\\',
272
+ indent: 2)
273
+ end
274
+ #@hlines = [0, maxima.size] if @hlines.empty?
275
+ @hlines << 0
276
+ @hlines << maxima.size
277
+ @hlines.sort.reverse.each do |index|
278
+ lines.insert(index, " \\hline")
279
+ end
280
+ io.puts lines.join("\n")
281
+ io.puts "\\end{tabular}"
282
+ end
283
+
284
+ def show_keys(io: $stdout, separator: ' ')
285
+ matrix = []
286
+ matrix << line_row
287
+
288
+ tmp = []
289
+ max_lengths.size.times do |i|
290
+ tmp << (i + 1).to_s
291
+ end
292
+ matrix << tmp
293
+
294
+ self.class.dump_column_format(matrix: matrix, io: io, indent: @indent)
295
+ end
296
+
297
+ # transpose matrix.
298
+ # empty cell is filled by empty String, ''.
299
+ # thanks: http://www.tom08.net/entry/2017/12/21/125127
300
+ def transpose!
301
+ max_length = @matrix.max_by(&:size).size
302
+ @matrix = @matrix.map { |m| m.fill('', m.size, max_length - m.size) }.transpose
303
+ end
304
+
305
+ def transpose
306
+ result = Marshal.load(Marshal.dump(self))
307
+ result.transpose!
308
+ result
309
+ end
310
+
311
+ def filter!(key, val)
312
+ @matrix.select! do |items|
313
+ items[key.to_i - 1] == val
314
+ end
315
+ end
316
+
317
+ def filter(key, val)
318
+ result = Marshal.load(Marshal.dump(self))
319
+ result.filter!(key, val)
320
+ result
321
+ end
322
+
323
+ def reform(key_x, key_y, key_val)
324
+ x_index = key_x .to_i - 1
325
+ y_index = key_y .to_i - 1
326
+ v_index = key_val.to_i - 1
327
+
328
+ data_hash = {}
329
+ xs = []
330
+ ys = []
331
+ @matrix.each do |items|
332
+ next if items.empty?
333
+ x = items[x_index]
334
+ y = items[y_index]
335
+ v = items[v_index]
336
+ data_hash[y] ||= {}
337
+ raise DuplicateCellError, "Duplicated condition: #{x}, #{y}" if data_hash[y][x]
338
+ data_hash[y][x] = v
339
+ xs << x
340
+ ys << y
341
+ end
342
+
343
+ xs = xs.sort.uniq
344
+ ys = ys.sort.uniq
345
+ matrix = []
346
+ matrix << [''] + xs
347
+ ys.each do |y|
348
+ items = []
349
+ items << y
350
+ xs.each { |x| items << data_hash[y][x] }
351
+ matrix << items
352
+ end
353
+ @matrix = matrix
354
+ end
355
+
356
+ # return array of max size of item at index
357
+ def max_lengths
358
+ matrix = @matrix
359
+ matrix = [@titles, * @matrix] if @titles
360
+ self.class.max_lengths(matrix)
361
+ end
362
+
363
+
364
+ def analyze(io: , keys:)
365
+ io.puts
366
+
367
+ unless @titles
368
+ @titles = []
369
+ num_columns.times do |i|
370
+ @titles[i] = @matrix[0][i].to_s
371
+ end
372
+ end
373
+
374
+ if @matrix.size != 0
375
+ results = []
376
+ results << %w(key head types)
377
+ @titles.size.times do |i|
378
+ results << [(i+1).to_s, @titles[i].strip,
379
+ @matrix.map {|items| items[i]}.sort_by{|j| j.to_s}.uniq.size.to_s
380
+ ]
381
+ end
382
+ Tabmani::Table.dump_column_format(matrix: results, io: io)
383
+ end
384
+
385
+ unless keys.empty?
386
+ io.puts
387
+ io.puts "key analysis"
388
+ keys.each do |key|
389
+ io.puts "(key=#{key})"
390
+ values = @matrix.map{|items| items[key.to_i-1] }
391
+ names = values.sort.uniq
392
+ results = []
393
+ names.each { |name| results << [name, values.count(name).to_s] }
394
+ results.sort_by!{|v| v[1].to_i}
395
+ Tabmani::Table.dump_column_format(matrix: results, io: io)
396
+ io.puts
397
+ end
398
+ end
399
+ end
400
+
401
+ def add_sum(key)
402
+ index = key.to_i - 1
403
+ sum = 0
404
+ @matrix.each do |items|
405
+ str = items[index]
406
+ if str.include?('.')
407
+ sum += str.to_f
408
+ else
409
+ sum += str.to_i
410
+ end
411
+ end
412
+ items = Array.new(num_columns).fill('')
413
+ items[index] = sum.to_s
414
+ add_hline( @matrix.size)
415
+ @matrix << items
416
+ end
417
+
418
+ # num is the index of row number below the horizontal line.
419
+ def add_hline(num)
420
+ @hlines << num
421
+ end
422
+
423
+ # delete spaces head or tail in each items. Destructive
424
+ def strip
425
+ @matrix.map! do |items|
426
+ items.map {|str| str.strip}
427
+ end
428
+ end
429
+
430
+ def set_title
431
+ @titles = @matrix.shift
432
+ end
433
+
434
+ private
435
+
436
+ def line_row
437
+ max_lengths.map {|i| '-' * i}
438
+ end
439
+
440
+ def num_columns
441
+ self.class.num_columns(@matrix)
442
+ end
443
+
444
+ end
445
+
446
+