From d48cf5c57958e2865f9b23f3fa037684c26cc48b Mon Sep 17 00:00:00 2001 From: root Date: Sun, 23 Apr 2023 13:28:43 +0200 Subject: [PATCH] Revert "upgrade icalparser from e6a3a285cf6e239236a40153a66b67b940220b43 to cae1631b9496a7415ef960b0dff1cd0f39fe3135" This reverts commit e2541c84e4cb0239ffb46236c8068729131e5c86. --- include/icalparser/.travis.yml | 27 + include/icalparser/LICENSE | 2 +- include/icalparser/composer.json | 21 +- include/icalparser/example/index.php | 32 +- include/icalparser/readme.md | 42 +- include/icalparser/src/EventsList.php | 54 -- include/icalparser/src/Freq.php | 527 ++++++++---------- include/icalparser/src/IcalParser.php | 443 +++++++-------- include/icalparser/src/Recurrence.php | 70 +-- include/icalparser/src/WindowsTimezones.php | 6 +- .../icalparser/tests/blank_description.phpt | 19 + include/icalparser/tests/bootstrap.php | 21 - .../38_weekly_recurring_event_missing_day.ics | 59 -- .../tests/cal/missing_RRULE_notice.ics | 39 -- .../tests/cal/multiple_categories.ics | 67 --- include/icalparser/tests/cal/url.ics | 32 -- .../icalparser/tests/event.attachements.phpt | 26 - .../icalparser/tests/event.categories.phpt | 25 - include/icalparser/tests/event.dates.phpt | 25 - .../icalparser/tests/event.description.phpt | 34 -- include/icalparser/tests/event.timezones.phpt | 39 -- include/icalparser/tests/event.url.phpt | 22 - .../icalparser/tests/events.recurring.phpt | 241 -------- include/icalparser/tests/events.sorting.phpt | 36 -- .../tests/multiline_description.phpt | 18 + .../tests/multiple_attachments.phpt | 20 + .../icalparser/tests/recurring_events.phpt | 170 ++++++ include/icalparser/tests/sort_events.phpt | 29 + include/icalparser/tests/timezone.phpt | 28 + include/icalparser/tests/wrong_dates.phpt | 19 + .../windowstimezones.php} | 11 +- 31 files changed, 876 insertions(+), 1328 deletions(-) create mode 100644 include/icalparser/.travis.yml delete mode 100644 include/icalparser/src/EventsList.php create mode 100644 include/icalparser/tests/blank_description.phpt delete mode 100644 include/icalparser/tests/bootstrap.php delete mode 100644 include/icalparser/tests/cal/38_weekly_recurring_event_missing_day.ics delete mode 100644 include/icalparser/tests/cal/missing_RRULE_notice.ics delete mode 100755 include/icalparser/tests/cal/multiple_categories.ics delete mode 100644 include/icalparser/tests/cal/url.ics delete mode 100644 include/icalparser/tests/event.attachements.phpt delete mode 100644 include/icalparser/tests/event.categories.phpt delete mode 100644 include/icalparser/tests/event.dates.phpt delete mode 100644 include/icalparser/tests/event.description.phpt delete mode 100644 include/icalparser/tests/event.timezones.phpt delete mode 100644 include/icalparser/tests/event.url.phpt delete mode 100644 include/icalparser/tests/events.recurring.phpt delete mode 100644 include/icalparser/tests/events.sorting.phpt create mode 100644 include/icalparser/tests/multiline_description.phpt create mode 100644 include/icalparser/tests/multiple_attachments.phpt create mode 100644 include/icalparser/tests/recurring_events.phpt create mode 100644 include/icalparser/tests/sort_events.phpt create mode 100644 include/icalparser/tests/timezone.phpt create mode 100644 include/icalparser/tests/wrong_dates.phpt rename include/icalparser/{bin/timezones.php => tools/windowstimezones.php} (52%) diff --git a/include/icalparser/.travis.yml b/include/icalparser/.travis.yml new file mode 100644 index 0000000..6eec526 --- /dev/null +++ b/include/icalparser/.travis.yml @@ -0,0 +1,27 @@ +language: php +php: + - 5.6 + - 7.0 + - 7.1 + - 7.2 + - hhvm + +matrix: + allow_failures: + - php: 7.1 + - php: 7.2 + - php: hhvm + + include: + - php: 5.6 + env: dependencies="--prefer-lowest --prefer-stable" + +script: + - vendor/bin/tester tests -s -p php + +after_failure: + - for i in $(find tests -name \*.actual); do echo "--- $i"; cat $i; echo; echo; done + +before_script: + - composer self-update + - composer update --no-interaction --prefer-source $dependencies \ No newline at end of file diff --git a/include/icalparser/LICENSE b/include/icalparser/LICENSE index ce7d41d..e449f31 100644 --- a/include/icalparser/LICENSE +++ b/include/icalparser/LICENSE @@ -1,4 +1,4 @@ -Copyright (c) 2014-2022, Roman Ožana +Copyright (c) 2014, Roman Ožana All rights reserved. Redistribution and use in source and binary forms, with or without diff --git a/include/icalparser/composer.json b/include/icalparser/composer.json index bf70a70..3eb9cc8 100644 --- a/include/icalparser/composer.json +++ b/include/icalparser/composer.json @@ -1,33 +1,28 @@ { "name": "om/icalparser", - "license": "BSD-3-Clause", "description": "Simple ical parser", "keywords": [ "ical", "calendar", "parser" ], + "license": ["BSD-3-Clause"], "authors": [ { "name": "Roman Ožana", - "email": "roman@ozana.cz" + "email": "ozana@omdesign.cz" } ], "require": { - "php": ">=7.4.0" - }, - "suggest": { - "ext-dom": "for timezone tool" + "php": ">=5.6.0" }, "autoload": { - "classmap": [ - "src/" - ] - }, - "scripts": { - "test": "tester tests -s -p php" + "classmap": ["src/"] }, "require-dev": { - "nette/tester": "^2.4" + "nette/tester": "v2.0.1" + }, + "scripts": { + "tests": ["./vendor/bin/tester tests -s -p php"] } } diff --git a/include/icalparser/example/index.php b/include/icalparser/example/index.php index de706f6..5c468d5 100644 --- a/include/icalparser/example/index.php +++ b/include/icalparser/example/index.php @@ -7,36 +7,26 @@ Ical Parser example - +

Czech holidays

- - - - - - - - + ', $event['DTSTART']->format('j.n.Y'), $event['SUMMARY']); + foreach ($cal->getSortedEvents() as $r) { + echo sprintf('
  • %s - %s
  • ' . PHP_EOL, $r['DTSTART']->format('j.n.Y'), $r['SUMMARY']); } - ?> - -
    DateSummary
    %s%s
    + + ?>
    - + \ No newline at end of file diff --git a/include/icalparser/readme.md b/include/icalparser/readme.md index 098e7a0..1b965c4 100644 --- a/include/icalparser/readme.md +++ b/include/icalparser/readme.md @@ -1,58 +1,44 @@ # PHP iCal Parser -[![GitHub Workflow Status](https://img.shields.io/github/workflow/status/OzzyCzech/icalparser/PHP%20Tests)](https://github.com/OzzyCzech/icalparser/actions) -[![Latest Stable Version](https://poser.pugx.org/om/icalparser/v/stable.png)](https://packagist.org/packages/om/icalparser) -[![Total Downloads](https://poser.pugx.org/om/icalparser/downloads.png)](https://packagist.org/packages/om/icalparser) -[![Latest Unstable Version](https://poser.pugx.org/om/icalparser/v/unstable.png)](https://packagist.org/packages/om/icalparser) -[![License](https://poser.pugx.org/om/icalparser/license.png)](https://packagist.org/packages/om/icalparser) +[![Build Status](https://travis-ci.org/OzzyCzech/icalparser.svg?branch=master)](https://travis-ci.org/OzzyCzech/icalparser) [![Latest Stable Version](https://poser.pugx.org/om/icalparser/v/stable.png)](https://packagist.org/packages/om/icalparser) [![Total Downloads](https://poser.pugx.org/om/icalparser/downloads.png)](https://packagist.org/packages/om/icalparser) [![Latest Unstable Version](https://poser.pugx.org/om/icalparser/v/unstable.png)](https://packagist.org/packages/om/icalparser) [![License](https://poser.pugx.org/om/icalparser/license.png)](https://packagist.org/packages/om/icalparser) -Internet Calendaring Parser [rfc2445](https://www.ietf.org/rfc/rfc2445.txt) or iCal parser is simple PHP 7.4+ class for parsing format into array. +Internet Calendaring Parser [rfc2445](http://www.ietf.org/rfc/rfc2445.txt) or iCal parser is simple PHP 5.6+ class for parsing format into array. ## How to install -The recommended way to is via Composer: - -```shell script +```bash composer require om/icalparser ``` -## Usage and example +## Usage ```php parseFile( 'https://www.google.com/calendar/ical/cs.czech%23holiday%40group.v.calendar.google.com/public/basic.ics' ); -foreach ($cal->getEvents()->sorted() as $event) { - printf('%s - %s' . PHP_EOL, $event['DTSTART']->format('j.n.Y'), $event['SUMMARY']); - +foreach ($cal->getSortedEvents() as $r) { + echo sprintf('
  • %s - %s
  • ' . PHP_EOL, $r['DTSTART']->format('j.n.Y'), $r['SUMMARY']); } ``` -You can run example with [PHP Built-in web server](https://www.php.net/manual/en/features.commandline.webserver.php) as follow: - -```shell -php -S localhost:8000 -t example -``` - ## Requirements -- PHP 7.4+ +- PHP 5.6+ ## Run tests -iCal parser using [Nette Tester](https://github.com/nette/tester). The tests can be invoked via [composer](https://getcomposer.org/). +iCal parser using [Nette Tester](https://github.com/nette/tester). +The tests can be invoked via [composer](https://getcomposer.org/). -```shell script +```bash composer update -composer test +composer tests ``` - + ## TODO -- add ATTENDEE support https://www.kanzaki.com/docs/ical/attendee.html +- add ATTENDEE support http://www.kanzaki.com/docs/ical/attendee.html diff --git a/include/icalparser/src/EventsList.php b/include/icalparser/src/EventsList.php deleted file mode 100644 index 3325368..0000000 --- a/include/icalparser/src/EventsList.php +++ /dev/null @@ -1,54 +0,0 @@ - - */ -class EventsList extends \ArrayObject { - - /** - * Return array of Events - * - * @return array - */ - public function getArrayCopy(): array { - return array_values(parent::getArrayCopy()); - } - - /** - * Return sorted EventList (the newest dates are first) - * - * @return $this - */ - public function sorted(): EventsList { - $this->uasort(static function ($a, $b): int { - if ($a['DTSTART'] === $b['DTSTART']) { - return 0; - } - return ($a['DTSTART'] < $b['DTSTART']) ? -1 : 1; - }); - - return $this; - } - - /** - * Return reversed sorted EventList (the oldest dates are first) - * - * @return $this - */ - public function reversed(): EventsList { - $this->uasort(static function ($a, $b): int { - if ($a['DTSTART'] === $b['DTSTART']) { - return 0; - } - return ($a['DTSTART'] > $b['DTSTART']) ? -1 : 1; - }); - - return $this; - } - -} \ No newline at end of file diff --git a/include/icalparser/src/Freq.php b/include/icalparser/src/Freq.php index 8557683..71ad208 100644 --- a/include/icalparser/src/Freq.php +++ b/include/icalparser/src/Freq.php @@ -1,11 +1,6 @@ 'monday', - 'TU' => 'tuesday', - 'WE' => 'wednesday', - 'TH' => 'thursday', - 'FR' => 'friday', - 'SA' => 'saturday', - 'SU' => 'sunday', + protected $weekdays = [ + 'MO' => 'monday', 'TU' => 'tuesday', 'WE' => 'wednesday', 'TH' => 'thursday', 'FR' => 'friday', 'SA' => 'saturday', + 'SU' => 'sunday' ]; - protected array $knownRules = [ - 'month', - 'weekno', - 'day', - 'monthday', - 'yearday', - 'hour', - 'minute', + protected $knownRules = [ + 'month', 'weekno', 'day', 'monthday', 'yearday', 'hour', 'minute' ]; //others : 'setpos', 'second' + protected $ruleModifiers = ['wkst']; + protected $simpleMode = true; - protected array $ruleModifiers = ['wkst']; - protected bool $simpleMode = true; + protected $rules = ['freq' => 'yearly', 'interval' => 1]; + protected $start = 0; + protected $freq = ''; - protected array $rules = ['freq' => 'yearly', 'interval' => 1]; - protected int $start = 0; - protected string $freq = ''; - - protected array $excluded; //EXDATE - protected array $added; //RDATE + protected $excluded; //EXDATE + protected $added; //RDATE protected $cache; // getAllOccurrences() /** - * Constructs a new Frequency-rule + * Constructs a new Freqency-rule * - * @param string|array $rule - * @param int $start Unix-timestamp (important : Need to be the start of Event) - * @param array $excluded of int (timestamps), see EXDATE documentation - * @param array $added of int (timestamps), see RDATE documentation - * @throws Exception + * @param $rule string + * @param $start int Unix-timestamp (important : Need to be the start of Event) + * @param $excluded array of int (timestamps), see EXDATE documentation + * @param $added array of int (timestamps), see RDATE documentation */ - public function __construct($rule, int $start, array $excluded = [], array $added = []) { + public function __construct($rule, $start, $excluded = [], $added = []) { $this->start = $start; $this->excluded = []; $rules = []; - foreach ($rule as $k => $v) { + foreach ($rule AS $k => $v) { $this->rules[strtolower($k)] = $v; } if (isset($this->rules['until']) && is_string($this->rules['until'])) { $this->rules['until'] = strtotime($this->rules['until']); - } elseif ($this->rules['until'] instanceof DateTime) { + } else if ($this->rules['until'] instanceof \DateTime) { $this->rules['until'] = $this->rules['until']->getTimestamp(); } $this->freq = strtolower($this->rules['freq']); - foreach ($this->knownRules as $rule) { + foreach ($this->knownRules AS $rule) { if (isset($this->rules['by' . $rule])) { if ($this->isPrerule($rule, $this->freq)) { $this->simpleMode = false; @@ -135,34 +116,95 @@ class Freq { $this->added = $added; } - private function isPrerule(string $rule, string $freq): bool { - if ($rule === 'year') { - return false; - } - if ($rule === 'month' && $freq === 'yearly') { - return true; - } - if ($rule === 'monthday' && in_array($freq, ['yearly', 'monthly']) && !isset($this->rules['byday'])) { - return true; - } - // TODO: is it faster to do monthday first, and ignore day if monthday exists? - prolly by a factor of 4.. - if ($rule === 'yearday' && $freq === 'yearly') { - return true; - } - if ($rule === 'weekno' && $freq === 'yearly') { - return true; - } - if ($rule === 'day' && in_array($freq, ['yearly', 'monthly', 'weekly'])) { - return true; - } - if ($rule === 'hour' && in_array($freq, ['yearly', 'monthly', 'weekly', 'daily'])) { - return true; - } - if ($rule === 'minute') { - return true; + /** + * Returns all timestamps array(), build the cache if not made before + * + * @return array + */ + public function getAllOccurrences() { + if (empty($this->cache)) { + $cache = []; + + //build cache + $next = $this->firstOccurrence(); + while ($next) { + $cache[] = $next; + $next = $this->findNext($next); + } + if (!empty($this->added)) { + $cache = array_unique(array_merge($cache, $this->added)); + asort($cache); + } + $this->cache = $cache; } - return false; + return $this->cache; + } + + /** + * Returns the previous (most recent) occurrence of the rule from the + * given offset + * + * @param int $offset + * @return int + */ + public function previousOccurrence($offset) { + if (!empty($this->cache)) { + $t2 = $this->start; + foreach ($this->cache as $ts) { + if ($ts >= $offset) + return $t2; + $t2 = $ts; + } + } else { + $ts = $this->start; + while (($t2 = $this->findNext($ts)) < $offset) { + if ($t2 == false) { + break; + } + $ts = $t2; + } + } + + return $ts; + } + + /** + * Returns the next occurrence of this rule after the given offset + * + * @param int $offset + * @return int + */ + public function nextOccurrence($offset) { + if ($offset < $this->start) + return $this->firstOccurrence(); + return $this->findNext($offset); + } + + /** + * Finds the first occurrence of the rule. + * + * @return int timestamp + */ + public function firstOccurrence() { + $t = $this->start; + if (in_array($t, $this->excluded)) + $t = $this->findNext($t); + + return $t; + } + + /** + * Finds the absolute last occurrence of the rule from the given offset. + * Builds also the cache, if not set before... + * + * @return int timestamp + */ + public function lastOccurrence() { + //build cache if not done + $this->getAllOccurrences(); + //return last timestamp in cache + return end($this->cache); } /** @@ -185,16 +227,14 @@ class Freq { * If no new timestamps were found in the period, we try in the * next period * - * @param int $offset - * @return int|bool - * @throws Exception + * @param int $offset + * @return int */ - public function findNext(int $offset) { + public function findNext($offset) { if (!empty($this->cache)) { foreach ($this->cache as $ts) { - if ($ts > $offset) { + if ($ts > $offset) return $ts; - } } } @@ -202,7 +242,8 @@ class Freq { //make sure the offset is valid if ($offset === false || (isset($this->rules['until']) && $offset > $this->rules['until'])) { - if ($debug) printf("STOP: %s\n", date('r', $offset)); + if ($debug) echo 'STOP: ' . date('r', $offset) . "\n"; + return false; } @@ -210,7 +251,7 @@ class Freq { //set the timestamp of the offset (ignoring hours and minutes unless we want them to be //part of the calculations. - if ($debug) printf("O: %s\n", date('r', $offset)); + if ($debug) echo 'O: ' . date('r', $offset) . "\n"; $hour = (in_array($this->freq, ['hourly', 'minutely']) && $offset > $this->start) ? date('H', $offset) : date( 'H', $this->start ); @@ -218,14 +259,13 @@ class Freq { 'i', $offset ) : date('i', $this->start); $t = mktime($hour, $minute, date('s', $this->start), date('m', $offset), date('d', $offset), date('Y', $offset)); - if ($debug) printf("START: %s\n", date('r', $t)); + if ($debug) echo 'START: ' . date('r', $t) . "\n"; if ($this->simpleMode) { if ($offset < $t) { $ts = $t; - if ($ts && in_array($ts, $this->excluded, true)) { + if ($ts && in_array($ts, $this->excluded)) $ts = $this->findNext($ts); - } } else { $ts = $this->findStartingPoint($t, $this->rules['interval'], false); if (!$this->validDate($ts)) { @@ -236,31 +276,22 @@ class Freq { return $ts; } - //EOP needs to have the same TIME as START ($t) - $tO = new DateTime('@' . $t, new DateTimeZone('UTC')); - $eop = $this->findEndOfPeriod($offset); - $eopO = new DateTime('@' . $eop, new DateTimeZone('UTC')); - $eopO->setTime($tO->format('H'), $tO->format('i'), $tO->format('s')); - $eop = $eopO->getTimestamp(); - unset($eopO, $tO); + if ($debug) echo 'EOP: ' . date('r', $eop) . "\n"; - if ($debug) { - echo 'EOP: ' . date('r', $eop) . "\n"; - } - foreach ($this->knownRules as $rule) { + foreach ($this->knownRules AS $rule) { if ($found && isset($this->rules['by' . $rule])) { if ($this->isPrerule($rule, $this->freq)) { - $subRules = explode(',', $this->rules['by' . $rule]); + $subrules = explode(',', $this->rules['by' . $rule]); $_t = null; - foreach ($subRules as $subRule) { - $imm = call_user_func_array([$this, "ruleBy$rule"], [$subRule, $t]); + foreach ($subrules AS $subrule) { + $imm = call_user_func_array([$this, 'ruleBy' . $rule], [$subrule, $t]); if ($imm === false) { break; } - if ($debug) { - printf("%s: %s A: %d\n", strtoupper($rule), date('r', $imm), intval($imm > $offset && $imm < $eop)); - } + if ($debug) echo strtoupper($rule) . ': ' . date( + 'r', $imm + ) . ' A: ' . ((int)($imm > $offset && $imm < $eop)) . "\n"; if ($imm > $offset && $imm <= $eop && ($_t == null || $imm < $_t)) { $_t = $imm; } @@ -288,9 +319,8 @@ class Freq { if ($debug) echo 'Not found' . "\n"; $ts = $this->findNext($this->findStartingPoint($offset, $this->rules['interval'])); } - if ($ts && in_array($ts, $this->excluded, true)) { + if ($ts && in_array($ts, $this->excluded)) return $this->findNext($ts); - } return $ts; } @@ -299,17 +329,17 @@ class Freq { * Finds the starting point for the next rule. It goes $interval * 'freq' forward in time since the given offset * - * @param int $offset - * @param int $interval - * @param boolean $truncate + * @param int $offset + * @param int $interval + * @param boolean $truncate * @return int */ - private function findStartingPoint(int $offset, int $interval, $truncate = true): int { + private function findStartingPoint($offset, $interval, $truncate = true) { $_freq = ($this->freq === 'daily') ? 'day__' : $this->freq; $t = '+' . $interval . ' ' . substr($_freq, 0, -2) . 's'; if ($_freq === 'monthly' && $truncate) { if ($interval > 1) { - $offset = strtotime('+' . ($interval - 1) . ' months ', $offset); // FIXME return type int|false + $offset = strtotime('+' . ($interval - 1) . ' months ', $offset); } $t = '+' . (date('t', $offset) - date('d', $offset) + 1) . ' days'; } @@ -323,17 +353,27 @@ class Freq { return $sp; } + /** + * Finds the earliest timestamp posible outside this perioid + * + * @param int $offset + * @return int + */ + public function findEndOfPeriod($offset) { + return $this->findStartingPoint($offset, 1); + } + /** * Resets the timestamp to the beginning of the * period specified by freq * * Yes - the fall-through is on purpose! * - * @param int $time - * @param string $freq + * @param int $time + * @param int $freq * @return int */ - private function truncateToPeriod(int $time, string $freq): int { + private function truncateToPeriod($time, $freq) { $date = getdate($time); switch ($freq) { case 'yearly': @@ -348,7 +388,7 @@ class Freq { $date['seconds'] = 0; break; case 'weekly': - if (date('N', $time) == 1) { // FIXME wrong compare, date return string|false + if (date('N', $time) == 1) { $date['hours'] = 0; $date['minutes'] = 0; $date['seconds'] = 0; @@ -357,179 +397,21 @@ class Freq { } break; } - return mktime($date['hours'], $date['minutes'], $date['seconds'], $date['mon'], $date['mday'], $date['year']); - } + $d = mktime($date['hours'], $date['minutes'], $date['seconds'], $date['mon'], $date['mday'], $date['year']); - private function validDate($t): bool { - if (isset($this->rules['until']) && $t > $this->rules['until']) { - return false; - } - - if (in_array($t, $this->excluded, true)) { - return false; - } - - if (isset($this->rules['bymonth'])) { - $months = explode(',', $this->rules['bymonth']); - if (!in_array(date('m', $t), $months, true)) { - return false; - } - } - if (isset($this->rules['byday'])) { - $days = explode(',', $this->rules['byday']); - foreach ($days as $i => $k) { - $days[$i] = $this->weekdays[preg_replace('/[^A-Z]/', '', $k)]; - } - if (!in_array(strtolower(date('l', $t)), $days, true)) { - return false; - } - } - if (isset($this->rules['byweekno'])) { - $weeks = explode(',', $this->rules['byweekno']); - if (!in_array(date('W', $t), $weeks, true)) { - return false; - } - } - if (isset($this->rules['bymonthday'])) { - $weekdays = explode(',', $this->rules['bymonthday']); - foreach ($weekdays as $i => $k) { - if ($k < 0) { - $weekdays[$i] = date('t', $t) + $k + 1; - } - } - if (!in_array(date('d', $t), $weekdays, true)) { - return false; - } - } - if (isset($this->rules['byhour'])) { - $hours = explode(',', $this->rules['byhour']); - if (!in_array(date('H', $t), $hours, true)) { - return false; - } - } - - return true; - } - - /** - * Finds the earliest timestamp possible outside this period. - * - * @param int $offset - * @return int - */ - public function findEndOfPeriod($offset = 0) { - return $this->findStartingPoint($offset, 1, false); - } - - /** - * Returns the previous (most recent) occurrence of the rule from the - * given offset - * - * @param int $offset - * @return int - * @throws Exception - */ - public function previousOccurrence(int $offset) { - if (!empty($this->cache)) { - $t2 = $this->start; - foreach ($this->cache as $ts) { - if ($ts >= $offset) { - return $t2; - } - $t2 = $ts; - } - } else { - $ts = $this->start; - while (($t2 = $this->findNext($ts)) < $offset) { - if ($t2 == false) { - break; - } - $ts = $t2; - } - } - - return $ts; - } - - /** - * Returns the next occurrence of this rule after the given offset - * - * @param int $offset - * @return int - * @throws Exception - */ - public function nextOccurrence(int $offset) { - if ($offset < $this->start) { - return $this->firstOccurrence(); - } - return $this->findNext($offset); - } - - /** - * Finds the first occurrence of the rule. - * - * @return int timestamp - * @throws Exception - */ - public function firstOccurrence() { - $t = $this->start; - if (in_array($t, $this->excluded)) { - $t = $this->findNext($t); - } - - return $t; - } - - /** - * Finds the absolute last occurrence of the rule from the given offset. - * Builds also the cache, if not set before... - * - * @return int timestamp - * @throws Exception - */ - public function lastOccurrence() { - //build cache if not done - $this->getAllOccurrences(); - //return last timestamp in cache - return end($this->cache); - } - - /** - * Returns all timestamps array(), build the cache if not made before - * - * @return array - * @throws Exception - */ - public function getAllOccurrences() { - if (empty($this->cache)) { - $cache = []; - - //build cache - $next = $this->firstOccurrence(); - while ($next) { - $cache[] = $next; - $next = $this->findNext($next); - } - if (!empty($this->added)) { - $cache = array_unique(array_merge($cache, $this->added)); - asort($cache); - } - $this->cache = $cache; - } - - return $this->cache; + return $d; } /** * Applies the BYDAY rule to the given timestamp * - * @param string $rule - * @param int $t + * @param string $rule + * @param int $t * @return int */ - private function ruleByDay(string $rule, int $t): int { - $dir = ($rule[0] === '-') ? -1 : 1; - $dir_t = ($dir === 1) ? 'next' : 'last'; + private function ruleByday($rule, $t) { + $dir = ($rule{0} == '-') ? -1 : 1; + $dir_t = ($dir == 1) ? 'next' : 'last'; $d = $this->weekdays[substr($rule, -2)]; $s = $dir_t . ' ' . $d . ' ' . date('H:i:s', $t); @@ -553,7 +435,7 @@ class Freq { if (isset($this->rules['bymonth']) && $this->freq === 'yearly') { $this->freq = 'monthly'; } - if ($dir === -1) { + if ($dir == -1) { $_t = $this->findEndOfPeriod($t); } else { $_t = $this->truncateToPeriod($t, $this->freq); @@ -565,7 +447,7 @@ class Freq { $n = $_t; while ($c > 0) { - if ($dir === 1 && $c == 1 && date('l', $t) == ucfirst($d)) { + if ($dir == 1 && $c == 1 && date('l', $t) == ucfirst($d)) { $s = 'today ' . date('H:i:s', $t); } $n = strtotime($s, $n); @@ -576,7 +458,7 @@ class Freq { } } - private function ruleByMonth($rule, int $t) { + private function ruleBymonth($rule, $t) { $_t = mktime(date('H', $t), date('i', $t), date('s', $t), $rule, date('d', $t), date('Y', $t)); if ($t == $_t && isset($this->rules['byday'])) { // TODO: this should check if one of the by*day's exists, and have a multi-day value @@ -586,7 +468,7 @@ class Freq { } } - private function ruleByMonthday($rule, int $t) { + private function ruleBymonthday($rule, $t) { if ($rule < 0) { $rule = date('t', $t) + $rule + 1; } @@ -594,7 +476,7 @@ class Freq { return mktime(date('H', $t), date('i', $t), date('s', $t), date('m', $t), $rule, date('Y', $t)); } - private function ruleByYearday($rule, int $t) { + private function ruleByyearday($rule, $t) { if ($rule < 0) { $_t = $this->findEndOfPeriod(); $d = '-'; @@ -607,7 +489,7 @@ class Freq { return strtotime($s, $_t); } - private function ruleByWeekno($rule, int $t) { + private function ruleByweekno($rule, $t) { if ($rule < 0) { $_t = $this->findEndOfPeriod(); $d = '-'; @@ -623,11 +505,96 @@ class Freq { return $_t; } - private function ruleByHour($rule, int $t) { - return mktime($rule, date('i', $t), date('s', $t), date('m', $t), date('d', $t), date('Y', $t)); + private function ruleByhour($rule, $t) { + $_t = mktime($rule, date('i', $t), date('s', $t), date('m', $t), date('d', $t), date('Y', $t)); + + return $_t; } - private function ruleByMinute($rule, int $t) { - return mktime(date('h', $t), $rule, date('s', $t), date('m', $t), date('d', $t), date('Y', $t)); + private function ruleByminute($rule, $t) { + $_t = mktime(date('h', $t), $rule, date('s', $t), date('m', $t), date('d', $t), date('Y', $t)); + + return $_t; + } + + private function validDate($t) { + if (isset($this->rules['until']) && $t > $this->rules['until']) { + return false; + } + + if (in_array($t, $this->excluded)) { + return false; + } + + if (isset($this->rules['bymonth'])) { + $months = explode(',', $this->rules['bymonth']); + if (!in_array(date('m', $t), $months)) { + return false; + } + } + if (isset($this->rules['byday'])) { + $days = explode(',', $this->rules['byday']); + foreach ($days As $i => $k) { + $days[$i] = $this->weekdays[preg_replace('/[^A-Z]/', '', $k)]; + } + if (!in_array(strtolower(date('l', $t)), $days)) { + return false; + } + } + if (isset($this->rules['byweekno'])) { + $weeks = explode(',', $this->rules['byweekno']); + if (!in_array(date('W', $t), $weeks)) { + return false; + } + } + if (isset($this->rules['bymonthday'])) { + $weekdays = explode(',', $this->rules['bymonthday']); + foreach ($weekdays As $i => $k) { + if ($k < 0) { + $weekdays[$i] = date('t', $t) + $k + 1; + } + } + if (!in_array(date('d', $t), $weekdays)) { + return false; + } + } + if (isset($this->rules['byhour'])) { + $hours = explode(',', $this->rules['byhour']); + if (!in_array(date('H', $t), $hours)) { + return false; + } + } + + return true; + } + + private function isPrerule($rule, $freq) { + if ($rule === 'year') + + return false; + if ($rule === 'month' && $freq === 'yearly') + + return true; + if ($rule === 'monthday' && in_array($freq, ['yearly', 'monthly']) && !isset($this->rules['byday'])) + + return true; + // TODO: is it faster to do monthday first, and ignore day if monthday exists? - prolly by a factor of 4.. + if ($rule === 'yearday' && $freq === 'yearly') + + return true; + if ($rule === 'weekno' && $freq === 'yearly') + + return true; + if ($rule === 'day' && in_array($freq, ['yearly', 'monthly', 'weekly'])) + + return true; + if ($rule === 'hour' && in_array($freq, ['yearly', 'monthly', 'weekly', 'daily'])) + + return true; + if ($rule === 'minute') + + return true; + + return false; } } diff --git a/include/icalparser/src/IcalParser.php b/include/icalparser/src/IcalParser.php index f471cdb..9a91656 100644 --- a/include/icalparser/src/IcalParser.php +++ b/include/icalparser/src/IcalParser.php @@ -1,47 +1,46 @@ + * @author Roman Ožana */ class IcalParser { - /** @var DateTimeZone */ - public DateTimeZone $timezone; - - /** @var array|null */ - public ?array $data = null; + /** @var \DateTimeZone */ + public $timezone; /** @var array */ - protected array $counters = []; + public $data = []; + + /** @var array */ + protected $counters = []; /** @var array */ private $windowsTimezones; + protected $arrayKeyMappings = [ + 'ATTACH' => 'ATTACHMENTS', + 'EXDATE' => 'EXDATES', + 'RDATE' => 'RDATES', + ]; + public function __construct() { $this->windowsTimezones = require __DIR__ . '/WindowsTimezones.php'; // load Windows timezones from separate file } /** * @param string $file - * @param callable|null $callback + * @param null $callback * @return array|null - * @throws Exception + * @throws \RuntimeException + * @throws \InvalidArgumentException + * @throws \Exception */ - public function parseFile(string $file, callable $callback = null): array { - if (!$handle = fopen($file, 'rb')) { - throw new RuntimeException('Can\'t open file' . $file . ' for reading'); + public function parseFile($file, $callback = null) { + if (!$handle = fopen($file, 'r')) { + throw new \RuntimeException('Can\'t open file' . $file . ' for reading'); } fclose($handle); @@ -50,12 +49,13 @@ class IcalParser { /** * @param string $string - * @param callable|null $callback + * @param null $callback * @param boolean $add if true the parsed string is added to existing data * @return array|null - * @throws Exception + * @throws \InvalidArgumentException + * @throws \Exception */ - public function parseString(string $string, callable $callback = null, bool $add = false): ?array { + public function parseString($string, $callback = null, $add = false) { if ($add === false) { // delete old data $this->data = []; @@ -63,7 +63,7 @@ class IcalParser { } if (!preg_match('/BEGIN:VCALENDAR/', $string)) { - throw new InvalidArgumentException('Invalid ICAL data format'); + throw new \InvalidArgumentException('Invalid ICAL data format'); } $section = 'VCALENDAR'; @@ -88,6 +88,7 @@ class IcalParser { $section = substr($row, 6); $this->counters[$section] = isset($this->counters[$section]) ? $this->counters[$section] + 1 : 0; continue 2; // while + break; case 'END:VEVENT': $section = substr($row, 4); $currCounter = $this->counters[$section]; @@ -97,6 +98,7 @@ class IcalParser { } continue 2; // while + break; case 'END:DAYLIGHT': case 'END:VALARM': case 'END:VTIMEZONE': @@ -105,6 +107,7 @@ class IcalParser { case 'END:STANDARD': case 'END:VTODO': continue 2; // while + break; case 'END:VCALENDAR': $veventSection = 'VEVENT'; @@ -123,9 +126,11 @@ class IcalParser { } } continue 2; // while + break; } - [$key, $middle, $value] = $this->parseRow($row); + list($key, $middle, $value) = $this->parseRow($row); + if ($callback) { // call user function for processing line @@ -134,33 +139,15 @@ class IcalParser { if ($section === 'VCALENDAR') { $this->data[$key] = $value; } else { - - // use an array since there can be multiple entries for this key. This does not - // break the current implementation--it leaves the original key alone and adds - // a new one specifically for the array of values. - - if ($newKey = $this->isMultipleKey($key)) { - $this->data[$section][$this->counters[$section]][$newKey][] = $value; - } - - // CATEGORIES can be multiple also but there is special case that there are comma separated categories - - if ($this->isMultipleKeyWithCommaSeparation($key)) { - - if (strpos($value, ',') !== false) { - $values = array_map('trim', preg_split('/(?data[$section][$this->counters[$section]][$key][] = $value; - } - - } else { - $this->data[$section][$this->counters[$section]][$key] = $value; + if (isset($this->arrayKeyMappings[$key])) { + // use an array since there can be multiple entries for this key. This does not + // break the current implementation--it leaves the original key alone and adds + // a new one specifically for the array of values. + $arrayKey = $this->arrayKeyMappings[$key]; + $this->data[$section][$this->counters[$section]][$arrayKey][] = $value; } + $this->data[$section][$this->counters[$section]][$key] = $value; } } @@ -169,12 +156,121 @@ class IcalParser { return ($callback) ? null : $this->data; } + /** + * @param $row + * @return array + */ + private function parseRow($row) { + preg_match('#^([\w-]+);?([\w-]+="[^"]*"|.*?):(.*)$#i', $row, $matches); + + $key = false; + $middle = null; + $value = null; + + if ($matches) { + $key = $matches[1]; + $middle = $matches[2]; + $value = $matches[3]; + $timezone = null; + + if ($key === 'X-WR-TIMEZONE' || $key === 'TZID') { + if (preg_match('#(\w+/\w+)$#i', $value, $matches)) { + $value = $matches[1]; + } + $value = $this->toTimezone($value); + $this->timezone = new \DateTimeZone($value); + } + + // have some middle part ? + if ($middle && preg_match_all('#(?[^=;]+)=(?[^;]+)#', $middle, $matches, PREG_SET_ORDER)) { + $middle = []; + foreach ($matches as $match) { + if ($match['key'] === 'TZID') { + $match['value'] = trim($match['value'], "'\""); + $match['value'] = $this->toTimezone($match['value']); + try { + $middle[$match['key']] = $timezone = new \DateTimeZone($match['value']); + } catch (\Exception $e) { + $middle[$match['key']] = $match['value']; + } + } else if ($match['key'] === 'ENCODING') { + if ($match['value'] === 'QUOTED-PRINTABLE') { + $value = quoted_printable_decode($value); + } + } + } + } + } + + // process simple dates with timezone + if (in_array($key, ['DTSTAMP', 'LAST-MODIFIED', 'CREATED', 'DTSTART', 'DTEND'], true)) { + try { + $value = new \DateTime($value, ($timezone ?: $this->timezone)); + } catch (\Exception $e) { + $value = null; + } + } else if (in_array($key, ['EXDATE', 'RDATE'])) { + $values = []; + foreach (explode(',', $value) as $singleValue) { + try { + $values[] = new \DateTime($singleValue, ($timezone ?: $this->timezone)); + } catch (\Exception $e) { + // pass + } + } + if (count($values) === 1) { + $value = $values[0]; + } else { + $value = $values; + } + } + + if ($key === 'RRULE' && preg_match_all('#(?[^=;]+)=(?[^;]+)#', $value, $matches, PREG_SET_ORDER)) { + $middle = null; + $value = []; + foreach ($matches as $match) { + if (in_array($match['key'], ['UNTIL'])) { + try { + $value[$match['key']] = new \DateTime($match['value'], ($timezone ?: $this->timezone)); + } catch (\Exception $e) { + $value[$match['key']] = $match['value']; + } + } else { + $value[$match['key']] = $match['value']; + } + } + } + + //split by comma, escape \, + if ($key === 'CATEGORIES') { + $value = preg_split('/(? '\\', '\\N' => "\n", '\\n' => "\n", '\\;' => ';', '\\,' => ',']); + } + } else { + $value = strtr($value, ['\\\\' => '\\', '\\N' => "\n", '\\n' => "\n", '\\;' => ';', '\\,' => ',']); + } + } + + return [$key, $middle, $value]; + } + /** * @param $event * @return array - * @throws Exception + * @throws \Exception */ - public function parseRecurrences($event): array { + public function parseRecurrences($event) { $recurring = new Recurrence($event['RRULE']); $exclusions = []; $additions = []; @@ -205,9 +301,9 @@ class IcalParser { $until = $recurring->getUntil(); if ($until === false) { - //forever... limit to 3 years from now - $end = new DateTime('now'); - $end->add(new DateInterval('P3Y')); // + 3 years + //forever... limit to 3 years + $end = clone($event['DTSTART']); + $end->add(new \DateInterval('P3Y')); // + 3 years $recurring->setUntil($end); $until = $recurring->getUntil(); } @@ -217,14 +313,14 @@ class IcalParser { $recurrenceTimestamps = $frequency->getAllOccurrences(); $recurrences = []; foreach ($recurrenceTimestamps as $recurrenceTimestamp) { - $tmp = new DateTime('now', $event['DTSTART']->getTimezone()); + $tmp = new \DateTime('now', $event['DTSTART']->getTimezone()); $tmp->setTimestamp($recurrenceTimestamp); $recurrenceIDDate = $tmp->format('Ymd'); $recurrenceIDDateTime = $tmp->format('Ymd\THis'); if (empty($this->data['_RECURRENCE_IDS'][$recurrenceIDDate]) && empty($this->data['_RECURRENCE_IDS'][$recurrenceIDDateTime])) { - $gmtCheck = new DateTime('now', new DateTimeZone('UTC')); + $gmtCheck = new \DateTime("now", new \DateTimeZone('UTC')); $gmtCheck->setTimestamp($recurrenceTimestamp); $recurrenceIDDateTimeZ = $gmtCheck->format('Ymd\THis\Z'); if (empty($this->data['_RECURRENCE_IDS'][$recurrenceIDDateTimeZ])) { @@ -236,172 +332,32 @@ class IcalParser { return $recurrences; } - private function parseRow($row): array { - preg_match('#^([\w-]+);?([\w-]+="[^"]*"|.*?):(.*)$#i', $row, $matches); - - $key = false; - $middle = null; - $value = null; - - if ($matches) { - $key = $matches[1]; - $middle = $matches[2]; - $value = $matches[3]; - $timezone = null; - - if ($key === 'X-WR-TIMEZONE' || $key === 'TZID') { - if (preg_match('#(\w+/\w+)$#i', $value, $matches)) { - $value = $matches[1]; - } - $value = $this->toTimezone($value); - $this->timezone = new DateTimeZone($value); - } - - // have some middle part ? - if ($middle && preg_match_all('#(?[^=;]+)=(?[^;]+)#', $middle, $matches, PREG_SET_ORDER)) { - $middle = []; - foreach ($matches as $match) { - if ($match['key'] === 'TZID') { - $match['value'] = trim($match['value'], "'\""); - $match['value'] = $this->toTimezone($match['value']); - try { - $middle[$match['key']] = $timezone = new DateTimeZone($match['value']); - } catch (Exception $e) { - $middle[$match['key']] = $match['value']; - } - } elseif ($match['key'] === 'ENCODING') { - if ($match['value'] === 'QUOTED-PRINTABLE') { - $value = quoted_printable_decode($value); - } - } - } - } - } - - // process simple dates with timezone - if (in_array($key, ['DTSTAMP', 'LAST-MODIFIED', 'CREATED', 'DTSTART', 'DTEND'], true)) { - try { - $value = new DateTime($value, ($timezone ?: $this->timezone)); - } catch (Exception $e) { - $value = null; - } - } elseif (in_array($key, ['EXDATE', 'RDATE'])) { - $values = []; - foreach (explode(',', $value) as $singleValue) { - try { - $values[] = new DateTime($singleValue, ($timezone ?: $this->timezone)); - } catch (Exception $e) { - // pass - } - } - if (count($values) === 1) { - $value = $values[0]; - } else { - $value = $values; - } - } - - if ($key === 'RRULE' && preg_match_all('#(?[^=;]+)=(?[^;]+)#', $value, $matches, PREG_SET_ORDER)) { - $middle = null; - $value = []; - foreach ($matches as $match) { - if (in_array($match['key'], ['UNTIL'])) { - try { - $value[$match['key']] = new DateTime($match['value'], ($timezone ?: $this->timezone)); - } catch (Exception $e) { - $value[$match['key']] = $match['value']; - } - } else { - $value[$match['key']] = $match['value']; - } - } - } - - //implement 4.3.11 Text ESCAPED-CHAR - $text_properties = [ - 'CALSCALE', 'METHOD', 'PRODID', 'VERSION', 'CATEGORIES', 'CLASS', 'COMMENT', 'DESCRIPTION', - 'LOCATION', 'RESOURCES', 'STATUS', 'SUMMARY', 'TRANSP', 'TZID', 'TZNAME', 'CONTACT', - 'RELATED-TO', 'UID', 'ACTION', 'REQUEST-STATUS', 'URL', - ]; - if (in_array($key, $text_properties, true) || strpos($key, 'X-') === 0) { - if (is_array($value)) { - foreach ($value as &$var) { - $var = strtr($var, ['\\\\' => '\\', '\\N' => "\n", '\\n' => "\n", '\\;' => ';', '\\,' => ',']); - } - } else { - $value = strtr($value, ['\\\\' => '\\', '\\N' => "\n", '\\n' => "\n", '\\;' => ';', '\\,' => ',']); - } - } - - return [$key, $middle, $value]; - } - /** - * Process timezone and return correct one... - * - * @param string $zone - * @return mixed|null + * @return array */ - private function toTimezone(string $zone) { - return $this->windowsTimezones[$zone] ?? $zone; - } - - public function isMultipleKey(string $key): ?string { - return (['ATTACH' => 'ATTACHMENTS', 'EXDATE' => 'EXDATES', 'RDATE' => 'RDATES'])[$key] ?? null; - } - - /** - * @param $key - * @return string|null - */ - public function isMultipleKeyWithCommaSeparation($key): ?string { - return (['X-CATEGORIES' => 'X-CATEGORIES', 'CATEGORIES' => 'CATEGORIES'])[$key] ?? null; - } - - public function getAlarms(): array { - return $this->data['VALARM'] ?? []; - } - - public function getTimezone(): array { - return $this->getTimezones(); - } - - public function getTimezones(): array { - return $this->data['VTIMEZONE'] ?? []; - } - - /** - * Return sorted event list as ArrayObject - * - * @deprecated use IcalParser::getEvents()->sorted() instead - */ - public function getSortedEvents(): \ArrayObject { - return $this->getEvents()->sorted(); - } - - public function getEvents(): EventsList { - $events = new EventsList(); + public function getEvents() { + $events = []; if (isset($this->data['VEVENT'])) { - foreach ($this->data['VEVENT'] as $iValue) { - $event = $iValue; + for ($i = 0; $i < count($this->data['VEVENT']); $i++) { + $event = $this->data['VEVENT'][$i]; if (empty($event['RECURRENCES'])) { if (!empty($event['RECURRENCE-ID']) && !empty($event['UID']) && isset($event['SEQUENCE'])) { $modifiedEventUID = $event['UID']; $modifiedEventRecurID = $event['RECURRENCE-ID']; - $modifiedEventSeq = (int)$event['SEQUENCE']; + $modifiedEventSeq = intval($event['SEQUENCE'], 10); - if (isset($this->data['_RECURRENCE_COUNTERS_BY_UID'][$modifiedEventUID])) { - $counter = $this->data['_RECURRENCE_COUNTERS_BY_UID'][$modifiedEventUID]; + if (isset($this->data["_RECURRENCE_COUNTERS_BY_UID"][$modifiedEventUID])) { + $counter = $this->data["_RECURRENCE_COUNTERS_BY_UID"][$modifiedEventUID]; - $originalEvent = $this->data['VEVENT'][$counter]; + $originalEvent = $this->data["VEVENT"][$counter]; if (isset($originalEvent['SEQUENCE'])) { - $originalEventSeq = (int)$originalEvent['SEQUENCE']; + $originalEventSeq = intval($originalEvent['SEQUENCE'], 10); $originalEventFormattedStartDate = $originalEvent['DTSTART']->format('Ymd\THis'); if ($modifiedEventRecurID === $originalEventFormattedStartDate && $modifiedEventSeq > $originalEventSeq) { // this modifies the original event $modifiedEvent = array_replace_recursive($originalEvent, $event); - $this->data['VEVENT'][$counter] = $modifiedEvent; + $this->data["VEVENT"][$counter] = $modifiedEvent; foreach ($events as $z => $event) { if ($events[$z]['UID'] === $originalEvent['UID'] && $events[$z]['SEQUENCE'] === $originalEvent['SEQUENCE']) { @@ -411,13 +367,13 @@ class IcalParser { } } $event = null; // don't add this to the $events[] array again - } elseif (!empty($originalEvent['RECURRENCES'])) { + } else if (!empty($originalEvent['RECURRENCES'])) { for ($j = 0; $j < count($originalEvent['RECURRENCES']); $j++) { $recurDate = $originalEvent['RECURRENCES'][$j]; $formattedStartDate = $recurDate->format('Ymd\THis'); if ($formattedStartDate === $modifiedEventRecurID) { - unset($this->data['VEVENT'][$counter]['RECURRENCES'][$j]); - $this->data['VEVENT'][$counter]['RECURRENCES'] = array_values($this->data['VEVENT'][$counter]['RECURRENCES']); + unset($this->data["VEVENT"][$counter]['RECURRENCES'][$j]); + $this->data["VEVENT"][$counter]['RECURRENCES'] = array_values($this->data["VEVENT"][$counter]['RECURRENCES']); break; } } @@ -427,7 +383,7 @@ class IcalParser { } if (!empty($event)) { - $events->append($event); + $events[] = $event; } } else { $recurrences = $event['RECURRENCES']; @@ -446,7 +402,7 @@ class IcalParser { } $newEvent['RECURRENCE_INSTANCE'] = $j; - $events->append($newEvent); + $events[] = $newEvent; $firstEvent = false; } } @@ -455,12 +411,61 @@ class IcalParser { return $events; } + /** - * @return \ArrayObject - * @deprecated use IcalParser::getEvents->reversed(); + * Process timezone and return correct one... + * + * @param string $zone + * @return mixed|null */ - public function getReverseSortedEvents(): \ArrayObject { - return $this->getEvents()->reversed(); + private function toTimezone($zone) { + return isset($this->windowsTimezones[$zone]) ? $this->windowsTimezones[$zone] : $zone; + } + + /** + * @return array + */ + public function getAlarms() { + return isset($this->data['VALARM']) ? $this->data['VALARM'] : []; + } + + /** + * @return array + */ + public function getTimezones() { + return isset($this->data['VTIMEZONE']) ? $this->data['VTIMEZONE'] : []; + } + + /** + * Return sorted event list as array + * + * @return array + */ + public function getSortedEvents() { + if ($events = $this->getEvents()) { + usort( + $events, function ($a, $b) { + return $a['DTSTART'] > $b['DTSTART']; + } + ); + return $events; + } + return []; + } + + /** + * @return array + */ + public function getReverseSortedEvents() { + if ($events = $this->getEvents()) { + usort( + $events, function ($a, $b) { + return $a['DTSTART'] < $b['DTSTART']; + } + ); + return $events; + } + return []; } } diff --git a/include/icalparser/src/Recurrence.php b/include/icalparser/src/Recurrence.php index 15f39cd..b199532 100644 --- a/include/icalparser/src/Recurrence.php +++ b/include/icalparser/src/Recurrence.php @@ -2,8 +2,7 @@ namespace om; -use DateTime; -use Exception; +use \DateTime; /** * Class taken from https://github.com/coopTilleuls/intouch-iCalendar.git (Recurrence.php) @@ -20,7 +19,6 @@ use Exception; * @license http://creativecommons.org/licenses/by-sa/2.5/dk/deed.en_GB CC-BY-SA-DK */ class Recurrence { - public $rrule; protected $freq; protected $until; @@ -41,9 +39,9 @@ class Recurrence { * * @var array */ - protected array $listProperties = [ + protected $listProperties = [ 'bysecond', 'byminute', 'byhour', 'byday', 'bymonthday', - 'byyearday', 'byweekno', 'bymonth', 'bysetpos', + 'byyearday', 'byweekno', 'bymonth', 'bysetpos' ]; /** @@ -52,7 +50,7 @@ class Recurrence { * @param array $rrule an om\icalparser row array which will be parsed to get the * desired information. */ - public function __construct(array $rrule) { + public function __construct($rrule) { $this->parseRrule($rrule); } @@ -62,7 +60,7 @@ class Recurrence { * * @param $rrule */ - protected function parseRrule($rrule): void { + protected function parseRrule($rrule) { $this->rrule = $rrule; //loop through the properties in the line and set their associated //member variables @@ -77,6 +75,36 @@ class Recurrence { } } + /** + * Set the $until member + * + * @param mixed timestamp (int) / Valid DateTime format (string) + */ + public function setUntil($ts) { + if ($ts instanceof DateTime) { + $dt = $ts; + } else if (is_int($ts)) { + $dt = new DateTime('@' . $ts); + } else { + $dt = new DateTime($ts); + } + $this->until = $dt->format('Ymd\THisO'); + $this->rrule['until'] = $this->until; + } + + /** + * Retrieves the desired member variable and returns it (if it's set) + * + * @param string $member name of the member variable + * @return mixed the variable value (if set), false otherwise + */ + protected function getMember($member) { + if (isset($this->$member)) { + return $this->$member; + } + return false; + } + /** * Returns the frequency - corresponds to FREQ in RFC 2445. * @@ -86,16 +114,6 @@ class Recurrence { return $this->getMember('freq'); } - /** - * Retrieves the desired member variable and returns it (if it's set) - * - * @param string $member name of the member variable - * @return mixed the variable value (if set), false otherwise - */ - protected function getMember(string $member) { - return $this->$member ?? false; - } - /** * Returns when the event will go until - corresponds to UNTIL in RFC 2445. * @@ -105,24 +123,6 @@ class Recurrence { return $this->getMember('until'); } - /** - * Set the $until member - * - * @param mixed $ts timestamp (int) / Valid DateTime format (string) - * @throws Exception - */ - public function setUntil($ts): void { - if ($ts instanceof DateTime) { - $dt = $ts; - } elseif (is_int($ts)) { - $dt = new DateTime('@' . $ts); - } else { - $dt = new DateTime($ts); - } - $this->until = $dt->format('Ymd\THisO'); - $this->rrule['until'] = $this->until; - } - /** * Returns the count of the times the event will occur (should only appear if 'until' * does not appear) - corresponds to COUNT in RFC 2445. diff --git a/include/icalparser/src/WindowsTimezones.php b/include/icalparser/src/WindowsTimezones.php index 9bfa391..f2105c3 100644 --- a/include/icalparser/src/WindowsTimezones.php +++ b/include/icalparser/src/WindowsTimezones.php @@ -1,5 +1,4 @@ 'Atlantic/Reykjavik', 'W. Europe Standard Time' => 'Europe/Berlin', '(UTC+01:00) Amsterdam, Berlin, Bern, Rome, Stockholm, Vienna' => 'Europe/Berlin', - '(UTC+01:00) Amsterdam\, Berlin\, Bern\, Rome\, Stockholm\, Vienna' => 'Europe/Berlin', - '(UTC+01:00) Amsterdam\, Berlin\, Bern\, Rom\, Stockholm\, Wien' => 'Europe/Berlin', 'Central Europe Standard Time' => 'Europe/Budapest', '(UTC+01:00) Belgrade, Bratislava, Budapest, Ljubljana, Prague' => 'Europe/Budapest', 'Romance Standard Time' => 'Europe/Paris', @@ -210,5 +207,4 @@ return [ '(UTC+13:00) Nuku\'alofa' => 'Pacific/Tongatapu', 'Samoa Standard Time' => 'Pacific/Apia', '(UTC-11:00)Samoa' => 'Pacific/Apia', - 'W. Europe Standard Time 1' => 'Europe/Berlin', -]; +]; \ No newline at end of file diff --git a/include/icalparser/tests/blank_description.phpt b/include/icalparser/tests/blank_description.phpt new file mode 100644 index 0000000..8ca0ec0 --- /dev/null +++ b/include/icalparser/tests/blank_description.phpt @@ -0,0 +1,19 @@ + + */ + +use Tester\Assert; +use Tester\Environment; + +require_once __DIR__ . '/../vendor/autoload.php'; +Environment::setup(); +date_default_timezone_set('Europe/Prague'); + +$cal = new \om\IcalParser(); +$results = $cal->parseFile(__DIR__ . '/cal/blank_description.ics'); + +Assert::same('', $results['VEVENT'][0]['DESCRIPTION']); +Assert::same('America/Los_Angeles', $cal->timezone->getName()); + +Assert::same($results['DAYLIGHT'][0]['RRULE']['FREQ'], 'YEARLY'); diff --git a/include/icalparser/tests/bootstrap.php b/include/icalparser/tests/bootstrap.php deleted file mode 100644 index 38a384d..0000000 --- a/include/icalparser/tests/bootstrap.php +++ /dev/null @@ -1,21 +0,0 @@ - - */ - -require_once __DIR__ . '/../vendor/autoload.php'; - -use Closure; -use Tester\Environment; - -function test($description, Closure $fn) { - printf("• %s%s%s", $description, PHP_EOL, $fn()); -} - -Environment::setup(); \ No newline at end of file diff --git a/include/icalparser/tests/cal/38_weekly_recurring_event_missing_day.ics b/include/icalparser/tests/cal/38_weekly_recurring_event_missing_day.ics deleted file mode 100644 index 26326c3..0000000 --- a/include/icalparser/tests/cal/38_weekly_recurring_event_missing_day.ics +++ /dev/null @@ -1,59 +0,0 @@ -BEGIN:VCALENDAR -PRODID:-//Google Inc//Google Calendar 70.9054//EN -VERSION:2.0 -CALSCALE:GREGORIAN -METHOD:PUBLISH -X-WR-CALNAME:Office Opening Hours -X-WR-TIMEZONE:Europe/London -BEGIN:VTIMEZONE -TZID:Europe/London -X-LIC-LOCATION:Europe/London -BEGIN:DAYLIGHT -TZOFFSETFROM:+0000 -TZOFFSETTO:+0100 -TZNAME:BST -DTSTART:19700329T010000 -RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU -END:DAYLIGHT -BEGIN:STANDARD -TZOFFSETFROM:+0100 -TZOFFSETTO:+0000 -TZNAME:GMT -DTSTART:19701025T020000 -RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU -END:STANDARD -END:VTIMEZONE -BEGIN:VEVENT -DTSTART;TZID=Europe/London:20190401T090000 -DTEND;TZID=Europe/London:20190401T170000 -RRULE:FREQ=WEEKLY;WKST=SU;BYDAY=MO,TU,WE,TH,FR -DTSTAMP:20190402T174536Z -UID:1nibcosj8r05bjoia671im7ulg@google.com -CREATED:20190401T144832Z -DESCRIPTION: -LAST-MODIFIED:20190401T145024Z -LOCATION: -SEQUENCE:0 -STATUS:CONFIRMED -SUMMARY:Office Opening Hours -TRANSP:OPAQUE -END:VEVENT -BEGIN:VEVENT -DTSTART;TZID=Europe/London:20190225T090000 -DTEND;TZID=Europe/London:20190225T170000 -RRULE:FREQ=WEEKLY;WKST=SU;UNTIL=20190329T235959Z;BYDAY=MO,TU,WE,TH,FR -DTSTAMP:20190402T174536Z -UID:7e581hcu1ub3nm0bb6c4o29suj@google.com -ATTENDEE;CUTYPE=INDIVIDUAL;ROLE=REQ-PARTICIPANT;PARTSTAT=ACCEPTED;CN=Office - Opening Hours;X-NUM-GUESTS=0:mailto:poweredpasture.com_la2jmsbphe5h11351kk - scnnqtg@group.calendar.google.com -CREATED:20190227T164630Z -DESCRIPTION: -LAST-MODIFIED:20190401T144725Z -LOCATION: -SEQUENCE:0 -STATUS:CONFIRMED -SUMMARY:Office Opening Hours -TRANSP:OPAQUE -END:VEVENT -END:VCALENDAR diff --git a/include/icalparser/tests/cal/missing_RRULE_notice.ics b/include/icalparser/tests/cal/missing_RRULE_notice.ics deleted file mode 100644 index 84ace92..0000000 --- a/include/icalparser/tests/cal/missing_RRULE_notice.ics +++ /dev/null @@ -1,39 +0,0 @@ -BEGIN:VCALENDAR -VERSION:2.0 -X-WR-CALNAME:URL -CALSCALE:GREGORIAN -BEGIN:VTIMEZONE -TZID:Europe/Berlin -TZURL:http://tzurl.org/zoneinfo-outlook/Europe/Berlin -X-LIC-LOCATION:Europe/Berlin -BEGIN:DAYLIGHT -TZOFFSETFROM:+0100 -TZOFFSETTO:+0200 -TZNAME:CEST -DTSTART:19700329T020000 -RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU -END:DAYLIGHT -BEGIN:STANDARD -TZOFFSETFROM:+0200 -TZOFFSETTO:+0100 -TZNAME:CET -DTSTART:19701025T030000 -RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU -END:STANDARD -END:VTIMEZONE -BEGIN:VEVENT -UID:111 -DTSTAMP:20181123T192651Z -CATEGORIES;LANGUAGE=de-DE:Party -CONTACT: -DESCRIPTION:xxx -DTSTART;TZID=Europe/Berlin:20160415T210000 -DTEND;TZID=Europe/Berlin:20160416T040000 -LOCATION:xxx -RDATE;TZID=Europe/Berlin:20161216T210000 -RDATE;TZID=Europe/Berlin:20161223T210000 -RDATE;TZID=Europe/Berlin:20161230T210000 -SEQUENCE:0 -SUMMARY:xxx -END:VEVENT -END:VCALENDAR diff --git a/include/icalparser/tests/cal/multiple_categories.ics b/include/icalparser/tests/cal/multiple_categories.ics deleted file mode 100755 index 5353845..0000000 --- a/include/icalparser/tests/cal/multiple_categories.ics +++ /dev/null @@ -1,67 +0,0 @@ -BEGIN:VCALENDAR -VERSION:2.0 -PRODID:Zimbra-Calendar-Provider -BEGIN:VTIMEZONE -TZID:America/Los_Angeles -BEGIN:STANDARD -DTSTART:19710101T020000 -TZOFFSETTO:-0800 -TZOFFSETFROM:-0700 -RRULE:FREQ=YEARLY;WKST=MO;INTERVAL=1;BYMONTH=11;BYDAY=1SU -TZNAME:PST -END:STANDARD -BEGIN:DAYLIGHT -DTSTART:19710101T020000 -TZOFFSETTO:-0700 -TZOFFSETFROM:-0800 -RRULE:FREQ=YEARLY;WKST=MO;INTERVAL=1;BYMONTH=3;BYDAY=2SU -TZNAME:PDT -END:DAYLIGHT -END:VTIMEZONE -BEGIN:VEVENT -UID:1334F9B7-6136-444E-A58D-472564C6AA73 -SUMMARY:sahaja <> frashed -DESCRIPTION:weekly 1on1 -CATEGORIES:one, two, three -ATTENDEE;CN=James Lal;CUTYPE=INDIVIDUAL;ROLE=REQ-PARTICIPANT;PARTSTAT=NEEDS - -ACTION;RSVP=TRUE:mailto:jlal@mozilla.com -ORGANIZER;CN=Faramarz Rashed:mailto:frashed@mozilla.com -DTSTART;TZID=America/Los_Angeles:20120326T110000 -DTEND;TZID=America/Los_Angeles:20120326T113000 -STATUS:CONFIRMED -CLASS:PUBLIC -TRANSP:OPAQUE -LAST-MODIFIED:20120326T161522Z -DTSTAMP:20120730T165637Z -SEQUENCE:9 -BEGIN:VALARM -ACTION:DISPLAY -TRIGGER;RELATED=START:-PT5M -DESCRIPTION:Reminder -END:VALARM -END:VEVENT -BEGIN:VEVENT -UID:14556F9B7-6136-444E-A58D-472564C6AA73 -SUMMARY:something something -DESCRIPTION:weekly 1on1 -CATEGORIES:one -CATEGORIES:two -CATEGORIES:three -ATTENDEE;CN=James Lal;CUTYPE=INDIVIDUAL;ROLE=REQ-PARTICIPANT;PARTSTAT=NEEDS - -ACTION;RSVP=TRUE:mailto:jlal@mozilla.com -ORGANIZER;CN=Faramarz Rashed:mailto:frashed@mozilla.com -DTSTART;TZID=America/Los_Angeles:20120326T110000 -DTEND;TZID=America/Los_Angeles:20120326T113000 -STATUS:CONFIRMED -CLASS:PUBLIC -TRANSP:OPAQUE -LAST-MODIFIED:20120326T161522Z -DTSTAMP:20120730T165637Z -SEQUENCE:9 -BEGIN:VALARM -ACTION:DISPLAY -TRIGGER;RELATED=START:-PT5M -DESCRIPTION:Reminder -END:VALARM -END:VEVENT -END:VCALENDAR diff --git a/include/icalparser/tests/cal/url.ics b/include/icalparser/tests/cal/url.ics deleted file mode 100644 index 8ae6620..0000000 --- a/include/icalparser/tests/cal/url.ics +++ /dev/null @@ -1,32 +0,0 @@ -BEGIN:VCALENDAR -VERSION:2.0 -X-WR-CALNAME:URL -CALSCALE:GREGORIAN -BEGIN:VTIMEZONE -TZID:Europe/Berlin -TZURL:http://tzurl.org/zoneinfo-outlook/Europe/Berlin -X-LIC-LOCATION:Europe/Berlin -BEGIN:DAYLIGHT -TZOFFSETFROM:+0100 -TZOFFSETTO:+0200 -TZNAME:CEST -DTSTART:19700329T020000 -RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU -END:DAYLIGHT -BEGIN:STANDARD -TZOFFSETFROM:+0200 -TZOFFSETTO:+0100 -TZNAME:CET -DTSTART:19701025T030000 -RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU -END:STANDARD -END:VTIMEZONE -BEGIN:VEVENT -DTSTAMP:20191106T093607Z -UID:20191106T093607Z-791992399@marudot.com -DTSTART;VALUE=DATE:20191101 -DTEND;VALUE=DATE:20191102 -SUMMARY:Example event -URL:https%3A%2F%2Fgithub.com%2FOzzyCzech%2Ficalparser%2F -END:VEVENT -END:VCALENDAR \ No newline at end of file diff --git a/include/icalparser/tests/event.attachements.phpt b/include/icalparser/tests/event.attachements.phpt deleted file mode 100644 index 1218fb7..0000000 --- a/include/icalparser/tests/event.attachements.phpt +++ /dev/null @@ -1,26 +0,0 @@ - - */ - -use om\IcalParser; -use Tester\Assert; -use Tester\Environment; -use function tests\test; - -require_once __DIR__ . '/bootstrap.php'; -date_default_timezone_set('Europe/Prague'); - -test('Event with multiple ATTACHMENTS', function () { - $cal = new IcalParser(); - $cal->parseFile(__DIR__ . '/cal/multiple_attachments.ics'); - $first = $cal->getEvents()->getIterator()->current(); - - // Backwards compatibility, there is only ever one key displayed - Assert::hasKey('ATTACH', $first); - Assert::type('string', $first['ATTACH']); - - // The new key 'ATTACHMENTS' is an array with 1 or more attachments - Assert::type('array', $first['ATTACHMENTS']); - Assert::count(2, $first['ATTACHMENTS']); -}); diff --git a/include/icalparser/tests/event.categories.phpt b/include/icalparser/tests/event.categories.phpt deleted file mode 100644 index 43c56cb..0000000 --- a/include/icalparser/tests/event.categories.phpt +++ /dev/null @@ -1,25 +0,0 @@ - - */ - -use om\IcalParser; -use Tester\Assert; -use function tests\test; - -require_once __DIR__ . '/bootstrap.php'; -date_default_timezone_set('Europe/Prague'); - -test('Multiple categories test', function () { - $cal = new IcalParser(); - $cal->parseFile(__DIR__ . '/cal/multiple_categories.ics'); - $events = $cal->getEvents()->sorted(); - - foreach ($events as $event) { - Assert::type('array', $event['CATEGORIES']); - Assert::same(['one', 'two', 'three'], $event['CATEGORIES']); - } -}); diff --git a/include/icalparser/tests/event.dates.phpt b/include/icalparser/tests/event.dates.phpt deleted file mode 100644 index 6af95ae..0000000 --- a/include/icalparser/tests/event.dates.phpt +++ /dev/null @@ -1,25 +0,0 @@ - - */ - -use om\IcalParser; -use Tester\Assert; -use function tests\test; - -require_once __DIR__ . '/bootstrap.php'; - -test('Events with wrong dates', function () { - $cal = new IcalParser(); - $cal->parseFile(__DIR__ . '/cal/wrong_dates.ics'); - $events = $cal->getEvents()->sorted(); - Assert::same('29.9.2014 00:00:00', $events[1]['DTSTART']->format('j.n.Y H:i:s')); - Assert::same(null, $events[1]['DTEND']); - - Assert::same(null, $events[0]['DTSTART']); - Assert::same('30.9.2014 00:00:00', $events[0]['DTEND']->format('j.n.Y H:i:s')); -}); - diff --git a/include/icalparser/tests/event.description.phpt b/include/icalparser/tests/event.description.phpt deleted file mode 100644 index 8426e32..0000000 --- a/include/icalparser/tests/event.description.phpt +++ /dev/null @@ -1,34 +0,0 @@ - - */ - -use om\IcalParser; -use Tester\Assert; -use function tests\test; - -require_once __DIR__ . '/bootstrap.php'; -date_default_timezone_set('Europe/Prague'); - -test('Blank description test', function () { - $cal = new IcalParser(); - $results = $cal->parseFile(__DIR__ . '/cal/blank_description.ics'); - $first = $cal->getEvents()->getIterator()->current(); - - Assert::hasKey('DESCRIPTION', $first); - Assert::same('', $first['DESCRIPTION']); -}); - -test('Multiple lines description', function () { - $cal = new IcalParser(); - $cal->parseFile(__DIR__ . '/cal/multiline_description.ics'); - $events = $cal->getEvents()->sorted(); - $first = $events->getIterator()->current(); - - Assert::same('30.6.2012 06:00:00', $first['DTSTART']->format('j.n.Y H:i:s')); - Assert::same("Here is a description that spans multiple lines!\n\nThis should be on a new line as well because the description contains newline characters.", $first['DESCRIPTION']); -}); - diff --git a/include/icalparser/tests/event.timezones.phpt b/include/icalparser/tests/event.timezones.phpt deleted file mode 100644 index fcb708b..0000000 --- a/include/icalparser/tests/event.timezones.phpt +++ /dev/null @@ -1,39 +0,0 @@ - - */ - -use om\IcalParser; -use Tester\Assert; -use function tests\test; - -require_once __DIR__ . '/bootstrap.php'; -date_default_timezone_set('Europe/Paris'); - -test('Normal time zone', function () { - $cal = new IcalParser(); - $cal->parseFile(__DIR__ . '/cal/blank_description.ics'); - Assert::same('America/Los_Angeles', $cal->timezone->getName()); -}); - -test('Negative zero UTC timezone', function () { - $cal = new IcalParser(); - $cal->parseFile(__DIR__ . '/cal/utc_negative_zero.ics'); - Assert::same('Etc/GMT', $cal->timezone->getName()); -}); - -/** - * Time zone with custom prefixes (Mozilla files tken from here: https://www.mozilla.org/en-US/projects/calendar/holidays/) - */ -test('Time zone with custom prefixes', function () { - $cal = new IcalParser(); - $cal->parseFile(__DIR__ . '/cal/FrenchHolidays.ics'); - Assert::same('Europe/Paris', $cal->timezone->getName()); -}); - -test('Weird windows timezones', function () { - $cal = new IcalParser(); - $cal->parseFile(__DIR__ . '/cal/weird_windows_timezones.ics'); - $cal->getEvents()->sorted(); - Assert::same('Atlantic/Reykjavik', $cal->timezone->getName()); -}); diff --git a/include/icalparser/tests/event.url.phpt b/include/icalparser/tests/event.url.phpt deleted file mode 100644 index b39d3e3..0000000 --- a/include/icalparser/tests/event.url.phpt +++ /dev/null @@ -1,22 +0,0 @@ - - */ - -use om\IcalParser; -use Tester\Assert; -use function tests\test; - -require_once __DIR__ . '/bootstrap.php'; - -test('URL parsing check', function () { - $cal = new IcalParser(); - $cal->parseFile(__DIR__ . '/cal/url.ics'); - $first = $cal->getEvents()->getIterator()->current(); - - Assert::hasKey('URL', $first); - Assert::same($first['URL'], urlencode('https://github.com/OzzyCzech/icalparser/')); -}); diff --git a/include/icalparser/tests/events.recurring.phpt b/include/icalparser/tests/events.recurring.phpt deleted file mode 100644 index 66136a9..0000000 --- a/include/icalparser/tests/events.recurring.phpt +++ /dev/null @@ -1,241 +0,0 @@ - - * @author Roman Ožana - */ - -use om\IcalParser; -use Tester\Assert; -use function tests\test; - -require_once __DIR__ . '/bootstrap.php'; - -test('Recurring instances finite', function () { - $cal = new IcalParser(); - - $cal->parseFile(__DIR__ . '/cal/recur_instances_finite.ics'); - $events = $cal->getEvents()->sorted(); - -// DTSTART;TZID=America/Los_Angeles:20121002T100000 -// DTEND;TZID=America/Los_Angeles:20121002T103000 -// RRULE:FREQ=MONTHLY;INTERVAL=1;BYDAY=1TU;UNTIL=20121231T100000 -// RDATE;TZID=America/Los_Angeles:20121110T100000 -// RDATE;TZID=America/Los_Angeles:20121105T100000 - Assert::equal(5, $events->count()); - Assert::equal('2.10.2012 10:00:00', $events[0]['DTSTART']->format('j.n.Y H:i:s')); - Assert::equal('5.11.2012 10:00:00', $events[1]['DTSTART']->format('j.n.Y H:i:s')); - Assert::equal('6.11.2012 10:00:00', $events[2]['DTSTART']->format('j.n.Y H:i:s')); - Assert::equal('10.11.2012 10:00:00', $events[3]['DTSTART']->format('j.n.Y H:i:s')); - Assert::equal('4.12.2012 10:00:00', $events[4]['DTSTART']->format('j.n.Y H:i:s')); -}); - -test('Recurring instance check', function () { - $cal = new IcalParser(); - $results = $cal->parseFile(__DIR__ . '/cal/recur_instances.ics'); - $events = $cal->getEvents()->sorted(); - - $recurrences = []; - foreach ($events as $i => $event) { - $recurrences[] = $event['DTSTART']; - } - -// DTSTART;TZID=America/Los_Angeles:20121002T100000 -// DTEND;TZID=America/Los_Angeles:20121002T103000 -// RRULE:FREQ=MONTHLY;INTERVAL=1;BYDAY=1TU -// RDATE;TZID=America/Los_Angeles:20121105T100000 -// RDATE;TZID=America/Los_Angeles:20121110T100000,20121130T100000 -// EXDATE;TZID=America/Los_Angeles:20130402T100000 -// EXDATE;TZID=America/Los_Angeles:20121204T100000 -// EXDATE;TZID=America/Los_Angeles:20130205T100000 -// because there is no "UNTIL", we calculate until 3 years from now of repeating events - $now = new DateTime('now'); - $diff = $now->diff(new DateTime('20121002T100000')); - $count = ($diff->y + 3) * 12 + $diff->m; - Assert::equal($count, count($recurrences)); - Assert::equal('02.10.2012 15:00:00', $recurrences[0]->format('d.m.Y H:i:s')); - Assert::equal('06.11.2012 20:00:00', $recurrences[1]->format('d.m.Y H:i:s')); - Assert::equal('10.11.2012 10:00:00', $recurrences[2]->format('d.m.Y H:i:s')); - Assert::equal('30.11.2012 10:00:00', $recurrences[3]->format('d.m.Y H:i:s')); - Assert::equal('01.01.2013 10:00:00', $recurrences[4]->format('d.m.Y H:i:s')); - Assert::equal('05.03.2013 10:00:00', $recurrences[5]->format('d.m.Y H:i:s')); - Assert::equal('07.05.2013 10:00:00', $recurrences[6]->format('d.m.Y H:i:s')); - Assert::equal('04.06.2013 10:00:00', $recurrences[7]->format('d.m.Y H:i:s')); - Assert::equal('02.07.2013 10:00:00', $recurrences[8]->format('d.m.Y H:i:s')); - Assert::equal('06.08.2013 10:00:00', $recurrences[9]->format('d.m.Y H:i:s')); - Assert::equal('03.09.2013 10:00:00', $recurrences[10]->format('d.m.Y H:i:s')); - Assert::equal('01.10.2013 10:00:00', $recurrences[11]->format('d.m.Y H:i:s')); - Assert::equal('05.11.2013 10:00:00', $recurrences[12]->format('d.m.Y H:i:s')); - Assert::equal('03.12.2013 10:00:00', $recurrences[13]->format('d.m.Y H:i:s')); - Assert::equal('07.01.2014 10:00:00', $recurrences[14]->format('d.m.Y H:i:s')); - Assert::equal('04.02.2014 10:00:00', $recurrences[15]->format('d.m.Y H:i:s')); - Assert::equal('04.03.2014 10:00:00', $recurrences[16]->format('d.m.Y H:i:s')); - Assert::equal('01.04.2014 10:00:00', $recurrences[17]->format('d.m.Y H:i:s')); - Assert::equal('06.05.2014 10:00:00', $recurrences[18]->format('d.m.Y H:i:s')); - Assert::equal('03.06.2014 10:00:00', $recurrences[19]->format('d.m.Y H:i:s')); - Assert::equal('01.07.2014 10:00:00', $recurrences[20]->format('d.m.Y H:i:s')); - Assert::equal('05.08.2014 10:00:00', $recurrences[21]->format('d.m.Y H:i:s')); - Assert::equal('02.09.2014 10:00:00', $recurrences[22]->format('d.m.Y H:i:s')); - Assert::equal('07.10.2014 10:00:00', $recurrences[23]->format('d.m.Y H:i:s')); - Assert::equal('04.11.2014 10:00:00', $recurrences[24]->format('d.m.Y H:i:s')); - Assert::equal('02.12.2014 10:00:00', $recurrences[25]->format('d.m.Y H:i:s')); - Assert::equal('06.01.2015 10:00:00', $recurrences[26]->format('d.m.Y H:i:s')); - Assert::equal('03.02.2015 10:00:00', $recurrences[27]->format('d.m.Y H:i:s')); - Assert::equal('03.03.2015 10:00:00', $recurrences[28]->format('d.m.Y H:i:s')); - Assert::equal('07.04.2015 10:00:00', $recurrences[29]->format('d.m.Y H:i:s')); - Assert::equal('05.05.2015 10:00:00', $recurrences[30]->format('d.m.Y H:i:s')); - Assert::equal('02.06.2015 10:00:00', $recurrences[31]->format('d.m.Y H:i:s')); - Assert::equal('07.07.2015 10:00:00', $recurrences[32]->format('d.m.Y H:i:s')); - Assert::equal('04.08.2015 10:00:00', $recurrences[33]->format('d.m.Y H:i:s')); - Assert::equal('01.09.2015 10:00:00', $recurrences[34]->format('d.m.Y H:i:s')); - - foreach ($events->getIterator()->current()['EXDATES'] as $exDate) { - Assert::notContains($exDate, $recurrences); - } -}); - -test('Recurrent event with modifications at single date', function () { - $cal = new IcalParser(); - $cal->parseFile(__DIR__ . '/cal/recur_instances_with_modifications.ics'); - $events = $cal->getEvents()->sorted(); - - // There should be 36 total events because of the modified event + 35 recurrences - Assert::count(36, $events); // 36 events - - // There should be 35 total recurrences because the modified event should've removed 1 recurrence - Assert::hasKey('RECURRENCES', $events->offsetGet(1)); - $recurrences = $events->getIterator()->current()['RECURRENCES']; - Assert::count(35, $recurrences); - - // reccurent event don't have RECURRENCES - foreach (range(2, 35) as $index) { - Assert::hasNotKey('RECURRENCES', $events->offsetGet($index)); - } - - // the date 8.8.2016 should be modified - $modifiedEvent = $events->offsetGet(0); - Assert::hasNotKey('RECURRENCES', $modifiedEvent); -// the 12th entry is the modified event, related to the remaining recurring events - Assert::same('8.8.2016', $modifiedEvent['DTSTART']->format('j.n.Y')); - Assert::notContains($modifiedEvent['DTSTART'], $recurrences); -}); - -test('Recuring instances with modifications and interval', function () { - $cal = new IcalParser(); - $results = $cal->parseFile(__DIR__ . '/cal/recur_instances_with_modifications_and_interval.ics'); - -// Build the cache of RECURRENCE-IDs and EXDATES first, so that we can properly determine the interval - $eventCache = []; - foreach ($results['VEVENT'] as $event) { - $eventSequence = empty($event['SEQUENCE']) ? "0" : $event['SEQUENCE']; - $eventRecurrenceID = empty($event['RECURRENCE-ID']) ? "0" : $event['RECURRENCE-ID']; - $eventCache[$event['UID']][$eventRecurrenceID][$eventSequence] = $event; - } - $trueEvents = []; - foreach ($results['VEVENT'] as $event) { - if (empty($event['RECURRENCES'])) { - $trueEvents[] = $event; - } else { - $eventUID = $event['UID']; - foreach ($event['RECURRENCES'] as $recurrence) { - $eventRecurrenceID = $recurrence->format("Ymd"); - if (empty($eventCache[$eventUID][$eventRecurrenceID])) { - $trueEvents[$eventRecurrenceID] = ['DTSTART' => $recurrence]; - } else { - krsort($eventCache[$eventUID][$eventRecurrenceID]); - $keys = array_keys($eventCache[$eventUID][$eventRecurrenceID]); - $trueEvents[$eventRecurrenceID] = $eventCache[$eventUID][$eventRecurrenceID][$keys[0]]; - } - } - } - } - - usort( - $trueEvents, - static function ($a, $b): int { - return ($a['DTSTART'] > $b['DTSTART']) ? 1 : -1; - } - ); - - $events = $cal->getEvents()->sorted()->getArrayCopy(); - - Assert::false(empty($events[0]['RECURRENCES'])); - Assert::equal(count($trueEvents), count($events)); - foreach ($trueEvents as $index => $trueEvent) { - Assert::equal($trueEvent['DTSTART']->format("Ymd"), $events[$index]['DTSTART']->format("Ymd")); - } - -}); - -test('', function () { - $cal = new IcalParser(); -// There is still an issue that needs to be resolved when modifications are made to the initial event that is the -// base of the recurrences. The below ICS file has a great edge case example: one event, no recurrences in the -// recurring ruleset, and a modification to the initial event. - $results = $cal->parseFile(__DIR__ . '/cal/recur_instances_with_modifications_to_first_day.ics'); - $events = $cal->getEvents()->sorted()->getArrayCopy(); - Assert::true(empty($events[0]['RECURRENCES'])); // edited event - Assert::true(empty($events[1]['RECURRENCES'])); // recurring event base with no recurrences - Assert::equal(1, count($events)); -}); - -test('', function () { - $cal = new IcalParser(); - $results = $cal->parseFile(__DIR__ . '/cal/daily_recur.ics'); - $events = $cal->getEvents()->sorted()->getArrayCopy(); - $period = new DatePeriod(new DateTime('20120801T050000'), new DateInterval('P1D'), new DateTime('20150801T050000')); - foreach ($period as $i => $day) { - Assert::equal($day->format('j.n.Y H:i:s'), $events[$i]['DTSTART']->format('j.n.Y H:i:s')); - } -}); - -test('', function () { - $cal = new IcalParser(); - $results = $cal->parseFile(__DIR__ . '/cal/daily_recur2.ics'); - $events = $cal->getEvents()->sorted()->getArrayCopy(); - - Assert::equal(4, count($events)); - Assert::equal('21.8.2017 00:00:00', $events[0]['DTSTART']->format('j.n.Y H:i:s')); - Assert::equal('28.8.2017 00:00:00', $events[1]['DTSTART']->format('j.n.Y H:i:s')); - Assert::equal('4.9.2017 00:00:00', $events[2]['DTSTART']->format('j.n.Y H:i:s')); - Assert::equal('11.9.2017 00:00:00', $events[3]['DTSTART']->format('j.n.Y H:i:s')); -}); - -test('', function () { -//https://github.com/OzzyCzech/icalparser/issues/38 - $cal = new IcalParser(); - $cal->parseFile(__DIR__ . '/cal/38_weekly_recurring_event_missing_day.ics'); - $events = $cal->getEvents()->sorted()->getArrayCopy(); - - //first monday - Assert::equal('25.2.2019 09:00:00', $events[0]['DTSTART']->format('j.n.Y H:i:s')); - //rest of week - Assert::equal('26.2.2019 09:00:00', $events[1]['DTSTART']->format('j.n.Y H:i:s')); - Assert::equal('27.2.2019 09:00:00', $events[2]['DTSTART']->format('j.n.Y H:i:s')); - Assert::equal('28.2.2019 09:00:00', $events[3]['DTSTART']->format('j.n.Y H:i:s')); - Assert::equal('1.3.2019 09:00:00', $events[4]['DTSTART']->format('j.n.Y H:i:s')); - //now check the next 4 mondays to make sure they exist as well - Assert::equal('4.3.2019 09:00:00', $events[5]['DTSTART']->format('j.n.Y H:i:s')); - Assert::equal('11.3.2019 09:00:00', $events[10]['DTSTART']->format('j.n.Y H:i:s')); - Assert::equal('18.3.2019 09:00:00', $events[15]['DTSTART']->format('j.n.Y H:i:s')); - Assert::equal('25.3.2019 09:00:00', $events[20]['DTSTART']->format('j.n.Y H:i:s')); - - //Last week that works correctly - Assert::equal('1.4.2019 09:00:00', $events[25]['DTSTART']->format('j.n.Y H:i:s')); - Assert::equal('2.4.2019 09:00:00', $events[26]['DTSTART']->format('j.n.Y H:i:s')); - Assert::equal('3.4.2019 09:00:00', $events[27]['DTSTART']->format('j.n.Y H:i:s')); - Assert::equal('4.4.2019 09:00:00', $events[28]['DTSTART']->format('j.n.Y H:i:s')); - Assert::equal('5.4.2019 09:00:00', $events[29]['DTSTART']->format('j.n.Y H:i:s')); - - //This week starts failing - Assert::equal('8.4.2019 09:00:00', $events[30]['DTSTART']->format('j.n.Y H:i:s')); - Assert::equal('9.4.2019 09:00:00', $events[31]['DTSTART']->format('j.n.Y H:i:s')); - Assert::equal('10.4.2019 09:00:00', $events[32]['DTSTART']->format('j.n.Y H:i:s')); - Assert::equal('11.4.2019 09:00:00', $events[33]['DTSTART']->format('j.n.Y H:i:s')); - Assert::equal('12.4.2019 09:00:00', $events[34]['DTSTART']->format('j.n.Y H:i:s')); - - Assert::equal('15.4.2019 09:00:00', $events[35]['DTSTART']->format('j.n.Y H:i:s')); - Assert::equal('16.4.2019 09:00:00', $events[36]['DTSTART']->format('j.n.Y H:i:s')); - Assert::equal('17.4.2019 09:00:00', $events[37]['DTSTART']->format('j.n.Y H:i:s')); - Assert::equal('18.4.2019 09:00:00', $events[38]['DTSTART']->format('j.n.Y H:i:s')); - Assert::equal('19.4.2019 09:00:00', $events[39]['DTSTART']->format('j.n.Y H:i:s')); -}); diff --git a/include/icalparser/tests/events.sorting.phpt b/include/icalparser/tests/events.sorting.phpt deleted file mode 100644 index 5463396..0000000 --- a/include/icalparser/tests/events.sorting.phpt +++ /dev/null @@ -1,36 +0,0 @@ - - */ - -use om\IcalParser; -use Tester\Assert; -use function tests\test; - -require_once __DIR__ . '/bootstrap.php'; - -date_default_timezone_set('Europe/Prague'); - -test('Natural sort order by date', function () { - $cal = new IcalParser(); - $cal->parseFile(__DIR__ . '/cal/basic.ics'); - $first = $cal->getEvents()->sorted()->getIterator()->current(); - Assert::same('1.1.2013 00:00:00', $first['DTSTART']->format('j.n.Y H:i:s')); -}); - -test('Reverse events sort (parseFile)', function () { - $cal = new IcalParser(); - $cal->parseFile(__DIR__ . '/cal/basic.ics'); - $first = $cal->getEvents()->reversed()->getIterator()->current(); - Assert::same('26.12.2015 00:00:00', $first['DTSTART']->format('j.n.Y H:i:s')); -}); - -test('Reverse events sort (parseString)', function () { - $cal = new IcalParser(); - $cal->parseString(file_get_contents(__DIR__ . '/cal/basic.ics')); - $first = $cal->getEvents()->reversed()->getIterator()->current(); - Assert::same('26.12.2015 00:00:00', $first['DTSTART']->format('j.n.Y H:i:s')); -}); diff --git a/include/icalparser/tests/multiline_description.phpt b/include/icalparser/tests/multiline_description.phpt new file mode 100644 index 0000000..c4f5d23 --- /dev/null +++ b/include/icalparser/tests/multiline_description.phpt @@ -0,0 +1,18 @@ + + */ + +use Tester\Assert; +use Tester\Environment; + +require_once __DIR__ . '/../vendor/autoload.php'; +Environment::setup(); +date_default_timezone_set('Europe/Prague'); + +// sort by date +$cal = new \om\IcalParser(); +$results = $cal->parseFile(__DIR__ . '/cal/multiline_description.ics'); +$events = $cal->getSortedEvents(); +Assert::same('30.6.2012 06:00:00', $events[0]['DTSTART']->format('j.n.Y H:i:s')); +Assert::same("Here is a description that spans multiple lines!\n\nThis should be on a new line as well because the description contains newline characters.", $events[0]['DESCRIPTION']); diff --git a/include/icalparser/tests/multiple_attachments.phpt b/include/icalparser/tests/multiple_attachments.phpt new file mode 100644 index 0000000..c689748 --- /dev/null +++ b/include/icalparser/tests/multiple_attachments.phpt @@ -0,0 +1,20 @@ + + */ + +use Tester\Assert; + +require_once __DIR__ . '/../vendor/autoload.php'; +\Tester\Environment::setup(); +date_default_timezone_set('Europe/Prague'); + +$cal = new \om\IcalParser(); +$results = $cal->parseFile(__DIR__ . '/cal/multiple_attachments.ics'); + +// Backwards compatibility, there is only ever one key displayed +Assert::type('string', $results['VEVENT'][0]['ATTACH']); + +// The new key 'ATTACHMENTS' is an array with 1 or more attachments +Assert::type('array', $results['VEVENT'][0]['ATTACHMENTS']); +Assert::count(2, $results['VEVENT'][0]['ATTACHMENTS']); diff --git a/include/icalparser/tests/recurring_events.phpt b/include/icalparser/tests/recurring_events.phpt new file mode 100644 index 0000000..ed73c3f --- /dev/null +++ b/include/icalparser/tests/recurring_events.phpt @@ -0,0 +1,170 @@ + + */ + +use Tester\Assert; +use Tester\Environment; + +require_once __DIR__ . '/../vendor/autoload.php'; +Environment::setup(); + +$cal = new \om\IcalParser(); + + +$results = $cal->parseFile(__DIR__ . '/cal/recur_instances_finite.ics'); +$events = $cal->getSortedEvents(); + +// DTSTART;TZID=America/Los_Angeles:20121002T100000 +// DTEND;TZID=America/Los_Angeles:20121002T103000 +// RRULE:FREQ=MONTHLY;INTERVAL=1;BYDAY=1TU;UNTIL=20121231T100000 +// RDATE;TZID=America/Los_Angeles:20121110T100000 +// RDATE;TZID=America/Los_Angeles:20121105T100000 +Assert::equal(5, sizeof($events)); +Assert::equal('2.10.2012 10:00:00', $events[0]['DTSTART']->format('j.n.Y H:i:s')); +Assert::equal('5.11.2012 10:00:00', $events[1]['DTSTART']->format('j.n.Y H:i:s')); +Assert::equal('6.11.2012 10:00:00', $events[2]['DTSTART']->format('j.n.Y H:i:s')); +Assert::equal('10.11.2012 10:00:00', $events[3]['DTSTART']->format('j.n.Y H:i:s')); +Assert::equal('4.12.2012 10:00:00', $events[4]['DTSTART']->format('j.n.Y H:i:s')); + +$results = $cal->parseFile(__DIR__ . '/cal/recur_instances.ics'); +$events = $cal->getSortedEvents(); + +$recurrences = []; +foreach($events as $i => $event) { + $recurrences[] = $event['DTSTART']; +} + +// DTSTART;TZID=America/Los_Angeles:20121002T100000 +// DTEND;TZID=America/Los_Angeles:20121002T103000 +// RRULE:FREQ=MONTHLY;INTERVAL=1;BYDAY=1TU +// RDATE;TZID=America/Los_Angeles:20121105T100000 +// RDATE;TZID=America/Los_Angeles:20121110T100000,20121130T100000 +// EXDATE;TZID=America/Los_Angeles:20130402T100000 +// EXDATE;TZID=America/Los_Angeles:20121204T100000 +// EXDATE;TZID=America/Los_Angeles:20130205T100000 +// total = 36 events - 3 exclusions + 3 additions +// because there is no "UNTIL", we only calculate the next 3 years of repeating events +Assert::equal(35, sizeof($recurrences)); +Assert::equal('02.10.2012 15:00:00', $recurrences[0]->format('d.m.Y H:i:s')); +Assert::equal('06.11.2012 20:00:00', $recurrences[1]->format('d.m.Y H:i:s')); +Assert::equal('10.11.2012 10:00:00', $recurrences[2]->format('d.m.Y H:i:s')); +Assert::equal('30.11.2012 10:00:00', $recurrences[3]->format('d.m.Y H:i:s')); +Assert::equal('01.01.2013 10:00:00', $recurrences[4]->format('d.m.Y H:i:s')); +Assert::equal('05.03.2013 10:00:00', $recurrences[5]->format('d.m.Y H:i:s')); +Assert::equal('07.05.2013 10:00:00', $recurrences[6]->format('d.m.Y H:i:s')); +Assert::equal('04.06.2013 10:00:00', $recurrences[7]->format('d.m.Y H:i:s')); +Assert::equal('02.07.2013 10:00:00', $recurrences[8]->format('d.m.Y H:i:s')); +Assert::equal('06.08.2013 10:00:00', $recurrences[9]->format('d.m.Y H:i:s')); +Assert::equal('03.09.2013 10:00:00', $recurrences[10]->format('d.m.Y H:i:s')); +Assert::equal('01.10.2013 10:00:00', $recurrences[11]->format('d.m.Y H:i:s')); +Assert::equal('05.11.2013 10:00:00', $recurrences[12]->format('d.m.Y H:i:s')); +Assert::equal('03.12.2013 10:00:00', $recurrences[13]->format('d.m.Y H:i:s')); +Assert::equal('07.01.2014 10:00:00', $recurrences[14]->format('d.m.Y H:i:s')); +Assert::equal('04.02.2014 10:00:00', $recurrences[15]->format('d.m.Y H:i:s')); +Assert::equal('04.03.2014 10:00:00', $recurrences[16]->format('d.m.Y H:i:s')); +Assert::equal('01.04.2014 10:00:00', $recurrences[17]->format('d.m.Y H:i:s')); +Assert::equal('06.05.2014 10:00:00', $recurrences[18]->format('d.m.Y H:i:s')); +Assert::equal('03.06.2014 10:00:00', $recurrences[19]->format('d.m.Y H:i:s')); +Assert::equal('01.07.2014 10:00:00', $recurrences[20]->format('d.m.Y H:i:s')); +Assert::equal('05.08.2014 10:00:00', $recurrences[21]->format('d.m.Y H:i:s')); +Assert::equal('02.09.2014 10:00:00', $recurrences[22]->format('d.m.Y H:i:s')); +Assert::equal('07.10.2014 10:00:00', $recurrences[23]->format('d.m.Y H:i:s')); +Assert::equal('04.11.2014 10:00:00', $recurrences[24]->format('d.m.Y H:i:s')); +Assert::equal('02.12.2014 10:00:00', $recurrences[25]->format('d.m.Y H:i:s')); +Assert::equal('06.01.2015 10:00:00', $recurrences[26]->format('d.m.Y H:i:s')); +Assert::equal('03.02.2015 10:00:00', $recurrences[27]->format('d.m.Y H:i:s')); +Assert::equal('03.03.2015 10:00:00', $recurrences[28]->format('d.m.Y H:i:s')); +Assert::equal('07.04.2015 10:00:00', $recurrences[29]->format('d.m.Y H:i:s')); +Assert::equal('05.05.2015 10:00:00', $recurrences[30]->format('d.m.Y H:i:s')); +Assert::equal('02.06.2015 10:00:00', $recurrences[31]->format('d.m.Y H:i:s')); +Assert::equal('07.07.2015 10:00:00', $recurrences[32]->format('d.m.Y H:i:s')); +Assert::equal('04.08.2015 10:00:00', $recurrences[33]->format('d.m.Y H:i:s')); +Assert::equal('01.09.2015 10:00:00', $recurrences[34]->format('d.m.Y H:i:s')); + +foreach ($events[0]['EXDATES'] as $exDate) { + Assert::notContains($exDate, $recurrences); +} + + +$results = $cal->parseFile(__DIR__ . '/cal/recur_instances_with_modifications.ics'); +$events = $cal->getSortedEvents(true); + +Assert::false(empty($events[0]['RECURRENCES'])); +// the 12th entry is the modified event, related to the remaining recurring events +Assert::true(empty($events[12]['RECURRENCES'])); + +$recurrences = $events[0]['RECURRENCES']; +$modifiedEvent = $events[12]; + +// There should be 35 total recurrences because the modified event should've removed 1 recurrence +Assert::equal(35, sizeof($recurrences)); +// There should be 36 total events because of the modified event + 35 recurrences +Assert::equal(36, sizeof($events)); +Assert::notContains($modifiedEvent['DTSTART'], $recurrences); + +$results = $cal->parseFile(__DIR__ . '/cal/recur_instances_with_modifications_and_interval.ics'); + +// Build the cache of RECURRENCE-IDs and EXDATES first, so that we can properly determine the interval +$eventCache = array(); +foreach($results['VEVENT'] as $event) { + $eventSequence = empty($event['SEQUENCE']) ? "0" : $event['SEQUENCE']; + $eventRecurrenceID = empty($event['RECURRENCE-ID']) ? "0" : $event['RECURRENCE-ID']; + + $eventCache[$event['UID']][$eventRecurrenceID][$eventSequence] = $event; +} +$trueEvents = array(); +foreach($results['VEVENT'] as $event) { + if(empty($event['RECURRENCES'])) { + $trueEvents[] = $event; + } else { + $eventUID = $event['UID']; + foreach($event['RECURRENCES'] as $recurrence) { + $eventRecurrenceID = $recurrence->format("Ymd"); + if(empty($eventCache[$eventUID][$eventRecurrenceID])) { + $trueEvents[$eventRecurrenceID] = array('DTSTART' => $recurrence); + } else { + krsort($eventCache[$eventUID][$eventRecurrenceID]); + $keys = array_keys($eventCache[$eventUID][$eventRecurrenceID]); + $trueEvents[$eventRecurrenceID] = $eventCache[$eventUID][$eventRecurrenceID][$keys[0]]; + } + } + } +} + +usort($trueEvents, function ($a, $b) { + return $a['DTSTART'] > $b['DTSTART']; +}); + +$events = $cal->getSortedEvents(true); +Assert::false(empty($events[0]['RECURRENCES'])); +Assert::equal(count($trueEvents), count($events)); +foreach($trueEvents as $index => $trueEvent) { + Assert::equal($trueEvent['DTSTART']->format("Ymd"), $events[$index]['DTSTART']->format("Ymd")); +} + + +// There is still an issue that needs to be resolved when modifications are made to the initial event that is the +// base of the recurrences. The below ICS file has a great edge case example: one event, no recurrences in the +// recurring ruleset, and a modification to the initial event. +$results = $cal->parseFile(__DIR__ . '/cal/recur_instances_with_modifications_to_first_day.ics'); +$events = $cal->getSortedEvents(); +Assert::true(empty($events[0]['RECURRENCES'])); // edited event +Assert::true(empty($events[1]['RECURRENCES'])); // recurring event base with no recurrences +Assert::equal(1, count($events)); + +$results = $cal->parseFile(__DIR__ . '/cal/daily_recur.ics'); +$events = $cal->getSortedEvents(); +$period = new DatePeriod(new DateTime('20120801T050000'), new DateInterval('P1D'), 365 * 3); +foreach($period as $i => $day) { + Assert::equal($day->format('j.n.Y H:i:s'), $events[$i]['DTSTART']->format('j.n.Y H:i:s')); +} + +$results = $cal->parseFile(__DIR__ . '/cal/daily_recur2.ics'); +$events = $cal->getSortedEvents(); + +Assert::equal(4, sizeof($events)); +Assert::equal('21.8.2017 00:00:00', $events[0]['DTSTART']->format('j.n.Y H:i:s')); +Assert::equal('28.8.2017 00:00:00', $events[1]['DTSTART']->format('j.n.Y H:i:s')); +Assert::equal('4.9.2017 00:00:00', $events[2]['DTSTART']->format('j.n.Y H:i:s')); +Assert::equal('11.9.2017 00:00:00', $events[3]['DTSTART']->format('j.n.Y H:i:s')); \ No newline at end of file diff --git a/include/icalparser/tests/sort_events.phpt b/include/icalparser/tests/sort_events.phpt new file mode 100644 index 0000000..d3cfbcf --- /dev/null +++ b/include/icalparser/tests/sort_events.phpt @@ -0,0 +1,29 @@ + + */ + +use Tester\Assert; +use Tester\Environment; + +require_once __DIR__ . '/../vendor/autoload.php'; +Environment::setup(); +date_default_timezone_set('Europe/Prague'); + +// sort by date +$cal = new \om\IcalParser(); +$results = $cal->parseFile(__DIR__ . '/cal/basic.ics'); +$events = $cal->getSortedEvents(); +Assert::same('1.1.2013 00:00:00', $events[0]['DTSTART']->format('j.n.Y H:i:s')); + +// reverse sort (parseFile) +$cal = new \om\IcalParser(); +$results = $cal->parseFile(__DIR__ . '/cal/basic.ics'); +$events = $cal->getReverseSortedEvents(); +Assert::same('26.12.2015 00:00:00', $events[0]['DTSTART']->format('j.n.Y H:i:s')); + +// reverse sort (parseString) +$cal = new \om\IcalParser(); +$results = $cal->parseString(file_get_contents(__DIR__ . '/cal/basic.ics')); +$events = $cal->getReverseSortedEvents(); +Assert::same('26.12.2015 00:00:00', $events[0]['DTSTART']->format('j.n.Y H:i:s')); diff --git a/include/icalparser/tests/timezone.phpt b/include/icalparser/tests/timezone.phpt new file mode 100644 index 0000000..d632622 --- /dev/null +++ b/include/icalparser/tests/timezone.phpt @@ -0,0 +1,28 @@ + + */ + +use Tester\Assert; +use Tester\Environment; + +require_once __DIR__ . '/../vendor/autoload.php'; +Environment::setup(); +date_default_timezone_set('Europe/Paris'); + +//some clean timezone +$cal = new \om\IcalParser(); +$results = $cal->parseFile(__DIR__ . '/cal/blank_description.ics'); +Assert::same('America/Los_Angeles', $cal->timezone->getName()); + +$results = $cal->parseFile(__DIR__ . '/cal/utc_negative_zero.ics'); +Assert::same('Etc/GMT', $cal->timezone->getName()); + +//time zone with custom prefixes (Mozilla files tken from here: https://www.mozilla.org/en-US/projects/calendar/holidays/) +$cal = new \om\IcalParser(); +$results = $cal->parseFile(__DIR__ . '/cal/FrenchHolidays.ics'); +Assert::same('Europe/Paris', $cal->timezone->getName()); + +$cal = new \om\IcalParser(); +$results = $cal->parseFile(__DIR__ . '/cal/weird_windows_timezones.ics'); +$events = $cal->getSortedEvents(); diff --git a/include/icalparser/tests/wrong_dates.phpt b/include/icalparser/tests/wrong_dates.phpt new file mode 100644 index 0000000..95ed7be --- /dev/null +++ b/include/icalparser/tests/wrong_dates.phpt @@ -0,0 +1,19 @@ + + */ + +use Tester\Assert; +use Tester\Environment; + +require_once __DIR__ . '/../vendor/autoload.php'; +Environment::setup(); + +$cal = new \om\IcalParser(); +$results = $cal->parseFile(__DIR__ . '/cal/wrong_dates.ics'); +$events = $cal->getSortedEvents(); +Assert::same('29.9.2014 00:00:00', $events[1]['DTSTART']->format('j.n.Y H:i:s')); +Assert::same(null, $events[1]['DTEND']); + +Assert::same(null, $events[0]['DTSTART']); +Assert::same('30.9.2014 00:00:00', $events[0]['DTEND']->format('j.n.Y H:i:s')); diff --git a/include/icalparser/bin/timezones.php b/include/icalparser/tools/windowstimezones.php similarity index 52% rename from include/icalparser/bin/timezones.php rename to include/icalparser/tools/windowstimezones.php index 3bf3708..46a15d0 100644 --- a/include/icalparser/bin/timezones.php +++ b/include/icalparser/tools/windowstimezones.php @@ -1,20 +1,19 @@ * @license http://www.opensource.org/licenses/mit-license.php MIT License */ -$windows_timezones = []; +$windows_timezones = array(); $windowstimezonexml = new DOMDocument(); -$windowstimezonexml->load('https://raw.githubusercontent.com/unicode-org/cldr/master/common/supplemental/windowsZones.xml'); +$windowstimezonexml->load('http://unicode.org/cldr/data/common/supplemental/windowsZones.xml'); $zones = $windowstimezonexml->getElementsByTagName('mapZone'); -foreach ($zones as $zone) { - if ($zone->getAttribute('territory') === '001') { +foreach($zones as $zone) { + if($zone->getAttribute('territory') == '001') { $windows_timezones[$zone->getAttribute('other')] = $zone->getAttribute('type'); } } -file_put_contents(__DIR__ . '/../src/WindowsTimezones.php', "