solvebio 1.5.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +7 -0
- data/.travis.yml +13 -0
- data/Gemfile +4 -0
- data/Gemspec +3 -0
- data/LICENSE +21 -0
- data/Makefile +17 -0
- data/README.md +64 -0
- data/Rakefile +59 -0
- data/bin/solvebio.rb +36 -0
- data/demo/README.md +14 -0
- data/demo/dataset/facets.rb +13 -0
- data/demo/dataset/field.rb +13 -0
- data/demo/depository/README.md +24 -0
- data/demo/depository/all.rb +13 -0
- data/demo/depository/retrieve.rb +13 -0
- data/demo/depository/versions-all.rb +13 -0
- data/demo/query/query-filter.rb +30 -0
- data/demo/query/query.rb +13 -0
- data/demo/query/range-filter.rb +18 -0
- data/demo/test-api.rb +98 -0
- data/lib/apiresource.rb +130 -0
- data/lib/cli/auth.rb +122 -0
- data/lib/cli/help.rb +13 -0
- data/lib/cli/irb.rb +58 -0
- data/lib/cli/irbrc.rb +53 -0
- data/lib/cli/options.rb +75 -0
- data/lib/client.rb +152 -0
- data/lib/credentials.rb +67 -0
- data/lib/errors.rb +81 -0
- data/lib/filter.rb +312 -0
- data/lib/help.rb +46 -0
- data/lib/locale.rb +47 -0
- data/lib/main.rb +37 -0
- data/lib/query.rb +415 -0
- data/lib/resource.rb +414 -0
- data/lib/solvebio.rb +14 -0
- data/lib/solveobject.rb +101 -0
- data/lib/tabulate.rb +706 -0
- data/solvebio.gemspec +75 -0
- data/test/data/netrc-save +6 -0
- data/test/helper.rb +3 -0
- data/test/test-auth.rb +54 -0
- data/test/test-client.rb +27 -0
- data/test/test-error.rb +36 -0
- data/test/test-filter.rb +70 -0
- data/test/test-netrc.rb +42 -0
- data/test/test-query-batch.rb +60 -0
- data/test/test-query-init.rb +29 -0
- data/test/test-query-paging.rb +123 -0
- data/test/test-query.rb +88 -0
- data/test/test-resource.rb +47 -0
- data/test/test-solveobject.rb +27 -0
- data/test/test-tabulate.rb +127 -0
- metadata +158 -0
data/lib/tabulate.rb
ADDED
@@ -0,0 +1,706 @@
|
|
1
|
+
# -*- coding: utf-8 -*-
|
2
|
+
#
|
3
|
+
# This file contains code from python-tabulate, modified for SolveBio
|
4
|
+
#
|
5
|
+
# Copyright © 2011-2013 Sergey Astanin
|
6
|
+
#
|
7
|
+
# Permission is hereby granted, free of charge, to any person obtaining
|
8
|
+
# a copy of this software and associated documentation files (the
|
9
|
+
# "Software"), to deal in the Software without restriction, including
|
10
|
+
# without limitation the rights to use, copy, modify, merge, publish,
|
11
|
+
# distribute, sublicense, and/or sell copies of the Software, and to
|
12
|
+
# permit persons to whom the Software is furnished to do so, subject to
|
13
|
+
# the following conditions:
|
14
|
+
#
|
15
|
+
# The above copyright notice and this permission notice shall be
|
16
|
+
# included in all copies or substantial portions of the Software.
|
17
|
+
#
|
18
|
+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
19
|
+
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
20
|
+
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
21
|
+
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
22
|
+
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
23
|
+
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
24
|
+
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
25
|
+
|
26
|
+
# from __future__ require 'print_function'
|
27
|
+
# from __future__ require 'unicode_literals'
|
28
|
+
|
29
|
+
# from collections require 'namedtuple'
|
30
|
+
# from platform require 'python_version_tuple'
|
31
|
+
|
32
|
+
# if python_version_tuple()[0] < "3"
|
33
|
+
# from itertools require 'izip_longest'
|
34
|
+
# _none_type = type(nil)
|
35
|
+
# _int_type = int
|
36
|
+
# _float_type = float
|
37
|
+
# _text_type = unicode
|
38
|
+
# _binary_type = str
|
39
|
+
# else
|
40
|
+
# from itertools require 'zip_longest as izip_longest'
|
41
|
+
# from functools require 'reduce'
|
42
|
+
# _none_type = type(nil)
|
43
|
+
# _int_type = int
|
44
|
+
# _float_type = float
|
45
|
+
# _text_type = str
|
46
|
+
# _binary_type = bytes
|
47
|
+
# end
|
48
|
+
|
49
|
+
|
50
|
+
require_relative 'main'
|
51
|
+
|
52
|
+
module SolveBio::Tabulate
|
53
|
+
|
54
|
+
VERSION = '0.6'
|
55
|
+
|
56
|
+
TYPES = {NilClass => 0, Fixnum => 1, Float => 2, String => 4}
|
57
|
+
|
58
|
+
INVISIBILE_CODES = %r{\\x1b\[\d*m} # ANSI color codes
|
59
|
+
|
60
|
+
Line = Struct.new(:start, :hline, :sep, :last)
|
61
|
+
|
62
|
+
DataRow = Struct.new(:start, :sep, :last)
|
63
|
+
|
64
|
+
TableFormat = Struct.new(:lineabove, :linebelowheader,
|
65
|
+
:linebetweenrows, :linebelow,
|
66
|
+
:headerrow, :datarow,
|
67
|
+
:padding, :usecolons,
|
68
|
+
:with_header_hide,
|
69
|
+
:without_header_hide)
|
70
|
+
|
71
|
+
FORMAT_DEFAULTS = {
|
72
|
+
:padding => 0,
|
73
|
+
:usecolons => false,
|
74
|
+
:with_header_hide => [],
|
75
|
+
:without_header_hide => []
|
76
|
+
}
|
77
|
+
|
78
|
+
|
79
|
+
INVTYPES = {
|
80
|
+
4 => String,
|
81
|
+
2 => Float,
|
82
|
+
1 => Fixnum,
|
83
|
+
0 => NilClass
|
84
|
+
}
|
85
|
+
|
86
|
+
SIMPLE_DATAROW = DataRow.new('', ' ', '')
|
87
|
+
PIPE_DATAROW = DataRow.new('|', '|', '|')
|
88
|
+
|
89
|
+
SIMPLE_LINE = Line.new('', '-', ' ', '')
|
90
|
+
GRID_LINE = Line.new('+', '-', '+', '+')
|
91
|
+
|
92
|
+
TABLE_FORMATS = {
|
93
|
+
:simple =>
|
94
|
+
TableFormat.new(lineabove = nil,
|
95
|
+
linebelowheader = SIMPLE_LINE,
|
96
|
+
linebetweenrows = nil,
|
97
|
+
linebelow = SIMPLE_LINE,
|
98
|
+
headerrow = SIMPLE_DATAROW,
|
99
|
+
datarow = SIMPLE_DATAROW,
|
100
|
+
padding = 0,
|
101
|
+
usecolons = false,
|
102
|
+
with_header_hide = ['linebelow'],
|
103
|
+
without_header_hide = []),
|
104
|
+
:grid =>
|
105
|
+
TableFormat.new(lineabove = SIMPLE_LINE,
|
106
|
+
linebelowheader = Line.new('+', '=', '+', '+'),
|
107
|
+
linebetweenrows = SIMPLE_LINE,
|
108
|
+
linebelow = SIMPLE_LINE,
|
109
|
+
headerrow = PIPE_DATAROW,
|
110
|
+
datarow = PIPE_DATAROW,
|
111
|
+
padding = 1,
|
112
|
+
usecolons = false,
|
113
|
+
with_header_hide = [],
|
114
|
+
without_header_hide = ['linebelowheader']),
|
115
|
+
|
116
|
+
:pipe =>
|
117
|
+
TableFormat.new(lineabove = nil,
|
118
|
+
linebelowheader = Line.new('|', '-', '|', '|'),
|
119
|
+
linebetweenrows = nil,
|
120
|
+
linebelow = nil,
|
121
|
+
headerrow = PIPE_DATAROW,
|
122
|
+
datarow = PIPE_DATAROW,
|
123
|
+
padding = 1,
|
124
|
+
usecolons = true,
|
125
|
+
with_header_hide = [],
|
126
|
+
without_header_hide = []),
|
127
|
+
|
128
|
+
:orgmode =>
|
129
|
+
TableFormat.new(lineabove=nil,
|
130
|
+
linebelowheader = Line.new('|', '-', '+', '|'),
|
131
|
+
linebetweenrows = nil,
|
132
|
+
linebelow = nil,
|
133
|
+
headerrow = PIPE_DATAROW,
|
134
|
+
datarow = PIPE_DATAROW,
|
135
|
+
padding = 1,
|
136
|
+
usecolons = false,
|
137
|
+
with_header_hide = [],
|
138
|
+
without_header_hide = ['linebelowheader'])
|
139
|
+
}
|
140
|
+
|
141
|
+
module_function
|
142
|
+
def simple_separated_format(separator)
|
143
|
+
# FIXME? python code hard-codes separator = "\n" below.
|
144
|
+
return TableFormat
|
145
|
+
.new(
|
146
|
+
:lineabove => nil,
|
147
|
+
:linebelowheader => nil,
|
148
|
+
:linebetweenrows => nil,
|
149
|
+
:linebelow => nil,
|
150
|
+
:headerrow => nil,
|
151
|
+
:datarow => DataRow.new('', separator, ''),
|
152
|
+
:padding => 0,
|
153
|
+
:usecolons => false,
|
154
|
+
:with_header_hide => [],
|
155
|
+
:without_header_hide => [],
|
156
|
+
)
|
157
|
+
end
|
158
|
+
|
159
|
+
# The least generic type, one of NilClass, Fixnum, Float, or String.
|
160
|
+
# _type(nil) => NilClass
|
161
|
+
# _type("foo") => String
|
162
|
+
# _type("1") => Fixnum
|
163
|
+
# _type("\x1b[31m42\x1b[0m") => Fixnum
|
164
|
+
def _type(obj, has_invisible=true)
|
165
|
+
|
166
|
+
obj = obj.strip_invisible if obj.kind_of?(String) and has_invisible
|
167
|
+
|
168
|
+
if obj.nil?
|
169
|
+
return NilClass
|
170
|
+
elsif obj.kind_of?(Fixnum) or obj.int?
|
171
|
+
return Fixnum
|
172
|
+
elsif obj.kind_of?(Float) or obj.number?
|
173
|
+
return Float
|
174
|
+
else
|
175
|
+
return String
|
176
|
+
end
|
177
|
+
end
|
178
|
+
|
179
|
+
# [string] -> [padded_string]
|
180
|
+
#
|
181
|
+
# align_column(
|
182
|
+
# ["12.345", "-1234.5", "1.23", "1234.5",
|
183
|
+
# "1e+234", "1.0e234"], "decimal") =>
|
184
|
+
# [' 12.345 ', '-1234.5 ', ' 1.23 ',
|
185
|
+
# ' 1234.5 ', ' 1e+234 ', ' 1.0e234']
|
186
|
+
def align_column(strings, alignment, minwidth=0, has_invisible=true)
|
187
|
+
if alignment == "right"
|
188
|
+
strings = strings.map{|s| s.to_s.strip}
|
189
|
+
padfn = :padleft
|
190
|
+
elsif alignment == 'center'
|
191
|
+
strings = strings.map{|s| s.to_s.strip}
|
192
|
+
padfn = :padboth
|
193
|
+
elsif alignment == 'decimal'
|
194
|
+
decimals = strings.map{|s| s.to_s.afterpoint}
|
195
|
+
maxdecimals = decimals.max
|
196
|
+
zipped = strings.zip(decimals)
|
197
|
+
strings = zipped.map{|s, decs|
|
198
|
+
s.to_s + " " * ((maxdecimals - decs))
|
199
|
+
}
|
200
|
+
padfn = :padleft
|
201
|
+
else
|
202
|
+
strings = strings.map{|s| s.to_s.strip}
|
203
|
+
padfn = :padright
|
204
|
+
end
|
205
|
+
|
206
|
+
if has_invisible
|
207
|
+
width_fn = :visible_width
|
208
|
+
else
|
209
|
+
width_fn = :size
|
210
|
+
end
|
211
|
+
|
212
|
+
maxwidth = [strings.map{|s| s.send(width_fn)}.max, minwidth].max
|
213
|
+
strings.map{|s| s.send(padfn, maxwidth, has_invisible) }
|
214
|
+
end
|
215
|
+
|
216
|
+
|
217
|
+
def more_generic(type1, type2)
|
218
|
+
moregeneric = [TYPES[type1] || 4, TYPES[type2] || 4].max
|
219
|
+
return INVTYPES[moregeneric]
|
220
|
+
end
|
221
|
+
|
222
|
+
|
223
|
+
# The least generic type all column values are convertible to.
|
224
|
+
#
|
225
|
+
# column_type(["1", "2"]) => Fixnum
|
226
|
+
# column_type(["1", "2.3"]) => Float
|
227
|
+
# column_type(["1", "2.3", "four"]) => String
|
228
|
+
# column_type(["four", '\u043f\u044f\u0442\u044c']) => String
|
229
|
+
# column_type([nil, "brux"]) => String
|
230
|
+
# column_type([1, 2, nil]) => Fixnum
|
231
|
+
def column_type(strings, has_invisible=true)
|
232
|
+
types = strings.map{|s| _type(s, has_invisible)}
|
233
|
+
# require 'trepanning'; debugger
|
234
|
+
return types.reduce(Fixnum){
|
235
|
+
|t, result|
|
236
|
+
more_generic(result, t)
|
237
|
+
}
|
238
|
+
end
|
239
|
+
|
240
|
+
|
241
|
+
# Format a value accoding to its type.
|
242
|
+
#
|
243
|
+
# Unicode is supported:
|
244
|
+
#
|
245
|
+
# >>> hrow = ["\u0431\u0443\u043a\u0432\u0430",
|
246
|
+
# "\u0446\u0438\u0444\u0440\u0430"]
|
247
|
+
# tbl = [["\u0430\u0437", 2], ["\u0431\u0443\u043a\u0438", 4]]
|
248
|
+
# expected = "\\u0431\\u0443\\u043a\\u0432\\u0430 \n
|
249
|
+
# \\u0446\\u0438\\u0444\\u0440\\u0430\\n-------\n
|
250
|
+
# -------\\n\\u0430\\u0437 \n
|
251
|
+
# 2\\n\\u0431\\u0443\\u043a\\u0438 4'
|
252
|
+
# tabulate(tbl, hrow) => good_result
|
253
|
+
# true
|
254
|
+
def format(val, valtype, floatfmt, missingval="")
|
255
|
+
if val.nil?
|
256
|
+
return missingval
|
257
|
+
end
|
258
|
+
|
259
|
+
if [Fixnum, String, Fixnum].member?(valtype)
|
260
|
+
return "%s" % val
|
261
|
+
elsif valtype.kind_of?(Float)
|
262
|
+
return "%#{floatfmt}" % Float(val)
|
263
|
+
else
|
264
|
+
return "%s" % val
|
265
|
+
end
|
266
|
+
end
|
267
|
+
|
268
|
+
|
269
|
+
def align_header(header, alignment, width)
|
270
|
+
if alignment == "left"
|
271
|
+
return header.padright(width)
|
272
|
+
elsif alignment == "center"
|
273
|
+
return header.padboth(width)
|
274
|
+
else
|
275
|
+
return header.padleft(width)
|
276
|
+
end
|
277
|
+
end
|
278
|
+
|
279
|
+
|
280
|
+
# Transform a supported data type to an Array of Arrays, and an
|
281
|
+
# Array of headers.
|
282
|
+
#
|
283
|
+
# Supported tabular data types:
|
284
|
+
#
|
285
|
+
# * Array-of-Arrays or another Enumerable of Enumerables
|
286
|
+
#
|
287
|
+
# * Hash of Enumerables
|
288
|
+
#
|
289
|
+
# The first row can be used as headers if headers="firstrow",
|
290
|
+
# column indices can be used as headers if headers="keys".
|
291
|
+
#
|
292
|
+
def normalize_tabular_data(tabular_data, headers)
|
293
|
+
if tabular_data.respond_to?(:keys) and tabular_data.respond_to?(:values)
|
294
|
+
# likely a Hash
|
295
|
+
keys = tabular_data.keys
|
296
|
+
## FIXME: what's different in the Python code?
|
297
|
+
# columns have to be transposed
|
298
|
+
# rows = list(izip_longest(*tabular_data.values()))
|
299
|
+
# rows = vals[0].zip(*vals[1..-1])
|
300
|
+
rows = tabular_data.values
|
301
|
+
if headers == "keys"
|
302
|
+
# headers should be strings
|
303
|
+
headers = keys.map{|k| k.to_s}
|
304
|
+
end
|
305
|
+
elsif tabular_data.kind_of?(Enumerable)
|
306
|
+
# Likely an Enumerable of Enumerables
|
307
|
+
rows = tabular_data.to_a
|
308
|
+
if headers == "keys" and not rows.empty? # keys are column indices
|
309
|
+
headers = (0..rows[0]).map {|i| i.to_s}
|
310
|
+
end
|
311
|
+
else
|
312
|
+
raise(ValueError, "tabular data doesn't appear to be a Hash" +
|
313
|
+
" or Array")
|
314
|
+
end
|
315
|
+
|
316
|
+
# take headers from the first row if necessary
|
317
|
+
if headers == "firstrow" and not rows.empty?
|
318
|
+
headers = rows[0].map{|row| [_text_type(row)]}
|
319
|
+
rows.shift
|
320
|
+
end
|
321
|
+
|
322
|
+
# pad with empty headers for initial columns if necessary
|
323
|
+
if not headers.empty? and not rows.empty?
|
324
|
+
nhs = headers.size
|
325
|
+
ncols = rows[0].size
|
326
|
+
if nhs < ncols
|
327
|
+
headers = [''] * (ncols - nhs) + headers
|
328
|
+
end
|
329
|
+
end
|
330
|
+
|
331
|
+
return rows, headers
|
332
|
+
end
|
333
|
+
|
334
|
+
TTY_COLS = ENV['COLUMNS'].to_i || 80 rescue 80
|
335
|
+
# Return a string which represents a row of data cells.
|
336
|
+
def build_row(cells, padding, first, sep, last)
|
337
|
+
|
338
|
+
pad = ' ' * padding
|
339
|
+
padded_cells = cells.map{|cell| pad + cell + pad }
|
340
|
+
|
341
|
+
# SolveBio: we're only displaying Key-Value tuples (dimension of 2).
|
342
|
+
# enforce that we don't wrap lines by setting a max
|
343
|
+
# limit on row width which is equal to TTY_COLS (see printing)
|
344
|
+
rendered_cells = (first + padded_cells.join(sep) + last).rstrip
|
345
|
+
if rendered_cells.size > TTY_COLS
|
346
|
+
if not cells[-1].end_with?(' ') and not cells[-1].end_with?('-')
|
347
|
+
terminating_str = ' ... '
|
348
|
+
else
|
349
|
+
terminating_str = ''
|
350
|
+
end
|
351
|
+
prefix = rendered_cells[1..TTY_COLS - terminating_str.size - 1]
|
352
|
+
rendered_cells = "%s%s%s" % [prefix, terminating_str, last]
|
353
|
+
end
|
354
|
+
|
355
|
+
return rendered_cells
|
356
|
+
end
|
357
|
+
|
358
|
+
|
359
|
+
# Return a string which represents a horizontal line.
|
360
|
+
def build_line(colwidths, padding, first, fill, sep, last)
|
361
|
+
cells = colwidths.map{|w| fill * (w + 2 * padding)}
|
362
|
+
return build_row(cells, 0, first, sep, last)
|
363
|
+
end
|
364
|
+
|
365
|
+
|
366
|
+
# Return a segment of a horizontal line with optional colons which
|
367
|
+
# indicate column's alignment (as in `pipe` output format).
|
368
|
+
def _line_segment_with_colons(linefmt, align, colwidth)
|
369
|
+
fill = linefmt.hline
|
370
|
+
w = colwidth
|
371
|
+
if ['right', 'decimal'].member?(align)
|
372
|
+
return (fill[0] * (w - 1)) + ":"
|
373
|
+
elsif align == "center"
|
374
|
+
return ":" + (fill[0] * (w - 2)) + ":"
|
375
|
+
elsif align == "left"
|
376
|
+
return ":" + (fill[0] * (w - 1))
|
377
|
+
else
|
378
|
+
return fill[0] * w
|
379
|
+
end
|
380
|
+
end
|
381
|
+
|
382
|
+
|
383
|
+
# Produce a plain-text representation of the table.
|
384
|
+
def format_table(fmt, headers, rows, colwidths, colaligns)
|
385
|
+
lines = []
|
386
|
+
hidden = headers ? fmt.with_header_hide : fmt.without_header_hide
|
387
|
+
pad = fmt.padding || 0
|
388
|
+
datarow = fmt.datarow ? fmt.datarow : SIMPLE_DATAROW
|
389
|
+
headerrow = fmt.headerrow ? fmt.headerrow : fmt.datarow
|
390
|
+
|
391
|
+
if fmt.lineabove and hidden and hidden.member?("lineabove")
|
392
|
+
lines << build_line(colwidths, pad, *fmt.lineabove)
|
393
|
+
end
|
394
|
+
|
395
|
+
unless headers.empty?
|
396
|
+
lines << build_row(headers, pad, headerrow.start, headerrow.sep,
|
397
|
+
headerrow.last)
|
398
|
+
end
|
399
|
+
|
400
|
+
if fmt.linebelowheader and not hidden.member?("linebelowheader")
|
401
|
+
first, _, sep, last = fmt.linebelowheader
|
402
|
+
if fmt.usecolons
|
403
|
+
segs = [
|
404
|
+
colwidths.zip(colaligns).map do |w, a|
|
405
|
+
_line_segment_with_colons(fmt.linebelowheader, a, w + 2 * pad)
|
406
|
+
end ]
|
407
|
+
lines << build_row(segs, 0, first, sep, last)
|
408
|
+
else
|
409
|
+
lines << build_line(colwidths, pad, fmt.linebelowheader.start,
|
410
|
+
fmt.linebelowheader.hline,
|
411
|
+
fmt.linebelowheader.sep,
|
412
|
+
fmt.linebelowheader.last)
|
413
|
+
end
|
414
|
+
end
|
415
|
+
|
416
|
+
if rows and fmt.linebetweenrows and hidden.member?('linebetweenrows')
|
417
|
+
# initial rows with a line below
|
418
|
+
rows[1..-1].each do |row|
|
419
|
+
lines << build_row(row, pad, fmt.datarow.start,
|
420
|
+
fmt.datarow.sep, fmt.datarow.last)
|
421
|
+
lines << build_line(colwidths, pad, fmt.linebetweenrows.start,
|
422
|
+
fmt.linebelowheader.hline,
|
423
|
+
fmt.linebetweenrows.sep,
|
424
|
+
fmt.linebetweenrows.last)
|
425
|
+
end
|
426
|
+
# the last row without a line below
|
427
|
+
lines << build_row(rows[-1], pad, datarow.start,
|
428
|
+
datarow.sep, datarow.last)
|
429
|
+
else
|
430
|
+
rows.each do |row|
|
431
|
+
lines << build_row(row, pad, datarow.start, datarow.sep,
|
432
|
+
datarow.last)
|
433
|
+
|
434
|
+
if fmt.linebelow and hidden.member?('linebelow')
|
435
|
+
lines << build_line(colwidths, pad, fmt.linebelow.start,
|
436
|
+
fmt.linebelowheader.hline,
|
437
|
+
fmt.linebelow.sep,
|
438
|
+
fmt.linebelow.last)
|
439
|
+
end
|
440
|
+
end
|
441
|
+
end
|
442
|
+
return lines.join("\n")
|
443
|
+
end
|
444
|
+
|
445
|
+
# Construct a simple TableFormat with columns separated by a separator.
|
446
|
+
#
|
447
|
+
# tsv = simple_separated_format("\t")
|
448
|
+
# tabulate([["foo", 1], ["spam", 23]], [], tsv) =>
|
449
|
+
# "foo 1\nspam 23"
|
450
|
+
def tabulate(tabular_data, headers=[], tablefmt=TABLE_FORMATS[:orgmode],
|
451
|
+
floatfmt="g", aligns=[], missingval='')
|
452
|
+
list_of_lists, headers = normalize_tabular_data(tabular_data, headers)
|
453
|
+
|
454
|
+
# optimization: look for ANSI control codes once,
|
455
|
+
# enable smart width functions only if a control code is found
|
456
|
+
plain_rows = [headers.map{|h| h.to_s}.join("\t")]
|
457
|
+
row_text = list_of_lists.map{|row|
|
458
|
+
row.map{|r| r.to_s}.join("\t")
|
459
|
+
}
|
460
|
+
plain_rows += row_text
|
461
|
+
plain_text = plain_rows.join("\n")
|
462
|
+
|
463
|
+
has_invisible = INVISIBILE_CODES.match(plain_text)
|
464
|
+
if has_invisible
|
465
|
+
width_fn = :visible_width
|
466
|
+
else
|
467
|
+
width_fn = :size
|
468
|
+
end
|
469
|
+
|
470
|
+
# format rows and columns, convert numeric values to strings
|
471
|
+
cols = list_of_lists[0].zip(*list_of_lists[1..-1]) if
|
472
|
+
list_of_lists.size > 1
|
473
|
+
|
474
|
+
coltypes = cols.map{|c| column_type(c)}
|
475
|
+
|
476
|
+
cols = cols.zip(coltypes).map do |c, ct|
|
477
|
+
c.map{|v| format(v, ct, floatfmt, missingval)}
|
478
|
+
end
|
479
|
+
|
480
|
+
# align columns
|
481
|
+
if aligns.empty?
|
482
|
+
# dynamic alignment by col type
|
483
|
+
aligns = coltypes.map do |ct|
|
484
|
+
[Fixnum, Float].member?(ct) ? 'decimal' : 'left'
|
485
|
+
end
|
486
|
+
end
|
487
|
+
|
488
|
+
minwidths =
|
489
|
+
if headers.empty? then
|
490
|
+
[0] * cols.size
|
491
|
+
else
|
492
|
+
headers.map{|h| h.send(width_fn) + 2}
|
493
|
+
end
|
494
|
+
|
495
|
+
cols = cols.zip(aligns, minwidths).map do |c, a, minw|
|
496
|
+
align_column(c, a, minw, has_invisible)
|
497
|
+
end
|
498
|
+
|
499
|
+
if headers.empty?
|
500
|
+
minwidths = cols.map{|c| c[0].send(width_fn)}
|
501
|
+
else
|
502
|
+
# align headers and add headers
|
503
|
+
minwidths =
|
504
|
+
minwidths.zip(cols).map{|minw, c| [minw, c[0].send(width_fn)].max}
|
505
|
+
headers =
|
506
|
+
headers.zip(aligns, minwidths).map{|h, a, minw| align_header(h, a, minw)}
|
507
|
+
end
|
508
|
+
rows = cols[0].zip(cols[1])
|
509
|
+
|
510
|
+
tablefmt = TABLE_FORMATS[:orgmode] unless
|
511
|
+
tablefmt.kind_of?(TableFormat)
|
512
|
+
|
513
|
+
# make sure values don't have newlines or tabs in them
|
514
|
+
rows = rows.each do |r|
|
515
|
+
r[1] = r[1].gsub("\n", '').gsub("\t", '')
|
516
|
+
end
|
517
|
+
return format_table(tablefmt, headers, rows, minwidths, aligns)
|
518
|
+
end
|
519
|
+
end
|
520
|
+
|
521
|
+
class Object
|
522
|
+
|
523
|
+
# "123.45".number? => true
|
524
|
+
# "123".number? => true
|
525
|
+
# "spam".number? => false
|
526
|
+
def number?
|
527
|
+
begin
|
528
|
+
Float(self)
|
529
|
+
return true
|
530
|
+
rescue
|
531
|
+
return false
|
532
|
+
end
|
533
|
+
end
|
534
|
+
|
535
|
+
# "123".int? => true
|
536
|
+
# "123.45".int? => false
|
537
|
+
def int?
|
538
|
+
begin
|
539
|
+
Integer(self)
|
540
|
+
return true
|
541
|
+
rescue
|
542
|
+
return false
|
543
|
+
end
|
544
|
+
end
|
545
|
+
end
|
546
|
+
|
547
|
+
class String
|
548
|
+
|
549
|
+
# Symbols after a decimal point, -1 if the string lacks the decimal point.
|
550
|
+
#
|
551
|
+
# "123.45".afterpoint => 2
|
552
|
+
# "1001".afterpoint => -1
|
553
|
+
# "eggs".afterpoint => -1
|
554
|
+
# "123e45".afterpoint => 2
|
555
|
+
def afterpoint
|
556
|
+
if self.number?
|
557
|
+
if self.int?
|
558
|
+
return -1
|
559
|
+
else
|
560
|
+
pos = self.rindex('.') || -1
|
561
|
+
pos = self.downcase().rindex('e') if pos < 0
|
562
|
+
if pos >= 0
|
563
|
+
return self.size - pos - 1
|
564
|
+
else
|
565
|
+
return -1 # no point
|
566
|
+
end
|
567
|
+
end
|
568
|
+
else
|
569
|
+
return -1 # not a number
|
570
|
+
end
|
571
|
+
end
|
572
|
+
|
573
|
+
def adjusted_size(has_invisible)
|
574
|
+
return has_invisible ? self.strip_invisible.size : self.size
|
575
|
+
end
|
576
|
+
|
577
|
+
# Visible width of a printed string. ANSI color codes are removed.
|
578
|
+
#
|
579
|
+
# ['\x1b[31mhello\x1b[0m' "world"].map{|s| s.visible_width} =>
|
580
|
+
# [5, 5]
|
581
|
+
def visible_width
|
582
|
+
# if self.kind_of?(_text_type) or self.kind_of?(_binary_type)
|
583
|
+
return self.strip_invisible.size
|
584
|
+
# else
|
585
|
+
# return _text_type(s).size
|
586
|
+
# end
|
587
|
+
end
|
588
|
+
|
589
|
+
|
590
|
+
# Flush right.
|
591
|
+
#
|
592
|
+
# '\u044f\u0439\u0446\u0430'.padleft(6) =>
|
593
|
+
# ' \u044f\u0439\u0446\u0430'
|
594
|
+
# 'abc'.padleft(2) => 'abc'
|
595
|
+
def padleft(width, has_invisible=true)
|
596
|
+
s_width = self.adjusted_size(has_invisible)
|
597
|
+
s_width < width ? (' ' * (width - s_width)) + self : self
|
598
|
+
end
|
599
|
+
|
600
|
+
# Flush left.
|
601
|
+
#
|
602
|
+
# padright(6, '\u044f\u0439\u0446\u0430') => '\u044f\u0439\u0446\u0430 '
|
603
|
+
# padright(2, 'abc') => 'abc'
|
604
|
+
def padright(width, has_invisible=true)
|
605
|
+
s_width = self.adjusted_size(has_invisible)
|
606
|
+
s_width < width ? self + (' ' * (width - s_width)) : self
|
607
|
+
end
|
608
|
+
|
609
|
+
|
610
|
+
# Center string with uneven space on the right
|
611
|
+
#
|
612
|
+
# '\u044f\u0439\u0446\u0430'.padboth(6) => ' \u044f\u0439\u0446\u0430 '
|
613
|
+
# 'abc'.padboth(2) => 'abc'
|
614
|
+
# 'abc'.padboth(6) => ' abc '
|
615
|
+
def padboth(width, has_invisible=true)
|
616
|
+
s_width = self.adjusted_size(has_invisible)
|
617
|
+
return self if s_width >= width
|
618
|
+
pad_size = width - s_width
|
619
|
+
pad_left = ' ' * (pad_size/2)
|
620
|
+
pad_right = ' ' * ((pad_size + 1)/ 2)
|
621
|
+
pad_left + self + pad_right
|
622
|
+
end
|
623
|
+
|
624
|
+
|
625
|
+
# Remove invisible ANSI color codes.
|
626
|
+
def strip_invisible
|
627
|
+
return self.gsub(SolveBio::Tabulate::INVISIBILE_CODES, '')
|
628
|
+
end
|
629
|
+
|
630
|
+
end
|
631
|
+
|
632
|
+
if __FILE__ == $0
|
633
|
+
include SolveBio::Tabulate
|
634
|
+
# puts '" 123.45".num? %s' % "123.45".number?() # true
|
635
|
+
# puts "'123'.num?: %s" % '123'.number? # true
|
636
|
+
# puts "'spam'.num? spam: %s" % "spam".number? # false
|
637
|
+
# puts "'123'.int? %s" % "123".int? # true
|
638
|
+
# puts "'123.45'int?: %s" % '124.45'.int? # false
|
639
|
+
|
640
|
+
# puts "_type(nil) %s = %s" % [_type(nil), NilClass]
|
641
|
+
# puts "_type('foo') %s = %s" % [_type('foo'), String]
|
642
|
+
# puts "_type('1') %s = %s" % [_type('1'), Fixnum]
|
643
|
+
# puts "_type(''\x1b[31m42\x1b[0m') %s = %s" % [_type('\x1b[31m42\x1b[0m'), Fixnum]
|
644
|
+
|
645
|
+
# puts "'123.45'.afterpoint: 2 == %d" % '123.45'.afterpoint
|
646
|
+
# puts "'1001'afterpoint : -1 == %d" % '1001'.afterpoint
|
647
|
+
# puts "'eggs'.afterpoint : -1 == %d" % 'eggs'.afterpoint
|
648
|
+
# puts "'123e45'.afterpoint: 2 == %d" % "123e45".afterpoint
|
649
|
+
|
650
|
+
# puts("'\u044f\u0439\u0446\u0430'.padleft(6) = '%s' == '%s'" %
|
651
|
+
# ["\u044f\u0439\u0446\u0430".padleft(6),
|
652
|
+
# " \u044f\u0439\u0446\u0430"])
|
653
|
+
# puts("'abc'.padleft(2) = '%s' == '%s'" %
|
654
|
+
# ['abc'.padleft(2), 'abc'])
|
655
|
+
# puts("padright(2, 'abc') = '%s' == '%s'" %
|
656
|
+
# ['abc'.padright(2), 'abc'])
|
657
|
+
# puts("'abc'.padboth(2) = '%s' == '%s'" %
|
658
|
+
# ['abc'.padboth(2), 'abc'])
|
659
|
+
# puts("'abc'.padboth(6) = '%s' == '%s'" %
|
660
|
+
# ['abc'.padboth(6), ' abc '])
|
661
|
+
|
662
|
+
# puts align_column(
|
663
|
+
# ["12.345", "-1234.5", "1.23", "1234.5",
|
664
|
+
# "1e+234", "1.0e234"], "decimal")
|
665
|
+
|
666
|
+
# puts '=' * 30
|
667
|
+
# puts [' 12.345 ', '-1234.5 ', ' 1.23 ',
|
668
|
+
# ' 1234.5 ', ' 1e+234 ', ' 1.0e234']
|
669
|
+
|
670
|
+
# puts('column_type(["1", "2"]) is Fixnum == %s ' %
|
671
|
+
# column_type(["1", "2"]))
|
672
|
+
# puts('column_type(["1", "2.3"]) is Float == %s ' %
|
673
|
+
# column_type(["1", "2.3"]))
|
674
|
+
# puts('column_type(["1", "2.3", "four"]) is String => %s ' %
|
675
|
+
# column_type(["1", "2.3", "four"]))
|
676
|
+
# puts('column_type(["four", "\u043f\u044f\u0442\u044c"]) is text => %s ' %
|
677
|
+
# column_type(["four", "\u043f\u044f\u0442\u044c"]))
|
678
|
+
# puts('column_type([nil, "brux"]) is String => %s ' %
|
679
|
+
# column_type([nil, "brux"]))
|
680
|
+
# puts('column_type([1, 2, nil]) is Fixnum => %s ' %
|
681
|
+
# column_type([1, 2, nil]))
|
682
|
+
# tsv = simple_separated_format("\t")
|
683
|
+
# puts tabulate([["foo", 1], ["spam", 23]], [], tsv)
|
684
|
+
# hrow = ["\u0431\u0443\u043a\u0432\u0430", "\u0446\u0438\u0444\u0440\u0430"]
|
685
|
+
# tbl = [["\u0430\u0437", 2], ["\u0431\u0443\u043a\u0438", 4]]
|
686
|
+
# puts SolveBio::Tabulate.tabulate(tbl, hrow)
|
687
|
+
|
688
|
+
hash = {
|
689
|
+
"rcvaccession_version"=>2,
|
690
|
+
"hg18_chromosome"=>"3",
|
691
|
+
"hg19_start"=>148562304,
|
692
|
+
"rcvaccession"=>"RCV000060731",
|
693
|
+
"hg38_start"=>148844517,
|
694
|
+
"reference_allele"=>"C",
|
695
|
+
"gene_symbols"=>["CPB1"],
|
696
|
+
"rsid"=>"rs150241322",
|
697
|
+
"hg19_chromosome"=>"3",
|
698
|
+
"hgvs"=>["NC_000003.12:g.148844517C>T"],
|
699
|
+
"clinical_significance"=>"other",
|
700
|
+
"alternate_alleles"=>["T"],
|
701
|
+
"clinical_origin"=>["somatic"],
|
702
|
+
"type"=>"SNV"}
|
703
|
+
puts SolveBio::Tabulate.tabulate(hash.to_a,
|
704
|
+
['Fields', 'Data'],
|
705
|
+
['right', 'left'])
|
706
|
+
end
|