bblib 0.3.0 → 0.4.1
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 +5 -5
- data/.gitignore +11 -10
- data/.rspec +2 -2
- data/.travis.yml +4 -4
- data/CODE_OF_CONDUCT.md +13 -13
- data/Gemfile +4 -4
- data/LICENSE.txt +21 -21
- data/README.md +247 -757
- data/Rakefile +6 -6
- data/bblib.gemspec +34 -34
- data/bin/console +14 -14
- data/bin/setup +7 -7
- data/lib/array/bbarray.rb +71 -29
- data/lib/bblib.rb +12 -12
- data/lib/bblib/version.rb +3 -3
- data/lib/class/effortless.rb +23 -0
- data/lib/error/abstract.rb +3 -0
- data/lib/file/bbfile.rb +93 -52
- data/lib/hash/bbhash.rb +130 -46
- data/lib/hash/hash_struct.rb +24 -0
- data/lib/hash/tree_hash.rb +364 -0
- data/lib/hash_path/hash_path.rb +210 -0
- data/lib/hash_path/part.rb +83 -0
- data/lib/hash_path/path_hash.rb +84 -0
- data/lib/hash_path/proc.rb +93 -0
- data/lib/hash_path/processors.rb +239 -0
- data/lib/html/bbhtml.rb +2 -0
- data/lib/html/builder.rb +34 -0
- data/lib/html/tag.rb +49 -0
- data/lib/logging/bblogging.rb +42 -0
- data/lib/mixins/attrs.rb +422 -0
- data/lib/mixins/bbmixins.rb +7 -0
- data/lib/mixins/bridge.rb +17 -0
- data/lib/mixins/family_tree.rb +41 -0
- data/lib/mixins/hooks.rb +139 -0
- data/lib/mixins/logger.rb +31 -0
- data/lib/mixins/serializer.rb +71 -0
- data/lib/mixins/simple_init.rb +160 -0
- data/lib/number/bbnumber.rb +15 -7
- data/lib/object/bbobject.rb +46 -19
- data/lib/opal/bbopal.rb +0 -4
- data/lib/os/bbos.rb +24 -16
- data/lib/os/bbsys.rb +60 -43
- data/lib/string/bbstring.rb +165 -66
- data/lib/string/cases.rb +37 -29
- data/lib/string/fuzzy_matcher.rb +48 -50
- data/lib/string/matching.rb +43 -30
- data/lib/string/pluralization.rb +156 -0
- data/lib/string/regexp.rb +45 -0
- data/lib/string/roman.rb +17 -30
- data/lib/system/bbsystem.rb +42 -0
- data/lib/time/bbtime.rb +79 -58
- data/lib/time/cron.rb +174 -132
- data/lib/time/task_timer.rb +86 -70
- metadata +27 -10
- data/lib/gem/bbgem.rb +0 -28
- data/lib/hash/hash_path.rb +0 -344
- data/lib/hash/hash_path_proc.rb +0 -256
- data/lib/hash/path_hash.rb +0 -81
- data/lib/object/attr.rb +0 -182
- data/lib/object/hooks.rb +0 -69
- data/lib/object/lazy_class.rb +0 -73
@@ -0,0 +1,45 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Opal seems to be missing the extended flag, this MonkeyPatch prevents errors.
|
4
|
+
if BBLib.in_opal?
|
5
|
+
class Regexp
|
6
|
+
EXTENDED = 2
|
7
|
+
end
|
8
|
+
end
|
9
|
+
|
10
|
+
module BBLib
|
11
|
+
REGEXP_MODE_HASH = {
|
12
|
+
i: Regexp::IGNORECASE,
|
13
|
+
m: Regexp::MULTILINE,
|
14
|
+
x: Regexp::EXTENDED
|
15
|
+
}.freeze
|
16
|
+
|
17
|
+
REGEXP_OPTIONS = {
|
18
|
+
i: [:ignore_case, :ignorecase, :i, :case_insensitive, Regexp::IGNORECASE],
|
19
|
+
m: [:multiline, :multi_line, :m, Regexp::MULTILINE],
|
20
|
+
x: [:extended, :x, Regexp::EXTENDED]
|
21
|
+
}.freeze
|
22
|
+
end
|
23
|
+
|
24
|
+
class Regexp
|
25
|
+
def self.from_s(str, *options, ignore_invalid: false)
|
26
|
+
opt_map = options.map { |o| BBLib::REGEXP_OPTIONS.find { |k, v| o == k || o == k.to_s || v.include?(o) || v.include?(o.to_s.to_sym) }.first }.compact
|
27
|
+
return Regexp.new(str, opt_map.inject(0) { |s, x| s += BBLib::REGEXP_MODE_HASH[x] }) if str.encap_by?('(') || !str.start_with?('/')
|
28
|
+
str += opt_map.join
|
29
|
+
mode = 0
|
30
|
+
unless str.end_with?('/')
|
31
|
+
str.split('/').last.chars.uniq.each do |l|
|
32
|
+
raise ArgumentError, "Invalid Regexp mode: '#{l}'" unless ignore_invalid || BBLib::REGEXP_MODE_HASH[l.to_sym]
|
33
|
+
mode += (BBLib::REGEXP_MODE_HASH[l.to_sym] || 0)
|
34
|
+
end
|
35
|
+
str = str[0..(str.rindex('/') || -1)]
|
36
|
+
end
|
37
|
+
Regexp.new(str.uncapsulate('/', limit: 1), mode)
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
class String
|
42
|
+
def to_regex(*options, ignore_invalid: false)
|
43
|
+
Regexp.from_s(self, *options, ignore_invalid: ignore_invalid)
|
44
|
+
end
|
45
|
+
end
|
data/lib/string/roman.rb
CHANGED
@@ -1,53 +1,48 @@
|
|
1
1
|
|
2
|
+
# frozen_string_literal: true
|
2
3
|
module BBLib
|
3
|
-
|
4
|
+
ROMAN_NUMERALS = { 1000 => 'M', 900 => 'CM', 500 => 'D', 400 => 'CD', 100 => 'C', 90 => 'XC', 50 => 'L',
|
5
|
+
40 => 'XL', 10 => 'X', 9 => 'IX', 5 => 'V', 4 => 'IV', 3 => 'III', 2 => 'II', 1 => 'I' }.freeze
|
4
6
|
# Converts any integer up to 1000 to a roman numeral
|
5
|
-
def self.to_roman
|
7
|
+
def self.to_roman(num)
|
6
8
|
return num.to_s if num > 1000
|
7
|
-
|
8
|
-
|
9
|
-
numeral = ""
|
10
|
-
roman.each do |n, r|
|
9
|
+
numeral = ''
|
10
|
+
ROMAN_NUMERALS.each do |n, r|
|
11
11
|
while num >= n
|
12
|
-
num-= n
|
13
|
-
numeral+= r
|
12
|
+
num -= n
|
13
|
+
numeral += r
|
14
14
|
end
|
15
15
|
end
|
16
16
|
numeral
|
17
17
|
end
|
18
18
|
|
19
|
-
def self.string_to_roman
|
19
|
+
def self.string_to_roman(str)
|
20
20
|
sp = str.split ' '
|
21
21
|
sp.map do |s|
|
22
22
|
if s.drop_symbols.to_i.to_s == s.drop_symbols && !(s =~ /\d+\.\d+/)
|
23
|
-
s
|
23
|
+
s.sub(s.scan(/\d+/).first.to_s, BBLib.to_roman(s.to_i))
|
24
24
|
else
|
25
25
|
s
|
26
26
|
end
|
27
|
-
end.join
|
27
|
+
end.join(' ')
|
28
28
|
end
|
29
29
|
|
30
|
-
|
31
|
-
def self.from_roman str
|
30
|
+
def self.from_roman(str)
|
32
31
|
sp = str.split(' ')
|
33
32
|
(0..1000).each do |n|
|
34
33
|
num = BBLib.to_roman n
|
35
|
-
if
|
36
|
-
|
37
|
-
|
38
|
-
sp[i] = sp[i].sub(num ,n.to_s)
|
39
|
-
end
|
40
|
-
end
|
34
|
+
next if sp.select { |i| i[/#{num}/i] }.empty?
|
35
|
+
(0..(sp.length-1)).each do |i|
|
36
|
+
sp[i] = sp[i].sub(num, n.to_s) if sp[i].drop_symbols.upcase == num
|
41
37
|
end
|
42
38
|
end
|
43
39
|
sp.join ' '
|
44
40
|
end
|
45
|
-
|
46
41
|
end
|
47
42
|
|
48
|
-
class
|
43
|
+
class Integer
|
49
44
|
def to_roman
|
50
|
-
BBLib.to_roman
|
45
|
+
BBLib.to_roman to_i
|
51
46
|
end
|
52
47
|
end
|
53
48
|
|
@@ -56,15 +51,7 @@ class String
|
|
56
51
|
BBLib.from_roman self
|
57
52
|
end
|
58
53
|
|
59
|
-
def from_roman!
|
60
|
-
replace self.from_roman
|
61
|
-
end
|
62
|
-
|
63
54
|
def to_roman
|
64
55
|
BBLib.string_to_roman self
|
65
56
|
end
|
66
|
-
|
67
|
-
def to_roman!
|
68
|
-
replace self.to_roman
|
69
|
-
end
|
70
57
|
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
module BBLib
|
2
|
+
# A string representation of the command line that evoked this ruby instance (platform agnostic)
|
3
|
+
def self.cmd_line(*args, include_args: true, include_ruby: true, prefix: nil, suffix: nil)
|
4
|
+
args = ARGV if args.empty?
|
5
|
+
include_ruby = false if special_program?
|
6
|
+
"#{prefix}#{include_ruby ? Command.quote(Gem.ruby) : nil} #{Command.quote($PROGRAM_NAME)}" \
|
7
|
+
" #{include_args ? args.map { |a| Command.quote(a) }.join(' ') : nil}#{suffix}"
|
8
|
+
.strip
|
9
|
+
end
|
10
|
+
|
11
|
+
# EXPERIMENTAL: Reloads the original file that was called
|
12
|
+
# Use at your own risk, this could cause some weird issues
|
13
|
+
def self.reload(include_args: true)
|
14
|
+
return false if special_program?
|
15
|
+
load cmd_line(*args, include_ruby: false, include_args: include_args)
|
16
|
+
end
|
17
|
+
|
18
|
+
# EXPERIMENTAL: Restart the ruby process that is currently running.
|
19
|
+
# Use at your own risk
|
20
|
+
def self.restart(*args, include_args: true, stay_alive: 1)
|
21
|
+
exit(0)
|
22
|
+
rescue SystemExit
|
23
|
+
opts = BBLib::OS.windows? ? { new_pgroup: true } : { pgroup: true }
|
24
|
+
pid = spawn(cmd_line(*args, include_args: include_args, prefix: (BBLib::OS.windows? ? 'start ' : nil)), **opts)
|
25
|
+
Process.detach(pid)
|
26
|
+
sleep(stay_alive)
|
27
|
+
exit(0) if special_program?
|
28
|
+
end
|
29
|
+
|
30
|
+
SPECIAL_PROGRAMS = ['pry', 'irb.cmd', 'irb'].freeze
|
31
|
+
|
32
|
+
def self.special_program?
|
33
|
+
SPECIAL_PROGRAMS.include?($PROGRAM_NAME)
|
34
|
+
end
|
35
|
+
|
36
|
+
module Command
|
37
|
+
def self.quote(arg)
|
38
|
+
arg =~ /\s+/ ? "\"#{arg}\"" : arg.to_s
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
end
|
data/lib/time/bbtime.rb
CHANGED
@@ -1,10 +1,10 @@
|
|
1
|
+
# frozen_string_literal: true
|
1
2
|
require_relative 'task_timer'
|
2
3
|
require_relative 'cron'
|
3
4
|
|
4
5
|
module BBLib
|
5
|
-
|
6
6
|
# Parses known time based patterns out of a string to construct a numeric duration.
|
7
|
-
def self.parse_duration
|
7
|
+
def self.parse_duration(str, output: :sec, min_interval: :sec)
|
8
8
|
msecs = 0.0
|
9
9
|
|
10
10
|
# Parse time expressions such as 04:05.
|
@@ -20,7 +20,7 @@ module BBLib
|
|
20
20
|
end
|
21
21
|
|
22
22
|
# Parse expressions such as '1m' or '1 min'
|
23
|
-
TIME_EXPS.each do |
|
23
|
+
TIME_EXPS.each do |_k, v|
|
24
24
|
v[:exp].each do |e|
|
25
25
|
numbers = str.downcase.scan(/(?=\w|\D|\A)\d*\.?\d+[[:space:]]*#{e}(?=\W|\d|\z)/i)
|
26
26
|
numbers.each do |n|
|
@@ -28,109 +28,130 @@ module BBLib
|
|
28
28
|
end
|
29
29
|
end
|
30
30
|
end
|
31
|
-
|
32
|
-
msecs / (TIME_EXPS[output][:mult] rescue 1)
|
31
|
+
msecs / TIME_EXPS[output][:mult]
|
33
32
|
end
|
34
33
|
|
35
34
|
# Turns a numeric input into a time string.
|
36
|
-
def self.to_duration
|
37
|
-
return nil unless Numeric
|
38
|
-
|
35
|
+
def self.to_duration(num, input: :sec, stop: :milli, style: :medium)
|
36
|
+
return nil unless num.is_a?(Numeric)
|
37
|
+
return '0' if num.zero?
|
38
|
+
style = :medium unless [:long, :medium, :short].include?(style)
|
39
39
|
expression = []
|
40
|
-
n
|
40
|
+
n = num * TIME_EXPS[input.to_sym][:mult]
|
41
|
+
done = false
|
41
42
|
TIME_EXPS.reverse.each do |k, v|
|
42
|
-
next
|
43
|
-
if k == stop
|
43
|
+
next if done
|
44
|
+
done = true if k == stop
|
44
45
|
div = n / v[:mult]
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
end
|
46
|
+
next unless div >= 1
|
47
|
+
val = (done ? div.round : div.floor)
|
48
|
+
expression << "#{val}#{v[:styles][style]}#{val > 1 && style != :short ? 's' : nil}"
|
49
|
+
n -= val.to_f * v[:mult]
|
50
50
|
end
|
51
|
-
expression.join
|
51
|
+
expression.join(' ')
|
52
|
+
end
|
53
|
+
|
54
|
+
def self.to_nearest_duration(num, input: :sec, style: :medium)
|
55
|
+
n = num * TIME_EXPS[input.to_sym][:mult]
|
56
|
+
stop = nil
|
57
|
+
TIME_EXPS.each do |k, v|
|
58
|
+
stop = k if v[:mult] <= n
|
59
|
+
end
|
60
|
+
stop = :year unless stop
|
61
|
+
to_duration(num, input: input, style: style, stop: stop)
|
52
62
|
end
|
53
63
|
|
54
64
|
TIME_EXPS = {
|
55
65
|
yocto: {
|
56
66
|
mult: 0.000000000000000000001,
|
57
|
-
styles: {
|
58
|
-
exp:
|
67
|
+
styles: { long: ' yoctosecond', medium: ' yocto', short: 'ys' },
|
68
|
+
exp: %w(yoctosecond yocto yoctoseconds yoctos ys)
|
59
69
|
},
|
60
70
|
zepto: {
|
61
71
|
mult: 0.000000000000000001,
|
62
|
-
styles: {
|
63
|
-
exp:
|
72
|
+
styles: { long: ' zeptosecond', medium: ' zepto', short: 'zs' },
|
73
|
+
exp: %w(zeptosecond zepto zeptoseconds zeptos zs)
|
64
74
|
},
|
65
75
|
atto: {
|
66
76
|
mult: 0.000000000000001,
|
67
|
-
styles: {
|
68
|
-
exp:
|
77
|
+
styles: { long: ' attosecond', medium: ' atto', short: 'as' },
|
78
|
+
exp: %w(attoseconds atto attoseconds attos as)
|
69
79
|
},
|
70
80
|
femto: {
|
71
81
|
mult: 0.000000000001,
|
72
|
-
styles: {
|
73
|
-
exp:
|
82
|
+
styles: { long: ' femtosecond', medium: ' fempto', short: 'fs' },
|
83
|
+
exp: %w(femtosecond fempto femtoseconds femptos fs)
|
74
84
|
},
|
75
85
|
pico: {
|
76
86
|
mult: 0.000000001,
|
77
|
-
styles: {
|
78
|
-
exp:
|
87
|
+
styles: { long: ' picosecond', medium: ' pico', short: 'ps' },
|
88
|
+
exp: %w(picosecond pico picoseconds picos ps)
|
79
89
|
},
|
80
90
|
nano: {
|
81
91
|
mult: 0.000001,
|
82
|
-
styles: {
|
83
|
-
exp:
|
92
|
+
styles: { long: ' nanosecond', medium: ' nano', short: 'ns' },
|
93
|
+
exp: %w(nanosecond nano nanoseconds nanos ns)
|
84
94
|
},
|
85
95
|
micro: {
|
86
96
|
mult: 0.001,
|
87
|
-
styles: {
|
88
|
-
exp:
|
97
|
+
styles: { long: ' microsecond', medium: ' micro', short: 'μs' },
|
98
|
+
exp: %W(microsecond micro microseconds micros \u03BCs)
|
89
99
|
},
|
90
100
|
milli: {
|
91
101
|
mult: 1,
|
92
|
-
styles: {
|
93
|
-
exp:
|
102
|
+
styles: { long: ' millisecond', medium: ' mil', short: 'ms' },
|
103
|
+
exp: %w(ms mil mils milli millis millisecond milliseconds milsec milsecs msec msecs msecond mseconds)
|
104
|
+
},
|
94
105
|
sec: {
|
95
106
|
mult: 1000,
|
96
|
-
styles: {
|
97
|
-
exp:
|
107
|
+
styles: { long: ' second', medium: ' sec', short: 's' },
|
108
|
+
exp: %w(s sec secs second seconds)
|
109
|
+
},
|
98
110
|
min: {
|
99
|
-
mult:
|
100
|
-
styles: {
|
101
|
-
exp:
|
111
|
+
mult: 60_000,
|
112
|
+
styles: { long: ' minute', medium: ' min', short: 'm' },
|
113
|
+
exp: %w(m mn mns min mins minute minutes)
|
114
|
+
},
|
102
115
|
hour: {
|
103
|
-
mult:
|
104
|
-
styles: {
|
105
|
-
exp:
|
116
|
+
mult: 3_600_000,
|
117
|
+
styles: { long: ' hour', medium: ' hr', short: 'h' },
|
118
|
+
exp: %w(h hr hrs hour hours)
|
119
|
+
},
|
106
120
|
day: {
|
107
|
-
mult:
|
108
|
-
styles: {
|
109
|
-
exp:
|
121
|
+
mult: 86_400_000,
|
122
|
+
styles: { long: ' day', medium: ' day', short: 'd' },
|
123
|
+
exp: %w(d day days)
|
124
|
+
},
|
110
125
|
week: {
|
111
|
-
mult:
|
112
|
-
styles: {
|
113
|
-
exp:
|
126
|
+
mult: 604_800_000,
|
127
|
+
styles: { long: ' week', medium: ' wk', short: 'w' },
|
128
|
+
exp: %w(w wk wks week weeks)
|
129
|
+
},
|
114
130
|
month: {
|
115
|
-
mult:
|
116
|
-
styles: {
|
117
|
-
exp:
|
131
|
+
mult: 2_592_000_000,
|
132
|
+
styles: { long: ' month', medium: ' mo', short: 'mo' },
|
133
|
+
exp: %w(mo mon mons month months mnth mnths mth mths)
|
134
|
+
},
|
118
135
|
year: {
|
119
|
-
mult:
|
120
|
-
styles: {
|
121
|
-
exp:
|
122
|
-
|
123
|
-
|
136
|
+
mult: 31_536_000_000,
|
137
|
+
styles: { long: ' year', medium: ' yr', short: 'y' },
|
138
|
+
exp: %w(y yr yrs year years)
|
139
|
+
}
|
140
|
+
}.freeze
|
124
141
|
end
|
125
142
|
|
126
143
|
class String
|
127
|
-
def parse_duration
|
128
|
-
BBLib.parse_duration self, output:output, min_interval:min_interval
|
144
|
+
def parse_duration(output: :sec, min_interval: :sec)
|
145
|
+
BBLib.parse_duration self, output: output, min_interval: min_interval
|
129
146
|
end
|
130
147
|
end
|
131
148
|
|
132
149
|
class Numeric
|
133
|
-
def to_duration
|
150
|
+
def to_duration(input: :sec, stop: :milli, style: :medium)
|
134
151
|
BBLib.to_duration self, input: input, stop: stop, style: style
|
135
152
|
end
|
153
|
+
|
154
|
+
def to_nearest_duration(*args)
|
155
|
+
BBLib.to_nearest_duration(self, *args)
|
156
|
+
end
|
136
157
|
end
|
data/lib/time/cron.rb
CHANGED
@@ -1,171 +1,213 @@
|
|
1
|
+
# frozen_string_literal: true
|
1
2
|
module BBLib
|
2
|
-
|
3
3
|
class Cron
|
4
|
-
|
4
|
+
include Effortless
|
5
|
+
attr_str :expression, default: '* * * * * *'
|
6
|
+
attr_reader :parts, serialize: false
|
5
7
|
|
6
|
-
def
|
7
|
-
|
8
|
-
|
8
|
+
def next(exp = expression, count: 1, time: Time.now)
|
9
|
+
self.expression = exp unless exp == expression
|
10
|
+
closest(count: count, time: time, direction: 1)
|
9
11
|
end
|
10
12
|
|
11
|
-
def
|
12
|
-
|
13
|
-
|
14
|
-
return results unless @exp
|
15
|
-
(1..count).each{ |i| results.push next_time(i == 1 ? time : results.last, direction) }
|
16
|
-
count <= 1 ? results.first : results.reject{ |r| r.nil? }
|
13
|
+
def prev(exp = expression, count: 1, time: Time.now)
|
14
|
+
self.expression = exp unless exp == expression
|
15
|
+
closest(count: count, time: time, direction: -1)
|
17
16
|
end
|
18
17
|
|
19
|
-
def
|
20
|
-
|
18
|
+
def expression=(e)
|
19
|
+
e = e.to_s.downcase
|
20
|
+
SPECIAL_EXP.each { |x, v| e = x if v.include?(e) }
|
21
|
+
@expression = e
|
22
|
+
parse
|
23
|
+
e
|
21
24
|
end
|
22
25
|
|
23
|
-
def
|
24
|
-
|
26
|
+
def self.next(exp, count: 1, time: Time.now)
|
27
|
+
BBLib::Cron.new(exp).next(count: count, time: time)
|
25
28
|
end
|
26
29
|
|
27
|
-
def exp
|
28
|
-
|
29
|
-
@exp = e
|
30
|
-
parse
|
30
|
+
def self.prev(exp, count: 1, time: Time.now)
|
31
|
+
BBLib::Cron.new(exp).prev(count: count, time: time)
|
31
32
|
end
|
32
33
|
|
33
|
-
def self.
|
34
|
-
|
34
|
+
def self.valid?(exp)
|
35
|
+
!(numeralize(exp) =~ /\A(.*?\s){4,5}.*?\S\z/).nil?
|
35
36
|
end
|
36
37
|
|
37
|
-
def
|
38
|
-
BBLib::Cron.
|
38
|
+
def valid?(exp)
|
39
|
+
BBLib::Cron.valid?(exp)
|
39
40
|
end
|
40
41
|
|
41
|
-
def self.
|
42
|
-
|
42
|
+
def self.numeralize(exp)
|
43
|
+
REPLACE.each do |k, v|
|
44
|
+
v.each do |r|
|
45
|
+
exp = exp.to_s.gsub(r.to_s, k.to_s)
|
46
|
+
end
|
47
|
+
end
|
48
|
+
exp
|
43
49
|
end
|
44
50
|
|
45
|
-
def
|
46
|
-
|
51
|
+
def time_match?(time)
|
52
|
+
(@parts[:minute].empty? || @parts[:minute].include?(time.min)) &&
|
53
|
+
(@parts[:hour].empty? || @parts[:hour].include?(time.hour)) &&
|
54
|
+
(@parts[:day].empty? || @parts[:day].include?(time.day)) &&
|
55
|
+
(@parts[:weekday].empty? || @parts[:weekday].include?(time.wday)) &&
|
56
|
+
(@parts[:month].empty? || @parts[:month].include?(time.month)) &&
|
57
|
+
(@parts[:year].empty? || @parts[:year].include?(time.year))
|
47
58
|
end
|
48
59
|
|
49
60
|
private
|
50
61
|
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
62
|
+
def simple_init(*args)
|
63
|
+
@parts = {}
|
64
|
+
self.expression = args.first if args.first.is_a?(String)
|
65
|
+
end
|
66
|
+
|
67
|
+
def parse
|
68
|
+
@parts = {}
|
69
|
+
PARTS.keys.zip(@expression.split(' ')).to_h.each do |part, piece|
|
70
|
+
info = PARTS[part]
|
71
|
+
@parts[part] = parse_cron_numbers(piece, info[:min], info[:max], Time.now.send(info[:send]))
|
58
72
|
end
|
73
|
+
end
|
59
74
|
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
75
|
+
def parse_cron_numbers(exp, min, max, qmark)
|
76
|
+
numbers = []
|
77
|
+
return numbers if exp == '*'
|
78
|
+
exp = Cron.numeralize(exp).gsub('?', qmark.to_s).gsub('*', "#{min}-#{max}")
|
79
|
+
exp.scan(/\*\/\d+|\d+\/\d+|\d+-\d+\/\d+/).each do |s|
|
80
|
+
range = s.split('/').first.split('-').map(&:to_i) + [max]
|
81
|
+
divisor = s.split('/').last.to_i
|
82
|
+
Range.new(*range[0..1]).each_with_index do |i, index|
|
83
|
+
numbers.push(i) if index.zero? || (index % divisor).zero?
|
66
84
|
end
|
67
|
-
exp
|
85
|
+
exp = exp.sub(s, '')
|
68
86
|
end
|
87
|
+
exp.scan(/\d+\-\d+/).each do |e|
|
88
|
+
nums = e.scan(/\d+/).map(&:to_i)
|
89
|
+
numbers.push(Range.new(*nums).to_a)
|
90
|
+
end
|
91
|
+
numbers.push(exp.scan(/\d+/).map(&:to_i))
|
92
|
+
numbers.flatten.uniq.sort.reject { |r| r < min || r > max }
|
93
|
+
end
|
69
94
|
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
exp.scan(/\*\/\d+|\d+\/\d+|\d+-\d+\/\d+/).each do |s|
|
75
|
-
range, divisor = s.split('/').first, s.split('/').last.to_i
|
76
|
-
if range == '*'
|
77
|
-
range = (min..max)
|
78
|
-
elsif range =~ /\d+\-\d+/
|
79
|
-
range = (range.split('-').first.to_i..range.split('-').last.to_i)
|
80
|
-
else
|
81
|
-
range = (range.to_i..max)
|
82
|
-
end
|
83
|
-
index = 0
|
84
|
-
range.each do |i|
|
85
|
-
if index == 0 || index % divisor.to_i == 0
|
86
|
-
numbers.push i
|
87
|
-
end
|
88
|
-
index+=1
|
89
|
-
end
|
90
|
-
exp = exp.sub(s, '')
|
91
|
-
end
|
92
|
-
numbers.push exp.scan(/\d+/).map{ |m| m.to_i }
|
93
|
-
exp.strip.scan(/\d+\-\d+/).each do |e|
|
94
|
-
nums = e.scan(/\d+/).map{ |n| n.to_i }
|
95
|
-
numbers.push (nums.min..nums.max).map{ |n| n }
|
96
|
-
end
|
97
|
-
numbers.flatten.sort.uniq.reject{ |r| r < min || r > max }
|
95
|
+
def closest(direction: 1, count: 1, time: Time.now)
|
96
|
+
return unless @expression
|
97
|
+
results = (1..count).flat_map do |_i|
|
98
|
+
time = next_time(time + 60 * direction, direction)
|
98
99
|
end
|
100
|
+
count <= 1 ? results.first : results.compact
|
101
|
+
end
|
99
102
|
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
else
|
108
|
-
time+= 24*60*60*direction
|
109
|
-
# time = Time.new(time.year, time.month, time.day, 0, 0)
|
110
|
-
end
|
111
|
-
safety+=1
|
103
|
+
def next_time(time, direction)
|
104
|
+
original = time.dup
|
105
|
+
safety = 0
|
106
|
+
methods = [:next_year, :next_month, :next_weekday, :next_day, :next_hour, :next_min]
|
107
|
+
until safety >= 1_000_000 || time_match?(time)
|
108
|
+
methods.each do |sym|
|
109
|
+
time = send(sym, time, direction)
|
112
110
|
end
|
113
|
-
|
114
|
-
time
|
111
|
+
safety += 1
|
115
112
|
end
|
113
|
+
time - (time.sec.zero? ? 0 : original.sec)
|
114
|
+
end
|
116
115
|
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
116
|
+
def next_min(time, direction)
|
117
|
+
return time if @parts[:minute].empty?
|
118
|
+
time += 60 * direction until @parts[:minute].include?(time.min)
|
119
|
+
time
|
120
|
+
end
|
121
|
+
|
122
|
+
def next_hour(time, direction)
|
123
|
+
return time if @parts[:hour].empty?
|
124
|
+
until @parts[:hour].include?(time.hour)
|
125
|
+
time -= time.min * 60 if direction.positive?
|
126
|
+
time += (59 - time.min) * 60 if direction.negative?
|
127
|
+
time += 60*60 * direction
|
128
|
+
end
|
129
|
+
time
|
130
|
+
end
|
131
|
+
|
132
|
+
def next_day(time, direction)
|
133
|
+
return time if @parts[:day].empty?
|
134
|
+
time += 24*60*60 * direction until @parts[:day].include?(time.day)
|
135
|
+
time
|
136
|
+
end
|
137
|
+
|
138
|
+
def next_weekday(time, direction)
|
139
|
+
return time if @parts[:weekday].empty?
|
140
|
+
time += 24*60*60 * direction until @parts[:weekday].include?(time.wday)
|
141
|
+
time
|
142
|
+
end
|
143
|
+
|
144
|
+
def next_month(time, direction)
|
145
|
+
return time if @parts[:month].empty?
|
146
|
+
until @parts[:month].include?(time.month)
|
147
|
+
original = time.month
|
148
|
+
min = direction.positive? ? 0 : 59
|
149
|
+
hour = direction.positive? ? 0 : 23
|
150
|
+
day = direction.positive? ? 1 : 31
|
151
|
+
month = BBLib.loop_between(time.month + direction, 1, 12)
|
152
|
+
year = if direction.positive? && month == 1
|
153
|
+
time.year + 1
|
154
|
+
elsif direction.negative? && month == 12
|
155
|
+
time.year - 1
|
156
|
+
else
|
157
|
+
time.year
|
158
|
+
end
|
159
|
+
time = Time.new(year, month, day, hour, min)
|
160
|
+
if direction.negative? && time.month == original
|
161
|
+
time -= 24 * 60 * 60 while time.month == original
|
131
162
|
end
|
132
|
-
current - current.sec
|
133
163
|
end
|
164
|
+
time
|
165
|
+
end
|
134
166
|
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
day
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
1 => [:monday, :mon, :january, :jan],
|
147
|
-
2 => [:tuesday, :tues, :february, :feb],
|
148
|
-
3 => [:wednesday, :wednes, :tue, :march, :mar],
|
149
|
-
4 => [:thursday, :thurs, :wed, :april, :apr],
|
150
|
-
5 => [:friday, :fri, :thu, :may],
|
151
|
-
6 => [:saturday, :sat, :june, :jun],
|
152
|
-
7 => [:july, :jul],
|
153
|
-
8 => [:august, :aug],
|
154
|
-
9 => [:september, :sept, :sep],
|
155
|
-
10 => [:october, :oct],
|
156
|
-
11 => [:november, :nov],
|
157
|
-
12 => [:december, :dec]
|
158
|
-
}
|
159
|
-
|
160
|
-
SPECIAL_EXP = {
|
161
|
-
'0 0 * * * *' => ['@daily', '@midnight', 'daily', 'midnight'],
|
162
|
-
'0 12 * * * *' => ['@noon', 'noon'],
|
163
|
-
'0 0 * * 0 *' => ['@weekly', 'weekly'],
|
164
|
-
'0 0 1 * * *' => ['@monthly', 'monthly'],
|
165
|
-
'0 0 1 1 * *' => ['@yearly', '@annually', 'yearly', 'annually'],
|
166
|
-
'? ? ? ? ? ?' => ['@reboot', '@restart', 'reboot', 'restart']
|
167
|
-
}
|
167
|
+
def next_year(time, direction)
|
168
|
+
return time if @parts[:year].empty?
|
169
|
+
until @parts[:year].include?(time.year)
|
170
|
+
day = direction.positive? ? 1 : 31
|
171
|
+
hour = direction.positive? ? 0 : 23
|
172
|
+
min = direction.positive? ? 0 : 59
|
173
|
+
month = direction.positive? ? 1 : 12
|
174
|
+
time = Time.new(time.year + direction, month, day, hour, min)
|
175
|
+
end
|
176
|
+
time
|
177
|
+
end
|
168
178
|
|
179
|
+
PARTS = {
|
180
|
+
minute: { send: :min, min: 0, max: 59, size: 60 },
|
181
|
+
hour: { send: :hour, min: 0, max: 23, size: 60*60 },
|
182
|
+
day: { send: :day, min: 1, max: 31, size: 60*60*24 },
|
183
|
+
month: { send: :month, min: 1, max: 12 },
|
184
|
+
weekday: { send: :wday, min: 0, max: 6 },
|
185
|
+
year: { send: :year, min: 0, max: 3_000 }
|
186
|
+
}.freeze
|
187
|
+
|
188
|
+
REPLACE = {
|
189
|
+
0 => [:sunday, :sun],
|
190
|
+
1 => [:monday, :mon, :january, :jan],
|
191
|
+
2 => [:tuesday, :tues, :february, :feb],
|
192
|
+
3 => [:wednesday, :wednes, :tue, :march, :mar],
|
193
|
+
4 => [:thursday, :thurs, :wed, :april, :apr],
|
194
|
+
5 => [:friday, :fri, :thu, :may],
|
195
|
+
6 => [:saturday, :sat, :june, :jun],
|
196
|
+
7 => [:july, :jul],
|
197
|
+
8 => [:august, :aug],
|
198
|
+
9 => [:september, :sept, :sep],
|
199
|
+
10 => [:october, :oct],
|
200
|
+
11 => [:november, :nov],
|
201
|
+
12 => [:december, :dec]
|
202
|
+
}.freeze
|
203
|
+
|
204
|
+
SPECIAL_EXP = {
|
205
|
+
'0 0 * * * *' => ['@daily', '@midnight', 'daily', 'midnight'],
|
206
|
+
'0 12 * * * *' => ['@noon', 'noon'],
|
207
|
+
'0 0 * * 0 *' => ['@weekly', 'weekly'],
|
208
|
+
'0 0 1 * * *' => ['@monthly', 'monthly'],
|
209
|
+
'0 0 1 1 * *' => ['@yearly', '@annually', 'yearly', 'annually'],
|
210
|
+
'? ? ? ? ? ?' => ['@reboot', '@restart', 'reboot', 'restart']
|
211
|
+
}.freeze
|
169
212
|
end
|
170
|
-
|
171
213
|
end
|