fat_core 1.5.1 → 1.5.2
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.
- checksums.yaml +4 -4
- data/lib/fat_core/column.rb +4 -0
- data/lib/fat_core/table.rb +304 -150
- data/lib/fat_core/version.rb +1 -1
- data/spec/lib/table_spec.rb +215 -1
- metadata +2 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 4617dd301a8a4f6ea796c27cfff91364f72b79f2
|
4
|
+
data.tar.gz: e6eb1b4057994c9672712b28189066af2ebc56d8
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 478b2f046e8cfc211eaed97bcd20ddf80b9cbb7c38e022931ceaeade08ba91d39f94e4ad085145426b7789a3795648112149cc0e53256bce17c0cc6241fbee2c
|
7
|
+
data.tar.gz: 32a4dbf0406779b323a76f61319f9553b9442d8a5ebce0c8567720b3134c9894087b6f22d38720d08f0070884f11eb74d8655ec771b51c4ac8f8e47f6442975a
|
data/lib/fat_core/column.rb
CHANGED
data/lib/fat_core/table.rb
CHANGED
@@ -46,6 +46,7 @@ module FatCore
|
|
46
46
|
def initialize(input = nil, ext = '.csv')
|
47
47
|
@columns = []
|
48
48
|
@footers = {}
|
49
|
+
@boundaries = []
|
49
50
|
return self if input.nil?
|
50
51
|
case input
|
51
52
|
when IO, StringIO
|
@@ -121,6 +122,11 @@ module FatCore
|
|
121
122
|
columns.first.size
|
122
123
|
end
|
123
124
|
|
125
|
+
# Return whether this table is empty.
|
126
|
+
def empty?
|
127
|
+
size.zero?
|
128
|
+
end
|
129
|
+
|
124
130
|
# Return the rows of the table as an array of hashes, keyed by the headers.
|
125
131
|
def rows
|
126
132
|
rows = []
|
@@ -136,8 +142,121 @@ module FatCore
|
|
136
142
|
rows
|
137
143
|
end
|
138
144
|
|
139
|
-
|
140
|
-
|
145
|
+
protected
|
146
|
+
|
147
|
+
# Return the rows from first to last. We could just index #rows, but in a
|
148
|
+
# large table, that would require that we construct all the rows for a range
|
149
|
+
# of any size.
|
150
|
+
def rows_range(first = 0, last = size - 1)
|
151
|
+
raise ArgumentError, 'first must be <= last' unless first <= last
|
152
|
+
rows = []
|
153
|
+
unless columns.empty?
|
154
|
+
first.upto(last) do |rnum|
|
155
|
+
row = {}
|
156
|
+
columns.each do |col|
|
157
|
+
row[col.header] = col[rnum]
|
158
|
+
end
|
159
|
+
rows << row
|
160
|
+
end
|
161
|
+
end
|
162
|
+
rows
|
163
|
+
end
|
164
|
+
|
165
|
+
## ###########################################################################
|
166
|
+
## Group Boundaries
|
167
|
+
##
|
168
|
+
## Boundaries mark the last row in each "group" within the table. The last
|
169
|
+
## row of the table is always an implicit boundary, and having the last row
|
170
|
+
## as the sole boundary is the default for new tables unless mentioned
|
171
|
+
## otherwise. Resetting the boundaries means to put it back in that default
|
172
|
+
## state.
|
173
|
+
##
|
174
|
+
## Note that tables are for the most part, immutable. That is, the data
|
175
|
+
## rows of the table, once set, are never changed by methods on the
|
176
|
+
## table. Any transformation of a table results in a new table. Boundaries
|
177
|
+
## and footers are exceptions to immutability, but even they only affect
|
178
|
+
## the boundary and footer attributes of the table, not the data rows.
|
179
|
+
##
|
180
|
+
## Boundaries can be added when a table is read in, for example, from the
|
181
|
+
## text of an org table in which each hline (other than the one separating
|
182
|
+
## the headers from the body) marks a boundary for the row immediately
|
183
|
+
## preceding the hline.
|
184
|
+
##
|
185
|
+
## The #order_by method resets the boundaries then adds boundaries at the
|
186
|
+
## last row of each group as a boundary.
|
187
|
+
##
|
188
|
+
## The #union_all (but not #union since it deletes duplicates) method adds
|
189
|
+
## a boundary between the constituent tables. #union_all also preserves any
|
190
|
+
## boundary markers within the constituent tables. In doing so, the
|
191
|
+
## boundaries of the second table in the #union_all are increased by the
|
192
|
+
## size of the first table so that they refer to rows in the new table.
|
193
|
+
##
|
194
|
+
## The #select method preserves any boundaries from the parent table
|
195
|
+
## without change, since it only selects columns for the output and deletes
|
196
|
+
## no rows.
|
197
|
+
##
|
198
|
+
## All the other table-transforming methods reset the boundaries in the new
|
199
|
+
## table. For example, #order_by and #where re-arrange and delete rows, so
|
200
|
+
## the old boundaries would make no sense anyway. Likewise, #union,
|
201
|
+
## #intersection, #except, and #join reset the boundaries to their default.
|
202
|
+
## ###########################################################################
|
203
|
+
|
204
|
+
public
|
205
|
+
|
206
|
+
# Return an array of an array of row hashes for the groups in this Table.
|
207
|
+
def groups
|
208
|
+
normalize_boundaries
|
209
|
+
groups = []
|
210
|
+
(0..boundaries.size - 1).each do |k|
|
211
|
+
groups << group_rows(k)
|
212
|
+
end
|
213
|
+
groups
|
214
|
+
end
|
215
|
+
|
216
|
+
protected
|
217
|
+
|
218
|
+
# Reader for boundaries, but not public.
|
219
|
+
def boundaries
|
220
|
+
@boundaries
|
221
|
+
end
|
222
|
+
|
223
|
+
# Writer for boundaries, but not public.
|
224
|
+
def boundaries=(bounds)
|
225
|
+
@boundaries = bounds
|
226
|
+
end
|
227
|
+
|
228
|
+
# Make sure size - 1 is last boundary and that they are unique and sorted.
|
229
|
+
def normalize_boundaries
|
230
|
+
unless empty?
|
231
|
+
boundaries.push(size - 1) unless boundaries.include?(size - 1)
|
232
|
+
self.boundaries = boundaries.uniq.sort
|
233
|
+
end
|
234
|
+
boundaries
|
235
|
+
end
|
236
|
+
|
237
|
+
# Mark a boundary at k, and if k is nil, the last row in the table
|
238
|
+
# as a group boundary.
|
239
|
+
def mark_boundary(k = nil)
|
240
|
+
if k
|
241
|
+
boundaries.push(k)
|
242
|
+
else
|
243
|
+
boundaries.push(size - 1)
|
244
|
+
end
|
245
|
+
end
|
246
|
+
|
247
|
+
# Concatenate the array of argument bounds to this table's boundaries, but
|
248
|
+
# increase each of the indexes in bounds by shift. This is used in the
|
249
|
+
# #union_all method.
|
250
|
+
def append_boundaries(bounds, shift: 0)
|
251
|
+
@boundaries += bounds.map { |k| k + shift }
|
252
|
+
end
|
253
|
+
|
254
|
+
def group_rows(k)
|
255
|
+
normalize_boundaries
|
256
|
+
return [] unless k < boundaries.size
|
257
|
+
first = k.zero? ? 0 : boundaries[k - 1] + 1
|
258
|
+
last = boundaries[k]
|
259
|
+
rows_range(first, last)
|
141
260
|
end
|
142
261
|
|
143
262
|
############################################################################
|
@@ -145,9 +264,11 @@ module FatCore
|
|
145
264
|
# all return a new Table object rather than modifying the table in place.
|
146
265
|
############################################################################
|
147
266
|
|
267
|
+
public
|
268
|
+
|
148
269
|
# Return a new Table sorted on the rows of this Table on the possibly
|
149
270
|
# multiple keys given in the array of syms in headers. Append a ! to the
|
150
|
-
# symbol name to indicate reverse sorting on that column.
|
271
|
+
# symbol name to indicate reverse sorting on that column. Resets groups.
|
151
272
|
def order_by(*sort_heads)
|
152
273
|
sort_heads = [sort_heads].flatten
|
153
274
|
rev_heads = sort_heads.select { |h| h.to_s.ends_with?('!') }
|
@@ -158,65 +279,62 @@ module FatCore
|
|
158
279
|
key2 = sort_heads.map { |h| rev_heads.include?(h) ? r1[h] : r2[h] }
|
159
280
|
key1 <=> key2
|
160
281
|
end
|
282
|
+
# Add the new rows to the table, but mark a group boundary at the points
|
283
|
+
# where the sort key changes value.
|
161
284
|
new_tab = Table.new
|
162
|
-
|
285
|
+
last_key = nil
|
286
|
+
new_rows.each_with_index do |nrow, k|
|
163
287
|
new_tab << nrow
|
288
|
+
key = nrow.fetch_values(*sort_heads)
|
289
|
+
new_tab.mark_boundary(k - 1) if last_key && key != last_key
|
290
|
+
last_key = key
|
164
291
|
end
|
165
292
|
new_tab
|
166
293
|
end
|
167
294
|
|
168
295
|
# Return a Table having the selected column expressions. Each expression can
|
169
|
-
# be either a (1) symbol,
|
170
|
-
#
|
171
|
-
#
|
172
|
-
#
|
173
|
-
|
296
|
+
# be either a (1) symbol, :old_col, representing a column in the current
|
297
|
+
# table, (2) a hash of new_col: :old_col to rename an existing :old_col
|
298
|
+
# column as :new_col, or (3) a hash of new_col: 'expression', to add a new
|
299
|
+
# column that is computed as an arbitrary ruby expression of the existing
|
300
|
+
# columns (whether selected for the output table or not) or any new_col
|
301
|
+
# defined earlier in the argument list. The expression string can also
|
302
|
+
# access the instance variable @row as the row number of the row being
|
303
|
+
# evaluated. The bare symbol arguments (1) must precede any hash arguments
|
304
|
+
# (2) or (3). Each expression results in a column in the resulting Table in
|
305
|
+
# the order given. The expressions are evaluated in left-to-right order as
|
306
|
+
# well. The output table preserves any groups present in the input table.
|
307
|
+
def select(*cols, **new_cols)
|
174
308
|
result = Table.new
|
175
|
-
new_cols = {}
|
176
309
|
ev = Evaluator.new(vars: { row: 0 }, before: '@row += 1')
|
177
310
|
rows.each do |old_row|
|
178
|
-
|
179
|
-
|
180
|
-
|
181
|
-
|
182
|
-
|
183
|
-
|
184
|
-
|
185
|
-
|
186
|
-
|
187
|
-
|
188
|
-
|
189
|
-
#
|
190
|
-
|
191
|
-
|
192
|
-
|
193
|
-
case val
|
194
|
-
when Symbol
|
195
|
-
h = val.as_sym
|
196
|
-
raise "Column '#{h}' in select does not exist" unless vars.keys.include?(h)
|
197
|
-
new_row[key] = vars[h]
|
198
|
-
when String
|
199
|
-
# Now we have a hash, vars, of all local variables we want to be
|
200
|
-
# defined while evaluating expression xp as the value of column
|
201
|
-
# key in the new column.
|
202
|
-
h = key.as_sym
|
203
|
-
new_row[h] = ev.evaluate(val, vars: vars)
|
204
|
-
# Don't add this column to new_heads until after the eval so it
|
205
|
-
# does not shadow the existing value of row[h].
|
206
|
-
else
|
207
|
-
raise 'Hash parameters to select must be a symbol or string'
|
208
|
-
end
|
209
|
-
end
|
311
|
+
new_row = {}
|
312
|
+
cols.each do |k|
|
313
|
+
h = k.as_sym
|
314
|
+
raise "Column '#{h}' in select does not exist" unless column?(h)
|
315
|
+
new_row[h] = old_row[h]
|
316
|
+
end
|
317
|
+
new_cols.each_pair do |key, val|
|
318
|
+
key = key.as_sym
|
319
|
+
vars = old_row.merge(new_row)
|
320
|
+
case val
|
321
|
+
when Symbol
|
322
|
+
raise "Column '#{val}' in select does not exist" unless vars.keys.include?(val)
|
323
|
+
new_row[key] = vars[val]
|
324
|
+
when String
|
325
|
+
new_row[key] = ev.evaluate(val, vars: vars)
|
210
326
|
else
|
211
|
-
raise '
|
327
|
+
raise 'Hash parameters to select must be a symbol or string'
|
212
328
|
end
|
213
329
|
end
|
214
330
|
result << new_row
|
215
331
|
end
|
332
|
+
result.boundaries = boundaries
|
216
333
|
result
|
217
334
|
end
|
218
335
|
|
219
|
-
# Return a Table containing only rows matching the where expression.
|
336
|
+
# Return a Table containing only rows matching the where expression. Resets
|
337
|
+
# groups.
|
220
338
|
def where(expr)
|
221
339
|
expr = expr.to_s
|
222
340
|
result = Table.new
|
@@ -227,7 +345,7 @@ module FatCore
|
|
227
345
|
result
|
228
346
|
end
|
229
347
|
|
230
|
-
# Return this table with all duplicate rows eliminated.
|
348
|
+
# Return this table with all duplicate rows eliminated. Resets groups.
|
231
349
|
def distinct
|
232
350
|
result = Table.new
|
233
351
|
uniq_rows = rows.uniq
|
@@ -237,6 +355,7 @@ module FatCore
|
|
237
355
|
result
|
238
356
|
end
|
239
357
|
|
358
|
+
# Return this table with all duplicate rows eliminated. Resets groups.
|
240
359
|
def uniq
|
241
360
|
distinct
|
242
361
|
end
|
@@ -247,23 +366,31 @@ module FatCore
|
|
247
366
|
# the same type in the two tables, or an exception will be thrown.
|
248
367
|
# Duplicates are eliminated from the result.
|
249
368
|
def union(other)
|
250
|
-
set_operation(other, :+,
|
369
|
+
set_operation(other, :+,
|
370
|
+
distinct: true,
|
371
|
+
add_boundaries: true)
|
251
372
|
end
|
252
373
|
|
253
374
|
# Return a Table that combines this table with another table. In other
|
254
375
|
# words, return the union of this table with the other. The headers of this
|
255
376
|
# table are used in the result. There must be the same number of columns of
|
256
377
|
# the same type in the two tables, or an exception will be thrown.
|
257
|
-
# Duplicates are not eliminated from the result.
|
378
|
+
# Duplicates are not eliminated from the result. Adds group boundaries at
|
379
|
+
# boundaries of the constituent tables. Preserves and adjusts the group
|
380
|
+
# boundaries of the constituent table.
|
258
381
|
def union_all(other)
|
259
|
-
set_operation(other, :+,
|
382
|
+
set_operation(other, :+,
|
383
|
+
distinct: false,
|
384
|
+
add_boundaries: true,
|
385
|
+
inherit_boundaries: true)
|
260
386
|
end
|
261
387
|
|
262
388
|
# Return a Table that includes the rows that appear in this table and in
|
263
389
|
# another table. In other words, return the intersection of this table with
|
264
390
|
# the other. The headers of this table are used in the result. There must be
|
265
391
|
# the same number of columns of the same type in the two tables, or an
|
266
|
-
# exception will be thrown. Duplicates are eliminated from the
|
392
|
+
# exception will be thrown. Duplicates are eliminated from the
|
393
|
+
# result. Resets groups.
|
267
394
|
def intersect(other)
|
268
395
|
set_operation(other, :intersect, true)
|
269
396
|
end
|
@@ -272,7 +399,8 @@ module FatCore
|
|
272
399
|
# another table. In other words, return the intersection of this table with
|
273
400
|
# the other. The headers of this table are used in the result. There must be
|
274
401
|
# the same number of columns of the same type in the two tables, or an
|
275
|
-
# exception will be thrown. Duplicates are not eliminated from the
|
402
|
+
# exception will be thrown. Duplicates are not eliminated from the
|
403
|
+
# result. Resets groups.
|
276
404
|
def intersect_all(other)
|
277
405
|
set_operation(other, :intersect, false)
|
278
406
|
end
|
@@ -282,7 +410,7 @@ module FatCore
|
|
282
410
|
# set difference between this table an the other. The headers of this table
|
283
411
|
# are used in the result. There must be the same number of columns of the
|
284
412
|
# same type in the two tables, or an exception will be thrown. Duplicates
|
285
|
-
# are eliminated from the result.
|
413
|
+
# are eliminated from the result. Resets groups.
|
286
414
|
def except(other)
|
287
415
|
set_operation(other, :difference, true)
|
288
416
|
end
|
@@ -292,7 +420,7 @@ module FatCore
|
|
292
420
|
# set difference between this table an the other. The headers of this table
|
293
421
|
# are used in the result. There must be the same number of columns of the
|
294
422
|
# same type in the two tables, or an exception will be thrown. Duplicates
|
295
|
-
# are not eliminated from the result.
|
423
|
+
# are not eliminated from the result. Resets groups.
|
296
424
|
def except_all(other)
|
297
425
|
set_operation(other, :difference, false)
|
298
426
|
end
|
@@ -302,7 +430,10 @@ module FatCore
|
|
302
430
|
# Apply the set operation given by op between this table and the other table
|
303
431
|
# given in the first argument. If distinct is true, eliminate duplicates
|
304
432
|
# from the result.
|
305
|
-
def set_operation(other, op = :+,
|
433
|
+
def set_operation(other, op = :+,
|
434
|
+
distinct: true,
|
435
|
+
add_boundaries: false,
|
436
|
+
inherit_boundaries: false)
|
306
437
|
unless columns.size == other.columns.size
|
307
438
|
raise 'Cannot apply a set operation to tables with a different number of columns.'
|
308
439
|
end
|
@@ -312,8 +443,14 @@ module FatCore
|
|
312
443
|
other_rows = other.rows.map { |r| r.replace_keys(headers) }
|
313
444
|
result = Table.new
|
314
445
|
new_rows = rows.send(op, other_rows)
|
315
|
-
new_rows.
|
446
|
+
new_rows.each_with_index do |row, k|
|
316
447
|
result << row
|
448
|
+
result.mark_boundary if k == size - 1 && add_boundaries
|
449
|
+
end
|
450
|
+
if inherit_boundaries
|
451
|
+
result.boundaries = normalize_boundaries
|
452
|
+
other.normalize_boundaries
|
453
|
+
result.append_boundaries(other.boundaries, shift: size)
|
317
454
|
end
|
318
455
|
distinct ? result.distinct : result
|
319
456
|
end
|
@@ -381,7 +518,7 @@ module FatCore
|
|
381
518
|
# of all columns in T1 followed by all columns in T2. If the tables
|
382
519
|
# have N and M rows respectively, the joined table will have N * M
|
383
520
|
# rows.
|
384
|
-
#
|
521
|
+
# Resets groups.
|
385
522
|
JOIN_TYPES = [:inner, :left, :right, :full, :cross]
|
386
523
|
|
387
524
|
def join(other, *exps, join_type: :inner)
|
@@ -449,6 +586,73 @@ module FatCore
|
|
449
586
|
join(other, join_type: :cross)
|
450
587
|
end
|
451
588
|
|
589
|
+
# Return a Table with a single row for each group of rows in the input table
|
590
|
+
# where the value of all columns named as simple symbols are equal. All
|
591
|
+
# other columns are set to the result of aggregating the values of that
|
592
|
+
# column within the group according to a aggregate function (:count, :sum,
|
593
|
+
# :min, :max, etc.), which defaults to the :first function, giving the value
|
594
|
+
# of that column for the first row in the group. You can specify a
|
595
|
+
# different aggregate function for a column by adding a hash parameter with
|
596
|
+
# the column as the key and a symbol for the aggregate function as the
|
597
|
+
# value. For example, consider the following call:
|
598
|
+
#
|
599
|
+
# tab.group_by(:date, :code, :price, shares: :sum, ).
|
600
|
+
#
|
601
|
+
# The first three parameters are simple symbols, so the table is divided
|
602
|
+
# into groups of rows in which the value of :date, :code, and :price are
|
603
|
+
# equal. The shares: hash parameter is set to the aggregate function :sum,
|
604
|
+
# so it will appear in the result as the sum of all the :shares values in
|
605
|
+
# each group. Any non-aggregate columns that have no aggregate function set
|
606
|
+
# default to using the aggregate function :first. Because of the way Ruby
|
607
|
+
# parses parameters to a method call, all the grouping symbols must appear
|
608
|
+
# first in the parameter list before any hash parameters.
|
609
|
+
def group_by(*group_cols, **agg_cols)
|
610
|
+
default_agg_func = :first
|
611
|
+
default_cols = headers - group_cols - agg_cols.keys
|
612
|
+
default_cols.each do |h|
|
613
|
+
agg_cols[h] = default_agg_func
|
614
|
+
end
|
615
|
+
|
616
|
+
sorted_tab = order_by(group_cols)
|
617
|
+
groups = sorted_tab.rows.group_by do |r|
|
618
|
+
group_cols.map { |k| r[k] }
|
619
|
+
end
|
620
|
+
result = Table.new
|
621
|
+
groups.each_pair do |_vals, grp_rows|
|
622
|
+
result << row_from_group(grp_rows, group_cols, agg_cols)
|
623
|
+
end
|
624
|
+
result
|
625
|
+
end
|
626
|
+
|
627
|
+
############################################################################
|
628
|
+
# Footer methods
|
629
|
+
############################################################################
|
630
|
+
def add_footer(label: 'Total', aggregate: :sum, heads: [])
|
631
|
+
foot = {}
|
632
|
+
heads.each do |h|
|
633
|
+
raise "No #{h} column in table to #{aggregate}" unless headers.include?(h)
|
634
|
+
foot[h] = column(h).send(aggregate)
|
635
|
+
end
|
636
|
+
@footers[label.as_sym] = foot
|
637
|
+
self
|
638
|
+
end
|
639
|
+
|
640
|
+
def add_sum_footer(cols, label = 'Total')
|
641
|
+
add_footer(heads: cols)
|
642
|
+
end
|
643
|
+
|
644
|
+
def add_avg_footer(cols, label = 'Average')
|
645
|
+
add_footer(label: label, aggregate: :avg, heads: cols)
|
646
|
+
end
|
647
|
+
|
648
|
+
def add_min_footer(cols, label = 'Minimum')
|
649
|
+
add_footer(label: label, aggregate: :min, heads: cols)
|
650
|
+
end
|
651
|
+
|
652
|
+
def add_max_footer(cols, label = 'Maximum')
|
653
|
+
add_footer(label: label, aggregate: :max, heads: cols)
|
654
|
+
end
|
655
|
+
|
452
656
|
private
|
453
657
|
|
454
658
|
# Return an output row appropriate to the given join type, including all the
|
@@ -585,64 +789,6 @@ module FatCore
|
|
585
789
|
self
|
586
790
|
end
|
587
791
|
|
588
|
-
public
|
589
|
-
|
590
|
-
# Return a Table in which all rows of the table are divided into groups
|
591
|
-
# where the value of all columns named as simple symbols are equal. All
|
592
|
-
# other columns are set to the result of aggregating the values of that
|
593
|
-
# column within the group according to the Column aggregate function (:sum,
|
594
|
-
# :min, :max, etc.) set in a hash parameter with the non-aggregate column
|
595
|
-
# name as a key and the symbol for the aggregate function as a value. For
|
596
|
-
# example, consider the following call:
|
597
|
-
#
|
598
|
-
# #+BEGIN_EXAMPLE
|
599
|
-
# tab.group_by(:date, :code, :price, shares: :sum, ).
|
600
|
-
# #+END_EXAMPLE
|
601
|
-
#
|
602
|
-
# The first three parameters are simple symbols, so the table is divided
|
603
|
-
# into groups of rows in which the value of :date, :code, and :price are
|
604
|
-
# equal. The :shares parameter is set to the aggregate function :sum, so it
|
605
|
-
# will appear in the result as the sum of all the :shares values in each
|
606
|
-
# group. Any non-aggregate columns that have no aggregate function set
|
607
|
-
# default to using the aggregate function :first. Note that because of the
|
608
|
-
# way Ruby parses parameters to a method call, all the grouping symbols must
|
609
|
-
# appear first in the parameter list.
|
610
|
-
def group_by(*exprs)
|
611
|
-
group_cols = []
|
612
|
-
agg_cols = {}
|
613
|
-
exprs.each do |xp|
|
614
|
-
case xp
|
615
|
-
when Symbol
|
616
|
-
group_cols << xp
|
617
|
-
when Hash
|
618
|
-
agg_cols = xp
|
619
|
-
else
|
620
|
-
raise "Cannot group by parameter '#{xp}'"
|
621
|
-
end
|
622
|
-
end
|
623
|
-
default_agg_func = :first
|
624
|
-
default_cols = headers - group_cols - agg_cols.keys
|
625
|
-
default_cols.each do |h|
|
626
|
-
agg_cols[h] = default_agg_func
|
627
|
-
end
|
628
|
-
|
629
|
-
sorted_tab = order_by(group_cols)
|
630
|
-
groups = sorted_tab.rows.group_by do |r|
|
631
|
-
group_cols.map { |k| r[k] }
|
632
|
-
end
|
633
|
-
result_rows = []
|
634
|
-
groups.each_pair do |_vals, grp_rows|
|
635
|
-
result_rows << row_from_group(grp_rows, group_cols, agg_cols)
|
636
|
-
end
|
637
|
-
result = Table.new
|
638
|
-
result_rows.each do |row|
|
639
|
-
result << row
|
640
|
-
end
|
641
|
-
result
|
642
|
-
end
|
643
|
-
|
644
|
-
private
|
645
|
-
|
646
792
|
def row_from_group(rows, grp_cols, agg_cols)
|
647
793
|
new_row = {}
|
648
794
|
grp_cols.each do |h|
|
@@ -663,38 +809,14 @@ module FatCore
|
|
663
809
|
|
664
810
|
public
|
665
811
|
|
666
|
-
def add_footer(label: 'Total', aggregate: :sum, heads: [])
|
667
|
-
foot = {}
|
668
|
-
heads.each do |h|
|
669
|
-
raise "No #{h} column in table to #{aggregate}" unless headers.include?(h)
|
670
|
-
foot[h] = column(h).send(aggregate)
|
671
|
-
end
|
672
|
-
@footers[label.as_sym] = foot
|
673
|
-
self
|
674
|
-
end
|
675
|
-
|
676
|
-
def add_sum_footer(cols, label = 'Total')
|
677
|
-
add_footer(heads: cols)
|
678
|
-
end
|
679
|
-
|
680
|
-
def add_avg_footer(cols, label = 'Average')
|
681
|
-
add_footer(label: label, aggregate: :avg, heads: cols)
|
682
|
-
end
|
683
|
-
|
684
|
-
def add_min_footer(cols, label = 'Minimum')
|
685
|
-
add_footer(label: label, aggregate: :min, heads: cols)
|
686
|
-
end
|
687
|
-
|
688
|
-
def add_max_footer(cols, label = 'Maximum')
|
689
|
-
add_footer(label: label, aggregate: :max, heads: cols)
|
690
|
-
end
|
691
|
-
|
692
812
|
# This returns the table as an Array of Arrays with formatting applied.
|
693
813
|
# This would normally called after all calculations on the table are done
|
694
814
|
# and you want to return the results. The Array of Arrays structure is
|
695
815
|
# what org-mode src blocks will render as an org table in the buffer.
|
696
816
|
def to_org(formats: {})
|
697
817
|
result = []
|
818
|
+
|
819
|
+
# Headers
|
698
820
|
header_row = []
|
699
821
|
headers.each do |hdr|
|
700
822
|
header_row << hdr.entitle
|
@@ -703,6 +825,7 @@ module FatCore
|
|
703
825
|
# This causes org to place an hline under the header row
|
704
826
|
result << nil unless header_row.empty?
|
705
827
|
|
828
|
+
# Body
|
706
829
|
rows.each do |row|
|
707
830
|
out_row = []
|
708
831
|
headers.each do |hdr|
|
@@ -710,6 +833,8 @@ module FatCore
|
|
710
833
|
end
|
711
834
|
result << out_row
|
712
835
|
end
|
836
|
+
|
837
|
+
# Footers
|
713
838
|
footers.each_pair do |label, footer|
|
714
839
|
foot_row = []
|
715
840
|
columns.each do |col|
|
@@ -726,17 +851,20 @@ module FatCore
|
|
726
851
|
# Table construction methods.
|
727
852
|
############################################################################
|
728
853
|
|
729
|
-
# Add a row represented by a Hash having the headers as keys.
|
730
|
-
#
|
731
|
-
|
854
|
+
# Add a row represented by a Hash having the headers as keys. If mark is
|
855
|
+
# true, mark this row as a boundary. All tables should be built ultimately
|
856
|
+
# using this method as a primitive.
|
857
|
+
def add_row(row, mark: false)
|
732
858
|
row.each_pair do |k, v|
|
733
859
|
key = k.as_sym
|
734
860
|
columns << Column.new(header: k) unless column?(k)
|
735
861
|
column(key) << v
|
736
862
|
end
|
863
|
+
@boundaries << (size - 1) if mark
|
737
864
|
self
|
738
865
|
end
|
739
866
|
|
867
|
+
# Add a row without marking.
|
740
868
|
def <<(row)
|
741
869
|
add_row(row)
|
742
870
|
end
|
@@ -753,24 +881,41 @@ module FatCore
|
|
753
881
|
# respond to #to_hash.
|
754
882
|
def from_array_of_hashes(rows)
|
755
883
|
rows.each do |row|
|
884
|
+
if row.nil?
|
885
|
+
mark_boundary
|
886
|
+
next
|
887
|
+
end
|
756
888
|
add_row(row.to_hash)
|
757
889
|
end
|
758
890
|
self
|
759
891
|
end
|
760
892
|
|
893
|
+
# Construct a new table from an array of arrays. If the second element of
|
894
|
+
# the array is a nil, a string that looks like an hrule, or an array whose
|
895
|
+
# first element is a string that looks like an hrule, interpret the first
|
896
|
+
# element of the array as a row of headers. Otherwise, synthesize headers of
|
897
|
+
# the form "col1", "col2", ... and so forth. The remaining elements are
|
898
|
+
# taken as the body of the table, except that if an element of the outer
|
899
|
+
# array is a nil or a string that looks like an hrule, mark the preceding
|
900
|
+
# row as a boundary.
|
761
901
|
def from_array_of_arrays(rows)
|
902
|
+
hrule_re = /\A\s*\|[-+]+/
|
762
903
|
headers = []
|
763
|
-
if rows[
|
764
|
-
|
765
|
-
first_data_row = 0
|
766
|
-
else
|
904
|
+
if rows[1].nil? || rows[1] =~ hrule_re || rows[1].first =~ hrule_re
|
905
|
+
# Take the first row as headers
|
767
906
|
# Use first row 0 as headers
|
768
907
|
headers = rows[0].map(&:as_sym)
|
769
|
-
first_data_row =
|
908
|
+
first_data_row = 2
|
909
|
+
else
|
910
|
+
# Synthesize headers
|
911
|
+
headers = (1..rows[0].size).to_a.map { |k| "col#{k}".as_sym }
|
912
|
+
first_data_row = 0
|
770
913
|
end
|
771
|
-
hrule_re = /\A\s*\|[-+]+/
|
772
914
|
rows[first_data_row..-1].each do |row|
|
773
|
-
|
915
|
+
if row.nil? || row[0] =~ hrule_re
|
916
|
+
mark_boundary
|
917
|
+
next
|
918
|
+
end
|
774
919
|
row = row.map { |s| s.to_s.strip }
|
775
920
|
hash_row = Hash[headers.zip(row)]
|
776
921
|
add_row(hash_row)
|
@@ -797,18 +942,27 @@ module FatCore
|
|
797
942
|
unless table_found
|
798
943
|
# Skip through the file until a table is found
|
799
944
|
next unless line =~ table_re
|
945
|
+
unless line =~ hrule_re
|
946
|
+
line = line.sub(/\A\s*\|/, '').sub(/\|\s*\z/, '')
|
947
|
+
rows << line.split('|').map(&:clean)
|
948
|
+
end
|
800
949
|
table_found = true
|
950
|
+
next
|
801
951
|
end
|
802
952
|
break unless line =~ table_re
|
803
953
|
if !header_found && line =~ hrule_re
|
954
|
+
rows << nil
|
804
955
|
header_found = true
|
805
956
|
next
|
806
957
|
elsif header_found && line =~ hrule_re
|
958
|
+
# Mark the boundary with a nil
|
959
|
+
rows << nil
|
960
|
+
elsif line !~ table_re
|
807
961
|
# Stop reading at the second hline
|
808
962
|
break
|
809
963
|
else
|
810
964
|
line = line.sub(/\A\s*\|/, '').sub(/\|\s*\z/, '')
|
811
|
-
rows << line.split('|')
|
965
|
+
rows << line.split('|').map(&:clean)
|
812
966
|
end
|
813
967
|
end
|
814
968
|
from_array_of_arrays(rows)
|
data/lib/fat_core/version.rb
CHANGED
data/spec/lib/table_spec.rb
CHANGED
@@ -124,6 +124,33 @@ EOS
|
|
124
124
|
| 42 | 2013-05-30 | S | 6,679 | 18 | 25.04710 | ZMEAC |
|
125
125
|
|
126
126
|
* Another Heading
|
127
|
+
EOS
|
128
|
+
|
129
|
+
@org_file_body_with_groups = <<EOS
|
130
|
+
|
131
|
+
#+TBLNAME: morgan_tab
|
132
|
+
|-----+------------+------+---------+--------+----------+--------|
|
133
|
+
| Ref | Date | Code | Raw | Shares | Price | Info |
|
134
|
+
|-----+------------+------+---------+--------+----------+--------|
|
135
|
+
| 29 | 2013-05-02 | P | 795,546 | 2,609 | 1.18500 | ZMPEF1 |
|
136
|
+
|-----+------------+------+---------+--------+----------+--------|
|
137
|
+
| 30 | 2013-05-02 | P | 118,186 | 388 | 11.85000 | ZMPEF1 |
|
138
|
+
| 31 | 2013-05-02 | P | 340,948 | 1,926 | 1.18500 | ZMPEF2 |
|
139
|
+
| 32 | 2013-05-02 | P | 50,651 | 286 | 11.85000 | ZMPEF2 |
|
140
|
+
|-----+------------+------+---------+--------+----------+--------|
|
141
|
+
| 33 | 2013-05-20 | S | 12,000 | 32 | 28.28040 | ZMEAC |
|
142
|
+
| 34 | 2013-05-20 | S | 85,000 | 226 | 28.32240 | ZMEAC |
|
143
|
+
| 35 | 2013-05-20 | S | 33,302 | 88 | 28.63830 | ZMEAC |
|
144
|
+
| 36 | 2013-05-23 | S | 8,000 | 21 | 27.10830 | ZMEAC |
|
145
|
+
| 37 | 2013-05-23 | S | 23,054 | 61 | 26.80150 | ZMEAC |
|
146
|
+
| 38 | 2013-05-23 | S | 39,906 | 106 | 25.17490 | ZMEAC |
|
147
|
+
| 39 | 2013-05-29 | S | 13,459 | 36 | 24.74640 | ZMEAC |
|
148
|
+
|-----+------------+------+---------+--------+----------+--------|
|
149
|
+
| 40 | 2013-05-29 | S | 15,700 | 42 | 24.77900 | ZMEAC |
|
150
|
+
| 41 | 2013-05-29 | S | 15,900 | 42 | 24.58020 | ZMEAC |
|
151
|
+
| 42 | 2013-05-30 | S | 6,679 | 18 | 25.04710 | ZMEAC |
|
152
|
+
|-----+------------+------+---------+--------+----------+--------|
|
153
|
+
|
127
154
|
EOS
|
128
155
|
end
|
129
156
|
|
@@ -171,6 +198,28 @@ EOS
|
|
171
198
|
end
|
172
199
|
end
|
173
200
|
|
201
|
+
it 'should be create-able from an Org IO object with groups' do
|
202
|
+
tab = Table.new(StringIO.new(@org_file_body), '.org')
|
203
|
+
expect(tab.class).to eq(Table)
|
204
|
+
expect(tab.rows.size).to be > 10
|
205
|
+
expect(tab.headers.sort)
|
206
|
+
.to eq [:code, :date, :info, :price, :raw, :ref, :shares]
|
207
|
+
tab.rows.each do |row|
|
208
|
+
row.each_pair do |k, _v|
|
209
|
+
expect(k.class).to eq Symbol
|
210
|
+
end
|
211
|
+
expect(row[:code].class).to eq String
|
212
|
+
expect(row[:date].class).to eq Date
|
213
|
+
expect(row[:shares].is_a?(Numeric)).to be true
|
214
|
+
unless row[:rawshares].nil?
|
215
|
+
expect(row[:rawshares].is_a?(Numeric)).to be true
|
216
|
+
end
|
217
|
+
expect(row[:price].is_a?(BigDecimal)).to be true
|
218
|
+
expect([Numeric, String].any? { |t| row[:ref].is_a?(t) }).to be true
|
219
|
+
expect(row[:info].class).to eq String
|
220
|
+
end
|
221
|
+
end
|
222
|
+
|
174
223
|
it 'should be create-able from a CSV file' do
|
175
224
|
File.open('/tmp/junk.csv', 'w') { |f| f.write(@csv_file_body) }
|
176
225
|
tab = Table.new('/tmp/junk.csv')
|
@@ -241,9 +290,10 @@ EOS
|
|
241
290
|
end
|
242
291
|
end
|
243
292
|
|
244
|
-
it 'should be create-able from an Array of Arrays with header
|
293
|
+
it 'should be create-able from an Array of Arrays with nil-marked header' do
|
245
294
|
aoa = [
|
246
295
|
['First', 'Second', 'Third'],
|
296
|
+
nil,
|
247
297
|
['1', '2', '3.2'],
|
248
298
|
['4', '5', '6.4'],
|
249
299
|
['7', '8', '9.0'],
|
@@ -633,6 +683,7 @@ EOS
|
|
633
683
|
it 'should select by boolean columns' do
|
634
684
|
tab =
|
635
685
|
[['Ref', 'Date', 'Code', 'Raw', 'Shares', 'Price', 'Info', 'Bool'],
|
686
|
+
nil,
|
636
687
|
[1, '2013-05-02', 'P', 795_546.20, 795_546.2, 1.1850, 'ZMPEF1', 'T'],
|
637
688
|
[2, '2013-05-02', 'P', 118_186.40, 118_186.4, 11.8500, 'ZMPEF1', 'T'],
|
638
689
|
[7, '2013-05-20', 'S', 12_000.00, 5046.00, 28.2804, 'ZMEAC', 'F'],
|
@@ -770,6 +821,168 @@ EOS
|
|
770
821
|
end
|
771
822
|
end
|
772
823
|
|
824
|
+
describe 'group boundaries' do
|
825
|
+
before :all do
|
826
|
+
@tab_a = Table.new([
|
827
|
+
{ id: 1, name: 'Paul', age: 32, address: 'California', salary: 20000, join_date: '2001-07-13' },
|
828
|
+
{ id: 3, name: 'Teddy', age: 23, address: 'Norway', salary: 20000},
|
829
|
+
{ id: 4, name: 'Mark', age: 25, address: 'Rich-Mond', salary: 65000, join_date: '2007-12-13' },
|
830
|
+
{ id: 5, name: 'David', age: 27, address: 'Texas', salary: 85000, join_date: '2007-12-13' },
|
831
|
+
{ id: 2, name: 'Allen', age: 25, address: 'Texas', salary: nil, join_date: '2007-12-13' },
|
832
|
+
{ id: 8, name: 'Paul', age: 24, address: 'Houston', salary: 20000, join_date: '2005-07-13' },
|
833
|
+
{ id: 9, name: 'James', age: 44, address: 'Norway', salary: 5000, join_date: '2005-07-13' },
|
834
|
+
{ id: 10, name: 'James', age: 45, address: 'Texas', salary: 5000, join_date: '2005-07-13' }
|
835
|
+
])
|
836
|
+
# Union compatible with tab_a
|
837
|
+
@tab_a1 = Table.new([
|
838
|
+
{ id: 21, name: 'Paula', age: 23, address: 'Kansas', salary: 20000, join_date: '2001-07-13' },
|
839
|
+
{ id: 23, name: 'Jenny', age: 32, address: 'Missouri', salary: 20000},
|
840
|
+
{ id: 24, name: 'Forrest', age: 52, address: 'Richmond', salary: 65000, join_date: '2007-12-13' },
|
841
|
+
{ id: 25, name: 'Syrano', age: 72, address: 'Nebraska', salary: 85000, join_date: '2007-12-13' },
|
842
|
+
# Next four are the same as row as in @tab_a
|
843
|
+
{ id: 2, name: 'Allen', age: 25, address: 'Texas', salary: nil, join_date: '2007-12-13' },
|
844
|
+
{ id: 8, name: 'Paul', age: 24, address: 'Houston', salary: 20000, join_date: '2005-07-13' },
|
845
|
+
{ id: 9, name: 'James', age: 44, address: 'Norway', salary: 5000, join_date: '2005-07-13' },
|
846
|
+
{ id: 10, name: 'James', age: 45, address: 'Texas', salary: 5000, join_date: '2005-07-13' },
|
847
|
+
{ id: 22, name: 'Paula', age: 52, address: 'Iowa', salary: nil, join_date: '2007-12-13' },
|
848
|
+
{ id: 28, name: 'Paula', age: 42, address: 'Oklahoma', salary: 20000, join_date: '2005-07-13' },
|
849
|
+
{ id: 29, name: 'Patrick', age: 44, address: 'Lindsbourg', salary: 5000, join_date: '2005-07-13' },
|
850
|
+
{ id: 30, name: 'James', age: 54, address: 'Ottawa', salary: 5000, join_date: '2005-07-13' }
|
851
|
+
])
|
852
|
+
@tab_b = Table.new([
|
853
|
+
{ id: 1, dept: 'IT Billing', emp_id: 1 },
|
854
|
+
{ id: 2, dept: 'Engineering', emp_id: 2 },
|
855
|
+
{ id: 3, dept: 'Finance', emp_id: 7 }
|
856
|
+
])
|
857
|
+
@aoa =
|
858
|
+
[['Ref', 'Date', 'Code', 'Raw', 'Shares', 'Price', 'Info', 'Bool'],
|
859
|
+
nil,
|
860
|
+
[1, '2013-05-02', 'P', 795_546.20, 795_546.2, 1.1850, 'ZMPEF1', 'T'],
|
861
|
+
nil,
|
862
|
+
[2, '2013-05-02', 'P', 118_186.40, 118_186.4, 11.8500, 'ZMPEF1', 'T'],
|
863
|
+
[7, '2013-05-20', 'S', 12_000.00, 5046.00, 28.2804, 'ZMEAC', 'F'],
|
864
|
+
[8, '2013-05-20', 'S', 85_000.00, 35_742.50, 28.3224, 'ZMEAC', 'T'],
|
865
|
+
nil,
|
866
|
+
[9, '2013-05-20', 'S', 33_302.00, 14_003.49, 28.6383, 'ZMEAC', 'T'],
|
867
|
+
[10, '2013-05-23', 'S', 8000.00, 3364.00, 27.1083, 'ZMEAC', 'T'],
|
868
|
+
[11, '2013-05-23', 'S', 23_054.00, 9694.21, 26.8015, 'ZMEAC', 'F'],
|
869
|
+
[12, '2013-05-23', 'S', 39_906.00, 16_780.47, 25.1749, 'ZMEAC', 'T'],
|
870
|
+
[13, '2013-05-29', 'S', 13_459.00, 5659.51, 24.7464, 'ZMEAC', 'T'],
|
871
|
+
[14, '2013-05-29', 'S', 15_700.00, 6601.85, 24.7790, 'ZMEAC', 'F'],
|
872
|
+
[15, '2013-05-29', 'S', 15_900.00, 6685.95, 24.5802, 'ZMEAC', 'T'],
|
873
|
+
nil,
|
874
|
+
[16, '2013-05-30', 'S', 6_679.00, 2808.52, 25.0471, 'ZMEAC', 'T']]
|
875
|
+
@aoh = [
|
876
|
+
{ id: 1, name: 'Paul', age: 32, address: 'California', salary: 20000, join_date: '2001-07-13' },
|
877
|
+
nil,
|
878
|
+
{ id: 3, name: 'Teddy', age: 23, address: 'Norway', salary: 20000},
|
879
|
+
{ id: 4, name: 'Mark', age: 25, address: 'Rich-Mond', salary: 65000, join_date: '2007-12-13' },
|
880
|
+
{ id: 5, name: 'David', age: 27, address: 'Texas', salary: 85000, join_date: '2007-12-13' },
|
881
|
+
nil,
|
882
|
+
{ id: 2, name: 'Allen', age: 25, address: 'Texas', salary: nil, join_date: '2007-12-13' },
|
883
|
+
{ id: 8, name: 'Paul', age: 24, address: 'Houston', salary: 20000, join_date: '2005-07-13' },
|
884
|
+
{ id: 9, name: 'James', age: 44, address: 'Norway', salary: 5000, join_date: '2005-07-13' },
|
885
|
+
nil,
|
886
|
+
{ id: 10, name: 'James', age: 45, address: 'Texas', salary: 5000, join_date: '2005-07-13' }
|
887
|
+
]
|
888
|
+
end
|
889
|
+
|
890
|
+
it 'an empty table should have no groups' do
|
891
|
+
expect(Table.new.groups.size).to eq(0)
|
892
|
+
end
|
893
|
+
|
894
|
+
it 'default group boundaries of whole table' do
|
895
|
+
expect(@tab_a.groups.size).to eq(1)
|
896
|
+
end
|
897
|
+
|
898
|
+
it 'add group boundaries on reading from org text' do
|
899
|
+
tab = Table.new(StringIO.new(@org_file_body_with_groups), '.org')
|
900
|
+
expect(tab.groups.size).to eq(4)
|
901
|
+
expect(tab.groups[0].size).to eq(1)
|
902
|
+
expect(tab.groups[1].size).to eq(3)
|
903
|
+
expect(tab.groups[2].size).to eq(7)
|
904
|
+
expect(tab.groups[3].size).to eq(3)
|
905
|
+
end
|
906
|
+
|
907
|
+
it 'add group boundaries on reading from aoa' do
|
908
|
+
tab = Table.new(@aoa)
|
909
|
+
expect(tab.groups.size).to eq(4)
|
910
|
+
expect(tab.groups[0].size).to eq(1)
|
911
|
+
expect(tab.groups[1].size).to eq(3)
|
912
|
+
expect(tab.groups[2].size).to eq(7)
|
913
|
+
expect(tab.groups[3].size).to eq(1)
|
914
|
+
end
|
915
|
+
|
916
|
+
it 'add group boundaries on reading from aoh' do
|
917
|
+
tab = Table.new(@aoh)
|
918
|
+
expect(tab.groups.size).to eq(4)
|
919
|
+
expect(tab.groups[0].size).to eq(1)
|
920
|
+
expect(tab.groups[1].size).to eq(3)
|
921
|
+
expect(tab.groups[2].size).to eq(3)
|
922
|
+
expect(tab.groups[3].size).to eq(1)
|
923
|
+
end
|
924
|
+
|
925
|
+
it 'add group boundaries on order_by' do
|
926
|
+
tab = @tab_a.order_by(:name)
|
927
|
+
# Now the table is ordered by name, and the names are: Allen, David,
|
928
|
+
# James, James, Mark, Paul, Paul, Teddy. So there are groups of size 1,
|
929
|
+
# 1, 2, 1, 2, and 1. Six groups in all.
|
930
|
+
expect(tab.groups.size).to eq(6)
|
931
|
+
expect(tab.groups[0].size).to eq(1)
|
932
|
+
expect(tab.groups[1].size).to eq(1)
|
933
|
+
expect(tab.groups[2].size).to eq(2)
|
934
|
+
tab.groups[2].each do |row|
|
935
|
+
expect(row[:name]).to eq('James')
|
936
|
+
end
|
937
|
+
expect(tab.groups[3].size).to eq(1)
|
938
|
+
expect(tab.groups[4].size).to eq(2)
|
939
|
+
tab.groups[4].each do |row|
|
940
|
+
expect(row[:name]).to eq('Paul')
|
941
|
+
end
|
942
|
+
expect(tab.groups[5].size).to eq(1)
|
943
|
+
end
|
944
|
+
|
945
|
+
it 'add group boundaries on union_all' do
|
946
|
+
tab = @tab_a.union_all(@tab_a1)
|
947
|
+
expect(tab.size).to eq(20)
|
948
|
+
expect(tab.groups.size).to eq(2)
|
949
|
+
expect(tab.groups[0].size).to eq(8)
|
950
|
+
expect(tab.groups[1].size).to eq(12)
|
951
|
+
end
|
952
|
+
|
953
|
+
it 'inherit group boundaries on union_all' do
|
954
|
+
tab1 = @tab_a.order_by(:name)
|
955
|
+
tab2 = @tab_a1.order_by(:name)
|
956
|
+
tab = tab1.union_all(tab2)
|
957
|
+
expect(tab.size).to eq(20)
|
958
|
+
expect(tab.groups.size).to eq(tab1.groups.size + tab2.groups.size)
|
959
|
+
tab.groups.each do |grp|
|
960
|
+
names = grp.map {|r| r[:name]}
|
961
|
+
expect(names.uniq.size).to eq(1)
|
962
|
+
end
|
963
|
+
end
|
964
|
+
|
965
|
+
it 'inherit group boundaries on select' do
|
966
|
+
tab = @tab_a.order_by(:name).select(:name, :age, :join_date)
|
967
|
+
# Now the table is ordered by name, and the names are: Allen, David,
|
968
|
+
# James, James, Mark, Paul, Paul, Teddy. So there are groups of size 1,
|
969
|
+
# 1, 2, 1, 2, and 1. Six groups in all.
|
970
|
+
expect(tab.groups.size).to eq(6)
|
971
|
+
expect(tab.groups[0].size).to eq(1)
|
972
|
+
expect(tab.groups[1].size).to eq(1)
|
973
|
+
expect(tab.groups[2].size).to eq(2)
|
974
|
+
tab.groups[2].each do |row|
|
975
|
+
expect(row[:name]).to eq('James')
|
976
|
+
end
|
977
|
+
expect(tab.groups[3].size).to eq(1)
|
978
|
+
expect(tab.groups[4].size).to eq(2)
|
979
|
+
tab.groups[4].each do |row|
|
980
|
+
expect(row[:name]).to eq('Paul')
|
981
|
+
end
|
982
|
+
expect(tab.groups[5].size).to eq(1)
|
983
|
+
end
|
984
|
+
end
|
985
|
+
|
773
986
|
describe 'output' do
|
774
987
|
it 'should be able to return itself as an array of arrays' do
|
775
988
|
aoh = [
|
@@ -789,6 +1002,7 @@ EOS
|
|
789
1002
|
# blocks.
|
790
1003
|
tab =
|
791
1004
|
[['Ref', 'Date', 'Code', 'Raw', 'Shares', 'Price', 'Info', 'Bool'],
|
1005
|
+
nil,
|
792
1006
|
[1, '2013-05-02', 'P', 795_546.20, 795_546.2, 1.1850, 'ZMPEF1', 'T'],
|
793
1007
|
[2, '2013-05-02', 'P', 118_186.40, 118_186.4, 11.8500, 'ZMPEF1', 'T'],
|
794
1008
|
[7, '2013-05-20', 'S', 12_000.00, 5046.00, 28.2804, 'ZMEAC', 'F'],
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: fat_core
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.5.
|
4
|
+
version: 1.5.2
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Daniel E. Doherty
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2017-03-
|
11
|
+
date: 2017-03-07 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: simplecov
|