rufus-scheduler 3.2.2 → 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.
@@ -86,7 +86,7 @@ module Rufus
86
86
  nil
87
87
  end
88
88
 
89
- @scheduled_at = Time.now
89
+ @scheduled_at = Rufus::Scheduler::ZoTime.now
90
90
  @unscheduled_at = nil
91
91
  @last_time = nil
92
92
 
@@ -124,27 +124,22 @@ module Rufus
124
124
  @previous_time = @next_time
125
125
  set_next_time(time)
126
126
 
127
- return if (
128
- opts[:overlap] == false &&
129
- running?
130
- )
131
- return if (
132
- callback(:confirm_lock, time) &&
133
- callback(:on_pre_trigger, time)
134
- ) == false
127
+ do_trigger(time)
128
+ end
135
129
 
136
- @count += 1
130
+ # Trigger the job right now, off of its schedule.
131
+ #
132
+ # Done in collaboration with Piavka in
133
+ # https://github.com/jmettraux/rufus-scheduler/issues/214
134
+ #
135
+ def trigger_off_schedule(time=Rufus::Scheduler::ZoTime.now)
137
136
 
138
- if opts[:blocking]
139
- do_trigger(time)
140
- else
141
- do_trigger_in_thread(time)
142
- end
137
+ do_trigger(time)
143
138
  end
144
139
 
145
140
  def unschedule
146
141
 
147
- @unscheduled_at = Time.now
142
+ @unscheduled_at = Rufus::Scheduler::ZoTime.now
148
143
  end
149
144
 
150
145
  def threads
@@ -204,7 +199,7 @@ module Rufus
204
199
  #
205
200
  def call(do_rescue=false)
206
201
 
207
- do_call(Time.now, do_rescue)
202
+ do_call(Rufus::Scheduler::ZoTime.now, do_rescue)
208
203
  end
209
204
 
210
205
  protected
@@ -251,7 +246,27 @@ module Rufus
251
246
 
252
247
  def do_trigger(time)
253
248
 
254
- t = Time.now
249
+ return if (
250
+ opts[:overlap] == false &&
251
+ running?
252
+ )
253
+ return if (
254
+ callback(:confirm_lock, time) &&
255
+ callback(:on_pre_trigger, time)
256
+ ) == false
257
+
258
+ @count += 1
259
+
260
+ if opts[:blocking]
261
+ trigger_now(time)
262
+ else
263
+ trigger_queue(time)
264
+ end
265
+ end
266
+
267
+ def trigger_now(time)
268
+
269
+ t = Rufus::Scheduler::ZoTime.now
255
270
  # if there are mutexes, t might be really bigger than time
256
271
 
257
272
  Thread.current[:rufus_scheduler_job] = self
@@ -265,7 +280,7 @@ module Rufus
265
280
  ensure
266
281
 
267
282
  @last_work_time =
268
- Time.now - Thread.current[:rufus_scheduler_time]
283
+ Rufus::Scheduler::ZoTime.now - Thread.current[:rufus_scheduler_time]
269
284
  @mean_work_time =
270
285
  ((@count - 1) * @mean_work_time + @last_work_time) / @count
271
286
 
@@ -302,7 +317,7 @@ module Rufus
302
317
  begin
303
318
 
304
319
  (job.opts[:mutex] || []).reduce(
305
- lambda { job.do_trigger(time) }
320
+ lambda { job.trigger_now(time) }
306
321
  ) do |b, m|
307
322
  lambda { mutex(m).synchronize { b.call } }
308
323
  end.call
@@ -321,9 +336,11 @@ module Rufus
321
336
  # but since it has to be done as quickly as possible.
322
337
  # So, whoever is running first (scheduler thread vs job thread)
323
338
  # sets this information
339
+
340
+ thread
324
341
  end
325
342
 
326
- def do_trigger_in_thread(time)
343
+ def trigger_queue(time)
327
344
 
328
345
  threads = @scheduler.work_threads
329
346
 
@@ -423,12 +440,12 @@ module Rufus
423
440
 
424
441
  return (@first_at = nil) if first == nil
425
442
 
426
- n0 = Time.now
443
+ n0 = Rufus::Scheduler::ZoTime.now
427
444
  n1 = n0 + 0.003
428
445
 
429
446
  first = n0 if first == :now || first == :immediately || first == 0
430
447
 
431
- @first_at = Rufus::Scheduler.parse_to_time(first)
448
+ @first_at = ZoTime.make(first)
432
449
  @first_at = n1 if @first_at >= n0 && @first_at < n1
433
450
 
434
451
  fail ArgumentError.new(
@@ -441,12 +458,13 @@ module Rufus
441
458
 
442
459
  def last_at=(last)
443
460
 
444
- @last_at = last ? Rufus::Scheduler.parse_to_time(last) : nil
461
+ #@last_at = last ? Rufus::Scheduler.parse_to_time(last) : nil
462
+ @last_at = last ? ZoTime.make(last) : nil
445
463
 
446
464
  fail ArgumentError.new(
447
465
  "cannot set last[_at|_in] in the past: " +
448
466
  "#{last.inspect} -> #{@last_at.inspect}"
449
- ) if last && @last_at < Time.now
467
+ ) if last && @last_at < Rufus::Scheduler::ZoTime.now
450
468
 
451
469
  @last_at
452
470
  end
@@ -458,7 +476,7 @@ module Rufus
458
476
  return (@next_time = nil) if @times && @times < 1
459
477
  return (@next_time = nil) if @last_at && time >= @last_at
460
478
  #
461
- # TODO: rework that, jobs are thus kept 1 step too much in @jobs
479
+ # It keeps jobs one step too much in @jobs, but it's OK
462
480
 
463
481
  super
464
482
 
@@ -467,7 +485,7 @@ module Rufus
467
485
 
468
486
  def pause
469
487
 
470
- @paused_at = Time.now
488
+ @paused_at = Rufus::Scheduler::ZoTime.now
471
489
  end
472
490
 
473
491
  def resume
@@ -518,9 +536,7 @@ module Rufus
518
536
 
519
537
  def first_at=(first)
520
538
 
521
- super
522
-
523
- @next_time = @first_at
539
+ @next_time = super
524
540
  end
525
541
  end
526
542
 
@@ -548,10 +564,10 @@ module Rufus
548
564
 
549
565
  return if is_post
550
566
 
551
- n = Time.now
567
+ n = Rufus::Scheduler::ZoTime.now
552
568
 
553
569
  @next_time =
554
- if @first_at == nil || @first_at < n
570
+ if @first_at == nil || @first_at < (n - @scheduler.frequency)
555
571
  nt = (@next_time || trigger_time || n) + @frequency
556
572
  nt > n ? nt : (trigger_time || n) + @frequency
557
573
  else
@@ -589,10 +605,10 @@ module Rufus
589
605
 
590
606
  @next_time =
591
607
  if is_post
592
- Time.now + @interval
608
+ Rufus::Scheduler::ZoTime.now + @interval
593
609
  elsif trigger_time.nil?
594
610
  if @first_at == nil || @first_at < Time.now
595
- Time.now + @interval
611
+ Rufus::Scheduler::ZoTime.now + @interval
596
612
  else
597
613
  @first_at
598
614
  end
@@ -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 = [
@@ -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 = /\A[+-][0-1][0-9]:?[0-5][0-9]\z/
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