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.
@@ -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,