fat_core 1.6.0 → 1.7.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,49 +1,32 @@
1
1
  module FatCore
2
2
  # A container for a two-dimensional table. All cells in the table must be a
3
- # String, a Date, a DateTime, a Bignum (or Integer), a BigDecimal, or a
4
- # boolean. All columns must be of one of those types or be a string
5
- # convertible into one of the supported types. It is considered an error if a
6
- # single column contains cells of different types. Any cell that cannot be
7
- # parsed as one of the numeric, date, or boolean types will have to_s applied
3
+ # String, a DateTime (or Date), a Numeric (Bignum, Integer, or BigDecimal), or
4
+ # a Boolean (TrueClass or FalseClass). All columns must be of one of those
5
+ # types or be a string convertible into one of them. It is considered an error
6
+ # if a single column contains cells of different types. Any cell that cannot
7
+ # be parsed as one of the Numeric, DateTime, or Boolean types will be treated
8
+ # as a String and have to_s applied. Until the column type is determined, it
9
+ # will have the type NilClass.
8
10
  #
9
11
  # You can initialize a Table in several ways:
10
12
  #
11
13
  # 1. with a Nil, which will return an empty table to which rows or columns can
12
- # be added later,
13
- # 2. with the name of a .csv file,
14
- # 3. with the name of an .org file,
15
- # 4. with an IO or StringIO object for either type of file, but in that case,
16
- # you need to specify 'csv' or 'org' as the second argument to tell it what
17
- # kind of file format to expect,
18
- # 5. with an Array of Arrays,
19
- # 6. with an Array of Hashes, all having the same keys, which become the names
20
- # of the column heads,
21
- # 7. with an Array of any objects that respond to .keys and .values methods,
22
- # 8. with another Table object.
23
- #
24
- # In the case of an array of arrays, if the second array's first element is a
25
- # string that looks like a rule separator, '-----------', '+----------', etc.,
26
- # the headers will be taken from the first array. In the case of an array of
27
- # Hashes or Hash-lime objects, the keys of the hashes will be used as the
28
- # headers. It is assumed that all the hashes have the same keys.
14
+ # be added later, 2. with the name of a .csv file, 3. with the name of an
15
+ # .org file, 4. with an IO or StringIO object for either type of file, but
16
+ # in that case, you need to specify 'csv' or 'org' as the second argument
17
+ # to tell it what kind of file format to expect, 5. with an Array of
18
+ # Arrays, 6. with an Array of Hashes, all having the same keys, which
19
+ # become the names of the column heads, 7. with an Array of any objects
20
+ # that respond to .keys and .values methods, 8. with another Table object.
29
21
  #
30
22
  # In the resulting Table, the headers are converted into symbols, with all
31
23
  # spaces converted to underscore and everything down-cased. So, the heading,
32
24
  # 'Two Words' becomes the hash header :two_words.
33
- #
34
- # An entire column can be retrieved by header from a Table, thus,
35
- #
36
- # tab = Table.from_org_file("example.org")
37
- # tab[:age].avg
38
- #
39
- # will extract the entire ~:age~ column and compute its average, since Column
40
- # objects respond to aggregate methods, such as ~sum~, ~min~, ~max~, and ~avg~.
41
25
  class Table
42
- attr_reader :columns, :footers
26
+ attr_reader :columns
43
27
 
44
- def initialize(input = nil, ext = '.csv')
28
+ def initialize
45
29
  @columns = []
46
- @footers = {}
47
30
  @boundaries = []
48
31
  end
49
32
 
@@ -51,32 +34,49 @@ module FatCore
51
34
  # Constructors
52
35
  ###########################################################################
53
36
 
54
- def self.from_csv_string(str)
55
- from_csv_io(StringIO.new(str))
56
- end
57
-
37
+ # Construct a Table from the contents of a CSV file. Headers will be taken
38
+ # from the first row and converted to symbols.
58
39
  def self.from_csv_file(fname)
59
40
  File.open(fname, 'r') do |io|
60
41
  from_csv_io(io)
61
42
  end
62
43
  end
63
44
 
64
- def self.from_org_string(str)
65
- from_org_io(StringIO.new(str))
45
+ # Construct a Table from a string, treated as the input from a CSV file.
46
+ def self.from_csv_string(str)
47
+ from_csv_io(StringIO.new(str))
66
48
  end
67
49
 
50
+ # Construct a Table from the first table found in the given org-mode file.
51
+ # Headers are taken from the first row if the second row is an hrule.``
68
52
  def self.from_org_file(fname)
69
53
  File.open(fname, 'r') do |io|
70
54
  from_org_io(io)
71
55
  end
72
56
  end
73
57
 
58
+ # Construct a Table from a string, treated as the contents of an org-mode
59
+ # file.
60
+ def self.from_org_string(str)
61
+ from_org_io(StringIO.new(str))
62
+ end
63
+
64
+ # Construct a Table from an array of arrays. If the second element is a nil
65
+ # or is an array whose first element is a string that looks like a rule
66
+ # separator, '|-----------', '+----------', etc., the headers will be taken
67
+ # from the first array converted to strings and then to symbols. Any
68
+ # following such rows mark a group boundary. Note that this is the form of
69
+ # a table used by org-mode src blocks, so it is useful for building Tables
70
+ # from the result of a src block.
74
71
  def self.from_aoa(aoa)
75
72
  from_array_of_arrays(aoa)
76
73
  end
77
74
 
75
+ # Construct a Table from an array of hashes, or any objects that respond to
76
+ # the #to_h method. All hashes must have the same keys, which, when
77
+ # converted to symbols will become the headers for the Table.
78
78
  def self.from_aoh(aoh)
79
- if aoh[0].respond_to?(:to_h)
79
+ if aoh.first.respond_to?(:to_h)
80
80
  from_array_of_hashes(aoh)
81
81
  else
82
82
  raise ArgumentError,
@@ -84,46 +84,192 @@ module FatCore
84
84
  end
85
85
  end
86
86
 
87
+ # Construct a Table from another Table. Inherit any group boundaries from
88
+ # the input table.
87
89
  def self.from_table(table)
88
90
  from_aoh(table.rows)
91
+ @boundaries = table.boundaries
89
92
  end
90
93
 
94
+ ############################################################################
95
+ # Class-level constructor helpers
96
+ ############################################################################
97
+
98
+ class << self
99
+ private
100
+
101
+ # Construct table from an array of hashes or an array of any object that can
102
+ # respond to #to_h. If an array element is a nil, mark it as a group
103
+ # boundary in the Table.
104
+ def from_array_of_hashes(hashes)
105
+ result = new
106
+ hashes.each do |hsh|
107
+ if hsh.nil?
108
+ result.mark_boundary
109
+ next
110
+ end
111
+ result << hsh.to_h
112
+ end
113
+ result
114
+ end
115
+
116
+ # Construct a new table from an array of arrays. If the second element of
117
+ # the array is a nil, a string that looks like an hrule, or an array whose
118
+ # first element is a string that looks like an hrule, interpret the first
119
+ # element of the array as a row of headers. Otherwise, synthesize headers of
120
+ # the form "col1", "col2", ... and so forth. The remaining elements are
121
+ # taken as the body of the table, except that if an element of the outer
122
+ # array is a nil or a string that looks like an hrule, mark the preceding
123
+ # row as a boundary.
124
+ def from_array_of_arrays(rows)
125
+ result = new
126
+ headers = []
127
+ if looks_like_boundary?(rows[1])
128
+ # Take the first row as headers
129
+ # Use first row 0 as headers
130
+ headers = rows[0].map(&:as_sym)
131
+ first_data_row = 2
132
+ else
133
+ # Synthesize headers
134
+ headers = (1..rows[0].size).to_a.map { |k| "col#{k}".as_sym }
135
+ first_data_row = 0
136
+ end
137
+ rows[first_data_row..-1].each do |row|
138
+ if looks_like_boundary?(row)
139
+ result.mark_boundary
140
+ next
141
+ end
142
+ row = row.map { |s| s.to_s.strip }
143
+ hash_row = Hash[headers.zip(row)]
144
+ result << hash_row
145
+ end
146
+ result
147
+ end
148
+
149
+ # Return true if row is nil, a string that matches hrule_re, or is an
150
+ # array whose first element matches hrule_re.
151
+ def looks_like_boundary?(row)
152
+ hrule_re = /\A\s*[\|+][-]+/
153
+ return true if row.nil?
154
+ if row.respond_to?(:first) && row.first.respond_to?(:to_s)
155
+ return row.first.to_s =~ hrule_re
156
+ end
157
+ if row.respond_to?(:to_s)
158
+ return row.to_s =~ hrule_re
159
+ end
160
+ false
161
+ end
162
+
163
+ def from_csv_io(io)
164
+ result = new
165
+ ::CSV.new(io, headers: true, header_converters: :symbol,
166
+ skip_blanks: true).each do |row|
167
+ result << row.to_h
168
+ end
169
+ result
170
+ end
171
+
172
+ # Form rows of table by reading the first table found in the org file.
173
+ def from_org_io(io)
174
+ table_re = /\A\s*\|/
175
+ hrule_re = /\A\s*\|[-+]+/
176
+ rows = []
177
+ table_found = false
178
+ header_found = false
179
+ io.each do |line|
180
+ unless table_found
181
+ # Skip through the file until a table is found
182
+ next unless line =~ table_re
183
+ unless line =~ hrule_re
184
+ line = line.sub(/\A\s*\|/, '').sub(/\|\s*\z/, '')
185
+ rows << line.split('|').map(&:clean)
186
+ end
187
+ table_found = true
188
+ next
189
+ end
190
+ break unless line =~ table_re
191
+ if !header_found && line =~ hrule_re
192
+ rows << nil
193
+ header_found = true
194
+ next
195
+ elsif header_found && line =~ hrule_re
196
+ # Mark the boundary with a nil
197
+ rows << nil
198
+ elsif line !~ table_re
199
+ # Stop reading at the second hline
200
+ break
201
+ else
202
+ line = line.sub(/\A\s*\|/, '').sub(/\|\s*\z/, '')
203
+ rows << line.split('|').map(&:clean)
204
+ end
205
+ end
206
+ from_array_of_arrays(rows)
207
+ end
208
+ end
209
+
210
+ ###########################################################################
211
+ # Attributes
212
+ ###########################################################################
213
+
91
214
  # Return the column with the given header.
92
215
  def column(key)
93
216
  columns.detect { |c| c.header == key.as_sym }
94
217
  end
95
218
 
96
- # Return the array of items of the column with the given header.
219
+ # Return the type of the column with the given header
220
+ def type(key)
221
+ column(key).type
222
+ end
223
+
224
+ # Return the array of items of the column with the given header, or if the
225
+ # index is an integer, return that row number. So a table's rows can be
226
+ # accessed by number, and its columns can be accessed by column header.
227
+ # Also, double indexing works in either row-major or column-majoir order:
228
+ # tab[:id][8] returns the 8th item in the column headed :id and so does
229
+ # tab[8][:id].
97
230
  def [](key)
98
- column(key)
231
+ case key
232
+ when Integer
233
+ raise "index '#{key}' out of range" unless (1..size).cover?(key)
234
+ rows[key - 1]
235
+ when String
236
+ raise "header '#{key}' not in table" unless headers.include?(key)
237
+ column(key).items
238
+ when Symbol
239
+ raise "header ':#{key}' not in table" unless headers.include?(key)
240
+ column(key).items
241
+ else
242
+ raise "cannot index table with a #{key.class}"
243
+ end
99
244
  end
100
245
 
246
+ # Return true if the table has a column with the given header.
101
247
  def column?(key)
102
248
  headers.include?(key.as_sym)
103
249
  end
104
250
 
105
- # Attr_reader as a plural
251
+ # Return an array of the Table's column types.
106
252
  def types
107
253
  columns.map(&:type)
108
254
  end
109
255
 
110
- # Return the headers for the table as an array of symbols.
256
+ # Return the headers for the Table as an array of symbols.
111
257
  def headers
112
258
  columns.map(&:header)
113
259
  end
114
260
 
115
- # Return the number of rows in the table.
261
+ # Return the number of rows in the Table.
116
262
  def size
117
263
  return 0 if columns.empty?
118
264
  columns.first.size
119
265
  end
120
266
 
121
- # Return whether this table is empty.
267
+ # Return whether this Table is empty.
122
268
  def empty?
123
269
  size.zero?
124
270
  end
125
271
 
126
- # Return the rows of the table as an array of hashes, keyed by the headers.
272
+ # Return the rows of the Table as an array of hashes, keyed by the headers.
127
273
  def rows
128
274
  rows = []
129
275
  unless columns.empty?
@@ -277,9 +423,9 @@ module FatCore
277
423
 
278
424
  public
279
425
 
280
- # Return a new Table sorted on the rows of this Table on the possibly
281
- # multiple keys given in the array of syms in headers. Append a ! to the
282
- # symbol name to indicate reverse sorting on that column. Resets groups.
426
+ # Return a new Table sorting the rows of this Table on the possibly multiple
427
+ # keys given in the array of syms in headers. Append a ! to the symbol name
428
+ # to indicate reverse sorting on that column. Resets groups.
283
429
  def order_by(*sort_heads)
284
430
  sort_heads = [sort_heads].flatten
285
431
  rev_heads = sort_heads.select { |h| h.to_s.ends_with?('!') }
@@ -542,10 +688,12 @@ module FatCore
542
688
  # have N and M rows respectively, the joined table will have N * M
543
689
  # rows.
544
690
  # Resets groups.
545
- JOIN_TYPES = [:inner, :left, :right, :full, :cross]
691
+ JOIN_TYPES = [:inner, :left, :right, :full, :cross].freeze
546
692
 
547
693
  def join(other, *exps, join_type: :inner)
548
- raise ArgumentError, 'need other table as first argument to join' unless other.is_a?(Table)
694
+ unless other.is_a?(Table)
695
+ raise ArgumentError, 'need other table as first argument to join'
696
+ end
549
697
  unless JOIN_TYPES.include?(join_type)
550
698
  raise ArgumentError, "join_type may only be: #{JOIN_TYPES.join(', ')}"
551
699
  end
@@ -610,74 +758,6 @@ module FatCore
610
758
  join(other, join_type: :cross)
611
759
  end
612
760
 
613
- # Return a Table with a single row for each group of rows in the input table
614
- # where the value of all columns named as simple symbols are equal. All
615
- # other columns are set to the result of aggregating the values of that
616
- # column within the group according to a aggregate function (:count, :sum,
617
- # :min, :max, etc.), which defaults to the :first function, giving the value
618
- # of that column for the first row in the group. You can specify a
619
- # different aggregate function for a column by adding a hash parameter with
620
- # the column as the key and a symbol for the aggregate function as the
621
- # value. For example, consider the following call:
622
- #
623
- # tab.group_by(:date, :code, :price, shares: :sum, ).
624
- #
625
- # The first three parameters are simple symbols, so the table is divided
626
- # into groups of rows in which the value of :date, :code, and :price are
627
- # equal. The shares: hash parameter is set to the aggregate function :sum,
628
- # so it will appear in the result as the sum of all the :shares values in
629
- # each group. Any non-aggregate columns that have no aggregate function set
630
- # default to using the aggregate function :first. Because of the way Ruby
631
- # parses parameters to a method call, all the grouping symbols must appear
632
- # first in the parameter list before any hash parameters.
633
- def group_by(*group_cols, **agg_cols)
634
- default_agg_func = :first
635
- default_cols = headers - group_cols - agg_cols.keys
636
- default_cols.each do |h|
637
- agg_cols[h] = default_agg_func
638
- end
639
-
640
- sorted_tab = order_by(group_cols)
641
- groups = sorted_tab.rows.group_by do |r|
642
- group_cols.map { |k| r[k] }
643
- end
644
- result = Table.new
645
- groups.each_pair do |_vals, grp_rows|
646
- result << row_from_group(grp_rows, group_cols, agg_cols)
647
- end
648
- result.normalize_boundaries
649
- result
650
- end
651
-
652
- ############################################################################
653
- # Footer methods
654
- ############################################################################
655
- def add_footer(label: 'Total', aggregate: :sum, heads: [])
656
- foot = {}
657
- heads.each do |h|
658
- raise "No #{h} column in table to #{aggregate}" unless headers.include?(h)
659
- foot[h] = column(h).send(aggregate)
660
- end
661
- @footers[label.as_sym] = foot
662
- self
663
- end
664
-
665
- def add_sum_footer(cols, label = 'Total')
666
- add_footer(heads: cols)
667
- end
668
-
669
- def add_avg_footer(cols, label = 'Average')
670
- add_footer(label: label, aggregate: :avg, heads: cols)
671
- end
672
-
673
- def add_min_footer(cols, label = 'Minimum')
674
- add_footer(label: label, aggregate: :min, heads: cols)
675
- end
676
-
677
- def add_max_footer(cols, label = 'Maximum')
678
- add_footer(label: label, aggregate: :max, heads: cols)
679
- end
680
-
681
761
  private
682
762
 
683
763
  # Return an output row appropriate to the given join type, including all the
@@ -693,9 +773,9 @@ module FatCore
693
773
  # Translate any remaining row_b heads to append '_b' if they have the
694
774
  # same name as a row_a key.
695
775
  a_heads = row_a.keys
696
- row_b = row_b.to_a.each.map do |k, v|
776
+ row_b = row_b.to_a.each.map { |k, v|
697
777
  [a_heads.include?(k) ? "#{k}_b".to_sym : k, v]
698
- end.to_h
778
+ }.to_h
699
779
  row_a.merge(row_b)
700
780
  end
701
781
 
@@ -814,6 +894,53 @@ module FatCore
814
894
  self
815
895
  end
816
896
 
897
+ ###################################################################################
898
+ # Group By
899
+ ###################################################################################
900
+
901
+ public
902
+
903
+ # Return a Table with a single row for each group of rows in the input table
904
+ # where the value of all columns named as simple symbols are equal. All
905
+ # other columns are set to the result of aggregating the values of that
906
+ # column within the group according to a aggregate function (:count, :sum,
907
+ # :min, :max, etc.), which defaults to the :first function, giving the value
908
+ # of that column for the first row in the group. You can specify a
909
+ # different aggregate function for a column by adding a hash parameter with
910
+ # the column as the key and a symbol for the aggregate function as the
911
+ # value. For example, consider the following call:
912
+ #
913
+ # tab.group_by(:date, :code, :price, shares: :sum, ).
914
+ #
915
+ # The first three parameters are simple symbols, so the table is divided
916
+ # into groups of rows in which the value of :date, :code, and :price are
917
+ # equal. The shares: hash parameter is set to the aggregate function :sum,
918
+ # so it will appear in the result as the sum of all the :shares values in
919
+ # each group. Any non-aggregate columns that have no aggregate function set
920
+ # default to using the aggregate function :first. Because of the way Ruby
921
+ # parses parameters to a method call, all the grouping symbols must appear
922
+ # first in the parameter list before any hash parameters.
923
+ def group_by(*group_cols, **agg_cols)
924
+ default_agg_func = :first
925
+ default_cols = headers - group_cols - agg_cols.keys
926
+ default_cols.each do |h|
927
+ agg_cols[h] = default_agg_func
928
+ end
929
+
930
+ sorted_tab = order_by(group_cols)
931
+ groups = sorted_tab.rows.group_by do |r|
932
+ group_cols.map { |k| r[k] }
933
+ end
934
+ result = Table.new
935
+ groups.each_pair do |_vals, grp_rows|
936
+ result << row_from_group(grp_rows, group_cols, agg_cols)
937
+ end
938
+ result.normalize_boundaries
939
+ result
940
+ end
941
+
942
+ private
943
+
817
944
  def row_from_group(rows, grp_cols, agg_cols)
818
945
  new_row = {}
819
946
  grp_cols.each do |h|
@@ -821,7 +948,7 @@ module FatCore
821
948
  end
822
949
  agg_cols.each_pair do |h, agg_func|
823
950
  items = rows.map { |r| r[h] }
824
- new_h = "#{agg_func}_#{h}"
951
+ new_h = "#{agg_func}_#{h}".as_sym
825
952
  new_row[new_h] = Column.new(header: h,
826
953
  items: items).send(agg_func)
827
954
  end
@@ -829,59 +956,16 @@ module FatCore
829
956
  end
830
957
 
831
958
  ############################################################################
832
- # Table output methods.
959
+ # Table construction methods.
833
960
  ############################################################################
834
961
 
835
962
  public
836
963
 
837
- # This returns the table as an Array of Arrays with formatting applied.
838
- # This would normally called after all calculations on the table are done
839
- # and you want to return the results. The Array of Arrays structure is
840
- # what org-mode src blocks will render as an org table in the buffer.
841
- def to_org(formats: {})
842
- result = []
843
-
844
- # Headers
845
- header_row = []
846
- headers.each do |hdr|
847
- header_row << hdr.entitle
848
- end
849
- result << header_row
850
- # This causes org to place an hline under the header row
851
- result << nil unless header_row.empty?
852
-
853
- # Body
854
- rows.each do |row|
855
- out_row = []
856
- headers.each do |hdr|
857
- out_row << row[hdr].format_by(formats[hdr])
858
- end
859
- result << out_row
860
- end
861
-
862
- # Footers
863
- footers.each_pair do |label, footer|
864
- foot_row = []
865
- columns.each do |col|
866
- hdr = col.header
867
- foot_row << footer[hdr].format_by(formats[hdr])
868
- end
869
- foot_row[0] = label.entitle
870
- result << foot_row
871
- end
872
- result
873
- end
874
-
875
- ############################################################################
876
- # Table construction methods.
877
- ############################################################################
878
-
879
964
  # Add a row represented by a Hash having the headers as keys. If mark is
880
965
  # true, mark this row as a boundary. All tables should be built ultimately
881
966
  # using this method as a primitive.
882
967
  def add_row(row, mark: false)
883
968
  row.each_pair do |k, v|
884
- binding.pry if k.nil?
885
969
  key = k.as_sym
886
970
  columns << Column.new(header: k) unless column?(k)
887
971
  column(key) << v
@@ -900,104 +984,5 @@ module FatCore
900
984
  columns << col
901
985
  self
902
986
  end
903
-
904
- class << self
905
-
906
- private
907
-
908
- # Construct table from an array of hashes or an array of any object that can
909
- # respond to #to_hash.
910
- def from_array_of_hashes(hashes)
911
- result = Table.new
912
- hashes.each do |hsh|
913
- if hsh.nil?
914
- result.mark_boundary
915
- next
916
- end
917
- result << hsh.to_h
918
- end
919
- result
920
- end
921
-
922
- # Construct a new table from an array of arrays. If the second element of
923
- # the array is a nil, a string that looks like an hrule, or an array whose
924
- # first element is a string that looks like an hrule, interpret the first
925
- # element of the array as a row of headers. Otherwise, synthesize headers of
926
- # the form "col1", "col2", ... and so forth. The remaining elements are
927
- # taken as the body of the table, except that if an element of the outer
928
- # array is a nil or a string that looks like an hrule, mark the preceding
929
- # row as a boundary.
930
- def from_array_of_arrays(rows)
931
- result = Table.new
932
- hrule_re = /\A\s*\|[-+]+/
933
- headers = []
934
- if rows[1].nil? || rows[1] =~ hrule_re || rows[1].first =~ hrule_re
935
- # Take the first row as headers
936
- # Use first row 0 as headers
937
- headers = rows[0].map(&:as_sym)
938
- first_data_row = 2
939
- else
940
- # Synthesize headers
941
- headers = (1..rows[0].size).to_a.map { |k| "col#{k}".as_sym }
942
- first_data_row = 0
943
- end
944
- rows[first_data_row..-1].each do |row|
945
- if row.nil? || row[0] =~ hrule_re
946
- result.mark_boundary
947
- next
948
- end
949
- row = row.map { |s| s.to_s.strip }
950
- hash_row = Hash[headers.zip(row)]
951
- result << hash_row
952
- end
953
- result
954
- end
955
-
956
- def from_csv_io(io)
957
- result = new
958
- ::CSV.new(io, headers: true, header_converters: :symbol,
959
- skip_blanks: true).each do |row|
960
- result << row.to_h
961
- end
962
- result
963
- end
964
-
965
- # Form rows of table by reading the first table found in the org file.
966
- def from_org_io(io)
967
- table_re = /\A\s*\|/
968
- hrule_re = /\A\s*\|[-+]+/
969
- rows = []
970
- table_found = false
971
- header_found = false
972
- io.each do |line|
973
- unless table_found
974
- # Skip through the file until a table is found
975
- next unless line =~ table_re
976
- unless line =~ hrule_re
977
- line = line.sub(/\A\s*\|/, '').sub(/\|\s*\z/, '')
978
- rows << line.split('|').map(&:clean)
979
- end
980
- table_found = true
981
- next
982
- end
983
- break unless line =~ table_re
984
- if !header_found && line =~ hrule_re
985
- rows << nil
986
- header_found = true
987
- next
988
- elsif header_found && line =~ hrule_re
989
- # Mark the boundary with a nil
990
- rows << nil
991
- elsif line !~ table_re
992
- # Stop reading at the second hline
993
- break
994
- else
995
- line = line.sub(/\A\s*\|/, '').sub(/\|\s*\z/, '')
996
- rows << line.split('|').map(&:clean)
997
- end
998
- end
999
- from_array_of_arrays(rows)
1000
- end
1001
- end
1002
987
  end
1003
988
  end
@@ -1,7 +1,7 @@
1
1
  module FatCore
2
2
  MAJOR = 1
3
- MINOR = 6
4
- PATCH = 0
3
+ MINOR = 7
4
+ PATCH = 1
5
5
 
6
6
  VERSION = [MAJOR, MINOR, PATCH].compact.join('.')
7
7
  end
data/lib/fat_core.rb CHANGED
@@ -2,6 +2,7 @@
2
2
  require 'date'
3
3
  require 'active_support'
4
4
  require 'active_support/core_ext'
5
+ require 'active_support/number_helper'
5
6
  require 'csv'
6
7
 
7
8
  require 'fat_core/version'
@@ -22,3 +23,4 @@ require 'fat_core/symbol'
22
23
  require 'fat_core/evaluator'
23
24
  require 'fat_core/column'
24
25
  require 'fat_core/table'
26
+ require 'fat_core/formatters'