/
timezone.rb
1158 lines (1078 loc) · 52.1 KB
/
timezone.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
# encoding: UTF-8
# frozen_string_literal: true
require 'set'
module TZInfo
# {AmbiguousTime} is raised to indicate that a specified local time has more
# than one possible equivalent UTC time. Such ambiguities arise when the
# clocks are set back in a time zone, most commonly during the repeated hour
# when transitioning from daylight savings time to standard time.
#
# {AmbiguousTime} is raised by {Timezone#local_datetime},
# {Timezone#local_time}, {Timezone#local_timestamp}, {Timezone#local_to_utc}
# and {Timezone#period_for_local} when using an ambiguous time and not
# specifying how to resolve the ambiguity.
class AmbiguousTime < StandardError
end
# {PeriodNotFound} is raised to indicate that no {TimezonePeriod} matching a
# given time could be found.
class PeriodNotFound < StandardError
end
# {InvalidTimezoneIdentifier} is raised by {Timezone.get} if the identifier
# given is not valid.
class InvalidTimezoneIdentifier < StandardError
end
# {UnknownTimezone} is raised when calling methods on an instance of
# {Timezone} that was created directly. To obtain {Timezone} instances the
# {Timezone.get} method should be used instead.
class UnknownTimezone < StandardError
end
# The {Timezone} class represents a time zone. It provides a factory method,
# {get}, to retrieve {Timezone} instances by their identifier.
#
# The {Timezone#to_local} method can be used to convert `Time` and `DateTime`
# instances to the local time for the zone. For example:
#
# tz = TZInfo::Timezone.get('America/New_York')
# local_time = tz.to_local(Time.utc(2005,8,29,15,35,0))
# local_datetime = tz.to_local(DateTime.new(2005,8,29,15,35,0))
#
# Local `Time` and `DateTime` instances returned by `Timezone` have the
# correct local offset.
#
# The {Timezone#local_to_utc} method can by used to convert local `Time` and
# `DateTime` instances to UTC. {Timezone#local_to_utc} ignores the UTC offset
# of the supplied value and treats if it is a local time for the zone. For
# example:
#
# tz = TZInfo::Timezone.get('America/New_York')
# utc_time = tz.local_to_utc(Time.new(2005,8,29,11,35,0))
# utc_datetime = tz.local_to_utc(DateTime.new(2005,8,29,11,35,0))
#
# Each time zone is treated as sequence of periods of time ({TimezonePeriod})
# that observe the same offset ({TimezoneOffset}). Transitions
# ({TimezoneTransition}) denote the end of one period and the start of the
# next. The {Timezone} class has methods that allow the periods, offsets and
# transitions of a time zone to be interrogated.
#
# All methods that take `Time` objects as parameters can be used with
# arbitrary `Time`-like objects that respond to both `to_i` and `subsec` and
# optionally `utc_offset`.
#
# The {Timezone} class is thread-safe. It is safe to use class and instance
# methods of {Timezone} in concurrently executing threads. Instances of
# {Timezone} can be shared across thread boundaries.
#
# The IANA Time Zone Database maintainers recommend that time zone identifiers
# are not made visible to end-users (see [Names of
# timezones](https://data.iana.org/time-zones/theory.html#naming)). The
# {Country} class can be used to obtain lists of time zones by country,
# including user-friendly descriptions and approximate locations.
#
# @abstract The {get} method returns an instance of either {DataTimezone} or
# {LinkedTimezone}. The {get_proxy} method and other methods returning
# collections of time zones return instances of {TimezoneProxy}.
class Timezone
include Comparable
# The default value of the dst parameter of the {local_datetime},
# {local_time}, {local_timestamp}, {local_to_utc} and {period_for_local}
# methods.
#
# @!visibility private
@@default_dst = nil
class << self
# Sets the default value of the optional `dst` parameter of the
# {local_datetime}, {local_time}, {local_timestamp}, {local_to_utc} and
# {period_for_local} methods. Can be set to `nil`, `true` or `false`.
#
# @param value [Boolean] `nil`, `true` or `false`.
def default_dst=(value)
@@default_dst = value.nil? ? nil : !!value
end
# Returns the default value of the optional `dst` parameter of the
# {local_time}, {local_datetime} and {local_timestamp}, {local_to_utc}
# and {period_for_local} methods (`nil`, `true` or `false`).
#
# {default_dst} defaults to `nil` unless changed with {default_dst=}.
#
# @return [Boolean] the default value of the optional `dst` parameter of
# the {local_time}, {local_datetime} and {local_timestamp},
# {local_to_utc} and {period_for_local} methods (`nil`, `true` or
# `false`).
def default_dst
@@default_dst
end
# Returns a time zone by its IANA Time Zone Database identifier (e.g.
# `"Europe/London"` or `"America/Chicago"`). Call {all_identifiers} for a
# list of all the valid identifiers.
#
# The {get} method will return a subclass of {Timezone}, either a
# {DataTimezone} (for a time zone defined by rules that set out when
# transitions occur) or a {LinkedTimezone} (for a time zone that is just a
# link to or alias for a another time zone).
#
# @param identifier [String] an IANA Time Zone Database time zone
# identifier.
# @return [Timezone] the {Timezone} with the given `identifier`.
# @raise [InvalidTimezoneIdentifier] if the `identifier` is not valid.
def get(identifier)
data_source.get_timezone_info(identifier).create_timezone
end
# Returns a proxy for the time zone with the given identifier. This allows
# loading of the time zone data to be deferred until it is first needed.
#
# The identifier will not be validated. If an invalid identifier is
# specified, no exception will be raised until the proxy is used.
#
# @param identifier [String] an IANA Time Zone Database time zone
# identifier.
# @return [TimezoneProxy] a proxy for the time zone with the given
# `identifier`.
def get_proxy(identifier)
TimezoneProxy.new(identifier)
end
# Returns an `Array` of all the available time zones.
#
# {TimezoneProxy} instances are returned to avoid the overhead of loading
# time zone data until it is first needed.
#
# @return [Array<Timezone>] all available time zones.
def all
get_proxies(all_identifiers)
end
# @return [Array<String>] an `Array` containing the identifiers of all the
# available time zones.
def all_identifiers
data_source.timezone_identifiers
end
# Returns an `Array` of all the available time zones that are
# defined by offsets and transitions.
#
# {TimezoneProxy} instances are returned to avoid the overhead of loading
# time zone data until it is first needed.
#
# @return [Array<Timezone>] an `Array` of all the available time zones
# that are defined by offsets and transitions.
def all_data_zones
get_proxies(all_data_zone_identifiers)
end
# @return [Array<String>] an `Array` of the identifiers of all available
# time zones that are defined by offsets and transitions.
def all_data_zone_identifiers
data_source.data_timezone_identifiers
end
# Returns an `Array` of all the available time zones that are
# defined as links to / aliases for other time zones.
#
# {TimezoneProxy} instances are returned to avoid the overhead of loading
# time zone data until it is first needed.
#
# @return [Array<Timezone>] an `Array` of all the available time zones
# that are defined as links to / aliases for other time zones.
def all_linked_zones
get_proxies(all_linked_zone_identifiers)
end
# @return [Array<String>] an `Array` of the identifiers of all available
# time zones that are defined as links to / aliases for other time zones.
def all_linked_zone_identifiers
data_source.linked_timezone_identifiers
end
# Returns an `Array` of all the time zones that are observed by at least
# one {Country}. This is not the complete set of time zones as some are
# not country specific (e.g. `'Etc/GMT'`).
#
# {TimezoneProxy} instances are returned to avoid the overhead of loading
# time zone data until it is first needed.
#
# @return [Array<Timezone>] an `Array` of all the time zones that are
# observed by at least one {Country}.
def all_country_zones
Country.all.map(&:zones).flatten.uniq
end
# Returns an `Array` of the identifiers of all the time zones that are
# observed by at least one {Country}. This is not the complete set of time
# zone identifiers as some are not country specific (e.g. `'Etc/GMT'`).
#
# {TimezoneProxy} instances are returned to avoid the overhead of loading
# time zone data until it is first needed.
#
# @return [Array<String>] an `Array` of the identifiers of all the time
# zones that are observed by at least one {Country}.
def all_country_zone_identifiers
Country.all.map(&:zone_identifiers).flatten.uniq
end
private
# @param [Enumerable<String>] identifiers an `Enumerable` of time zone
# identifiers.
# @return [Array<TimezoneProxy>] an `Array` of {TimezoneProxy}
# instances corresponding to the given identifiers.
def get_proxies(identifiers)
identifiers.collect {|identifier| get_proxy(identifier)}
end
# @return [DataSource] the current DataSource.
def data_source
DataSource.get
end
end
# @return [String] the identifier of the time zone, for example,
# `"Europe/Paris"`.
def identifier
raise_unknown_timezone
end
# @return [String] the identifier of the time zone, for example,
# `"Europe/Paris"`.
def name
# Don't use alias, as identifier gets overridden.
identifier
end
# @return [String] {identifier}, modified to make it more readable.
def to_s
friendly_identifier
end
# @return [String] the internal object state as a programmer-readable
# `String`.
def inspect
"#<#{self.class}: #{identifier}>"
end
# Returns {identifier}, modified to make it more readable. Set
# `skip_first_part` to omit the first part of the identifier (typically a
# region name) where there is more than one part.
#
# For example:
#
# TZInfo::Timezone.get('Europe/Paris').friendly_identifier(false) #=> "Europe - Paris"
# TZInfo::Timezone.get('Europe/Paris').friendly_identifier(true) #=> "Paris"
# TZInfo::Timezone.get('America/Indiana/Knox').friendly_identifier(false) #=> "America - Knox, Indiana"
# TZInfo::Timezone.get('America/Indiana/Knox').friendly_identifier(true) #=> "Knox, Indiana"
#
# @param skip_first_part [Boolean] whether the first part of the identifier
# (typically a region name) should be omitted.
# @return [String] the modified identifier.
def friendly_identifier(skip_first_part = false)
id = identifier
id = id.encode(Encoding::UTF_8) unless id.encoding.ascii_compatible?
parts = id.split('/')
if parts.empty?
# shouldn't happen
identifier
elsif parts.length == 1
parts[0]
else
prefix = skip_first_part ? nil : "#{parts[0]} - "
parts = parts.drop(1).map do |part|
part.gsub!(/_/, ' ')
if part.index(/[a-z]/)
# Missing a space if a lower case followed by an upper case and the
# name isn't McXxxx.
part.gsub!(/([^M][a-z])([A-Z])/, '\1 \2')
part.gsub!(/([M][a-bd-z])([A-Z])/, '\1 \2')
# Missing an apostrophe if two consecutive upper case characters.
part.gsub!(/([A-Z])([A-Z])/, '\1\'\2')
end
part
end
"#{prefix}#{parts.reverse.join(', ')}"
end
end
# Returns the {TimezonePeriod} that is valid at a given time.
#
# Unlike {period_for_local} and {period_for_utc}, the UTC offset of the
# `time` parameter is taken into consideration.
#
# @param time [Object] a `Time`, `DateTime` or {Timestamp}.
# @return [TimezonePeriod] the {TimezonePeriod} that is valid at `time`.
# @raise [ArgumentError] if `time` is `nil`.
# @raise [ArgumentError] if `time` is a {Timestamp} with an unspecified
# offset.
def period_for(time)
raise_unknown_timezone
end
# Returns the set of {TimezonePeriod}s that are valid for the given
# local time as an `Array`.
#
# The UTC offset of the `local_time` parameter is ignored (it is treated as
# a time in the time zone represented by `self`).
#
# This will typically return an `Array` containing a single
# {TimezonePeriod}. More than one {TimezonePeriod} will be returned when the
# local time is ambiguous (for example, when daylight savings time ends). An
# empty `Array` will be returned when the local time is not valid (for
# example, when daylight savings time begins).
#
# To obtain just a single {TimezonePeriod} in all cases, use
# {period_for_local} instead and specify how ambiguities should be resolved.
#
# @param local_time [Object] a `Time`, `DateTime` or {Timestamp}.
# @return [Array<TimezonePeriod>] the set of {TimezonePeriod}s that are
# valid at `local_time`.
# @raise [ArgumentError] if `local_time` is `nil`.
def periods_for_local(local_time)
raise_unknown_timezone
end
# Returns an `Array` of {TimezoneTransition} instances representing the
# times where the UTC offset of the timezone changes.
#
# Transitions are returned up to a given time (`to`).
#
# A from time may also be supplied using the `from` parameter. If from is
# not `nil`, only transitions from that time onwards will be returned.
#
# Comparisons with `to` are exclusive. Comparisons with `from` are
# inclusive. If a transition falls precisely on `to`, it will be excluded.
# If a transition falls on `from`, it will be included.
#
# @param to [Object] a `Time`, `DateTime` or {Timestamp} specifying the
# latest (exclusive) transition to return.
# @param from [Object] an optional `Time`, `DateTime` or {Timestamp}
# specifying the earliest (inclusive) transition to return.
# @return [Array<TimezoneTransition>] the transitions that are earlier than
# `to` and, if specified, at or later than `from`. Transitions are ordered
# by when they occur, from earliest to latest.
# @raise [ArgumentError] if `from` is specified and `to` is not greater than
# `from`.
# @raise [ArgumentError] is raised if `to` is `nil`.
# @raise [ArgumentError] if either `to` or `from` is a {Timestamp} with an
# unspecified offset.
def transitions_up_to(to, from = nil)
raise_unknown_timezone
end
# Returns the canonical {Timezone} instance for this {Timezone}.
#
# The IANA Time Zone database contains two types of definition: Zones and
# Links. Zones are defined by rules that set out when transitions occur.
# Links are just references to fully defined Zone, creating an alias for
# that Zone.
#
# Links are commonly used where a time zone has been renamed in a release of
# the Time Zone database. For example, the US/Eastern Zone was renamed as
# America/New_York. A US/Eastern Link was added in its place, linking to
# (and creating an alias for) America/New_York.
#
# Links are also used for time zones that are currently identical to a full
# Zone, but that are administered separately. For example, Europe/Vatican is
# a Link to (and alias for) Europe/Rome.
#
# For a full Zone (implemented by {DataTimezone}), {canonical_zone} returns
# self.
#
# For a Link (implemented by {LinkedTimezone}), {canonical_zone} returns a
# {Timezone} instance representing the full Zone that the link targets.
#
# TZInfo can be used with different data sources (see the documentation for
# {TZInfo::DataSource}). Some DataSource implementations may not support
# distinguishing between full Zones and Links and will treat all time zones
# as full Zones. In this case, {canonical_zone} will always return `self`.
#
# There are two built-in DataSource implementations.
# {DataSources::RubyDataSource} (which will be used if the tzinfo-data gem
# is available) supports Link zones. {DataSources::ZoneinfoDataSource}
# returns Link zones as if they were full Zones. If the {canonical_zone} or
# {canonical_identifier} methods are needed, the tzinfo-data gem should be
# installed.
#
# The {TZInfo::DataSource.get} method can be used to check which DataSource
# implementation is being used.
#
# @return [Timezone] the canonical {Timezone} instance for this {Timezone}.
def canonical_zone
raise_unknown_timezone
end
# Returns the {TimezonePeriod} that is valid at a given time.
#
# The UTC offset of the `utc_time` parameter is ignored (it is treated as a
# UTC time). Use the {period_for} method instead if the UTC offset of the
# time needs to be taken into consideration.
#
# @param utc_time [Object] a `Time`, `DateTime` or {Timestamp}.
# @return [TimezonePeriod] the {TimezonePeriod} that is valid at `utc_time`.
# @raise [ArgumentError] if `utc_time` is `nil`.
def period_for_utc(utc_time)
raise ArgumentError, 'utc_time must be specified' unless utc_time
period_for(Timestamp.for(utc_time, :treat_as_utc))
end
# Returns the {TimezonePeriod} that is valid at the given local time.
#
# The UTC offset of the `local_time` parameter is ignored (it is treated as
# a time in the time zone represented by `self`). Use the {period_for}
# method instead if the the UTC offset of the time needs to be taken into
# consideration.
#
# _Warning:_ There are local times that have no equivalent UTC times (for
# example, during the transition from standard time to daylight savings
# time). There are also local times that have more than one UTC equivalent
# (for example, during the transition from daylight savings time to standard
# time).
#
# In the first case (no equivalent UTC time), a {PeriodNotFound} exception
# will be raised.
#
# In the second case (more than one equivalent UTC time), an {AmbiguousTime}
# exception will be raised unless the optional `dst` parameter or block
# handles the ambiguity.
#
# If the ambiguity is due to a transition from daylight savings time to
# standard time, the `dst` parameter can be used to select whether the
# daylight savings time or local time is used. For example, the following
# code would raise an {AmbiguousTime} exception:
#
# tz = TZInfo::Timezone.get('America/New_York')
# tz.period_for_local(Time.new(2004,10,31,1,30,0))
#
# Specifying `dst = true` would select the daylight savings period from
# April to October 2004. Specifying `dst = false` would return the
# standard time period from October 2004 to April 2005.
#
# The `dst` parameter will not be able to resolve an ambiguity resulting
# from the clocks being set back without changing from daylight savings time
# to standard time. In this case, if a block is specified, it will be called
# to resolve the ambiguity. The block must take a single parameter - an
# `Array` of {TimezonePeriod}s that need to be resolved. The block can
# select and return a single {TimezonePeriod} or return `nil` or an empty
# `Array` to cause an {AmbiguousTime} exception to be raised.
#
# The default value of the `dst` parameter can be specified using
# {Timezone.default_dst=}.
#
# @param local_time [Object] a `Time`, `DateTime` or {Timestamp}.
# @param dst [Boolean] whether to resolve ambiguous local times by always
# selecting the period observing daylight savings time (`true`), always
# selecting the period observing standard time (`false`), or leaving the
# ambiguity unresolved (`nil`).
# @yield [periods] if the `dst` parameter did not resolve an ambiguity, an
# optional block is yielded to.
# @yieldparam periods [Array<TimezonePeriod>] an `Array` containing all
# the {TimezonePeriod}s that still match `local_time` after applying the
# `dst` parameter.
# @yieldreturn [Object] to resolve the ambiguity: a chosen {TimezonePeriod}
# or an `Array` containing a chosen {TimezonePeriod}; to leave the
# ambiguity unresolved: an empty `Array`, an `Array` containing more than
# one {TimezonePeriod}, or `nil`.
# @return [TimezonePeriod] the {TimezonePeriod} that is valid at
# `local_time`.
# @raise [ArgumentError] if `local_time` is `nil`.
# @raise [PeriodNotFound] if `local_time` is not valid for the time zone
# (there is no equivalent UTC time).
# @raise [AmbiguousTime] if `local_time` was ambiguous for the time zone and
# the `dst` parameter or block did not resolve the ambiguity.
def period_for_local(local_time, dst = Timezone.default_dst)
raise ArgumentError, 'local_time must be specified' unless local_time
local_time = Timestamp.for(local_time, :ignore)
results = periods_for_local(local_time)
if results.empty?
raise PeriodNotFound, "#{local_time.strftime('%Y-%m-%d %H:%M:%S')} is an invalid local time."
elsif results.size < 2
results.first
else
# ambiguous result try to resolve
if !dst.nil?
matches = results.find_all {|period| period.dst? == dst}
results = matches if !matches.empty?
end
if results.size < 2
results.first
else
# still ambiguous, try the block
if block_given?
results = yield results
end
if results.is_a?(TimezonePeriod)
results
elsif results && results.size == 1
results.first
else
raise AmbiguousTime, "#{local_time.strftime('%Y-%m-%d %H:%M:%S')} is an ambiguous local time."
end
end
end
end
# Converts a time to the local time for the time zone.
#
# The result will be of type {TimeWithOffset} (if passed a `Time`),
# {DateTimeWithOffset} (if passed a `DateTime`) or {TimestampWithOffset} (if
# passed a {Timestamp}). {TimeWithOffset}, {DateTimeWithOffset} and
# {TimestampWithOffset} are subclasses of `Time`, `DateTime` and {Timestamp}
# that provide additional information about the local result.
#
# Unlike {utc_to_local}, {to_local} takes the UTC offset of the given time
# into consideration.
#
# @param time [Object] a `Time`, `DateTime` or {Timestamp}.
# @return [Object] the local equivalent of `time` as a {TimeWithOffset},
# {DateTimeWithOffset} or {TimestampWithOffset}.
# @raise [ArgumentError] if `time` is `nil`.
# @raise [ArgumentError] if `time` is a {Timestamp} that does not have a
# specified UTC offset.
def to_local(time)
raise ArgumentError, 'time must be specified' unless time
Timestamp.for(time) do |ts|
TimestampWithOffset.set_timezone_offset(ts, period_for(ts).offset)
end
end
# Converts a time in UTC to the local time for the time zone.
#
# The result will be of type {TimeWithOffset} (if passed a `Time`),
# {DateTimeWithOffset} (if passed a `DateTime`) or {TimestampWithOffset} (if
# passed a {Timestamp}). {TimeWithOffset}, {DateTimeWithOffset} and
# {TimestampWithOffset} are subclasses of `Time`, `DateTime` and {Timestamp}
# that provide additional information about the local result.
#
# The UTC offset of the `utc_time` parameter is ignored (it is treated as a
# UTC time). Use the {to_local} method instead if the the UTC offset of the
# time needs to be taken into consideration.
#
# @param utc_time [Object] a `Time`, `DateTime` or {Timestamp}.
# @return [Object] the local equivalent of `utc_time` as a {TimeWithOffset},
# {DateTimeWithOffset} or {TimestampWithOffset}.
# @raise [ArgumentError] if `utc_time` is `nil`.
def utc_to_local(utc_time)
raise ArgumentError, 'utc_time must be specified' unless utc_time
Timestamp.for(utc_time, :treat_as_utc) do |ts|
to_local(ts)
end
end
# Converts a local time for the time zone to UTC.
#
# The result will either be a `Time`, `DateTime` or {Timestamp} according to
# the type of the `local_time` parameter.
#
# The UTC offset of the `local_time` parameter is ignored (it is treated as
# a time in the time zone represented by `self`).
#
# _Warning:_ There are local times that have no equivalent UTC times (for
# example, during the transition from standard time to daylight savings
# time). There are also local times that have more than one UTC equivalent
# (for example, during the transition from daylight savings time to standard
# time).
#
# In the first case (no equivalent UTC time), a {PeriodNotFound} exception
# will be raised.
#
# In the second case (more than one equivalent UTC time), an {AmbiguousTime}
# exception will be raised unless the optional `dst` parameter or block
# handles the ambiguity.
#
# If the ambiguity is due to a transition from daylight savings time to
# standard time, the `dst` parameter can be used to select whether the
# daylight savings time or local time is used. For example, the following
# code would raise an {AmbiguousTime} exception:
#
# tz = TZInfo::Timezone.get('America/New_York')
# tz.period_for_local(Time.new(2004,10,31,1,30,0))
#
# Specifying `dst = true` would select the daylight savings period from
# April to October 2004. Specifying `dst = false` would return the
# standard time period from October 2004 to April 2005.
#
# The `dst` parameter will not be able to resolve an ambiguity resulting
# from the clocks being set back without changing from daylight savings time
# to standard time. In this case, if a block is specified, it will be called
# to resolve the ambiguity. The block must take a single parameter - an
# `Array` of {TimezonePeriod}s that need to be resolved. The block can
# select and return a single {TimezonePeriod} or return `nil` or an empty
# `Array` to cause an {AmbiguousTime} exception to be raised.
#
# The default value of the `dst` parameter can be specified using
# {Timezone.default_dst=}.
#
# @param local_time [Object] a `Time`, `DateTime` or {Timestamp}.
# @param dst [Boolean] whether to resolve ambiguous local times by always
# selecting the period observing daylight savings time (`true`), always
# selecting the period observing standard time (`false`), or leaving the
# ambiguity unresolved (`nil`).
# @yield [periods] if the `dst` parameter did not resolve an ambiguity, an
# optional block is yielded to.
# @yieldparam periods [Array<TimezonePeriod>] an `Array` containing all
# the {TimezonePeriod}s that still match `local_time` after applying the
# `dst` parameter.
# @yieldreturn [Object] to resolve the ambiguity: a chosen {TimezonePeriod}
# or an `Array` containing a chosen {TimezonePeriod}; to leave the
# ambiguity unresolved: an empty `Array`, an `Array` containing more than
# one {TimezonePeriod}, or `nil`.
# @return [Object] the UTC equivalent of `local_time` as a `Time`,
# `DateTime` or {Timestamp}.
# @raise [ArgumentError] if `local_time` is `nil`.
# @raise [PeriodNotFound] if `local_time` is not valid for the time zone
# (there is no equivalent UTC time).
# @raise [AmbiguousTime] if `local_time` was ambiguous for the time zone and
# the `dst` parameter or block did not resolve the ambiguity.
def local_to_utc(local_time, dst = Timezone.default_dst)
raise ArgumentError, 'local_time must be specified' unless local_time
Timestamp.for(local_time, :ignore) do |ts|
period = if block_given?
period_for_local(ts, dst) {|periods| yield periods }
else
period_for_local(ts, dst)
end
ts.add_and_set_utc_offset(-period.observed_utc_offset, :utc)
end
end
# Creates a `Time` object based on the given (Gregorian calendar) date and
# time parameters. The parameters are interpreted as a local time in the
# time zone. The result has the appropriate `utc_offset`, `zone` and
# {TimeWithOffset#timezone_offset timezone_offset}.
#
# _Warning:_ There are time values that are not valid as local times in a
# time zone (for example, during the transition from standard time to
# daylight savings time). There are also time values that are ambiguous,
# occurring more than once with different offsets to UTC (for example,
# during the transition from daylight savings time to standard time).
#
# In the first case (an invalid local time), a {PeriodNotFound} exception
# will be raised.
#
# In the second case (more than one occurrence), an {AmbiguousTime}
# exception will be raised unless the optional `dst` parameter or block
# handles the ambiguity.
#
# If the ambiguity is due to a transition from daylight savings time to
# standard time, the `dst` parameter can be used to select whether the
# daylight savings time or local time is used. For example, the following
# code would raise an {AmbiguousTime} exception:
#
# tz = TZInfo::Timezone.get('America/New_York')
# tz.local_time(2004,10,31,1,30,0,0)
#
# Specifying `dst = true` would return a `Time` with a UTC offset of -4
# hours and abbreviation EDT (Eastern Daylight Time). Specifying `dst =
# false` would return a `Time` with a UTC offset of -5 hours and
# abbreviation EST (Eastern Standard Time).
#
# The `dst` parameter will not be able to resolve an ambiguity resulting
# from the clocks being set back without changing from daylight savings time
# to standard time. In this case, if a block is specified, it will be called
# to resolve the ambiguity. The block must take a single parameter - an
# `Array` of {TimezonePeriod}s that need to be resolved. The block can
# select and return a single {TimezonePeriod} or return `nil` or an empty
# `Array` to cause an {AmbiguousTime} exception to be raised.
#
# The default value of the `dst` parameter can be specified using
# {Timezone.default_dst=}.
#
# @param year [Integer] the year.
# @param month [Integer] the month (1-12).
# @param day [Integer] the day of the month (1-31).
# @param hour [Integer] the hour (0-23).
# @param minute [Integer] the minute (0-59).
# @param second [Integer] the second (0-59).
# @param sub_second [Numeric] the fractional part of the second as either
# a `Rational` that is greater than or equal to 0 and less than 1, or
# the `Integer` 0.
# @param dst [Boolean] whether to resolve ambiguous local times by always
# selecting the period observing daylight savings time (`true`), always
# selecting the period observing standard time (`false`), or leaving the
# ambiguity unresolved (`nil`).
# @yield [periods] if the `dst` parameter did not resolve an ambiguity, an
# optional block is yielded to.
# @yieldparam periods [Array<TimezonePeriod>] an `Array` containing all
# the {TimezonePeriod}s that still match `local_time` after applying the
# `dst` parameter.
# @yieldreturn [Object] to resolve the ambiguity: a chosen {TimezonePeriod}
# or an `Array` containing a chosen {TimezonePeriod}; to leave the
# ambiguity unresolved: an empty `Array`, an `Array` containing more than
# one {TimezonePeriod}, or `nil`.
# @return [TimeWithOffset] a new `Time` object based on the given values,
# interpreted as a local time in the time zone.
# @raise [ArgumentError] if either of `year`, `month`, `day`, `hour`,
# `minute`, or `second` is not an `Integer`.
# @raise [ArgumentError] if `sub_second` is not a `Rational`, or the
# `Integer` 0.
# @raise [ArgumentError] if `utc_offset` is not `nil`, not an `Integer`
# and not the `Symbol` `:utc`.
# @raise [RangeError] if `month` is not between 1 and 12.
# @raise [RangeError] if `day` is not between 1 and 31.
# @raise [RangeError] if `hour` is not between 0 and 23.
# @raise [RangeError] if `minute` is not between 0 and 59.
# @raise [RangeError] if `second` is not between 0 and 59.
# @raise [RangeError] if `sub_second` is a `Rational` but that is less
# than 0 or greater than or equal to 1.
# @raise [PeriodNotFound] if the date and time parameters do not specify a
# valid local time in the time zone.
# @raise [AmbiguousTime] if the date and time parameters are ambiguous for
# the time zone and the `dst` parameter or block did not resolve the
# ambiguity.
def local_time(year, month = 1, day = 1, hour = 0, minute = 0, second = 0, sub_second = 0, dst = Timezone.default_dst, &block)
local_timestamp(year, month, day, hour, minute, second, sub_second, dst, &block).to_time
end
# Creates a `DateTime` object based on the given (Gregorian calendar) date
# and time parameters. The parameters are interpreted as a local time in the
# time zone. The result has the appropriate `offset` and
# {DateTimeWithOffset#timezone_offset timezone_offset}.
#
# _Warning:_ There are time values that are not valid as local times in a
# time zone (for example, during the transition from standard time to
# daylight savings time). There are also time values that are ambiguous,
# occurring more than once with different offsets to UTC (for example,
# during the transition from daylight savings time to standard time).
#
# In the first case (an invalid local time), a {PeriodNotFound} exception
# will be raised.
#
# In the second case (more than one occurrence), an {AmbiguousTime}
# exception will be raised unless the optional `dst` parameter or block
# handles the ambiguity.
#
# If the ambiguity is due to a transition from daylight savings time to
# standard time, the `dst` parameter can be used to select whether the
# daylight savings time or local time is used. For example, the following
# code would raise an {AmbiguousTime} exception:
#
# tz = TZInfo::Timezone.get('America/New_York')
# tz.local_datetime(2004,10,31,1,30,0,0)
#
# Specifying `dst = true` would return a `Time` with a UTC offset of -4
# hours and abbreviation EDT (Eastern Daylight Time). Specifying `dst =
# false` would return a `Time` with a UTC offset of -5 hours and
# abbreviation EST (Eastern Standard Time).
#
# The `dst` parameter will not be able to resolve an ambiguity resulting
# from the clocks being set back without changing from daylight savings time
# to standard time. In this case, if a block is specified, it will be called
# to resolve the ambiguity. The block must take a single parameter - an
# `Array` of {TimezonePeriod}s that need to be resolved. The block can
# select and return a single {TimezonePeriod} or return `nil` or an empty
# `Array` to cause an {AmbiguousTime} exception to be raised.
#
# The default value of the `dst` parameter can be specified using
# {Timezone.default_dst=}.
#
# @param year [Integer] the year.
# @param month [Integer] the month (1-12).
# @param day [Integer] the day of the month (1-31).
# @param hour [Integer] the hour (0-23).
# @param minute [Integer] the minute (0-59).
# @param second [Integer] the second (0-59).
# @param sub_second [Numeric] the fractional part of the second as either
# a `Rational` that is greater than or equal to 0 and less than 1, or
# the `Integer` 0.
# @param dst [Boolean] whether to resolve ambiguous local times by always
# selecting the period observing daylight savings time (`true`), always
# selecting the period observing standard time (`false`), or leaving the
# ambiguity unresolved (`nil`).
# @yield [periods] if the `dst` parameter did not resolve an ambiguity, an
# optional block is yielded to.
# @yieldparam periods [Array<TimezonePeriod>] an `Array` containing all
# the {TimezonePeriod}s that still match `local_time` after applying the
# `dst` parameter.
# @yieldreturn [Object] to resolve the ambiguity: a chosen {TimezonePeriod}
# or an `Array` containing a chosen {TimezonePeriod}; to leave the
# ambiguity unresolved: an empty `Array`, an `Array` containing more than
# one {TimezonePeriod}, or `nil`.
# @return [DateTimeWithOffset] a new `DateTime` object based on the given
# values, interpreted as a local time in the time zone.
# @raise [ArgumentError] if either of `year`, `month`, `day`, `hour`,
# `minute`, or `second` is not an `Integer`.
# @raise [ArgumentError] if `sub_second` is not a `Rational`, or the
# `Integer` 0.
# @raise [ArgumentError] if `utc_offset` is not `nil`, not an `Integer`
# and not the `Symbol` `:utc`.
# @raise [RangeError] if `month` is not between 1 and 12.
# @raise [RangeError] if `day` is not between 1 and 31.
# @raise [RangeError] if `hour` is not between 0 and 23.
# @raise [RangeError] if `minute` is not between 0 and 59.
# @raise [RangeError] if `second` is not between 0 and 59.
# @raise [RangeError] if `sub_second` is a `Rational` but that is less
# than 0 or greater than or equal to 1.
# @raise [PeriodNotFound] if the date and time parameters do not specify a
# valid local time in the time zone.
# @raise [AmbiguousTime] if the date and time parameters are ambiguous for
# the time zone and the `dst` parameter or block did not resolve the
# ambiguity.
def local_datetime(year, month = 1, day = 1, hour = 0, minute = 0, second = 0, sub_second = 0, dst = Timezone.default_dst, &block)
local_timestamp(year, month, day, hour, minute, second, sub_second, dst, &block).to_datetime
end
# Creates a {Timestamp} object based on the given (Gregorian calendar) date
# and time parameters. The parameters are interpreted as a local time in the
# time zone. The result has the appropriate {Timestamp#utc_offset
# utc_offset} and {TimestampWithOffset#timezone_offset timezone_offset}.
#
# _Warning:_ There are time values that are not valid as local times in a
# time zone (for example, during the transition from standard time to
# daylight savings time). There are also time values that are ambiguous,
# occurring more than once with different offsets to UTC (for example,
# during the transition from daylight savings time to standard time).
#
# In the first case (an invalid local time), a {PeriodNotFound} exception
# will be raised.
#
# In the second case (more than one occurrence), an {AmbiguousTime}
# exception will be raised unless the optional `dst` parameter or block
# handles the ambiguity.
#
# If the ambiguity is due to a transition from daylight savings time to
# standard time, the `dst` parameter can be used to select whether the
# daylight savings time or local time is used. For example, the following
# code would raise an {AmbiguousTime} exception:
#
# tz = TZInfo::Timezone.get('America/New_York')
# tz.local_timestamp(2004,10,31,1,30,0,0)
#
# Specifying `dst = true` would return a `Time` with a UTC offset of -4
# hours and abbreviation EDT (Eastern Daylight Time). Specifying `dst =
# false` would return a `Time` with a UTC offset of -5 hours and
# abbreviation EST (Eastern Standard Time).
#
# The `dst` parameter will not be able to resolve an ambiguity resulting
# from the clocks being set back without changing from daylight savings time
# to standard time. In this case, if a block is specified, it will be called
# to resolve the ambiguity. The block must take a single parameter - an
# `Array` of {TimezonePeriod}s that need to be resolved. The block can
# select and return a single {TimezonePeriod} or return `nil` or an empty
# `Array` to cause an {AmbiguousTime} exception to be raised.
#
# The default value of the `dst` parameter can be specified using
# {Timezone.default_dst=}.
#
# @param year [Integer] the year.
# @param month [Integer] the month (1-12).
# @param day [Integer] the day of the month (1-31).
# @param hour [Integer] the hour (0-23).
# @param minute [Integer] the minute (0-59).
# @param second [Integer] the second (0-59).
# @param sub_second [Numeric] the fractional part of the second as either
# a `Rational` that is greater than or equal to 0 and less than 1, or
# the `Integer` 0.
# @param dst [Boolean] whether to resolve ambiguous local times by always
# selecting the period observing daylight savings time (`true`), always
# selecting the period observing standard time (`false`), or leaving the
# ambiguity unresolved (`nil`).
# @yield [periods] if the `dst` parameter did not resolve an ambiguity, an
# optional block is yielded to.
# @yieldparam periods [Array<TimezonePeriod>] an `Array` containing all
# the {TimezonePeriod}s that still match `local_time` after applying the
# `dst` parameter.
# @yieldreturn [Object] to resolve the ambiguity: a chosen {TimezonePeriod}
# or an `Array` containing a chosen {TimezonePeriod}; to leave the
# ambiguity unresolved: an empty `Array`, an `Array` containing more than
# one {TimezonePeriod}, or `nil`.
# @return [TimestampWithOffset] a new {Timestamp} object based on the given
# values, interpreted as a local time in the time zone.
# @raise [ArgumentError] if either of `year`, `month`, `day`, `hour`,
# `minute`, or `second` is not an `Integer`.
# @raise [ArgumentError] if `sub_second` is not a `Rational`, or the
# `Integer` 0.
# @raise [ArgumentError] if `utc_offset` is not `nil`, not an `Integer`
# and not the `Symbol` `:utc`.
# @raise [RangeError] if `month` is not between 1 and 12.
# @raise [RangeError] if `day` is not between 1 and 31.
# @raise [RangeError] if `hour` is not between 0 and 23.
# @raise [RangeError] if `minute` is not between 0 and 59.
# @raise [RangeError] if `second` is not between 0 and 59.
# @raise [RangeError] if `sub_second` is a `Rational` but that is less
# than 0 or greater than or equal to 1.
# @raise [PeriodNotFound] if the date and time parameters do not specify a
# valid local time in the time zone.
# @raise [AmbiguousTime] if the date and time parameters are ambiguous for
# the time zone and the `dst` parameter or block did not resolve the
# ambiguity.
def local_timestamp(year, month = 1, day = 1, hour = 0, minute = 0, second = 0, sub_second = 0, dst = Timezone.default_dst, &block)
ts = Timestamp.create(year, month, day, hour, minute, second, sub_second)
timezone_offset = period_for_local(ts, dst, &block).offset
utc_offset = timezone_offset.observed_utc_offset
TimestampWithOffset.new(ts.value - utc_offset, sub_second, utc_offset).set_timezone_offset(timezone_offset)
end
# Returns the unique offsets used by the time zone up to a given time (`to`)
# as an `Array` of {TimezoneOffset} instances.
#
# A from time may also be supplied using the `from` parameter. If from is
# not `nil`, only offsets used from that time onwards will be returned.
#
# Comparisons with `to` are exclusive. Comparisons with `from` are
# inclusive.
#
# @param to [Object] a `Time`, `DateTime` or {Timestamp} specifying the
# latest (exclusive) offset to return.
# @param from [Object] an optional `Time`, `DateTime` or {Timestamp}
# specifying the earliest (inclusive) offset to return.
# @return [Array<TimezoneOffsets>] the offsets that are used earlier than
# `to` and, if specified, at or later than `from`. Offsets may be returned
# in any order.
# @raise [ArgumentError] if `from` is specified and `to` is not greater than
# `from`.
# @raise [ArgumentError] is raised if `to` is `nil`.
# @raise [ArgumentError] if either `to` or `from` is a {Timestamp} with an
# unspecified offset.
def offsets_up_to(to, from = nil)
raise ArgumentError, 'to must be specified' unless to
to_timestamp = Timestamp.for(to)
from_timestamp = from && Timestamp.for(from)
transitions = transitions_up_to(to_timestamp, from_timestamp)
if transitions.empty?
# No transitions in the range, find the period that covers it.
if from_timestamp
# Use the from date as it is inclusive.
period = period_for(from_timestamp)
else
# to is exclusive, so this can't be used with period_for. However, any
# time earlier than to can be used. Subtract 1 hour.
period = period_for(to_timestamp.add_and_set_utc_offset(-3600, :utc))
end
[period.offset]
else
result = Set.new
first = transitions.first
result << first.previous_offset unless from_timestamp && first.at == from_timestamp
transitions.each do |t|
result << t.offset
end
result.to_a
end
end
# Returns the canonical identifier of this time zone.
#
# This is a shortcut for calling `canonical_zone.identifier`. Please refer
# to the {canonical_zone} documentation for further information.
#
# @return [String] the canonical identifier of this time zone.
def canonical_identifier
canonical_zone.identifier
end
# @return [TimeWithOffset] the current local time in the time zone.
def now
to_local(Time.now)
end
# @return [TimezonePeriod] the current {TimezonePeriod} for the time zone.
def current_period
period_for(Time.now)
end