opening_hours_converter 1.16.0 → 1.17.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 3b633bae1e9cb5898370921f08cd07dc343b837c7139236883930769b91e3738
4
- data.tar.gz: 675e149ec88ead4dee904967288dc36f9ea40708c3742f9afab291375c75f33b
3
+ metadata.gz: ecd7eb467184d013601216945d337758dd52b4824640a61fc5826e28f74c2e4f
4
+ data.tar.gz: 5804ea9f19e7d0f6f5f538cba377a8d7aa1b0700384588b8214aaf6c2634b933
5
5
  SHA512:
6
- metadata.gz: 8348466a3dab6669c2b1ae059dabc1bf32c9ccb81161066f35118a01860e88be85b53e1446e035dfa2f12a76d9c6b3298c7f1b5556fec460feb24f34df850a65
7
- data.tar.gz: a44a50c9bbc2080d8bc7c31e504149732a47277036ddcaec7e50e5700eb7357259d7e9921175e4508dc468ff25aed6785583fc90792a7346b712d3f97298d78b
6
+ metadata.gz: 2d697f34e86039dd4854e3e7dd0ab8332654d239971d3fcd12f69703ebf629ea2ab0380811bd460afde0ab06aceca1ae0ffac7eda30592c557e27c1ea76fdfef
7
+ data.tar.gz: 985cf1ab1967b7c7d69e4d1b593e83976ba4799bca329849fb26582d55d66ce10bba1a76de4d75cf206d4ff434f153628248b8dbb0d4640d3ea83b183b8a008e
@@ -11,6 +11,8 @@ module OpeningHoursConverter
11
11
  DIMANCHE: 6
12
12
  }.freeze
13
13
  OSM_DAYS = %w[Mo Tu We Th Fr Sa Su].freeze
14
+ # Not specification, but common enough in the data to be read.
15
+ OSM_DAYS_ABBREVIATIONS = %w[Mon Tue Wed Thu Fri Sat Sun].freeze
14
16
  IRL_DAYS = %w[Lundi Mardi Mercredi Jeudi Vendredi Samedi Dimanche].freeze
15
17
  OSM_MONTHS = %w[Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec].freeze
16
18
  IRL_MONTHS = %W[Janvier Fevrier Mars Avril Mai Juin Juillet Aout Septembre
@@ -20,5 +22,6 @@ module OpeningHoursConverter
20
22
  DAYS_MAX = 6
21
23
  YEAR_DAYS_MAX = 365
22
24
  PH_WEEKDAY = -2
25
+ EASTER_WEEKDAY = -3
23
26
  end
24
27
  end
@@ -1,11 +1,13 @@
1
1
  module OpeningHoursConverter
2
2
  class DateRange
3
- attr_accessor :wide_interval, :typical, :comment
3
+ attr_accessor :wide_interval, :typical, :comment, :fallback_suffix, :fallback_ranges
4
4
 
5
5
  def initialize(wide_interval = nil)
6
6
  @wide_interval = nil
7
7
  @typical = nil
8
8
  @comment = ''
9
+ @fallback_suffix = nil
10
+ @fallback_ranges = []
9
11
  update_range(wide_interval)
10
12
  end
11
13
 
@@ -3,14 +3,28 @@ require 'opening_hours_converter/constants'
3
3
  module OpeningHoursConverter
4
4
  class Interval
5
5
  include Constants
6
- attr_reader :day_start, :day_end, :start, :end, :is_off
6
+ attr_reader :day_start, :day_end, :start, :end, :is_off, :index, :is_unknown, :open_ended, :day_offset
7
7
 
8
- def initialize(day_start, min_start, day_end = 0, min_end = 0, is_off = false)
8
+ # is_unknown is the OSM third state ("unknown", alongside open/off) and
9
+ # open_ended is a time with no known closing time ("18:00+"). Both are
10
+ # carried purely for round-trip serialization; for interval computation
11
+ # is_unknown behaves like is_off (not open) and open_ended is treated as
12
+ # running to the end of the day.
13
+ #
14
+ # day_offset is the OSM day offset ("easter +1 day"): the interval falls
15
+ # that many days after the date day_start selects.
16
+ def initialize(day_start, min_start, day_end = 0, min_end = 0, is_off = false, index = nil, is_unknown = false, open_ended = false, day_offset = 0)
9
17
  @day_start = day_start
10
18
  @day_end = day_end
11
19
  @start = min_start
12
20
  @end = min_end
13
21
  @is_off = is_off
22
+ # The weekday index is an OSM nth_entry list ("Sa[1,3]"), so it is always
23
+ # held as an array; a lone index is wrapped rather than special-cased.
24
+ @index = index.nil? ? nil : Array(index)
25
+ @is_unknown = is_unknown
26
+ @open_ended = open_ended
27
+ @day_offset = day_offset || 0
14
28
 
15
29
  if @day_end == 0 && @end == 0
16
30
  @day_end = DAYS_MAX
@@ -8,10 +8,19 @@ module OpeningHoursConverter
8
8
  # an earlier one on the minutes it selects, as the OpenStreetMap
9
9
  # specification requires: "Jul-Aug off; Mo-Fr 09:00-17:00" is open in July,
10
10
  # while "Mo-Fr 09:00-17:00; Jul-Aug off" is closed.
11
+ #
12
+ # A rule behind a "||" is the exception. It is a fallback: it speaks only for
13
+ # the minutes the rules before it leave closed, and never over the ones they
14
+ # open. "Mo-Fr 08:00-12:00 || Sa 10:00-12:00" opens on Saturday and changes
15
+ # nothing from Monday to Friday.
11
16
  class OpenIntervals
12
17
  include Constants
13
18
  include Utils
14
19
 
20
+ # The state of a minute an open-ended time covers, told apart from a plain
21
+ # open minute so that the two never run into a single interval.
22
+ OPEN_ENDED = :open_ended
23
+
15
24
  def self.call(opening_hours_string, from, to)
16
25
  date_ranges = OpeningHoursConverter::OpeningHoursParser.new.parse(opening_hours_string)
17
26
  new(from, to).apply(date_ranges).intervals
@@ -25,12 +34,20 @@ module OpeningHoursConverter
25
34
  @first_date = from.to_date - 1
26
35
  @last_date = to.to_date
27
36
  @masks = {}
37
+ @fallback = false
28
38
  @holidays = {}
29
39
  end
30
40
 
31
41
  def apply(date_ranges)
32
42
  expand(date_ranges).each { |date_range, bounds| apply_date_range(date_range, bounds) }
33
- self
43
+
44
+ fallback = date_ranges.first&.fallback_ranges
45
+ return self if fallback.nil? || fallback.empty?
46
+
47
+ # Everything past a "||" is a fallback, and stays one: a chained
48
+ # "a || b || c" nests, c falling back on what a and b leave closed.
49
+ @fallback = true
50
+ apply(fallback)
34
51
  end
35
52
 
36
53
  def intervals
@@ -51,11 +68,24 @@ module OpeningHoursConverter
51
68
  case date_range.wide_interval.type
52
69
  when 'week' then expand_to_days(date_range, :get_weeks_for_year)
53
70
  when 'holiday' then expand_to_days(date_range, :get_public_holidays_for_year)
54
- else [[date_range, nil]]
71
+ when 'variable_day' then expand_to_days(date_range, :get_variable_days_for_year)
72
+ else [[clamp_open_ended_year(date_range), nil]]
55
73
  end
56
74
  end
57
75
  end
58
76
 
77
+ # "2020+" applies from its start year through the end of the window:
78
+ # Year#build_day_array_from_date_range has no concept of an unbounded
79
+ # range, so it needs a concrete (if window-dependent) end year here.
80
+ def clamp_open_ended_year(date_range)
81
+ wide_interval = date_range.wide_interval
82
+ return date_range unless wide_interval.type == 'year' && wide_interval.open_ended && wide_interval.end.nil?
83
+
84
+ copy = date_range.dup
85
+ copy.update_range(OpeningHoursConverter::WideInterval.new.year(wide_interval.start[:year], @last_date.year))
86
+ copy
87
+ end
88
+
59
89
  # An ISO week declared with a year can start in the previous year or end in
60
90
  # the next one ("2025 week 1" starts on 2025-12-29), so the neighbouring
61
91
  # years are expanded too and their surplus days are cut back by the declared
@@ -119,9 +149,27 @@ module OpeningHoursConverter
119
149
  end
120
150
 
121
151
  def selects?(interval, date)
152
+ # A day offset shifts the date the selector names onto another one, so
153
+ # the selector is asked about the day it counts from rather than the day
154
+ # the interval is written on.
155
+ date -= interval.day_offset
122
156
  return public_holiday?(date) if interval.day_start == PH_WEEKDAY
157
+ return easter?(date) if interval.day_start == EASTER_WEEKDAY
158
+ return false unless interval.day_start == reindex_sunday_week_to_monday_week(date.wday)
159
+ return true if interval.index.nil?
123
160
 
124
- interval.day_start == reindex_sunday_week_to_monday_week(date.wday)
161
+ interval.index.any? { |index| nth_weekday_of_month?(index, interval.day_start, date) }
162
+ end
163
+
164
+ def easter?(date)
165
+ OpeningHoursConverter::PublicHoliday.easter(date.year).to_date == date
166
+ end
167
+
168
+ def nth_weekday_of_month?(index, weekday, date)
169
+ target = OpeningHoursConverter::WeekIndex.nth_wday_of_month(index, weekday, date.month, date.year)
170
+ target == date
171
+ rescue ArgumentError
172
+ false
125
173
  end
126
174
 
127
175
  def public_holiday?(date)
@@ -133,6 +181,10 @@ module OpeningHoursConverter
133
181
  # An interval runs from (day_start, start) to (day_end, end). Only midnight
134
182
  # crossings give day_end > day_start, and their remaining minutes are
135
183
  # written on the dates that follow.
184
+ #
185
+ # A fallback rule skips the minutes that are already open: what the rules
186
+ # before the "||" leave closed is what it answers for, whether they were
187
+ # silent about it or closed it with an "off".
136
188
  def write(date, interval)
137
189
  remaining = (interval.day_end - interval.day_start) * MINUTES_MAX + interval.end
138
190
  first_minute = interval.start
@@ -141,11 +193,25 @@ module OpeningHoursConverter
141
193
 
142
194
  while remaining > 0
143
195
  mask = mask_for(current)
144
- ([first_minute, 0].max...[remaining, MINUTES_MAX].min).each { |minute| mask[minute] = open } unless mask.nil?
196
+ minutes = [first_minute, 0].max...[remaining, MINUTES_MAX].min
197
+ minutes.each { |minute| mask[minute] = open unless @fallback && mask[minute] } unless mask.nil?
145
198
  remaining -= MINUTES_MAX
146
199
  first_minute = 0
147
200
  current += 1
148
201
  end
202
+
203
+ write_open_end(date, interval) if interval.open_ended && !interval.is_off
204
+ end
205
+
206
+ # An open end names no closing time ("10:00-12:00+"), so the rest of the
207
+ # day stays open without being certain. It is written as a state of its
208
+ # own, which keeps it from merging with the hours the string does name:
209
+ # the reference reports the two as two intervals.
210
+ def write_open_end(date, interval)
211
+ mask = mask_for(date + (interval.day_end - interval.day_start))
212
+ return if mask.nil?
213
+
214
+ (interval.end...MINUTES_MAX).each { |minute| mask[minute] = OPEN_ENDED }
149
215
  end
150
216
 
151
217
  def mask_for(date)
@@ -155,10 +221,13 @@ module OpeningHoursConverter
155
221
  end
156
222
 
157
223
  # Walks the window as a continuous timeline, so a run of open minutes that
158
- # spans midnight comes out as a single interval.
224
+ # spans midnight comes out as a single interval. A minute carries the state
225
+ # that opened it rather than a plain flag, so an open end closes the
226
+ # interval before it instead of extending it.
159
227
  def runs
160
228
  result = []
161
229
  start = nil
230
+ state = nil
162
231
 
163
232
  (@first_date..(@last_date + 1)).each do |date|
164
233
  mask = @masks[date]
@@ -168,16 +237,16 @@ module OpeningHoursConverter
168
237
 
169
238
  result << { start: start, end: time_at(date, 0) }
170
239
  start = nil
240
+ state = nil
171
241
  next
172
242
  end
173
243
 
174
244
  (0...MINUTES_MAX).each do |minute|
175
- if mask[minute]
176
- start ||= time_at(date, minute)
177
- elsif start
178
- result << { start: start, end: time_at(date, minute) }
179
- start = nil
180
- end
245
+ next if mask[minute] == state
246
+
247
+ result << { start: start, end: time_at(date, minute) } if start
248
+ start = mask[minute] ? time_at(date, minute) : nil
249
+ state = mask[minute]
181
250
  end
182
251
  end
183
252
 
@@ -19,7 +19,7 @@ module OpeningHoursConverter
19
19
  phs = []
20
20
 
21
21
  date_range.typical.intervals.each_with_index do |interval, interval_id|
22
- next unless interval&.day_start == -2 && interval&.day_start == interval&.day_end
22
+ next unless interval&.day_start == PH_WEEKDAY && interval&.day_start == interval&.day_end
23
23
 
24
24
  if interval.is_off
25
25
  off_day_ph = true
@@ -31,6 +31,23 @@ module OpeningHoursConverter
31
31
  date_range.typical.remove_interval(interval_id)
32
32
  end
33
33
 
34
+ day_easter = false
35
+ off_day_easter = false
36
+ easters = []
37
+
38
+ date_range.typical.intervals.each_with_index do |interval, interval_id|
39
+ next unless interval&.day_start == EASTER_WEEKDAY && interval&.day_start == interval&.day_end
40
+
41
+ if interval.is_off
42
+ off_day_easter = true
43
+ else
44
+ day_easter = true
45
+ end
46
+
47
+ easters << interval
48
+ date_range.typical.remove_interval(interval_id)
49
+ end
50
+
34
51
  range_general = nil
35
52
  range_general_for = nil
36
53
  range_general_id = date_range_index - 1
@@ -120,6 +137,36 @@ module OpeningHoursConverter
120
137
  end
121
138
  end
122
139
 
140
+ if off_day_easter || day_easter
141
+ if date_range.typical.intervals.uniq == [nil]
142
+ oh_rule.date.first.weekdays = [EASTER_WEEKDAY]
143
+ if off_day_easter
144
+ oh_rule.is_defined_off = off_day_easter
145
+ easters = []
146
+ else
147
+ easters.reverse.each do |interval|
148
+ oh_rule.add_time(OpeningHoursConverter::OpeningHoursTime.new(interval.start, interval.end))
149
+ end
150
+ end
151
+ else
152
+ if times_are_compatible?(oh_rule, easters)
153
+ easter_intervals = get_compatible_intervals(oh_rule, easters)
154
+ oh_rule.add_easter_weekday if easter_intervals[:compatible].length > 0
155
+ rules << oh_rule if !oh_rule_added
156
+ oh_rule_added = true
157
+ easter_intervals[:incompatible].each do |interval|
158
+ rules += build_off_easter(date_range) if off_day_easter
159
+ rules += build_easter(date_range) if day_easter
160
+ end
161
+ else
162
+ rules << oh_rule if !oh_rule_added
163
+ oh_rule_added = true
164
+ rules += build_off_easter(date_range) if off_day_easter
165
+ rules += build_easter(date_range) if day_easter
166
+ end
167
+ end
168
+ end
169
+
123
170
  rules << oh_rule if !oh_rule_added
124
171
 
125
172
  next unless oh_rule == oh_rules.last && oh_rule.has_overwritten_weekday?
@@ -142,7 +189,9 @@ module OpeningHoursConverter
142
189
  result += rules.map(&:get).join('; ')
143
190
  end
144
191
 
145
- result.strip
192
+ result = result.strip
193
+ result += " || #{date_ranges.first.fallback_suffix}" if date_ranges.first&.fallback_suffix
194
+ result
146
195
  end
147
196
 
148
197
  def times_are_compatible?(oh_rule, phs)
@@ -220,6 +269,53 @@ module OpeningHoursConverter
220
269
  [rule]
221
270
  end
222
271
 
272
+ def build_off_easter(date_range)
273
+ start_year = date_range.wide_interval.start&.key?(:year) ? date_range.wide_interval.start[:year] : date_range.wide_interval.start
274
+ end_year = date_range.wide_interval.end&.key?(:year) ? date_range.wide_interval.end[:year] : date_range.wide_interval.end
275
+
276
+ date_range = OpeningHoursConverter::DateRange.new(OpeningHoursConverter::WideInterval.new.holiday('easter', start_year, end_year))
277
+
278
+ rule = OpeningHoursConverter::OpeningHoursRule.new
279
+ date = OpeningHoursConverter::OpeningHoursDate.new(date_range.wide_interval, [-1])
280
+ rule.add_date(date)
281
+ rule.is_defined_off = true
282
+
283
+ [rule]
284
+ end
285
+
286
+ def build_easter(date_range)
287
+ start_year = date_range.wide_interval.start&.key?(:year) ? date_range.wide_interval.start[:year] : date_range.wide_interval.start
288
+ end_year = date_range.wide_interval.end&.key?(:year) ? date_range.wide_interval.end[:year] : date_range.wide_interval.end
289
+ intervals = date_range.typical.get_intervals(true)
290
+
291
+ if date_range.wide_interval.type == 'week'
292
+ intervals.each do |interval|
293
+ date_range.typical.add_interval(OpeningHoursConverter::Interval.new(EASTER_WEEKDAY, interval.start, EASTER_WEEKDAY, interval.end, interval.is_off))
294
+ end
295
+ else
296
+ date_range = OpeningHoursConverter::DateRange.new(OpeningHoursConverter::WideInterval.new.holiday('easter', start_year, end_year))
297
+ end
298
+
299
+ for i in 0..6
300
+ intervals.each do |interval|
301
+ if !interval.nil?
302
+ date_range.typical.add_interval(OpeningHoursConverter::Interval.new(i, interval.start, i, interval.end, interval.is_off))
303
+ end
304
+ end
305
+ end
306
+ rule = OpeningHoursConverter::OpeningHoursRule.new
307
+ date = OpeningHoursConverter::OpeningHoursDate.new(date_range.wide_interval, [-1])
308
+ rule.add_date(date)
309
+
310
+ date_range.typical.intervals.each do |interval|
311
+ if !interval.nil?
312
+ rule.add_time(OpeningHoursConverter::OpeningHoursTime.new(interval.start, interval.end))
313
+ rule.is_defined_off = rule.is_defined_off || interval.is_off
314
+ end
315
+ end
316
+ [rule]
317
+ end
318
+
223
319
  def build_day(date_range)
224
320
  intervals = date_range.typical.get_intervals(true)
225
321
 
@@ -240,7 +336,7 @@ module OpeningHoursConverter
240
336
  def build_week(date_range)
241
337
  result = []
242
338
 
243
- intervals = date_range.typical.get_intervals(true)
339
+ intervals = clean_intervals_keeping_index(date_range.typical)
244
340
  days = create_time_intervals(date_range.wide_interval, intervals)
245
341
 
246
342
  days_status = Array.new(OSM_DAYS.length, 0)
@@ -385,6 +481,26 @@ module OpeningHoursConverter
385
481
  result
386
482
  end
387
483
 
484
+ # An index ("We[1]"), the "unknown" state, and an open-ended time
485
+ # ("18:00+") don't survive Week#get_intervals(true), which rebuilds
486
+ # intervals from a per-minute true/false/"off" grid that has no room for
487
+ # any of them. All three are pulled out first and passed through
488
+ # untouched instead; none of them can collide with anything the grid
489
+ # pass produces (an index never applies to more than a single day, and
490
+ # "unknown"/open-ended are only ever full-interval markers here).
491
+ def clean_intervals_keeping_index(typical)
492
+ special = typical.intervals.compact.select { |interval| carries_metadata?(interval) }
493
+ return typical.get_intervals(true) if special.empty?
494
+
495
+ plain = OpeningHoursConverter::Week.new
496
+ typical.intervals.compact.each { |interval| plain.add_interval(interval) unless carries_metadata?(interval) }
497
+ plain.get_intervals(true) + special
498
+ end
499
+
500
+ def carries_metadata?(interval)
501
+ !interval.index.nil? || interval.is_unknown || interval.open_ended
502
+ end
503
+
388
504
  def create_time_intervals(wide_interval, intervals)
389
505
  days = []
390
506
 
@@ -398,16 +514,26 @@ module OpeningHoursConverter
398
514
 
399
515
  begin
400
516
  if interval.day_start == interval.day_end
401
- days[interval.day_start].add_time(OpeningHoursConverter::OpeningHoursTime.new(interval.start, interval.end))
517
+ days[interval.day_start].add_time(OpeningHoursConverter::OpeningHoursTime.new(interval.start, interval.end, interval.open_ended))
402
518
  days[interval.day_start].is_defined_off = days[interval.day_start].is_defined_off ? true : interval.is_off
519
+ days[interval.day_start].is_defined_unknown = days[interval.day_start].is_defined_unknown ? true : interval.is_unknown
520
+ if interval.index
521
+ # One weekday can carry several intervals, one per entry of its
522
+ # index list ("Sa[2],Sa[4]"), and the list is their union.
523
+ date = days[interval.day_start].date.first
524
+ date.weekday_index = Array(date.weekday_index) | interval.index
525
+ end
403
526
  elsif interval.day_end - interval.day_start == 1
404
527
  days[interval.day_start].add_time(OpeningHoursConverter::OpeningHoursTime.new(interval.start, MINUTES_MAX))
405
528
  days[interval.day_start].is_defined_off = days[interval.day_start].is_defined_off ? true : interval.is_off
529
+ days[interval.day_start].is_defined_unknown = days[interval.day_start].is_defined_unknown ? true : interval.is_unknown
406
530
  days[interval.day_end].add_time(OpeningHoursConverter::OpeningHoursTime.new(0, interval.end))
407
531
  days[interval.day_end].is_defined_off = days[interval.day_end].is_defined_off ? true : interval.is_off
532
+ days[interval.day_end].is_defined_unknown = days[interval.day_end].is_defined_unknown ? true : interval.is_unknown
408
533
  else
409
534
  for j in interval.day_start..interval.day_end
410
535
  days[j].is_defined_off = days[j].is_defined_off ? true : interval.is_off
536
+ days[j].is_defined_unknown = days[j].is_defined_unknown ? true : interval.is_unknown
411
537
  if j == interval.day_start
412
538
  days[j].add_time(OpeningHoursConverter::OpeningHoursTime.new(interval.start, MINUTES_MAX))
413
539
  elsif j == interval.day_end
@@ -3,10 +3,10 @@ require 'opening_hours_converter/constants'
3
3
  module OpeningHoursConverter
4
4
  class OpeningHoursDate
5
5
  include Constants
6
- attr_accessor :weekdays, :weekdays_over
6
+ attr_accessor :weekdays, :weekdays_over, :weekday_index
7
7
  attr_reader :wide_interval
8
8
 
9
- def initialize(wide_interval, weekdays)
9
+ def initialize(wide_interval, weekdays, weekday_index = nil)
10
10
  if wide_interval.nil? || weekdays.nil? || !wide_interval.is_a?(OpeningHoursConverter::WideInterval)
11
11
  raise ArgumentError
12
12
  end
@@ -14,14 +14,15 @@ module OpeningHoursConverter
14
14
  @wide_interval = wide_interval
15
15
  @weekdays = weekdays.sort
16
16
  @weekdays_over = []
17
+ @weekday_index = weekday_index
17
18
  end
18
19
 
19
20
  def get_weekdays
20
21
  result = ''
21
22
  weekdays = @weekdays.concat(@weekdays_over).sort.uniq
22
23
 
23
- if !weekdays.empty? && weekdays[0] == -2
24
- result = 'PH'
24
+ if !weekdays.empty? && (weekdays[0] == PH_WEEKDAY || weekdays[0] == EASTER_WEEKDAY)
25
+ result = weekdays[0] == PH_WEEKDAY ? 'PH' : 'easter'
25
26
  weekdays.shift
26
27
  end
27
28
 
@@ -94,6 +95,7 @@ module OpeningHoursConverter
94
95
  end
95
96
 
96
97
  result = '' if result == 'Mo-Su'
98
+ result += "[#{Array(@weekday_index).join(',')}]" if weekdays.length == 1 && !@weekday_index.nil?
97
99
  result
98
100
  end
99
101
 
@@ -105,7 +107,11 @@ module OpeningHoursConverter
105
107
  end
106
108
 
107
109
  def add_ph_weekday
108
- add_weekday(-2)
110
+ add_weekday(PH_WEEKDAY)
111
+ end
112
+
113
+ def add_easter_weekday
114
+ add_weekday(EASTER_WEEKDAY)
109
115
  end
110
116
 
111
117
  def add_overwritten_weekday(weekday)