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