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.
Files changed (62) hide show
  1. checksums.yaml +5 -5
  2. data/.gitignore +11 -10
  3. data/.rspec +2 -2
  4. data/.travis.yml +4 -4
  5. data/CODE_OF_CONDUCT.md +13 -13
  6. data/Gemfile +4 -4
  7. data/LICENSE.txt +21 -21
  8. data/README.md +247 -757
  9. data/Rakefile +6 -6
  10. data/bblib.gemspec +34 -34
  11. data/bin/console +14 -14
  12. data/bin/setup +7 -7
  13. data/lib/array/bbarray.rb +71 -29
  14. data/lib/bblib.rb +12 -12
  15. data/lib/bblib/version.rb +3 -3
  16. data/lib/class/effortless.rb +23 -0
  17. data/lib/error/abstract.rb +3 -0
  18. data/lib/file/bbfile.rb +93 -52
  19. data/lib/hash/bbhash.rb +130 -46
  20. data/lib/hash/hash_struct.rb +24 -0
  21. data/lib/hash/tree_hash.rb +364 -0
  22. data/lib/hash_path/hash_path.rb +210 -0
  23. data/lib/hash_path/part.rb +83 -0
  24. data/lib/hash_path/path_hash.rb +84 -0
  25. data/lib/hash_path/proc.rb +93 -0
  26. data/lib/hash_path/processors.rb +239 -0
  27. data/lib/html/bbhtml.rb +2 -0
  28. data/lib/html/builder.rb +34 -0
  29. data/lib/html/tag.rb +49 -0
  30. data/lib/logging/bblogging.rb +42 -0
  31. data/lib/mixins/attrs.rb +422 -0
  32. data/lib/mixins/bbmixins.rb +7 -0
  33. data/lib/mixins/bridge.rb +17 -0
  34. data/lib/mixins/family_tree.rb +41 -0
  35. data/lib/mixins/hooks.rb +139 -0
  36. data/lib/mixins/logger.rb +31 -0
  37. data/lib/mixins/serializer.rb +71 -0
  38. data/lib/mixins/simple_init.rb +160 -0
  39. data/lib/number/bbnumber.rb +15 -7
  40. data/lib/object/bbobject.rb +46 -19
  41. data/lib/opal/bbopal.rb +0 -4
  42. data/lib/os/bbos.rb +24 -16
  43. data/lib/os/bbsys.rb +60 -43
  44. data/lib/string/bbstring.rb +165 -66
  45. data/lib/string/cases.rb +37 -29
  46. data/lib/string/fuzzy_matcher.rb +48 -50
  47. data/lib/string/matching.rb +43 -30
  48. data/lib/string/pluralization.rb +156 -0
  49. data/lib/string/regexp.rb +45 -0
  50. data/lib/string/roman.rb +17 -30
  51. data/lib/system/bbsystem.rb +42 -0
  52. data/lib/time/bbtime.rb +79 -58
  53. data/lib/time/cron.rb +174 -132
  54. data/lib/time/task_timer.rb +86 -70
  55. metadata +27 -10
  56. data/lib/gem/bbgem.rb +0 -28
  57. data/lib/hash/hash_path.rb +0 -344
  58. data/lib/hash/hash_path_proc.rb +0 -256
  59. data/lib/hash/path_hash.rb +0 -81
  60. data/lib/object/attr.rb +0 -182
  61. data/lib/object/hooks.rb +0 -69
  62. 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
@@ -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 num
7
+ def self.to_roman(num)
6
8
  return num.to_s if num > 1000
7
- roman = {1000 => 'M', 900 => 'CM', 500 => 'D', 400 => 'CD', 100 => 'C', 90 => 'XC', 50 => 'L',
8
- 40 => 'XL', 10 => 'X', 9 => 'IX', 5 => 'V', 4 => 'IV', 3 => 'III', 2 => 'II', 1 => 'I'}
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 str
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 = s.sub(s.scan(/\d+/).first.to_s, BBLib.to_roman(s.to_i))
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 !sp.select{ |i| i[/#{num}/i]}.empty?
36
- for i in 0..(sp.length-1)
37
- if sp[i].drop_symbols.upcase == num
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 Fixnum
43
+ class Integer
49
44
  def to_roman
50
- BBLib.to_roman self.to_i
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
@@ -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 str, output: :sec, min_interval: :sec
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 |k, v|
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 num, input: :sec, stop: :milli, style: :medium
37
- return nil unless Numeric === num || num > 0
38
- if ![:full, :medium, :short].include?(style) then style = :medium end
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, done = num * TIME_EXPS[input.to_sym][:mult], false
40
+ n = num * TIME_EXPS[input.to_sym][:mult]
41
+ done = false
41
42
  TIME_EXPS.reverse.each do |k, v|
42
- next unless !done
43
- if k == stop then done = true end
43
+ next if done
44
+ done = true if k == stop
44
45
  div = n / v[:mult]
45
- if div >= 1
46
- val = (done ? div.round : div.floor)
47
- expression << "#{val}#{v[:styles][style]}#{val > 1 && style != :short ? "s" : nil}"
48
- n-= val.to_f * v[:mult]
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: {full: ' yoctosecond', medium: ' yocto', short: 'ys'},
58
- exp: ['yoctosecond', 'yocto', 'yoctoseconds', 'yoctos', 'ys']
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: {full: ' zeptosecond', medium: ' zepto', short: 'zs'},
63
- exp: ['zeptosecond', 'zepto', 'zeptoseconds', 'zeptos', 'zs']
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: {full: ' attosecond', medium: ' atto', short: 'as'},
68
- exp: ['attoseconds', 'atto', 'attoseconds', 'attos', 'as']
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: {full: ' femtosecond', medium: ' fempto', short: 'fs'},
73
- exp: ['femtosecond', 'fempto', 'femtoseconds', 'femptos', 'fs']
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: {full: ' picosecond', medium: ' pico', short: 'ps'},
78
- exp: ['picosecond', 'pico', 'picoseconds', 'picos', 'ps']
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: {full: ' nanosecond', medium: ' nano', short: 'ns'},
83
- exp: ['nanosecond', 'nano', 'nanoseconds', 'nanos', 'ns']
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: {full: ' microsecond', medium: ' micro', short: 'μs'},
88
- exp: ['microsecond', 'micro', 'microseconds', 'micros', 'μs']
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: {full: ' millisecond', medium: ' mil', short: 'ms'},
93
- exp: ['ms', 'mil', 'mils', 'milli', 'millis', 'millisecond', 'milliseconds', 'milsec', 'milsecs', 'msec', 'msecs', 'msecond', 'mseconds']},
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: {full: ' second', medium: ' sec', short: 's'},
97
- exp: ['s', 'sec', 'secs', 'second', 'seconds']},
107
+ styles: { long: ' second', medium: ' sec', short: 's' },
108
+ exp: %w(s sec secs second seconds)
109
+ },
98
110
  min: {
99
- mult: 60000,
100
- styles: {full: ' minute', medium: ' min', short: 'm'},
101
- exp: ['m', 'mn', 'mns', 'min', 'mins', 'minute', 'minutes']},
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: 3600000,
104
- styles: {full: ' hour', medium: ' hr', short: 'h'},
105
- exp: ['h', 'hr', 'hrs', 'hour', 'hours']},
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: 86400000,
108
- styles: {full: ' day', medium: ' day', short: 'd'},
109
- exp: ['d', 'day', 'days']},
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: 604800000,
112
- styles: {full: ' week', medium: ' wk', short: 'w'},
113
- exp: ['w', 'wk', 'wks', 'week', 'weeks']},
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: 2592000000,
116
- styles: {full: ' month', medium: ' mo', short: 'mo'},
117
- exp: ['mo', 'mon', 'mons', 'month', 'months', 'mnth', 'mnths', 'mth', 'mths']},
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: 31536000000,
120
- styles: {full: ' year', medium: ' yr', short: 'y'},
121
- exp: ['y', 'yr', 'yrs', 'year', 'years']}
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 output: :sec, min_interval: :sec
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 input: :sec, stop: :milli, style: :medium
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
@@ -1,171 +1,213 @@
1
+ # frozen_string_literal: true
1
2
  module BBLib
2
-
3
3
  class Cron
4
- attr_reader :exp, :parts, :time
4
+ include Effortless
5
+ attr_str :expression, default: '* * * * * *'
6
+ attr_reader :parts, serialize: false
5
7
 
6
- def initialize exp = '* * * * * *'
7
- @parts = Hash.new
8
- self.exp = exp
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 closest exp = @exp, direction:1, count: 1, time: Time.now
12
- if exp then self.exp = exp end
13
- results = []
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 next exp = @exp, count: 1, time: Time.now
20
- closest exp, count:count, time:time, direction:1
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 prev exp = @exp, count: 1, time: Time.now
24
- closest exp, count:count, time:time, direction:-1
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= e
28
- SPECIAL_EXP.each{ |x, v| if v.include?(e) then e = x end }
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.next exp, count: 1, time: Time.now
34
- t = BBLib::Cron.new(exp).next(count:count, time:time)
34
+ def self.valid?(exp)
35
+ !(numeralize(exp) =~ /\A(.*?\s){4,5}.*?\S\z/).nil?
35
36
  end
36
37
 
37
- def self.prev exp, count: 1, time: Time.now
38
- BBLib::Cron.new(exp).prev(count:count, time:time)
38
+ def valid?(exp)
39
+ BBLib::Cron.valid?(exp)
39
40
  end
40
41
 
41
- def self.valid? exp
42
- !(numeralize(exp) =~ /\A(.*?\s){4,5}.*?\S\z/).nil?
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 valid? exp
46
- BBLib::Cron.valid?(exp)
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
- def parse
52
- return nil unless @exp
53
- pieces, i = @exp.split(' '), 0
54
- PARTS.each do |part, info|
55
- @parts[part] = parse_cron_numbers(pieces[i], info[:min], info[:max], Time.now.send(info[:send]))
56
- i+=1
57
- end
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
- def self.numeralize exp
61
- exp = exp.to_s.downcase
62
- REPLACE.each do |k, v|
63
- v.each do |r|
64
- exp = exp.gsub(r.to_s, k.to_s)
65
- end
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
- def parse_cron_numbers exp, min, max, qmark
71
- numbers = Array.new
72
- exp = Cron.numeralize(exp)
73
- exp = exp.gsub('?', qmark.to_s)
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
- def next_day time, direction
101
- return nil unless time
102
- weekdays, days, months, years = @parts[:weekday], @parts[:day], @parts[:month], @parts[:year]
103
- date, safety = nil, 0
104
- while date.nil? && safety < 50000
105
- if (days.empty? || days.include?(time.day)) && (months.empty? || months.include?(time.month)) && (years.empty? || years.include?(time.year)) && (weekdays.empty? || weekdays.include?(time.wday))
106
- date = time
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
- return nil if safety == 50000
114
- time
111
+ safety += 1
115
112
  end
113
+ time - (time.sec.zero? ? 0 : original.sec)
114
+ end
116
115
 
117
- def next_time time, direction
118
- orig, fw = time.to_f, (direction == 1)
119
- current = next_day(time, direction)
120
- return nil unless current
121
- if (fw ? current.to_f > orig : current.to_f < orig)
122
- current = Time.new(current.year, current.month, current.day, (fw ? 0 : 23), (fw ? 0 : 59))
123
- else
124
- current+= (fw ? 60 : -60)
125
- end
126
- while !@parts[:day].empty? && !@parts[:day].include?(current.day) || !@parts[:hour].empty? && !@parts[:hour].include?(current.hour) || !@parts[:minute].empty? && !@parts[:minute].include?(current.min)
127
- day = [current.day, current.month, current.year]
128
- current+= (fw ? 60 : -60)
129
- if day != [current.day, current.month, current.year] then current = next_day(current, direction) end
130
- return nil unless current
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
- PARTS = {
136
- minute: {send: :min, min:0, max:59, size: 60},
137
- hour: {send: :hour, min:0, max:23, size: 60*60},
138
- day: {send: :day, min:1, max:31, size: 60*60*24},
139
- month: {send: :month, min:1, max:12},
140
- weekday: {send: :wday, min:0, max:6},
141
- year: {send: :year, min:0, max:90000}
142
- }
143
-
144
- REPLACE = {
145
- 0 => [:sunday, :sun],
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