Skip nil-anchor events in non_recurring instead of crashing#58
Skip nil-anchor events in non_recurring instead of crashing#58tilthouse wants to merge 1 commit intoajrosen:mainfrom
Conversation
`Event#non_recurring` does arithmetic on `self['duration']`,
`self['sdate']`, and `self['edate']` without nil checks. Real
Calendar.app data sometimes contains rows where these fields are
missing — malformed subscribed calendars, partial syncs from CalDAV
servers, or post-Sequoia migration rows are likely culprits.
When that happens, icalpal aborts the entire output with one of:
NoMethodError: undefined method '/' for nil:NilClass
(lib/event.rb:120 — `(self['duration'] / 86_400).to_i`)
ArgumentError: comparison of NilClass with ICalPal::RDT failed
(lib/event.rb:321 — `[ s, e ].max >= $opts[:from]` with nil e)
NoMethodError: undefined method 'to_a' for nil:NilClass
(lib/event.rb:126 — `@self['sdate'].to_a` with nil sdate)
In the user's case, a single malformed row in a subscribed calendar
takes the whole `events`/`eventsToday`/`week` output to zero events.
## The fix
Three lines at the top of `non_recurring`:
```ruby
return events if self['sdate'].nil?
self['duration'] ||= 0
self['edate'] ||= self['sdate']
```
- nil sdate is unrecoverable (no anchor) → skip the row, return [].
- nil duration is treated as a zero-length event → 1 iteration.
- nil edate falls back to sdate → in_window?'s `[s, e].max` works.
Same crash patterns are preserved as before for any future regression
(the test file documents all three with line refs).
## Tests
Adds `test/event_nil_safe_test.rb` — 4 cases using stdlib minitest:
- nil sdate returns [] without raising NoMethodError
- nil duration is coerced to 0
- nil edate is coerced to sdate (avoids the in_window? ArgumentError)
- well-formed events still produce a result (sanity)
Run with:
ruby test/event_nil_safe_test.rb
I confirmed the suite catches the regressions — reverting the fix
produces 3 errors with the exact line numbers cited in the
documentation comment.
## Notes
- No changes to runtime dependencies, gemspec, or executable surface.
- The `Reminder` path doesn't reach `non_recurring` (icalpal's
`bin/icalPal` only calls it on Events), so no test there.
|
Do you have a corrupted If icalPal ignored any events it thinks are malformed, it looks like the result would be the same: those events would simply not be included in the output. I'd like to add sanity checks to # Basic sanity checks
%w[ start_date end_date ].each do |s|
raise TypeError.new("event has no #{s}") unless @self[s]
end...and handle the exception in bin/icalPal: begin
# Instantiate an item
item = klass.new(row)
rescue StandardError => e
$log.error("#{e.class}: #{e}")
$log.error(row)
next
end |
|
Quick note since I'm spamming you with PRs at the moment. This applies to all of them:
As for this PR, here’s what’s going on on my end:
The two rows in question have Source data: two rows summarized Your One correction to my own PR description while I'm here: I cited "missing One small note on your proposed sanity check: I'd keep it to literal Happy to close this PR in favor of your |
Summary
Event#non_recurringdoes arithmetic onself['duration'],self['sdate'], andself['edate']without nil checks. Real Calendar.app data occasionally contains rows where these fields are missing — likely malformed subscribed calendars, partial syncs from CalDAV servers, or post-Sequoia migration rows.When that happens, icalpal aborts the entire output with one of:
In my case a single malformed row in a subscribed calendar takes the whole
events/eventsToday/weekoutput to zero events. I've been working around it locally with aModule#prependpatch in a downstream tool (calq/remq), but it really belongs upstream.The fix
Three lines at the top of
non_recurring:[].nDays = 0, runs once.in_window?'s[s, e].maxno longer raises.Order matters: the sdate guard must come first so the subsequent
||= self['sdate']is safe.Tests
Adds
test/event_nil_safe_test.rb— 4 cases using stdlib minitest:[]without raisingNoMethodErrorArgumentError)The test class subclasses
ICalPal::Eventand overridesinitializeto bypass the JSON.parse / date-conversion machinery, isolating the test surface tonon_recurringitself.Run with:
I confirmed the suite catches the regressions — reverting the patch produces three errors with the exact line numbers cited in the documentation comment.
Notes
Reminderpath doesn't reachnon_recurring(icalpal'sbin/icalPalonly calls it on Events), so no test there.idfor reminders in sectioned lists #53 (RestoreIdField, merged in 4.2.0) and Skip nil input and memoize plconvert #55 (plconvert nil-skip + memoize, currently open). Combined with this one, my downstream tool'slib/.../icalpal_patch.rbbecomes empty and the icalPal version pin can be bumped.