tzinfo 1.2.7 → 1.2.10

Sign up to get free protection for your applications and to get access to all the features.
Files changed (61) hide show
  1. checksums.yaml +4 -4
  2. checksums.yaml.gz.sig +2 -3
  3. data/CHANGES.md +30 -0
  4. data/LICENSE +1 -1
  5. data/README.md +1 -1
  6. data/lib/tzinfo/annual_rules.rb +51 -0
  7. data/lib/tzinfo/posix_time_zone_parser.rb +136 -0
  8. data/lib/tzinfo/ruby_data_source.rb +1 -1
  9. data/lib/tzinfo/time_or_datetime.rb +11 -0
  10. data/lib/tzinfo/transition_rule.rb +325 -0
  11. data/lib/tzinfo/zoneinfo_data_source.rb +27 -11
  12. data/lib/tzinfo/zoneinfo_timezone_info.rb +260 -40
  13. data/lib/tzinfo.rb +3 -0
  14. data/test/assets/payload.rb +1 -0
  15. data/test/tc_annual_rules.rb +95 -0
  16. data/test/tc_posix_time_zone_parser.rb +261 -0
  17. data/test/tc_ruby_data_source.rb +7 -1
  18. data/test/tc_time_or_datetime.rb +14 -0
  19. data/test/tc_timezone.rb +1 -1
  20. data/test/tc_transition_rule.rb +663 -0
  21. data/test/tc_zoneinfo_data_source.rb +20 -1
  22. data/test/tc_zoneinfo_timezone_info.rb +1030 -113
  23. data/test/test_utils.rb +16 -0
  24. data/test/tzinfo-data/tzinfo/data/definitions/America/Argentina/Buenos_Aires.rb +5 -5
  25. data/test/tzinfo-data/tzinfo/data/definitions/America/New_York.rb +13 -1
  26. data/test/tzinfo-data/tzinfo/data/definitions/Australia/Melbourne.rb +13 -1
  27. data/test/tzinfo-data/tzinfo/data/definitions/EST.rb +1 -1
  28. data/test/tzinfo-data/tzinfo/data/definitions/Etc/GMT__m__1.rb +2 -2
  29. data/test/tzinfo-data/tzinfo/data/definitions/Etc/GMT__p__1.rb +2 -2
  30. data/test/tzinfo-data/tzinfo/data/definitions/Etc/UTC.rb +1 -1
  31. data/test/tzinfo-data/tzinfo/data/definitions/Europe/Amsterdam.rb +15 -3
  32. data/test/tzinfo-data/tzinfo/data/definitions/Europe/Andorra.rb +13 -1
  33. data/test/tzinfo-data/tzinfo/data/definitions/Europe/London.rb +13 -1
  34. data/test/tzinfo-data/tzinfo/data/definitions/Europe/Paris.rb +15 -3
  35. data/test/tzinfo-data/tzinfo/data/definitions/Europe/Prague.rb +19 -4
  36. data/test/tzinfo-data/tzinfo/data/definitions/UTC.rb +1 -1
  37. data/test/tzinfo-data/tzinfo/data/indexes/countries.rb +197 -184
  38. data/test/tzinfo-data/tzinfo/data/indexes/timezones.rb +60 -47
  39. data/test/tzinfo-data/tzinfo/data/version.rb +9 -3
  40. data/test/zoneinfo/America/Argentina/Buenos_Aires +0 -0
  41. data/test/zoneinfo/America/New_York +0 -0
  42. data/test/zoneinfo/Australia/Melbourne +0 -0
  43. data/test/zoneinfo/EST +0 -0
  44. data/test/zoneinfo/Etc/UTC +0 -0
  45. data/test/zoneinfo/Europe/Amsterdam +0 -0
  46. data/test/zoneinfo/Europe/Andorra +0 -0
  47. data/test/zoneinfo/Europe/London +0 -0
  48. data/test/zoneinfo/Europe/Paris +0 -0
  49. data/test/zoneinfo/Europe/Prague +0 -0
  50. data/test/zoneinfo/Factory +0 -0
  51. data/test/zoneinfo/iso3166.tab +13 -14
  52. data/test/zoneinfo/leapseconds +38 -21
  53. data/test/zoneinfo/posix/Europe/London +0 -0
  54. data/test/zoneinfo/posixrules +0 -0
  55. data/test/zoneinfo/right/Europe/London +0 -0
  56. data/test/zoneinfo/zone.tab +172 -159
  57. data/test/zoneinfo/zone1970.tab +185 -170
  58. data/tzinfo.gemspec +1 -1
  59. data.tar.gz.sig +0 -0
  60. metadata +10 -3
  61. metadata.gz.sig +0 -0
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: c5b964ab83943597cb8ce6f141378398fe1bc4fe4b767dd9434f04030332b008
4
- data.tar.gz: bcdda83a2bfc8e90d0f62a8ddab56f6de246c2c44b64cc00347838f64fc28a87
3
+ metadata.gz: e6364432a0aef34ccf3b6b1ecad65dd6f7f13843ac503cbcea1f693b74c96b46
4
+ data.tar.gz: 825fd6905101f51fa700dfa682490851952de8a692c03954d12f38944f8814c8
5
5
  SHA512:
6
- metadata.gz: 7921a1c86b6307ee7b554edc6de33d2ed07ca8660c804f3ca78099485a72e26bcd23af498a955dfd7dfa5efb0c2f95e6adbdadbd0d60ef492732f1fdfd79f0d9
7
- data.tar.gz: a0b59904238b08bbebc38a359db873dfadc0a98ef7a2fb6241f91219c642432dc8ae643ff0cf844d16bae0ef61173bac16c5f69863cde60fe504105d4240f48f
6
+ metadata.gz: ef4b1b6a189bbf011294210d2e0651f41bc82e1db8fe342c9f8dbcefd473e8b49b9affa67bc9a395a5831b376db8d37b5942cfade1dacf5485f23ce3d6f78a46
7
+ data.tar.gz: 2871fbd7aded391c88a74724138073675690710dfca6adbbbe610ec4395e8d6631fad93b22d684650d04d9affeed0ab64a1d7489f766eb9ab1996556329c6ddc
checksums.yaml.gz.sig CHANGED
@@ -1,3 +1,2 @@
1
- \*��81l.��V��O8rx�H�'��D9[4��DrK4jO��HO��p_QK���_�)��w& C���(����OF&Q�b
2
- ��pL5!��l��ψè���2r4iq&�86��MX����Ƭ��K_W�#�!����]\�@,ބ3��,H�?���'̙V�6����̰������"͗�¡�#C��|f:��*�X(���a�Í�
3
-  ѿs!�BS���H��3�yD��q�X�g�׌
1
+ Y�1]Q�ti�t���mPo���ڊ -O��Dcu���[<��oͽc5�}�x`��[^�J?7��s����+��ȶ��Mv��ǐ�� 9e���l9J��ۑ�3?e�V~�E����6E����Eb�)��xdTk�^�BdAC?�����=Jcr�%�����l�~��)�aPʃ\�=[ݪ��{l��fFBݦ��]�_����<vS��0��7�z|�-�$���ؑ����^ڐ��(�ReaKs�|�c��d
2
+ K�W��
data/CHANGES.md CHANGED
@@ -1,3 +1,33 @@
1
+ Version 1.2.10 - 19-Jul-2022
2
+ ----------------------------
3
+
4
+ * Fixed a relative path traversal bug that could cause arbitrary files to be
5
+ loaded with require when used with RubyDataSource. Please refer to
6
+ https://github.com/tzinfo/tzinfo/security/advisories/GHSA-5cm2-9h8c-rvfx for
7
+ details. CVE-2022-31163.
8
+ * Ignore the SECURITY file from Arch Linux's tzdata package. #134.
9
+
10
+
11
+ Version 1.2.9 - 16-Dec-2020
12
+ ---------------------------
13
+
14
+ * Fixed an incorrect InvalidTimezoneIdentifier exception raised when loading a
15
+ zoneinfo file that includes rules specifying an additional transition to the
16
+ final defined offset (for example, Africa/Casablanca in version 2018e of the
17
+ Time Zone Database). #123.
18
+
19
+
20
+ Version 1.2.8 - 8-Nov-2020
21
+ --------------------------
22
+
23
+ * Added support for handling "slim" format zoneinfo files that are produced by
24
+ default by zic version 2020b and later. The POSIX-style TZ string is now used
25
+ calculate DST transition times after the final defined transition in the file.
26
+ The 64-bit section is now always used regardless of whether Time has support
27
+ for 64-bit times. #120.
28
+ * Rubinius is no longer supported.
29
+
30
+
1
31
  Version 1.2.7 - 2-Apr-2020
2
32
  --------------------------
3
33
 
data/LICENSE CHANGED
@@ -1,4 +1,4 @@
1
- Copyright (c) 2005-2020 Philip Ross
1
+ Copyright (c) 2005-2022 Philip Ross
2
2
 
3
3
  Permission is hereby granted, free of charge, to any person obtaining a copy of
4
4
  this software and associated documentation files (the "Software"), to deal in
data/README.md CHANGED
@@ -1,7 +1,7 @@
1
1
  TZInfo - Ruby Timezone Library
2
2
  ==============================
3
3
 
4
- [![RubyGems](https://img.shields.io/gem/v/tzinfo)](https://rubygems.org/gems/tzinfo) [![Travis CI Build](https://img.shields.io/travis/tzinfo/tzinfo/1.2?logo=travis)](https://travis-ci.org/tzinfo/tzinfo) [![AppVeyor Build](https://img.shields.io/appveyor/build/philr/tzinfo/1.2?logo=appveyor)](https://ci.appveyor.com/project/philr/tzinfo/branch/1.2)
4
+ [![RubyGems](https://img.shields.io/gem/v/tzinfo?logo=rubygems&label=Gem)](https://rubygems.org/gems/tzinfo) [![Tests](https://github.com/tzinfo/tzinfo/workflows/Tests/badge.svg?branch=1.2&event=push)](https://github.com/tzinfo/tzinfo/actions?query=workflow%3ATests+branch%3A1.2+event%3Apush)
5
5
 
6
6
  [TZInfo](https://tzinfo.github.io) provides daylight savings aware
7
7
  transformations between times in different timezones.
@@ -0,0 +1,51 @@
1
+ module TZInfo
2
+ # A set of rules that define when transitions occur in time zones with
3
+ # annually occurring daylight savings time.
4
+ #
5
+ # @private
6
+ class AnnualRules #:nodoc:
7
+ # Returned by #transitions. #offset is the TimezoneOffset that applies
8
+ # from the UTC TimeOrDateTime #at. #previous_offset is the prior
9
+ # TimezoneOffset.
10
+ Transition = Struct.new(:offset, :previous_offset, :at)
11
+
12
+ # The standard offset that applies when daylight savings time is not in
13
+ # force.
14
+ attr_reader :std_offset
15
+
16
+ # The offset that applies when daylight savings time is in force.
17
+ attr_reader :dst_offset
18
+
19
+ # The rule that determines when daylight savings time starts.
20
+ attr_reader :dst_start_rule
21
+
22
+ # The rule that determines when daylight savings time ends.
23
+ attr_reader :dst_end_rule
24
+
25
+ # Initializes a new {AnnualRules} instance.
26
+ def initialize(std_offset, dst_offset, dst_start_rule, dst_end_rule)
27
+ @std_offset = std_offset
28
+ @dst_offset = dst_offset
29
+ @dst_start_rule = dst_start_rule
30
+ @dst_end_rule = dst_end_rule
31
+ end
32
+
33
+ # Returns the transitions between standard and daylight savings time for a
34
+ # given year. The results are ordered by time of occurrence (earliest to
35
+ # latest).
36
+ def transitions(year)
37
+ start_dst = apply_rule(@dst_start_rule, @std_offset, @dst_offset, year)
38
+ end_dst = apply_rule(@dst_end_rule, @dst_offset, @std_offset, year)
39
+
40
+ end_dst.at < start_dst.at ? [end_dst, start_dst] : [start_dst, end_dst]
41
+ end
42
+
43
+ private
44
+
45
+ # Applies a given rule between offsets on a year.
46
+ def apply_rule(rule, from_offset, to_offset, year)
47
+ at = rule.at(from_offset, year)
48
+ Transition.new(to_offset, from_offset, at)
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,136 @@
1
+ # encoding: UTF-8
2
+ # frozen_string_literal: true
3
+
4
+ require 'strscan'
5
+
6
+ module TZInfo
7
+ # An {InvalidPosixTimeZone} exception is raised if an invalid POSIX-style
8
+ # time zone string is encountered.
9
+ #
10
+ # @private
11
+ class InvalidPosixTimeZone < StandardError #:nodoc:
12
+ end
13
+
14
+ # A parser for POSIX-style TZ strings used in zoneinfo files and specified
15
+ # by tzfile.5 and tzset.3.
16
+ #
17
+ # @private
18
+ class PosixTimeZoneParser #:nodoc:
19
+ # Parses a POSIX-style TZ string, returning either a TimezoneOffset or
20
+ # an AnnualRules instance.
21
+ def parse(tz_string)
22
+ raise InvalidPosixTimeZone unless tz_string.kind_of?(String)
23
+ return nil if tz_string.empty?
24
+
25
+ s = StringScanner.new(tz_string)
26
+ check_scan(s, /([^-+,\d<][^-+,\d]*) | <([^>]+)>/x)
27
+ std_abbrev = s[1] || s[2]
28
+ check_scan(s, /([-+]?\d+)(?::(\d+)(?::(\d+))?)?/)
29
+ std_offset = get_offset_from_hms(s[1], s[2], s[3])
30
+
31
+ if s.scan(/([^-+,\d<][^-+,\d]*) | <([^>]+)>/x)
32
+ dst_abbrev = s[1] || s[2]
33
+
34
+ if s.scan(/([-+]?\d+)(?::(\d+)(?::(\d+))?)?/)
35
+ dst_offset = get_offset_from_hms(s[1], s[2], s[3])
36
+ else
37
+ # POSIX is negative for ahead of UTC.
38
+ dst_offset = std_offset - 3600
39
+ end
40
+
41
+ dst_difference = std_offset - dst_offset
42
+
43
+ start_rule = parse_rule(s, 'start')
44
+ end_rule = parse_rule(s, 'end')
45
+
46
+ raise InvalidPosixTimeZone, "Expected the end of a POSIX-style time zone string but found '#{s.rest}'." if s.rest?
47
+
48
+ if start_rule.is_always_first_day_of_year? && start_rule.transition_at == 0 &&
49
+ end_rule.is_always_last_day_of_year? && end_rule.transition_at == 86400 + dst_difference
50
+ # Constant daylight savings time.
51
+ # POSIX is negative for ahead of UTC.
52
+ TimezoneOffset.new(-std_offset, dst_difference, dst_abbrev.to_sym)
53
+ else
54
+ AnnualRules.new(
55
+ TimezoneOffset.new(-std_offset, 0, std_abbrev.to_sym),
56
+ TimezoneOffset.new(-std_offset, dst_difference, dst_abbrev.to_sym),
57
+ start_rule,
58
+ end_rule)
59
+ end
60
+ elsif !s.rest?
61
+ # Constant standard time.
62
+ # POSIX is negative for ahead of UTC.
63
+ TimezoneOffset.new(-std_offset, 0, std_abbrev.to_sym)
64
+ else
65
+ raise InvalidPosixTimeZone, "Expected the end of a POSIX-style time zone string but found '#{s.rest}'."
66
+ end
67
+ end
68
+
69
+ private
70
+
71
+ # Parses the rule from the TZ string, returning a TransitionRule.
72
+ def parse_rule(s, type)
73
+ check_scan(s, /,(?: (?: J(\d+) ) | (\d+) | (?: M(\d+)\.(\d)\.(\d) ) )/x)
74
+ julian_day_of_year = s[1]
75
+ absolute_day_of_year = s[2]
76
+ month = s[3]
77
+ week = s[4]
78
+ day_of_week = s[5]
79
+
80
+ if s.scan(/\//)
81
+ check_scan(s, /([-+]?\d+)(?::(\d+)(?::(\d+))?)?/)
82
+ transition_at = get_seconds_after_midnight_from_hms(s[1], s[2], s[3])
83
+ else
84
+ transition_at = 7200
85
+ end
86
+
87
+ begin
88
+ if julian_day_of_year
89
+ JulianDayOfYearTransitionRule.new(julian_day_of_year.to_i, transition_at)
90
+ elsif absolute_day_of_year
91
+ AbsoluteDayOfYearTransitionRule.new(absolute_day_of_year.to_i, transition_at)
92
+ elsif week == '5'
93
+ LastDayOfMonthTransitionRule.new(month.to_i, day_of_week.to_i, transition_at)
94
+ else
95
+ DayOfMonthTransitionRule.new(month.to_i, week.to_i, day_of_week.to_i, transition_at)
96
+ end
97
+ rescue ArgumentError => e
98
+ raise InvalidPosixTimeZone, "Invalid #{type} rule in POSIX-style time zone string: #{e}"
99
+ end
100
+ end
101
+
102
+ # Returns an offset in seconds from hh:mm:ss values. The value can be
103
+ # negative. -02:33:12 would represent 2 hours, 33 minutes and 12 seconds
104
+ # ahead of UTC.
105
+ def get_offset_from_hms(h, m, s)
106
+ h = h.to_i
107
+ m = m.to_i
108
+ s = s.to_i
109
+ raise InvalidPosixTimeZone, "Invalid minute #{m} in offset for POSIX-style time zone string." if m > 59
110
+ raise InvalidPosixTimeZone, "Invalid second #{s} in offset for POSIX-style time zone string." if s > 59
111
+ magnitude = (h.abs * 60 + m) * 60 + s
112
+ h < 0 ? -magnitude : magnitude
113
+ end
114
+
115
+ # Returns the seconds from midnight from hh:mm:ss values. Hours can exceed
116
+ # 24 for a time on the following day. Hours can be negative to subtract
117
+ # hours from midnight on the given day. -02:33:12 represents 22:33:12 on
118
+ # the prior day.
119
+ def get_seconds_after_midnight_from_hms(h, m, s)
120
+ h = h.to_i
121
+ m = m.to_i
122
+ s = s.to_i
123
+ raise InvalidPosixTimeZone, "Invalid minute #{m} in time for POSIX-style time zone string." if m > 59
124
+ raise InvalidPosixTimeZone, "Invalid second #{s} in time for POSIX-style time zone string." if s > 59
125
+ (h * 3600) + m * 60 + s
126
+ end
127
+
128
+ # Scans for a pattern and raises an exception if the pattern does not
129
+ # match the input.
130
+ def check_scan(s, pattern)
131
+ result = s.scan(pattern)
132
+ raise InvalidPosixTimeZone, "Expected '#{s.rest}' to match #{pattern} in POSIX-style time zone string." unless result
133
+ result
134
+ end
135
+ end
136
+ end
@@ -38,7 +38,7 @@ module TZInfo
38
38
  # Raises InvalidTimezoneIdentifier if the timezone is not found or the
39
39
  # identifier is invalid.
40
40
  def load_timezone_info(identifier)
41
- raise InvalidTimezoneIdentifier, 'Invalid identifier' if identifier !~ /^[A-Za-z0-9\+\-_]+(\/[A-Za-z0-9\+\-_]+)*$/
41
+ raise InvalidTimezoneIdentifier, 'Invalid identifier' if identifier !~ /\A[A-Za-z0-9+\-_]+(\/[A-Za-z0-9+\-_]+)*\z/
42
42
 
43
43
  identifier = identifier.gsub(/-/, '__m__').gsub(/\+/, '__p__')
44
44
 
@@ -160,6 +160,17 @@ module TZInfo
160
160
  end
161
161
  end
162
162
  alias :day :mday
163
+
164
+ # Returns the day of the week (0..6 for Sunday to Saturday).
165
+ def wday
166
+ if @time
167
+ @time.wday
168
+ elsif @datetime
169
+ @datetime.wday
170
+ else
171
+ to_time.wday
172
+ end
173
+ end
163
174
 
164
175
  # Returns the hour of the day (0..23).
165
176
  def hour
@@ -0,0 +1,325 @@
1
+ require 'date'
2
+
3
+ module TZInfo
4
+ # Base class for rules definining the transition between standard and daylight
5
+ # savings time.
6
+ class TransitionRule #:nodoc:
7
+ # Returns the number of seconds after midnight local time on the day
8
+ # identified by the rule at which the transition occurs. Can be negative to
9
+ # denote a time on the prior day. Can be greater than or equal to 86,400 to
10
+ # denote a time of the following day.
11
+ attr_reader :transition_at
12
+
13
+ # Initializes a new TransitionRule.
14
+ def initialize(transition_at)
15
+ raise ArgumentError, 'Invalid transition_at' unless transition_at.kind_of?(Integer)
16
+ @transition_at = transition_at
17
+ end
18
+
19
+ # Calculates the UTC time of the transition from a given offset on a given
20
+ # year.
21
+ def at(offset, year)
22
+ day = get_day(year)
23
+ day.add_with_convert(@transition_at - offset.utc_total_offset)
24
+ end
25
+
26
+ # Determines if this TransitionRule is equal to another instance.
27
+ def ==(r)
28
+ r.kind_of?(TransitionRule) && @transition_at == r.transition_at
29
+ end
30
+ alias eql? ==
31
+
32
+ # Returns a hash based on hash_args (defaulting to transition_at).
33
+ def hash
34
+ hash_args.hash
35
+ end
36
+
37
+ protected
38
+
39
+ # Returns an Array of parameters that will influence the output of hash.
40
+ def hash_args
41
+ [@transition_at]
42
+ end
43
+
44
+ def new_time_or_datetime(year, month = 1, day = 1)
45
+ result = if ((year >= 2039 || (year == 2038 && (month >= 2 || (month == 1 && day >= 20)))) && !RubyCoreSupport.time_supports_64bit) ||
46
+ (year < 1970 && !RubyCoreSupport.time_supports_negative)
47
+
48
+ # Time handles 29 February on a non-leap year as 1 March.
49
+ # DateTime rejects. Advance manually.
50
+ if month == 2 && day == 29 && !Date.gregorian_leap?(year)
51
+ month = 3
52
+ day = 1
53
+ end
54
+
55
+ RubyCoreSupport.datetime_new(year, month, day)
56
+ else
57
+ Time.utc(year, month, day)
58
+ end
59
+
60
+ TimeOrDateTime.wrap(result)
61
+ end
62
+ end
63
+
64
+ # A base class for transition rules that activate based on an integer day of
65
+ # the year.
66
+ #
67
+ # @private
68
+ class DayOfYearTransitionRule < TransitionRule #:nodoc:
69
+ # Initializes a new DayOfYearTransitionRule.
70
+ def initialize(day, transition_at)
71
+ super(transition_at)
72
+ raise ArgumentError, 'Invalid day' unless day.kind_of?(Integer)
73
+ @seconds = day * 86400
74
+ end
75
+
76
+ # Determines if this DayOfYearTransitionRule is equal to another instance.
77
+ def ==(r)
78
+ super(r) && r.kind_of?(DayOfYearTransitionRule) && @seconds == r.seconds
79
+ end
80
+ alias eql? ==
81
+
82
+ protected
83
+
84
+ # @return [Integer] the day multipled by the number of seconds in a day.
85
+ attr_reader :seconds
86
+
87
+ # Returns an Array of parameters that will influence the output of hash.
88
+ def hash_args
89
+ [@seconds] + super
90
+ end
91
+ end
92
+
93
+ # Defines transitions that occur on the zero-based nth day of the year.
94
+ #
95
+ # Day 0 is 1 January.
96
+ #
97
+ # Leap days are counted. Day 59 will be 29 February on a leap year and 1 March
98
+ # on a non-leap year. Day 365 will be 31 December on a leap year and 1 January
99
+ # the following year on a non-leap year.
100
+ #
101
+ # @private
102
+ class AbsoluteDayOfYearTransitionRule < DayOfYearTransitionRule #:nodoc:
103
+ # Initializes a new AbsoluteDayOfYearTransitionRule.
104
+ def initialize(day, transition_at = 0)
105
+ super(day, transition_at)
106
+ raise ArgumentError, 'Invalid day' unless day >= 0 && day <= 365
107
+ end
108
+
109
+ # Returns true if the day specified by this transition is the first in the
110
+ # year (a day number of 0), otherwise false.
111
+ def is_always_first_day_of_year?
112
+ seconds == 0
113
+ end
114
+
115
+ # @returns false.
116
+ def is_always_last_day_of_year?
117
+ false
118
+ end
119
+
120
+ # Determines if this AbsoluteDayOfYearTransitionRule is equal to another
121
+ # instance.
122
+ def ==(r)
123
+ super(r) && r.kind_of?(AbsoluteDayOfYearTransitionRule)
124
+ end
125
+ alias eql? ==
126
+
127
+ protected
128
+
129
+ # Returns a TimeOrDateTime representing midnight local time on the day
130
+ # specified by the rule for the given offset and year.
131
+ def get_day(year)
132
+ new_time_or_datetime(year).add_with_convert(seconds)
133
+ end
134
+
135
+ # Returns an Array of parameters that will influence the output of hash.
136
+ def hash_args
137
+ [AbsoluteDayOfYearTransitionRule] + super
138
+ end
139
+ end
140
+
141
+ # Defines transitions that occur on the one-based nth Julian day of the year.
142
+ #
143
+ # Leap days are not counted. Day 1 is 1 January. Day 60 is always 1 March.
144
+ # Day 365 is always 31 December.
145
+ #
146
+ # @private
147
+ class JulianDayOfYearTransitionRule < DayOfYearTransitionRule #:nodoc:
148
+ # The 60 days in seconds.
149
+ LEAP = 60 * 86400
150
+
151
+ # The length of a non-leap year in seconds.
152
+ YEAR = 365 * 86400
153
+
154
+ # Initializes a new JulianDayOfYearTransitionRule.
155
+ def initialize(day, transition_at = 0)
156
+ super(day, transition_at)
157
+ raise ArgumentError, 'Invalid day' unless day >= 1 && day <= 365
158
+ end
159
+
160
+ # Returns true if the day specified by this transition is the first in the
161
+ # year (a day number of 1), otherwise false.
162
+ def is_always_first_day_of_year?
163
+ seconds == 86400
164
+ end
165
+
166
+ # Returns true if the day specified by this transition is the last in the
167
+ # year (a day number of 365), otherwise false.
168
+ def is_always_last_day_of_year?
169
+ seconds == YEAR
170
+ end
171
+
172
+ # Determines if this JulianDayOfYearTransitionRule is equal to another
173
+ # instance.
174
+ def ==(r)
175
+ super(r) && r.kind_of?(JulianDayOfYearTransitionRule)
176
+ end
177
+ alias eql? ==
178
+
179
+ protected
180
+
181
+ # Returns a TimeOrDateTime representing midnight local time on the day
182
+ # specified by the rule for the given offset and year.
183
+ def get_day(year)
184
+ # Returns 1 March on non-leap years.
185
+ leap = new_time_or_datetime(year, 2, 29)
186
+ diff = seconds - LEAP
187
+ diff += 86400 if diff >= 0 && leap.mday == 29
188
+ leap.add_with_convert(diff)
189
+ end
190
+
191
+ # Returns an Array of parameters that will influence the output of hash.
192
+ def hash_args
193
+ [JulianDayOfYearTransitionRule] + super
194
+ end
195
+ end
196
+
197
+ # A base class for rules that transition on a particular day of week of a
198
+ # given week (subclasses specify which week of the month).
199
+ #
200
+ # @private
201
+ class DayOfWeekTransitionRule < TransitionRule #:nodoc:
202
+ # Initializes a new DayOfWeekTransitionRule.
203
+ def initialize(month, day_of_week, transition_at)
204
+ super(transition_at)
205
+ raise ArgumentError, 'Invalid month' unless month.kind_of?(Integer) && month >= 1 && month <= 12
206
+ raise ArgumentError, 'Invalid day_of_week' unless day_of_week.kind_of?(Integer) && day_of_week >= 0 && day_of_week <= 6
207
+ @month = month
208
+ @day_of_week = day_of_week
209
+ end
210
+
211
+ # Returns false.
212
+ def is_always_first_day_of_year?
213
+ false
214
+ end
215
+
216
+ # Returns false.
217
+ def is_always_last_day_of_year?
218
+ false
219
+ end
220
+
221
+ # Determines if this DayOfWeekTransitionRule is equal to another instance.
222
+ def ==(r)
223
+ super(r) && r.kind_of?(DayOfWeekTransitionRule) && @month == r.month && @day_of_week == r.day_of_week
224
+ end
225
+ alias eql? ==
226
+
227
+ protected
228
+
229
+ # Returns the month of the year (1 to 12).
230
+ attr_reader :month
231
+
232
+ # Returns the day of the week (0 to 6 for Sunday to Monday).
233
+ attr_reader :day_of_week
234
+
235
+ # Returns an Array of parameters that will influence the output of hash.
236
+ def hash_args
237
+ [@month, @day_of_week] + super
238
+ end
239
+ end
240
+
241
+ # A rule that transitions on the nth occurrence of a particular day of week
242
+ # of a calendar month.
243
+ #
244
+ # @private
245
+ class DayOfMonthTransitionRule < DayOfWeekTransitionRule #:nodoc:
246
+ # Initializes a new DayOfMonthTransitionRule.
247
+ def initialize(month, week, day_of_week, transition_at = 0)
248
+ super(month, day_of_week, transition_at)
249
+ raise ArgumentError, 'Invalid week' unless week.kind_of?(Integer) && week >= 1 && week <= 4
250
+ @offset_start = (week - 1) * 7 + 1
251
+ end
252
+
253
+ # Determines if this DayOfMonthTransitionRule is equal to another instance.
254
+ def ==(r)
255
+ super(r) && r.kind_of?(DayOfMonthTransitionRule) && @offset_start == r.offset_start
256
+ end
257
+ alias eql? ==
258
+
259
+ protected
260
+
261
+ # Returns the day the week starts on for a month starting on a Sunday.
262
+ attr_reader :offset_start
263
+
264
+ # Returns a TimeOrDateTime representing midnight local time on the day
265
+ # specified by the rule for the given offset and year.
266
+ def get_day(year)
267
+ candidate = new_time_or_datetime(year, month, @offset_start)
268
+ diff = day_of_week - candidate.wday
269
+
270
+ if diff < 0
271
+ candidate.add_with_convert((7 + diff) * 86400)
272
+ elsif diff > 0
273
+ candidate.add_with_convert(diff * 86400)
274
+ else
275
+ candidate
276
+ end
277
+ end
278
+
279
+ # Returns an Array of parameters that will influence the output of hash.
280
+ def hash_args
281
+ [@offset_start] + super
282
+ end
283
+ end
284
+
285
+ # A rule that transitions on the last occurrence of a particular day of week
286
+ # of a calendar month.
287
+ #
288
+ # @private
289
+ class LastDayOfMonthTransitionRule < DayOfWeekTransitionRule #:nodoc:
290
+ # Initializes a new LastDayOfMonthTransitionRule.
291
+ def initialize(month, day_of_week, transition_at = 0)
292
+ super(month, day_of_week, transition_at)
293
+ end
294
+
295
+ # Determines if this LastDayOfMonthTransitionRule is equal to another
296
+ # instance.
297
+ def ==(r)
298
+ super(r) && r.kind_of?(LastDayOfMonthTransitionRule)
299
+ end
300
+ alias eql? ==
301
+
302
+ protected
303
+
304
+ # Returns a TimeOrDateTime representing midnight local time on the day
305
+ # specified by the rule for the given offset and year.
306
+ def get_day(year)
307
+ next_month = month + 1
308
+ if next_month == 13
309
+ year += 1
310
+ next_month = 1
311
+ end
312
+
313
+ candidate = new_time_or_datetime(year, next_month).add_with_convert(-86400)
314
+ diff = candidate.wday - day_of_week
315
+
316
+ if diff < 0
317
+ candidate - (diff + 7) * 86400
318
+ elsif diff > 0
319
+ candidate - diff * 86400
320
+ else
321
+ candidate
322
+ end
323
+ end
324
+ end
325
+ end
@@ -87,6 +87,29 @@ module TZInfo
87
87
  # The default value of ZoneinfoDataSource.alternate_iso3166_tab_search_path.
88
88
  DEFAULT_ALTERNATE_ISO3166_TAB_SEARCH_PATH = ['/usr/share/misc/iso3166.tab', '/usr/share/misc/iso3166'].freeze
89
89
 
90
+ # File and directories in the top level zoneinfo directory that will be
91
+ # excluded from the list of available time zones:
92
+ #
93
+ # - +VERSION is included on Mac OS X.
94
+ # - leapseconds is a list of leap seconds.
95
+ # - localtime is the current local timezone (may be a link).
96
+ # - posix, posixrules and right are directories containing other versions
97
+ # of the zoneinfo files.
98
+ # - SECURITY is included in the Arch Linux tzdata package.
99
+ # - src is a directory containing the tzdata source included on Solaris.
100
+ # - timeconfig is a symlink included on Slackware.
101
+ EXCLUDED_FILENAMES = [
102
+ '+VERSION',
103
+ 'leapseconds',
104
+ 'localtime',
105
+ 'posix',
106
+ 'posixrules',
107
+ 'right',
108
+ 'SECURITY',
109
+ 'src',
110
+ 'timeconfig'
111
+ ].freeze
112
+
90
113
  # Paths to be checked to find the system zoneinfo directory.
91
114
  @@search_path = DEFAULT_SEARCH_PATH.dup
92
115
 
@@ -192,6 +215,7 @@ module TZInfo
192
215
  @zoneinfo_dir = File.expand_path(@zoneinfo_dir).freeze
193
216
  @timezone_index = load_timezone_index.freeze
194
217
  @country_index = load_country_index(iso3166_tab_path, zone_tab_path).freeze
218
+ @posix_tz_parser = PosixTimeZoneParser.new
195
219
  end
196
220
 
197
221
  # Returns a TimezoneInfo instance for a given identifier.
@@ -208,7 +232,7 @@ module TZInfo
208
232
  path.untaint
209
233
 
210
234
  begin
211
- ZoneinfoTimezoneInfo.new(identifier, path)
235
+ ZoneinfoTimezoneInfo.new(identifier, path, @posix_tz_parser)
212
236
  rescue InvalidZoneinfoFile => e
213
237
  raise InvalidTimezoneIdentifier, e.message
214
238
  end
@@ -351,16 +375,8 @@ module TZInfo
351
375
  # identifiers.
352
376
  def load_timezone_index
353
377
  index = []
354
-
355
- # Ignoring particular files:
356
- # +VERSION is included on Mac OS X.
357
- # leapseconds is a list of leap seconds.
358
- # localtime is the current local timezone (may be a link).
359
- # posix, posixrules and right are directories containing other versions of the zoneinfo files.
360
- # src is a directory containing the tzdata source included on Solaris.
361
- # timeconfig is a symlink included on Slackware.
362
-
363
- enum_timezones(nil, ['+VERSION', 'leapseconds', 'localtime', 'posix', 'posixrules', 'right', 'src', 'timeconfig']) do |identifier|
378
+
379
+ enum_timezones(nil, EXCLUDED_FILENAMES) do |identifier|
364
380
  index << identifier
365
381
  end
366
382