uv-rays 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,386 @@
1
+ #--
2
+ # Copyright (c) 2006-2013, John Mettraux, jmettraux@gmail.com
3
+ #
4
+ # Permission is hereby granted, free of charge, to any person obtaining a copy
5
+ # of this software and associated documentation files (the "Software"), to deal
6
+ # in the Software without restriction, including without limitation the rights
7
+ # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
8
+ # copies of the Software, and to permit persons to whom the Software is
9
+ # furnished to do so, subject to the following conditions:
10
+ #
11
+ # The above copyright notice and this permission notice shall be included in
12
+ # all copies or substantial portions of the Software.
13
+ #
14
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15
+ # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
16
+ # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
17
+ # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
18
+ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
19
+ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
20
+ # THE SOFTWARE.
21
+ #
22
+ # Made in Japan.
23
+ #++
24
+
25
+
26
+ module UvRays
27
+ class Scheduler
28
+ #
29
+ # A 'cron line' is a line in the sense of a crontab
30
+ # (man 5 crontab) file line.
31
+ #
32
+ class CronLine
33
+
34
+ # The string used for creating this cronline instance.
35
+ #
36
+ attr_reader :original
37
+
38
+ attr_reader :seconds
39
+ attr_reader :minutes
40
+ attr_reader :hours
41
+ attr_reader :days
42
+ attr_reader :months
43
+ attr_reader :weekdays
44
+ attr_reader :monthdays
45
+ attr_reader :timezone
46
+
47
+ def initialize(line)
48
+
49
+ raise ArgumentError.new(
50
+ "not a string: #{line.inspect}"
51
+ ) unless line.is_a?(String)
52
+
53
+ @original = line
54
+
55
+ items = line.split
56
+
57
+ @timezone = (TZInfo::Timezone.get(items.last) rescue nil)
58
+ items.pop if @timezone
59
+
60
+ raise ArgumentError.new(
61
+ "not a valid cronline : '#{line}'"
62
+ ) unless items.length == 5 or items.length == 6
63
+
64
+ offset = items.length - 5
65
+
66
+ @seconds = offset == 1 ? parse_item(items[0], 0, 59) : [ 0 ]
67
+ @minutes = parse_item(items[0 + offset], 0, 59)
68
+ @hours = parse_item(items[1 + offset], 0, 24)
69
+ @days = parse_item(items[2 + offset], 1, 31)
70
+ @months = parse_item(items[3 + offset], 1, 12)
71
+ @weekdays, @monthdays = parse_weekdays(items[4 + offset])
72
+
73
+ [ @seconds, @minutes, @hours, @months ].each do |es|
74
+
75
+ raise ArgumentError.new(
76
+ "invalid cronline: '#{line}'"
77
+ ) if es && es.find { |e| ! e.is_a?(Fixnum) }
78
+ end
79
+ end
80
+
81
+ # Returns true if the given time matches this cron line.
82
+ #
83
+ def matches?(time)
84
+
85
+ time = Time.at(time) unless time.kind_of?(Time)
86
+
87
+ time = @timezone.utc_to_local(time.getutc) if @timezone
88
+
89
+ return false unless sub_match?(time, :sec, @seconds)
90
+ return false unless sub_match?(time, :min, @minutes)
91
+ return false unless sub_match?(time, :hour, @hours)
92
+ return false unless date_match?(time)
93
+ true
94
+ end
95
+
96
+ # Returns the next time that this cron line is supposed to 'fire'
97
+ #
98
+ # This is raw, 3 secs to iterate over 1 year on my macbook :( brutal.
99
+ # (Well, I was wrong, takes 0.001 sec on 1.8.7 and 1.9.1)
100
+ #
101
+ # This method accepts an optional Time parameter. It's the starting point
102
+ # for the 'search'. By default, it's Time.now
103
+ #
104
+ # Note that the time instance returned will be in the same time zone that
105
+ # the given start point Time (thus a result in the local time zone will
106
+ # be passed if no start time is specified (search start time set to
107
+ # Time.now))
108
+ #
109
+ # Rufus::Scheduler::CronLine.new('30 7 * * *').next_time(
110
+ # Time.mktime(2008, 10, 24, 7, 29))
111
+ # #=> Fri Oct 24 07:30:00 -0500 2008
112
+ #
113
+ # Rufus::Scheduler::CronLine.new('30 7 * * *').next_time(
114
+ # Time.utc(2008, 10, 24, 7, 29))
115
+ # #=> Fri Oct 24 07:30:00 UTC 2008
116
+ #
117
+ # Rufus::Scheduler::CronLine.new('30 7 * * *').next_time(
118
+ # Time.utc(2008, 10, 24, 7, 29)).localtime
119
+ # #=> Fri Oct 24 02:30:00 -0500 2008
120
+ #
121
+ # (Thanks to K Liu for the note and the examples)
122
+ #
123
+ def next_time(from=Time.now)
124
+
125
+ time = @timezone ? @timezone.utc_to_local(from.getutc) : from
126
+
127
+ time = time.respond_to?(:round) ? time.round : time - time.usec * 1e-6
128
+ # chop off subseconds (and yes, Ruby 1.8 doesn't have #round)
129
+
130
+ time = time + 1
131
+ # start at the next second
132
+
133
+ loop do
134
+
135
+ unless date_match?(time)
136
+ time += (24 - time.hour) * 3600 - time.min * 60 - time.sec; next
137
+ end
138
+ unless sub_match?(time, :hour, @hours)
139
+ time += (60 - time.min) * 60 - time.sec; next
140
+ end
141
+ unless sub_match?(time, :min, @minutes)
142
+ time += 60 - time.sec; next
143
+ end
144
+ unless sub_match?(time, :sec, @seconds)
145
+ time += 1; next
146
+ end
147
+
148
+ break
149
+ end
150
+
151
+ if @timezone
152
+ time = @timezone.local_to_utc(time)
153
+ time = time.getlocal unless from.utc?
154
+ end
155
+
156
+ time
157
+ end
158
+
159
+ # Returns the previous the cronline matched. It's like next_time, but
160
+ # for the past.
161
+ #
162
+ def previous_time(from=Time.now)
163
+
164
+ # looks back by slices of two hours,
165
+ #
166
+ # finds for '* * * * sun', '* * 13 * *' and '0 12 13 * *'
167
+ # starting 1970, 1, 1 in 1.8 to 2 seconds (says Rspec)
168
+
169
+ start = current = from - 2 * 3600
170
+ result = nil
171
+
172
+ loop do
173
+ nex = next_time(current)
174
+ return (result ? result : previous_time(start)) if nex > from
175
+ result = current = nex
176
+ end
177
+
178
+ # never reached
179
+ end
180
+
181
+ # Returns an array of 6 arrays (seconds, minutes, hours, days,
182
+ # months, weekdays).
183
+ # This method is used by the cronline unit tests.
184
+ #
185
+ def to_array
186
+
187
+ [
188
+ @seconds,
189
+ @minutes,
190
+ @hours,
191
+ @days,
192
+ @months,
193
+ @weekdays,
194
+ @monthdays,
195
+ @timezone ? @timezone.name : nil
196
+ ]
197
+ end
198
+
199
+ # Returns the shortest delta between two potential occurences of the
200
+ # schedule described by this cronline.
201
+ #
202
+ def frequency
203
+
204
+ delta = 366 * DAY_S
205
+
206
+ t0 = previous_time(Time.local(2000, 1, 1))
207
+
208
+ loop do
209
+
210
+ break if delta <= 1
211
+ break if delta <= 60 && @seconds && @seconds.size == 1
212
+
213
+ t1 = next_time(t0)
214
+ d = t1 - t0
215
+ delta = d if d < delta
216
+
217
+ break if @months == nil && t1.month == 2
218
+ break if t1.year == 2001
219
+
220
+ t0 = t1
221
+ end
222
+
223
+ delta
224
+ end
225
+
226
+ protected
227
+
228
+ WEEKDAYS = %w[ sun mon tue wed thu fri sat ]
229
+ DAY_S = 24 * 3600
230
+ WEEK_S = 7 * DAY_S
231
+
232
+ def parse_weekdays(item)
233
+
234
+ return nil if item == '*'
235
+
236
+ items = item.downcase.split(',')
237
+
238
+ weekdays = nil
239
+ monthdays = nil
240
+
241
+ items.each do |it|
242
+
243
+ if m = it.match(/^(.+)#(l|-?[12345])$/)
244
+
245
+ raise ArgumentError.new(
246
+ "ranges are not supported for monthdays (#{it})"
247
+ ) if m[1].index('-')
248
+
249
+ expr = it.gsub(/#l/, '#-1')
250
+
251
+ (monthdays ||= []) << expr
252
+
253
+ else
254
+
255
+ expr = it.dup
256
+ WEEKDAYS.each_with_index { |a, i| expr.gsub!(/#{a}/, i.to_s) }
257
+
258
+ raise ArgumentError.new(
259
+ "invalid weekday expression (#{it})"
260
+ ) if expr !~ /^0*[0-7](-0*[0-7])?$/
261
+
262
+ its = expr.index('-') ? parse_range(expr, 0, 7) : [ Integer(expr) ]
263
+ its = its.collect { |i| i == 7 ? 0 : i }
264
+
265
+ (weekdays ||= []).concat(its)
266
+ end
267
+ end
268
+
269
+ weekdays = weekdays.uniq if weekdays
270
+
271
+ [ weekdays, monthdays ]
272
+ end
273
+
274
+ def parse_item(item, min, max)
275
+
276
+ return nil if item == '*'
277
+
278
+ r = item.split(',').map { |i| parse_range(i.strip, min, max) }.flatten
279
+
280
+ raise ArgumentError.new(
281
+ "found duplicates in #{item.inspect}"
282
+ ) if r.uniq.size < r.size
283
+
284
+ r
285
+ end
286
+
287
+ RANGE_REGEX = /^(\*|\d{1,2})(?:-(\d{1,2}))?(?:\/(\d{1,2}))?$/
288
+
289
+ def parse_range(item, min, max)
290
+
291
+ return %w[ L ] if item == 'L'
292
+
293
+ item = '*' + item if item.match(/^\//)
294
+
295
+ m = item.match(RANGE_REGEX)
296
+
297
+ raise ArgumentError.new(
298
+ "cannot parse #{item.inspect}"
299
+ ) unless m
300
+
301
+ sta = m[1]
302
+ sta = sta == '*' ? min : sta.to_i
303
+
304
+ edn = m[2]
305
+ edn = edn ? edn.to_i : sta
306
+ edn = max if m[1] == '*'
307
+
308
+ inc = m[3]
309
+ inc = inc ? inc.to_i : 1
310
+
311
+ raise ArgumentError.new(
312
+ "#{item.inspect} is not in range #{min}..#{max}"
313
+ ) if sta < min || edn > max
314
+
315
+ r = []
316
+ val = sta
317
+
318
+ loop do
319
+ v = val
320
+ v = 0 if max == 24 && v == 24
321
+ r << v
322
+ break if inc == 1 && val == edn
323
+ val += inc
324
+ break if inc > 1 && val > edn
325
+ val = min if val > max
326
+ end
327
+
328
+ r.uniq
329
+ end
330
+
331
+ def sub_match?(time, accessor, values)
332
+
333
+ value = time.send(accessor)
334
+
335
+ return true if values.nil?
336
+ return true if values.include?('L') && (time + DAY_S).day == 1
337
+
338
+ return true if value == 0 && accessor == :hour && values.include?(24)
339
+
340
+ values.include?(value)
341
+ end
342
+
343
+ def monthday_match?(date, values)
344
+
345
+ return true if values.nil?
346
+
347
+ today_values = monthdays(date)
348
+
349
+ (today_values & values).any?
350
+ end
351
+
352
+ def date_match?(date)
353
+
354
+ return false unless sub_match?(date, :day, @days)
355
+ return false unless sub_match?(date, :month, @months)
356
+ return false unless sub_match?(date, :wday, @weekdays)
357
+ return false unless monthday_match?(date, @monthdays)
358
+ true
359
+ end
360
+
361
+ def monthdays(date)
362
+
363
+ pos = 1
364
+ d = date.dup
365
+
366
+ loop do
367
+ d = d - WEEK_S
368
+ break if d.month != date.month
369
+ pos = pos + 1
370
+ end
371
+
372
+ neg = -1
373
+ d = date.dup
374
+
375
+ loop do
376
+ d = d + WEEK_S
377
+ break if d.month != date.month
378
+ neg = neg - 1
379
+ end
380
+
381
+ [ "#{WEEKDAYS[date.wday]}##{pos}", "#{WEEKDAYS[date.wday]}##{neg}" ]
382
+ end
383
+ end
384
+ end
385
+ end
386
+
@@ -0,0 +1,275 @@
1
+ #--
2
+ # Copyright (c) 2006-2013, John Mettraux, jmettraux@gmail.com
3
+ #
4
+ # Permission is hereby granted, free of charge, to any person obtaining a copy
5
+ # of this software and associated documentation files (the "Software"), to deal
6
+ # in the Software without restriction, including without limitation the rights
7
+ # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
8
+ # copies of the Software, and to permit persons to whom the Software is
9
+ # furnished to do so, subject to the following conditions:
10
+ #
11
+ # The above copyright notice and this permission notice shall be included in
12
+ # all copies or substantial portions of the Software.
13
+ #
14
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15
+ # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
16
+ # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
17
+ # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
18
+ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
19
+ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
20
+ # THE SOFTWARE.
21
+ #
22
+ # Made in Japan.
23
+ #++
24
+
25
+
26
+ module UvRays
27
+ class Scheduler
28
+
29
+ def self.parse_in(o, quiet = false)
30
+ # if o is an integer we are looking at seconds
31
+ o.is_a?(String) ? parse_duration(o, quiet) : (o * 1000)
32
+ end
33
+
34
+ TZ_REGEX = /\b((?:[a-zA-Z][a-zA-z0-9\-+]+)(?:\/[a-zA-Z0-9\-+]+)?)\b/
35
+
36
+ def self.parse_at(o, quiet = false)
37
+ return (o.to_f * 1000).to_i if o.is_a?(Time)
38
+
39
+ tz = nil
40
+ s = o.to_s.gsub(TZ_REGEX) { |m|
41
+ t = TZInfo::Timezone.get(m) rescue nil
42
+ tz ||= t
43
+ t ? '' : m
44
+ }
45
+
46
+ begin
47
+ DateTime.parse(o)
48
+ rescue
49
+ raise ArgumentError, "no time information in #{o.inspect}"
50
+ end if RUBY_VERSION < '1.9.0'
51
+
52
+ t = Time.parse(s)
53
+ t = tz.local_to_utc(t) if tz
54
+ (t.to_f * 1000).to_i # Convert to milliseconds
55
+
56
+ rescue StandardError => se
57
+ return nil if quiet
58
+ raise se
59
+ end
60
+
61
+ def self.parse_cron(o, quiet)
62
+ CronLine.new(o)
63
+
64
+ rescue ArgumentError => ae
65
+ return nil if quiet
66
+ raise ae
67
+ end
68
+
69
+ def self.parse_to_time(o)
70
+ t = o
71
+ t = parse(t) if t.is_a?(String)
72
+ t = Time.now + t if t.is_a?(Numeric)
73
+
74
+ raise ArgumentError.new(
75
+ "cannot turn #{o.inspect} to a point in time, doesn't make sense"
76
+ ) unless t.is_a?(Time)
77
+
78
+ t
79
+ end
80
+
81
+ DURATIONS2M = [
82
+ [ 'y', 365 * 24 * 3600 * 1000 ],
83
+ [ 'M', 30 * 24 * 3600 * 1000 ],
84
+ [ 'w', 7 * 24 * 3600 * 1000 ],
85
+ [ 'd', 24 * 3600 * 1000 ],
86
+ [ 'h', 3600 * 1000 ],
87
+ [ 'm', 60 * 1000 ],
88
+ [ 's', 1000 ]
89
+ ]
90
+ DURATIONS2 = DURATIONS2M.dup
91
+ DURATIONS2.delete_at(1)
92
+
93
+ DURATIONS = DURATIONS2M.inject({}) { |r, (k, v)| r[k] = v; r }
94
+ DURATION_LETTERS = DURATIONS.keys.join
95
+
96
+ DU_KEYS = DURATIONS2M.collect { |k, v| k.to_sym }
97
+
98
+ # Turns a string like '1m10s' into a float like '70.0', more formally,
99
+ # turns a time duration expressed as a string into a Float instance
100
+ # (millisecond count).
101
+ #
102
+ # w -> week
103
+ # d -> day
104
+ # h -> hour
105
+ # m -> minute
106
+ # s -> second
107
+ # M -> month
108
+ # y -> year
109
+ # 'nada' -> millisecond
110
+ #
111
+ # Some examples:
112
+ #
113
+ # Rufus::Scheduler.parse_duration "0.5" # => 0.5
114
+ # Rufus::Scheduler.parse_duration "500" # => 0.5
115
+ # Rufus::Scheduler.parse_duration "1000" # => 1.0
116
+ # Rufus::Scheduler.parse_duration "1h" # => 3600.0
117
+ # Rufus::Scheduler.parse_duration "1h10s" # => 3610.0
118
+ # Rufus::Scheduler.parse_duration "1w2d" # => 777600.0
119
+ #
120
+ # Negative time strings are OK (Thanks Danny Fullerton):
121
+ #
122
+ # Rufus::Scheduler.parse_duration "-0.5" # => -0.5
123
+ # Rufus::Scheduler.parse_duration "-1h" # => -3600.0
124
+ #
125
+ def self.parse_duration(string, quiet = false)
126
+ string = string.to_s
127
+
128
+ return 0 if string == ''
129
+
130
+ m = string.match(/^(-?)([\d\.#{DURATION_LETTERS}]+)$/)
131
+
132
+ return nil if m.nil? && quiet
133
+ raise ArgumentError.new("cannot parse '#{string}'") if m.nil?
134
+
135
+ mod = m[1] == '-' ? -1.0 : 1.0
136
+ val = 0.0
137
+
138
+ s = m[2]
139
+
140
+ while s.length > 0
141
+ m = nil
142
+ if m = s.match(/^(\d+|\d+\.\d*|\d*\.\d+)([#{DURATION_LETTERS}])(.*)$/)
143
+ val += m[1].to_f * DURATIONS[m[2]]
144
+ elsif s.match(/^\d+$/)
145
+ val += s.to_i
146
+ elsif s.match(/^\d*\.\d*$/)
147
+ val += s.to_f
148
+ elsif quiet
149
+ return nil
150
+ else
151
+ raise ArgumentError.new(
152
+ "cannot parse '#{string}' (unexpected '#{s}')"
153
+ )
154
+ end
155
+ break unless m && m[3]
156
+ s = m[3]
157
+ end
158
+
159
+ res = mod * val
160
+ res.to_i
161
+ end
162
+
163
+
164
+ # Turns a number of seconds into a a time string
165
+ #
166
+ # Rufus.to_duration 0 # => '0s'
167
+ # Rufus.to_duration 60 # => '1m'
168
+ # Rufus.to_duration 3661 # => '1h1m1s'
169
+ # Rufus.to_duration 7 * 24 * 3600 # => '1w'
170
+ # Rufus.to_duration 30 * 24 * 3600 + 1 # => "4w2d1s"
171
+ #
172
+ # It goes from seconds to the year. Months are not counted (as they
173
+ # are of variable length). Weeks are counted.
174
+ #
175
+ # For 30 days months to be counted, the second parameter of this
176
+ # method can be set to true.
177
+ #
178
+ # Rufus.to_duration 30 * 24 * 3600 + 1, true # => "1M1s"
179
+ #
180
+ # If a Float value is passed, milliseconds will be displayed without
181
+ # 'marker'
182
+ #
183
+ # Rufus.to_duration 0.051 # => "51"
184
+ # Rufus.to_duration 7.051 # => "7s51"
185
+ # Rufus.to_duration 0.120 + 30 * 24 * 3600 + 1 # => "4w2d1s120"
186
+ #
187
+ # (this behaviour mirrors the one found for parse_time_string()).
188
+ #
189
+ # Options are :
190
+ #
191
+ # * :months, if set to true, months (M) of 30 days will be taken into
192
+ # account when building up the result
193
+ # * :drop_seconds, if set to true, seconds and milliseconds will be trimmed
194
+ # from the result
195
+ #
196
+ def self.to_duration(seconds, options = {})
197
+ h = to_duration_hash(seconds, options)
198
+
199
+ return (options[:drop_seconds] ? '0m' : '0s') if h.empty?
200
+
201
+ s = DU_KEYS.inject('') { |r, key|
202
+ count = h[key]
203
+ count = nil if count == 0
204
+ r << "#{count}#{key}" if count
205
+ r
206
+ }
207
+
208
+ ms = h[:ms]
209
+ s << ms.to_s if ms
210
+ s
211
+ end
212
+
213
+ # Turns a number of seconds (integer or Float) into a hash like in :
214
+ #
215
+ # Rufus.to_duration_hash 0.051
216
+ # # => { :ms => "51" }
217
+ # Rufus.to_duration_hash 7.051
218
+ # # => { :s => 7, :ms => "51" }
219
+ # Rufus.to_duration_hash 0.120 + 30 * 24 * 3600 + 1
220
+ # # => { :w => 4, :d => 2, :s => 1, :ms => "120" }
221
+ #
222
+ # This method is used by to_duration behind the scenes.
223
+ #
224
+ # Options are :
225
+ #
226
+ # * :months, if set to true, months (M) of 30 days will be taken into
227
+ # account when building up the result
228
+ # * :drop_seconds, if set to true, seconds and milliseconds will be trimmed
229
+ # from the result
230
+ #
231
+ def self.to_duration_hash(seconds, options = {})
232
+ h = {}
233
+
234
+ if (seconds % 1000) > 0
235
+ h[:ms] = (seconds % 1000).to_i
236
+ seconds = (seconds / 1000).to_i * 1000
237
+ end
238
+
239
+ if options[:drop_seconds]
240
+ h.delete(:ms)
241
+ seconds = (seconds - seconds % 60000)
242
+ end
243
+
244
+ durations = options[:months] ? DURATIONS2M : DURATIONS2
245
+
246
+ durations.each do |key, duration|
247
+ count = seconds / duration
248
+ seconds = seconds % duration
249
+
250
+ h[key.to_sym] = count if count > 0
251
+ end
252
+
253
+ h
254
+ end
255
+
256
+ #--
257
+ # misc
258
+ #++
259
+
260
+ # Produces the UTC string representation of a Time instance
261
+ #
262
+ # like "2009/11/23 11:11:50.947109 UTC"
263
+ #
264
+ def self.utc_to_s(t=Time.now)
265
+ "#{t.utc.strftime('%Y-%m-%d %H:%M:%S')}.#{sprintf('%06d', t.usec)} UTC"
266
+ end
267
+
268
+ # Produces a hour/min/sec/milli string representation of Time instance
269
+ #
270
+ def self.h_to_s(t=Time.now)
271
+ "#{t.strftime('%H:%M:%S')}.#{sprintf('%06d', t.usec)}"
272
+ end
273
+ end
274
+ end
275
+