fat_table 0.2.6 → 0.3.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +5 -5
- data/.gitignore +3 -0
- data/.rubocop.yml +3 -0
- data/.travis.yml +7 -2
- data/.yardopts +5 -1
- data/README.org +271 -251
- data/README.rdoc +4 -4
- data/TODO.org +7 -0
- data/bin/ft_console +82 -79
- data/fat_table.gemspec +47 -46
- data/lib/fat_table.rb +11 -2
- data/lib/fat_table/column.rb +41 -29
- data/lib/fat_table/db_handle.rb +31 -50
- data/lib/fat_table/evaluator.rb +26 -24
- data/lib/fat_table/formatters/aoa_formatter.rb +5 -6
- data/lib/fat_table/formatters/aoh_formatter.rb +6 -7
- data/lib/fat_table/formatters/formatter.rb +67 -48
- data/lib/fat_table/formatters/latex_formatter.rb +9 -7
- data/lib/fat_table/formatters/org_formatter.rb +8 -9
- data/lib/fat_table/formatters/term_formatter.rb +19 -18
- data/lib/fat_table/formatters/text_formatter.rb +6 -7
- data/lib/fat_table/patches.rb +30 -1
- data/lib/fat_table/table.rb +143 -117
- data/lib/fat_table/version.rb +1 -1
- data/md/README.md +2167 -0
- metadata +78 -84
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module FatTable
|
2
4
|
# A subclass of Formatter for rendering the table as a LaTeX table. It allows
|
3
5
|
# foreground colors through LaTeX's xcolor package but ignores background
|
@@ -17,7 +19,6 @@ module FatTable
|
|
17
19
|
# LaTeX tabular-like environment to use for the table. The default is good
|
18
20
|
# for tables that might continue over multiple pages since it repeats the
|
19
21
|
# header at the top of each continuation page.
|
20
|
-
|
21
22
|
def initialize(table = Table.new, **options)
|
22
23
|
super
|
23
24
|
@options[:document] = options.fetch(:document, false)
|
@@ -25,7 +26,7 @@ module FatTable
|
|
25
26
|
end
|
26
27
|
|
27
28
|
# Taken from the Rainbow gem's list of valid colors.
|
28
|
-
self.valid_colors = %w
|
29
|
+
self.valid_colors = %w[
|
29
30
|
none black blue brown cyan darkgray gray green lightgray lime magenta
|
30
31
|
olive orange pink purple red teal violet white yellow AntiqueWhite1
|
31
32
|
AntiqueWhite2 AntiqueWhite3 AntiqueWhite4 Aquamarine1 Aquamarine2
|
@@ -76,7 +77,7 @@ module FatTable
|
|
76
77
|
Turquoise1 Turquoise2 Turquoise3 Turquoise4 VioletRed1 VioletRed2
|
77
78
|
VioletRed3 VioletRed4 Wheat1 Wheat2 Wheat3 Wheat4 Yellow1 Yellow2 Yellow3
|
78
79
|
Yellow4
|
79
|
-
|
80
|
+
]
|
80
81
|
|
81
82
|
# LaTeX commands to load the needed packages based on the :environement
|
82
83
|
# option. For now, just handles the default 'longtable' :environment. The
|
@@ -115,7 +116,8 @@ module FatTable
|
|
115
116
|
result = ''
|
116
117
|
result += '\\bfseries{}' if istruct.bold
|
117
118
|
result += '\\itshape{}' if istruct.italic
|
118
|
-
result += "\\color{#{istruct.color}}" if istruct.color &&
|
119
|
+
result += "\\color{#{istruct.color}}" if istruct.color &&
|
120
|
+
istruct.color != 'none'
|
119
121
|
result = "#{result}#{str}"
|
120
122
|
unless istruct.alignment == format_at[:body][istruct._h].alignment
|
121
123
|
ac = alignment_code(istruct.alignment)
|
@@ -173,14 +175,14 @@ module FatTable
|
|
173
175
|
''
|
174
176
|
end
|
175
177
|
|
176
|
-
def pre_cell(
|
178
|
+
def pre_cell(_head)
|
177
179
|
''
|
178
180
|
end
|
179
181
|
|
180
182
|
# We do quoting before applying decoration, so do not re-quote here. We
|
181
183
|
# will have LaTeX commands in v.
|
182
|
-
def quote_cell(
|
183
|
-
|
184
|
+
def quote_cell(val)
|
185
|
+
val
|
184
186
|
end
|
185
187
|
|
186
188
|
def post_cell
|
@@ -4,10 +4,9 @@ module FatTable
|
|
4
4
|
# timestamps and the connector at the beginning of hlines is a '|' rather than
|
5
5
|
# a '+' as for text tables.
|
6
6
|
class OrgFormatter < Formatter
|
7
|
-
|
8
7
|
self.default_format = default_format.dup
|
9
|
-
|
10
|
-
|
8
|
+
default_format[:date_fmt] = '[%F]'
|
9
|
+
default_format[:datetime_fmt] = '[%F %a %H:%M:%S]'
|
11
10
|
|
12
11
|
private
|
13
12
|
|
@@ -20,7 +19,7 @@ module FatTable
|
|
20
19
|
|
21
20
|
def pre_header(widths)
|
22
21
|
result = '|'
|
23
|
-
widths.
|
22
|
+
widths.each_value do |w|
|
24
23
|
result += '-' * (w + 2) + '+'
|
25
24
|
end
|
26
25
|
result[-1] = '|'
|
@@ -31,12 +30,12 @@ module FatTable
|
|
31
30
|
'|'
|
32
31
|
end
|
33
32
|
|
34
|
-
def pre_cell(
|
33
|
+
def pre_cell(_head)
|
35
34
|
''
|
36
35
|
end
|
37
36
|
|
38
|
-
def quote_cell(
|
39
|
-
|
37
|
+
def quote_cell(val)
|
38
|
+
val
|
40
39
|
end
|
41
40
|
|
42
41
|
def post_cell
|
@@ -53,7 +52,7 @@ module FatTable
|
|
53
52
|
|
54
53
|
def hline(widths)
|
55
54
|
result = '|'
|
56
|
-
widths.
|
55
|
+
widths.each_value do |w|
|
57
56
|
result += '-' * (w + 2) + '+'
|
58
57
|
end
|
59
58
|
result[-1] = '|'
|
@@ -62,7 +61,7 @@ module FatTable
|
|
62
61
|
|
63
62
|
def post_footers(widths)
|
64
63
|
result = '|'
|
65
|
-
widths.
|
64
|
+
widths.each_value do |w|
|
66
65
|
result += '-' * (w + 2) + '+'
|
67
66
|
end
|
68
67
|
result[-1] = '|'
|
@@ -1,5 +1,3 @@
|
|
1
|
-
# -*- coding: utf-8 -*-
|
2
|
-
|
3
1
|
require 'rainbow'
|
4
2
|
|
5
3
|
module FatTable
|
@@ -29,6 +27,7 @@ module FatTable
|
|
29
27
|
@options[:unicode] = options.fetch(:unicode, true)
|
30
28
|
@options[:framecolor] = options.fetch(:framecolor, 'none.none')
|
31
29
|
return unless @options[:framecolor] =~ /([-_a-zA-Z]*)(\.([-_a-zA-Z]*))/
|
30
|
+
|
32
31
|
@options[:frame_fg] = $1.downcase unless $1.blank?
|
33
32
|
@options[:frame_bg] = $3.downcase unless $3.blank?
|
34
33
|
end
|
@@ -61,6 +60,7 @@ module FatTable
|
|
61
60
|
|
62
61
|
def strip_ansi(str)
|
63
62
|
return '' unless str
|
63
|
+
|
64
64
|
str.gsub(/\e\[[0-9;]+m/, '')
|
65
65
|
end
|
66
66
|
|
@@ -75,18 +75,19 @@ module FatTable
|
|
75
75
|
result
|
76
76
|
end
|
77
77
|
|
78
|
-
def colorize(str,
|
79
|
-
|
80
|
-
|
81
|
-
return str unless
|
78
|
+
def colorize(str, fg_color, bg_color)
|
79
|
+
fg_color = nil if fg_color == 'none'
|
80
|
+
bg_color = nil if bg_color == 'none'
|
81
|
+
return str unless fg_color || bg_color
|
82
|
+
|
82
83
|
result = Rainbow(str)
|
83
|
-
if
|
84
|
-
|
85
|
-
result = result.color(
|
84
|
+
if fg_color
|
85
|
+
fg_color = fg_color.tr(' ', '').downcase.as_sym
|
86
|
+
result = result.color(fg_color) if fg_color
|
86
87
|
end
|
87
|
-
if
|
88
|
-
|
89
|
-
result = result.bg(
|
88
|
+
if bg_color
|
89
|
+
bg_color = bg_color.tr(' ', '').downcase.as_sym
|
90
|
+
result = result.bg(bg_color) if bg_color
|
90
91
|
end
|
91
92
|
result
|
92
93
|
end
|
@@ -219,7 +220,7 @@ module FatTable
|
|
219
220
|
|
220
221
|
def pre_header(widths)
|
221
222
|
result = upper_left
|
222
|
-
widths.
|
223
|
+
widths.each_value do |w|
|
223
224
|
result += double_rule * (w + 2) + upper_tee
|
224
225
|
end
|
225
226
|
result[-1] = upper_right
|
@@ -231,12 +232,12 @@ module FatTable
|
|
231
232
|
frame_colorize(vertical_rule)
|
232
233
|
end
|
233
234
|
|
234
|
-
def pre_cell(
|
235
|
+
def pre_cell(_head)
|
235
236
|
''
|
236
237
|
end
|
237
238
|
|
238
|
-
def quote_cell(
|
239
|
-
|
239
|
+
def quote_cell(val)
|
240
|
+
val
|
240
241
|
end
|
241
242
|
|
242
243
|
def post_cell
|
@@ -253,7 +254,7 @@ module FatTable
|
|
253
254
|
|
254
255
|
def hline(widths)
|
255
256
|
result = left_tee
|
256
|
-
widths.
|
257
|
+
widths.each_value do |w|
|
257
258
|
result += horizontal_rule * (w + 2) + single_cross
|
258
259
|
end
|
259
260
|
result[-1] = right_tee
|
@@ -287,7 +288,7 @@ module FatTable
|
|
287
288
|
|
288
289
|
def post_footers(widths)
|
289
290
|
result = lower_left
|
290
|
-
widths.
|
291
|
+
widths.each_value do |w|
|
291
292
|
result += double_rule * (w + 2) + lower_tee
|
292
293
|
end
|
293
294
|
result[-1] = lower_right
|
@@ -4,7 +4,6 @@ module FatTable
|
|
4
4
|
# connector at the beginning of hlines is a '+' rather than a '|' as for org
|
5
5
|
# tables.
|
6
6
|
class TextFormatter < Formatter
|
7
|
-
|
8
7
|
private
|
9
8
|
|
10
9
|
# Does this Formatter require a second pass over the cells to align the
|
@@ -16,7 +15,7 @@ module FatTable
|
|
16
15
|
|
17
16
|
def pre_header(widths)
|
18
17
|
result = '+'
|
19
|
-
widths.
|
18
|
+
widths.each_value do |w|
|
20
19
|
result += '=' * (w + 2) + '+'
|
21
20
|
end
|
22
21
|
result[-1] = '+'
|
@@ -27,12 +26,12 @@ module FatTable
|
|
27
26
|
'|'
|
28
27
|
end
|
29
28
|
|
30
|
-
def pre_cell(
|
29
|
+
def pre_cell(_head)
|
31
30
|
''
|
32
31
|
end
|
33
32
|
|
34
|
-
def quote_cell(
|
35
|
-
|
33
|
+
def quote_cell(val)
|
34
|
+
val
|
36
35
|
end
|
37
36
|
|
38
37
|
def post_cell
|
@@ -49,7 +48,7 @@ module FatTable
|
|
49
48
|
|
50
49
|
def hline(widths)
|
51
50
|
result = '+'
|
52
|
-
widths.
|
51
|
+
widths.each_value do |w|
|
53
52
|
result += '-' * (w + 2) + '+'
|
54
53
|
end
|
55
54
|
result[-1] = '+'
|
@@ -82,7 +81,7 @@ module FatTable
|
|
82
81
|
|
83
82
|
def post_footers(widths)
|
84
83
|
result = '+'
|
85
|
-
widths.
|
84
|
+
widths.each_value do |w|
|
86
85
|
result += '=' * (w + 2) + '+'
|
87
86
|
end
|
88
87
|
result[-1] = '+'
|
data/lib/fat_table/patches.rb
CHANGED
@@ -1,4 +1,5 @@
|
|
1
|
-
unless {a: 1}.respond_to?(:fetch_values)
|
1
|
+
unless { a: 1 }.respond_to?(:fetch_values)
|
2
|
+
# Add fetch_values if this version of ruby does not define it.
|
2
3
|
class Hash
|
3
4
|
def fetch_values(*keys)
|
4
5
|
result = []
|
@@ -14,3 +15,31 @@ unless {a: 1}.respond_to?(:fetch_values)
|
|
14
15
|
end
|
15
16
|
end
|
16
17
|
end
|
18
|
+
|
19
|
+
unless ''.respond_to?(:match?)
|
20
|
+
# Add String#match? to pre-2.4 ruby
|
21
|
+
class String
|
22
|
+
def match?(regexp)
|
23
|
+
self =~ regexp
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
unless //.respond_to?(:match?)
|
29
|
+
# Add Regexp#match? to pre-2.4 ruby
|
30
|
+
class Regexp
|
31
|
+
def match?(str)
|
32
|
+
self =~ str
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
unless ''.respond_to?(:strip_heredoc)
|
38
|
+
# Patch String to provide heredocs with whitespace stripped
|
39
|
+
class String
|
40
|
+
def strip_heredoc
|
41
|
+
indent = chomp.scan(/^\s*/).min.size
|
42
|
+
gsub(/^\s{#{indent}}/, '')
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
data/lib/fat_table/table.rb
CHANGED
@@ -49,9 +49,9 @@ module FatTable
|
|
49
49
|
# spaces converted to underscore and everything down-cased. So, the heading,
|
50
50
|
# 'Two Words' becomes the header +:two_words+.
|
51
51
|
class Table
|
52
|
-
|
53
52
|
# An Array of FatTable::Columns that constitute the table.
|
54
53
|
attr_reader :columns
|
54
|
+
attr_accessor :boundaries
|
55
55
|
|
56
56
|
###########################################################################
|
57
57
|
# Constructors
|
@@ -106,18 +106,19 @@ module FatTable
|
|
106
106
|
# :category: Constructors
|
107
107
|
|
108
108
|
# Construct a new table from an Array of Arrays +aoa+. By default, with
|
109
|
-
# +hlines+ set to false, do not look for separators, i.e. +nils+, just
|
110
|
-
# the first row as headers. With +hlines+ set true, expect +nil+
|
111
|
-
# to mark the header row and any boundaries. If the second
|
112
|
-
# array is a +nil+, interpret the first element of the
|
113
|
-
# headers. Otherwise, synthesize headers of the form
|
114
|
-
# and so forth. The remaining elements are taken
|
115
|
-
# except that if an element of the outer array
|
116
|
-
# preceding row as a group boundary. Note for Emacs
|
117
|
-
# blocks when an org-mode table is passed in as a
|
118
|
-
# as an Array of Arrays. By default (+ HEADER:
|
119
|
-
# all from the table; otherwise (+
|
120
|
-
# with nil elements in the outer
|
109
|
+
# +hlines+ set to false, do not look for separators, i.e. +nils+, just
|
110
|
+
# treat the first row as headers. With +hlines+ set true, expect +nil+
|
111
|
+
# separators to mark the header row and any boundaries. If the second
|
112
|
+
# element of the array is a +nil+, interpret the first element of the
|
113
|
+
# array as a row of headers. Otherwise, synthesize headers of the form
|
114
|
+
# +:col_1+, +:col_2+, ... and so forth. The remaining elements are taken
|
115
|
+
# as the body of the table, except that if an element of the outer array
|
116
|
+
# is a +nil+, mark the preceding row as a group boundary. Note for Emacs
|
117
|
+
# users: In org mode code blocks when an org-mode table is passed in as a
|
118
|
+
# variable it is passed in as an Array of Arrays. By default (+ HEADER:
|
119
|
+
# :hlines no +) org-mode strips all hrules from the table; otherwise (+
|
120
|
+
# HEADER: :hlines yes +) they are indicated with nil elements in the outer
|
121
|
+
# array.
|
121
122
|
def self.from_aoa(aoa, hlines: false)
|
122
123
|
from_array_of_arrays(aoa, hlines: hlines)
|
123
124
|
end
|
@@ -140,8 +141,8 @@ module FatTable
|
|
140
141
|
|
141
142
|
# :category: Constructors
|
142
143
|
|
143
|
-
# Construct a new table from another FatTable::Table object +table+. Inherit
|
144
|
-
# group boundaries from the input table.
|
144
|
+
# Construct a new table from another FatTable::Table object +table+. Inherit
|
145
|
+
# any group boundaries from the input table.
|
145
146
|
def self.from_table(table)
|
146
147
|
table.deep_dup
|
147
148
|
end
|
@@ -149,9 +150,11 @@ module FatTable
|
|
149
150
|
# :category: Constructors
|
150
151
|
|
151
152
|
# Construct a Table by running a SQL +query+ against the database set up
|
152
|
-
# with FatTable.
|
153
|
+
# with FatTable.connect, with the rows of the query result as rows.
|
153
154
|
def self.from_sql(query)
|
154
|
-
|
155
|
+
msg = 'FatTable.db must be set with FatTable.connect'
|
156
|
+
raise UserError, msg if FatTable.db.nil?
|
157
|
+
|
155
158
|
result = Table.new
|
156
159
|
FatTable.db[query].each do |h|
|
157
160
|
result << h
|
@@ -166,15 +169,16 @@ module FatTable
|
|
166
169
|
class << self
|
167
170
|
private
|
168
171
|
|
169
|
-
# Construct table from an array of hashes or an array of any object that
|
170
|
-
# respond to #to_h.
|
172
|
+
# Construct table from an array of hashes or an array of any object that
|
173
|
+
# can respond to #to_h. If an array element is a nil, mark it as a group
|
171
174
|
# boundary in the Table.
|
172
175
|
def from_array_of_hashes(hashes, hlines: false)
|
173
176
|
result = new
|
174
177
|
hashes.each do |hsh|
|
175
178
|
if hsh.nil?
|
176
179
|
unless hlines
|
177
|
-
|
180
|
+
msg = 'found an hline in input: try setting hlines true'
|
181
|
+
raise UserError, msg
|
178
182
|
end
|
179
183
|
result.mark_boundary
|
180
184
|
next
|
@@ -219,7 +223,8 @@ module FatTable
|
|
219
223
|
rows[first_data_row..-1].each do |row|
|
220
224
|
if row.nil?
|
221
225
|
unless hlines
|
222
|
-
|
226
|
+
msg = 'found an hline in input: try setting hlines true'
|
227
|
+
raise UserError, msg
|
223
228
|
end
|
224
229
|
result.mark_boundary
|
225
230
|
next
|
@@ -253,15 +258,17 @@ module FatTable
|
|
253
258
|
io.each do |line|
|
254
259
|
unless table_found
|
255
260
|
# Skip through the file until a table is found
|
256
|
-
next unless line
|
257
|
-
|
261
|
+
next unless line.match?(table_re)
|
262
|
+
|
263
|
+
unless line.match?(hrule_re)
|
258
264
|
line = line.sub(/\A\s*\|/, '').sub(/\|\s*\z/, '')
|
259
265
|
rows << line.split('|').map(&:clean)
|
260
266
|
end
|
261
267
|
table_found = true
|
262
268
|
next
|
263
269
|
end
|
264
|
-
break unless line
|
270
|
+
break unless line.match?(table_re)
|
271
|
+
|
265
272
|
if !header_found && line =~ hrule_re
|
266
273
|
rows << nil
|
267
274
|
header_found = true
|
@@ -269,7 +276,7 @@ module FatTable
|
|
269
276
|
elsif header_found && line =~ hrule_re
|
270
277
|
# Mark the boundary with a nil
|
271
278
|
rows << nil
|
272
|
-
elsif line
|
279
|
+
elsif !line.match?(table_re)
|
273
280
|
# Stop reading at the second hline
|
274
281
|
break
|
275
282
|
else
|
@@ -311,13 +318,19 @@ module FatTable
|
|
311
318
|
def [](key)
|
312
319
|
case key
|
313
320
|
when Integer
|
314
|
-
|
321
|
+
msg = "index '#{key}' out of range"
|
322
|
+
raise UserError, msg unless (0..size - 1).cover?(key.abs)
|
323
|
+
|
315
324
|
rows[key]
|
316
325
|
when String
|
317
|
-
|
326
|
+
msg = "header '#{key}' not in table"
|
327
|
+
raise UserError, msg unless headers.include?(key)
|
328
|
+
|
318
329
|
column(key).items
|
319
330
|
when Symbol
|
320
|
-
|
331
|
+
msg = "header ':#{key}' not in table"
|
332
|
+
raise UserError, msg unless headers.include?(key)
|
333
|
+
|
321
334
|
column(key).items
|
322
335
|
else
|
323
336
|
raise UserError, "cannot index table with a #{key.class}"
|
@@ -354,6 +367,7 @@ module FatTable
|
|
354
367
|
# Return the number of rows in the Table.
|
355
368
|
def size
|
356
369
|
return 0 if columns.empty?
|
370
|
+
|
357
371
|
columns.first.size
|
358
372
|
end
|
359
373
|
|
@@ -362,6 +376,7 @@ module FatTable
|
|
362
376
|
# Return the number of Columns in the Table.
|
363
377
|
def width
|
364
378
|
return 0 if columns.empty?
|
379
|
+
|
365
380
|
columns.size
|
366
381
|
end
|
367
382
|
|
@@ -400,6 +415,7 @@ module FatTable
|
|
400
415
|
last ||= size - 1
|
401
416
|
last = [last, 0].max
|
402
417
|
raise UserError, 'first must be <= last' unless first <= last
|
418
|
+
|
403
419
|
rows = []
|
404
420
|
unless columns.empty?
|
405
421
|
first.upto(last) do |rnum|
|
@@ -413,9 +429,9 @@ module FatTable
|
|
413
429
|
rows
|
414
430
|
end
|
415
431
|
|
416
|
-
|
432
|
+
############################################################################
|
417
433
|
# Enumerable
|
418
|
-
|
434
|
+
############################################################################
|
419
435
|
|
420
436
|
public
|
421
437
|
|
@@ -430,9 +446,6 @@ module FatTable
|
|
430
446
|
end
|
431
447
|
end
|
432
448
|
|
433
|
-
|
434
|
-
public
|
435
|
-
|
436
449
|
# :category: Attributes
|
437
450
|
|
438
451
|
# Boundaries mark the last row in each "group" within the table. The last
|
@@ -490,12 +503,12 @@ module FatTable
|
|
490
503
|
self
|
491
504
|
end
|
492
505
|
|
493
|
-
# Mark a group boundary at row +
|
494
|
-
# in the table as a group boundary. This is mainly used for internal
|
506
|
+
# Mark a group boundary at row +row+, and if +row+ is +nil+, mark the last
|
507
|
+
# row in the table as a group boundary. This is mainly used for internal
|
495
508
|
# purposes.
|
496
|
-
def mark_boundary(
|
497
|
-
if
|
498
|
-
boundaries.push(
|
509
|
+
def mark_boundary(row = nil) # :nodoc:
|
510
|
+
if row
|
511
|
+
boundaries.push(row)
|
499
512
|
else
|
500
513
|
boundaries.push(size - 1)
|
501
514
|
end
|
@@ -505,16 +518,6 @@ module FatTable
|
|
505
518
|
|
506
519
|
# :stopdoc:
|
507
520
|
|
508
|
-
# Reader for boundaries, but not public.
|
509
|
-
def boundaries
|
510
|
-
@boundaries
|
511
|
-
end
|
512
|
-
|
513
|
-
# Writer for boundaries, but not public.
|
514
|
-
def boundaries=(bounds)
|
515
|
-
@boundaries = bounds
|
516
|
-
end
|
517
|
-
|
518
521
|
# Make sure size - 1 is last boundary and that they are unique and sorted.
|
519
522
|
def normalize_boundaries
|
520
523
|
unless empty?
|
@@ -531,20 +534,21 @@ module FatTable
|
|
531
534
|
@boundaries += bounds.map { |k| k + shift }
|
532
535
|
end
|
533
536
|
|
534
|
-
# Return the group number to which row
|
535
|
-
# point of view are indexed starting at 1.
|
536
|
-
def row_index_to_group_index(
|
537
|
+
# Return the group number to which row ~row~ belongs. Groups, from the
|
538
|
+
# user's point of view are indexed starting at 1.
|
539
|
+
def row_index_to_group_index(row)
|
537
540
|
boundaries.each_with_index do |b_last, g_num|
|
538
|
-
return (g_num + 1) if
|
541
|
+
return (g_num + 1) if row <= b_last
|
539
542
|
end
|
540
543
|
1
|
541
544
|
end
|
542
545
|
|
543
|
-
def group_rows(
|
546
|
+
def group_rows(row) # :nodoc:
|
544
547
|
normalize_boundaries
|
545
|
-
return [] unless
|
546
|
-
|
547
|
-
|
548
|
+
return [] unless row < boundaries.size
|
549
|
+
|
550
|
+
first = row.zero? ? 0 : boundaries[row - 1] + 1
|
551
|
+
last = boundaries[row]
|
548
552
|
rows_range(first, last)
|
549
553
|
end
|
550
554
|
|
@@ -682,9 +686,8 @@ module FatTable
|
|
682
686
|
ivars = ivars.merge(new_cols[:ivars])
|
683
687
|
new_cols.delete(:ivars)
|
684
688
|
end
|
685
|
-
before_hook = '@row += 1'
|
686
689
|
if new_cols.key?(:before_hook)
|
687
|
-
before_hook
|
690
|
+
before_hook = new_cols[:before_hook].to_s
|
688
691
|
new_cols.delete(:before_hook)
|
689
692
|
end
|
690
693
|
after_hook = nil
|
@@ -702,32 +705,37 @@ module FatTable
|
|
702
705
|
# Set the group number in the before hook and run the hook with the
|
703
706
|
# local variables set to the row before the new row is evaluated.
|
704
707
|
grp = row_index_to_group_index(old_k)
|
705
|
-
|
706
|
-
ev.eval_before_hook(
|
708
|
+
ev.update_ivars(row: old_k + 1, group: grp)
|
709
|
+
ev.eval_before_hook(locals: old_row)
|
707
710
|
# Compute the new row.
|
708
711
|
new_row = {}
|
709
712
|
cols.each do |k|
|
710
713
|
h = k.as_sym
|
711
|
-
|
714
|
+
msg = "Column '#{h}' in select does not exist"
|
715
|
+
raise UserError, msg unless column?(h)
|
716
|
+
|
712
717
|
new_row[h] = old_row[h]
|
713
718
|
end
|
714
|
-
new_cols.each_pair do |key,
|
719
|
+
new_cols.each_pair do |key, expr|
|
715
720
|
key = key.as_sym
|
716
721
|
vars = old_row.merge(new_row)
|
717
|
-
case
|
722
|
+
case expr
|
718
723
|
when Symbol
|
719
|
-
|
720
|
-
|
724
|
+
msg = "Column '#{expr}' in select does not exist"
|
725
|
+
raise UserError, msg unless vars.key?(expr)
|
726
|
+
|
727
|
+
new_row[key] = vars[expr]
|
721
728
|
when String
|
722
|
-
new_row[key] = ev.evaluate(
|
729
|
+
new_row[key] = ev.evaluate(expr, locals: vars)
|
723
730
|
else
|
724
|
-
|
731
|
+
msg = "Hash parameter '#{key}' to select must be a symbol or string"
|
732
|
+
raise UserError, msg
|
725
733
|
end
|
726
734
|
end
|
727
735
|
# Set the group number and run the hook with the local variables set to
|
728
736
|
# the row after the new row is evaluated.
|
729
|
-
vars = new_row.merge(__group: grp)
|
730
|
-
ev.eval_after_hook(
|
737
|
+
# vars = new_row.merge(__group: grp)
|
738
|
+
ev.eval_after_hook(locals: new_row)
|
731
739
|
result << new_row
|
732
740
|
end
|
733
741
|
result.boundaries = boundaries
|
@@ -753,14 +761,13 @@ module FatTable
|
|
753
761
|
col = Column.new(header: h)
|
754
762
|
result.add_column(col)
|
755
763
|
end
|
756
|
-
ev = Evaluator.new(ivars: { row: 0, group: 0 }
|
757
|
-
before: '@row += 1')
|
764
|
+
ev = Evaluator.new(ivars: { row: 0, group: 0 })
|
758
765
|
rows.each_with_index do |row, k|
|
759
766
|
grp = row_index_to_group_index(k)
|
760
|
-
|
761
|
-
ev.eval_before_hook(
|
762
|
-
result << row if ev.evaluate(expr,
|
763
|
-
ev.eval_after_hook(
|
767
|
+
ev.update_ivars(row: k + 1, group: grp)
|
768
|
+
ev.eval_before_hook(locals: row)
|
769
|
+
result << row if ev.evaluate(expr, locals: row)
|
770
|
+
ev.eval_after_hook(locals: row)
|
764
771
|
end
|
765
772
|
result.normalize_boundaries
|
766
773
|
result
|
@@ -870,22 +877,24 @@ module FatTable
|
|
870
877
|
|
871
878
|
private
|
872
879
|
|
873
|
-
# Apply the set operation given by
|
874
|
-
# given in the first argument. If distinct is true, eliminate
|
875
|
-
# from the result.
|
876
|
-
def set_operation(other,
|
880
|
+
# Apply the set operation given by ~oper~ between this table and the other
|
881
|
+
# table given in the first argument. If distinct is true, eliminate
|
882
|
+
# duplicates from the result.
|
883
|
+
def set_operation(other, oper = :+,
|
877
884
|
distinct: true,
|
878
885
|
add_boundaries: true,
|
879
886
|
inherit_boundaries: false)
|
880
887
|
unless columns.size == other.columns.size
|
881
|
-
|
888
|
+
msg = "can't apply set ops to tables with a different number of columns"
|
889
|
+
raise UserError, msg
|
882
890
|
end
|
883
891
|
unless columns.map(&:type) == other.columns.map(&:type)
|
884
|
-
|
892
|
+
msg = "can't apply a set ops to tables with different column types."
|
893
|
+
raise UserError, msg
|
885
894
|
end
|
886
895
|
other_rows = other.rows.map { |r| r.replace_keys(headers) }
|
887
896
|
result = Table.new
|
888
|
-
new_rows = rows.send(
|
897
|
+
new_rows = rows.send(oper, other_rows)
|
889
898
|
new_rows.each_with_index do |row, k|
|
890
899
|
result << row
|
891
900
|
result.mark_boundary if k == size - 1 && add_boundaries
|
@@ -902,7 +911,7 @@ module FatTable
|
|
902
911
|
public
|
903
912
|
|
904
913
|
# An Array of symbols for the valid join types.
|
905
|
-
JOIN_TYPES = [
|
914
|
+
JOIN_TYPES = %i[inner left right full cross].freeze
|
906
915
|
|
907
916
|
# :category: Operators
|
908
917
|
#
|
@@ -979,10 +988,12 @@ module FatTable
|
|
979
988
|
unless JOIN_TYPES.include?(join_type)
|
980
989
|
raise UserError, "join_type may only be: #{JOIN_TYPES.join(', ')}"
|
981
990
|
end
|
991
|
+
|
982
992
|
# These may be needed for outer joins.
|
983
993
|
self_row_nils = headers.map { |h| [h, nil] }.to_h
|
984
994
|
other_row_nils = other.headers.map { |h| [h, nil] }.to_h
|
985
|
-
|
995
|
+
join_exp, other_common_heads =
|
996
|
+
build_join_expression(exps, other, join_type)
|
986
997
|
ev = Evaluator.new
|
987
998
|
result = Table.new
|
988
999
|
other_rows = other.rows
|
@@ -993,27 +1004,29 @@ module FatTable
|
|
993
1004
|
# Same as other_row, but with keys that are common with self and equal
|
994
1005
|
# in value, removed, so the output table need not repeat them.
|
995
1006
|
locals = build_locals_hash(row_a: self_row, row_b: other_row)
|
996
|
-
matches = ev.evaluate(
|
1007
|
+
matches = ev.evaluate(join_exp, locals: locals)
|
997
1008
|
next unless matches
|
1009
|
+
|
998
1010
|
self_row_matched = other_row_matches[k] = true
|
999
1011
|
out_row = build_out_row(row_a: self_row, row_b: other_row,
|
1000
1012
|
common_heads: other_common_heads,
|
1001
1013
|
type: join_type)
|
1002
1014
|
result << out_row
|
1003
1015
|
end
|
1004
|
-
|
1005
|
-
|
1006
|
-
|
1007
|
-
|
1008
|
-
|
1009
|
-
|
1016
|
+
next unless %i[left full].include?(join_type)
|
1017
|
+
next if self_row_matched
|
1018
|
+
|
1019
|
+
result << build_out_row(row_a: self_row,
|
1020
|
+
row_b: other_row_nils,
|
1021
|
+
type: join_type)
|
1010
1022
|
end
|
1011
|
-
if
|
1023
|
+
if %i[right full].include?(join_type)
|
1012
1024
|
other_rows.each_with_index do |other_row, k|
|
1013
|
-
|
1014
|
-
|
1015
|
-
|
1016
|
-
|
1025
|
+
next if other_row_matches[k]
|
1026
|
+
|
1027
|
+
result << build_out_row(row_a: self_row_nils,
|
1028
|
+
row_b: other_row,
|
1029
|
+
type: join_type)
|
1017
1030
|
end
|
1018
1031
|
end
|
1019
1032
|
result.normalize_boundaries
|
@@ -1094,14 +1107,15 @@ module FatTable
|
|
1094
1107
|
# and all the headers in the other table with '_b' appended.
|
1095
1108
|
def build_join_expression(exps, other, type)
|
1096
1109
|
return ['true', []] if type == :cross
|
1110
|
+
|
1097
1111
|
a_heads = headers
|
1098
1112
|
b_heads = other.headers
|
1099
1113
|
common_heads = a_heads & b_heads
|
1100
1114
|
b_common_heads = []
|
1101
1115
|
if exps.empty?
|
1102
1116
|
if common_heads.empty?
|
1103
|
-
|
1104
|
-
|
1117
|
+
msg = "#{type}-join with no common column names needs join expression"
|
1118
|
+
raise UserError, msg
|
1105
1119
|
else
|
1106
1120
|
# A Natural join on all common heads
|
1107
1121
|
common_heads.each do |h|
|
@@ -1124,9 +1138,12 @@ module FatTable
|
|
1124
1138
|
unless a_heads.include?(a_head)
|
1125
1139
|
raise UserError, "no column '#{a_head}' in table"
|
1126
1140
|
end
|
1141
|
+
|
1127
1142
|
if partial_result
|
1128
1143
|
# Second of a pair
|
1129
|
-
ensure_common_types!(self_h: a_head,
|
1144
|
+
ensure_common_types!(self_h: a_head,
|
1145
|
+
other_h: last_sym,
|
1146
|
+
other: other)
|
1130
1147
|
partial_result << "#{a_head}_a)"
|
1131
1148
|
and_conds << partial_result
|
1132
1149
|
partial_result = nil
|
@@ -1140,9 +1157,12 @@ module FatTable
|
|
1140
1157
|
unless b_heads.include?(b_head)
|
1141
1158
|
raise UserError, "no column '#{b_head}' in second table"
|
1142
1159
|
end
|
1160
|
+
|
1143
1161
|
if partial_result
|
1144
1162
|
# Second of a pair
|
1145
|
-
ensure_common_types!(self_h: last_sym,
|
1163
|
+
ensure_common_types!(self_h: last_sym,
|
1164
|
+
other_h: b_head,
|
1165
|
+
other: other)
|
1146
1166
|
partial_result << "#{b_head}_b)"
|
1147
1167
|
and_conds << partial_result
|
1148
1168
|
partial_result = nil
|
@@ -1158,12 +1178,13 @@ module FatTable
|
|
1158
1178
|
# We were expecting the second of a modified pair, but got an
|
1159
1179
|
# unmodified symbol instead.
|
1160
1180
|
msg =
|
1161
|
-
"
|
1181
|
+
"follow '#{last_sym}' by qualified exp from the other table"
|
1162
1182
|
raise UserError, msg
|
1163
1183
|
end
|
1164
1184
|
# We have an unqualified symbol that must appear in both tables
|
1165
1185
|
unless common_heads.include?(exp)
|
1166
|
-
|
1186
|
+
msg = "unqualified column '#{exp}' must occur in both tables"
|
1187
|
+
raise UserError, msg
|
1167
1188
|
end
|
1168
1189
|
ensure_common_types!(self_h: exp, other_h: exp, other: other)
|
1169
1190
|
and_conds << "(#{exp}_a == #{exp}_b)"
|
@@ -1174,7 +1195,8 @@ module FatTable
|
|
1174
1195
|
# qualified.
|
1175
1196
|
and_conds << "(#{exp})"
|
1176
1197
|
else
|
1177
|
-
|
1198
|
+
msg = "invalid join expression '#{exp}' of class #{exp.class}"
|
1199
|
+
raise UserError, msg
|
1178
1200
|
end
|
1179
1201
|
end
|
1180
1202
|
[and_conds.join(' && '), b_common_heads]
|
@@ -1185,15 +1207,15 @@ module FatTable
|
|
1185
1207
|
# have the same types.
|
1186
1208
|
def ensure_common_types!(self_h:, other_h:, other:)
|
1187
1209
|
unless column(self_h).type == other.column(other_h).type
|
1188
|
-
|
1189
|
-
|
1210
|
+
msg = "column '#{self_h}' type does not match column '#{other_h}"
|
1211
|
+
raise UserError, msg
|
1190
1212
|
end
|
1191
1213
|
self
|
1192
1214
|
end
|
1193
1215
|
|
1194
|
-
|
1216
|
+
############################################################################
|
1195
1217
|
# Group By
|
1196
|
-
|
1218
|
+
############################################################################
|
1197
1219
|
|
1198
1220
|
public
|
1199
1221
|
|
@@ -1268,7 +1290,7 @@ module FatTable
|
|
1268
1290
|
|
1269
1291
|
# :category: Constructors
|
1270
1292
|
|
1271
|
-
# Add a +row+ without marking it as a group boundary.
|
1293
|
+
# Add a +row+ to this Table without marking it as a group boundary.
|
1272
1294
|
def <<(row)
|
1273
1295
|
add_row(row)
|
1274
1296
|
end
|
@@ -1277,7 +1299,9 @@ module FatTable
|
|
1277
1299
|
|
1278
1300
|
# Add a FatTable::Column object +col+ to the table.
|
1279
1301
|
def add_column(col)
|
1280
|
-
|
1302
|
+
msg = "Table already has a column with header '#{col.header}'"
|
1303
|
+
raise msg if column?(col.header)
|
1304
|
+
|
1281
1305
|
columns << col
|
1282
1306
|
self
|
1283
1307
|
end
|
@@ -1324,7 +1348,9 @@ module FatTable
|
|
1324
1348
|
#
|
1325
1349
|
def to_any(fmt_type, options = {})
|
1326
1350
|
fmt = fmt_type.as_sym
|
1327
|
-
|
1351
|
+
msg = "unknown format '#{fmt}'"
|
1352
|
+
raise UserError, msg unless FatTable::FORMATS.include?(fmt)
|
1353
|
+
|
1328
1354
|
method = "to_#{fmt}"
|
1329
1355
|
if block_given?
|
1330
1356
|
send method, options, &Proc.new
|
@@ -1343,7 +1369,7 @@ module FatTable
|
|
1343
1369
|
# default format for Formatter, there is no class PsvFormatter as you might
|
1344
1370
|
# expect.
|
1345
1371
|
def to_psv(options = {})
|
1346
|
-
fmt = Formatter.new(self, options)
|
1372
|
+
fmt = Formatter.new(self, **options)
|
1347
1373
|
yield fmt if block_given?
|
1348
1374
|
fmt.output
|
1349
1375
|
end
|
@@ -1356,7 +1382,7 @@ module FatTable
|
|
1356
1382
|
# the block to which formatting instructions and footers can be added by
|
1357
1383
|
# calling methods on it.
|
1358
1384
|
def to_aoa(options = {})
|
1359
|
-
fmt = FatTable::AoaFormatter.new(self, options)
|
1385
|
+
fmt = FatTable::AoaFormatter.new(self, **options)
|
1360
1386
|
yield fmt if block_given?
|
1361
1387
|
fmt.output
|
1362
1388
|
end
|
@@ -1370,7 +1396,7 @@ module FatTable
|
|
1370
1396
|
# given, it yields an AohFormatter to the block to which formatting
|
1371
1397
|
# instructions and footers can be added by calling methods on it.
|
1372
1398
|
def to_aoh(options = {})
|
1373
|
-
fmt = AohFormatter.new(self, options)
|
1399
|
+
fmt = AohFormatter.new(self, **options)
|
1374
1400
|
yield fmt if block_given?
|
1375
1401
|
fmt.output
|
1376
1402
|
end
|
@@ -1383,7 +1409,7 @@ module FatTable
|
|
1383
1409
|
# LaTeXFormatter to the block to which formatting instructions and footers
|
1384
1410
|
# can be added by calling methods on it.
|
1385
1411
|
def to_latex(options = {})
|
1386
|
-
fmt = LaTeXFormatter.new(self, options)
|
1412
|
+
fmt = LaTeXFormatter.new(self, **options)
|
1387
1413
|
yield fmt if block_given?
|
1388
1414
|
fmt.output
|
1389
1415
|
end
|
@@ -1396,7 +1422,7 @@ module FatTable
|
|
1396
1422
|
# OrgFormatter to the block to which formatting instructions and footers can
|
1397
1423
|
# be added by calling methods on it.
|
1398
1424
|
def to_org(options = {})
|
1399
|
-
fmt = OrgFormatter.new(self, options)
|
1425
|
+
fmt = OrgFormatter.new(self, **options)
|
1400
1426
|
yield fmt if block_given?
|
1401
1427
|
fmt.output
|
1402
1428
|
end
|
@@ -1409,7 +1435,7 @@ module FatTable
|
|
1409
1435
|
# given, it yields a TermFormatter to the block to which formatting
|
1410
1436
|
# instructions and footers can be added by calling methods on it.
|
1411
1437
|
def to_term(options = {})
|
1412
|
-
fmt = TermFormatter.new(self, options)
|
1438
|
+
fmt = TermFormatter.new(self, **options)
|
1413
1439
|
yield fmt if block_given?
|
1414
1440
|
fmt.output
|
1415
1441
|
end
|
@@ -1423,7 +1449,7 @@ module FatTable
|
|
1423
1449
|
# footers can be added by calling methods on it.
|
1424
1450
|
# @return [String]
|
1425
1451
|
def to_text(options = {})
|
1426
|
-
fmt = TextFormatter.new(self, options)
|
1452
|
+
fmt = TextFormatter.new(self, **options)
|
1427
1453
|
yield fmt if block_given?
|
1428
1454
|
fmt.output
|
1429
1455
|
end
|