fat_table 0.5.3 → 0.6.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.
@@ -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