fat_table 0.3.4 → 0.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/.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,
|