rufus-scheduler 3.4.2 → 3.5.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.
@@ -21,17 +21,19 @@ Job scheduler for Ruby (at, cron, in and every jobs). Not a replacement for cron
21
21
 
22
22
  #s.files = `git ls-files`.split("\n")
23
23
  s.files = Dir[
24
+ 'README.{md,txt}',
25
+ 'CHANGELOG.{md,txt}', 'CREDITS.{md,txt}', 'LICENSE.{md,txt}',
24
26
  'Makefile',
25
27
  'lib/**/*.rb', #'spec/**/*.rb', 'test/**/*.rb',
26
- '*.gemspec', '*.txt', '*.rdoc', '*.md'
28
+ "#{s.name}.gemspec",
27
29
  ]
28
30
 
29
31
  s.required_ruby_version = '>= 1.9'
30
32
 
31
- s.add_runtime_dependency 'et-orbi', '~> 1.0'
33
+ s.add_runtime_dependency 'fugit', '~> 1.1', '>= 1.1.1'
32
34
 
33
- s.add_development_dependency 'rspec', '~> 3.4'
34
- s.add_development_dependency 'chronic'
35
+ s.add_development_dependency 'rspec', '~> 3.7'
36
+ s.add_development_dependency 'chronic', '~> 0.10'
35
37
 
36
38
  s.require_path = 'lib'
37
39
  end
metadata CHANGED
@@ -1,57 +1,63 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rufus-scheduler
3
3
  version: !ruby/object:Gem::Version
4
- version: 3.4.2
4
+ version: 3.5.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - John Mettraux
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2017-05-24 00:00:00.000000000 Z
11
+ date: 2018-05-14 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
- name: et-orbi
14
+ name: fugit
15
15
  requirement: !ruby/object:Gem::Requirement
16
16
  requirements:
17
- - - ~>
17
+ - - "~>"
18
18
  - !ruby/object:Gem::Version
19
- version: '1.0'
19
+ version: '1.1'
20
+ - - ">="
21
+ - !ruby/object:Gem::Version
22
+ version: 1.1.1
20
23
  type: :runtime
21
24
  prerelease: false
22
25
  version_requirements: !ruby/object:Gem::Requirement
23
26
  requirements:
24
- - - ~>
27
+ - - "~>"
28
+ - !ruby/object:Gem::Version
29
+ version: '1.1'
30
+ - - ">="
25
31
  - !ruby/object:Gem::Version
26
- version: '1.0'
32
+ version: 1.1.1
27
33
  - !ruby/object:Gem::Dependency
28
34
  name: rspec
29
35
  requirement: !ruby/object:Gem::Requirement
30
36
  requirements:
31
- - - ~>
37
+ - - "~>"
32
38
  - !ruby/object:Gem::Version
33
- version: '3.4'
39
+ version: '3.7'
34
40
  type: :development
35
41
  prerelease: false
36
42
  version_requirements: !ruby/object:Gem::Requirement
37
43
  requirements:
38
- - - ~>
44
+ - - "~>"
39
45
  - !ruby/object:Gem::Version
40
- version: '3.4'
46
+ version: '3.7'
41
47
  - !ruby/object:Gem::Dependency
42
48
  name: chronic
43
49
  requirement: !ruby/object:Gem::Requirement
44
50
  requirements:
45
- - - '>='
51
+ - - "~>"
46
52
  - !ruby/object:Gem::Version
47
- version: '0'
53
+ version: '0.10'
48
54
  type: :development
49
55
  prerelease: false
50
56
  version_requirements: !ruby/object:Gem::Requirement
51
57
  requirements:
52
- - - '>='
58
+ - - "~>"
53
59
  - !ruby/object:Gem::Version
54
- version: '0'
60
+ version: '0.10'
55
61
  description: Job scheduler for Ruby (at, cron, in and every jobs). Not a replacement
56
62
  for crond.
57
63
  email:
@@ -60,26 +66,18 @@ executables: []
60
66
  extensions: []
61
67
  extra_rdoc_files: []
62
68
  files:
69
+ - CHANGELOG.txt
70
+ - CREDITS.txt
71
+ - LICENSE.txt
63
72
  - Makefile
64
- - lib/rufus/scheduler/cronline.rb
73
+ - README.md
74
+ - lib/rufus-scheduler.rb
75
+ - lib/rufus/scheduler.rb
65
76
  - lib/rufus/scheduler/job_array.rb
66
77
  - lib/rufus/scheduler/jobs.rb
67
78
  - lib/rufus/scheduler/locks.rb
68
79
  - lib/rufus/scheduler/util.rb
69
- - lib/rufus/scheduler.rb
70
- - lib/rufus-scheduler.rb
71
80
  - rufus-scheduler.gemspec
72
- - CHANGELOG.txt
73
- - CREDITS.txt
74
- - fail.txt
75
- - fail18.txt
76
- - LICENSE.txt
77
- - log.txt
78
- - n.txt
79
- - pics.txt
80
- - TODO.txt
81
- - README.md
82
- - sofia.md
83
81
  homepage: http://github.com/jmettraux/rufus-scheduler
84
82
  licenses:
85
83
  - MIT
@@ -90,17 +88,17 @@ require_paths:
90
88
  - lib
91
89
  required_ruby_version: !ruby/object:Gem::Requirement
92
90
  requirements:
93
- - - '>='
91
+ - - ">="
94
92
  - !ruby/object:Gem::Version
95
93
  version: '1.9'
96
94
  required_rubygems_version: !ruby/object:Gem::Requirement
97
95
  requirements:
98
- - - '>='
96
+ - - ">="
99
97
  - !ruby/object:Gem::Version
100
98
  version: '0'
101
99
  requirements: []
102
100
  rubyforge_project: rufus
103
- rubygems_version: 2.0.14
101
+ rubygems_version: 2.6.13
104
102
  signing_key:
105
103
  specification_version: 4
106
104
  summary: job scheduler for Ruby (at, cron, in and every jobs)
data/TODO.txt DELETED
@@ -1,151 +0,0 @@
1
-
2
- [o] merge schedule_queue and unschedule_queue (and merge [un]schedule steps)
3
- [x] OR stop using queue, since we've got the thread-safe JobArray
4
- [x] if possible, drop the mutex in JobArray
5
- NO, that mutex is necessary for Scheduler#jobs (on JRuby an co)...
6
- [o] named mutexes
7
- [o] drop the schedule queue, rely on the mutex in JobArray
8
- [o] def jobs; (@jobs.to_a + running_jobs).uniq; end
9
- [o] replace @unscheduled by @unscheduled_at
10
- [o] make sure #jobs doesn't return unscheduled jobs
11
- [o] job tags and find_by_tag(t) (as in rs 2.x)
12
- [o] require tzinfo anyway (runtime dep)
13
- [o] document frequency
14
- [o] accept :frequency => '5s'
15
- [o] timeout (as in rufus-scheduler 2.x)
16
- [o] Rufus::Scheduler#running_jobs (as in rufus-scheduler 2.x)
17
- [o] Rufus::Scheduler#terminate_all_jobs
18
- [o] Rufus::Scheduler::Job#kill
19
- [x] Rufus::Scheduler#kill_all_jobs
20
- [o] Rufus::Scheduler#shutdown(:terminate or :kill (or nothing))
21
- [o] RepeatJob #pause / #resume (think about discard past)
22
- [o] Rufus::Scheduler.start_new (backward comp) (with deprec note?)
23
- [o] pass job to scheduled block? What does rs 2.x do?
24
- [o] :first[_in|_at] for RepeatJob
25
- [o] :last[_in|_at] for RepeatJob
26
- [o] :times for RepeatJob (how many recurrences)
27
- [o] fix issue #39 (first_at parses as UTC)
28
- [o] about issue #43, raise if cron/every job frequency < scheduler frequency
29
- [o] unlock spec/parse_spec.rb:30 "parse datimes with timezones"
30
- [o] some kind of Schedulable (Xyz#call(job, time))
31
- [o] add Jruby and Rubinius to Travis
32
- [o] make Job #first_at= / #last_at= automatically parse strings?
33
- [o] bring in Kratob's spec about mutex vs timeout and adapt 3.0 to it,
34
- https://github.com/jmettraux/rufus-scheduler/pull/67
35
- [x] :unschedule_if => lambda { |job| ... }
36
- [o] OR look at how it was done in rs 2.0.x, some return value?
37
- no, pass the job as arg to the block, then let the block do job.unschedule
38
- so, document schedule.every('10d') { |j| j.unschedule if x?() }
39
- [x] remove the time in job.trigger(time)
40
- [o] add spec for job queued then unscheduled
41
- [o] add spec for Scheduler#shutdown and work threads
42
- [o] at some point, bring back rbx19 to Travis
43
- [o] move the parse/util part of scheduler.rb to util.rb
44
- [o] rescue KillSignal in job thread loop to kill just the job
45
- [o] add spec for raise if scheduling a job while scheduler is shutting down
46
- [o] schedule_in(2.days.from_now) {}
47
- at and in could understand each others time parameter, ftw...
48
- use the new #parse_to_time? no
49
- [o] do repeat jobs reschedule after timing out? yes
50
- [o] schedule_interval('20s')?
51
- [x] Scheduler#reschedule(job) (new copy of the job)
52
- [x] #free_all_work_threads is missing an implementation
53
- [x] rescue StandardError
54
- :on_error => :crash[_scheduler]
55
- :on_error => :ignore
56
- :on_error => ...
57
- [o] on_error: what about TimeoutError in that scheme?
58
- TimeoutError goes to $stderr, like a normal error
59
- [o] link to SO for support
60
- - sublink to "how to report bugs effectively"
61
- [o] link to #ruote for support
62
- [x] lockblock? pass a block to teach the scheduler how to lock?
63
- is not necessary, @scheduler = Scheduler.new if should_start?
64
- the surrounding Ruby code checks
65
- [o] introduce job "vars", as in
66
- http://stackoverflow.com/questions/18202848/how-to-have-a-variable-that-will-available-to-particular-scheduled-task-whenever
67
- or job['key'] Job #[] and #[]=, as with Thread #[] #[]=
68
- job-local variables #keys #key?
69
- [o] thread-safety for job-local variables?
70
- [x] discard past? discard_past => true or => "1d"
71
- default would be discard_past => "1m" or scheduler freq * 2 ?
72
- jobs would adjust their next_time until it fits the window...
73
- ~~ discard past by default
74
- [o] expanded block/schedulable (it's "callable")
75
- ```
76
- scheduler.every '10m' do
77
- def pre
78
- return false if Backend.down?
79
- # ...
80
- end
81
- def post
82
- # ...
83
- end
84
- def trigger
85
- puts "oh hai!"
86
- end
87
- end
88
- ```
89
- or something like that...
90
- ...
91
- OR accept a class (and instantiate it the first time)
92
- ```
93
- scheduler.every '10m', Class.new do
94
- def call(job, time)
95
- # ...
96
- end
97
- end
98
- ```
99
- the job contains the instance in its @callable
100
- [x] add spec case for corner case in Job#trigger (overlap vs reschedule) !!!
101
- [o] rethink job array vs job set for #scheduled?
102
- [x] introduce common parent class for EveryJob and IntervalJob
103
- [o] create spec/ at_job_spec.rb, repeat_job_spec.rb, cron_job_spec.rb, ...
104
- [x] ensure EveryJob do not schedule in the past (it's already like that)
105
- [o] CronLine#next_time should return a time with subseconds chopped off
106
- [o] drop min work threads setting?
107
- [o] thread pool something? Thread upper limit?
108
- [o] Rufus::Scheduler.singleton, Rufus::Scheduler.s
109
- [o] EveryJob#first_at= and IntervalJob#first_at= should alter @next_time
110
- [o] scheduler.schedule duration/time/cron ... for at/in/cron
111
- (not every, nor interval)
112
- scheduler.repeat time/cron ... for every/cron
113
-
114
- [o] :lockfile => x, timestamp, process_id, thread_id...
115
- warning: have to clean up that file on exit... or does the scheduler
116
- timestamps it?
117
- [ ] develop lockfile timestamp thinggy
118
- ~ if the timestamp is too old (twice the default frequency?) then
119
- lock [file] take over...
120
- Is that really what we want all the time?
121
-
122
- [ ] idea: :mutex => x and :skip_on_mutex => true ?
123
- would prevent blocking/waiting for the mutex to get available
124
- :mutex => [ "mutex_name", true ]
125
- :mutex => [ [ "mutex_name", true ], [ "other_mutex_name", false ] ]
126
-
127
- [ ] bring back EM (but only EM.defer ?) :defer => true (Job or Scheduler
128
- or both option?)
129
-
130
- [ ] prepare a daemon, trust daemon-kit for that
131
-
132
- [ ] :if => lambda { |job, time| ... } why not?
133
- :unless => lambda { ...
134
- :block => lambda { ...
135
- can help get the block themselves leaner
136
- #
137
- investigate guards for schedulables... def if_guard; ...; end
138
-
139
- [ ] scheduler.every '10', Class.new do
140
- def call(job, time)
141
- # might fail...
142
- end
143
- def on_error(err, job)
144
- # catches...
145
- end
146
- end
147
-
148
- ~~~
149
-
150
- [ ] scheduler.at('chronic string', chronic_options...)
151
-
data/fail.txt DELETED
@@ -1,2 +0,0 @@
1
- rspec ./spec/job_spec.rb:465 # Rufus::Scheduler::Job :mutex :mutex => [ array_of_mutex_names_or_instances ] prevents concurrent executions
2
- rspec ./spec/scheduler_spec.rb:534 # Rufus::Scheduler#running_jobs(:tag/:tags => x) returns a list of running jobs filtered by tag
data/fail18.txt DELETED
@@ -1,12 +0,0 @@
1
-
2
- rspec ./spec/job_spec.rb:640 # Rufus::Scheduler::Job work time #mean_work_time gathers work times and computes the mean
3
- rspec ./spec/schedule_at_spec.rb:60 # Rufus::Scheduler#at triggers a job
4
- rspec ./spec/schedule_in_spec.rb:44 # Rufus::Scheduler#in removes the job after execution
5
- rspec ./spec/scheduler_spec.rb:83 # Rufus::Scheduler a schedule method passes the job to its block when it triggers
6
- rspec ./spec/scheduler_spec.rb:534 # Rufus::Scheduler#running_jobs(:tag/:tags => x) returns a list of running jobs filtered by tag
7
- rspec ./spec/scheduler_spec.rb:601 # Rufus::Scheduler#occurrences(time0, time1) respects :times for repeat jobs
8
- rspec ./spec/scheduler_spec.rb:1019 # Rufus::Scheduler#on_post_trigger is called right after a job triggers
9
-
10
-
11
- determine_id specs are slower... much slower...
12
-
@@ -1,498 +0,0 @@
1
-
2
-
3
- class Rufus::Scheduler
4
-
5
- #
6
- # A 'cron line' is a line in the sense of a crontab
7
- # (man 5 crontab) file line.
8
- #
9
- class CronLine
10
-
11
- # The max number of years in the future or the past before giving up
12
- # searching for #next_time or #previous_time respectively
13
- #
14
- NEXT_TIME_MAX_YEARS = 14
15
-
16
- # The string used for creating this cronline instance.
17
- #
18
- attr_reader :original
19
- attr_reader :original_timezone
20
-
21
- attr_reader :seconds
22
- attr_reader :minutes
23
- attr_reader :hours
24
- attr_reader :days
25
- attr_reader :months
26
- #attr_reader :monthdays # reader defined below
27
- attr_reader :weekdays
28
- attr_reader :timezone
29
-
30
- def initialize(line)
31
-
32
- fail ArgumentError.new(
33
- "not a string: #{line.inspect}"
34
- ) unless line.is_a?(String)
35
-
36
- @original = line
37
- @original_timezone = nil
38
-
39
- items = line.split
40
-
41
- if @timezone = EoTime.get_tzone(items.last)
42
- @original_timezone = items.pop
43
- else
44
- @timezone = EoTime.local_tzone
45
- end
46
-
47
- fail ArgumentError.new(
48
- "not a valid cronline : '#{line}'"
49
- ) unless items.length == 5 or items.length == 6
50
-
51
- offset = items.length - 5
52
-
53
- @seconds = offset == 1 ? parse_item(items[0], 0, 59) : [ 0 ]
54
- @minutes = parse_item(items[0 + offset], 0, 59)
55
- @hours = parse_item(items[1 + offset], 0, 24)
56
- @days = parse_item(items[2 + offset], -30, 31)
57
- @months = parse_item(items[3 + offset], 1, 12)
58
- @weekdays, @monthdays = parse_weekdays(items[4 + offset])
59
-
60
- [ @seconds, @minutes, @hours, @months ].each do |es|
61
-
62
- fail ArgumentError.new(
63
- "invalid cronline: '#{line}'"
64
- ) if es && es.find { |e| ! e.is_a?(Integer) }
65
- end
66
-
67
- if @days && @days.include?(0) # gh-221
68
-
69
- fail ArgumentError.new('invalid day 0 in cronline')
70
- end
71
- end
72
-
73
- # Returns true if the given time matches this cron line.
74
- #
75
- def matches?(time)
76
-
77
- # FIXME Don't create a new EoTime if time is already a EoTime in same
78
- # zone ...
79
- # Wait, this seems only used in specs...
80
- t = EoTime.new(time.to_f, @timezone)
81
-
82
- return false unless sub_match?(t, :sec, @seconds)
83
- return false unless sub_match?(t, :min, @minutes)
84
- return false unless sub_match?(t, :hour, @hours)
85
- return false unless date_match?(t)
86
- true
87
- end
88
-
89
- # Returns the next time that this cron line is supposed to 'fire'
90
- #
91
- # This is raw, 3 secs to iterate over 1 year on my macbook :( brutal.
92
- # (Well, I was wrong, takes 0.001 sec on 1.8.7 and 1.9.1)
93
- #
94
- # This method accepts an optional Time parameter. It's the starting point
95
- # for the 'search'. By default, it's Time.now
96
- #
97
- # Note that the time instance returned will be in the same time zone that
98
- # the given start point Time (thus a result in the local time zone will
99
- # be passed if no start time is specified (search start time set to
100
- # Time.now))
101
- #
102
- # Rufus::Scheduler::CronLine.new('30 7 * * *').next_time(
103
- # Time.mktime(2008, 10, 24, 7, 29))
104
- # #=> Fri Oct 24 07:30:00 -0500 2008
105
- #
106
- # Rufus::Scheduler::CronLine.new('30 7 * * *').next_time(
107
- # Time.utc(2008, 10, 24, 7, 29))
108
- # #=> Fri Oct 24 07:30:00 UTC 2008
109
- #
110
- # Rufus::Scheduler::CronLine.new('30 7 * * *').next_time(
111
- # Time.utc(2008, 10, 24, 7, 29)).localtime
112
- # #=> Fri Oct 24 02:30:00 -0500 2008
113
- #
114
- # (Thanks to K Liu for the note and the examples)
115
- #
116
- def next_time(from=EoTime.now)
117
-
118
- nt = nil
119
- zt = EoTime.new(from.to_i + 1, @timezone)
120
- maxy = from.year + NEXT_TIME_MAX_YEARS
121
-
122
- loop do
123
-
124
- nt = zt.dup
125
-
126
- fail RangeError.new(
127
- "failed to reach occurrence within " +
128
- "#{NEXT_TIME_MAX_YEARS} years for '#{original}'"
129
- ) if nt.year > maxy
130
-
131
- unless date_match?(nt)
132
- zt.add((24 - nt.hour) * 3600 - nt.min * 60 - nt.sec)
133
- next
134
- end
135
- unless sub_match?(nt, :hour, @hours)
136
- zt.add((60 - nt.min) * 60 - nt.sec)
137
- next
138
- end
139
- unless sub_match?(nt, :min, @minutes)
140
- zt.add(60 - nt.sec)
141
- next
142
- end
143
- unless sub_match?(nt, :sec, @seconds)
144
- zt.add(next_second(nt))
145
- next
146
- end
147
-
148
- break
149
- end
150
-
151
- nt
152
- end
153
-
154
- # Returns the previous time the cronline matched. It's like next_time, but
155
- # for the past.
156
- #
157
- def previous_time(from=EoTime.now)
158
-
159
- pt = nil
160
- zt = EoTime.new(from.to_i - 1, @timezone)
161
- miny = from.year - NEXT_TIME_MAX_YEARS
162
-
163
- loop do
164
-
165
- pt = zt.dup
166
-
167
- fail RangeError.new(
168
- "failed to reach occurrence within " +
169
- "#{NEXT_TIME_MAX_YEARS} years for '#{original}'"
170
- ) if pt.year < miny
171
-
172
- unless date_match?(pt)
173
- zt.subtract(pt.hour * 3600 + pt.min * 60 + pt.sec + 1)
174
- next
175
- end
176
- unless sub_match?(pt, :hour, @hours)
177
- zt.subtract(pt.min * 60 + pt.sec + 1)
178
- next
179
- end
180
- unless sub_match?(pt, :min, @minutes)
181
- zt.subtract(pt.sec + 1)
182
- next
183
- end
184
- unless sub_match?(pt, :sec, @seconds)
185
- zt.subtract(prev_second(pt))
186
- next
187
- end
188
-
189
- break
190
- end
191
-
192
- pt
193
- end
194
-
195
- # Returns an array of 6 arrays (seconds, minutes, hours, days,
196
- # months, weekdays).
197
- # This method is mostly used by the cronline specs.
198
- #
199
- def to_a
200
-
201
- [
202
- toa(@seconds),
203
- toa(@minutes),
204
- toa(@hours),
205
- toa(@days),
206
- toa(@months),
207
- toa(@weekdays),
208
- toa(@monthdays),
209
- @timezone.name
210
- ]
211
- end
212
- alias to_array to_a
213
-
214
- # Returns a quickly computed approximation of the frequency for this
215
- # cron line.
216
- #
217
- # #brute_frequency, on the other hand, will compute the frequency by
218
- # examining a whole year, that can take more than seconds for a seconds
219
- # level cron...
220
- #
221
- def frequency
222
-
223
- return brute_frequency unless @seconds && @seconds.length > 1
224
-
225
- secs = toa(@seconds)
226
-
227
- secs[1..-1].inject([ secs[0], 60 ]) { |(prev, delta), sec|
228
- d = sec - prev
229
- [ sec, d < delta ? d : delta ]
230
- }[1]
231
- end
232
-
233
- # Caching facility. Currently only used for brute frequencies.
234
- #
235
- @cache = {}; class << self; attr_reader :cache; end
236
-
237
- # Returns the shortest delta between two potential occurrences of the
238
- # schedule described by this cronline.
239
- #
240
- # .
241
- #
242
- # For a simple cronline like "*/5 * * * *", obviously the frequency is
243
- # five minutes. Why does this method look at a whole year of #next_time ?
244
- #
245
- # Consider "* * * * sun#2,sun#3", the computed frequency is 1 week
246
- # (the shortest delta is the one between the second sunday and the third
247
- # sunday). This method takes no chance and runs next_time for the span
248
- # of a whole year and keeps the shortest.
249
- #
250
- # Of course, this method can get VERY slow if you call on it a second-
251
- # based cronline...
252
- #
253
- def brute_frequency
254
-
255
- key = "brute_frequency:#{@original}"
256
-
257
- delta = self.class.cache[key]
258
- return delta if delta
259
-
260
- delta = 366 * DAY_S
261
-
262
- t0 = previous_time(Time.local(2000, 1, 1))
263
-
264
- loop do
265
-
266
- break if delta <= 1
267
- break if delta <= 60 && @seconds && @seconds.size == 1
268
-
269
- #st = Time.now
270
- t1 = next_time(t0)
271
- #p Time.now - st
272
- d = t1 - t0
273
- delta = d if d < delta
274
- break if @months.nil? && t1.month == 2
275
- break if @months.nil? && @days.nil? && t1.day == 2
276
- break if @months.nil? && @days.nil? && @hours.nil? && t1.hour == 1
277
- break if @months.nil? && @days.nil? && @hours.nil? && @minutes.nil? && t1.min == 1
278
- break if t1.year >= 2001
279
-
280
- t0 = t1
281
- end
282
-
283
- self.class.cache[key] = delta
284
- end
285
-
286
- def next_second(time)
287
-
288
- secs = toa(@seconds)
289
-
290
- return secs.first + 60 - time.sec if time.sec > secs.last
291
-
292
- secs.shift while secs.first < time.sec
293
-
294
- secs.first - time.sec
295
- end
296
-
297
- def prev_second(time)
298
-
299
- secs = toa(@seconds)
300
-
301
- return time.sec + 60 - secs.last if time.sec < secs.first
302
-
303
- secs.pop while time.sec < secs.last
304
-
305
- time.sec - secs.last
306
- end
307
-
308
- protected
309
-
310
- def sc_sort(a)
311
-
312
- a.sort_by { |e| e.is_a?(String) ? 61 : e.to_i }
313
- end
314
-
315
- if RUBY_VERSION >= '1.9'
316
- def toa(item)
317
- item == nil ? nil : item.to_a
318
- end
319
- else
320
- def toa(item)
321
- item.is_a?(Set) ? sc_sort(item.to_a) : item
322
- end
323
- end
324
-
325
- WEEKDAYS = %w[ sun mon tue wed thu fri sat ]
326
- DAY_S = 24 * 3600
327
-
328
- def parse_weekdays(item)
329
-
330
- return nil if item == '*'
331
-
332
- weekdays = nil
333
- monthdays = nil
334
-
335
- item.downcase.split(',').each do |it|
336
-
337
- WEEKDAYS.each_with_index { |a, i| it.gsub!(/#{a}/, i.to_s) }
338
-
339
- it = it.gsub(/([^#])l/, '\1#-1')
340
- # "5L" == "5#-1" == the last Friday
341
-
342
- if m = it.match(/\A(.+)#(l|-?[12345])\z/)
343
-
344
- fail ArgumentError.new(
345
- "ranges are not supported for monthdays (#{it})"
346
- ) if m[1].index('-')
347
-
348
- it = it.gsub(/#l/, '#-1')
349
-
350
- (monthdays ||= []) << it
351
-
352
- else
353
-
354
- fail ArgumentError.new(
355
- "invalid weekday expression (#{item})"
356
- ) if it !~ /\A0*[0-7](-0*[0-7])?\z/
357
-
358
- its = it.index('-') ? parse_range(it, 0, 7) : [ Integer(it) ]
359
- its = its.collect { |i| i == 7 ? 0 : i }
360
-
361
- (weekdays ||= []).concat(its)
362
- end
363
- end
364
-
365
- weekdays = weekdays.uniq.sort if weekdays
366
-
367
- [ weekdays, monthdays ]
368
- end
369
-
370
- def parse_item(item, min, max)
371
-
372
- return nil if item == '*'
373
-
374
- r = item.split(',').map { |i| parse_range(i.strip, min, max) }.flatten
375
-
376
- fail ArgumentError.new(
377
- "found duplicates in #{item.inspect}"
378
- ) if r.uniq.size < r.size
379
-
380
- r = sc_sort(r)
381
-
382
- Set.new(r)
383
- end
384
-
385
- RANGE_REGEX = /\A(\*|-?\d{1,2})(?:-(-?\d{1,2}))?(?:\/(\d{1,2}))?\z/
386
-
387
- def parse_range(item, min, max)
388
-
389
- return %w[ L ] if item == 'L'
390
-
391
- item = '*' + item if item[0, 1] == '/'
392
-
393
- m = item.match(RANGE_REGEX)
394
-
395
- fail ArgumentError.new(
396
- "cannot parse #{item.inspect}"
397
- ) unless m
398
-
399
- mmin = min == -30 ? 1 : min # days
400
-
401
- sta = m[1]
402
- sta = sta == '*' ? mmin : sta.to_i
403
-
404
- edn = m[2]
405
- edn = edn ? edn.to_i : sta
406
- edn = max if m[1] == '*'
407
-
408
- inc = m[3]
409
- inc = inc ? inc.to_i : 1
410
-
411
- fail ArgumentError.new(
412
- "#{item.inspect} positive/negative ranges not allowed"
413
- ) if (sta < 0 && edn > 0) || (sta > 0 && edn < 0)
414
-
415
- fail ArgumentError.new(
416
- "#{item.inspect} descending day ranges not allowed"
417
- ) if min == -30 && sta > edn
418
-
419
- fail ArgumentError.new(
420
- "#{item.inspect} is not in range #{min}..#{max}"
421
- ) if sta < min || edn > max
422
-
423
- fail ArgumentError.new(
424
- "#{item.inspect} increment must be greater than zero"
425
- ) if inc == 0
426
-
427
- r = []
428
- val = sta
429
-
430
- loop do
431
- v = val
432
- v = 0 if max == 24 && v == 24 # hours
433
- r << v
434
- break if inc == 1 && val == edn
435
- val += inc
436
- break if inc > 1 && val > edn
437
- val = min if val > max
438
- end
439
-
440
- r.uniq
441
- end
442
-
443
- # FIXME: Eventually split into day_match?, hour_match? and monthdays_match?o
444
- #
445
- def sub_match?(time, accessor, values)
446
-
447
- return true if values.nil?
448
-
449
- value = time.send(accessor)
450
-
451
- if accessor == :day
452
-
453
- values.each do |v|
454
- return true if v == 'L' && (time + DAY_S).day == 1
455
- return true if v.to_i < 0 && (time + (1 - v) * DAY_S).day == 1
456
- end
457
- end
458
-
459
- if accessor == :hour
460
-
461
- return true if value == 0 && values.include?(24)
462
- end
463
-
464
- if accessor == :monthdays
465
-
466
- return true if (values & value).any?
467
- end
468
-
469
- values.include?(value)
470
- end
471
-
472
- # def monthday_match?(zt, values)
473
- #
474
- # return true if values.nil?
475
- #
476
- # today_values = monthdays(zt)
477
- #
478
- # (today_values & values).any?
479
- # end
480
-
481
- def date_match?(zt)
482
-
483
- return false unless sub_match?(zt, :day, @days)
484
- return false unless sub_match?(zt, :month, @months)
485
-
486
- return true if (
487
- (@weekdays && @monthdays) &&
488
- (sub_match?(zt, :wday, @weekdays) ||
489
- sub_match?(zt, :monthdays, @monthdays)))
490
-
491
- return false unless sub_match?(zt, :wday, @weekdays)
492
- return false unless sub_match?(zt, :monthdays, @monthdays)
493
-
494
- true
495
- end
496
- end
497
- end
498
-