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.
- checksums.yaml +4 -4
- data/lib/fat_core/column.rb +71 -9
- data/lib/fat_core/date.rb +6 -6
- data/lib/fat_core/enumerable.rb +12 -1
- data/lib/fat_core/formatters/aoa_formatter.rb +84 -0
- data/lib/fat_core/formatters/aoh_formatter.rb +82 -0
- data/lib/fat_core/formatters/formatter.rb +973 -0
- data/lib/fat_core/formatters/org_formatter.rb +72 -0
- data/lib/fat_core/formatters/text_formatter.rb +91 -0
- data/lib/fat_core/formatters.rb +5 -0
- data/lib/fat_core/hash.rb +13 -0
- data/lib/fat_core/numeric.rb +3 -3
- data/lib/fat_core/period.rb +5 -1
- data/lib/fat_core/string.rb +20 -0
- data/lib/fat_core/symbol.rb +1 -1
- data/lib/fat_core/table.rb +251 -266
- data/lib/fat_core/version.rb +2 -2
- data/lib/fat_core.rb +2 -0
- data/spec/lib/column_spec.rb +24 -8
- data/spec/lib/formatters/aoa_formatter_spec.rb +62 -0
- data/spec/lib/formatters/aoh_formatter_spec.rb +61 -0
- data/spec/lib/formatters/formatter_spec.rb +371 -0
- data/spec/lib/formatters/org_formatter_spec.rb +60 -0
- data/spec/lib/formatters/text_formatter_spec.rb +60 -0
- data/spec/lib/period_spec.rb +9 -2
- data/spec/lib/symbol_spec.rb +1 -1
- data/spec/lib/table_spec.rb +86 -167
- metadata +18 -2
data/lib/fat_core/table.rb
CHANGED
@@ -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
|
4
|
-
#
|
5
|
-
# convertible into one of
|
6
|
-
# single column contains cells of different types. Any cell that cannot
|
7
|
-
# parsed as one of the
|
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
|
-
#
|
14
|
-
#
|
15
|
-
#
|
16
|
-
#
|
17
|
-
#
|
18
|
-
#
|
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
|
26
|
+
attr_reader :columns
|
43
27
|
|
44
|
-
def initialize
|
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
|
-
|
55
|
-
|
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
|
-
|
65
|
-
|
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
|
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
|
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
|
-
|
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
|
-
#
|
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
|
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
|
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
|
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
|
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
|
281
|
-
#
|
282
|
-
#
|
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
|
-
|
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
|
776
|
+
row_b = row_b.to_a.each.map { |k, v|
|
697
777
|
[a_heads.include?(k) ? "#{k}_b".to_sym : k, v]
|
698
|
-
|
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
|
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
|
data/lib/fat_core/version.rb
CHANGED
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'
|