fat_core 1.0.3 → 1.2.0

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,16 +1,16 @@
1
1
  require 'spec_helper'
2
2
 
3
3
  describe Symbol do
4
- it "should be able to convert to a capitalized string" do
4
+ it 'should be able to convert to a capitalized string' do
5
5
  expect(:i_am_a_symbol.entitle).to eq 'I Am a Symbol'
6
6
  expect(:i_am_a_symbol.to_string).to eq 'I Am a Symbol'
7
7
  end
8
8
 
9
- it "should respond to tex_quote" do
9
+ it 'should respond to tex_quote' do
10
10
  expect(:i_am_a_symbol.tex_quote).to eq 'i\\_am\\_a\\_symbol'
11
11
  end
12
12
 
13
- it "should respond to :as_sym as identity" do
13
+ it 'should respond to :as_sym as identity' do
14
14
  expect(:i_am_a_symbol.as_sym).to eq :i_am_a_symbol
15
15
  end
16
16
  end
@@ -0,0 +1,659 @@
1
+ require 'spec_helper'
2
+
3
+ module FatCore
4
+ describe Table do
5
+ before :all do
6
+ @csv_file_body = <<-EOS
7
+ Ref,Date,Code,RawShares,Shares,Price,Info
8
+ 1,2006-05-02,P,5000,5000,8.6000,2006-08-09-1-I
9
+ 2,2006-05-03,P,5000,5000,8.4200,2006-08-09-1-I
10
+ 3,2006-05-04,P,5000,5000,8.4000,2006-08-09-1-I
11
+ 4,2006-05-10,P,8600,8600,8.0200,2006-08-09-1-D
12
+ 5,2006-05-12,P,10000,10000,7.2500,2006-08-09-1-D
13
+ 6,2006-05-12,P,2000,2000,6.7400,2006-08-09-1-I
14
+ 7,2006-05-16,P,5000,5000,7.0000,2006-08-09-1-D
15
+ 8,2006-05-17,P,5000,5000,6.7000,2006-08-09-1-D
16
+ 9,2006-05-17,P,2000,2000,6.7400,2006-08-09-1-I
17
+ 10,2006-05-19,P,1000,1000,7.2500,2006-08-09-1-I
18
+ 11,2006-05-19,P,1000,1000,7.2500,2006-08-09-1-I
19
+ 12,2006-05-31,P,2000,2000,7.9200,2006-08-09-1-I
20
+ 13,2006-06-05,P,1000,1000,7.9200,2006-08-09-1-I
21
+ 14,2006-06-15,P,5000,5000,6.9800,2006-08-09-1-I
22
+ 15,2006-06-16,P,1000,1000,6.9300,2006-08-09-1-I
23
+ 16,2006-06-16,P,1000,1000,6.9400,2006-08-09-1-I
24
+ 17,2006-06-29,P,4000,4000,7.0000,2006-08-09-1-I
25
+ 18,2006-07-14,P,2000,2000,6.2000,2006-08-09-1-D
26
+ 19,2006-08-03,P,1400,1400,4.3900,2006-08-09-1-D
27
+ 20,2006-08-07,P,1100,1100,4.5900,2006-08-09-1-D
28
+ 21,2006-08-08,P,16000,16000,4.7000,2006-08-21-1-D
29
+ 22,2006-08-08,S,16000,16000,4.7000,2006-08-21-1-I
30
+ 23,2006-08-15,P,16000,16000,4.8000,2006-08-21-1-D
31
+ 24,2006-08-15,S,16000,16000,4.8000,2006-08-21-1-I
32
+ 25,2006-08-16,P,2100,2100,5.2900,2006-08-21-1-I
33
+ 26,2006-08-17,P,2900,2900,5.7000,2006-08-21-1-I
34
+ 27,2006-08-23,P,8000,8000,5.2400,2006-08-25-1-D
35
+ 28,2006-08-23,S,8000,8000,5.2400,2006-08-29-1-I
36
+ 29,2006-08-28,P,1000,1000,5.4000,2006-08-29-1-D
37
+ 30,2006-08-29,P,2000,2000,5.4000,2006-08-30-1-D
38
+ 31,2006-08-29,S,2000,2000,5.4000,2006-08-30-1-I
39
+ 32,2006-09-05,P,2700,2700,5.7500,2006-09-06-1-I
40
+ 33,2006-09-11,P,4000,4000,5.7200,2006-09-15-1-D
41
+ 34,2006-09-11,S,4000,4000,5.7200,2006-09-15-1-I
42
+ 35,2006-09-12,P,3000,3000,5.4800,2006-09-15-2-I
43
+ 36,2006-09-13,P,1700,1700,5.4100,2006-09-15-2-I
44
+ 37,2006-09-20,P,7500,7500,5.4900,2006-09-21-1-I
45
+ 38,2006-12-07,S,6000,6000,7.8900,2006-12-11-1-I
46
+ 39,2006-12-11,S,100,100,8.0000,2006-12-11-1-I
47
+ 40,2007-01-29,P,2500,2500,12.1000,2007-04-27-1-I
48
+ 41,2007-01-31,P,2500,2500,13.7000,2007-04-27-1-I
49
+ 42,2007-02-02,P,4000,4000,15.1500,2007-04-27-1-I
50
+ 43,2007-02-06,P,5000,5000,14.9500,2007-04-27-1-I
51
+ 44,2007-02-07,P,400,400,15.0000,2007-04-27-1-I
52
+ 45,2007-02-08,P,4600,4600,15.0000,2007-04-27-1-I
53
+ 46,2007-02-12,P,3500,3500,14.9100,2007-04-27-1-I
54
+ 47,2007-02-13,P,1500,1500,14.6500,2007-04-27-1-D
55
+ 48,2007-02-14,P,2000,2000,14.4900,2007-04-27-1-D
56
+ 49,2007-02-15,P,3000,3000,14.3000,2007-04-27-1-I
57
+ 50,2007-02-21,P,8500,8500,14.6500,2007-04-27-1-D
58
+ 51,2007-02-21,S,8500,8500,14.6500,2007-04-27-1-I
59
+ 52,2007-02-22,P,1500,1500,14.8800,2007-04-27-1-I
60
+ 53,2007-02-23,P,3000,3000,14.9700,2007-04-27-1-I
61
+ 54,2007-02-23,P,5000,5000,14.9700,2007-04-27-1-I
62
+ 55,2007-02-27,P,5200,5200,13.8800,2007-04-27-1-I
63
+ 56,2007-02-28,P,6700,6700,13.0000,2007-04-27-1-D
64
+ 57,2007-02-28,P,800,800,13.0000,2007-04-27-1-I
65
+ 58,2007-02-28,P,8400,8400,13.0000,2007-04-27-1-I
66
+ 59,2007-03-01,P,2500,2500,12.2500,2007-04-27-1-D
67
+ 60,2007-03-05,P,1800,1800,11.9700,2007-04-27-1-D
68
+ 61,2007-03-06,P,500,500,12.1300,2007-04-27-1-D
69
+ 62,2007-03-07,P,3000,3000,12.3700,2007-04-27-1-D
70
+ 63,2007-03-08,P,2000,2000,12.6000,2007-04-27-1-I
71
+ 64,2007-03-09,P,7700,7700,12.8100,2007-04-27-1-I
72
+ 65,2007-03-12,P,4200,4200,12.4600,2007-04-27-1-I
73
+ 66,2007-03-13,P,800,800,12.2500,2007-04-27-1-I
74
+ 67,2007-03-19,P,2000,2000,14.5500,2007-04-27-2-I
75
+ 68,2007-03-19,P,5000,5000,14.5500,2007-04-27-2-I
76
+ 69,2007-03-19,P,2000,2000,14.3300,2007-04-27-2-I
77
+ 70,2007-03-20,P,1000,1000,14.4600,2007-04-27-2-I
78
+ 71,2007-03-20,P,1500,1500,14.4600,2007-04-27-2-I
79
+ 72,2007-03-21,P,3900,3900,16.9000,2007-04-27-2-I
80
+ 73,2007-03-23,P,8000,8000,14.9700,2007-04-27-1-D
81
+ 74,2007-03-27,P,1000,1000,16.9300,2007-04-27-2-I
82
+ 75,2007-03-28,P,1000,1000,16.5000,2007-04-27-2-D
83
+ 76,2007-03-29,P,1000,1000,16.2500,2007-04-27-2-D
84
+ 77,2007-04-04,P,200,200,17.8600,2007-04-27-2-I
85
+ 78,2007-04-04,P,2000,2000,19.5000,2007-04-27-2-I
86
+ 79,2007-04-04,P,3000,3000,19.1300,2007-04-27-2-I
87
+ 80,2007-04-05,P,1000,1000,19.1500,2007-04-27-2-I
88
+ 81,2007-04-10,P,2000,2000,20.7500,2007-04-27-2-I
89
+ 82,2007-04-11,P,1000,1000,20.5000,2007-04-27-2-I
90
+ 83,2007-04-12,P,600,600,21.5000,2007-04-27-2-I
91
+ 84,2007-04-12,P,1000,1000,21.4500,2007-04-27-2-I
92
+ 85,2007-04-13,P,2100,2100,21.5000,2007-04-27-2-I
93
+ 86,2007-04-16,P,500,500,22.6000,2007-04-27-2-I
94
+ 87,2007-04-17,P,3500,3500,23.5500,2007-04-27-2-D
95
+ 88,2007-04-17,S,3500,3500,23.5500,2007-04-27-2-I
96
+ 89,2007-04-23,P,5000,5000,23.4500,2007-04-27-2-I
97
+ 90,2007-04-24,P,5000,5000,24.3000,2007-04-27-2-I
98
+ 91,2007-04-25,S,10000,10000,25.7000,2007-04-27-2-I
99
+ EOS
100
+
101
+ @org_file_body = <<-EOS
102
+
103
+ * Morgan Transactions
104
+ :PROPERTIES:
105
+ :TABLE_EXPORT_FILE: morgan.csv
106
+ :END:
107
+
108
+ #+TBLNAME: morgan_tab
109
+ | Ref | Date | Code | Raw | Shares | Price | Info |
110
+ |-----+------------+------+---------+--------+----------+--------|
111
+ | 29 | 2013-05-02 | P | 795,546 | 2,609 | 1.18500 | ZMPEF1 |
112
+ | 30 | 2013-05-02 | P | 118,186 | 388 | 11.85000 | ZMPEF1 |
113
+ | 31 | 2013-05-02 | P | 340,948 | 1,926 | 1.18500 | ZMPEF2 |
114
+ | 32 | 2013-05-02 | P | 50,651 | 286 | 11.85000 | ZMPEF2 |
115
+ | 33 | 2013-05-20 | S | 12,000 | 32 | 28.28040 | ZMEAC |
116
+ | 34 | 2013-05-20 | S | 85,000 | 226 | 28.32240 | ZMEAC |
117
+ | 35 | 2013-05-20 | S | 33,302 | 88 | 28.63830 | ZMEAC |
118
+ | 36 | 2013-05-23 | S | 8,000 | 21 | 27.10830 | ZMEAC |
119
+ | 37 | 2013-05-23 | S | 23,054 | 61 | 26.80150 | ZMEAC |
120
+ | 38 | 2013-05-23 | S | 39,906 | 106 | 25.17490 | ZMEAC |
121
+ | 39 | 2013-05-29 | S | 13,459 | 36 | 24.74640 | ZMEAC |
122
+ | 40 | 2013-05-29 | S | 15,700 | 42 | 24.77900 | ZMEAC |
123
+ | 41 | 2013-05-29 | S | 15,900 | 42 | 24.58020 | ZMEAC |
124
+ | 42 | 2013-05-30 | S | 6,679 | 18 | 25.04710 | ZMEAC |
125
+
126
+ * Another Heading
127
+ EOS
128
+ end
129
+
130
+ describe 'construction' do
131
+ it 'should be create-able from a CSV IO object' do
132
+ tab = Table.new(StringIO.new(@csv_file_body), '.csv')
133
+ expect(tab.class).to eq(Table)
134
+ expect(tab.rows.size).to be > 20
135
+ expect(tab.headers.sort)
136
+ .to eq [:code, :date, :info, :price, :rawshares, :ref, :shares]
137
+ tab.rows.each do |row|
138
+ row.each_pair do |k, _v|
139
+ expect(k.class).to eq Symbol
140
+ end
141
+ expect(row[:code].class).to eq String
142
+ expect(row[:date].class).to eq Date
143
+ expect(row[:shares].is_a?(Numeric)).to be true
144
+ unless row[:rawshares].nil?
145
+ expect(row[:rawshares].is_a?(Numeric)).to be true
146
+ end
147
+ expect(row[:price].is_a?(Numeric)).to be true
148
+ expect([Numeric, String].any? { |t| row[:ref].is_a?(t) }).to be true
149
+ end
150
+ end
151
+
152
+ it 'should be create-able from an Org IO object' do
153
+ tab = Table.new(StringIO.new(@org_file_body), '.org')
154
+ expect(tab.class).to eq(Table)
155
+ expect(tab.rows.size).to be > 10
156
+ expect(tab.headers.sort)
157
+ .to eq [:code, :date, :info, :price, :raw, :ref, :shares]
158
+ tab.rows.each do |row|
159
+ row.each_pair do |k, _v|
160
+ expect(k.class).to eq Symbol
161
+ end
162
+ expect(row[:code].class).to eq String
163
+ expect(row[:date].class).to eq Date
164
+ expect(row[:shares].is_a?(Numeric)).to be true
165
+ unless row[:rawshares].nil?
166
+ expect(row[:rawshares].is_a?(Numeric)).to be true
167
+ end
168
+ expect(row[:price].is_a?(BigDecimal)).to be true
169
+ expect([Numeric, String].any? { |t| row[:ref].is_a?(t) }).to be true
170
+ expect(row[:info].class).to eq String
171
+ end
172
+ end
173
+
174
+ it 'should be create-able from a CSV file' do
175
+ File.open('/tmp/junk.csv', 'w') { |f| f.write(@csv_file_body) }
176
+ tab = Table.new('/tmp/junk.csv')
177
+ expect(tab.class).to eq(Table)
178
+ expect(tab.rows.size).to be > 20
179
+ expect(tab.headers.sort)
180
+ .to eq [:code, :date, :info, :price, :rawshares, :ref, :shares]
181
+ tab.rows.each do |row|
182
+ row.each_pair do |k, _v|
183
+ expect(k.class).to eq Symbol
184
+ end
185
+ expect(row[:code].class).to eq String
186
+ expect(row[:date].class).to eq Date
187
+ expect(row[:shares].is_a?(Numeric)).to be true
188
+ unless row[:rawshares].nil?
189
+ expect(row[:rawshares].is_a?(Numeric)).to be true
190
+ end
191
+ expect(row[:price].is_a?(BigDecimal)).to be true
192
+ expect([Numeric, String].any? { |t| row[:ref].is_a?(t) }).to be true
193
+ expect(row[:info].class).to eq Date
194
+ end
195
+ end
196
+
197
+ it 'should be create-able from an Org IO object' do
198
+ File.open('/tmp/junk.org', 'w') { |f| f.write(@org_file_body) }
199
+ tab = Table.new('/tmp/junk.org')
200
+ expect(tab.class).to eq(Table)
201
+ expect(tab.rows.size).to be > 10
202
+ expect(tab.rows[0].keys.sort)
203
+ .to eq [:code, :date, :info, :price, :raw, :ref, :shares]
204
+ tab.rows.each do |row|
205
+ row.each_pair do |k, _v|
206
+ expect(k.class).to eq Symbol
207
+ end
208
+ expect(row[:code].class).to eq String
209
+ expect(row[:date].class).to eq Date
210
+ expect(row[:shares].is_a?(Numeric)).to be true
211
+ unless row[:rawshares].nil?
212
+ expect(row[:rawshares].is_a?(Numeric)).to be true
213
+ end
214
+ expect(row[:price].is_a?(BigDecimal)).to be true
215
+ expect([Numeric, String].any? { |t| row[:ref].is_a?(t) }).to be true
216
+ expect(row[:info].class).to eq String
217
+ end
218
+ end
219
+
220
+ it 'should be create-able from an Array of Arrays with header and hrule' do
221
+ # rubocop:disable Style/WordArray
222
+ aoa = [
223
+ ['First', 'Second', 'Third'],
224
+ ['|---------+----------+---------|', nil, nil],
225
+ ['1', '2', '3.2'],
226
+ ['4', '5', '6.4'],
227
+ ['7', '8', '9.0'],
228
+ [10, 11, 12.1]
229
+ ]
230
+ tab = Table.new(aoa)
231
+ expect(tab.class).to eq(Table)
232
+ expect(tab.rows.size).to eq(4)
233
+ expect(tab.rows[0].keys.sort).to eq [:first, :second, :third]
234
+ tab.rows.each do |row|
235
+ row.each_pair do |k, _v|
236
+ expect(k.class).to eq Symbol
237
+ end
238
+ expect(row[:first].is_a?(Numeric)).to be true
239
+ expect(row[:second].is_a?(Numeric)).to be true
240
+ expect(row[:third].is_a?(BigDecimal)).to be true
241
+ end
242
+ end
243
+
244
+ it 'should be create-able from an Array of Arrays with header no hrule' do
245
+ aoa = [
246
+ ['First', 'Second', 'Third'],
247
+ ['1', '2', '3.2'],
248
+ ['4', '5', '6.4'],
249
+ ['7', '8', '9.0'],
250
+ [10, 11, 12.1]
251
+ ]
252
+ tab = Table.new(aoa)
253
+ expect(tab.class).to eq(Table)
254
+ expect(tab.rows.size).to eq(4)
255
+ expect(tab.headers.sort).to eq [:first, :second, :third]
256
+ tab.rows.each do |row|
257
+ row.each_pair do |k, _v|
258
+ expect(k.class).to eq Symbol
259
+ end
260
+ expect(row[:first].is_a?(Numeric)).to be true
261
+ expect(row[:second].is_a?(Numeric)).to be true
262
+ expect(row[:third].is_a?(BigDecimal)).to be true
263
+ end
264
+ end
265
+
266
+ it 'should be create-able from an Array of Arrays sans Header' do
267
+ aoa = [
268
+ ['1', '2', '3.2'],
269
+ ['4', '5', '6.4'],
270
+ ['7', '8', '9.0'],
271
+ [7, 8, 9.3]
272
+ ]
273
+ # rubocop:enable Style/WordArray
274
+ tab = Table.new(aoa)
275
+ expect(tab.class).to eq(Table)
276
+ expect(tab.rows.size).to eq(4)
277
+ expect(tab.headers.sort).to eq [:col1, :col2, :col3]
278
+ tab.rows.each do |row|
279
+ row.each_pair do |k, _v|
280
+ expect(k.class).to eq Symbol
281
+ end
282
+ expect(row[:col1].is_a?(Numeric)).to be true
283
+ expect(row[:col2].is_a?(Numeric)).to be true
284
+ expect(row[:col3].is_a?(BigDecimal)).to be true
285
+ end
286
+ end
287
+
288
+ it 'should be create-able from an Array of Hashes' do
289
+ aoh = [
290
+ { a: '1', 'Two words' => '2', c: '3.2' },
291
+ { a: '4', 'Two words' => '5', c: '6.4' },
292
+ { a: '7', 'Two words' => '8', c: '9.0' },
293
+ { a: 10, 'Two words' => 11, c: 12.4 }
294
+ ]
295
+ tab = Table.new(aoh)
296
+ expect(tab.class).to eq(Table)
297
+ expect(tab.rows.size).to eq(4)
298
+ expect(tab.rows[0].keys.sort).to eq [:a, :c, :two_words]
299
+ tab.rows.each do |row|
300
+ row.each_pair do |k, _v|
301
+ expect(k.class).to eq Symbol
302
+ end
303
+ expect(row[:a].is_a?(Numeric)).to be true
304
+ expect(row[:two_words].is_a?(Numeric)).to be true
305
+ expect(row[:c].is_a?(BigDecimal)).to be true
306
+ end
307
+ end
308
+ end
309
+
310
+ describe 'column operations' do
311
+ it 'should be able to sum a column' do
312
+ aoh = [
313
+ { a: '1', 'Two words' => '2', c: '3,123', d: 'apple' },
314
+ { a: '4', 'Two words' => '5', c: '6,412', d: 'orange' },
315
+ { a: '7', 'Two words' => '8', c: '$9,888', d: 'pear' }
316
+ ]
317
+ tab = Table.new(aoh)
318
+ expect(tab[:a].sum).to eq 12
319
+ expect(tab[:two_words].sum).to eq 15
320
+ expect(tab[:c].sum).to eq 19_423
321
+ expect(tab[:d].sum).to eq 'appleorangepear'
322
+ end
323
+
324
+ it 'should be able to sum a column ignoring nils' do
325
+ aoh = [
326
+ { a: '', 'Two words' => '2', c: '', d: 'apple' },
327
+ { a: '4', 'Two words' => '5', c: '6,412', d: 'orange' },
328
+ { a: '7', 'Two words' => '8', c: '$9,888', d: 'pear' }
329
+ ]
330
+ tab = Table.new(aoh)
331
+ expect(tab[:a].sum).to eq 11
332
+ expect(tab[:two_words].sum).to eq 15
333
+ expect(tab[:c].sum).to eq 16_300
334
+ expect(tab[:d].sum).to eq 'appleorangepear'
335
+ end
336
+
337
+ it 'should be able to report its headings' do
338
+ tab = Table.new(StringIO.new(@csv_file_body), '.csv')
339
+ expect(tab.headers.sort)
340
+ .to eq [:code, :date, :info, :price, :rawshares, :ref, :shares]
341
+ end
342
+
343
+ it 'should be able to extract a column as an array' do
344
+ aoh = [
345
+ { a: '1', 'Two words' => '2', c: '3,123', d: 'apple' },
346
+ { a: '4', 'Two words' => '5', c: '6,412', d: 'orange' },
347
+ { a: '7', 'Two words' => '8', c: '$9,888', d: 'pear' }
348
+ ]
349
+ tab = Table.new(aoh)
350
+ expect(tab[:a].to_a).to eq [1, 4, 7]
351
+ expect(tab[:c].to_a).to eq [3123, 6412, 9888]
352
+ end
353
+
354
+ it 'should be able to sum a column' do
355
+ aoh = [
356
+ { a: '1', 'Two words' => '2', c: '3,123', d: 'apple' },
357
+ { a: '4', 'Two words' => '5', c: '6,412', d: 'orange' },
358
+ { a: '7', 'Two words' => '8', c: '$9,888', d: 'pear' }
359
+ ]
360
+ tab = Table.new(aoh)
361
+ expect(tab[:a].sum).to eq 12
362
+ expect(tab[:c].sum).to eq 19_423
363
+ expect(tab[:c].sum.is_a?(Integer)).to be true
364
+ end
365
+
366
+ it 'should be able to average a column' do
367
+ aoh = [
368
+ { a: '1', 'Two words' => '2', c: '3,123', d: 'apple' },
369
+ { a: '4', 'Two words' => '5', c: '6,412', d: 'orange' },
370
+ { a: '7', 'Two words' => '8', c: '$9,888', d: 'pear' }
371
+ ]
372
+ tab = Table.new(aoh)
373
+ expect(tab[:a].avg).to eq 4
374
+ expect(tab[:c].avg.round(4)).to eq 6474.3333
375
+ expect(tab[:c].avg.class).to eq BigDecimal
376
+ end
377
+
378
+ it 'should be able to get column minimum' do
379
+ aoh = [
380
+ { a: '1', 'Two words' => '2', c: '3,123', d: 'apple' },
381
+ { a: '4', 'Two words' => '5', c: '6,412', d: 'orange' },
382
+ { a: '7', 'Two words' => '8', c: '$9,888', d: 'pear' }
383
+ ]
384
+ tab = Table.new(aoh)
385
+ expect(tab[:a].min).to eq 1
386
+ expect(tab[:c].min.round(4)).to eq 3123
387
+ expect(tab[:c].min.is_a?(Integer)).to be true
388
+ expect(tab[:d].min).to eq 'apple'
389
+ end
390
+
391
+ it 'should be able to get column maximum' do
392
+ aoh = [
393
+ { a: '1', 'Two words' => '2', c: '3,123', d: 'apple' },
394
+ { a: '4', 'Two words' => '5', c: '6,412', d: 'orange' },
395
+ { a: '7', 'Two words' => '8', c: '$9,888', d: 'pear' }
396
+ ]
397
+ tab = Table.new(aoh)
398
+ expect(tab[:a].max).to eq 7
399
+ expect(tab[:c].max.round(4)).to eq 9888
400
+ expect(tab[:c].max.is_a?(Integer)).to be true
401
+ expect(tab[:d].max).to eq 'pear'
402
+ end
403
+ end
404
+
405
+ describe 'footers' do
406
+ it 'should be able to add a total footer to the table' do
407
+ aoh = [
408
+ { a: '1', 'Two words' => '2', c: '3,123', d: 'apple' },
409
+ { a: '4', 'Two words' => '5', c: '6,412', d: 'orange' },
410
+ { a: '7', 'Two words' => '8', c: '$9,888', d: 'pear' }
411
+ ]
412
+ tab = Table.new(aoh)
413
+ tab.add_sum_footer([:a, :c, :two_words])
414
+ expect(tab.footers[:total][:a]).to eq 12
415
+ expect(tab.footers[:total][:c]).to eq 19_423
416
+ expect(tab.footers[:total][:two_words]).to eq 15
417
+ expect(tab.footers[:total][:d]).to be_nil
418
+ end
419
+
420
+ it 'should be able to add an average footer to the table' do
421
+ aoh = [
422
+ { a: '1', 'Two words' => '2', c: '3,123', d: 'apple' },
423
+ { a: '4', 'Two words' => '5', c: '6,412', d: 'orange' },
424
+ { a: '7', 'Two words' => '8', c: '$9,888', d: 'pear' }
425
+ ]
426
+ tab = Table.new(aoh)
427
+ tab.add_avg_footer([:a, :c, :two_words])
428
+ expect(tab.footers[:average][:a]).to eq 4
429
+ expect(tab.footers[:average][:c].round(4)).to eq 6474.3333
430
+ expect(tab.footers[:average][:two_words]).to eq 5
431
+ expect(tab.footers[:average][:d]).to be_nil
432
+ end
433
+
434
+ it 'should be able to add a minimum footer to the table' do
435
+ aoh = [
436
+ { a: '1', 'Two words' => '2', c: '3,123', d: 'apple' },
437
+ { a: '4', 'Two words' => '5', c: '6,412', d: 'orange' },
438
+ { a: '7', 'Two words' => '8', c: '$9,888', d: 'pear' }
439
+ ]
440
+ tab = Table.new(aoh)
441
+ tab.add_min_footer([:a, :c, :two_words])
442
+ expect(tab.footers[:minimum][:a]).to eq 1
443
+ expect(tab.footers[:minimum][:c]).to eq 3123
444
+ expect(tab.footers[:minimum][:two_words]).to eq 2
445
+ expect(tab.footers[:minimum][:d]).to be_nil
446
+ end
447
+
448
+ it 'should be able to add a maximum footer to the table' do
449
+ aoh = [
450
+ { a: '1', 'Two words' => '2', c: '3,123', d: 'apple' },
451
+ { a: '4', 'Two words' => '5', c: '6,412', d: 'orange' },
452
+ { a: '7', 'Two words' => '8', c: '$9,888', d: 'pear' }
453
+ ]
454
+ tab = Table.new(aoh)
455
+ tab.add_max_footer([:a, :c, :two_words])
456
+ expect(tab.footers[:maximum][:a]).to eq 7
457
+ expect(tab.footers[:maximum][:c]).to eq 9888
458
+ expect(tab.footers[:maximum][:two_words]).to eq 8
459
+ expect(tab.footers[:maximum][:d]).to be_nil
460
+ end
461
+ end
462
+
463
+ describe 'sorting' do
464
+ it 'should be able to sort its rows on one column' do
465
+ aoh = [
466
+ { a: '5', 'Two words' => '20', c: '3,123', d: 'apple' },
467
+ { a: '4', 'Two words' => '5', c: '6,412', d: 'orange' },
468
+ { a: '7', 'Two words' => '8', c: '$1,888', d: 'apple' }
469
+ ]
470
+ tab = Table.new(aoh).order_by(:a)
471
+ expect(tab.rows[0][:a]).to eq 4
472
+ end
473
+
474
+ it 'should be able to sort its rows on multiple columns' do
475
+ aoh = [
476
+ { a: '5', 'Two words' => '20', c: '3,123', d: 'apple' },
477
+ { a: '4', 'Two words' => '5', c: '6,412', d: 'orange' },
478
+ { a: '7', 'Two words' => '8', c: '$1,888', d: 'apple' }
479
+ ]
480
+ tab = Table.new(aoh).order_by(:d, :c)
481
+ expect(tab.rows[0][:a]).to eq 7
482
+ end
483
+
484
+ it 'should be able to reverse sort its rows on one column' do
485
+ aoh = [
486
+ { a: '5', 'Two words' => '20', c: '3,123', d: 'apple' },
487
+ { a: '4', 'Two words' => '5', c: '6,412', d: 'orange' },
488
+ { a: '7', 'Two words' => '8', c: '$1,888', d: 'apple' }
489
+ ]
490
+ tab = Table.new(aoh).order_by(:d!)
491
+ expect(tab.rows[0][:d]).to eq 'orange'
492
+ expect(tab.rows[2][:d]).to eq 'apple'
493
+ end
494
+
495
+ it 'should sort its rows on mixed forward and reverse columns' do
496
+ aoh = [
497
+ { a: '5', 'Two words' => '20', c: '3,123', d: 'apple' },
498
+ { a: '4', 'Two words' => '5', c: 6412, d: 'orange' },
499
+ { a: '7', 'Two words' => '8', c: '$1,888', d: 'apple' }
500
+ ]
501
+ tab = Table.new(aoh).order_by(:d!, :c)
502
+ expect(tab.rows[0][:d]).to eq 'orange'
503
+ expect(tab.rows[1][:d]).to eq 'apple'
504
+ expect(tab.rows[1][:c]).to eq 1888
505
+ expect(tab.rows[2][:d]).to eq 'apple'
506
+ end
507
+ end
508
+
509
+ describe 'union' do
510
+ it 'should be able to union with a compatible table' do
511
+ aoh = [
512
+ { a: '5', 'Two words' => '20', c: '3,123', d: 'apple' },
513
+ { a: '4', 'Two words' => '5', c: 6412, d: 'orange' },
514
+ { a: '7', 'Two words' => '8', c: '$1,888', d: 'apple' }
515
+ ]
516
+ tab1 = Table.new(aoh)
517
+ aoh2 = [
518
+ { t: '8', 'Two worlds' => '65', s: '5,143', u: 'kiwi' },
519
+ { t: '87', 'Two worlds' => '12', s: 412, u: 'banana' },
520
+ { t: '13', 'Two worlds' => '11', s: '$1,821', u: 'grape' }
521
+ ]
522
+ tab2 = Table.new(aoh2)
523
+ utab = tab1.union(tab2)
524
+ expect(utab.rows.size).to eq(6)
525
+ end
526
+
527
+ it 'should throw an exception for union with different sized tables' do
528
+ aoh = [
529
+ { a: '5', 'Two words' => '20', c: '3,123' },
530
+ { a: '4', 'Two words' => '5', c: 6412 },
531
+ { a: '7', 'Two words' => '8', c: '$1,888' }
532
+ ]
533
+ tab1 = Table.new(aoh)
534
+ aoh2 = [
535
+ { t: '8', 'Two worlds' => '65', s: '5,143', u: 'kiwi' },
536
+ { t: '87', 'Two worlds' => '12', s: 412, u: 'banana' },
537
+ { t: '13', 'Two worlds' => '11', s: '$1,821', u: 'grape' }
538
+ ]
539
+ tab2 = Table.new(aoh2)
540
+ expect {
541
+ tab1.union(tab2)
542
+ }.to raise_error(/different number of columns/)
543
+ end
544
+
545
+ it 'should throw an exception for union with different types' do
546
+ aoh = [
547
+ { a: '5', 'Two words' => '20', s: '5143', c: '3123' },
548
+ { a: '4', 'Two words' => '5', s: 412, c: 6412 },
549
+ { a: '7', 'Two words' => '8', s: '$1821', c: '$1888' }
550
+ ]
551
+ tab1 = Table.new(aoh)
552
+ aoh2 = [
553
+ { t: '8', 'Two worlds' => '65', s: '2016-01-17', u: 'kiwi' },
554
+ { t: '87', 'Two worlds' => '12', s: Date.today, u: 'banana' },
555
+ { t: '13', 'Two worlds' => '11', s: '[2015-05-21]', u: 'grape' }
556
+ ]
557
+ tab2 = Table.new(aoh2)
558
+ expect {
559
+ tab1.union(tab2)
560
+ }.to raise_error(/different types/)
561
+ end
562
+ end
563
+
564
+ describe 'select' do
565
+ it 'should be able to select by column names' do
566
+ aoh = [
567
+ { a: '5', 'Two words' => '20', s: '5143', c: '3123' },
568
+ { a: '4', 'Two words' => '5', s: 412, c: 6412 },
569
+ { a: '7', 'Two words' => '8', s: '$1821', c: '$1888' }
570
+ ]
571
+ tab1 = Table.new(aoh)
572
+ tab2 = tab1.select(:s, :a, :c)
573
+ expect(tab2.headers).to eq [:s, :a, :c]
574
+ end
575
+
576
+ it 'should be able to select by column names renaming columns' do
577
+ aoh = [
578
+ { a: '5', 'Two words' => '20', s: '5143', c: '3123' },
579
+ { a: '4', 'Two words' => '5', s: 412, c: 6412 },
580
+ { a: '7', 'Two words' => '8', s: '$1821', c: '$1888' }
581
+ ]
582
+ tab1 = Table.new(aoh)
583
+ tab2 = tab1.select(s: :former_s, a: :new_a, c: :renew_c)
584
+ expect(tab2.headers).to eq [:former_s, :new_a, :renew_c]
585
+ end
586
+
587
+ it 'should be able to select new columns computed from prior' do
588
+ aoh = [
589
+ { a: '5', 'Two words' => '20', s: '5143', c: '3123' },
590
+ { a: '4', 'Two words' => '5', s: 412, c: 6412 },
591
+ { a: '7', 'Two words' => '8', s: '$1821', c: '$1888' }
592
+ ]
593
+ tab1 = Table.new(aoh)
594
+ tab2 = tab1.select(:two_words, row: '@row', s_squared: 's * s',
595
+ arb: 's_squared / (a + c).to_d')
596
+ expect(tab2.headers).to eq [:two_words, :row, :s_squared, :arb]
597
+ end
598
+ end
599
+
600
+ describe 'where' do
601
+ it 'should be able to filter rows by expression' do
602
+ tab1 = Table.new(StringIO.new(@csv_file_body), '.csv')
603
+ tab2 = tab1.where("date < Date.parse('2006-06-01')")
604
+ expect(tab2[:date].max).to be < Date.parse('2006-06-01')
605
+ end
606
+ end
607
+
608
+ describe 'group_by' do
609
+ it 'should be able to group by equal columns' do
610
+ tab1 = Table.new(StringIO.new(@csv_file_body), '.csv')
611
+ tab2 = tab1.group_by(:date, :code, shares: :sum, ref: :first)
612
+ expect(tab2.headers).to eq([:date, :code, :sum_shares, :first_ref,
613
+ :first_rawshares, :first_price, :first_info])
614
+ end
615
+ end
616
+
617
+ describe 'output' do
618
+ it 'should be able to return itself as an array of arrays' do
619
+ aoh = [
620
+ { a: '1', 'Two words' => '2', c: '3,123', d: 'apple' },
621
+ { a: '4', 'Two words' => '5', c: '6,412', d: 'orange' },
622
+ { a: '7', 'Two words' => '8', c: '$9,888', d: 'pear' }
623
+ ]
624
+ tab = Table.new(aoh)
625
+ aoa = tab.to_org
626
+ expect(aoa.class).to eq Array
627
+ expect(aoa[0].class).to eq Array
628
+ expect(aoa[0][0]).to eq 'A'
629
+ end
630
+
631
+ it 'should be able to output an org babel aoa' do
632
+ # This is what the data looks like when called from org babel code
633
+ # blocks.
634
+ tab =
635
+ [['Ref', 'Date', 'Code', 'Raw', 'Shares', 'Price', 'Info'],
636
+ [1, '2013-05-02', 'P', 795_546.20, 795_546.2, 1.1850, 'ZMPEF1'],
637
+ [2, '2013-05-02', 'P', 118_186.40, 118_186.4, 11.8500, 'ZMPEF1'],
638
+ [7, '2013-05-20', 'S', 12_000.00, 5046.00, 28.2804, 'ZMEAC'],
639
+ [8, '2013-05-20', 'S', 85_000.00, 35_742.50, 28.3224, 'ZMEAC'],
640
+ [9, '2013-05-20', 'S', 33_302.00, 14_003.49, 28.6383, 'ZMEAC'],
641
+ [10, '2013-05-23', 'S', 8000.00, 3364.00, 27.1083, 'ZMEAC'],
642
+ [11, '2013-05-23', 'S', 23_054.00, 9694.21, 26.8015, 'ZMEAC'],
643
+ [12, '2013-05-23', 'S', 39_906.00, 16_780.47, 25.1749, 'ZMEAC'],
644
+ [13, '2013-05-29', 'S', 13_459.00, 5659.51, 24.7464, 'ZMEAC'],
645
+ [14, '2013-05-29', 'S', 15_700.00, 6601.85, 24.7790, 'ZMEAC'],
646
+ [15, '2013-05-29', 'S', 15_900.00, 6685.95, 24.5802, 'ZMEAC'],
647
+ [16, '2013-05-30', 'S', 6_679.00, 2808.52, 25.0471, 'ZMEAC']]
648
+ tg = Table.new(tab).add_sum_footer([:raw, :shares, :price])
649
+ aoa = tg.to_org(formats: { raw: '%,', shares: '%,', price: '%,4' })
650
+ expect(aoa[-1][0]).to eq 'Total'
651
+ expect(aoa[-1][1]).to eq ''
652
+ expect(aoa[-1][2]).to eq ''
653
+ expect(aoa[-1][3]).to eq '1,166,733'
654
+ expect(aoa[-1][4]).to eq '1,020,119'
655
+ expect(aoa[-1][5]).to eq '276.5135'
656
+ end
657
+ end
658
+ end
659
+ end