object_table 0.3.4 → 0.4.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.travis.yml +0 -1
- data/README.md +206 -108
- data/lib/object_table/basic_grid.rb +1 -1
- data/lib/object_table/column.rb +6 -7
- data/lib/object_table/factory.rb +46 -0
- data/lib/object_table/grouping/grid.rb +47 -0
- data/lib/object_table/grouping.rb +109 -0
- data/lib/object_table/joining.rb +71 -0
- data/lib/object_table/masked_column.rb +2 -2
- data/lib/object_table/printing.rb +69 -0
- data/lib/object_table/stacking.rb +66 -0
- data/lib/object_table/static_view.rb +2 -5
- data/lib/object_table/table_methods.rb +35 -22
- data/lib/object_table/util.rb +19 -0
- data/lib/object_table/version.rb +1 -1
- data/lib/object_table/view.rb +7 -5
- data/lib/object_table/view_methods.rb +3 -2
- data/lib/object_table.rb +8 -19
- data/object_table.gemspec +2 -0
- data/spec/object_table/column_spec.rb +2 -2
- data/spec/object_table/grouping_spec.rb +475 -0
- data/spec/object_table/static_view_spec.rb +2 -2
- data/spec/object_table/util_spec.rb +43 -0
- data/spec/object_table/view_spec.rb +6 -16
- data/spec/object_table_spec.rb +45 -3
- data/spec/subclassing_spec.rb +44 -5
- data/spec/support/joining_example.rb +171 -0
- data/spec/support/object_table_example.rb +124 -29
- data/spec/support/stacking_example.rb +111 -0
- data/spec/support/utils.rb +8 -0
- data/spec/support/view_example.rb +10 -13
- metadata +20 -12
- data/lib/object_table/group.rb +0 -10
- data/lib/object_table/grouped.rb +0 -93
- data/lib/object_table/printable.rb +0 -72
- data/lib/object_table/stacker.rb +0 -59
- data/lib/object_table/table_child.rb +0 -19
- data/spec/object_table/grouped_spec.rb +0 -351
- data/spec/support/stacker_example.rb +0 -158
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 02f7e1642a2f1f8106f32e3b04f8cfebf8676d52
|
4
|
+
data.tar.gz: 0986af221eab29e6654f511cebd5534962d82412
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: d8ee1c4350da59156c2f17e23431969d126a5f8bac0a21348d048fb18486c765d1fe6f4f551a53ef18e03a9642e0704da93e997bb16c2d700003f5cde4f64079
|
7
|
+
data.tar.gz: d15636d515d1d5439f5233e58738b5ffdb273a4df77000a4fbdc5ba6ff5018e16e382eb487c6aa0d27e6a9b390e1d6b551a3010ca843123a227c4ccbdee0278d
|
data/.travis.yml
CHANGED
data/README.md
CHANGED
@@ -1,25 +1,17 @@
|
|
1
1
|
ruby-object-table
|
2
2
|
=================
|
3
3
|
|
4
|
-
[![Gem Version]
|
5
|
-
[![Build Status]
|
6
|
-
[![Code Climate]
|
7
|
-
[![Coverage Status]
|
8
|
-
|
9
|
-
|
10
|
-
[Build Status]: https://travis-ci.org/lincheney/ruby-object-table
|
11
|
-
[Code Climate]: https://codeclimate.com/github/lincheney/ruby-object-table
|
12
|
-
[Coverage Status]: https://coveralls.io/r/lincheney/ruby-object-table
|
13
|
-
|
14
|
-
[GV img]: https://badge.fury.io/rb/object_table.png
|
15
|
-
[BS img]: https://travis-ci.org/lincheney/ruby-object-table.png
|
16
|
-
[CC img]: https://codeclimate.com/github/lincheney/ruby-object-table.png
|
17
|
-
[CS img]: https://coveralls.io/repos/lincheney/ruby-object-table/badge.png?branch=master
|
18
|
-
|
19
|
-
Simple data table/frame implementation in ruby
|
4
|
+
[![Gem Version](https://badge.fury.io/rb/object_table.svg)](http://badge.fury.io/rb/object_table)
|
5
|
+
[![Build Status](https://travis-ci.org/lincheney/ruby-object-table.svg?branch=master)](https://travis-ci.org/lincheney/ruby-object-table)
|
6
|
+
[![Code Climate](https://codeclimate.com/github/lincheney/ruby-object-table/badges/gpa.svg)](https://codeclimate.com/github/lincheney/ruby-object-table)
|
7
|
+
[![Coverage Status](https://coveralls.io/repos/lincheney/ruby-object-table/badge.svg?branch=master)](https://coveralls.io/r/lincheney/ruby-object-table?branch=master)
|
8
|
+
|
9
|
+
Simple data table/frame implementation in ruby.
|
20
10
|
Probably slow and extremely inefficient, but it works and that's all that matters.
|
21
11
|
Uses NArrays (https://github.com/masa16/narray) for storing data.
|
22
12
|
|
13
|
+
Be sure to check out the [release notes](https://github.com/lincheney/ruby-object-table/releases).
|
14
|
+
|
23
15
|
## Creating a table
|
24
16
|
|
25
17
|
Just pass a hash of columns into the constructor.
|
@@ -69,7 +61,7 @@ Otherwise the scalars are extended to match the length of the vector columns
|
|
69
61
|
- `#nrows` returns the number of rows
|
70
62
|
- `#colnames` returns an array of the column names
|
71
63
|
- `#clone` make a copy of the table
|
72
|
-
- `#stack(table1, table2, ...)` appends
|
64
|
+
- `#stack(table1, table2, ...)` appends the supplied tables
|
73
65
|
- `#apply(&block)` evaluates `block` in the context of the table
|
74
66
|
- `#where(&block)` filters the table
|
75
67
|
- `#group_by(&block)` splits the table into groups
|
@@ -369,104 +361,223 @@ If you want to filter a table and keep that data (i.e. without it syncing with t
|
|
369
361
|
## Grouping (and aggregating)
|
370
362
|
|
371
363
|
Use the `#group_by` method and pass column names or a block that returns grouping keys.
|
372
|
-
Then call `#each` to iterate through the groups or `#apply` to aggregate the results.
|
373
364
|
|
374
|
-
|
365
|
+
```ruby
|
366
|
+
# group by column_1
|
367
|
+
>>> data.group_by(:column_1)
|
368
|
+
# or group by a dynamically calculated value
|
369
|
+
# note the double braces is actually a hash inside a block
|
370
|
+
>>> data.group_by{{ key: column_1.round }}
|
371
|
+
```
|
372
|
+
|
373
|
+
This gives you a `ObjectTable::Grouping`.
|
374
|
+
There are two ways to perform aggregation with a grouping: using `apply`/`each` or using `reduce`.
|
375
|
+
|
376
|
+
Using `apply`/`each` is the most flexible and powerful.
|
377
|
+
It iterates through each group and calls a supplied block for each group.
|
378
|
+
|
379
|
+
`reduce` instead iterates through each *row* and keeps track of which group the row belongs to.
|
380
|
+
It can only be used with (online algorithms)[http://en.wikipedia.org/wiki/Online_algorithm]
|
381
|
+
but can be much faster if there is a large number of groups (relative to the number of rows).
|
382
|
+
|
383
|
+
### Using `apply`/`each`
|
384
|
+
|
385
|
+
`each` enumerates through the groups.
|
386
|
+
`apply` is similar to doing `grouping.each.map` but instead of collecting results in an `Array`
|
387
|
+
the results are stacked into a new table.
|
375
388
|
|
376
389
|
```ruby
|
377
|
-
|
378
|
-
|
379
|
-
|
380
|
-
|
381
|
-
|
382
|
-
|
383
|
-
|
384
|
-
|
385
|
-
|
386
|
-
|
387
|
-
|
388
|
-
|
389
|
-
|
390
|
-
|
391
|
-
|
392
|
-
|
393
|
-
#
|
394
|
-
>>>
|
395
|
-
|
396
|
-
|
397
|
-
|
398
|
-
|
390
|
+
# let's create some data
|
391
|
+
>>> data = ObjectTable.new(col1: 1..10, col2: (1..20).step(2).to_a)
|
392
|
+
=> ObjectTable(10, 2)
|
393
|
+
col1 col2
|
394
|
+
0: 1 1
|
395
|
+
1: 2 3
|
396
|
+
2: 3 5
|
397
|
+
3: 4 7
|
398
|
+
4: 5 9
|
399
|
+
5: 6 11
|
400
|
+
6: 7 13
|
401
|
+
7: 8 15
|
402
|
+
8: 9 17
|
403
|
+
9: 10 19
|
404
|
+
col1 col2
|
405
|
+
|
406
|
+
# print sum of col2 for col1 remainder 3
|
407
|
+
>>> data.group_by{{ rem: col1 % 3 }}.each{ p col2.sum }; nil
|
408
|
+
40
|
409
|
+
27
|
410
|
+
33
|
411
|
+
|
412
|
+
# which sum is which group?
|
413
|
+
# we can access the group keys through @K
|
414
|
+
>>> data.group_by{{ rem: col1 > 0 }}.each{ p [@K.rem, col2.sum] }; nil
|
415
|
+
[1, 40]
|
416
|
+
[2, 27]
|
417
|
+
[0, 33]
|
418
|
+
|
419
|
+
# collect results into an array
|
420
|
+
# note that we need an argument to the map block
|
421
|
+
>>> data.group_by{{ rem: col1 % 3 }}.each.map{|grp| [grp.K.rem, grp.col2.sum] }
|
422
|
+
=> [[1, 40], [2, 27], [0, 33]]
|
423
|
+
|
424
|
+
# collect the results into a new table using apply()
|
425
|
+
>>> data.group_by{{ rem: col1 % 3 }}.apply{ col2.sum }
|
426
|
+
=> ObjectTable(3, 2)
|
427
|
+
rem v_0
|
428
|
+
0: 1 40
|
429
|
+
1: 2 27
|
430
|
+
2: 0 33
|
431
|
+
rem v_0
|
432
|
+
|
433
|
+
# aggregated columns are given default names of v_0, v_1, etc.
|
434
|
+
# let's set the names ourselves
|
435
|
+
>>> data.group_by{{ rem: col1 % 3 }}.apply{ @R[sum: col2.sum] }
|
436
|
+
=> ObjectTable(3, 2)
|
437
|
+
rem sum
|
438
|
+
0: 1 40
|
439
|
+
1: 2 27
|
440
|
+
2: 0 33
|
441
|
+
rem sum
|
399
442
|
```
|
400
443
|
|
401
|
-
|
444
|
+
We can also assign new columns based on the group (you cannot do this with `reduce`).
|
402
445
|
|
403
446
|
```ruby
|
404
|
-
>>> data
|
405
|
-
>>> data
|
406
|
-
|
407
|
-
|
408
|
-
|
409
|
-
|
410
|
-
|
411
|
-
|
412
|
-
|
413
|
-
|
414
|
-
|
447
|
+
>>> data.group_by{{ rem: col1 % 3 }}.each{ self[:sum] = col2.sum }
|
448
|
+
>>> data
|
449
|
+
=> ObjectTable(10, 3)
|
450
|
+
col1 col2 sum
|
451
|
+
0: 1 1 40
|
452
|
+
1: 2 3 27
|
453
|
+
2: 3 5 33
|
454
|
+
3: 4 7 40
|
455
|
+
4: 5 9 27
|
456
|
+
5: 6 11 33
|
457
|
+
6: 7 13 40
|
458
|
+
7: 8 15 27
|
459
|
+
8: 9 17 33
|
460
|
+
9: 10 19 40
|
461
|
+
col1 col2 sum
|
415
462
|
```
|
416
463
|
|
464
|
+
### Using `reduce`
|
417
465
|
|
418
|
-
|
466
|
+
`reduce` returns a new table like `apply`
|
467
|
+
(and there is no equivalent for `each`, i.e. iterating through groups).
|
419
468
|
|
420
|
-
|
469
|
+
Pass a block to `reduce`; you will have access to the `@R` variable
|
470
|
+
which is a group-specific hash where you can accumulate results.
|
471
|
+
See the examples below.
|
421
472
|
|
422
473
|
```ruby
|
423
|
-
|
424
|
-
>>> data.group_by
|
474
|
+
# sum of column 2
|
475
|
+
>>> data.group_by{{ rem: col1 % 3 }}.reduce{ @R[:sum] += col2 }
|
476
|
+
=> ObjectTable(3, 2)
|
477
|
+
rem sum
|
478
|
+
0: 1 40
|
479
|
+
1: 2 27
|
480
|
+
2: 0 33
|
481
|
+
rem sum
|
482
|
+
|
483
|
+
# we can supply initial values, e.g. if we wish to calculate product
|
484
|
+
>>> data.group_by{{ rem: col1 % 3 }}.reduce(prod: 1){ @R[:prod] *= col2 }
|
425
485
|
=> ObjectTable(3, 2)
|
426
|
-
|
427
|
-
0:
|
428
|
-
1:
|
429
|
-
2:
|
430
|
-
|
486
|
+
rem prod
|
487
|
+
0: 1 1729
|
488
|
+
1: 2 405
|
489
|
+
2: 0 935
|
490
|
+
rem prod
|
491
|
+
```
|
492
|
+
|
493
|
+
You should avoid reduce unless your aggregating operation is simply
|
494
|
+
and you have a relatively large number of groups
|
495
|
+
(`reduce` is slower than `apply` with few groups).
|
496
|
+
|
497
|
+
### Comparison of `apply` and `reduce`
|
498
|
+
|
499
|
+
The `reduce` version is more complicated because we must implement the
|
500
|
+
online algorithm ourselves.
|
501
|
+
|
502
|
+
#### Sum
|
503
|
+
|
504
|
+
```ruby
|
505
|
+
>>> data.group_by{{ rem: col1 % 3 }}.apply{ @R[sum: col2.sum] }
|
506
|
+
>>> data.group_by{{ rem: col1 % 3 }}.reduce{ @R[:sum] += col2 }
|
431
507
|
```
|
432
508
|
|
433
|
-
|
434
|
-
You can have more columns and set column names by making a `ObjectTable` or using the @R shortcut.
|
509
|
+
#### Product
|
435
510
|
|
436
511
|
```ruby
|
437
|
-
>>> data.group_by
|
438
|
-
|
439
|
-
|
440
|
-
|
441
|
-
|
442
|
-
|
443
|
-
|
444
|
-
|
445
|
-
|
446
|
-
|
447
|
-
|
448
|
-
|
449
|
-
|
450
|
-
|
451
|
-
|
452
|
-
|
512
|
+
>>> data.group_by{{ rem: col1 % 3 }}.apply{ @R[prod: col2.prod] }
|
513
|
+
>>> data.group_by{{ rem: col1 % 3 }}.reduce(prod: 1){ @R[:prod] *= col2 }
|
514
|
+
```
|
515
|
+
|
516
|
+
#### Variance
|
517
|
+
|
518
|
+
Online algorithm for variance taken from:
|
519
|
+
http://en.wikipedia.org/wiki/Algorithms_for_calculating_variance#Online_algorithm
|
520
|
+
|
521
|
+
```ruby
|
522
|
+
>>> data.group_by{{ rem: col1 % 3 }}.apply{ @R[var: col2.stddev**2] }
|
523
|
+
>>> data.group_by{{ rem: col1 % 3 }}.reduce(n: 0, mean: 0.0, m2: 0) do
|
524
|
+
@R[:n] += 1
|
525
|
+
delta = col2 - @R[:mean]
|
526
|
+
@R[:mean] += delta / @R[:n]
|
527
|
+
@R[:m2] += delta * (col2 - @R[:mean])
|
528
|
+
end.apply{ @R[rem: rem, variance: m2 / (n - 1)] }
|
453
529
|
```
|
454
530
|
|
455
|
-
|
531
|
+
## Joining
|
456
532
|
|
457
|
-
|
533
|
+
Note the current joining algorithm is quite slow.
|
458
534
|
|
459
535
|
```ruby
|
460
|
-
#
|
461
|
-
>>>
|
536
|
+
# let's create some data
|
537
|
+
>>> left = ObjectTable.new( key: [1, 2, 3, 5, 7], val_1: 1..5 )
|
538
|
+
>>> right = ObjectTable.new( key: [2, 3, 4, 5], val_2: 'a'..'d')
|
539
|
+
|
540
|
+
# inner join
|
541
|
+
>>> left.join(right, :key)
|
542
|
+
=> ObjectTable(3, 3)
|
543
|
+
key val_1 val_2
|
544
|
+
0: 2 2 "a"
|
545
|
+
1: 3 3 "b"
|
546
|
+
2: 5 4 "d"
|
547
|
+
key val_1 val_2
|
548
|
+
|
549
|
+
# left join
|
550
|
+
>>> left.join(right, :key, type: 'left')
|
462
551
|
=> ObjectTable(5, 3)
|
463
|
-
|
464
|
-
0:
|
465
|
-
1:
|
466
|
-
2:
|
467
|
-
3:
|
468
|
-
4:
|
469
|
-
|
552
|
+
key val_1 val_2
|
553
|
+
0: 1 1 nil
|
554
|
+
1: 2 2 "a"
|
555
|
+
2: 3 3 "b"
|
556
|
+
3: 5 4 "d"
|
557
|
+
4: 7 5 nil
|
558
|
+
key val_1 val_2
|
559
|
+
|
560
|
+
# right join
|
561
|
+
>>> left.join(right, :key, type: 'right')
|
562
|
+
=> ObjectTable(4, 3)
|
563
|
+
key val_1 val_2
|
564
|
+
0: 2 2 "a"
|
565
|
+
1: 3 3 "b"
|
566
|
+
2: 5 4 "d"
|
567
|
+
3: 4 0 "c"
|
568
|
+
key val_1 val_2
|
569
|
+
|
570
|
+
# outer join
|
571
|
+
>>> left.join(right, :key, type: 'outer')
|
572
|
+
=> ObjectTable(6, 3)
|
573
|
+
key val_1 val_2
|
574
|
+
0: 1 1 nil
|
575
|
+
1: 2 2 "a"
|
576
|
+
2: 3 3 "b"
|
577
|
+
3: 5 4 "d"
|
578
|
+
4: 7 5 nil
|
579
|
+
5: 4 0 "c"
|
580
|
+
key val_1 val_2
|
470
581
|
```
|
471
582
|
|
472
583
|
## Subclassing ObjectTable
|
@@ -491,8 +602,8 @@ The act of subclassing itself is easy, but any methods you add won't be availabl
|
|
491
602
|
NoMethodError: undefined method `a_plus_b' for #<ObjectTable::View:0x000000011d4dd0>
|
492
603
|
```
|
493
604
|
|
494
|
-
|
495
|
-
|
605
|
+
The easiest way to make it work is to put your methods into a mixin
|
606
|
+
and use the `fully_include` class method.
|
496
607
|
|
497
608
|
```ruby
|
498
609
|
>>> class WorkingTable < ObjectTable
|
@@ -502,12 +613,7 @@ The easiest way is just to include a module with your common methods.
|
|
502
613
|
end
|
503
614
|
end
|
504
615
|
|
505
|
-
|
506
|
-
|
507
|
-
# subclass each of these and include the Mixin too
|
508
|
-
class StaticView < StaticView; include Mixin; end
|
509
|
-
class View < View; include Mixin; end
|
510
|
-
class Group < Group; include Mixin; end
|
616
|
+
fully_include Mixin
|
511
617
|
end
|
512
618
|
...
|
513
619
|
|
@@ -518,15 +624,7 @@ The easiest way is just to include a module with your common methods.
|
|
518
624
|
|
519
625
|
# hurrah!
|
520
626
|
>>> data.where{ a > 1 }.a_plus_b
|
521
|
-
=>
|
627
|
+
=> ObjectTable::MaskedColumn.int(2):
|
522
628
|
[ 7, 9 ]
|
523
629
|
|
524
|
-
# also works in groups!
|
525
|
-
>>> data.group_by{{odd: a % 2}}.each do
|
526
|
-
p "when a % 2 == #{@K[:odd]}, a + b == #{a_plus_b.to_a}"
|
527
|
-
end
|
528
|
-
...
|
529
|
-
|
530
|
-
"when a % 2 == 1, a + b == [5, 9]"
|
531
|
-
"when a % 2 == 0, a + b == [7]"
|
532
630
|
```
|
@@ -8,7 +8,7 @@ class ObjectTable::BasicGrid < Hash
|
|
8
8
|
|
9
9
|
def _get_number_rows!
|
10
10
|
each{|k, v| self[k] = v.to_a if v.is_a?(Range)}
|
11
|
-
rows = map{|k, v| ObjectTable::Column.length_of(v)
|
11
|
+
rows = map{|k, v| ObjectTable::Column.length_of(v)}.compact.uniq
|
12
12
|
end
|
13
13
|
|
14
14
|
def _ensure_uniform_columns!(rows = nil)
|
data/lib/object_table/column.rb
CHANGED
@@ -4,17 +4,16 @@ module ObjectTable::Column
|
|
4
4
|
|
5
5
|
def self.length_of(array)
|
6
6
|
case array
|
7
|
-
when Array
|
8
|
-
|
9
|
-
|
10
|
-
array.shape.last or 0
|
11
|
-
else
|
12
|
-
raise "Expected Array or NArray, got #{array}"
|
7
|
+
when Array then array.length
|
8
|
+
when NArray then (array.shape.last or 0)
|
9
|
+
else nil
|
13
10
|
end
|
14
11
|
end
|
15
12
|
|
16
13
|
|
17
|
-
def self.stack(*columns)
|
14
|
+
def self.stack(*columns); _stack(columns); end
|
15
|
+
|
16
|
+
def self._stack(columns)
|
18
17
|
columns = columns.reject(&:empty?)
|
19
18
|
return NArray[] if columns.empty?
|
20
19
|
return columns[0].clone if columns.length == 1
|
@@ -0,0 +1,46 @@
|
|
1
|
+
require 'forwardable'
|
2
|
+
|
3
|
+
module ObjectTable::Factory
|
4
|
+
|
5
|
+
CLASS_MAP = {
|
6
|
+
'__static_view_cls__' => 'StaticView',
|
7
|
+
'__view_cls__' => 'View',
|
8
|
+
'__group_cls__' => 'Group',
|
9
|
+
}.freeze
|
10
|
+
FACTORIES = (CLASS_MAP.keys + ['__table_cls__']).freeze
|
11
|
+
|
12
|
+
module ClassMethods
|
13
|
+
CLASS_MAP.each do |name, const|
|
14
|
+
eval "def #{name}; self::#{const}; end"
|
15
|
+
end
|
16
|
+
|
17
|
+
def __table_cls__
|
18
|
+
self
|
19
|
+
end
|
20
|
+
|
21
|
+
def fully_include(mixin)
|
22
|
+
include(mixin)
|
23
|
+
constants = constants(false)
|
24
|
+
CLASS_MAP.each do |name, const|
|
25
|
+
child_cls = send(name)
|
26
|
+
# create a new subclass if there isn't already one
|
27
|
+
child_cls = const_set(const, Class.new(child_cls)) unless constants.include?(child_cls)
|
28
|
+
child_cls.send(:include, mixin)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
extend Forwardable
|
34
|
+
def_delegators 'self.class', *FACTORIES
|
35
|
+
|
36
|
+
def self.included(base)
|
37
|
+
base.extend(ClassMethods)
|
38
|
+
end
|
39
|
+
|
40
|
+
module SubFactory
|
41
|
+
FACTORIES.each do |name|
|
42
|
+
eval "def #{name}; @#{name} ||= @parent.#{name}; end"
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
end
|
@@ -0,0 +1,47 @@
|
|
1
|
+
require_relative '../util'
|
2
|
+
|
3
|
+
class ObjectTable::Grouping
|
4
|
+
|
5
|
+
class Grid
|
6
|
+
attr_reader :values, :index
|
7
|
+
|
8
|
+
def initialize(keys, defaults)
|
9
|
+
unless defaults.is_a?(Hash)
|
10
|
+
raise "Expected defaults to be a hash, got: #{defaults.inspect}"
|
11
|
+
end
|
12
|
+
defaults.default = 0
|
13
|
+
@defaults = defaults
|
14
|
+
|
15
|
+
@values = {}
|
16
|
+
@index = {}
|
17
|
+
@ids = keys.map{|k| @index[k] ||= @index.length}
|
18
|
+
@keys = keys
|
19
|
+
@length = @index.length
|
20
|
+
end
|
21
|
+
|
22
|
+
def [](k)
|
23
|
+
(@values[k] ||= Array.new(@length, @defaults[k]))[@id]
|
24
|
+
end
|
25
|
+
|
26
|
+
def []=(k, v)
|
27
|
+
@values[k][@id] = v
|
28
|
+
end
|
29
|
+
|
30
|
+
module RowFactory
|
31
|
+
def self.new(*args)
|
32
|
+
Struct.new(*args){ attr_accessor :K, :R }
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
def apply_to_rows(rows, key_struct, block)
|
37
|
+
@ids.zip(@keys, rows) do |id, key, row|
|
38
|
+
@id = id
|
39
|
+
row.K = key_struct.new(*key)
|
40
|
+
row.R = self
|
41
|
+
ObjectTable::Util.apply_block(row, block)
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
end
|
46
|
+
|
47
|
+
end
|
@@ -0,0 +1,109 @@
|
|
1
|
+
require_relative 'factory'
|
2
|
+
require_relative 'util'
|
3
|
+
require_relative 'static_view'
|
4
|
+
require_relative 'grouping/grid'
|
5
|
+
|
6
|
+
class ObjectTable
|
7
|
+
class Group < StaticView
|
8
|
+
attr_reader :K
|
9
|
+
def initialize(parent, keys, value)
|
10
|
+
super(parent, value)
|
11
|
+
@K = keys
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
class Grouping
|
16
|
+
DEFAULT_VALUE_PREFIX = 'v_'.freeze
|
17
|
+
include Factory::SubFactory
|
18
|
+
|
19
|
+
def initialize(parent, *columns, &grouper)
|
20
|
+
@parent = parent
|
21
|
+
@grouper = grouper
|
22
|
+
@columns = columns
|
23
|
+
@names = columns
|
24
|
+
end
|
25
|
+
|
26
|
+
def _keys
|
27
|
+
return Util.get_rows(@parent, @columns) unless @columns.empty?
|
28
|
+
|
29
|
+
keys = @parent.apply(&@grouper)
|
30
|
+
raise 'Group keys must be hashes' unless keys.is_a?(Hash)
|
31
|
+
keys = BasicGrid.new.replace keys
|
32
|
+
keys._ensure_uniform_columns!(@parent.nrows)
|
33
|
+
|
34
|
+
@names = keys.keys
|
35
|
+
keys.values.map(&:to_a).transpose
|
36
|
+
end
|
37
|
+
|
38
|
+
def each(&block)
|
39
|
+
groups = Util.group_indices(_keys)
|
40
|
+
return to_enum(:_make_groups, groups) unless block
|
41
|
+
_make_groups(groups){|grp| Util.apply_block(grp, block)}
|
42
|
+
end
|
43
|
+
|
44
|
+
def apply(&block)
|
45
|
+
groups = Util.group_indices(_keys)
|
46
|
+
return empty_aggregation if groups.empty?
|
47
|
+
|
48
|
+
value_key = self.class.generate_name(DEFAULT_VALUE_PREFIX, @names).to_sym
|
49
|
+
keys = []
|
50
|
+
|
51
|
+
data = groups.keys.zip(to_enum(:_make_groups, groups)).map do |key, group|
|
52
|
+
value = Util.apply_block(group, block)
|
53
|
+
|
54
|
+
case value
|
55
|
+
when TableMethods
|
56
|
+
nrows = value.nrows
|
57
|
+
when BasicGrid
|
58
|
+
nrows = value._ensure_uniform_columns!
|
59
|
+
else
|
60
|
+
nrows = (Column.length_of(value) or 1)
|
61
|
+
value = BasicGrid[value_key, value]
|
62
|
+
end
|
63
|
+
|
64
|
+
keys.concat( Array.new(nrows, key) )
|
65
|
+
value
|
66
|
+
end
|
67
|
+
|
68
|
+
keys = BasicGrid[@names.zip(keys.transpose)]
|
69
|
+
result = __table_cls__._stack(data)
|
70
|
+
__table_cls__.new(keys.merge!(result.columns))
|
71
|
+
end
|
72
|
+
|
73
|
+
def reduce(defaults={}, &block)
|
74
|
+
keys = _keys()
|
75
|
+
return empty_aggregation if keys.empty?
|
76
|
+
|
77
|
+
grid = Grid.new(keys, defaults)
|
78
|
+
rows = @parent.each_row(row_factory: Grid::RowFactory)
|
79
|
+
grid.apply_to_rows(rows, self.class.key_struct(@names), block)
|
80
|
+
|
81
|
+
keys = BasicGrid[@names.zip(grid.index.keys.transpose)]
|
82
|
+
__table_cls__.new(keys.merge!(grid.values))
|
83
|
+
end
|
84
|
+
|
85
|
+
def _make_groups(groups)
|
86
|
+
key_struct = self.class.key_struct(@names)
|
87
|
+
groups.each do |k, v|
|
88
|
+
yield __group_cls__.new(@parent, key_struct.new(*k), NArray.to_na(v))
|
89
|
+
end
|
90
|
+
@parent
|
91
|
+
end
|
92
|
+
|
93
|
+
def self.generate_name(prefix, names)
|
94
|
+
regex = Regexp.new(Regexp.quote(prefix) + '(\d+)')
|
95
|
+
i = names.map{|n| n =~ regex and $1.to_i}.compact.max || -1
|
96
|
+
"#{prefix}#{i + 1}"
|
97
|
+
end
|
98
|
+
|
99
|
+
def self.key_struct(names)
|
100
|
+
Struct.new(*names.map(&:to_sym))
|
101
|
+
end
|
102
|
+
|
103
|
+
def empty_aggregation
|
104
|
+
__table_cls__.new(@names.map{|n| [n, []]})
|
105
|
+
end
|
106
|
+
|
107
|
+
end
|
108
|
+
|
109
|
+
end
|