fat_core 1.0.3 → 1.2.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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