fat_table 0.3.4 → 0.5.2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.rspec +2 -1
- data/.rubocop.yml +3 -5
- data/README.org +1343 -456
- data/README.rdoc +1 -2
- data/TODO.org +17 -10
- data/examples/create_trans.sql +14 -0
- data/examples/quick.pdf +0 -0
- data/examples/quick.png +0 -0
- data/examples/quick.ppm +0 -0
- data/examples/quick.tex +8 -0
- data/examples/quick_small.png +0 -0
- data/examples/quicktable.tex +123 -0
- data/examples/trades.db +0 -0
- data/examples/trans.csv +13 -0
- data/fat_table.gemspec +1 -0
- data/lib/ext/array.rb +15 -0
- data/lib/fat_table/column.rb +69 -206
- data/lib/fat_table/convert.rb +173 -0
- data/lib/fat_table/evaluator.rb +7 -0
- data/lib/fat_table/footer.rb +228 -0
- data/lib/fat_table/formatters/formatter.rb +200 -166
- data/lib/fat_table/formatters/latex_formatter.rb +9 -7
- data/lib/fat_table/table.rb +303 -98
- data/lib/fat_table/version.rb +1 -1
- data/lib/fat_table.rb +5 -2
- data/md/README.md +1 -2
- metadata +28 -2
@@ -91,7 +91,7 @@ module FatTable
|
|
91
91
|
else
|
92
92
|
''
|
93
93
|
end
|
94
|
-
result += "\\usepackage[pdftex,x11names]{xcolor}\n"
|
94
|
+
result += "\\usepackage[pdftex,table,x11names]{xcolor}\n"
|
95
95
|
result
|
96
96
|
end
|
97
97
|
|
@@ -113,12 +113,14 @@ module FatTable
|
|
113
113
|
# default.
|
114
114
|
def decorate_string(str, istruct)
|
115
115
|
str = quote(str)
|
116
|
-
result =
|
117
|
-
result
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
116
|
+
result = istruct.italic ? "\\itshape{#{str}}" : str
|
117
|
+
result = istruct.bold ? "\\bfseries{#{result}}" : result
|
118
|
+
if istruct.color && istruct.color != 'none'
|
119
|
+
result = "{\\textcolor{#{istruct.color}}{#{result}}}"
|
120
|
+
end
|
121
|
+
if istruct.bgcolor && istruct.bgcolor != 'none'
|
122
|
+
result = "\\cellcolor{#{istruct.bgcolor}}#{result}"
|
123
|
+
end
|
122
124
|
unless istruct.alignment == format_at[:body][istruct._h].alignment
|
123
125
|
ac = alignment_code(istruct.alignment)
|
124
126
|
result = "\\multicolumn{1}{#{ac}}{#{result}}"
|
data/lib/fat_table/table.rb
CHANGED
@@ -53,7 +53,14 @@ module FatTable
|
|
53
53
|
class Table
|
54
54
|
# An Array of FatTable::Columns that constitute the table.
|
55
55
|
attr_reader :columns
|
56
|
-
|
56
|
+
|
57
|
+
# Record boundaries set explicitly with mark_boundaries or from reading
|
58
|
+
# hlines from input. When we want to access boundaries, however, we want
|
59
|
+
# to add an implict boundary at the last row of the table. Since, as the
|
60
|
+
# table grows, the implict boundary changes index, we synthesize the
|
61
|
+
# boundaries by dynamically adding the final boundary with the #boundaries
|
62
|
+
# method call.
|
63
|
+
attr_accessor :explicit_boundaries
|
57
64
|
|
58
65
|
###########################################################################
|
59
66
|
# Constructors
|
@@ -62,9 +69,30 @@ module FatTable
|
|
62
69
|
# :category: Constructors
|
63
70
|
|
64
71
|
# Return an empty FatTable::Table object.
|
65
|
-
def initialize
|
72
|
+
def initialize(*heads)
|
66
73
|
@columns = []
|
67
|
-
@
|
74
|
+
@explicit_boundaries = []
|
75
|
+
unless heads.empty?
|
76
|
+
heads.each do |h|
|
77
|
+
@columns << Column.new(header: h)
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
# :category: Constructors
|
83
|
+
|
84
|
+
# Return an empty duplicate of self. This allows the library to create an
|
85
|
+
# empty table that preserves all the instance variables from self. Even
|
86
|
+
# though FatTable::Table objects have no instance variables, a class that
|
87
|
+
# inherits from it might.
|
88
|
+
def empty_dup
|
89
|
+
self.dup.__empty!
|
90
|
+
end
|
91
|
+
|
92
|
+
def __empty!
|
93
|
+
@columns = []
|
94
|
+
@explicit_boundaries = []
|
95
|
+
self
|
68
96
|
end
|
69
97
|
|
70
98
|
# :category: Constructors
|
@@ -188,6 +216,7 @@ module FatTable
|
|
188
216
|
end
|
189
217
|
result << hsh.to_h
|
190
218
|
end
|
219
|
+
result.normalize_boundaries
|
191
220
|
result
|
192
221
|
end
|
193
222
|
|
@@ -236,6 +265,7 @@ module FatTable
|
|
236
265
|
hash_row = Hash[headers.zip(row)]
|
237
266
|
result << hash_row
|
238
267
|
end
|
268
|
+
result.normalize_boundaries
|
239
269
|
result
|
240
270
|
end
|
241
271
|
|
@@ -245,6 +275,7 @@ module FatTable
|
|
245
275
|
skip_blanks: true).each do |row|
|
246
276
|
result << row.to_h
|
247
277
|
end
|
278
|
+
result.normalize_boundaries
|
248
279
|
result
|
249
280
|
end
|
250
281
|
|
@@ -317,8 +348,11 @@ module FatTable
|
|
317
348
|
# Set the column type for Column with the given +key+ as a String type,
|
318
349
|
# but only if empty. Otherwise, we would have to worry about converting
|
319
350
|
# existing items in the column to String. Perhaps that's a TODO.
|
320
|
-
def
|
321
|
-
|
351
|
+
def force_string!(*keys)
|
352
|
+
keys.each do |h|
|
353
|
+
column(h).force_string!
|
354
|
+
end
|
355
|
+
self
|
322
356
|
end
|
323
357
|
|
324
358
|
# :category: Attributes
|
@@ -426,8 +460,6 @@ module FatTable
|
|
426
460
|
# large table, that would require that we construct all the rows for a range
|
427
461
|
# of any size.
|
428
462
|
def rows_range(first = 0, last = nil) # :nodoc:
|
429
|
-
last ||= size - 1
|
430
|
-
last = [last, 0].max
|
431
463
|
raise UserError, 'first must be <= last' unless first <= last
|
432
464
|
|
433
465
|
rows = []
|
@@ -473,6 +505,8 @@ module FatTable
|
|
473
505
|
# the headers from the body) marks a boundary for the row immediately
|
474
506
|
# preceding the hline.
|
475
507
|
#
|
508
|
+
# Boundaries can also be added manually with the +mark_boundary+ method.
|
509
|
+
#
|
476
510
|
# The #order_by method resets the boundaries then adds boundaries at the
|
477
511
|
# last row of each group of rows on which the sort keys were equal as a
|
478
512
|
# boundary.
|
@@ -506,6 +540,42 @@ module FatTable
|
|
506
540
|
groups
|
507
541
|
end
|
508
542
|
|
543
|
+
# Return the number of groups in the table.
|
544
|
+
def number_of_groups
|
545
|
+
empty? ? 0 : boundaries.size
|
546
|
+
end
|
547
|
+
|
548
|
+
# Return the range of row indexes for boundary number +k+
|
549
|
+
def group_row_range(k)
|
550
|
+
last_k = boundaries.size - 1
|
551
|
+
if k < 0 || k > last_k
|
552
|
+
raise ArgumentError, "boundary number '#{k}' out of range in boundary_row_range"
|
553
|
+
end
|
554
|
+
|
555
|
+
if boundaries.size == 1
|
556
|
+
(0..boundaries.first)
|
557
|
+
elsif k.zero?
|
558
|
+
# Keep index at or above zero
|
559
|
+
(0..boundaries[k])
|
560
|
+
else
|
561
|
+
((boundaries[k - 1] + 1)..boundaries[k])
|
562
|
+
end
|
563
|
+
end
|
564
|
+
|
565
|
+
# Return an Array of Column objects for header +col+ representing a
|
566
|
+
# sub-column for each group in the table under that header.
|
567
|
+
def group_cols(col)
|
568
|
+
normalize_boundaries
|
569
|
+
cols = []
|
570
|
+
(0..boundaries.size - 1).each do |k|
|
571
|
+
range = group_row_range(k)
|
572
|
+
tab_col = column(col)
|
573
|
+
gitems = tab_col.items[range]
|
574
|
+
cols << Column.new(header: col, items: gitems, type: tab_col.type)
|
575
|
+
end
|
576
|
+
cols
|
577
|
+
end
|
578
|
+
|
509
579
|
# :category: Operators
|
510
580
|
|
511
581
|
# Return this table mutated with all groups removed. Useful after something
|
@@ -513,56 +583,99 @@ module FatTable
|
|
513
583
|
# the groups displayed in the output. This modifies the input table, so is a
|
514
584
|
# departure from the otherwise immutability of Tables.
|
515
585
|
def degroup!
|
516
|
-
|
586
|
+
self.explicit_boundaries = []
|
517
587
|
self
|
518
588
|
end
|
519
589
|
|
520
590
|
# Mark a group boundary at row +row+, and if +row+ is +nil+, mark the last
|
521
|
-
# row in the table as a group boundary.
|
522
|
-
#
|
523
|
-
|
524
|
-
|
525
|
-
|
526
|
-
|
527
|
-
|
591
|
+
# row in the table as a group boundary. An attempt to add a boundary to
|
592
|
+
# an empty table has no effect. We adopt the convention that the last row
|
593
|
+
# of the table always marks an implicit boundary even if it is not in the
|
594
|
+
# @explicit_boundaries array. When we "mark" a boundary, we intend it to
|
595
|
+
# be an explicit boundary, even if it marks the last row of the table.
|
596
|
+
def mark_boundary(row_num = nil)
|
597
|
+
return self if empty?
|
598
|
+
|
599
|
+
if row_num
|
600
|
+
unless row_num < size
|
601
|
+
raise ArgumentError, "can't mark boundary at row #{row_num}, last row is #{size - 1}"
|
602
|
+
end
|
603
|
+
unless row_num >= 0
|
604
|
+
raise ArgumentError, "can't mark boundary at non-positive row #{row_num}"
|
605
|
+
end
|
606
|
+
explicit_boundaries.push(row_num)
|
607
|
+
elsif size > 0
|
608
|
+
explicit_boundaries.push(size - 1)
|
528
609
|
end
|
610
|
+
normalize_boundaries
|
611
|
+
self
|
529
612
|
end
|
530
613
|
|
531
|
-
protected
|
532
|
-
|
533
614
|
# :stopdoc:
|
534
615
|
|
535
616
|
# Make sure size - 1 is last boundary and that they are unique and sorted.
|
536
617
|
def normalize_boundaries
|
537
618
|
unless empty?
|
538
|
-
|
539
|
-
self.boundaries = boundaries.uniq.sort
|
619
|
+
self.explicit_boundaries = explicit_boundaries.uniq.sort
|
540
620
|
end
|
541
|
-
|
621
|
+
explicit_boundaries
|
542
622
|
end
|
543
623
|
|
624
|
+
# Return the explicit_boundaries, augmented by an implicit boundary for
|
625
|
+
# the end of the table, unless it's already an implicit boundary.
|
626
|
+
def boundaries
|
627
|
+
return [] if empty?
|
628
|
+
|
629
|
+
if explicit_boundaries.last == size - 1
|
630
|
+
explicit_boundaries
|
631
|
+
else
|
632
|
+
explicit_boundaries + [size - 1]
|
633
|
+
end
|
634
|
+
end
|
635
|
+
|
636
|
+
protected
|
637
|
+
|
544
638
|
# Concatenate the array of argument bounds to this table's boundaries, but
|
545
639
|
# increase each of the indexes in bounds by shift. This is used in the
|
546
640
|
# #union_all method.
|
547
641
|
def append_boundaries(bounds, shift: 0)
|
548
|
-
@
|
642
|
+
@explicit_boundaries += bounds.map { |k| k + shift }
|
549
643
|
end
|
550
644
|
|
551
|
-
# Return the group number to which row ~
|
552
|
-
# user's point of view are indexed starting at
|
553
|
-
def row_index_to_group_index(
|
645
|
+
# Return the group number to which row ~row_num~ belongs. Groups, from the
|
646
|
+
# user's point of view are indexed starting at 0.
|
647
|
+
def row_index_to_group_index(row_num)
|
554
648
|
boundaries.each_with_index do |b_last, g_num|
|
555
|
-
return (g_num + 1) if
|
649
|
+
return (g_num + 1) if row_num <= b_last
|
650
|
+
end
|
651
|
+
0
|
652
|
+
end
|
653
|
+
|
654
|
+
# Return the index of the first row in group number +grp_num+
|
655
|
+
def first_row_num_in_group(grp_num)
|
656
|
+
if grp_num >= boundaries.size || grp_num < 0
|
657
|
+
raise ArgumentError, "group number #{grp_num} out of bounds"
|
658
|
+
end
|
659
|
+
|
660
|
+
grp_num.zero? ? 0 : boundaries[grp_num - 1] + 1
|
661
|
+
end
|
662
|
+
|
663
|
+
# Return the index of the last row in group number +grp_num+
|
664
|
+
def last_row_num_in_group(grp_num)
|
665
|
+
if grp_num > boundaries.size || grp_num < 0
|
666
|
+
raise ArgumentError, "group number #{grp_num} out of bounds"
|
667
|
+
else
|
668
|
+
boundaries[grp_num]
|
556
669
|
end
|
557
|
-
1
|
558
670
|
end
|
559
671
|
|
560
|
-
|
672
|
+
# Return the rows for group number +grp_num+.
|
673
|
+
def group_rows(grp_num) # :nodoc:
|
561
674
|
normalize_boundaries
|
562
|
-
return [] unless
|
675
|
+
return [] unless grp_num < boundaries.size
|
563
676
|
|
564
|
-
first =
|
565
|
-
last =
|
677
|
+
first = first_row_num_in_group(grp_num)
|
678
|
+
last = last_row_num_in_group(grp_num)
|
566
679
|
rows_range(first, last)
|
567
680
|
end
|
568
681
|
|
@@ -587,22 +700,43 @@ module FatTable
|
|
587
700
|
# After sorting, the output Table will have group boundaries added after
|
588
701
|
# each row where the sort key changes.
|
589
702
|
def order_by(*sort_heads)
|
590
|
-
|
591
|
-
|
592
|
-
sort_heads = sort_heads.map { |h| h.to_s.sub(/\!\z/, '').to_sym }
|
593
|
-
rev_heads = rev_heads.map { |h| h.to_s.sub(/\!\z/, '').to_sym }
|
703
|
+
# Sort the rows in order and add to new_rows.
|
704
|
+
key_hash = partition_sort_keys(sort_heads)
|
594
705
|
new_rows = rows.sort do |r1, r2|
|
595
|
-
|
596
|
-
|
597
|
-
|
706
|
+
# Set the sort keys based on direction
|
707
|
+
key1 = []
|
708
|
+
key2 = []
|
709
|
+
key_hash.each_pair do |h, dir|
|
710
|
+
if dir == :forward
|
711
|
+
key1 << r1[h]
|
712
|
+
key2 << r2[h]
|
713
|
+
else
|
714
|
+
key1 << r2[h]
|
715
|
+
key2 << r1[h]
|
716
|
+
end
|
717
|
+
end
|
718
|
+
# Make any booleans comparable with <=>
|
719
|
+
key1 = key1.map_booleans
|
720
|
+
key2 = key2.map_booleans
|
721
|
+
|
722
|
+
# If there are any nils, <=> will return nil, and we have to use the
|
723
|
+
# special comparison method, compare_with_nils, instead.
|
724
|
+
result = (key1 <=> key2)
|
725
|
+
result.nil? ? compare_with_nils(key1, key2) : result
|
598
726
|
end
|
599
|
-
|
600
|
-
#
|
601
|
-
|
727
|
+
|
728
|
+
# Add the new_rows to the table, but mark a group boundary at the points
|
729
|
+
# where the sort key changes value. NB: I use self.class.new here
|
730
|
+
# rather than Table.new because if this class is inherited, I want the
|
731
|
+
# new_tab to be an instance of the subclass. With Table.new, this
|
732
|
+
# method's result will be an instance of FatTable::Table rather than of
|
733
|
+
# the subclass.
|
734
|
+
new_tab = empty_dup
|
602
735
|
last_key = nil
|
603
736
|
new_rows.each_with_index do |nrow, k|
|
604
737
|
new_tab << nrow
|
605
|
-
key = nrow.fetch_values(*sort_heads)
|
738
|
+
# key = nrow.fetch_values(*sort_heads)
|
739
|
+
key = nrow.fetch_values(*key_hash.keys)
|
606
740
|
new_tab.mark_boundary(k - 1) if last_key && key != last_key
|
607
741
|
last_key = key
|
608
742
|
end
|
@@ -610,6 +744,33 @@ module FatTable
|
|
610
744
|
new_tab
|
611
745
|
end
|
612
746
|
|
747
|
+
# :category: Operators
|
748
|
+
|
749
|
+
# Return a new Table sorting the rows of this Table on an any expression
|
750
|
+
# +expr+ that is valid with the +select+ method, except that they
|
751
|
+
# expression may end with an exclamation mark +!+ to indicate a reverse
|
752
|
+
# sort. The new table will have an additional column called +sort_key+
|
753
|
+
# populated with the result of evaluating the given expression and will be
|
754
|
+
# sorted (or reverse sorted) on that column.
|
755
|
+
#
|
756
|
+
# tab.order_with('date.year') => table sorted by date's year
|
757
|
+
# tab.order_with('date.year!') => table reverse sorted by date's year
|
758
|
+
#
|
759
|
+
# After sorting, the output Table will have group boundaries added after
|
760
|
+
# each row where the sort key changes.
|
761
|
+
def order_with(expr)
|
762
|
+
unless expr.is_a?(String)
|
763
|
+
raise "must call FatTable::Table\#order_with with a single string expression"
|
764
|
+
end
|
765
|
+
rev = false
|
766
|
+
if expr.match?(/\s*!\s*\z/)
|
767
|
+
rev = true
|
768
|
+
expr = expr.sub(/\s*!\s*\z/, '')
|
769
|
+
end
|
770
|
+
sort_sym = rev ? :sort_key! : :sort_key
|
771
|
+
dup.select(*headers, sort_key: expr).order_by(sort_sym)
|
772
|
+
end
|
773
|
+
|
613
774
|
# :category: Operators
|
614
775
|
#
|
615
776
|
# Return a Table having the selected column expressions. Each expression can
|
@@ -713,7 +874,7 @@ module FatTable
|
|
713
874
|
before: before_hook,
|
714
875
|
after: after_hook)
|
715
876
|
# Compute the new Table from this Table
|
716
|
-
result =
|
877
|
+
result = empty_dup
|
717
878
|
normalize_boundaries
|
718
879
|
rows.each_with_index do |old_row, old_k|
|
719
880
|
# Set the group number in the before hook and run the hook with the
|
@@ -723,7 +884,15 @@ module FatTable
|
|
723
884
|
ev.eval_before_hook(locals: old_row)
|
724
885
|
# Compute the new row.
|
725
886
|
new_row = {}
|
726
|
-
|
887
|
+
# Allow the :omni col to stand for all columns if it is alone and
|
888
|
+
# first.
|
889
|
+
cols_to_include =
|
890
|
+
if cols.size == 1 && cols.first.as_sym == :omni
|
891
|
+
headers
|
892
|
+
else
|
893
|
+
cols
|
894
|
+
end
|
895
|
+
cols_to_include.each do |k|
|
727
896
|
h = k.as_sym
|
728
897
|
msg = "Column '#{h}' in select does not exist"
|
729
898
|
raise UserError, msg unless column?(h)
|
@@ -752,7 +921,7 @@ module FatTable
|
|
752
921
|
ev.eval_after_hook(locals: new_row)
|
753
922
|
result << new_row
|
754
923
|
end
|
755
|
-
result.
|
924
|
+
result.explicit_boundaries = explicit_boundaries
|
756
925
|
result.normalize_boundaries
|
757
926
|
result
|
758
927
|
end
|
@@ -770,7 +939,7 @@ module FatTable
|
|
770
939
|
# tab.where('@row.even? && shares > 500') => even rows with lots of shares
|
771
940
|
def where(expr)
|
772
941
|
expr = expr.to_s
|
773
|
-
result =
|
942
|
+
result = empty_dup
|
774
943
|
headers.each do |h|
|
775
944
|
col = Column.new(header: h)
|
776
945
|
result.add_column(col)
|
@@ -792,7 +961,7 @@ module FatTable
|
|
792
961
|
# Return a new table with all duplicate rows eliminated. Resets groups. Same
|
793
962
|
# as #uniq.
|
794
963
|
def distinct
|
795
|
-
result =
|
964
|
+
result = empty_dup
|
796
965
|
uniq_rows = rows.uniq
|
797
966
|
uniq_rows.each do |row|
|
798
967
|
result << row
|
@@ -889,38 +1058,6 @@ module FatTable
|
|
889
1058
|
set_operation(other, :difference, distinct: false)
|
890
1059
|
end
|
891
1060
|
|
892
|
-
private
|
893
|
-
|
894
|
-
# Apply the set operation given by ~oper~ between this table and the other
|
895
|
-
# table given in the first argument. If distinct is true, eliminate
|
896
|
-
# duplicates from the result.
|
897
|
-
def set_operation(other, oper = :+, distinct: true, add_boundaries: true, inherit_boundaries: false)
|
898
|
-
unless columns.size == other.columns.size
|
899
|
-
msg = "can't apply set ops to tables with a different number of columns"
|
900
|
-
raise UserError, msg
|
901
|
-
end
|
902
|
-
unless columns.map(&:type) == other.columns.map(&:type)
|
903
|
-
msg = "can't apply a set ops to tables with different column types."
|
904
|
-
raise UserError, msg
|
905
|
-
end
|
906
|
-
other_rows = other.rows.map { |r| r.replace_keys(headers) }
|
907
|
-
result = Table.new
|
908
|
-
new_rows = rows.send(oper, other_rows)
|
909
|
-
new_rows.each_with_index do |row, k|
|
910
|
-
result << row
|
911
|
-
result.mark_boundary if k == size - 1 && add_boundaries
|
912
|
-
end
|
913
|
-
if inherit_boundaries
|
914
|
-
result.boundaries = normalize_boundaries
|
915
|
-
other.normalize_boundaries
|
916
|
-
result.append_boundaries(other.boundaries, shift: size)
|
917
|
-
end
|
918
|
-
result.normalize_boundaries
|
919
|
-
distinct ? result.distinct : result
|
920
|
-
end
|
921
|
-
|
922
|
-
public
|
923
|
-
|
924
1061
|
# An Array of symbols for the valid join types.
|
925
1062
|
JOIN_TYPES = %i[inner left right full cross].freeze
|
926
1063
|
|
@@ -1011,7 +1148,7 @@ module FatTable
|
|
1011
1148
|
join_exp, other_common_heads =
|
1012
1149
|
build_join_expression(exps, other, join_type)
|
1013
1150
|
ev = Evaluator.new
|
1014
|
-
result =
|
1151
|
+
result = empty_dup
|
1015
1152
|
other_rows = other.rows
|
1016
1153
|
other_row_matches = Array.new(other_rows.size, false)
|
1017
1154
|
rows.each do |self_row|
|
@@ -1029,14 +1166,14 @@ module FatTable
|
|
1029
1166
|
type: join_type)
|
1030
1167
|
result << out_row
|
1031
1168
|
end
|
1032
|
-
next unless
|
1169
|
+
next unless [:left, :full].include?(join_type)
|
1033
1170
|
next if self_row_matched
|
1034
1171
|
|
1035
1172
|
result << build_out_row(row_a: self_row,
|
1036
1173
|
row_b: other_row_nils,
|
1037
1174
|
type: join_type)
|
1038
1175
|
end
|
1039
|
-
if
|
1176
|
+
if [:right, :full].include?(join_type)
|
1040
1177
|
other_rows.each_with_index do |other_row, k|
|
1041
1178
|
next if other_row_matches[k]
|
1042
1179
|
|
@@ -1165,7 +1302,7 @@ module FatTable
|
|
1165
1302
|
partial_result = nil
|
1166
1303
|
else
|
1167
1304
|
# First of a pair of _a or _b
|
1168
|
-
partial_result =
|
1305
|
+
partial_result = +"(#{a_head}_a == "
|
1169
1306
|
end
|
1170
1307
|
last_sym = a_head
|
1171
1308
|
when /\A(?<sy>.*)_b\z/
|
@@ -1184,7 +1321,7 @@ module FatTable
|
|
1184
1321
|
partial_result = nil
|
1185
1322
|
else
|
1186
1323
|
# First of a pair of _a or _b
|
1187
|
-
partial_result =
|
1324
|
+
partial_result = +"(#{b_head}_b == "
|
1188
1325
|
end
|
1189
1326
|
b_common_heads << b_head
|
1190
1327
|
last_sym = b_head
|
@@ -1259,7 +1396,7 @@ module FatTable
|
|
1259
1396
|
groups = sorted_tab.rows.group_by do |r|
|
1260
1397
|
group_cols.map { |k| r[k] }
|
1261
1398
|
end
|
1262
|
-
result =
|
1399
|
+
result = empty_dup
|
1263
1400
|
groups.each_pair do |_vals, grp_rows|
|
1264
1401
|
result << row_from_group(grp_rows, group_cols, agg_cols)
|
1265
1402
|
end
|
@@ -1291,25 +1428,24 @@ module FatTable
|
|
1291
1428
|
|
1292
1429
|
# :category: Constructors
|
1293
1430
|
|
1294
|
-
# Add a group boundary mark at the given row, or at the end of the table
|
1295
|
-
# by default.
|
1296
|
-
def add_boundary(at_row = nil)
|
1297
|
-
row = at_row || (size - 1)
|
1298
|
-
@boundaries << row
|
1299
|
-
end
|
1300
|
-
|
1301
|
-
# :category: Constructors
|
1302
|
-
|
1303
1431
|
# Add a +row+ represented by a Hash having the headers as keys. If +mark:+
|
1304
1432
|
# is set true, mark this row as a boundary. All tables should be built
|
1305
1433
|
# ultimately using this method as a primitive.
|
1306
1434
|
def add_row(row, mark: false)
|
1307
|
-
row.
|
1308
|
-
|
1309
|
-
|
1310
|
-
|
1435
|
+
row.transform_keys!(&:as_sym)
|
1436
|
+
# Make sure there is a column for each known header and each new key
|
1437
|
+
# present in row.
|
1438
|
+
new_heads = row.keys - headers
|
1439
|
+
new_heads.each do |h|
|
1440
|
+
# This column is new, so it needs nil items for all prior rows lest
|
1441
|
+
# the value be added to a prior row.
|
1442
|
+
items = Array.new(size, nil)
|
1443
|
+
columns << Column.new(header: h, items: items)
|
1444
|
+
end
|
1445
|
+
headers.each do |h|
|
1446
|
+
# NB: This adds a nil if h is not in row.
|
1447
|
+
column(h) << row[h]
|
1311
1448
|
end
|
1312
|
-
add_boundary if mark
|
1313
1449
|
self
|
1314
1450
|
end
|
1315
1451
|
|
@@ -1478,5 +1614,74 @@ module FatTable
|
|
1478
1614
|
yield fmt if block_given?
|
1479
1615
|
fmt.output
|
1480
1616
|
end
|
1617
|
+
|
1618
|
+
private
|
1619
|
+
|
1620
|
+
# Apply the set operation given by ~oper~ between this table and the other
|
1621
|
+
# table given in the first argument. If distinct is true, eliminate
|
1622
|
+
# duplicates from the result.
|
1623
|
+
def set_operation(other, oper = :+, distinct: true, add_boundaries: true, inherit_boundaries: false)
|
1624
|
+
unless columns.size == other.columns.size
|
1625
|
+
msg = "can't apply set ops to tables with a different number of columns"
|
1626
|
+
raise UserError, msg
|
1627
|
+
end
|
1628
|
+
unless columns.map(&:type) == other.columns.map(&:type)
|
1629
|
+
msg = "can't apply a set ops to tables with different column types."
|
1630
|
+
raise UserError, msg
|
1631
|
+
end
|
1632
|
+
other_rows = other.rows.map { |r| r.replace_keys(headers) }
|
1633
|
+
result = empty_dup
|
1634
|
+
new_rows = rows.send(oper, other_rows)
|
1635
|
+
new_rows.each_with_index do |row, k|
|
1636
|
+
result << row
|
1637
|
+
result.mark_boundary if k == size - 1 && add_boundaries
|
1638
|
+
end
|
1639
|
+
if inherit_boundaries
|
1640
|
+
result.explicit_boundaries = boundaries
|
1641
|
+
result.append_boundaries(other.boundaries, shift: size)
|
1642
|
+
end
|
1643
|
+
result.normalize_boundaries
|
1644
|
+
distinct ? result.distinct : result
|
1645
|
+
end
|
1646
|
+
|
1647
|
+
# Return a hash with the key being the header to sort on and the value
|
1648
|
+
# being either :forward or :reverse to indicate the sort order on that
|
1649
|
+
# key.
|
1650
|
+
def partition_sort_keys(keys)
|
1651
|
+
result = {}
|
1652
|
+
[keys].flatten.each do |h|
|
1653
|
+
if h.to_s.match?(/\s*!\s*\z/)
|
1654
|
+
result[h.to_s.sub(/\s*!\s*\z/, '').to_sym] = :reverse
|
1655
|
+
else
|
1656
|
+
result[h] = :forward
|
1657
|
+
end
|
1658
|
+
end
|
1659
|
+
result
|
1660
|
+
end
|
1661
|
+
|
1662
|
+
# The <=> operator cannot handle nils without some help. Treat a nil as
|
1663
|
+
# smaller than any other value, but equal to other nils. The two keys are assumed to be arrays of values to be
|
1664
|
+
# compared with <=>.
|
1665
|
+
def compare_with_nils(key1, key2)
|
1666
|
+
result = nil
|
1667
|
+
key1.zip(key2) do |k1, k2|
|
1668
|
+
if k1.nil? && k2.nil?
|
1669
|
+
result = 0
|
1670
|
+
next
|
1671
|
+
elsif k1.nil?
|
1672
|
+
result = -1
|
1673
|
+
break
|
1674
|
+
elsif k2.nil?
|
1675
|
+
result = 1
|
1676
|
+
break
|
1677
|
+
elsif (k1 <=> k2) == 0
|
1678
|
+
next
|
1679
|
+
else
|
1680
|
+
result = (k1 <=> k2)
|
1681
|
+
break
|
1682
|
+
end
|
1683
|
+
end
|
1684
|
+
result
|
1685
|
+
end
|
1481
1686
|
end
|
1482
1687
|
end
|
data/lib/fat_table/version.rb
CHANGED
data/lib/fat_table.rb
CHANGED
@@ -19,9 +19,12 @@ module FatTable
|
|
19
19
|
|
20
20
|
require 'fat_table/version'
|
21
21
|
require 'fat_table/patches'
|
22
|
+
require 'ext/array'
|
22
23
|
require 'fat_table/evaluator'
|
24
|
+
require 'fat_table/convert'
|
23
25
|
require 'fat_table/column'
|
24
26
|
require 'fat_table/table'
|
27
|
+
require 'fat_table/footer'
|
25
28
|
require 'fat_table/formatters'
|
26
29
|
require 'fat_table/db_handle'
|
27
30
|
require 'fat_table/errors'
|
@@ -58,8 +61,8 @@ module FatTable
|
|
58
61
|
|
59
62
|
# Return an empty FatTable::Table object. You can use FatTable::Table#add_row
|
60
63
|
# or FatTable::Table#add_column to populate the table with data.
|
61
|
-
def self.new
|
62
|
-
Table.new
|
64
|
+
def self.new(*args)
|
65
|
+
Table.new(*args)
|
63
66
|
end
|
64
67
|
|
65
68
|
# Construct a FatTable::Table from the contents of a CSV file given by the
|
data/md/README.md
CHANGED
@@ -1109,8 +1109,7 @@ will raise an exception.
|
|
1109
1109
|
|
1110
1110
|
- **`first`:** the first non-nil item in the column,
|
1111
1111
|
- **`last`:** the last non-nil item in the column,
|
1112
|
-
- **`
|
1113
|
-
values in the column,
|
1112
|
+
- **`range`:** form a Range ~~{min}..{max}~ to show the range of values in the column,
|
1114
1113
|
- **`sum`:** for `Numeric` and `String` columns, apply ’+’ to all the non-nil
|
1115
1114
|
values,
|
1116
1115
|
- **`count`:** the number of non-nil values in the column,
|