rufus-scheduler 3.2.0 → 3.3.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.
@@ -1,5 +1,5 @@
1
1
  #--
2
- # Copyright (c) 2006-2015, John Mettraux, jmettraux@gmail.com
2
+ # Copyright (c) 2006-2016, John Mettraux, jmettraux@gmail.com
3
3
  #
4
4
  # Permission is hereby granted, free of charge, to any person obtaining a copy
5
5
  # of this software and associated documentation files (the "Software"), to deal
@@ -38,27 +38,27 @@ module Rufus
38
38
  parse_cron(o, opts) ||
39
39
  parse_in(o, opts) || # covers 'every' schedule strings
40
40
  parse_at(o, opts) ||
41
- fail(ArgumentError.new("couldn't parse \"#{o}\""))
41
+ fail(ArgumentError.new("couldn't parse #{o.inspect} (#{o.class})"))
42
42
  end
43
43
 
44
- def self.parse_in(o, opts={})
45
-
46
- o.is_a?(String) ? parse_duration(o, opts) : o
47
- end
48
-
49
- def self.parse_at(o, opts={})
44
+ def self.parse_cron(o, opts)
50
45
 
51
- Rufus::Scheduler::ZoTime.parse(o, opts).time
46
+ CronLine.new(o)
52
47
 
53
- rescue StandardError => se
48
+ rescue ArgumentError => ae
54
49
 
55
50
  return nil if opts[:no_error]
56
- fail se
51
+ fail ae
57
52
  end
58
53
 
59
- def self.parse_cron(o, opts)
54
+ def self.parse_in(o, opts={})
60
55
 
61
- CronLine.new(o)
56
+ #o.is_a?(String) ? parse_duration(o, opts) : o
57
+
58
+ return parse_duration(o, opts) if o.is_a?(String)
59
+ return o if o.is_a?(Numeric)
60
+
61
+ fail ArgumentError.new("couldn't parse time point in #{o.inspect}")
62
62
 
63
63
  rescue ArgumentError => ae
64
64
 
@@ -66,17 +66,16 @@ module Rufus
66
66
  fail ae
67
67
  end
68
68
 
69
- def self.parse_to_time(o)
69
+ def self.parse_at(o, opts={})
70
70
 
71
- t = o
72
- t = parse(t) if t.is_a?(String)
73
- t = Time.now + t if t.is_a?(Numeric)
71
+ return o if o.is_a?(Rufus::Scheduler::ZoTime)
72
+ return Rufus::Scheduler::ZoTime.make(o) if o.is_a?(Time)
73
+ Rufus::Scheduler::ZoTime.parse(o, opts)
74
74
 
75
- fail ArgumentError.new(
76
- "cannot turn #{o.inspect} to a point in time, doesn't make sense"
77
- ) unless t.is_a?(Time)
75
+ rescue StandardError => se
78
76
 
79
- t
77
+ return nil if opts[:no_error]
78
+ fail se
80
79
  end
81
80
 
82
81
  DURATIONS2M = [
@@ -125,38 +124,24 @@ module Rufus
125
124
  #
126
125
  def self.parse_duration(string, opts={})
127
126
 
128
- string = string.to_s
129
-
130
- return 0.0 if string == ''
127
+ s = string.to_s.strip
128
+ mod = s[0, 1] == '-' ? -1 : 1
129
+ s = s[1..-1] if mod == -1
131
130
 
132
- m = string.match(/^(-?)([\d\.#{DURATION_LETTERS}]+)$/)
131
+ ss = mod < 0 ? '-' : ''
132
+ r = 0.0
133
133
 
134
- return nil if m.nil? && opts[:no_error]
135
- fail ArgumentError.new("cannot parse '#{string}'") if m.nil?
136
-
137
- mod = m[1] == '-' ? -1.0 : 1.0
138
- val = 0.0
139
-
140
- s = m[2]
134
+ s.scan(/(\d*\.\d+|\d+\.?)([#{DURATION_LETTERS}]?)/) do |f, d|
135
+ ss += "#{f}#{d}"
136
+ r += f.to_f * (DURATIONS[d] || 1.0)
137
+ end
141
138
 
142
- while s.length > 0
143
- m = nil
144
- if m = s.match(/^(\d+|\d+\.\d*|\d*\.\d+)([#{DURATION_LETTERS}])(.*)$/)
145
- val += m[1].to_f * DURATIONS[m[2]]
146
- elsif s.match(/^\d+$/)
147
- val += s.to_i
148
- elsif s.match(/^\d*\.\d*$/)
149
- val += s.to_f
150
- elsif opts[:no_error]
151
- return nil
152
- else
153
- fail ArgumentError.new("cannot parse '#{string}' (especially '#{s}')")
154
- end
155
- break unless m && m[3]
156
- s = m[3]
139
+ if ss == '-' || ss != string.to_s.strip
140
+ return nil if opts[:no_error]
141
+ fail ArgumentError.new("invalid time duration #{string.inspect}")
157
142
  end
158
143
 
159
- mod * val
144
+ mod * r
160
145
  end
161
146
 
162
147
  class << self
@@ -1,5 +1,5 @@
1
1
  #--
2
- # Copyright (c) 2006-2015, John Mettraux, jmettraux@gmail.com
2
+ # Copyright (c) 2006-2016, John Mettraux, jmettraux@gmail.com
3
3
  #
4
4
  # Permission is hereby granted, free of charge, to any person obtaining a copy
5
5
  # of this software and associated documentation files (the "Software"), to deal
@@ -22,8 +22,6 @@
22
22
  # Made in Japan.
23
23
  #++
24
24
 
25
- require 'rufus/scheduler/zones'
26
-
27
25
 
28
26
  class Rufus::Scheduler
29
27
 
@@ -32,52 +30,72 @@ class Rufus::Scheduler
32
30
  #
33
31
  class ZoTime
34
32
 
35
- attr_accessor :seconds
36
- attr_accessor :zone
33
+ attr_reader :seconds
34
+ attr_reader :zone
37
35
 
38
36
  def initialize(s, zone)
39
37
 
40
38
  @seconds = s.to_f
41
- @zone = zone
42
- end
39
+ @zone = self.class.get_tzone(zone || :current)
43
40
 
44
- def time
41
+ fail ArgumentError.new(
42
+ "cannot determine timezone from #{zone.inspect}"
43
+ ) unless @zone
45
44
 
46
- in_zone do
45
+ @time = nil # cache for #to_time result
46
+ end
47
47
 
48
- t = Time.at(@seconds)
48
+ def seconds=(f)
49
49
 
50
- #if t.isdst
51
- # t1 = Time.at(@seconds + 3600)
52
- # t = t1 if t.zone != t1.zone && t.hour == t1.hour && t.min == t1.min
53
- # # ambiguous TZ (getting out of DST)
54
- #else
55
- # t.hour # force t to compute itself
56
- #end
57
- #
58
- # jump out of DST as soon as possible, jumps 1h as seen from UTC
50
+ @time = nil
51
+ @seconds = f
52
+ end
59
53
 
60
- t.hour # force t to compute itself
61
- #
62
- # stay in DST as long as possible, no jump seen from UTC
54
+ def zone=(z)
63
55
 
64
- t
65
- end
56
+ @time = nil
57
+ @zone = self.class.get_tzone(zone || :current)
66
58
  end
67
59
 
68
60
  def utc
69
61
 
70
- time.utc
62
+ Time.utc(1970, 1, 1) + @seconds
63
+ end
64
+
65
+ # Returns a Ruby Time instance.
66
+ #
67
+ # Warning: the timezone of that Time instance will be UTC.
68
+ #
69
+ def to_time
70
+
71
+ @time ||= begin; u = utc; @zone.period_for_utc(u).to_local(u); end
72
+ end
73
+
74
+ %w[
75
+ year month day wday hour min sec usec asctime
76
+ ].each do |m|
77
+ define_method(m) { to_time.send(m) }
71
78
  end
79
+ def iso8601(fraction_digits=0); to_time.iso8601(fraction_digits); end
72
80
 
73
- def add(s)
81
+ def ==(o)
74
82
 
75
- @seconds += s.to_f
83
+ o.is_a?(ZoTime) && o.seconds == @seconds && o.zone == @zone
76
84
  end
85
+ #alias eq? == # FIXME see Object#== (ri)
86
+
87
+ def >(o); @seconds > _to_f(o); end
88
+ def >=(o); @seconds >= _to_f(o); end
89
+ def <(o); @seconds < _to_f(o); end
90
+ def <=(o); @seconds <= _to_f(o); end
91
+ def <=>(o); @seconds <=> _to_f(o); end
92
+
93
+ alias getutc utc
94
+ alias getgm utc
77
95
 
78
- def substract(s)
96
+ def to_i
79
97
 
80
- @seconds -= s.to_f
98
+ @seconds.to_i
81
99
  end
82
100
 
83
101
  def to_f
@@ -85,82 +103,346 @@ class Rufus::Scheduler
85
103
  @seconds
86
104
  end
87
105
 
88
- #DELTA_TZ_REX = /^[+-][0-1][0-9]:?[0-5][0-9]$/
106
+ def is_dst?
107
+
108
+ @zone.period_for_utc(utc).std_offset != 0
109
+ end
110
+ alias isdst is_dst?
111
+
112
+ def utc_offset
113
+
114
+ #@zone.period_for_utc(utc).utc_offset
115
+ #@zone.period_for_utc(utc).utc_total_offset
116
+ #@zone.period_for_utc(utc).std_offset
117
+ @zone.period_for_utc(utc).utc_offset
118
+ end
119
+
120
+ def strftime(format)
121
+
122
+ format = format.gsub(/%(\/?Z|:{0,2}z)/) { |f| strfz(f) }
123
+
124
+ to_time.strftime(format)
125
+ end
126
+
127
+ def add(t); @time = nil; @seconds += t.to_f; end
128
+ def substract(t); @time = nil; @seconds -= t.to_f; end
129
+
130
+ def +(t); inc(t, 1); end
131
+ def -(t); inc(t, -1); end
132
+
133
+ WEEK_S = 7 * 24 * 3600
134
+
135
+ def monthdays
136
+
137
+ date = to_time
138
+
139
+ pos = 1
140
+ d = self.dup
141
+
142
+ loop do
143
+ d.add(-WEEK_S)
144
+ break if d.month != date.month
145
+ pos = pos + 1
146
+ end
147
+
148
+ neg = -1
149
+ d = self.dup
150
+
151
+ loop do
152
+ d.add(WEEK_S)
153
+ break if d.month != date.month
154
+ neg = neg - 1
155
+ end
156
+
157
+ [ "#{date.wday}##{pos}", "#{date.wday}##{neg}" ]
158
+ end
159
+
160
+ def to_s
161
+
162
+ strftime('%Y-%m-%d %H:%M:%S %z')
163
+ end
164
+
165
+ def to_debug_s
166
+
167
+ uo = self.utc_offset
168
+ uos = uo < 0 ? '-' : '+'
169
+ uo = uo.abs
170
+ uoh, uom = [ uo / 3600, uo % 3600 ]
171
+
172
+ [
173
+ 'zt',
174
+ self.strftime('%Y-%m-%d %H:%M:%S'),
175
+ "%s%02d:%02d" % [ uos, uoh, uom ],
176
+ "dst:#{self.isdst}"
177
+ ].join(' ')
178
+ end
179
+
180
+ # Debug current time by showing local time / delta / utc time
181
+ # for example: "0120-7(0820)"
182
+ #
183
+ def to_utc_comparison_s
184
+
185
+ per = @zone.period_for_utc(utc)
186
+ off = per.utc_total_offset
187
+
188
+ off = off / 3600
189
+ off = off >= 0 ? "+#{off}" : off.to_s
190
+
191
+ strftime('%H%M') + off + utc.strftime('(%H%M)')
192
+ end
193
+
194
+ def self.now(zone=nil)
195
+
196
+ ZoTime.new(Time.now.to_f, zone)
197
+ end
198
+
199
+ # https://en.wikipedia.org/wiki/ISO_8601
200
+ # Postel's law applies
201
+ #
202
+ def self.extract_iso8601_zone(s)
203
+
204
+ m = s.match(
205
+ /[0-2]\d(?::?[0-6]\d(?::?[0-6]\d))?\s*([+-]\d\d(?::?\d\d)?)\s*\z/)
206
+ return nil unless m
207
+
208
+ zs = m[1].split(':')
209
+ zs << '00' if zs.length < 2
89
210
 
90
- def self.envtzable?(s)
211
+ zh = zs[0].to_i.abs
91
212
 
92
- TIMEZONES.include?(s)
213
+ return nil if zh > 24
214
+ return nil if zh == 24 && zs[1].to_i != 0
215
+
216
+ zs.join(':')
93
217
  end
94
218
 
95
219
  def self.parse(str, opts={})
96
220
 
97
221
  if defined?(::Chronic) && t = ::Chronic.parse(str, opts)
98
- return ZoTime.new(t, ENV['TZ'])
222
+ return ZoTime.new(t, nil)
99
223
  end
100
224
 
225
+ #rold = RUBY_VERSION < '1.9.0'
226
+ #rold = RUBY_VERSION < '2.0.0'
227
+
101
228
  begin
102
229
  DateTime.parse(str)
103
230
  rescue
104
- fail ArgumentError, "no time information in #{o.inspect}"
105
- end if RUBY_VERSION < '1.9.0'
231
+ fail ArgumentError, "no time information in #{str.inspect}"
232
+ end #if rold
233
+ #
234
+ # is necessary since Time.parse('xxx') in Ruby < 1.9 yields `now`
106
235
 
107
236
  zone = nil
108
237
 
109
238
  s =
110
- str.gsub(/\S+/) { |m|
111
- if envtzable?(m)
112
- zone ||= m
239
+ str.gsub(/\S+/) do |w|
240
+ if z = get_tzone(w)
241
+ zone ||= z
113
242
  ''
114
243
  else
115
- m
244
+ w
245
+ end
246
+ end
247
+
248
+ local = Time.parse(s)
249
+ izone = extract_iso8601_zone(s)
250
+
251
+ zone ||=
252
+ if s.match(/\dZ\b/)
253
+ get_tzone('Zulu')
254
+ #elsif rold && izone
255
+ elsif izone
256
+ get_tzone(izone)
257
+ elsif local.zone.nil? && izone
258
+ get_tzone(local.strftime('%:z'))
259
+ else
260
+ get_tzone(:local)
261
+ end
262
+
263
+ secs =
264
+ #if rold && izone
265
+ if izone
266
+ local.to_f
267
+ else
268
+ zone.period_for_local(local).to_utc(local).to_f
269
+ end
270
+
271
+ ZoTime.new(secs, zone)
272
+ end
273
+
274
+ def self.get_tzone(str)
275
+
276
+ return str if str.is_a?(::TZInfo::Timezone)
277
+
278
+ # discard quickly when it's certainly not a timezone
279
+
280
+ return nil if str == nil
281
+ return nil if str == '*'
282
+
283
+ # ok, it's a timezone then
284
+
285
+ str = Time.now.zone if str == :current || str == :local
286
+
287
+ # utc_offset
288
+
289
+ if str.is_a?(Numeric)
290
+ i = str.to_i
291
+ sn = i < 0 ? '-' : '+'; i = i.abs
292
+ hr = i / 3600; mn = i % 3600; sc = i % 60
293
+ str = (sc > 0 ? "%s%02d:%02d:%02d" : "%s%02d:%02d") % [ sn, hr, mn, sc ]
294
+ end
295
+
296
+ return nil if str.index('#')
297
+ # counters "sun#2", etc... On OSX would go all the way to true
298
+
299
+ # vanilla time zones
300
+
301
+ z = (::TZInfo::Timezone.get(str) rescue nil)
302
+ return z if z
303
+
304
+ # time zone abbreviations
305
+
306
+ if str.match(/\A[A-Z0-9-]{3,6}\z/)
307
+
308
+ twin = Time.utc(Time.now.year, 1, 1)
309
+ tsum = Time.utc(Time.now.year, 7, 1)
310
+
311
+ z =
312
+ ::TZInfo::Timezone.all.find do |tz|
313
+ tz.period_for_utc(twin).abbreviation.to_s == str ||
314
+ tz.period_for_utc(tsum).abbreviation.to_s == str
116
315
  end
117
- }
316
+ return z if z
317
+ end
318
+
319
+ # some time zone aliases
320
+
321
+ return ::TZInfo::Timezone.get('Zulu') if %w[ Z ].include?(str)
118
322
 
119
- return nil unless zone.nil? || is_timezone?(zone)
323
+ # custom timezones, no DST, just an offset, like "+08:00" or "-01:30"
324
+
325
+ tz = (@custom_tz_cache ||= {})[str]
326
+ return tz if tz
327
+
328
+ if m = str.match(/\A([+-][0-1][0-9]):?([0-5][0-9])\z/)
329
+
330
+ hr = m[1].to_i
331
+ mn = m[2].to_i
332
+
333
+ hr = nil if hr.abs > 11
334
+ hr = nil if mn > 59
335
+ mn = -mn if hr && hr < 0
336
+
337
+ return (
338
+ @custom_tz_cache[str] =
339
+ begin
340
+ tzi = TZInfo::TransitionDataTimezoneInfo.new(str)
341
+ tzi.offset(str, hr * 3600 + mn * 60, 0, str)
342
+ tzi.create_timezone
343
+ end
344
+ ) if hr
345
+ end
120
346
 
121
- zt = ZoTime.new(0, zone || ENV['TZ'])
122
- zt.in_zone { zt.seconds = Time.parse(s).to_f }
347
+ # so it's not a timezone.
123
348
 
124
- zt.seconds == nil ? nil : zt
349
+ nil
125
350
  end
126
351
 
127
- def self.is_timezone?(str)
352
+ def self.local_tzone
128
353
 
129
- return false if str == nil
130
- return false if str == '*'
354
+ get_tzone(:local)
355
+ end
131
356
 
132
- return false if str.index('#')
133
- # "sun#2", etc... On OSX would go all the way to true
357
+ def self.make(o)
358
+
359
+ zt =
360
+ case o
361
+ when Time
362
+ ZoTime.new(o.to_f, o.zone)
363
+ when Date
364
+ t =
365
+ o.respond_to?(:to_time) ?
366
+ o.to_time :
367
+ Time.parse(o.strftime('%Y-%m-%d %H:%M:%S'))
368
+ ZoTime.new(t.to_f, t.zone)
369
+ when String
370
+ Rufus::Scheduler.parse_in(o, :no_error => true) || self.parse(o)
371
+ else
372
+ o
373
+ end
134
374
 
135
- return true if Time.zone_offset(str)
375
+ zt = ZoTime.new(Time.now.to_f + zt, nil) if zt.is_a?(Numeric)
136
376
 
137
- return !! (::TZInfo::Timezone.get(str) rescue nil) if defined?(::TZInfo)
377
+ fail ArgumentError.new(
378
+ "cannot turn #{o.inspect} to a ZoTime instance"
379
+ ) unless zt.is_a?(ZoTime)
138
380
 
139
- return true if TIMEZONES.include?(str)
140
- return true if TIMEZONEs.include?(str)
381
+ zt
382
+ end
141
383
 
142
- t = ZoTime.new(0, str).time
384
+ # def in_zone(&block)
385
+ #
386
+ # current_timezone = ENV['TZ']
387
+ # ENV['TZ'] = @zone
388
+ #
389
+ # block.call
390
+ #
391
+ # ensure
392
+ #
393
+ # ENV['TZ'] = current_timezone
394
+ # end
395
+
396
+ protected
397
+
398
+ def inc(t, dir)
399
+
400
+ if t.is_a?(Numeric)
401
+ nt = self.dup
402
+ nt.seconds += dir * t.to_f
403
+ nt
404
+ elsif t.respond_to?(:to_f)
405
+ @seconds + dir * t.to_f
406
+ else
407
+ fail ArgumentError.new(
408
+ "cannot call ZoTime #- or #+ with arg of class #{t.class}")
409
+ end
410
+ end
143
411
 
144
- return false if t.zone == ''
145
- return false if t.zone == 'UTC'
146
- return false if t.utc_offset == 0 && str.start_with?(t.zone)
147
- # 3 common fallbacks...
412
+ def _to_f(o)
148
413
 
149
- return false if RUBY_PLATFORM.include?('java') && ! envtzable?(str)
414
+ fail ArgumentError(
415
+ "comparison of ZoTime with #{o.inspect} failed"
416
+ ) unless o.is_a?(ZoTime) || o.is_a?(Time)
150
417
 
151
- true
418
+ o.to_f
152
419
  end
153
420
 
154
- def in_zone(&block)
421
+ def strfz(code)
422
+
423
+ return @zone.name if code == '%/Z'
424
+
425
+ per = @zone.period_for_utc(utc)
155
426
 
156
- current_timezone = ENV['TZ']
157
- ENV['TZ'] = @zone
427
+ return per.abbreviation.to_s if code == '%Z'
158
428
 
159
- block.call
429
+ off = per.utc_total_offset
430
+ #
431
+ sn = off < 0 ? '-' : '+'; off = off.abs
432
+ hr = off / 3600
433
+ mn = (off % 3600) / 60
434
+ sc = 0
160
435
 
161
- ensure
436
+ fmt =
437
+ if code == '%z'
438
+ "%s%02d%02d"
439
+ elsif code == '%:z'
440
+ "%s%02d:%02d"
441
+ else
442
+ "%s%02d:%02d:%02d"
443
+ end
162
444
 
163
- ENV['TZ'] = current_timezone
445
+ fmt % [ sn, hr, mn, sc ]
164
446
  end
165
447
  end
166
448
  end