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.
- 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
|