et-orbi 1.1.6 → 1.1.7

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 7a483e51ede4f1880d1d3ccafcbabcf418f34abf
4
- data.tar.gz: 537d66567277eba18947f3c175c244715ce9917f
3
+ metadata.gz: 63be401cf92b7f62cbb8c778d9ec512c89cd7fc1
4
+ data.tar.gz: 9e9668a7250fe51ead41650067e04e7c4a29d505
5
5
  SHA512:
6
- metadata.gz: 975490af3b9c016e8d8c3d30f19d4503ad6753fa7ef56493998f115aac78603db00f04790f04ec1d37bef2fe6132fa6290f3244b526a274eac95c512b7e83499
7
- data.tar.gz: ab06522781f0bdf7caeb533c1a499999c9b363ee2d7f55a28d38fcd36706ad007645e66c9393f99cf62c1acc33c6c652d53d408952713e703f723999827545cc
6
+ metadata.gz: ec5bf95f98d1a528e2411b39c7bea0a32154b6c11345cb37b0da89aabdcf2d110dccee14e045433961bf849e78b381b945279cdeb4575bcd4a4da4c3f888e1c0
7
+ data.tar.gz: 33028c93d34cd0129f09f4dee8213311588f56f87c3b69495c58dae0f4de23803919b406df8538878c9ce0bb4628cf211f1c0cd27bc7c0b4acac6fb33382fcb1
@@ -2,6 +2,13 @@
2
2
  # CHANGELOG.md
3
3
 
4
4
 
5
+ ## et-orbi 1.1.7 released 2019-01-14
6
+
7
+ - Rework Chronic integration, prevent conflict with ActiveSupport Time.zone
8
+ - Implement EtOrbi.extract_zone(s) (returns s1 and zone name)
9
+ - Adapt specs and EoTime#to_debug_s to Windows on Appveyor
10
+
11
+
5
12
  ## et-orbi 1.1.6 released 2018-09-05
6
13
 
7
14
  - Ensure Olson timezone name regex covers all timezone names
data/CREDITS.md CHANGED
@@ -1,6 +1,7 @@
1
1
 
2
2
  # et-orbi credits
3
3
 
4
+ * Wenhui Wang https://github.com/w11th .parse vs Chronic+ActiveSupport, fugit 11
4
5
  * Marcel https://github.com/MTRNord "Mitteleuropaeische Sommerzeit", gh-15
5
6
  * Stanisław Pitucha https://github.com/viraptor rubygems link to changelog, gh-14
6
7
  * Jamie Stackhouse https://github.com/itsjamie reported warnings, gh-13
@@ -1,5 +1,5 @@
1
1
 
2
- Copyright (c) 2017-2018, John Mettraux, jmettraux+flor@gmail.com
2
+ Copyright (c) 2017-2019, John Mettraux, jmettraux+flor@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
data/README.md CHANGED
@@ -70,7 +70,7 @@ EtOrbi.platform_info
70
70
 
71
71
  ### Rails?
72
72
 
73
- If Rails is present, `Time.zone` is provided and EtOrbi will use it.
73
+ If Rails is present, `Time.zone` is provided and EtOrbi will use it, unless `ENV['TZ']` is set to a valid timezone name. Setting `ENV['TZ']` to nil can give back precedence to `Time.zone`.
74
74
 
75
75
  Rails sets its timezone under `config/application.rb`.
76
76
 
@@ -38,9 +38,18 @@ Time zones for fugit and rufus-scheduler. Urbi et Orbi.
38
38
  ]
39
39
 
40
40
  s.add_runtime_dependency 'tzinfo'
41
+ #
42
+ # YES, open dependency, fill an issue at
43
+ # https://github.com/floraison/et-orbi/issues
44
+ # if you experience a conflict between et-orbi and tzinfo.
45
+ #
46
+ # DO NOT raise an issue at tzinfo
47
+ # this open dependency is my (@jmettraux) responsibility.
48
+
41
49
  #s.add_runtime_dependency 'raabro', '>= 1.1.3'
42
50
 
43
51
  s.add_development_dependency 'rspec', '~> 3.4'
52
+ s.add_development_dependency 'chronic', '~> 0.10'
44
53
 
45
54
  s.require_path = 'lib'
46
55
  end
@@ -4,12 +4,13 @@ require 'time'
4
4
 
5
5
  require 'tzinfo'
6
6
 
7
+ require 'et-orbi/eo_time'
7
8
  require 'et-orbi/zone_aliases'
8
9
 
9
10
 
10
11
  module EtOrbi
11
12
 
12
- VERSION = '1.1.6'
13
+ VERSION = '1.1.7'
13
14
 
14
15
  #
15
16
  # module methods
@@ -23,63 +24,40 @@ module EtOrbi
23
24
 
24
25
  def parse(str, opts={})
25
26
 
27
+ str, str_zone = extract_zone(str)
28
+
26
29
  if defined?(::Chronic) && t = ::Chronic.parse(str, opts)
27
- return EoTime.new(t, nil)
30
+
31
+ str = [ t.strftime('%F %T'), str_zone ].compact.join(' ')
28
32
  end
29
33
 
30
- #rold = RUBY_VERSION < '1.9.0'
31
- #rold = RUBY_VERSION < '2.0.0'
32
34
  begin
33
35
  DateTime.parse(str)
34
36
  rescue
35
37
  fail ArgumentError, "No time information in #{str.inspect}"
36
- end #if rold
38
+ end
39
+ #end if RUBY_VERSION < '1.9.0'
40
+ #end if RUBY_VERSION < '2.0.0'
37
41
  #
38
42
  # is necessary since Time.parse('xxx') in Ruby < 1.9 yields `now`
39
43
 
40
- str_zone = get_tzone(list_iso8601_zones(str).last)
41
- #p [ :parse, str, str_zone ]
42
- #p ENV['TZ']
43
-
44
- #p [ :parse, :oz, opts[:zone] ]
45
- #p [ :parse, :sz, str_zone ]
46
- #p [ :parse, :foz, find_olson_zone(str) ]
47
- #p [ :parse, :ltz, local_tzone ]
48
44
  zone =
49
45
  opts[:zone] ||
50
- str_zone ||
51
- find_olson_zone(str) ||
46
+ get_tzone(str_zone) ||
52
47
  determine_local_tzone
53
- #p [ :parse, :zone, zone ]
54
-
55
- str = str.sub(zone.name, '') unless zone.name.match(/\A[-+]/)
56
- #
57
- # for 'Sun Nov 18 16:01:00 Asia/Singapore 2012',
58
- # although where does rufus-scheduler have it from?
59
48
 
60
49
  local = Time.parse(str)
61
- #p [ :parse, :local, local, local.zone ]
62
-
63
- secs =
64
- if str_zone
65
- local.to_f
66
- else
67
- zone.local_to_utc(local).to_f
68
- end
69
- #p [ :parse, :secs, secs ]
50
+ secs = zone.local_to_utc(local).to_f
70
51
 
71
52
  EoTime.new(secs, zone)
72
53
  end
73
54
 
74
55
  def make_time(*a)
75
56
 
76
- #p [ :mt, a ]
77
57
  zone = a.length > 1 ? get_tzone(a.last) : nil
78
58
  a.pop if zone
79
- #p [ :mt, zone ]
80
59
 
81
60
  o = a.length > 1 ? a : a.first
82
- #p [ :mt, :o, o ]
83
61
 
84
62
  case o
85
63
  when Time then make_from_time(o, zone)
@@ -92,6 +70,7 @@ module EtOrbi
92
70
  "Cannot turn #{o.inspect} to a ::EtOrbi::EoTime instance")
93
71
  end
94
72
  end
73
+ alias make make_time
95
74
 
96
75
  def make_from_time(t, zone)
97
76
 
@@ -156,6 +135,7 @@ module EtOrbi
156
135
  s = unalias(o)
157
136
 
158
137
  get_offset_tzone(s) ||
138
+ get_x_offset_tzone(s) ||
159
139
  (::TZInfo::Timezone.get(s) rescue nil)
160
140
  end
161
141
 
@@ -174,6 +154,20 @@ module EtOrbi
174
154
  "(secs:#{seconds},utc~:#{ts.inspect},ltz~:#{z.inspect})"
175
155
  end
176
156
 
157
+ def tzinfo_version
158
+
159
+ #TZInfo::VERSION
160
+ Gem.loaded_specs['tzinfo'].version.to_s
161
+ rescue => err
162
+ err.inspect
163
+ end
164
+
165
+ def tzinfo_data_version
166
+
167
+ #TZInfo::Data::VERSION rescue nil
168
+ Gem.loaded_specs['tzinfo-data'].version.to_s rescue nil
169
+ end
170
+
177
171
  def platform_info
178
172
 
179
173
  etos = Proc.new { |k, v| "#{k}:#{v.inspect}" }
@@ -181,7 +175,8 @@ module EtOrbi
181
175
  h = {
182
176
  'etz' => ENV['TZ'],
183
177
  'tnz' => Time.now.zone,
184
- 'tzid' => defined?(TZInfo::Data),
178
+ 'tziv' => tzinfo_version,
179
+ 'tzidv' => tzinfo_data_version,
185
180
  'rv' => RUBY_VERSION,
186
181
  'rp' => RUBY_PLATFORM,
187
182
  'win' => Gem.win_platform?,
@@ -194,14 +189,13 @@ module EtOrbi
194
189
  if ltz = EtOrbi::EoTime.local_tzone
195
190
  h['eotnz'] = EtOrbi::EoTime.now.zone
196
191
  h['eotnfz'] = EtOrbi::EoTime.now.strftime('%z')
192
+ h['eotnfZ'] = EtOrbi::EoTime.now.strftime('%Z')
197
193
  h['eotlzn'] = ltz.name
198
194
  end
199
195
 
200
196
  "(#{h.map(&etos).join(',')},#{gather_tzs.map(&etos).join(',')})"
201
197
  end
202
198
 
203
- alias make make_time
204
-
205
199
  # For `make info`
206
200
  #
207
201
  def _make_info
@@ -210,497 +204,104 @@ module EtOrbi
210
204
  puts platform_info
211
205
  end
212
206
 
213
- protected
214
-
215
- def get_local_tzone(t)
216
-
217
- #lt = local_tzone
218
- #lp = lt.period_for_local(t)
219
- #ab = lp.abbreviation.to_s
220
- #
221
- #return lt \
222
- # if ab == t.zone
223
- #return lt \
224
- # if ab.match(/\A[-+]\d{2}(:?\d{2})?\z/) && lp.utc_offset == t.utc_offset
225
- #
226
- #nil
227
- #
228
- # keep that in the fridge for now
229
-
230
- l = Time.local(t.year, t.month, t.day, t.hour, t.min, t.sec, t.usec)
231
-
232
- (t.zone == l.zone) ? determine_local_tzone : nil
233
- end
234
-
235
- def get_as_tzone(t)
236
-
237
- t.respond_to?(:time_zone) ? t.time_zone : nil
238
- end
239
- end
240
-
241
- # Our EoTime class (which quacks like a ::Time).
242
- #
243
- # An EoTime instance should respond to most of the methods ::Time instances
244
- # respond to. If a method is missing, feel free to open an issue to
245
- # ask (politely) for it. If it makes sense, it'll get added, else
246
- # a workaround will get suggested.
247
- # The immediate workaround is to call #to_t on the EoTime instance to get
248
- # equivalent ::Time instance in the local, current, timezone.
249
- #
250
- class EoTime
251
-
252
- #
253
- # class methods
254
-
255
- class << self
256
-
257
- def now(zone=nil)
258
-
259
- EtOrbi.now(zone)
260
- end
261
-
262
- def parse(str, opts={})
263
-
264
- EtOrbi.parse(str, opts)
265
- end
266
-
267
- def get_tzone(o)
268
-
269
- EtOrbi.get_tzone(o)
270
- end
271
-
272
- def local_tzone
273
-
274
- EtOrbi.determine_local_tzone
275
- end
276
-
277
- def platform_info
278
-
279
- EtOrbi.platform_info
280
- end
281
-
282
- def make(o)
283
-
284
- EtOrbi.make_time(o)
285
- end
286
-
287
- def utc(*a)
288
-
289
- EtOrbi.make_from_array(a, EtOrbi.get_tzone('UTC'))
290
- end
291
-
292
- def local(*a)
293
-
294
- EtOrbi.make_from_array(a, local_tzone)
295
- end
296
- end
297
-
298
- #
299
- # instance methods
300
-
301
- attr_reader :seconds
302
- attr_reader :zone
303
-
304
- def initialize(s, zone)
305
-
306
- @seconds = s.to_f
307
- @zone = self.class.get_tzone(zone || :local)
308
-
309
- fail ArgumentError.new(
310
- "Cannot determine timezone from #{zone.inspect}" +
311
- "\n#{EtOrbi.render_nozone_time(@seconds)}" +
312
- "\n#{EtOrbi.platform_info.sub(',debian:', ",\ndebian:")}" +
313
- "\nTry setting `ENV['TZ'] = 'Continent/City'` in your script " +
314
- "(see https://en.wikipedia.org/wiki/List_of_tz_database_time_zones)" +
315
- (defined?(TZInfo::Data) ? '' : "\nand adding gem 'tzinfo-data'")
316
- ) unless @zone
317
-
318
- @time = nil
319
- # cache for #to_time result
320
- end
321
-
322
- def seconds=(f)
323
-
324
- @time = nil
325
- @seconds = f
326
- end
327
-
328
- def zone=(z)
329
-
330
- @time = nil
331
- @zone = self.class.get_tzone(zone || :current)
332
- end
333
-
334
- # Returns true if this EoTime instance corresponds to 2 different UTC
335
- # times.
336
- # It happens when transitioning from DST to winter time.
337
- #
338
- # https://www.timeanddate.com/time/change/usa/new-york?year=2018
339
- #
340
- def ambiguous?
341
-
342
- @zone.local_to_utc(@zone.utc_to_local(utc))
343
-
344
- false
345
-
346
- rescue TZInfo::AmbiguousTime
347
-
348
- true
349
- end
350
-
351
- # Returns this ::EtOrbi::EoTime as a ::Time instance
352
- # in the current UTC timezone.
353
- #
354
- def utc
355
-
356
- Time.utc(1970) + @seconds
357
- end
358
-
359
- # Returns true if this ::EtOrbi::EoTime instance timezone is UTC.
360
- # Returns false else.
361
- #
362
- def utc?
363
-
364
- %w[ gmt utc zulu etc/gmt etc/utc ].include?(
365
- @zone.canonical_identifier.downcase)
366
- end
367
-
368
- alias getutc utc
369
- alias getgm utc
370
- alias to_utc_time utc
371
-
372
- def to_f
373
-
374
- @seconds
375
- end
376
-
377
- def to_i
378
-
379
- @seconds.to_i
380
- end
381
-
382
- def strftime(format)
383
-
384
- format = format.gsub(/%(\/?Z|:{0,2}z)/) { |f| strfz(f) }
385
-
386
- to_time.strftime(format)
387
- end
388
-
389
- # Returns this ::EtOrbi::EoTime as a ::Time instance
390
- # in the current timezone.
391
- #
392
- # Has a #to_t alias.
393
- #
394
- def to_local_time
395
-
396
- Time.at(@seconds)
397
- end
398
-
399
- alias to_t to_local_time
400
-
401
- def is_dst?
402
-
403
- @zone.period_for_utc(utc).std_offset != 0
404
- end
405
- alias isdst is_dst?
406
-
407
- def to_debug_s
408
-
409
- uo = self.utc_offset
410
- uos = uo < 0 ? '-' : '+'
411
- uo = uo.abs
412
- uoh, uom = [ uo / 3600, uo % 3600 ]
413
-
414
- [
415
- 'ot',
416
- self.strftime('%Y-%m-%d %H:%M:%S'),
417
- "%s%02d:%02d" % [ uos, uoh, uom ],
418
- "dst:#{self.isdst}"
419
- ].join(' ')
420
- end
421
-
422
- def utc_offset
423
-
424
- @zone.period_for_utc(utc).utc_offset
425
- end
426
-
427
- %w[
428
- year month day wday hour min sec usec asctime
429
- ].each do |m|
430
- define_method(m) { to_time.send(m) }
431
- end
432
-
433
- def ==(o)
434
-
435
- o.is_a?(EoTime) &&
436
- o.seconds == @seconds &&
437
- (o.zone == @zone || o.zone.current_period == @zone.current_period)
438
- end
439
- #alias eql? == # FIXME see Object#== (ri)
440
-
441
- def >(o); @seconds > _to_f(o); end
442
- def >=(o); @seconds >= _to_f(o); end
443
- def <(o); @seconds < _to_f(o); end
444
- def <=(o); @seconds <= _to_f(o); end
445
- def <=>(o); @seconds <=> _to_f(o); end
446
-
447
- def add(t); @time = nil; @seconds += t.to_f; self; end
448
- def subtract(t); @time = nil; @seconds -= t.to_f; self; end
449
-
450
- def +(t); inc(t, 1); end
451
- def -(t); inc(t, -1); end
452
-
453
- WEEK_S = 7 * 24 * 3600
454
-
455
- def monthdays
456
-
457
- date = to_time
458
-
459
- pos = 1
460
- d = self.dup
461
-
462
- loop do
463
- d.add(-WEEK_S)
464
- break if d.month != date.month
465
- pos = pos + 1
466
- end
467
-
468
- neg = -1
469
- d = self.dup
470
-
471
- loop do
472
- d.add(WEEK_S)
473
- break if d.month != date.month
474
- neg = neg - 1
475
- end
476
-
477
- [ "#{date.wday}##{pos}", "#{date.wday}##{neg}" ]
478
- end
479
-
480
- def to_s
481
-
482
- strftime('%Y-%m-%d %H:%M:%S %z')
483
- end
484
-
485
- def to_zs
486
-
487
- strftime('%Y-%m-%d %H:%M:%S %/Z')
488
- end
489
-
490
- def iso8601(fraction_digits=0)
491
-
492
- s = (fraction_digits || 0) > 0 ? ".%#{fraction_digits}N" : ''
493
- strftime("%Y-%m-%dT%H:%M:%S#{s}%:z")
494
- end
495
-
496
- # Debug current time by showing local time / delta / utc time
497
- # for example: "0120-7(0820)"
498
- #
499
- def to_utc_comparison_s
500
-
501
- per = @zone.period_for_utc(utc)
502
- off = per.utc_total_offset
503
-
504
- off = off / 3600
505
- off = off >= 0 ? "+#{off}" : off.to_s
506
-
507
- strftime('%H%M') + off + utc.strftime('(%H%M)')
508
- end
509
-
510
- def to_time_s
511
-
512
- strftime("%H:%M:%S.#{'%06d' % usec}")
513
- end
514
-
515
- def inc(t, dir=1)
516
-
517
- case t
518
- when Numeric
519
- nt = self.dup
520
- nt.seconds += dir * t.to_f
521
- nt
522
- when ::Time, ::EtOrbi::EoTime
523
- fail ArgumentError.new(
524
- "Cannot add #{t.class} to EoTime") if dir > 0
525
- @seconds + dir * t.to_f
526
- else
527
- fail ArgumentError.new(
528
- "Cannot call add or subtract #{t.class} to EoTime instance")
529
- end
530
- end
531
-
532
- def localtime(zone=nil)
207
+ ZONES_ISO8601 =
208
+ %r{
209
+ (?<=:\d\d)\s*
210
+ (?:
211
+ [-+]
212
+ (?:[0-1][0-9]|2[0-4])
213
+ (?:(?::)?(?:[0-5][0-9]|60))?
214
+ (?![-+])
215
+ |Z
216
+ )
217
+ }x
533
218
 
534
- EoTime.new(self.to_f, zone)
535
- end
536
-
537
- alias translate localtime
538
-
539
- def wday_in_month
540
-
541
- [ count_weeks(-1), - count_weeks(1) ]
542
- end
543
-
544
- def reach(points)
545
-
546
- t = EoTime.new(self.to_f, @zone)
547
- step = 1
548
-
549
- s = points[:second] || points[:sec] || points[:s]
550
- m = points[:minute] || points[:min] || points[:m]
551
- h = points[:hour] || points[:hou] || points[:h]
552
-
553
- fail ArgumentError.new("missing :second, :minute, and :hour") \
554
- unless s || m || h
555
-
556
- if !s && !m
557
- step = 60 * 60
558
- t -= t.sec
559
- t -= t.min * 60
560
- elsif !s
561
- step = 60
562
- t -= t.sec
563
- end
564
-
565
- loop do
566
- t += step
567
- next if s && t.sec != s
568
- next if m && t.min != m
569
- next if h && t.hour != h
570
- break
571
- end
572
-
573
- t
574
- end
575
-
576
- protected
577
-
578
- # Returns a Ruby Time instance.
579
- #
580
- # Warning: the timezone of that Time instance will be UTC when used with
581
- # TZInfo < 2.0.0.
219
+ # https://en.wikipedia.org/wiki/ISO_8601
220
+ # Postel's law applies
582
221
  #
583
- def to_time
584
-
585
- @time ||= @zone.utc_to_local(utc)
586
- end
587
-
588
- def count_weeks(dir)
589
-
590
- c = 0
591
- t = self
592
- until t.month != self.month
593
- c += 1
594
- t += dir * (7 * 24 * 3600)
595
- end
222
+ def list_iso8601_zones(s)
596
223
 
597
- c
224
+ s.scan(ZONES_ISO8601).collect(&:strip)
598
225
  end
599
226
 
600
- def strfz(code)
227
+ ZONES_OLSON = (
228
+ TZInfo::Timezone.all.collect { |z| z.name }.sort +
229
+ (0..12).collect { |i| [ "UTC-#{i}", "UTC+#{i}" ] })
230
+ .flatten
231
+ .sort_by(&:size)
232
+ .reverse
601
233
 
602
- return @zone.name if code == '%/Z'
603
-
604
- per = @zone.period_for_utc(utc)
234
+ def list_olson_zones(s)
605
235
 
606
- return per.abbreviation.to_s if code == '%Z'
236
+ s = s.dup
607
237
 
608
- off = per.utc_total_offset
609
- #
610
- sn = off < 0 ? '-' : '+'; off = off.abs
611
- hr = off / 3600
612
- mn = (off % 3600) / 60
613
- sc = 0
614
-
615
- if @zone.name == 'UTC'
616
- 'Z' # align on Ruby ::Time#iso8601
617
- elsif code == '%z'
618
- '%s%02d%02d' % [ sn, hr, mn ]
619
- elsif code == '%:z'
620
- '%s%02d:%02d' % [ sn, hr, mn ]
621
- else
622
- '%s%02d:%02d:%02d' % [ sn, hr, mn, sc ]
623
- end
238
+ ZONES_OLSON
239
+ .inject([]) { |a, z|
240
+ i = s.index(z); next a unless i
241
+ s[i, z.length] = ''
242
+ a << z
243
+ a }
624
244
  end
625
245
 
626
- def _to_f(o)
627
-
628
- fail ArgumentError(
629
- "Comparison of EoTime with #{o.inspect} failed"
630
- ) unless o.is_a?(EoTime) || o.is_a?(Time)
246
+ def find_olson_zone(str)
631
247
 
632
- o.to_f
248
+ list_olson_zones(str).each { |s| z = get_tzone(s); return z if z }
249
+ nil
633
250
  end
634
- end
635
251
 
636
- class << self
252
+ def extract_zone(str)
637
253
 
638
- #
639
- # extra public methods
254
+ s = str.dup
640
255
 
641
- # https://en.wikipedia.org/wiki/ISO_8601
642
- # Postel's law applies
643
- #
644
- def list_iso8601_zones(s)
256
+ zs = ZONES_OLSON
257
+ .inject([]) { |a, z|
258
+ i = s.index(z); next a unless i
259
+ a << z
260
+ s[i, z.length] = ''
261
+ a }
645
262
 
646
- s
647
- .scan(
648
- %r{
649
- (?<=:\d\d)
650
- \s*
651
- (?:
652
- [-+]
653
- (?:[0-1][0-9]|2[0-4])
654
- (?:(?::)?(?:[0-5][0-9]|60))?
655
- (?![-+])
656
- |
657
- Z
658
- )
659
- }x)
660
- .collect(&:strip)
661
- end
263
+ s.gsub!(ZONES_ISO8601) { |m| zs << m.strip; '' } #if zs.empty?
662
264
 
663
- def list_olson_zones(s)
265
+ zs = zs.sort_by { |z| str.index(z) }
664
266
 
665
- s
666
- .scan(
667
- %r{
668
- (?<=\s|\A)
669
- (?:[A-Z][A-Za-z0-9+_-]+)
670
- (?:\/(?:[A-Z][A-Za-z0-9+_-]+)){0,2}
671
- }x)
672
- end
673
-
674
- def find_olson_zone(str)
675
-
676
- list_olson_zones(str).each { |s| z = get_tzone(s); return z if z }
677
- nil
267
+ [ s.strip, zs.last ]
678
268
  end
679
269
 
680
270
  def determine_local_tzone
681
271
 
272
+ # ENV has the priority
273
+
682
274
  etz = ENV['TZ']
683
275
 
684
- tz = etz && (::TZInfo::Timezone.get(etz) rescue nil)
276
+ tz = etz && get_tzone(etz)
685
277
  return tz if tz
686
278
 
279
+ # then Rails/ActiveSupport has the priority
280
+
687
281
  if Time.respond_to?(:zone) && Time.zone.respond_to?(:tzinfo)
688
282
  tz = Time.zone.tzinfo
689
283
  return tz if tz
690
284
  end
691
285
 
286
+ # then the operating system is queried
287
+
692
288
  tz = ::TZInfo::Timezone.get(os_tz) rescue nil
693
289
  return tz if tz
694
290
 
291
+ # then Ruby's time zone abbs are looked at CST, JST, CEST, ... :-(
292
+
695
293
  tzs = determine_local_tzones
696
294
  tz = (etz && tzs.find { |z| z.name == etz }) || tzs.first
697
295
  return tz if tz
698
296
 
297
+ # then, fall back to GMT offest :-(
298
+
699
299
  n = Time.now
700
300
 
701
301
  get_tzone(n.zone) ||
702
302
  get_tzone(n.strftime('%Z%z'))
703
303
  end
304
+ alias zone determine_local_tzone
704
305
 
705
306
  attr_accessor :_os_zone # test tool
706
307
 
@@ -712,7 +313,9 @@ module EtOrbi
712
313
  @os_tz ||= (debian_tz || centos_tz || osx_tz)
713
314
  end
714
315
 
715
- def to_windows_tz(zone_name, time=Time.now)
316
+ # Semi-helpful, since it requires the current time
317
+ #
318
+ def windows_zone_name(zone_name, time)
716
319
 
717
320
  twin = Time.utc(time.year, 1, 1) # winter
718
321
  tsum = Time.utc(time.year, 7, 1) # summer
@@ -729,7 +332,13 @@ module EtOrbi
729
332
  tz.period_for_utc(tsum).abbreviation.to_s ]
730
333
  .uniq
731
334
 
732
- [ abbs[0], tzop, tzoh, tzos, abbs[1] ].compact.join
335
+ if abbs[0].match(/\A[A-Z]/)
336
+ [ abbs[0], tzop, tzoh, tzos, abbs[1] ]
337
+ .compact.join
338
+ else
339
+ [ windows_zone_code_x(zone_name), tzop, tzoh, tzos || ':00', zone_name ]
340
+ .collect(&:to_s).join
341
+ end
733
342
  end
734
343
 
735
344
  #
@@ -737,6 +346,31 @@ module EtOrbi
737
346
 
738
347
  protected
739
348
 
349
+ def windows_zone_code_x(zone_name)
350
+
351
+ a = [ '_' ]
352
+ a.concat(zone_name.split('/')[0, 2].collect { |s| s[0, 1].upcase })
353
+ a << '_' if a.size < 3
354
+
355
+ a.join
356
+ end
357
+
358
+ def get_local_tzone(t)
359
+
360
+ l = Time.local(t.year, t.month, t.day, t.hour, t.min, t.sec, t.usec)
361
+
362
+ (t.zone == l.zone) ? determine_local_tzone : nil
363
+ end
364
+
365
+ # https://api.rubyonrails.org/classes/ActiveSupport/TimeWithZone.html
366
+ #
367
+ # If it responds to #time_zone, then return that time zone.
368
+ #
369
+ def get_as_tzone(t)
370
+
371
+ t.respond_to?(:time_zone) ? t.time_zone : nil
372
+ end
373
+
740
374
  def to_offset(n)
741
375
 
742
376
  i = n.to_i
@@ -748,17 +382,20 @@ module EtOrbi
748
382
  '%s%02d:%02d' % [ sn, hr, mn ]
749
383
  end
750
384
 
385
+ # custom timezones, no DST, just an offset, like "+08:00" or "-01:30"
386
+ #
751
387
  def get_offset_tzone(str)
752
388
 
753
- # custom timezones, no DST, just an offset, like "+08:00" or "-01:30"
754
-
755
- m = str.match(/\A([+-][0-1][0-9]):?([0-5][0-9])?\z/) rescue nil
389
+ m = str.match(/\A([+-][0-1]?[0-9]):?([0-5][0-9])?\z/) rescue nil
756
390
  #
757
391
  # On Windows, the real encoding could be something other than UTF-8,
758
392
  # and make the match fail
759
393
  #
760
394
  return nil unless m
761
395
 
396
+ tz = custom_tzs[str]
397
+ return tz if tz
398
+
762
399
  hr = m[1].to_i
763
400
  mn = m[2].to_i
764
401
 
@@ -766,15 +403,12 @@ module EtOrbi
766
403
  hr = nil if mn > 59
767
404
  mn = -mn if hr && hr < 0
768
405
 
769
- return (
770
- (@custom_tz_cache ||= {})[str] =
771
- create_offset_tzone(hr * 3600 + mn * 60, str)
772
- ) if hr
773
-
774
- nil
406
+ hr ?
407
+ custom_tzs[str] = create_offset_tzone(hr * 3600 + mn * 60, str) :
408
+ nil
775
409
  end
776
410
 
777
- if defined? TZInfo::DataSources::ConstantOffsetDataTimezoneInfo
411
+ if defined?(TZInfo::DataSources::ConstantOffsetDataTimezoneInfo)
778
412
  # TZInfo >= 2.0.0
779
413
 
780
414
  def create_offset_tzone(utc_off, id)
@@ -795,6 +429,16 @@ module EtOrbi
795
429
  end
796
430
  end
797
431
 
432
+ def get_x_offset_tzone(str)
433
+
434
+ m = str.match(/\A_..-?[0-1]?\d:?(?:[0-5]\d)?(.+)\z/) rescue nil
435
+ #
436
+ # On Windows, the real encoding could be something other than UTF-8,
437
+ # and make the match fail (as in .get_offset_tzone above)
438
+
439
+ m ? ::TZInfo::Timezone.get(m[1]) : nil
440
+ end
441
+
798
442
  def determine_local_tzones
799
443
 
800
444
  tabbs = (-6..5)
@@ -811,10 +455,9 @@ module EtOrbi
811
455
  twin = Time.local(t.year, 1, 1) # winter
812
456
  tsum = Time.local(t.year, 7, 1) # summer
813
457
 
814
- @tz_all ||= ::TZInfo::Timezone.all
815
458
  @tz_winter_summer ||= {}
816
459
 
817
- @tz_winter_summer[tabbs] ||= @tz_all
460
+ @tz_winter_summer[tabbs] ||= tz_all
818
461
  .select { |tz|
819
462
  pw = tz.period_for_local(twin)
820
463
  ps = tz.period_for_local(tsum)
@@ -826,6 +469,9 @@ module EtOrbi
826
469
  @tz_winter_summer[tabbs]
827
470
  end
828
471
 
472
+ def custom_tzs; @custom_tzs ||= {}; end
473
+ def tz_all; @tz_all ||= ::TZInfo::Timezone.all; end
474
+
829
475
  #
830
476
  # system tz determination
831
477
 
@@ -863,19 +509,5 @@ module EtOrbi
863
509
  { :debian => debian_tz, :centos => centos_tz, :osx => osx_tz }
864
510
  end
865
511
  end
866
-
867
- #def in_zone(&block)
868
- #
869
- # current_timezone = ENV['TZ']
870
- # ENV['TZ'] = @zone
871
- #
872
- # block.call
873
- #
874
- #ensure
875
- #
876
- # ENV['TZ'] = current_timezone
877
- #end
878
- #
879
- # kept around as a (thread-unsafe) relic
880
512
  end
881
513
 
@@ -0,0 +1,409 @@
1
+
2
+ module EtOrbi
3
+
4
+ # Our EoTime class (which quacks like a ::Time).
5
+ #
6
+ # An EoTime instance should respond to most of the methods ::Time instances
7
+ # respond to. If a method is missing, feel free to open an issue to
8
+ # ask (politely) for it. If it makes sense, it'll get added, else
9
+ # a workaround will get suggested.
10
+ # The immediate workaround is to call #to_t on the EoTime instance to get
11
+ # equivalent ::Time instance in the local, current, timezone.
12
+ #
13
+ class EoTime
14
+
15
+ #
16
+ # class methods
17
+
18
+ class << self
19
+
20
+ def now(zone=nil)
21
+
22
+ EtOrbi.now(zone)
23
+ end
24
+
25
+ def parse(str, opts={})
26
+
27
+ EtOrbi.parse(str, opts)
28
+ end
29
+
30
+ def get_tzone(o)
31
+
32
+ EtOrbi.get_tzone(o)
33
+ end
34
+
35
+ def local_tzone
36
+
37
+ EtOrbi.determine_local_tzone
38
+ end
39
+
40
+ def platform_info
41
+
42
+ EtOrbi.platform_info
43
+ end
44
+
45
+ def make(o)
46
+
47
+ EtOrbi.make_time(o)
48
+ end
49
+
50
+ def utc(*a)
51
+
52
+ EtOrbi.make_from_array(a, EtOrbi.get_tzone('UTC'))
53
+ end
54
+
55
+ def local(*a)
56
+
57
+ EtOrbi.make_from_array(a, local_tzone)
58
+ end
59
+ end
60
+
61
+ #
62
+ # instance methods
63
+
64
+ attr_reader :seconds
65
+ attr_reader :zone
66
+
67
+ def initialize(s, zone)
68
+
69
+ z = zone
70
+ z = nil if zone.is_a?(String) && zone.strip == ''
71
+ #
72
+ # happens with JRuby (and offset tzones like +04:00)
73
+ #
74
+ # $ jruby -r time -e "p Time.parse('2012-1-1 12:00 +04:00').zone"
75
+ # # => ""
76
+ # ruby -r time -e "p Time.parse('2012-1-1 12:00 +04:00').zone"
77
+ # # => nil
78
+
79
+ @seconds = s.to_f
80
+ @zone = self.class.get_tzone(z || :local)
81
+
82
+ fail ArgumentError.new(
83
+ "Cannot determine timezone from #{zone.inspect}" +
84
+ "\n#{EtOrbi.render_nozone_time(@seconds)}" +
85
+ "\n#{EtOrbi.platform_info.sub(',debian:', ",\ndebian:")}" +
86
+ "\nTry setting `ENV['TZ'] = 'Continent/City'` in your script " +
87
+ "(see https://en.wikipedia.org/wiki/List_of_tz_database_time_zones)" +
88
+ (defined?(TZInfo::Data) ? '' : "\nand adding gem 'tzinfo-data'")
89
+ ) unless @zone
90
+
91
+ @time = nil
92
+ # cache for #to_time result
93
+ end
94
+
95
+ def seconds=(f)
96
+
97
+ @time = nil
98
+ @seconds = f
99
+ end
100
+
101
+ def zone=(z)
102
+
103
+ @time = nil
104
+ @zone = self.class.get_tzone(zone || :current)
105
+ end
106
+
107
+ # Returns true if this EoTime instance corresponds to 2 different UTC
108
+ # times.
109
+ # It happens when transitioning from DST to winter time.
110
+ #
111
+ # https://www.timeanddate.com/time/change/usa/new-york?year=2018
112
+ #
113
+ def ambiguous?
114
+
115
+ @zone.local_to_utc(@zone.utc_to_local(utc))
116
+
117
+ false
118
+
119
+ rescue TZInfo::AmbiguousTime
120
+
121
+ true
122
+ end
123
+
124
+ # Returns this ::EtOrbi::EoTime as a ::Time instance
125
+ # in the current UTC timezone.
126
+ #
127
+ def utc
128
+
129
+ Time.utc(1970) + @seconds
130
+ end
131
+
132
+ # Returns true if this ::EtOrbi::EoTime instance timezone is UTC.
133
+ # Returns false else.
134
+ #
135
+ def utc?
136
+
137
+ %w[ gmt utc zulu etc/gmt etc/utc ].include?(
138
+ @zone.canonical_identifier.downcase)
139
+ end
140
+
141
+ alias getutc utc
142
+ alias getgm utc
143
+ alias to_utc_time utc
144
+
145
+ def to_f
146
+
147
+ @seconds
148
+ end
149
+
150
+ def to_i
151
+
152
+ @seconds.to_i
153
+ end
154
+
155
+ def strftime(format)
156
+
157
+ format = format.gsub(/%(\/?Z|:{0,2}z)/) { |f| strfz(f) }
158
+
159
+ to_time.strftime(format)
160
+ end
161
+
162
+ # Returns this ::EtOrbi::EoTime as a ::Time instance
163
+ # in the current timezone.
164
+ #
165
+ # Has a #to_t alias.
166
+ #
167
+ def to_local_time
168
+
169
+ Time.at(@seconds)
170
+ end
171
+
172
+ alias to_t to_local_time
173
+
174
+ def is_dst?
175
+
176
+ @zone.period_for_utc(utc).std_offset != 0
177
+ end
178
+ alias isdst is_dst?
179
+
180
+ def to_debug_s
181
+
182
+ uo = self.utc_offset
183
+ uos = uo < 0 ? '-' : '+'
184
+ uo = uo.abs
185
+ uoh, uom = [ uo / 3600, uo % 3600 ]
186
+
187
+ [
188
+ 'ot',
189
+ self.strftime('%Y-%m-%d %H:%M:%S'),
190
+ "%s%02d:%02d" % [ uos, uoh, uom ],
191
+ "dst:#{self.isdst}"
192
+ ].join(' ')
193
+ end
194
+
195
+ def utc_offset
196
+
197
+ @zone.period_for_utc(utc).utc_total_offset
198
+ end
199
+
200
+ %w[
201
+ year month day wday hour min sec usec asctime
202
+ ].each do |m|
203
+ define_method(m) { to_time.send(m) }
204
+ end
205
+
206
+ def ==(o)
207
+
208
+ o.is_a?(EoTime) &&
209
+ o.seconds == @seconds &&
210
+ (o.zone == @zone || o.zone.current_period == @zone.current_period)
211
+ end
212
+ #alias eql? == # FIXME see Object#== (ri)
213
+
214
+ def >(o); @seconds > _to_f(o); end
215
+ def >=(o); @seconds >= _to_f(o); end
216
+ def <(o); @seconds < _to_f(o); end
217
+ def <=(o); @seconds <= _to_f(o); end
218
+ def <=>(o); @seconds <=> _to_f(o); end
219
+
220
+ def add(t); @time = nil; @seconds += t.to_f; self; end
221
+ def subtract(t); @time = nil; @seconds -= t.to_f; self; end
222
+
223
+ def +(t); inc(t, 1); end
224
+ def -(t); inc(t, -1); end
225
+
226
+ WEEK_S = 7 * 24 * 3600
227
+
228
+ def monthdays
229
+
230
+ date = to_time
231
+
232
+ pos = 1
233
+ d = self.dup
234
+
235
+ loop do
236
+ d.add(-WEEK_S)
237
+ break if d.month != date.month
238
+ pos = pos + 1
239
+ end
240
+
241
+ neg = -1
242
+ d = self.dup
243
+
244
+ loop do
245
+ d.add(WEEK_S)
246
+ break if d.month != date.month
247
+ neg = neg - 1
248
+ end
249
+
250
+ [ "#{date.wday}##{pos}", "#{date.wday}##{neg}" ]
251
+ end
252
+
253
+ def to_s
254
+
255
+ strftime('%Y-%m-%d %H:%M:%S %z')
256
+ end
257
+
258
+ def to_zs
259
+
260
+ strftime('%Y-%m-%d %H:%M:%S %/Z')
261
+ end
262
+
263
+ def iso8601(fraction_digits=0)
264
+
265
+ s = (fraction_digits || 0) > 0 ? ".%#{fraction_digits}N" : ''
266
+ strftime("%Y-%m-%dT%H:%M:%S#{s}%:z")
267
+ end
268
+
269
+ # Debug current time by showing local time / delta / utc time
270
+ # for example: "0120-7(0820)"
271
+ #
272
+ def to_utc_comparison_s
273
+
274
+ per = @zone.period_for_utc(utc)
275
+ off = per.utc_total_offset
276
+
277
+ off = off / 3600
278
+ off = off >= 0 ? "+#{off}" : off.to_s
279
+
280
+ strftime('%H%M') + off + utc.strftime('(%H%M)')
281
+ end
282
+
283
+ def to_time_s
284
+
285
+ strftime("%H:%M:%S.#{'%06d' % usec}")
286
+ end
287
+
288
+ def inc(t, dir=1)
289
+
290
+ case t
291
+ when Numeric
292
+ nt = self.dup
293
+ nt.seconds += dir * t.to_f
294
+ nt
295
+ when ::Time, ::EtOrbi::EoTime
296
+ fail ArgumentError.new(
297
+ "Cannot add #{t.class} to EoTime") if dir > 0
298
+ @seconds + dir * t.to_f
299
+ else
300
+ fail ArgumentError.new(
301
+ "Cannot call add or subtract #{t.class} to EoTime instance")
302
+ end
303
+ end
304
+
305
+ def localtime(zone=nil)
306
+
307
+ EoTime.new(self.to_f, zone)
308
+ end
309
+
310
+ alias translate localtime
311
+
312
+ def wday_in_month
313
+
314
+ [ count_weeks(-1), - count_weeks(1) ]
315
+ end
316
+
317
+ def reach(points)
318
+
319
+ t = EoTime.new(self.to_f, @zone)
320
+ step = 1
321
+
322
+ s = points[:second] || points[:sec] || points[:s]
323
+ m = points[:minute] || points[:min] || points[:m]
324
+ h = points[:hour] || points[:hou] || points[:h]
325
+
326
+ fail ArgumentError.new("missing :second, :minute, and :hour") \
327
+ unless s || m || h
328
+
329
+ if !s && !m
330
+ step = 60 * 60
331
+ t -= t.sec
332
+ t -= t.min * 60
333
+ elsif !s
334
+ step = 60
335
+ t -= t.sec
336
+ end
337
+
338
+ loop do
339
+ t += step
340
+ next if s && t.sec != s
341
+ next if m && t.min != m
342
+ next if h && t.hour != h
343
+ break
344
+ end
345
+
346
+ t
347
+ end
348
+
349
+ protected
350
+
351
+ # Returns a Ruby Time instance.
352
+ #
353
+ # Warning: the timezone of that Time instance will be UTC when used with
354
+ # TZInfo < 2.0.0.
355
+ #
356
+ def to_time
357
+
358
+ @time ||= @zone.utc_to_local(utc)
359
+ end
360
+
361
+ def count_weeks(dir)
362
+
363
+ c = 0
364
+ t = self
365
+ until t.month != self.month
366
+ c += 1
367
+ t += dir * (7 * 24 * 3600)
368
+ end
369
+
370
+ c
371
+ end
372
+
373
+ def strfz(code)
374
+
375
+ return @zone.name if code == '%/Z'
376
+
377
+ per = @zone.period_for_utc(utc)
378
+
379
+ return per.abbreviation.to_s if code == '%Z'
380
+
381
+ off = per.utc_total_offset
382
+ #
383
+ sn = off < 0 ? '-' : '+'; off = off.abs
384
+ hr = off / 3600
385
+ mn = (off % 3600) / 60
386
+ sc = 0
387
+
388
+ if @zone.name == 'UTC'
389
+ 'Z' # align on Ruby ::Time#iso8601
390
+ elsif code == '%z'
391
+ '%s%02d%02d' % [ sn, hr, mn ]
392
+ elsif code == '%:z'
393
+ '%s%02d:%02d' % [ sn, hr, mn ]
394
+ else
395
+ '%s%02d:%02d:%02d' % [ sn, hr, mn, sc ]
396
+ end
397
+ end
398
+
399
+ def _to_f(o)
400
+
401
+ fail ArgumentError(
402
+ "Comparison of EoTime with #{o.inspect} failed"
403
+ ) unless o.is_a?(EoTime) || o.is_a?(Time)
404
+
405
+ o.to_f
406
+ end
407
+ end
408
+ end
409
+
@@ -37,6 +37,10 @@ module EtOrbi
37
37
  nil
38
38
  end
39
39
 
40
+ # https://docs.microsoft.com/en-us/windows-hardware/manufacture/desktop/default-time-zones
41
+ # https://support.microsoft.com/en-ca/help/973627/microsoft-time-zone-index-values
42
+ # https://ss64.com/nt/timezones.html
43
+
40
44
  ZONE_ALIASES = {
41
45
  'Coordinated Universal Time' => 'UTC',
42
46
  'Afghanistan Standard Time' => 'Asia/Kabul',
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: et-orbi
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.1.6
4
+ version: 1.1.7
5
5
  platform: ruby
6
6
  authors:
7
7
  - John Mettraux
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2018-09-04 00:00:00.000000000 Z
11
+ date: 2019-01-14 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: tzinfo
@@ -38,6 +38,20 @@ dependencies:
38
38
  - - "~>"
39
39
  - !ruby/object:Gem::Version
40
40
  version: '3.4'
41
+ - !ruby/object:Gem::Dependency
42
+ name: chronic
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '0.10'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '0.10'
41
55
  description: Time zones for fugit and rufus-scheduler. Urbi et Orbi.
42
56
  email:
43
57
  - jmettraux+flor@gmail.com
@@ -52,6 +66,7 @@ files:
52
66
  - README.md
53
67
  - et-orbi.gemspec
54
68
  - lib/et-orbi.rb
69
+ - lib/et-orbi/eo_time.rb
55
70
  - lib/et-orbi/zone_aliases.rb
56
71
  - lib/et_orbi.rb
57
72
  - lib/etorbi.rb