tzinfo 1.2.7 → 1.2.8

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


This version of tzinfo might be problematic. Click here for more details.

Files changed (54) hide show
  1. checksums.yaml +4 -4
  2. checksums.yaml.gz.sig +0 -0
  3. data.tar.gz.sig +0 -0
  4. data/CHANGES.md +11 -0
  5. data/README.md +1 -1
  6. data/lib/tzinfo.rb +3 -0
  7. data/lib/tzinfo/annual_rules.rb +51 -0
  8. data/lib/tzinfo/posix_time_zone_parser.rb +136 -0
  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 +2 -1
  12. data/lib/tzinfo/zoneinfo_timezone_info.rb +255 -40
  13. data/test/tc_annual_rules.rb +95 -0
  14. data/test/tc_posix_time_zone_parser.rb +261 -0
  15. data/test/tc_time_or_datetime.rb +14 -0
  16. data/test/tc_transition_rule.rb +663 -0
  17. data/test/tc_zoneinfo_timezone_info.rb +952 -113
  18. data/test/tzinfo-data/tzinfo/data/definitions/America/Argentina/Buenos_Aires.rb +5 -5
  19. data/test/tzinfo-data/tzinfo/data/definitions/America/New_York.rb +13 -1
  20. data/test/tzinfo-data/tzinfo/data/definitions/Australia/Melbourne.rb +13 -1
  21. data/test/tzinfo-data/tzinfo/data/definitions/EST.rb +1 -1
  22. data/test/tzinfo-data/tzinfo/data/definitions/Etc/GMT__m__1.rb +2 -2
  23. data/test/tzinfo-data/tzinfo/data/definitions/Etc/GMT__p__1.rb +2 -2
  24. data/test/tzinfo-data/tzinfo/data/definitions/Etc/UTC.rb +1 -1
  25. data/test/tzinfo-data/tzinfo/data/definitions/Europe/Amsterdam.rb +15 -3
  26. data/test/tzinfo-data/tzinfo/data/definitions/Europe/Andorra.rb +13 -1
  27. data/test/tzinfo-data/tzinfo/data/definitions/Europe/London.rb +13 -1
  28. data/test/tzinfo-data/tzinfo/data/definitions/Europe/Paris.rb +15 -3
  29. data/test/tzinfo-data/tzinfo/data/definitions/Europe/Prague.rb +19 -4
  30. data/test/tzinfo-data/tzinfo/data/definitions/UTC.rb +1 -1
  31. data/test/tzinfo-data/tzinfo/data/indexes/countries.rb +197 -184
  32. data/test/tzinfo-data/tzinfo/data/indexes/timezones.rb +60 -47
  33. data/test/tzinfo-data/tzinfo/data/version.rb +9 -3
  34. data/test/zoneinfo/America/Argentina/Buenos_Aires +0 -0
  35. data/test/zoneinfo/America/New_York +0 -0
  36. data/test/zoneinfo/Australia/Melbourne +0 -0
  37. data/test/zoneinfo/EST +0 -0
  38. data/test/zoneinfo/Etc/UTC +0 -0
  39. data/test/zoneinfo/Europe/Amsterdam +0 -0
  40. data/test/zoneinfo/Europe/Andorra +0 -0
  41. data/test/zoneinfo/Europe/London +0 -0
  42. data/test/zoneinfo/Europe/Paris +0 -0
  43. data/test/zoneinfo/Europe/Prague +0 -0
  44. data/test/zoneinfo/Factory +0 -0
  45. data/test/zoneinfo/iso3166.tab +13 -14
  46. data/test/zoneinfo/leapseconds +38 -21
  47. data/test/zoneinfo/posix/Europe/London +0 -0
  48. data/test/zoneinfo/posixrules +0 -0
  49. data/test/zoneinfo/right/Europe/London +0 -0
  50. data/test/zoneinfo/zone.tab +172 -159
  51. data/test/zoneinfo/zone1970.tab +185 -170
  52. data/tzinfo.gemspec +1 -1
  53. metadata +9 -3
  54. 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: 9569ae7387d4f2847ba35f6a1268fbbc9019f9bbef1cc72e70569757320db079
4
+ data.tar.gz: bb66169c01610b980c35be5c7102d780d07167b785acd6ea5887b89d15eb107d
5
5
  SHA512:
6
- metadata.gz: 7921a1c86b6307ee7b554edc6de33d2ed07ca8660c804f3ca78099485a72e26bcd23af498a955dfd7dfa5efb0c2f95e6adbdadbd0d60ef492732f1fdfd79f0d9
7
- data.tar.gz: a0b59904238b08bbebc38a359db873dfadc0a98ef7a2fb6241f91219c642432dc8ae643ff0cf844d16bae0ef61173bac16c5f69863cde60fe504105d4240f48f
6
+ metadata.gz: d9847f779c051d96c2457d364b18bdc9bc74e9b17ffd8e84106ce9c6562b2634e60f08b81af9fff6ba7c0af3b898ba4483fe8e9fa31b3e004251b203c1bee933
7
+ data.tar.gz: 8737907d69e81c06650a1f01b1ba45afc83f38a66b44fca093431cfea65208a92f56ada755e9f87724a6b866ecbfed11d1ef09552bbf983ce47c15c90269a322
Binary file
data.tar.gz.sig CHANGED
Binary file
data/CHANGES.md CHANGED
@@ -1,3 +1,14 @@
1
+ Version 1.2.8 - 8-Nov-2020
2
+ --------------------------
3
+
4
+ * Added support for handling "slim" format zoneinfo files that are produced by
5
+ default by zic version 2020b and later. The POSIX-style TZ string is now used
6
+ calculate DST transition times after the final defined transition in the file.
7
+ The 64-bit section is now always used regardless of whether Time has support
8
+ for 64-bit times.
9
+ * Rubinius is no longer supported.
10
+
11
+
1
12
  Version 1.2.7 - 2-Apr-2020
2
13
  --------------------------
3
14
 
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)](https://rubygems.org/gems/tzinfo) [![Travis CI Build](https://img.shields.io/travis/com/tzinfo/tzinfo/1.2?logo=travis)](https://travis-ci.com/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)
5
5
 
6
6
  [TZInfo](https://tzinfo.github.io) provides daylight savings aware
7
7
  transformations between times in different timezones.
@@ -10,6 +10,8 @@ require 'tzinfo/timezone_definition'
10
10
 
11
11
  require 'tzinfo/timezone_offset'
12
12
  require 'tzinfo/timezone_transition'
13
+ require 'tzinfo/transition_rule'
14
+ require 'tzinfo/annual_rules'
13
15
  require 'tzinfo/timezone_transition_definition'
14
16
 
15
17
  require 'tzinfo/timezone_index_definition'
@@ -22,6 +24,7 @@ require 'tzinfo/zoneinfo_timezone_info'
22
24
 
23
25
  require 'tzinfo/data_source'
24
26
  require 'tzinfo/ruby_data_source'
27
+ require 'tzinfo/posix_time_zone_parser'
25
28
  require 'tzinfo/zoneinfo_data_source'
26
29
 
27
30
  require 'tzinfo/timezone_period'
@@ -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
@@ -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