fat_core 1.6.0 → 1.7.1

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