et-orbi 1.1.6 → 1.1.7

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.
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