fat_table 0.5.3 → 0.6.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -191,12 +191,15 @@ module FatTable
191
191
 
192
192
  # :category: Aggregates
193
193
 
194
- # Return the first non-nil item in the Column. Works with any Column type.
194
+ # Return the first non-nil item in the Column, or nil if all items are
195
+ # nil. Works with any Column type.
195
196
  def first
197
+ return nil if items.all?(&:nil?)
198
+
196
199
  if type == 'String'
197
200
  items.reject(&:blank?).first
198
201
  else
199
- items.compact.first
202
+ items.filter_to_type(type).first
200
203
  end
201
204
  end
202
205
 
@@ -204,83 +207,94 @@ module FatTable
204
207
 
205
208
  # Return the last non-nil item in the Column. Works with any Column type.
206
209
  def last
210
+ return nil if items.all?(&:nil?)
211
+
207
212
  if type == 'String'
208
213
  items.reject(&:blank?).last
209
214
  else
210
- items.compact.last
215
+ items.filter_to_type(type).last
211
216
  end
212
217
  end
213
218
 
214
219
  # :category: Aggregates
215
220
 
216
- # Return a count of the non-nil items in the Column. Works with any Column
217
- # type.
221
+ # Return a count of the non-nil items in the Column, or the size of the
222
+ # column if all items are nil. Works with any Column type.
218
223
  def count
224
+ return items.size if items.all?(&:nil?)
225
+
219
226
  if type == 'String'
220
227
  items.reject(&:blank?).count.to_d
221
228
  else
222
- items.compact.count.to_d
229
+ items.filter_to_type(type).count.to_d
223
230
  end
224
231
  end
225
232
 
226
233
  # :category: Aggregates
227
234
 
228
- # Return the smallest non-nil, non-blank item in the Column. Works with
229
- # numeric, string, and datetime Columns.
235
+ # Return the smallest non-nil, non-blank item in the Column, or nil if all
236
+ # items are nil. Works with numeric, string, and datetime Columns.
230
237
  def min
231
238
  only_with('min', 'NilClass', 'Numeric', 'String', 'DateTime')
232
239
  if type == 'String'
233
240
  items.reject(&:blank?).min
234
241
  else
235
- items.compact.min
242
+ items.filter_to_type(type).min
236
243
  end
237
244
  end
238
245
 
239
246
  # :category: Aggregates
240
247
 
241
- # Return the largest non-nil, non-blank item in the Column. Works with
242
- # numeric, string, and datetime Columns.
248
+ # Return the largest non-nil, non-blank item in the Column, or nil if all
249
+ # items are nil. Works with numeric, string, and datetime Columns.
243
250
  def max
244
251
  only_with('max', 'NilClass', 'Numeric', 'String', 'DateTime')
245
252
  if type == 'String'
246
253
  items.reject(&:blank?).max
247
254
  else
248
- items.compact.max
255
+ items.filter_to_type(type).max
249
256
  end
250
257
  end
251
258
 
252
259
  # :category: Aggregates
253
260
 
254
- # Return a Range object for the smallest to largest value in the column.
255
- # Works with numeric, string, and datetime Columns.
261
+ # Return a Range object for the smallest to largest value in the column,
262
+ # or nil if all items are nil. Works with numeric, string, and datetime
263
+ # Columns.
256
264
  def range
257
265
  only_with('range', 'NilClass', 'Numeric', 'String', 'DateTime')
266
+ return nil if items.all?(&:nil?)
267
+
258
268
  Range.new(min, max)
259
269
  end
260
270
 
261
271
  # :category: Aggregates
262
272
 
263
- # Return the sum of the non-nil items in the Column. Works with numeric and
264
- # string Columns. For a string Column, it will return the concatenation of
265
- # the non-nil items.
273
+ # Return the sum of the non-nil items in the Column, or 0 if all items are
274
+ # nil. Works with numeric and string Columns. For a string Column, it
275
+ # will return the concatenation of the non-nil items.
266
276
  def sum
277
+ return 0 if type == 'NilClass' || items.all?(&:nil?)
278
+
267
279
  only_with('sum', 'Numeric', 'String')
268
280
  if type == 'String'
269
281
  items.reject(&:blank?).join(' ')
270
282
  else
271
- items.compact.sum
283
+ items.filter_to_type(type).sum
272
284
  end
273
285
  end
274
286
 
275
287
  # :category: Aggregates
276
288
 
277
- # Return the average value of the non-nil items in the Column. Works with
278
- # numeric and datetime Columns. For datetime Columns, it converts each date
279
- # to its Julian day number, computes the average, and then converts the
280
- # average back to a DateTime.
289
+ # Return the average value of the non-nil items in the Column, or 0 if all
290
+ # items are nil. Works with numeric and datetime Columns. For datetime
291
+ # Columns, it converts each date to its Julian day number, computes the
292
+ # average, and then converts the average back to a DateTime.
281
293
  def avg
294
+ return 0 if type == 'NilClass' || items.all?(&:nil?)
295
+
282
296
  only_with('avg', 'DateTime', 'Numeric')
283
- itms = items.compact
297
+ itms = items.filter_to_type(type)
284
298
  size = itms.size.to_d
285
299
  if type == 'DateTime'
286
300
  avg_jd = itms.map(&:jd).sum / size
@@ -293,17 +307,20 @@ module FatTable
293
307
  # :category: Aggregates
294
308
 
295
309
  # Return the sample variance (the unbiased estimator of the population
296
- # variance using a divisor of N-1) as the average squared deviation from the
297
- # mean, of the non-nil items in the Column. Works with numeric and datetime
298
- # Columns. For datetime Columns, it converts each date to its Julian day
299
- # number and computes the variance of those numbers.
310
+ # variance using a divisor of N-1) as the average squared deviation from
311
+ # the mean, of the non-nil items in the Column, or 0 if all items are
312
+ # nil. Works with numeric and datetime Columns. For datetime Columns, it
313
+ # converts each date to its Julian day number and computes the variance of
314
+ # those numbers.
300
315
  def var
316
+ return 0 if type == 'NilClass' || items.all?(&:nil?)
317
+
301
318
  only_with('var', 'DateTime', 'Numeric')
302
319
  all_items =
303
320
  if type == 'DateTime'
304
- items.compact.map(&:jd)
321
+ items.filter_to_type(type).map(&:jd)
305
322
  else
306
- items.compact
323
+ items.filter_to_type(type)
307
324
  end
308
325
  n = count
309
326
  return BigDecimal('0.0') if n <= 1
@@ -319,12 +336,15 @@ module FatTable
319
336
 
320
337
  # Return the population variance (the biased estimator of the population
321
338
  # variance using a divisor of N) as the average squared deviation from the
322
- # mean, of the non-nil items in the Column. Works with numeric and datetime
323
- # Columns. For datetime Columns, it converts each date to its Julian day
324
- # number and computes the variance of those numbers.
339
+ # mean, of the non-nil items in the Column, or 0 if all items are
340
+ # nil. Works with numeric and datetime Columns. For datetime Columns, it
341
+ # converts each date to its Julian day number and computes the variance of
342
+ # those numbers.
325
343
  def pvar
344
+ return 0 if type == 'NilClass' || items.all?(&:nil?)
345
+
326
346
  only_with('var', 'DateTime', 'Numeric')
327
- n = items.compact.size.to_d
347
+ n = items.filter_to_type(type).size.to_d
328
348
  return BigDecimal('0.0') if n <= 1
329
349
  var * ((n - 1) / n)
330
350
  end
@@ -333,11 +353,13 @@ module FatTable
333
353
 
334
354
  # Return the sample standard deviation (the unbiased estimator of the
335
355
  # population standard deviation using a divisor of N-1) as the square root
336
- # of the sample variance, of the non-nil items in the Column. Works with
337
- # numeric and datetime Columns. For datetime Columns, it converts each date
338
- # to its Julian day number and computes the standard deviation of those
339
- # numbers.
356
+ # of the sample variance, of the non-nil items in the Column, or 0 if all
357
+ # items are nil. Works with numeric and datetime Columns. For datetime
358
+ # Columns, it converts each date to its Julian day number and computes the
359
+ # standard deviation of those numbers.
340
360
  def dev
361
+ return 0 if type == 'NilClass' || items.all?(&:nil?)
362
+
341
363
  only_with('dev', 'DateTime', 'Numeric')
342
364
  var.sqrt(20)
343
365
  end
@@ -345,12 +367,14 @@ module FatTable
345
367
  # :category: Aggregates
346
368
 
347
369
  # Return the population standard deviation (the biased estimator of the
348
- # population standard deviation using a divisor of N) as the square root of
349
- # the population variance, of the non-nil items in the Column. Works with
350
- # numeric and datetime Columns. For datetime Columns, it converts each date
351
- # to its Julian day number and computes the standard deviation of those
352
- # numbers.
370
+ # population standard deviation using a divisor of N) as the square root
371
+ # of the population variance, of the non-nil items in the Column, or 0 if
372
+ # all items are nil. Works with numeric and datetime Columns. For datetime
373
+ # Columns, it converts each date to its Julian day number and computes the
374
+ # standard deviation of those numbers.
353
375
  def pdev
376
+ return 0 if type == 'NilClass' || items.all?(&:nil?)
377
+
354
378
  only_with('dev', 'DateTime', 'Numeric')
355
379
  Math.sqrt(pvar)
356
380
  end
@@ -358,28 +382,35 @@ module FatTable
358
382
  # :category: Aggregates
359
383
 
360
384
  # Return true if any of the items in the Column are true; otherwise return
361
- # false. Works only with boolean Columns.
385
+ # false, or false if all items are nil. Works only with boolean Columns.
362
386
  def any?
387
+ return false if type == 'NilClass' || items.all?(&:nil?)
388
+
363
389
  only_with('any?', 'Boolean')
364
- items.compact.any?
390
+ items.filter_to_type(type).any?
365
391
  end
366
392
 
367
393
  # :category: Aggregates
368
394
 
369
395
  # Return true if all of the items in the Column are true; otherwise return
370
- # false. Works only with boolean Columns.
396
+ # false, or false if all items are nil. Works only with boolean Columns.
371
397
  def all?
398
+ return false if type == 'NilClass' || items.all?(&:nil?)
399
+
372
400
  only_with('all?', 'Boolean')
373
- items.compact.all?
401
+ items.filter_to_type(type).all?
374
402
  end
375
403
 
376
404
  # :category: Aggregates
377
405
 
378
- # Return true if none of the items in the Column are true; otherwise return
379
- # false. Works only with boolean Columns.
406
+ # Return true if none of the items in the Column are true; otherwise
407
+ # return false, or true if all items are nil. Works only with boolean
408
+ # Columns.
380
409
  def none?
410
+ return true if type == 'NilClass' || items.all?(&:nil?)
411
+
381
412
  only_with('none?', 'Boolean')
382
- items.compact.none?
413
+ items.filter_to_type(type).none?
383
414
  end
384
415
 
385
416
  # :category: Aggregates
@@ -387,14 +418,17 @@ module FatTable
387
418
  # Return true if precisely one of the items in the Column is true;
388
419
  # otherwise return false. Works only with boolean Columns.
389
420
  def one?
421
+ return false if type == 'NilClass' || items.all?(&:nil?)
422
+
390
423
  only_with('one?', 'Boolean')
391
- items.compact.one?
424
+ items.filter_to_type(type).one?
392
425
  end
393
426
 
394
427
  private
395
428
 
396
429
  def only_with(agg, *valid_types)
397
430
  return self if valid_types.include?(type)
431
+
398
432
  msg = "aggregate '#{agg}' cannot be applied to a #{type} column"
399
433
  raise UserError, msg
400
434
  end
@@ -413,11 +447,10 @@ module FatTable
413
447
  # a tolerant column, respond to type errors by converting the column to a
414
448
  # String type.
415
449
  def <<(itm)
416
- items << convert_to_type(itm)
450
+ items << convert_and_set_type(itm)
417
451
  rescue IncompatibleTypeError => ex
418
452
  if tolerant?
419
- force_string!
420
- retry
453
+ items << Convert.convert_to_string(itm)
421
454
  else
422
455
  raise ex
423
456
  end
@@ -436,9 +469,14 @@ module FatTable
436
469
 
437
470
  private
438
471
 
439
- def convert_to_type(val)
440
- new_val = Convert.convert_to_type(val, type)
441
- if new_val && type == 'NilClass'
472
+ def convert_and_set_type(val)
473
+ begin
474
+ new_val = Convert.convert_to_type(val, type, tolerant: tolerant?)
475
+ rescue IncompatibleTypeError
476
+ err_msg = "attempt to add '#{val}' to column '#{header}' already typed as #{type}"
477
+ raise IncompatibleTypeError, err_msg
478
+ end
479
+ if new_val && (type == 'NilClass' || type == 'String')
442
480
  @type =
443
481
  if [true, false].include?(new_val)
444
482
  'Boolean'
@@ -10,7 +10,7 @@ module FatTable
10
10
  # determined, raise an error if the val cannot be converted to the Column
11
11
  # type. Otherwise, returns the converted val as an object of the correct
12
12
  # class.
13
- def self.convert_to_type(val, type)
13
+ def self.convert_to_type(val, type, tolerant: false)
14
14
  case type
15
15
  when 'NilClass'
16
16
  if val != false && val.blank?
@@ -36,8 +36,7 @@ module FatTable
36
36
  else
37
37
  new_val = convert_to_boolean(val)
38
38
  if new_val.nil?
39
- msg = "attempt to add '#{val}' to a column already typed as #{type}"
40
- raise IncompatibleTypeError, msg
39
+ raise IncompatibleTypeError
41
40
  end
42
41
  new_val
43
42
  end
@@ -47,8 +46,7 @@ module FatTable
47
46
  else
48
47
  new_val = convert_to_date_time(val)
49
48
  if new_val.nil?
50
- msg = "attempt to add '#{val}' to a column already typed as #{type}"
51
- raise IncompatibleTypeError, msg
49
+ raise IncompatibleTypeError
52
50
  end
53
51
  new_val
54
52
  end
@@ -58,19 +56,30 @@ module FatTable
58
56
  else
59
57
  new_val = convert_to_numeric(val)
60
58
  if new_val.nil?
61
- msg = "attempt to add '#{val}' to a column already typed as #{type}"
62
- raise IncompatibleTypeError, msg
59
+ raise IncompatibleTypeError
63
60
  end
64
61
  new_val
65
62
  end
66
63
  when 'String'
67
64
  if val.nil?
68
65
  nil
66
+ elsif tolerant
67
+ # Allow String to upgrade to one of Numeric, DateTime, or Boolean if
68
+ # possible.
69
+ if (new_val = convert_to_numeric(val))
70
+ new_val
71
+ elsif (new_val = convert_to_date_time(val))
72
+ new_val
73
+ elsif (new_val = convert_to_boolean(val))
74
+ new_val
75
+ else
76
+ new_val = convert_to_string(val)
77
+ end
78
+ new_val
69
79
  else
70
80
  new_val = convert_to_string(val)
71
81
  if new_val.nil?
72
- msg = "attempt to add '#{val}' to a column already typed as #{type}"
73
- raise IncompatibleTypeError, msg
82
+ raise IncompatibleTypeError
74
83
  end
75
84
  new_val
76
85
  end
@@ -45,17 +45,15 @@ module FatTable
45
45
  end
46
46
 
47
47
  # Return the result of evaluating +expr+ as a Ruby expression in which the
48
- # instance variables set in Evaluator.new and any local variables set in the
49
- # Hash parameter +locals+ are available to the expression.
48
+ # instance variables set in Evaluator.new and any local variables set in
49
+ # the Hash parameter +locals+ are available to the expression. Certain
50
+ # errors simply return nil as the result. This can happen, for example,
51
+ # when a string gets into an otherwise numeric column because the column
52
+ # is set to tolerant.
50
53
  def evaluate(expr = '', locals: {})
51
54
  eval(expr, local_vars(binding, locals))
52
55
  rescue NoMethodError, TypeError => ex
53
- if ex.to_s =~ /for nil:NilClass|nil can't be coerced/
54
- # Likely one of the locals was nil, so let nil be the result.
55
- return nil
56
- else
57
- raise ex
58
- end
56
+ nil
59
57
  end
60
58
 
61
59
  private
@@ -2,7 +2,7 @@
2
2
 
3
3
  module FatTable
4
4
  class Footer
5
- attr_reader :table, :label, :label_col, :values, :group
5
+ attr_reader :table, :label_col, :values, :group
6
6
 
7
7
  ###########################################################################
8
8
  # Constructors
@@ -15,6 +15,7 @@ module FatTable
15
15
  # for the footer are added later with the #add_value method.
16
16
  def initialize(label = 'Total', table, label_col: nil, group: false)
17
17
  @label = label
18
+
18
19
  unless table.is_a?(Table)
19
20
  raise ArgumentError, 'Footer.new needs a table argument'
20
21
  end
@@ -30,14 +31,7 @@ module FatTable
30
31
  @group = group
31
32
  @group_cols = {}
32
33
  @values = {}
33
- if group
34
- @values[@label_col] = []
35
- table.number_of_groups.times do
36
- @values[@label_col] << @label
37
- end
38
- else
39
- @values[@label_col] = [@label]
40
- end
34
+ insert_labels_in_label_col
41
35
  make_accessor_methods
42
36
  end
43
37
 
@@ -71,6 +65,11 @@ module FatTable
71
65
  end
72
66
  end
73
67
 
68
+ # Return the value of the label, for the kth group if grouped.
69
+ def label(k = 0)
70
+ calc_label(k)
71
+ end
72
+
74
73
  # :category: Accessors
75
74
 
76
75
  # Return the value of under the +key+ header, or if this is a group
@@ -108,8 +107,10 @@ module FatTable
108
107
  if group && k.nil?
109
108
  raise ArgumentError, 'Footer#column(h, k) missing the group number argument k'
110
109
  end
110
+
111
111
  if group
112
- k.nil? ? @group_cols[h] : @group_cols[h][k]
112
+ @group_cols[h] ||= table.group_cols(h)
113
+ @group_cols[h][k]
113
114
  else
114
115
  table.column(h)
115
116
  end
@@ -151,14 +152,13 @@ module FatTable
151
152
 
152
153
  # Evaluate the given agg for the header col and, in the case of a group
153
154
  # footer, the group k.
154
- def calc_val(agg, col, k = nil)
155
- column =
156
- if group
157
- @group_cols[col] ||= table.group_cols(col)
158
- @group_cols[col][k]
159
- else
160
- table.column(col)
161
- end
155
+ def calc_val(agg, h, k = nil)
156
+ column = column(h, k)
157
+
158
+ # Convert Date and Time objects to DateTime
159
+ if [Date, Time].include?(agg.class)
160
+ agg = agg.to_datetime
161
+ end
162
162
 
163
163
  case agg
164
164
  when Symbol
@@ -166,7 +166,7 @@ module FatTable
166
166
  when String
167
167
  begin
168
168
  converted_val = Convert.convert_to_type(agg, column.type)
169
- rescue UserError
169
+ rescue UserError, IncompatibleTypeError
170
170
  converted_val = false
171
171
  end
172
172
  if converted_val
@@ -179,29 +179,69 @@ module FatTable
179
179
  when Proc
180
180
  result =
181
181
  if group
182
- unless agg.arity == 3
183
- msg = 'a lambda used in a group footer must have three arguments: (f, c, k)'
182
+ case agg.arity
183
+ when 0
184
+ agg.call
185
+ when 1
186
+ agg.call(k)
187
+ when 2
188
+ agg.call(k, column)
189
+ when 3
190
+ agg.call(k, column, self)
191
+ else
192
+ msg = 'a lambda used in a group footer may have 0 to 3 three arguments: (k, c, f)'
184
193
  raise ArgumentError, msg
185
194
  end
186
- agg.call(self, col, k)
187
195
  else
188
- unless agg.arity == 2
189
- msg = 'a lambda used in a non-group footer must have two arguments: (f, c)'
196
+ case agg.arity
197
+ when 0
198
+ agg.call
199
+ when 1
200
+ agg.call(column)
201
+ when 2
202
+ agg.call(column, self)
203
+ else
204
+ msg = 'a lambda used in a non-group footer may have 0 to 2 arguments: (c, f)'
190
205
  raise ArgumentError, msg
191
206
  end
192
- agg.call(self, col)
193
207
  end
194
- # Make sure the result returned can be inserted into footer field.
195
- case result
196
- when Symbol, String
197
- calc_val(result, col, k)
198
- when column.type.constantize
199
- result
208
+ # Pass the result back into this method as the new agg
209
+ calc_val(result, h, k)
210
+ else
211
+ agg.to_s
212
+ end
213
+ end
214
+
215
+ # Insert a possibly calculated value for the label in the appropriate
216
+ # @values column.
217
+ def insert_labels_in_label_col
218
+ if group
219
+ @values[@label_col] = []
220
+ table.number_of_groups.times do |k|
221
+ @values[@label_col] << calc_label(k)
222
+ end
223
+ else
224
+ @values[@label_col] = [calc_label(0)]
225
+ end
226
+ end
227
+
228
+ # Calculate the label for the kth group, using k = 0 for non-group
229
+ # footers. If the label is a proc, call it with the group number.
230
+ def calc_label(k)
231
+ case @label
232
+ when Proc
233
+ case @label.arity
234
+ when 0
235
+ @label.call
236
+ when 1
237
+ @label.call(k)
238
+ when 2
239
+ @label.call(k, self)
200
240
  else
201
- raise ArgumentError, "lambda cannot return an object of class #{result.class}"
241
+ raise ArgumentError, "footer label proc may only have 1 argument for group number k"
202
242
  end
203
243
  else
204
- agg
244
+ @label.to_s
205
245
  end
206
246
  end
207
247