bblib 0.3.0 → 0.4.1

Sign up to get free protection for your applications and to get access to all the features.
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