From 8014e87613a69560bab9240d016924e3f350ece8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marcel=20St=C3=B6r?= Date: Wed, 31 Oct 2018 23:56:34 +0100 Subject: [PATCH] Add early date range filter to parser Yields a significant performance boost if you only need a window into a large calendar. --- README.md | 2 ++ src/ICal/ICal.php | 53 +++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 55 insertions(+) diff --git a/README.md b/README.md index 4dc96b7..e536419 100644 --- a/README.md +++ b/README.md @@ -81,6 +81,8 @@ | `$defaultWeekStart` | :ballot_box_with_check: | `MO` | The two letter representation of the first day of the week | | `$disableCharacterReplacement` | :ballot_box_with_check: | `false` | Toggles whether to disable all character replacement. Will replace curly quotes and other special characters with their standard equivalents if `false`. Can be a costly operation! | | `$eventCount` | :heavy_multiplication_x: | N/A | Tracks the number of events in the current iCal feed | +| `$filterDaysAfter` | :ballot_box_with_check: | N/A | With this set the parser will ignore all events more than roughly this many days _after_ now. To be on the safe side it is advised that you make the filter window +-1d larger than necessary. For performance reasons this filter is applied before any date and time zone calculations are done. Hence, depending the time zone settings of the parser and the calendar the cut-off date is not "calibrated". You can then use `$ical->eventsFromRange()` to precisely shrink the window. See [#184](https://github.com/u01jmg3/ics-parser/issues/184#issuecomment-423669971) for rationale and performance analysis.| +| `$filterDaysBefore` | :ballot_box_with_check: | N/A | With this set the parser will ignore all events more than roughly this many days _before_ now. See `$filterDaysAfter` above for more details. | | `$freeBusyCount` | :heavy_multiplication_x: | N/A | Tracks the free/busy count in the current iCal feed | | `$replaceWindowsTimeZoneIds` | :ballot_box_with_check: | `false` | Toggles whether to replace (non-CLDR) Windows time zone IDs with their IANA equivalent e.g. "Mountain Standard Time" would be replaced with "America/Denver". As there are 130+ Windows time zone IDs that need to be searched and replaced this flag should only be turned on if you know that your calendar file contains such time zone IDs. **Microsoft Exchange** calendars are often seen using such IDs. | | `$skipRecurrence` | :ballot_box_with_check: | `false` | Toggles whether to skip the parsing of recurrence rules | diff --git a/src/ICal/ICal.php b/src/ICal/ICal.php index d7a6ca3..95c6641 100644 --- a/src/ICal/ICal.php +++ b/src/ICal/ICal.php @@ -105,6 +105,20 @@ class ICal */ public $replaceWindowsTimeZoneIds = false; + /** + * With this being non-null the parser will ignore all events more than roughly this many days before now. + * + * @var integer + */ + public $filterDaysAfter; + + /** + * With this being non-null the parser will ignore all events more than roughly this many days after now. + * + * @var integer + */ + public $filterDaysBefore; + /** * The parsed calendar * @@ -222,6 +236,8 @@ class ICal 'defaultTimeZone', 'defaultWeekStart', 'disableCharacterReplacement', + 'filterDaysAfter', + 'filterDaysBefore', 'replaceWindowsTimeZoneIds', 'skipRecurrence', 'useTimeZoneWithRRules', @@ -607,10 +623,47 @@ protected function initLines(array $lines) } } + if (!is_null($this->filterDaysAfter) || !is_null($this->filterDaysBefore)) { + $this->reduceEventsToMinMaxRange(); + } + $this->processDateConversions(); } } + function reduceEventsToMinMaxRange() + { + $events = (isset($this->cal['VEVENT'])) ? $this->cal['VEVENT'] : array(); + + if (empty($events)) { + return false; + } + + // ideally you would use PHP_INT_MIN but that was only introduced with PHP 7 + $minTimestamp = is_null($this->filterDaysBefore) ? -2147483648 : (new \DateTime('now'))->sub(new \DateInterval('P' . $this->filterDaysBefore . 'D'))->getTimestamp(); + $maxTimestamp = is_null($this->filterDaysAfter) ? PHP_INT_MAX : (new \DateTime('now'))->add(new \DateInterval('P' . $this->filterDaysAfter . 'D'))->getTimestamp(); + + foreach ($events as $key => $anEvent) { + if (!$this->isValidDate($anEvent['DTSTART']) || $this->isOutOfRange($anEvent['DTSTART'], $minTimestamp, $maxTimestamp)) { + unset($events[$key]); + $this->eventCount--; + + continue; + } + } + + $this->cal['VEVENT'] = $events; + } + + + private function isOutOfRange($eventStart, $minTimestamp, $maxTimestamp) + { + // at this point $dtstart is guaranteed to be stripped of any timezone identifier i.e. it's a pure timestamp + // and won't look like e.g. DTSTART;TZID=US-Eastern:19980119T020000 + $eventStartTimestamp = strtotime(explode("T", $eventStart)[0]); + return $eventStartTimestamp < $minTimestamp || $eventStartTimestamp > $maxTimestamp; + } + /** * Unfolds an iCal file in preparation for parsing * (https://icalendar.org/iCalendar-RFC-5545/3-1-content-lines.html)