solvebio 1.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|