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.
- checksums.yaml +4 -4
- data/.gitignore +1 -0
- data/.rubocop.yml +3 -0
- data/Rakefile +2 -2
- data/fat_core.gemspec +19 -18
- data/lib/core_extensions/date/fat_core.rb +0 -1
- data/lib/fat_core.rb +5 -1
- data/lib/fat_core/column.rb +197 -0
- data/lib/fat_core/date.rb +192 -132
- data/lib/fat_core/enumerable.rb +1 -1
- data/lib/fat_core/evaluator.rb +43 -0
- data/lib/fat_core/hash.rb +1 -1
- data/lib/fat_core/nil.rb +4 -0
- data/lib/fat_core/numeric.rb +30 -17
- data/lib/fat_core/period.rb +58 -67
- data/lib/fat_core/range.rb +20 -25
- data/lib/fat_core/string.rb +95 -55
- data/lib/fat_core/symbol.rb +12 -14
- data/lib/fat_core/table.rb +515 -0
- data/lib/fat_core/version.rb +2 -2
- data/spec/example_files/goldberg.org +199 -0
- data/spec/example_files/wpcs.csv +92 -0
- data/spec/lib/array_spec.rb +1 -1
- data/spec/lib/date_spec.rb +5 -6
- data/spec/lib/enumerable_spec.rb +1 -1
- data/spec/lib/evaluator_spec.rb +34 -0
- data/spec/lib/hash_spec.rb +5 -5
- data/spec/lib/kernel_spec.rb +3 -3
- data/spec/lib/nil_spec.rb +2 -2
- data/spec/lib/numeric_spec.rb +22 -22
- data/spec/lib/period_spec.rb +11 -12
- data/spec/lib/range_spec.rb +50 -50
- data/spec/lib/string_spec.rb +71 -74
- data/spec/lib/symbol_spec.rb +3 -3
- data/spec/lib/table_spec.rb +659 -0
- data/spec/spec_helper.rb +1 -1
- metadata +28 -2
data/lib/fat_core/enumerable.rb
CHANGED
@@ -0,0 +1,43 @@
|
|
1
|
+
module FatCore
|
2
|
+
# The Evaluator class provides a class for evaluating Ruby expressions based
|
3
|
+
# on variable settings provided at runtime. If the same Evaluator object is
|
4
|
+
# used for several successive calls, it can maintain state between calls with
|
5
|
+
# instance variables. The call to Evaluator.new can be given a hash of
|
6
|
+
# instance variable names and values that will be maintained across all calls
|
7
|
+
# to the #evaluate method. In addition, on each evaluate call, a set of
|
8
|
+
# /local/ variables can be supplied to provide variables that exist only for
|
9
|
+
# the duration of that evaluate call. An optional before and after string can
|
10
|
+
# be given to the constructor that will evaluate the given expression before
|
11
|
+
# and, respectively, after each call to #evaluate. This provides a way to
|
12
|
+
# update values of instance variables for use in subsequent calls to
|
13
|
+
# #evaluate.
|
14
|
+
class Evaluator
|
15
|
+
def initialize(vars: {}, before: nil, after: nil)
|
16
|
+
@before = before
|
17
|
+
@after = after
|
18
|
+
set_instance_vars(vars)
|
19
|
+
end
|
20
|
+
|
21
|
+
def set_instance_vars(vars = {})
|
22
|
+
vars.each_pair do |name, val|
|
23
|
+
name = "@#{name}" unless name.to_s.start_with?('@')
|
24
|
+
instance_variable_set(name, val)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
def set_local_vars(vars = {}, bnd)
|
29
|
+
vars.each_pair do |name, val|
|
30
|
+
bnd.local_variable_set(name, val)
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
def evaluate(expr = '', vars: {})
|
35
|
+
bdg = binding
|
36
|
+
set_local_vars(vars, bdg)
|
37
|
+
eval(@before, bdg) if @before
|
38
|
+
result = eval(expr, bdg)
|
39
|
+
eval(@after, bdg) if @after
|
40
|
+
result
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
data/lib/fat_core/hash.rb
CHANGED
data/lib/fat_core/nil.rb
CHANGED
data/lib/fat_core/numeric.rb
CHANGED
@@ -13,11 +13,12 @@ class Numeric
|
|
13
13
|
# By default, use zero places for whole numbers; four places for
|
14
14
|
# numbers containing a fractional part to 4 places.
|
15
15
|
if places.nil?
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
16
|
+
places =
|
17
|
+
if abs.modulo(1).round(4) > 0.0
|
18
|
+
4
|
19
|
+
else
|
20
|
+
0
|
21
|
+
end
|
21
22
|
end
|
22
23
|
group(places, ',')
|
23
24
|
end
|
@@ -29,11 +30,9 @@ class Numeric
|
|
29
30
|
|
30
31
|
# Only convert to string numbers with exponent unless they are
|
31
32
|
# less than 1 (to ensure that really small numbers round to 0.0)
|
32
|
-
if
|
33
|
-
return self.to_s
|
34
|
-
end
|
33
|
+
return to_s if abs > 1.0 && to_s =~ /e/
|
35
34
|
|
36
|
-
str =
|
35
|
+
str = to_f.round(places).to_s
|
37
36
|
|
38
37
|
# Break the number into parts
|
39
38
|
str =~ /^(-)?(\d*)((\.)?(\d*))?$/
|
@@ -43,7 +42,7 @@ class Numeric
|
|
43
42
|
|
44
43
|
# Pad out the fractional part with zeroes to the right
|
45
44
|
n_zeroes = [places - frac.length, 0].max
|
46
|
-
frac +=
|
45
|
+
frac += '0' * n_zeroes if n_zeroes > 0
|
47
46
|
|
48
47
|
# Place the commas in the whole part only
|
49
48
|
whole = whole.reverse
|
@@ -51,31 +50,45 @@ class Numeric
|
|
51
50
|
whole.gsub!(/#{Regexp.escape(delim)}$/, '')
|
52
51
|
whole.reverse!
|
53
52
|
if frac.nil? || places <= 0
|
54
|
-
|
53
|
+
neg + whole
|
55
54
|
else
|
56
|
-
|
55
|
+
neg + whole + '.' + frac
|
57
56
|
end
|
58
57
|
end
|
59
58
|
|
60
59
|
# Determine if this is a whole number.
|
61
60
|
def whole?
|
62
|
-
|
61
|
+
floor == self
|
63
62
|
end
|
64
63
|
|
65
64
|
# Return an integer type, but only if the fractional part of self
|
66
65
|
# is zero
|
67
66
|
def int_if_whole
|
68
|
-
|
67
|
+
whole? ? floor : self
|
69
68
|
end
|
70
69
|
|
71
70
|
def secs_to_hms
|
72
71
|
frac = self % 1
|
73
|
-
mins, secs =
|
72
|
+
mins, secs = divmod(60)
|
74
73
|
hrs, mins = mins.divmod(60)
|
75
74
|
if frac.round(5) > 0.0
|
76
|
-
|
75
|
+
'%02d:%02d:%02d.%d' % [hrs, mins, secs, frac.round(5) * 100]
|
76
|
+
else
|
77
|
+
'%02d:%02d:%02d' % [hrs, mins, secs]
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
# Format the number according to the given sprintf format. Besides the
|
82
|
+
# sprintf formats, a format string of '%,2', for example, will return the
|
83
|
+
# number grouped by commas and rounded to 2 places. If no number of places
|
84
|
+
# is given, the number will be rounded to an integer.
|
85
|
+
def format_by(fmt = nil)
|
86
|
+
return to_s unless fmt
|
87
|
+
if /%,(?<places>\d*)/ =~ fmt.to_s.clean
|
88
|
+
places ||= 0
|
89
|
+
commas(places.to_i)
|
77
90
|
else
|
78
|
-
|
91
|
+
format fmt, self
|
79
92
|
end
|
80
93
|
end
|
81
94
|
|
data/lib/fat_core/period.rb
CHANGED
@@ -21,7 +21,7 @@ class Period
|
|
21
21
|
when Date
|
22
22
|
first = first
|
23
23
|
else
|
24
|
-
raise ArgumentError,
|
24
|
+
raise ArgumentError, 'use Date or String to initialize Period'
|
25
25
|
end
|
26
26
|
|
27
27
|
case last
|
@@ -38,7 +38,7 @@ class Period
|
|
38
38
|
when Date
|
39
39
|
last = last
|
40
40
|
else
|
41
|
-
raise ArgumentError,
|
41
|
+
raise ArgumentError, 'use Date or String to initialize Period'
|
42
42
|
end
|
43
43
|
|
44
44
|
@first = first
|
@@ -54,21 +54,21 @@ class Period
|
|
54
54
|
|
55
55
|
# Need custom setters to ensure first <= last
|
56
56
|
def first=(new_first)
|
57
|
-
unless new_first.
|
57
|
+
unless new_first.is_a?(Date)
|
58
58
|
raise ArgumentError, "can't set Period#first to non-date"
|
59
59
|
end
|
60
60
|
unless new_first <= last
|
61
|
-
raise ArgumentError,
|
61
|
+
raise ArgumentError, 'cannot make Period#first > Period#last'
|
62
62
|
end
|
63
63
|
@first = new_first
|
64
64
|
end
|
65
65
|
|
66
66
|
def last=(new_last)
|
67
|
-
unless new_last.
|
68
|
-
raise ArgumentError,
|
67
|
+
unless new_last.is_a?(Date)
|
68
|
+
raise ArgumentError, 'cannot set Period#last to non-date'
|
69
69
|
end
|
70
70
|
unless new_last >= first
|
71
|
-
raise ArgumentError,
|
71
|
+
raise ArgumentError, 'cannot make Period#last < Period#first'
|
72
72
|
end
|
73
73
|
@last = new_last
|
74
74
|
end
|
@@ -91,13 +91,13 @@ class Period
|
|
91
91
|
d = first
|
92
92
|
while d <= last
|
93
93
|
yield d
|
94
|
-
d
|
94
|
+
d += 1.day
|
95
95
|
end
|
96
96
|
end
|
97
97
|
|
98
98
|
# Case equality checks for inclusion of date in period.
|
99
|
-
def ===(
|
100
|
-
|
99
|
+
def ===(other)
|
100
|
+
contains?(other)
|
101
101
|
end
|
102
102
|
|
103
103
|
# Return the number of days in the period
|
@@ -146,13 +146,13 @@ class Period
|
|
146
146
|
def self.parse_phrase(phrase)
|
147
147
|
phrase = phrase.clean
|
148
148
|
if phrase =~ /\Afrom (.*) to (.*)\z/
|
149
|
-
from_phrase =
|
150
|
-
to_phrase =
|
149
|
+
from_phrase = $1
|
150
|
+
to_phrase = $2
|
151
151
|
elsif phrase =~ /\Afrom (.*)\z/
|
152
|
-
from_phrase =
|
152
|
+
from_phrase = $1
|
153
153
|
to_phrase = nil
|
154
154
|
elsif phrase =~ /\Ato (.*)\z/
|
155
|
-
from_phrase =
|
155
|
+
from_phrase = $1
|
156
156
|
else
|
157
157
|
from_phrase = phrase
|
158
158
|
end
|
@@ -232,7 +232,7 @@ class Period
|
|
232
232
|
when :year
|
233
233
|
366
|
234
234
|
when :irregular
|
235
|
-
raise ArgumentError,
|
235
|
+
raise ArgumentError, 'no maximum period for :irregular chunk'
|
236
236
|
else
|
237
237
|
chunk_sym_to_days(sym)
|
238
238
|
end
|
@@ -273,16 +273,16 @@ class Period
|
|
273
273
|
|
274
274
|
def to_s
|
275
275
|
if first.beginning_of_year? && last.end_of_year? && first.year == last.year
|
276
|
-
|
276
|
+
first.year.to_s
|
277
277
|
elsif first.beginning_of_quarter? &&
|
278
|
-
|
279
|
-
|
280
|
-
|
278
|
+
last.end_of_quarter? &&
|
279
|
+
first.year == last.year &&
|
280
|
+
first.quarter == last.quarter
|
281
281
|
"#{first.year}-#{first.quarter}Q"
|
282
282
|
elsif first.beginning_of_month? &&
|
283
|
-
|
284
|
-
|
285
|
-
|
283
|
+
last.end_of_month? &&
|
284
|
+
first.year == last.year &&
|
285
|
+
first.month == last.month
|
286
286
|
"#{first.year}-%02d" % first.month
|
287
287
|
else
|
288
288
|
"#{first.iso} to #{last.iso}"
|
@@ -319,58 +319,54 @@ class Period
|
|
319
319
|
to_range.proper_superset_of?(other.to_range)
|
320
320
|
end
|
321
321
|
|
322
|
-
def overlaps?(other)
|
323
|
-
self.to_range.overlaps?(other.to_range)
|
324
|
-
end
|
325
|
-
|
326
322
|
def intersection(other)
|
327
|
-
result =
|
323
|
+
result = to_range.intersection(other.to_range)
|
328
324
|
if result.nil?
|
329
325
|
nil
|
330
326
|
else
|
331
327
|
Period.new(result.first, result.last)
|
332
328
|
end
|
333
329
|
end
|
334
|
-
|
335
|
-
|
330
|
+
alias & intersection
|
331
|
+
alias narrow_to intersection
|
336
332
|
|
337
333
|
def union(other)
|
338
|
-
result =
|
334
|
+
result = to_range.union(other.to_range)
|
339
335
|
Period.new(result.first, result.last)
|
340
336
|
end
|
341
|
-
|
337
|
+
alias + union
|
342
338
|
|
343
339
|
def difference(other)
|
344
|
-
ranges =
|
345
|
-
ranges.each.map{ |r| Period.new(r.first, r.last) }
|
340
|
+
ranges = to_range.difference(other.to_range)
|
341
|
+
ranges.each.map { |r| Period.new(r.first, r.last) }
|
346
342
|
end
|
347
|
-
|
343
|
+
alias - difference
|
348
344
|
|
349
345
|
# returns the chunk sym represented by the period
|
350
346
|
def chunk_sym
|
351
347
|
if first.beginning_of_year? && last.end_of_year? &&
|
352
|
-
|
348
|
+
(365..366) === last - first + 1
|
353
349
|
:year
|
354
350
|
elsif first.beginning_of_half? && last.end_of_half? &&
|
355
|
-
|
351
|
+
(180..183) === last - first + 1
|
356
352
|
:half
|
357
353
|
elsif first.beginning_of_quarter? && last.end_of_quarter? &&
|
358
|
-
|
354
|
+
(90..92) === last - first + 1
|
359
355
|
:quarter
|
360
356
|
elsif first.beginning_of_bimonth? && last.end_of_bimonth? &&
|
361
|
-
|
357
|
+
(58..62) === last - first + 1
|
362
358
|
:bimonth
|
363
359
|
elsif first.beginning_of_month? && last.end_of_month? &&
|
364
|
-
|
360
|
+
(28..31) === last - first + 1
|
365
361
|
:month
|
366
362
|
elsif first.beginning_of_semimonth? && last.end_of_semimonth &&
|
367
|
-
|
363
|
+
(13..16) === last - first + 1
|
368
364
|
:semimonth
|
369
365
|
elsif first.beginning_of_biweek? && last.end_of_biweek? &&
|
370
|
-
|
366
|
+
last - first + 1 == 14
|
371
367
|
:biweek
|
372
368
|
elsif first.beginning_of_week? && last.end_of_week? &&
|
373
|
-
|
369
|
+
last - first + 1 == 7
|
374
370
|
:week
|
375
371
|
elsif first == last
|
376
372
|
:day
|
@@ -410,32 +406,28 @@ class Period
|
|
410
406
|
end
|
411
407
|
|
412
408
|
def contains?(date)
|
413
|
-
if date.respond_to?(:to_date)
|
414
|
-
|
415
|
-
|
416
|
-
unless (date.is_a? Date)
|
417
|
-
raise ArgumentError, "argument must be a Date"
|
418
|
-
end
|
419
|
-
self.to_range.cover?(date)
|
409
|
+
date = date.to_date if date.respond_to?(:to_date)
|
410
|
+
raise ArgumentError, 'argument must be a Date' unless date.is_a?(Date)
|
411
|
+
to_range.cover?(date)
|
420
412
|
end
|
421
413
|
|
422
414
|
def overlaps?(other)
|
423
|
-
|
415
|
+
to_range.overlaps?(other.to_range)
|
424
416
|
end
|
425
417
|
|
426
418
|
# Return whether any of the Periods that are within self overlap one
|
427
419
|
# another
|
428
420
|
def has_overlaps_within?(periods)
|
429
|
-
|
421
|
+
to_range.has_overlaps_within?(periods.map(&:to_range))
|
430
422
|
end
|
431
423
|
|
432
424
|
def spanned_by?(periods)
|
433
|
-
to_range.spanned_by?(periods.map
|
425
|
+
to_range.spanned_by?(periods.map(&:to_range))
|
434
426
|
end
|
435
427
|
|
436
428
|
def gaps(periods)
|
437
|
-
to_range.gaps(periods.map
|
438
|
-
map { |r| Period.new(r.first, r.last)}
|
429
|
+
to_range.gaps(periods.map(&:to_range))
|
430
|
+
.map { |r| Period.new(r.first, r.last) }
|
439
431
|
end
|
440
432
|
|
441
433
|
# Return an array of Periods wholly-contained within self in chunks of
|
@@ -444,7 +436,8 @@ class Period
|
|
444
436
|
# respectively, are set true. The last chunk can be made to extend beyond
|
445
437
|
# the end of self to make it a whole chunk if round_up_last is set true,
|
446
438
|
# in which case, partial_last is ignored.
|
447
|
-
def chunks(size: :month, partial_first: false, partial_last: false,
|
439
|
+
def chunks(size: :month, partial_first: false, partial_last: false,
|
440
|
+
round_up_last: false)
|
448
441
|
size = size.to_sym
|
449
442
|
result = []
|
450
443
|
chunk_start = first.dup
|
@@ -467,27 +460,27 @@ class Period
|
|
467
460
|
chunk_end = chunk_start.end_of_quarter
|
468
461
|
when :bimonth
|
469
462
|
unless partial_first
|
470
|
-
|
463
|
+
chunk_start += 1.day until chunk_start.beginning_of_bimonth?
|
471
464
|
end
|
472
465
|
chunk_end = (chunk_start.end_of_month + 1.day).end_of_month
|
473
466
|
when :month
|
474
467
|
unless partial_first
|
475
|
-
|
468
|
+
chunk_start += 1.day until chunk_start.beginning_of_month?
|
476
469
|
end
|
477
470
|
chunk_end = chunk_start.end_of_month
|
478
471
|
when :semimonth
|
479
472
|
unless partial_first
|
480
|
-
|
473
|
+
chunk_start += 1.day until chunk_start.beginning_of_semimonth?
|
481
474
|
end
|
482
475
|
chunk_end = chunk_start.end_of_semimonth
|
483
476
|
when :biweek
|
484
477
|
unless partial_first
|
485
|
-
|
478
|
+
chunk_start += 1.day until chunk_start.beginning_of_biweek?
|
486
479
|
end
|
487
480
|
chunk_end = chunk_start.end_of_biweek
|
488
481
|
when :week
|
489
482
|
unless partial_first
|
490
|
-
|
483
|
+
chunk_start += 1.day until chunk_start.beginning_of_week?
|
491
484
|
end
|
492
485
|
chunk_end = chunk_start.end_of_week
|
493
486
|
when :day
|
@@ -497,14 +490,12 @@ class Period
|
|
497
490
|
end
|
498
491
|
if chunk_end <= last
|
499
492
|
result << Period.new(chunk_start, chunk_end)
|
493
|
+
elsif round_up_last
|
494
|
+
result << Period.new(chunk_start, chunk_end)
|
495
|
+
elsif partial_last
|
496
|
+
result << Period.new(chunk_start, last)
|
500
497
|
else
|
501
|
-
|
502
|
-
result << Period.new(chunk_start, chunk_end)
|
503
|
-
elsif partial_last
|
504
|
-
result << Period.new(chunk_start, last)
|
505
|
-
else
|
506
|
-
break
|
507
|
-
end
|
498
|
+
break
|
508
499
|
end
|
509
500
|
chunk_start = result.last.last + 1.day
|
510
501
|
end
|