active-icalendar-events 0.1.2 → 0.2.1

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.
Files changed (3) hide show
  1. checksums.yaml +4 -4
  2. data/lib/active-icalendar-events.rb +149 -116
  3. metadata +1 -1
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 981c99140882554d70998f2b1225ce89cb47b80fb7471172c62c215204aecdb5
4
- data.tar.gz: 5d58a68cb6195024aedb6a0d461c8c03a98a1f88ad5d4c0f0250e4fe08e5d077
3
+ metadata.gz: ee38fd75cc71289c8cf434fcf1d107f00619e3fd1b4556a775ea8c62d7ec129d
4
+ data.tar.gz: 64c6215c1072a70e6d67f50c5040c1e358a7320162c05ef0851a433b40b356b3
5
5
  SHA512:
6
- metadata.gz: 426bb70b73ab75023013c15040498a1fc69605c5c39f08d06a83457e8e29467074064c7058083dd7223d4afad2be5a3d5c281d8a289b79d36560eff124b57ad4
7
- data.tar.gz: be2600c740997701ffeab23b6eb51e844cefe31912f8c7baa9b383501a4505e50381e52c0fa844be76aad25ca2591403770999809ff0e7d0f88974f15b435083
6
+ metadata.gz: cc21a38b8102ec9ae3983c040f897ec73b15188da380f8a6ebcfb08163b80157f0e2e7b05c6dfd5a7c7a6bc0fe4255c27d79ee99b20d2e34c7888f9a936c7336
7
+ data.tar.gz: d68e25d312753fe97b67c5471a78fb7172ca29280cafa298c48fddce2400601a77824ef125e40c1ef07266948d86b9b1266591f2ec440090558bd96b2e877a95
@@ -1,4 +1,5 @@
1
1
  #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
2
3
 
3
4
  require 'active_support'
4
5
  require 'active_support/core_ext'
@@ -51,36 +52,36 @@ module ActiveIcalendarEvents
51
52
  active_events = Set.new
52
53
 
53
54
  format_icalendar_data(icalendar_data).each do |_, events|
54
- recurrence_definition = events.select { |e|
55
+ recurrence_definition = events.select do |e|
55
56
  !e[:recurrence_rule].empty? || !e[:recurrence_dates].empty?
56
- }
57
+ end
57
58
  if recurrence_definition.size > 1
58
- raise RuntimeError, 'Should only have one event that defines the recurrence in a group'
59
+ raise 'Should only have one event that defines the recurrence in a group'
59
60
  elsif recurrence_definition.size == 1
60
61
  r = recurrence_definition.first
61
62
  if r[:recurrence_rule].size > 1
62
- raise RuntimeError, 'Multiple recurrence rules not supported'
63
+ raise 'Multiple recurrence rules not supported'
63
64
  elsif r[:recurrence_rule].size == 1
64
65
  # TODO: Validate the overrides
65
66
  active_events << get_active_event_for_datetime(
66
- :datetime => datetime,
67
- :name => r[:name],
68
- :event_start => r[:event_start],
69
- :event_end => r[:event_end],
70
- :recurrence_rule => r[:recurrence_rule].first,
71
- :recurrence_dates => r[:recurrence_dates],
72
- :excluding_dates => r[:excluding_dates],
73
- :overrides => events.reject { |e| e == r }.group_by { |e| e[:recurrence_id] }
67
+ datetime: datetime,
68
+ name: r[:name],
69
+ event_start: r[:event_start],
70
+ event_end: r[:event_end],
71
+ recurrence_rule: r[:recurrence_rule].first,
72
+ recurrence_dates: r[:recurrence_dates],
73
+ excluding_dates: r[:excluding_dates],
74
+ overrides: events.reject { |e| e == r }.group_by { |e| e[:recurrence_id] }
74
75
  )
75
76
  else
76
77
  # TODO: Haven't bothered implementing this as Google Calendar doesn't seem to use these
77
- raise RuntimeError, 'Not yet implemented when only recurrence_dates are provided'
78
+ raise 'Not yet implemented when only recurrence_dates are provided'
78
79
  end
79
80
  else
80
81
  # Non reccurring events
81
- events.each { |e|
82
- active_events.add(e[:name]) if is_event_active?(datetime, e[:event_start].to_time, e[:event_end].to_time)
83
- }
82
+ events.each do |e|
83
+ active_events.add(e[:name]) if event_active?(datetime, e[:event_start], e[:event_end])
84
+ end
84
85
  end
85
86
  end
86
87
 
@@ -90,34 +91,67 @@ module ActiveIcalendarEvents
90
91
  active_events.to_a
91
92
  end
92
93
 
94
+ def timezone_for_event(event)
95
+ if event.parent.timezones.empty?
96
+ ActiveSupport::TimeZone.new(event.parent.custom_properties['x_wr_timezone'].first.to_s)
97
+ else
98
+ ActiveSupport::TimeZone.new(event.parent.timezones.first.tzid.to_s)
99
+ end
100
+ end
101
+
93
102
  def format_icalendar_data(icalendar_data)
94
- icalendar_data.first.events.map { |e|
103
+ icalendar_data.first.events.map do |e|
95
104
  event_start = e.dtstart
96
105
  if event_start.is_a?(Icalendar::Values::Date)
97
- timezone = ActiveSupport::TimeZone.new(e.parent.timezones.first.tzid.to_s)
106
+ timezone ||= timezone_for_event(e)
98
107
  event_start = timezone.local(event_start.year, event_start.month, event_start.day)
99
108
  end
100
109
 
101
110
  event_end = e.dtend
102
111
  if event_end.is_a?(Icalendar::Values::Date)
103
- timezone = ActiveSupport::TimeZone.new(e.parent.timezones.first.tzid.to_s)
112
+ timezone ||= timezone_for_event(e)
104
113
  event_end = timezone.local(event_end.year, event_end.month, event_end.day)
105
114
  end
106
115
 
116
+ excluding_dates = e.exdate.map do |d|
117
+ if d.is_a?(Icalendar::Values::Date)
118
+ timezone ||= timezone_for_event(e)
119
+ timezone.local(d.year, d.month, d.day)
120
+ else
121
+ d
122
+ end
123
+ end
124
+
125
+ recurrence_dates = e.rdate.map do |d|
126
+ if d.is_a?(Icalendar::Values::Date)
127
+ timezone ||= timezone_for_event(e)
128
+ timezone.local(d.year, d.month, d.day)
129
+ else
130
+ d
131
+ end
132
+ end
133
+
134
+ e.rrule.each do |rrule|
135
+ unless rrule.until.nil?
136
+ timezone ||= timezone_for_event(e)
137
+ rrule.until = timezone.parse(rrule.until)
138
+ end
139
+ end
140
+
107
141
  {
108
142
  name: e.summary,
109
143
  event_start: event_start,
110
144
  event_end: event_end,
111
145
  recurrence_rule: e.rrule,
112
- recurrence_dates: e.rdate,
113
- excluding_dates: e.exdate,
146
+ recurrence_dates: recurrence_dates,
147
+ excluding_dates: excluding_dates,
114
148
  recurrence_id: e.recurrence_id,
115
149
  uid: e.uid
116
150
  }
117
- }.group_by { |e| e[:uid] }
151
+ end.group_by { |e| e[:uid] }
118
152
  end
119
153
 
120
- def is_event_active?(datetime, event_start, event_end)
154
+ def event_active?(datetime, event_start, event_end)
121
155
  event_start <= datetime.to_time &&
122
156
  event_end > datetime.to_time
123
157
  end
@@ -130,14 +164,14 @@ module ActiveIcalendarEvents
130
164
  !count.nil? && considered_count > count
131
165
  end
132
166
 
133
- def is_daily_event_active_for_datetime?(datetime,
134
- event_start,
135
- event_end,
136
- until_datetime,
137
- count,
138
- interval,
139
- excluding_dates,
140
- overridden_dates)
167
+ def daily_event_active_for_datetime?(datetime,
168
+ event_start,
169
+ event_end,
170
+ until_datetime,
171
+ count,
172
+ interval,
173
+ excluding_dates,
174
+ overridden_dates)
141
175
  event_start_considered = event_start
142
176
  event_end_considered = event_end
143
177
  considered_count = 1
@@ -145,7 +179,7 @@ module ActiveIcalendarEvents
145
179
  !instance_count_exceeded?(considered_count, count) &&
146
180
  event_start_considered <= datetime
147
181
 
148
- if is_event_active?(datetime, event_start_considered, event_end_considered)
182
+ if event_active?(datetime, event_start_considered, event_end_considered)
149
183
  return !excluding_dates.include?(event_start_considered) &&
150
184
  !overridden_dates.include?(event_start_considered)
151
185
  end
@@ -153,34 +187,35 @@ module ActiveIcalendarEvents
153
187
  # We consider both active dates and excluded dates for the recurrence count
154
188
  considered_count += 1
155
189
 
156
- event_start_considered = event_start_considered + interval.days
157
- event_end_considered = event_end_considered + interval.days
190
+ event_start_considered += interval.days
191
+ event_end_considered += interval.days
158
192
  end
159
193
 
160
194
  false
161
195
  end
162
196
 
163
- def is_weekly_event_active_for_datetime?(datetime,
164
- event_start,
165
- event_end,
166
- until_datetime,
167
- count,
168
- interval,
169
- by_day,
170
- excluding_dates,
171
- overridden_dates)
197
+ def weekly_event_active_for_datetime?(datetime,
198
+ event_start,
199
+ event_end,
200
+ until_datetime,
201
+ count,
202
+ interval,
203
+ by_day,
204
+ excluding_dates,
205
+ overridden_dates)
172
206
  event_start_considered = event_start
173
207
  event_end_considered = event_end
174
208
  considered_count = 1
175
- while !instance_count_exceeded?(considered_count, count)
209
+ until instance_count_exceeded?(considered_count, count)
176
210
 
211
+ # NOTE: Google Calendar does not appear to produce weekly events that do not specify a "by_day" array, so this path is untested
177
212
  if by_day.empty?
178
213
  if until_datetime_passed?(event_start_considered, until_datetime) ||
179
214
  event_start_considered > datetime
180
215
  return false
181
216
  end
182
217
 
183
- if is_event_active?(datetime, event_start_considered, event_end_considered)
218
+ if event_active?(datetime, event_start_considered, event_end_considered)
184
219
  return !excluding_dates.include?(event_start_considered) &&
185
220
  !overridden_dates.include?(event_start_considered)
186
221
  end
@@ -189,11 +224,14 @@ module ActiveIcalendarEvents
189
224
  considered_count += 1
190
225
  else
191
226
  week_event_start_considered =
192
- event_start_considered.monday? ? event_start_considered :
193
- event_start_considered.prev_occurring(:monday)
227
+ if event_start_considered.monday?
228
+ event_start_considered
229
+ else
230
+ event_start_considered.prev_occurring(:monday)
231
+ end
194
232
  week_event_end_considered = week_event_start_considered + (event_end.to_time - event_start.to_time).seconds
195
233
 
196
- (1..7).each { |_|
234
+ (1..7).each do |_|
197
235
  if week_event_start_considered >= event_start
198
236
  if until_datetime_passed?(week_event_start_considered, until_datetime) ||
199
237
  instance_count_exceeded?(considered_count, count) ||
@@ -201,10 +239,10 @@ module ActiveIcalendarEvents
201
239
  return false
202
240
  end
203
241
 
204
- day_code = week_event_start_considered.strftime("%^a").chop
242
+ day_code = week_event_start_considered.strftime('%^a').chop
205
243
 
206
244
  if by_day.include?(day_code)
207
- if is_event_active?(datetime, week_event_start_considered, week_event_end_considered)
245
+ if event_active?(datetime, week_event_start_considered, week_event_end_considered)
208
246
  return !excluding_dates.include?(week_event_start_considered) &&
209
247
  !overridden_dates.include?(week_event_start_considered)
210
248
  end
@@ -214,23 +252,26 @@ module ActiveIcalendarEvents
214
252
  end
215
253
  end
216
254
 
217
- week_event_start_considered = week_event_start_considered + 1.days
218
- week_event_end_considered = week_event_end_considered + 1.days
219
- }
255
+ week_event_start_considered += 1.days
256
+ week_event_end_considered += 1.days
257
+ end
220
258
  end
221
259
 
222
- event_start_considered = event_start_considered + interval.weeks
223
- event_end_considered = event_end_considered + interval.weeks
260
+ event_start_considered += interval.weeks
261
+ event_end_considered += interval.weeks
224
262
  end
225
263
 
226
264
  false
227
265
  end
228
266
 
267
+ # Get the beginning of the month, maintaining the timestamp
268
+ def beginning_of_month(datetime)
269
+ datetime - (datetime.day - 1).days
270
+ end
271
+
229
272
  def get_nth_day_in_month(datetime, day)
230
- matches = day.match /^([0-9]+)([A-Z]+)$/
231
- if matches.nil?
232
- raise RuntimeError, "Unexpected by_day format found"
233
- end
273
+ matches = day.match(/^([0-9]+)([A-Z]+)$/)
274
+ raise 'Unexpected by_day format found' if matches.nil?
234
275
 
235
276
  number, day_code = matches.captures
236
277
 
@@ -250,32 +291,30 @@ module ActiveIcalendarEvents
250
291
  when 'SU'
251
292
  :sunday
252
293
  else
253
- raise RuntimeError, "Unexpected day code used"
294
+ raise 'Unexpected day code used'
254
295
  end
255
296
 
256
- target_day = datetime.beginning_of_month
297
+ target_day = beginning_of_month(datetime)
257
298
 
258
- if target_day.strftime("%^a").chop != day_code
259
- target_day = target_day.next_occurring(day_label)
260
- end
299
+ target_day = target_day.next_occurring(day_label) if target_day.strftime('%^a').chop != day_code
261
300
 
262
- (2..number.to_i).each { |_|
301
+ (2..number.to_i).each do |_|
263
302
  target_day = target_day.next_occurring(day_label)
264
- }
303
+ end
265
304
 
266
305
  target_day
267
306
  end
268
307
 
269
- def is_monthly_event_active_for_datetime?(datetime,
270
- event_start,
271
- event_end,
272
- until_datetime,
273
- count,
274
- interval,
275
- by_day,
276
- by_month_day,
277
- excluding_dates,
278
- overridden_dates)
308
+ def monthly_event_active_for_datetime?(datetime,
309
+ event_start,
310
+ event_end,
311
+ until_datetime,
312
+ count,
313
+ interval,
314
+ by_day,
315
+ by_month_day,
316
+ excluding_dates,
317
+ overridden_dates)
279
318
  # TODO: We will ignore the contents of "by_month_day" for now and assume
280
319
  # always contains one number which is the same as the day of
281
320
  # "event_start". We additionally assume that "by_day" will only contain
@@ -288,7 +327,7 @@ module ActiveIcalendarEvents
288
327
  !instance_count_exceeded?(considered_count, count) &&
289
328
  event_start_considered <= datetime
290
329
 
291
- if is_event_active?(datetime, event_start_considered, event_end_considered)
330
+ if event_active?(datetime, event_start_considered, event_end_considered)
292
331
  return !excluding_dates.include?(event_start_considered) &&
293
332
  !overridden_dates.include?(event_start_considered)
294
333
  end
@@ -297,11 +336,11 @@ module ActiveIcalendarEvents
297
336
  considered_count += 1
298
337
 
299
338
  if by_day.nil? || by_day.empty?
300
- event_start_considered = event_start_considered + interval.month
301
- event_end_considered = event_end_considered + interval.month
339
+ event_start_considered += interval.month
340
+ event_end_considered += interval.month
302
341
  else
303
342
  event_start_considered =
304
- get_nth_day_in_month(event_start_considered.beginning_of_month + interval.month,
343
+ get_nth_day_in_month(beginning_of_month(event_start_considered) + interval.month,
305
344
  by_day.first)
306
345
  event_end_considered = event_start_considered + (event_end.to_time - event_start.to_time).seconds
307
346
  end
@@ -310,14 +349,14 @@ module ActiveIcalendarEvents
310
349
  false
311
350
  end
312
351
 
313
- def is_yearly_event_active_for_datetime?(datetime,
314
- event_start,
315
- event_end,
316
- until_datetime,
317
- count,
318
- interval,
319
- excluding_dates,
320
- overridden_dates)
352
+ def yearly_event_active_for_datetime?(datetime,
353
+ event_start,
354
+ event_end,
355
+ until_datetime,
356
+ count,
357
+ interval,
358
+ excluding_dates,
359
+ overridden_dates)
321
360
  event_start_considered = event_start
322
361
  event_end_considered = event_end
323
362
  considered_count = 1
@@ -325,7 +364,7 @@ module ActiveIcalendarEvents
325
364
  !instance_count_exceeded?(considered_count, count) &&
326
365
  event_start_considered <= datetime
327
366
 
328
- if is_event_active?(datetime, event_start_considered, event_end_considered)
367
+ if event_active?(datetime, event_start_considered, event_end_considered)
329
368
  return !excluding_dates.include?(event_start_considered) &&
330
369
  !overridden_dates.include?(event_start_considered)
331
370
  end
@@ -333,8 +372,8 @@ module ActiveIcalendarEvents
333
372
  # We consider both active dates and excluded dates for the recurrence count
334
373
  considered_count += 1
335
374
 
336
- event_start_considered = event_start_considered + interval.years
337
- event_end_considered = event_end_considered + interval.years
375
+ event_start_considered += interval.years
376
+ event_end_considered += interval.years
338
377
  end
339
378
 
340
379
  false
@@ -349,54 +388,48 @@ module ActiveIcalendarEvents
349
388
  excluding_dates: [],
350
389
  overrides:)
351
390
  # Can return early if one of the overrides matches as they always take precendence
352
- overrides.values.flatten.each { |e|
353
- return e[:name] if e[:event_start] <= datetime.to_time &&
354
- e[:event_end] > datetime.to_time
355
- }
391
+ overrides.values.flatten.each do |e|
392
+ return e[:name] if event_active?(datetime, e[:event_start], e[:event_end])
393
+ end
356
394
 
357
395
  # Can return early if one of the recurrence dates matches and is not overridden
358
396
  # Note: I've just made an assumption about how this data could be presented.
359
397
  # Google Calendar does not seem to create rdates, only rrules.
360
- (recurrence_dates - overrides.keys).each { |recurrence_event_start|
361
- recurrence_event_end = recurrence_event_start + (event_end.to_time - event_start.to_time)
362
- return name if is_event_active?(datetime, recurrence_event_start, recurrence_event_end)
363
- }
364
-
365
- until_datetime = nil
366
- if !recurrence_rule.until.nil?
367
- until_datetime = DateTime.parse(recurrence_rule.until)
398
+ (recurrence_dates - overrides.keys).each do |recurrence_event_start|
399
+ recurrence_event_end = recurrence_event_start + (event_end.to_time - event_start.to_time).seconds
400
+ return name if event_active?(datetime, recurrence_event_start, recurrence_event_end)
368
401
  end
369
402
 
370
403
  case recurrence_rule.frequency
371
- when "DAILY"
372
- return name if is_daily_event_active_for_datetime?(
404
+ when 'DAILY'
405
+ return name if daily_event_active_for_datetime?(
373
406
  datetime,
374
407
  event_start,
375
408
  event_end,
376
- until_datetime,
409
+ recurrence_rule.until,
377
410
  recurrence_rule.count,
378
411
  recurrence_rule.interval.nil? ? 1 : recurrence_rule.interval,
379
412
  excluding_dates,
380
413
  overrides.keys
381
414
  )
382
- when "WEEKLY"
383
- return name if is_weekly_event_active_for_datetime?(
415
+ when 'WEEKLY'
416
+ return name if weekly_event_active_for_datetime?(
384
417
  datetime,
385
418
  event_start,
386
419
  event_end,
387
- until_datetime,
420
+ recurrence_rule.until,
388
421
  recurrence_rule.count,
389
422
  recurrence_rule.interval.nil? ? 1 : recurrence_rule.interval,
390
423
  recurrence_rule.by_day,
391
424
  excluding_dates,
392
425
  overrides.keys
393
426
  )
394
- when "MONTHLY"
395
- return name if is_monthly_event_active_for_datetime?(
427
+ when 'MONTHLY'
428
+ return name if monthly_event_active_for_datetime?(
396
429
  datetime,
397
430
  event_start,
398
431
  event_end,
399
- until_datetime,
432
+ recurrence_rule.until,
400
433
  recurrence_rule.count,
401
434
  recurrence_rule.interval.nil? ? 1 : recurrence_rule.interval,
402
435
  recurrence_rule.by_day,
@@ -404,19 +437,19 @@ module ActiveIcalendarEvents
404
437
  excluding_dates,
405
438
  overrides.keys
406
439
  )
407
- when "YEARLY"
408
- return name if is_yearly_event_active_for_datetime?(
440
+ when 'YEARLY'
441
+ return name if yearly_event_active_for_datetime?(
409
442
  datetime,
410
443
  event_start,
411
444
  event_end,
412
- until_datetime,
445
+ recurrence_rule.until,
413
446
  recurrence_rule.count,
414
447
  recurrence_rule.interval.nil? ? 1 : recurrence_rule.interval,
415
448
  excluding_dates,
416
449
  overrides.keys
417
450
  )
418
451
  else
419
- throw RuntimeError, "Invalid event frequency"
452
+ throw RuntimeError, 'Invalid event frequency'
420
453
  end
421
454
 
422
455
  nil
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: active-icalendar-events
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.2
4
+ version: 0.2.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - William Starling