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.
@@ -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 += '\\bfseries{}' if istruct.bold
118
- result += '\\itshape{}' if istruct.italic
119
- result += "\\color{#{istruct.color}}" if istruct.color &&
120
- istruct.color != 'none'
121
- result = "#{result}#{str}"
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}}"
@@ -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
- attr_accessor :boundaries
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
- @boundaries = []
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 set_column_to_string_type(key)
321
- column(key).force_to_string_type
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
- @boundaries = []
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. This is mainly used for internal
522
- # purposes.
523
- def mark_boundary(row = nil) # :nodoc:
524
- if row
525
- boundaries.push(row)
526
- else
527
- boundaries.push(size - 1)
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
- boundaries.push(size - 1) unless boundaries.include?(size - 1)
539
- self.boundaries = boundaries.uniq.sort
619
+ self.explicit_boundaries = explicit_boundaries.uniq.sort
540
620
  end
541
- boundaries
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
- @boundaries += bounds.map { |k| k + shift }
642
+ @explicit_boundaries += bounds.map { |k| k + shift }
549
643
  end
550
644
 
551
- # Return the group number to which row ~row~ belongs. Groups, from the
552
- # user's point of view are indexed starting at 1.
553
- def row_index_to_group_index(row)
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 row <= b_last
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
- def group_rows(row) # :nodoc:
672
+ # Return the rows for group number +grp_num+.
673
+ def group_rows(grp_num) # :nodoc:
561
674
  normalize_boundaries
562
- return [] unless row < boundaries.size
675
+ return [] unless grp_num < boundaries.size
563
676
 
564
- first = row.zero? ? 0 : boundaries[row - 1] + 1
565
- last = boundaries[row]
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
- sort_heads = [sort_heads].flatten
591
- rev_heads = sort_heads.select { |h| h.to_s.ends_with?('!') }
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
- key1 = sort_heads.map { |h| rev_heads.include?(h) ? r2[h] : r1[h] }
596
- key2 = sort_heads.map { |h| rev_heads.include?(h) ? r1[h] : r2[h] }
597
- key1 <=> key2
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
- # Add the new rows to the table, but mark a group boundary at the points
600
- # where the sort key changes value.
601
- new_tab = Table.new
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 = Table.new
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
- cols.each do |k|
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.boundaries = boundaries
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 = Table.new
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 = Table.new
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 = Table.new
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 %i[left full].include?(join_type)
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 %i[right full].include?(join_type)
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 = String.new("(#{a_head}_a == ")
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 = String.new("(#{b_head}_b == ")
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 = Table.new
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.each_pair do |k, v|
1308
- key = k.as_sym
1309
- columns << Column.new(header: k) unless column?(k)
1310
- column(key) << v
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
@@ -2,5 +2,5 @@
2
2
 
3
3
  module FatTable
4
4
  # The current version of FatTable
5
- VERSION = '0.3.4'
5
+ VERSION = '0.5.2'
6
6
  end
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
- - **`rng`:** form a string of the form `"#{first}..#{last}"` to show the range of
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 &rsquo;+&rsquo; to all the non-nil
1115
1114
  values,
1116
1115
  - **`count`:** the number of non-nil values in the column,