tabmani 0.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.document +5 -0
- data/CHANGES +7 -0
- data/Gemfile +16 -0
- data/LICENSE.txt +20 -0
- data/README.rdoc +19 -0
- data/Rakefile +51 -0
- data/VERSION +1 -0
- data/bin/tabmani +81 -0
- data/lib/tabmani.rb +6 -0
- data/lib/tabmani/table.rb +446 -0
- data/test/helper.rb +33 -0
- data/test/test_table.rb +582 -0
- data/test/test_tabmani.rb +7 -0
- metadata +158 -0
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
data/CHANGES
ADDED
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,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
|
+
|