upgrade icalparser from e6a3a285cf6e239236a40153a66b67b940220b43 to cae1631b9496a7415ef960b0dff1cd0f39fe3135

This commit is contained in:
root 2022-04-19 21:15:47 +02:00 committed by Florian Schlegel
parent 2395e88e36
commit e2541c84e4
31 changed files with 1329 additions and 877 deletions

View File

@ -1,27 +0,0 @@
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

View File

@ -1,4 +1,4 @@
Copyright (c) 2014, Roman Ožana Copyright (c) 2014-2022, Roman Ožana
All rights reserved. All rights reserved.
Redistribution and use in source and binary forms, with or without Redistribution and use in source and binary forms, with or without

View File

@ -1,19 +1,20 @@
<?php <?php
/** /**
* This file generates a map from windows timezones to tz database timezones * This file generates a map from windows timezones to tz database timezones
* *
* @author Clement Wong <cw@clement.hk> * @author Clement Wong <cw@clement.hk>
* @license http://www.opensource.org/licenses/mit-license.php MIT License * @license http://www.opensource.org/licenses/mit-license.php MIT License
*/ */
$windows_timezones = array(); $windows_timezones = [];
$windowstimezonexml = new DOMDocument(); $windowstimezonexml = new DOMDocument();
$windowstimezonexml->load('http://unicode.org/cldr/data/common/supplemental/windowsZones.xml'); $windowstimezonexml->load('https://raw.githubusercontent.com/unicode-org/cldr/master/common/supplemental/windowsZones.xml');
$zones = $windowstimezonexml->getElementsByTagName('mapZone'); $zones = $windowstimezonexml->getElementsByTagName('mapZone');
foreach($zones as $zone) { foreach ($zones as $zone) {
if($zone->getAttribute('territory') == '001') { if ($zone->getAttribute('territory') === '001') {
$windows_timezones[$zone->getAttribute('other')] = $zone->getAttribute('type'); $windows_timezones[$zone->getAttribute('other')] = $zone->getAttribute('type');
} }
} }
file_put_contents(__DIR__.'/windows_timezones.php', "<?php\n\$windows_timezones = ".var_export($windows_timezones, true).';'); file_put_contents(__DIR__ . '/../src/WindowsTimezones.php', "<?php\n\$windows_timezones = " . var_export($windows_timezones, true) . ';');

View File

@ -1,28 +1,33 @@
{ {
"name": "om/icalparser", "name": "om/icalparser",
"license": "BSD-3-Clause",
"description": "Simple ical parser", "description": "Simple ical parser",
"keywords": [ "keywords": [
"ical", "ical",
"calendar", "calendar",
"parser" "parser"
], ],
"license": ["BSD-3-Clause"],
"authors": [ "authors": [
{ {
"name": "Roman Ožana", "name": "Roman Ožana",
"email": "ozana@omdesign.cz" "email": "roman@ozana.cz"
} }
], ],
"require": { "require": {
"php": ">=5.6.0" "php": ">=7.4.0"
},
"suggest": {
"ext-dom": "for timezone tool"
}, },
"autoload": { "autoload": {
"classmap": ["src/"] "classmap": [
}, "src/"
"require-dev": { ]
"nette/tester": "v2.0.1"
}, },
"scripts": { "scripts": {
"tests": ["./vendor/bin/tester tests -s -p php"] "test": "tester tests -s -p php"
},
"require-dev": {
"nette/tester": "^2.4"
} }
} }

View File

@ -7,26 +7,36 @@
<title>Ical Parser example</title> <title>Ical Parser example</title>
<link href="//netdna.bootstrapcdn.com/bootstrap/3.0.3/css/bootstrap.min.css" rel="stylesheet"> <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-EVSTQN3/azprG1Anm3QDgpJLIm9Nao0Yz1ztcQTwFspd3yD65VohhpuuCOmLASjC" crossorigin="anonymous">
</head> </head>
<body> <body>
<div class="container"> <div class="container">
<h1>Czech holidays</h1> <h1>Czech holidays</h1>
<ul> <table class="table table-bordered">
<thead>
<tr>
<th class="text-end">Date</th>
<th>Summary</th>
</tr>
</thead>
<tbody>
<?php <?php
use om\IcalParser;
require_once __DIR__ . '/../vendor/autoload.php'; require_once __DIR__ . '/../vendor/autoload.php';
$cal = new \om\IcalParser();
$results = $cal->parseFile(
'https://www.google.com/calendar/ical/cs.czech%23holiday%40group.v.calendar.google.com/public/basic.ics'
);
foreach ($cal->getSortedEvents() as $r) { $cal = new IcalParser();
echo sprintf(' <li>%s - %s</li>' . PHP_EOL, $r['DTSTART']->format('j.n.Y'), $r['SUMMARY']); $results = $cal->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('<tr><th class="text-end">%s</th><td>%s</td></tr>', $event['DTSTART']->format('j.n.Y'), $event['SUMMARY']);
} }
?>
?></ul> </tbody>
</table>
</div> </div>
</body> </body>
</html> </html>

View File

@ -1,44 +1,58 @@
# PHP iCal Parser # PHP iCal Parser
[![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) [![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)
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. 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.
## How to install ## How to install
```bash The recommended way to is via Composer:
```shell script
composer require om/icalparser composer require om/icalparser
``` ```
## Usage ## Usage and example
```php ```php
<?php <?php
use om\IcalParser;
require_once '../vendor/autoload.php'; require_once '../vendor/autoload.php';
$cal = new \om\IcalParser();
$cal = new IcalParser();
$results = $cal->parseFile( $results = $cal->parseFile(
'https://www.google.com/calendar/ical/cs.czech%23holiday%40group.v.calendar.google.com/public/basic.ics' 'https://www.google.com/calendar/ical/cs.czech%23holiday%40group.v.calendar.google.com/public/basic.ics'
); );
foreach ($cal->getSortedEvents() as $r) { foreach ($cal->getEvents()->sorted() as $event) {
echo sprintf(' <li>%s - %s</li>' . PHP_EOL, $r['DTSTART']->format('j.n.Y'), $r['SUMMARY']); printf('%s - %s' . PHP_EOL, $event['DTSTART']->format('j.n.Y'), $event['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 ## Requirements
- PHP 5.6+ - PHP 7.4+
## Run tests ## Run tests
iCal parser using [Nette Tester](https://github.com/nette/tester). iCal parser using [Nette Tester](https://github.com/nette/tester). The tests can be invoked via [composer](https://getcomposer.org/).
The tests can be invoked via [composer](https://getcomposer.org/).
```bash ```shell script
composer update composer update
composer tests composer test
``` ```
## TODO ## TODO
- add ATTENDEE support http://www.kanzaki.com/docs/ical/attendee.html - add ATTENDEE support https://www.kanzaki.com/docs/ical/attendee.html

View File

@ -0,0 +1,54 @@
<?php
namespace om;
/**
* Copyright (c) 2004-2022 Roman Ožana (https://ozana.cz)
*
* @license BSD-3-Clause
* @author Roman Ožana <roman@ozana.cz>
*/
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;
}
}

View File

@ -1,6 +1,11 @@
<?php <?php
namespace om; namespace om;
use DateTime;
use DateTimeZone;
use Exception;
/** /**
* Class taken from https://github.com/coopTilleuls/intouch-iCalendar.git (Freq.php) * Class taken from https://github.com/coopTilleuls/intouch-iCalendar.git (Freq.php)
* *
@ -30,50 +35,64 @@ namespace om;
* @license http://creativecommons.org/licenses/by-sa/2.5/dk/deed.en_GB CC-BY-SA-DK * @license http://creativecommons.org/licenses/by-sa/2.5/dk/deed.en_GB CC-BY-SA-DK
*/ */
class Freq { class Freq {
protected $weekdays = [
'MO' => 'monday', 'TU' => 'tuesday', 'WE' => 'wednesday', 'TH' => 'thursday', 'FR' => 'friday', 'SA' => 'saturday', protected array $weekdays = [
'SU' => 'sunday' 'MO' => 'monday',
'TU' => 'tuesday',
'WE' => 'wednesday',
'TH' => 'thursday',
'FR' => 'friday',
'SA' => 'saturday',
'SU' => 'sunday',
]; ];
protected $knownRules = [ protected array $knownRules = [
'month', 'weekno', 'day', 'monthday', 'yearday', 'hour', 'minute' 'month',
'weekno',
'day',
'monthday',
'yearday',
'hour',
'minute',
]; //others : 'setpos', 'second' ]; //others : 'setpos', 'second'
protected $ruleModifiers = ['wkst'];
protected $simpleMode = true;
protected $rules = ['freq' => 'yearly', 'interval' => 1]; protected array $ruleModifiers = ['wkst'];
protected $start = 0; protected bool $simpleMode = true;
protected $freq = '';
protected $excluded; //EXDATE protected array $rules = ['freq' => 'yearly', 'interval' => 1];
protected $added; //RDATE protected int $start = 0;
protected string $freq = '';
protected array $excluded; //EXDATE
protected array $added; //RDATE
protected $cache; // getAllOccurrences() protected $cache; // getAllOccurrences()
/** /**
* Constructs a new Freqency-rule * Constructs a new Frequency-rule
* *
* @param $rule string * @param string|array $rule
* @param $start int Unix-timestamp (important : Need to be the start of Event) * @param int $start Unix-timestamp (important : Need to be the start of Event)
* @param $excluded array of int (timestamps), see EXDATE documentation * @param array $excluded of int (timestamps), see EXDATE documentation
* @param $added array of int (timestamps), see RDATE documentation * @param array $added of int (timestamps), see RDATE documentation
* @throws Exception
*/ */
public function __construct($rule, $start, $excluded = [], $added = []) { public function __construct($rule, int $start, array $excluded = [], array $added = []) {
$this->start = $start; $this->start = $start;
$this->excluded = []; $this->excluded = [];
$rules = []; $rules = [];
foreach ($rule AS $k => $v) { foreach ($rule as $k => $v) {
$this->rules[strtolower($k)] = $v; $this->rules[strtolower($k)] = $v;
} }
if (isset($this->rules['until']) && is_string($this->rules['until'])) { if (isset($this->rules['until']) && is_string($this->rules['until'])) {
$this->rules['until'] = strtotime($this->rules['until']); $this->rules['until'] = strtotime($this->rules['until']);
} else if ($this->rules['until'] instanceof \DateTime) { } elseif ($this->rules['until'] instanceof DateTime) {
$this->rules['until'] = $this->rules['until']->getTimestamp(); $this->rules['until'] = $this->rules['until']->getTimestamp();
} }
$this->freq = strtolower($this->rules['freq']); $this->freq = strtolower($this->rules['freq']);
foreach ($this->knownRules AS $rule) { foreach ($this->knownRules as $rule) {
if (isset($this->rules['by' . $rule])) { if (isset($this->rules['by' . $rule])) {
if ($this->isPrerule($rule, $this->freq)) { if ($this->isPrerule($rule, $this->freq)) {
$this->simpleMode = false; $this->simpleMode = false;
@ -116,95 +135,34 @@ class Freq {
$this->added = $added; $this->added = $added;
} }
/** private function isPrerule(string $rule, string $freq): bool {
* Returns all timestamps array(), build the cache if not made before if ($rule === 'year') {
* return false;
* @return array }
*/ if ($rule === 'month' && $freq === 'yearly') {
public function getAllOccurrences() { return true;
if (empty($this->cache)) { }
$cache = []; if ($rule === 'monthday' && in_array($freq, ['yearly', 'monthly']) && !isset($this->rules['byday'])) {
return true;
//build cache }
$next = $this->firstOccurrence(); // TODO: is it faster to do monthday first, and ignore day if monthday exists? - prolly by a factor of 4..
while ($next) { if ($rule === 'yearday' && $freq === 'yearly') {
$cache[] = $next; return true;
$next = $this->findNext($next); }
} if ($rule === 'weekno' && $freq === 'yearly') {
if (!empty($this->added)) { return true;
$cache = array_unique(array_merge($cache, $this->added)); }
asort($cache); if ($rule === 'day' && in_array($freq, ['yearly', 'monthly', 'weekly'])) {
} return true;
$this->cache = $cache; }
if ($rule === 'hour' && in_array($freq, ['yearly', 'monthly', 'weekly', 'daily'])) {
return true;
}
if ($rule === 'minute') {
return true;
} }
return $this->cache; return false;
}
/**
* 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);
} }
/** /**
@ -227,14 +185,16 @@ class Freq {
* If no new timestamps were found in the period, we try in the * If no new timestamps were found in the period, we try in the
* next period * next period
* *
* @param int $offset * @param int $offset
* @return int * @return int|bool
* @throws Exception
*/ */
public function findNext($offset) { public function findNext(int $offset) {
if (!empty($this->cache)) { if (!empty($this->cache)) {
foreach ($this->cache as $ts) { foreach ($this->cache as $ts) {
if ($ts > $offset) if ($ts > $offset) {
return $ts; return $ts;
}
} }
} }
@ -242,8 +202,7 @@ class Freq {
//make sure the offset is valid //make sure the offset is valid
if ($offset === false || (isset($this->rules['until']) && $offset > $this->rules['until'])) { if ($offset === false || (isset($this->rules['until']) && $offset > $this->rules['until'])) {
if ($debug) echo 'STOP: ' . date('r', $offset) . "\n"; if ($debug) printf("STOP: %s\n", date('r', $offset));
return false; return false;
} }
@ -251,7 +210,7 @@ class Freq {
//set the timestamp of the offset (ignoring hours and minutes unless we want them to be //set the timestamp of the offset (ignoring hours and minutes unless we want them to be
//part of the calculations. //part of the calculations.
if ($debug) echo 'O: ' . date('r', $offset) . "\n"; if ($debug) printf("O: %s\n", date('r', $offset));
$hour = (in_array($this->freq, ['hourly', 'minutely']) && $offset > $this->start) ? date('H', $offset) : date( $hour = (in_array($this->freq, ['hourly', 'minutely']) && $offset > $this->start) ? date('H', $offset) : date(
'H', $this->start 'H', $this->start
); );
@ -259,13 +218,14 @@ class Freq {
'i', $offset 'i', $offset
) : date('i', $this->start); ) : date('i', $this->start);
$t = mktime($hour, $minute, date('s', $this->start), date('m', $offset), date('d', $offset), date('Y', $offset)); $t = mktime($hour, $minute, date('s', $this->start), date('m', $offset), date('d', $offset), date('Y', $offset));
if ($debug) echo 'START: ' . date('r', $t) . "\n"; if ($debug) printf("START: %s\n", date('r', $t));
if ($this->simpleMode) { if ($this->simpleMode) {
if ($offset < $t) { if ($offset < $t) {
$ts = $t; $ts = $t;
if ($ts && in_array($ts, $this->excluded)) if ($ts && in_array($ts, $this->excluded, true)) {
$ts = $this->findNext($ts); $ts = $this->findNext($ts);
}
} else { } else {
$ts = $this->findStartingPoint($t, $this->rules['interval'], false); $ts = $this->findStartingPoint($t, $this->rules['interval'], false);
if (!$this->validDate($ts)) { if (!$this->validDate($ts)) {
@ -276,22 +236,31 @@ class Freq {
return $ts; return $ts;
} }
$eop = $this->findEndOfPeriod($offset); //EOP needs to have the same TIME as START ($t)
if ($debug) echo 'EOP: ' . date('r', $eop) . "\n"; $tO = new DateTime('@' . $t, new DateTimeZone('UTC'));
foreach ($this->knownRules AS $rule) { $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";
}
foreach ($this->knownRules as $rule) {
if ($found && isset($this->rules['by' . $rule])) { if ($found && isset($this->rules['by' . $rule])) {
if ($this->isPrerule($rule, $this->freq)) { if ($this->isPrerule($rule, $this->freq)) {
$subrules = explode(',', $this->rules['by' . $rule]); $subRules = explode(',', $this->rules['by' . $rule]);
$_t = null; $_t = null;
foreach ($subrules AS $subrule) { foreach ($subRules as $subRule) {
$imm = call_user_func_array([$this, 'ruleBy' . $rule], [$subrule, $t]); $imm = call_user_func_array([$this, "ruleBy$rule"], [$subRule, $t]);
if ($imm === false) { if ($imm === false) {
break; break;
} }
if ($debug) echo strtoupper($rule) . ': ' . date( if ($debug) {
'r', $imm printf("%s: %s A: %d\n", strtoupper($rule), date('r', $imm), intval($imm > $offset && $imm < $eop));
) . ' A: ' . ((int)($imm > $offset && $imm < $eop)) . "\n"; }
if ($imm > $offset && $imm <= $eop && ($_t == null || $imm < $_t)) { if ($imm > $offset && $imm <= $eop && ($_t == null || $imm < $_t)) {
$_t = $imm; $_t = $imm;
} }
@ -319,8 +288,9 @@ class Freq {
if ($debug) echo 'Not found' . "\n"; if ($debug) echo 'Not found' . "\n";
$ts = $this->findNext($this->findStartingPoint($offset, $this->rules['interval'])); $ts = $this->findNext($this->findStartingPoint($offset, $this->rules['interval']));
} }
if ($ts && in_array($ts, $this->excluded)) if ($ts && in_array($ts, $this->excluded, true)) {
return $this->findNext($ts); return $this->findNext($ts);
}
return $ts; return $ts;
} }
@ -329,17 +299,17 @@ class Freq {
* Finds the starting point for the next rule. It goes $interval * Finds the starting point for the next rule. It goes $interval
* 'freq' forward in time since the given offset * 'freq' forward in time since the given offset
* *
* @param int $offset * @param int $offset
* @param int $interval * @param int $interval
* @param boolean $truncate * @param boolean $truncate
* @return int * @return int
*/ */
private function findStartingPoint($offset, $interval, $truncate = true) { private function findStartingPoint(int $offset, int $interval, $truncate = true): int {
$_freq = ($this->freq === 'daily') ? 'day__' : $this->freq; $_freq = ($this->freq === 'daily') ? 'day__' : $this->freq;
$t = '+' . $interval . ' ' . substr($_freq, 0, -2) . 's'; $t = '+' . $interval . ' ' . substr($_freq, 0, -2) . 's';
if ($_freq === 'monthly' && $truncate) { if ($_freq === 'monthly' && $truncate) {
if ($interval > 1) { if ($interval > 1) {
$offset = strtotime('+' . ($interval - 1) . ' months ', $offset); $offset = strtotime('+' . ($interval - 1) . ' months ', $offset); // FIXME return type int|false
} }
$t = '+' . (date('t', $offset) - date('d', $offset) + 1) . ' days'; $t = '+' . (date('t', $offset) - date('d', $offset) + 1) . ' days';
} }
@ -353,27 +323,17 @@ class Freq {
return $sp; 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 * Resets the timestamp to the beginning of the
* period specified by freq * period specified by freq
* *
* Yes - the fall-through is on purpose! * Yes - the fall-through is on purpose!
* *
* @param int $time * @param int $time
* @param int $freq * @param string $freq
* @return int * @return int
*/ */
private function truncateToPeriod($time, $freq) { private function truncateToPeriod(int $time, string $freq): int {
$date = getdate($time); $date = getdate($time);
switch ($freq) { switch ($freq) {
case 'yearly': case 'yearly':
@ -388,7 +348,7 @@ class Freq {
$date['seconds'] = 0; $date['seconds'] = 0;
break; break;
case 'weekly': case 'weekly':
if (date('N', $time) == 1) { if (date('N', $time) == 1) { // FIXME wrong compare, date return string|false
$date['hours'] = 0; $date['hours'] = 0;
$date['minutes'] = 0; $date['minutes'] = 0;
$date['seconds'] = 0; $date['seconds'] = 0;
@ -397,21 +357,179 @@ class Freq {
} }
break; break;
} }
$d = mktime($date['hours'], $date['minutes'], $date['seconds'], $date['mon'], $date['mday'], $date['year']); return mktime($date['hours'], $date['minutes'], $date['seconds'], $date['mon'], $date['mday'], $date['year']);
}
return $d; 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;
} }
/** /**
* Applies the BYDAY rule to the given timestamp * Applies the BYDAY rule to the given timestamp
* *
* @param string $rule * @param string $rule
* @param int $t * @param int $t
* @return int * @return int
*/ */
private function ruleByday($rule, $t) { private function ruleByDay(string $rule, int $t): int {
$dir = ($rule{0} == '-') ? -1 : 1; $dir = ($rule[0] === '-') ? -1 : 1;
$dir_t = ($dir == 1) ? 'next' : 'last'; $dir_t = ($dir === 1) ? 'next' : 'last';
$d = $this->weekdays[substr($rule, -2)]; $d = $this->weekdays[substr($rule, -2)];
$s = $dir_t . ' ' . $d . ' ' . date('H:i:s', $t); $s = $dir_t . ' ' . $d . ' ' . date('H:i:s', $t);
@ -435,7 +553,7 @@ class Freq {
if (isset($this->rules['bymonth']) && $this->freq === 'yearly') { if (isset($this->rules['bymonth']) && $this->freq === 'yearly') {
$this->freq = 'monthly'; $this->freq = 'monthly';
} }
if ($dir == -1) { if ($dir === -1) {
$_t = $this->findEndOfPeriod($t); $_t = $this->findEndOfPeriod($t);
} else { } else {
$_t = $this->truncateToPeriod($t, $this->freq); $_t = $this->truncateToPeriod($t, $this->freq);
@ -447,7 +565,7 @@ class Freq {
$n = $_t; $n = $_t;
while ($c > 0) { 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); $s = 'today ' . date('H:i:s', $t);
} }
$n = strtotime($s, $n); $n = strtotime($s, $n);
@ -458,7 +576,7 @@ class Freq {
} }
} }
private function ruleBymonth($rule, $t) { private function ruleByMonth($rule, int $t) {
$_t = mktime(date('H', $t), date('i', $t), date('s', $t), $rule, date('d', $t), date('Y', $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'])) { 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 // TODO: this should check if one of the by*day's exists, and have a multi-day value
@ -468,7 +586,7 @@ class Freq {
} }
} }
private function ruleBymonthday($rule, $t) { private function ruleByMonthday($rule, int $t) {
if ($rule < 0) { if ($rule < 0) {
$rule = date('t', $t) + $rule + 1; $rule = date('t', $t) + $rule + 1;
} }
@ -476,7 +594,7 @@ class Freq {
return mktime(date('H', $t), date('i', $t), date('s', $t), date('m', $t), $rule, date('Y', $t)); return mktime(date('H', $t), date('i', $t), date('s', $t), date('m', $t), $rule, date('Y', $t));
} }
private function ruleByyearday($rule, $t) { private function ruleByYearday($rule, int $t) {
if ($rule < 0) { if ($rule < 0) {
$_t = $this->findEndOfPeriod(); $_t = $this->findEndOfPeriod();
$d = '-'; $d = '-';
@ -489,7 +607,7 @@ class Freq {
return strtotime($s, $_t); return strtotime($s, $_t);
} }
private function ruleByweekno($rule, $t) { private function ruleByWeekno($rule, int $t) {
if ($rule < 0) { if ($rule < 0) {
$_t = $this->findEndOfPeriod(); $_t = $this->findEndOfPeriod();
$d = '-'; $d = '-';
@ -505,96 +623,11 @@ class Freq {
return $_t; return $_t;
} }
private function ruleByhour($rule, $t) { private function ruleByHour($rule, int $t) {
$_t = mktime($rule, date('i', $t), date('s', $t), date('m', $t), date('d', $t), date('Y', $t)); return mktime($rule, date('i', $t), date('s', $t), date('m', $t), date('d', $t), date('Y', $t));
return $_t;
} }
private function ruleByminute($rule, $t) { private function ruleByMinute($rule, int $t) {
$_t = mktime(date('h', $t), $rule, date('s', $t), date('m', $t), date('d', $t), date('Y', $t)); return 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;
} }
} }

View File

@ -1,46 +1,47 @@
<?php <?php
namespace om; namespace om;
use DateInterval;
use DateTime;
use DateTimeZone;
use Exception;
use InvalidArgumentException;
use RuntimeException;
/** /**
* Copyright (c) 2004-2015 Roman Ožana (http://www.omdesign.cz) * Copyright (c) 2004-2022 Roman Ožana (https://ozana.cz)
* *
* @author Roman Ožana <ozana@omdesign.cz> * @license BSD-3-Clause
* @author Roman Ožana <roman@ozana.cz>
*/ */
class IcalParser { class IcalParser {
/** @var \DateTimeZone */ /** @var DateTimeZone */
public $timezone; public DateTimeZone $timezone;
/** @var array|null */
public ?array $data = null;
/** @var array */ /** @var array */
public $data = []; protected array $counters = [];
/** @var array */
protected $counters = [];
/** @var array */ /** @var array */
private $windowsTimezones; private $windowsTimezones;
protected $arrayKeyMappings = [
'ATTACH' => 'ATTACHMENTS',
'EXDATE' => 'EXDATES',
'RDATE' => 'RDATES',
];
public function __construct() { public function __construct() {
$this->windowsTimezones = require __DIR__ . '/WindowsTimezones.php'; // load Windows timezones from separate file $this->windowsTimezones = require __DIR__ . '/WindowsTimezones.php'; // load Windows timezones from separate file
} }
/** /**
* @param string $file * @param string $file
* @param null $callback * @param callable|null $callback
* @return array|null * @return array|null
* @throws \RuntimeException * @throws Exception
* @throws \InvalidArgumentException
* @throws \Exception
*/ */
public function parseFile($file, $callback = null) { public function parseFile(string $file, callable $callback = null): array {
if (!$handle = fopen($file, 'r')) { if (!$handle = fopen($file, 'rb')) {
throw new \RuntimeException('Can\'t open file' . $file . ' for reading'); throw new RuntimeException('Can\'t open file' . $file . ' for reading');
} }
fclose($handle); fclose($handle);
@ -49,13 +50,12 @@ class IcalParser {
/** /**
* @param string $string * @param string $string
* @param null $callback * @param callable|null $callback
* @param boolean $add if true the parsed string is added to existing data * @param boolean $add if true the parsed string is added to existing data
* @return array|null * @return array|null
* @throws \InvalidArgumentException * @throws Exception
* @throws \Exception
*/ */
public function parseString($string, $callback = null, $add = false) { public function parseString(string $string, callable $callback = null, bool $add = false): ?array {
if ($add === false) { if ($add === false) {
// delete old data // delete old data
$this->data = []; $this->data = [];
@ -63,7 +63,7 @@ class IcalParser {
} }
if (!preg_match('/BEGIN:VCALENDAR/', $string)) { if (!preg_match('/BEGIN:VCALENDAR/', $string)) {
throw new \InvalidArgumentException('Invalid ICAL data format'); throw new InvalidArgumentException('Invalid ICAL data format');
} }
$section = 'VCALENDAR'; $section = 'VCALENDAR';
@ -88,7 +88,6 @@ class IcalParser {
$section = substr($row, 6); $section = substr($row, 6);
$this->counters[$section] = isset($this->counters[$section]) ? $this->counters[$section] + 1 : 0; $this->counters[$section] = isset($this->counters[$section]) ? $this->counters[$section] + 1 : 0;
continue 2; // while continue 2; // while
break;
case 'END:VEVENT': case 'END:VEVENT':
$section = substr($row, 4); $section = substr($row, 4);
$currCounter = $this->counters[$section]; $currCounter = $this->counters[$section];
@ -98,7 +97,6 @@ class IcalParser {
} }
continue 2; // while continue 2; // while
break;
case 'END:DAYLIGHT': case 'END:DAYLIGHT':
case 'END:VALARM': case 'END:VALARM':
case 'END:VTIMEZONE': case 'END:VTIMEZONE':
@ -107,7 +105,6 @@ class IcalParser {
case 'END:STANDARD': case 'END:STANDARD':
case 'END:VTODO': case 'END:VTODO':
continue 2; // while continue 2; // while
break;
case 'END:VCALENDAR': case 'END:VCALENDAR':
$veventSection = 'VEVENT'; $veventSection = 'VEVENT';
@ -126,11 +123,9 @@ class IcalParser {
} }
} }
continue 2; // while continue 2; // while
break;
} }
list($key, $middle, $value) = $this->parseRow($row); [$key, $middle, $value] = $this->parseRow($row);
if ($callback) { if ($callback) {
// call user function for processing line // call user function for processing line
@ -139,15 +134,33 @@ class IcalParser {
if ($section === 'VCALENDAR') { if ($section === 'VCALENDAR') {
$this->data[$key] = $value; $this->data[$key] = $value;
} else { } else {
if (isset($this->arrayKeyMappings[$key])) {
// use an array since there can be multiple entries for this key. This does not // 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 // break the current implementation--it leaves the original key alone and adds
// a new one specifically for the array of values. // a new one specifically for the array of values.
$arrayKey = $this->arrayKeyMappings[$key];
$this->data[$section][$this->counters[$section]][$arrayKey][] = $value; 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('/(?<![^\\\\]\\\\),/', $value));
} else {
$values = [$value];
}
foreach ($values as $value) {
$this->data[$section][$this->counters[$section]][$key][] = $value;
}
} else {
$this->data[$section][$this->counters[$section]][$key] = $value;
} }
$this->data[$section][$this->counters[$section]][$key] = $value;
} }
} }
@ -156,121 +169,12 @@ class IcalParser {
return ($callback) ? null : $this->data; 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('#(?<key>[^=;]+)=(?<value>[^;]+)#', $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('#(?<key>[^=;]+)=(?<value>[^;]+)#', $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('/(?<![^\\\\]\\\\),/', $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'
];
if (in_array($key, $text_properties) || 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];
}
/** /**
* @param $event * @param $event
* @return array * @return array
* @throws \Exception * @throws Exception
*/ */
public function parseRecurrences($event) { public function parseRecurrences($event): array {
$recurring = new Recurrence($event['RRULE']); $recurring = new Recurrence($event['RRULE']);
$exclusions = []; $exclusions = [];
$additions = []; $additions = [];
@ -301,9 +205,9 @@ class IcalParser {
$until = $recurring->getUntil(); $until = $recurring->getUntil();
if ($until === false) { if ($until === false) {
//forever... limit to 3 years //forever... limit to 3 years from now
$end = clone($event['DTSTART']); $end = new DateTime('now');
$end->add(new \DateInterval('P3Y')); // + 3 years $end->add(new DateInterval('P3Y')); // + 3 years
$recurring->setUntil($end); $recurring->setUntil($end);
$until = $recurring->getUntil(); $until = $recurring->getUntil();
} }
@ -313,14 +217,14 @@ class IcalParser {
$recurrenceTimestamps = $frequency->getAllOccurrences(); $recurrenceTimestamps = $frequency->getAllOccurrences();
$recurrences = []; $recurrences = [];
foreach ($recurrenceTimestamps as $recurrenceTimestamp) { foreach ($recurrenceTimestamps as $recurrenceTimestamp) {
$tmp = new \DateTime('now', $event['DTSTART']->getTimezone()); $tmp = new DateTime('now', $event['DTSTART']->getTimezone());
$tmp->setTimestamp($recurrenceTimestamp); $tmp->setTimestamp($recurrenceTimestamp);
$recurrenceIDDate = $tmp->format('Ymd'); $recurrenceIDDate = $tmp->format('Ymd');
$recurrenceIDDateTime = $tmp->format('Ymd\THis'); $recurrenceIDDateTime = $tmp->format('Ymd\THis');
if (empty($this->data['_RECURRENCE_IDS'][$recurrenceIDDate]) && if (empty($this->data['_RECURRENCE_IDS'][$recurrenceIDDate]) &&
empty($this->data['_RECURRENCE_IDS'][$recurrenceIDDateTime])) { empty($this->data['_RECURRENCE_IDS'][$recurrenceIDDateTime])) {
$gmtCheck = new \DateTime("now", new \DateTimeZone('UTC')); $gmtCheck = new DateTime('now', new DateTimeZone('UTC'));
$gmtCheck->setTimestamp($recurrenceTimestamp); $gmtCheck->setTimestamp($recurrenceTimestamp);
$recurrenceIDDateTimeZ = $gmtCheck->format('Ymd\THis\Z'); $recurrenceIDDateTimeZ = $gmtCheck->format('Ymd\THis\Z');
if (empty($this->data['_RECURRENCE_IDS'][$recurrenceIDDateTimeZ])) { if (empty($this->data['_RECURRENCE_IDS'][$recurrenceIDDateTimeZ])) {
@ -332,32 +236,172 @@ class IcalParser {
return $recurrences; 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('#(?<key>[^=;]+)=(?<value>[^;]+)#', $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('#(?<key>[^=;]+)=(?<value>[^;]+)#', $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];
}
/** /**
* @return array * Process timezone and return correct one...
*
* @param string $zone
* @return mixed|null
*/ */
public function getEvents() { private function toTimezone(string $zone) {
$events = []; 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();
if (isset($this->data['VEVENT'])) { if (isset($this->data['VEVENT'])) {
for ($i = 0; $i < count($this->data['VEVENT']); $i++) { foreach ($this->data['VEVENT'] as $iValue) {
$event = $this->data['VEVENT'][$i]; $event = $iValue;
if (empty($event['RECURRENCES'])) { if (empty($event['RECURRENCES'])) {
if (!empty($event['RECURRENCE-ID']) && !empty($event['UID']) && isset($event['SEQUENCE'])) { if (!empty($event['RECURRENCE-ID']) && !empty($event['UID']) && isset($event['SEQUENCE'])) {
$modifiedEventUID = $event['UID']; $modifiedEventUID = $event['UID'];
$modifiedEventRecurID = $event['RECURRENCE-ID']; $modifiedEventRecurID = $event['RECURRENCE-ID'];
$modifiedEventSeq = intval($event['SEQUENCE'], 10); $modifiedEventSeq = (int)$event['SEQUENCE'];
if (isset($this->data["_RECURRENCE_COUNTERS_BY_UID"][$modifiedEventUID])) { if (isset($this->data['_RECURRENCE_COUNTERS_BY_UID'][$modifiedEventUID])) {
$counter = $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'])) { if (isset($originalEvent['SEQUENCE'])) {
$originalEventSeq = intval($originalEvent['SEQUENCE'], 10); $originalEventSeq = (int)$originalEvent['SEQUENCE'];
$originalEventFormattedStartDate = $originalEvent['DTSTART']->format('Ymd\THis'); $originalEventFormattedStartDate = $originalEvent['DTSTART']->format('Ymd\THis');
if ($modifiedEventRecurID === $originalEventFormattedStartDate && $modifiedEventSeq > $originalEventSeq) { if ($modifiedEventRecurID === $originalEventFormattedStartDate && $modifiedEventSeq > $originalEventSeq) {
// this modifies the original event // this modifies the original event
$modifiedEvent = array_replace_recursive($originalEvent, $event); $modifiedEvent = array_replace_recursive($originalEvent, $event);
$this->data["VEVENT"][$counter] = $modifiedEvent; $this->data['VEVENT'][$counter] = $modifiedEvent;
foreach ($events as $z => $event) { foreach ($events as $z => $event) {
if ($events[$z]['UID'] === $originalEvent['UID'] && if ($events[$z]['UID'] === $originalEvent['UID'] &&
$events[$z]['SEQUENCE'] === $originalEvent['SEQUENCE']) { $events[$z]['SEQUENCE'] === $originalEvent['SEQUENCE']) {
@ -367,13 +411,13 @@ class IcalParser {
} }
} }
$event = null; // don't add this to the $events[] array again $event = null; // don't add this to the $events[] array again
} else if (!empty($originalEvent['RECURRENCES'])) { } elseif (!empty($originalEvent['RECURRENCES'])) {
for ($j = 0; $j < count($originalEvent['RECURRENCES']); $j++) { for ($j = 0; $j < count($originalEvent['RECURRENCES']); $j++) {
$recurDate = $originalEvent['RECURRENCES'][$j]; $recurDate = $originalEvent['RECURRENCES'][$j];
$formattedStartDate = $recurDate->format('Ymd\THis'); $formattedStartDate = $recurDate->format('Ymd\THis');
if ($formattedStartDate === $modifiedEventRecurID) { if ($formattedStartDate === $modifiedEventRecurID) {
unset($this->data["VEVENT"][$counter]['RECURRENCES'][$j]); unset($this->data['VEVENT'][$counter]['RECURRENCES'][$j]);
$this->data["VEVENT"][$counter]['RECURRENCES'] = array_values($this->data["VEVENT"][$counter]['RECURRENCES']); $this->data['VEVENT'][$counter]['RECURRENCES'] = array_values($this->data['VEVENT'][$counter]['RECURRENCES']);
break; break;
} }
} }
@ -383,7 +427,7 @@ class IcalParser {
} }
if (!empty($event)) { if (!empty($event)) {
$events[] = $event; $events->append($event);
} }
} else { } else {
$recurrences = $event['RECURRENCES']; $recurrences = $event['RECURRENCES'];
@ -402,7 +446,7 @@ class IcalParser {
} }
$newEvent['RECURRENCE_INSTANCE'] = $j; $newEvent['RECURRENCE_INSTANCE'] = $j;
$events[] = $newEvent; $events->append($newEvent);
$firstEvent = false; $firstEvent = false;
} }
} }
@ -411,61 +455,12 @@ class IcalParser {
return $events; return $events;
} }
/** /**
* Process timezone and return correct one... * @return \ArrayObject
* * @deprecated use IcalParser::getEvents->reversed();
* @param string $zone
* @return mixed|null
*/ */
private function toTimezone($zone) { public function getReverseSortedEvents(): \ArrayObject {
return isset($this->windowsTimezones[$zone]) ? $this->windowsTimezones[$zone] : $zone; return $this->getEvents()->reversed();
}
/**
* @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 [];
} }
} }

View File

@ -2,7 +2,8 @@
namespace om; namespace om;
use \DateTime; use DateTime;
use Exception;
/** /**
* Class taken from https://github.com/coopTilleuls/intouch-iCalendar.git (Recurrence.php) * Class taken from https://github.com/coopTilleuls/intouch-iCalendar.git (Recurrence.php)
@ -19,6 +20,7 @@ use \DateTime;
* @license http://creativecommons.org/licenses/by-sa/2.5/dk/deed.en_GB CC-BY-SA-DK * @license http://creativecommons.org/licenses/by-sa/2.5/dk/deed.en_GB CC-BY-SA-DK
*/ */
class Recurrence { class Recurrence {
public $rrule; public $rrule;
protected $freq; protected $freq;
protected $until; protected $until;
@ -39,9 +41,9 @@ class Recurrence {
* *
* @var array * @var array
*/ */
protected $listProperties = [ protected array $listProperties = [
'bysecond', 'byminute', 'byhour', 'byday', 'bymonthday', 'bysecond', 'byminute', 'byhour', 'byday', 'bymonthday',
'byyearday', 'byweekno', 'bymonth', 'bysetpos' 'byyearday', 'byweekno', 'bymonth', 'bysetpos',
]; ];
/** /**
@ -50,7 +52,7 @@ class Recurrence {
* @param array $rrule an om\icalparser row array which will be parsed to get the * @param array $rrule an om\icalparser row array which will be parsed to get the
* desired information. * desired information.
*/ */
public function __construct($rrule) { public function __construct(array $rrule) {
$this->parseRrule($rrule); $this->parseRrule($rrule);
} }
@ -60,7 +62,7 @@ class Recurrence {
* *
* @param $rrule * @param $rrule
*/ */
protected function parseRrule($rrule) { protected function parseRrule($rrule): void {
$this->rrule = $rrule; $this->rrule = $rrule;
//loop through the properties in the line and set their associated //loop through the properties in the line and set their associated
//member variables //member variables
@ -75,36 +77,6 @@ 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. * Returns the frequency - corresponds to FREQ in RFC 2445.
* *
@ -114,6 +86,16 @@ class Recurrence {
return $this->getMember('freq'); 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. * Returns when the event will go until - corresponds to UNTIL in RFC 2445.
* *
@ -123,6 +105,24 @@ class Recurrence {
return $this->getMember('until'); 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' * Returns the count of the times the event will occur (should only appear if 'until'
* does not appear) - corresponds to COUNT in RFC 2445. * does not appear) - corresponds to COUNT in RFC 2445.

View File

@ -1,4 +1,5 @@
<?php <?php
/** /**
* List of Windows Timezones * List of Windows Timezones
*/ */
@ -84,6 +85,8 @@ return [
'(UTC) Monrovia, Reykjavik' => 'Atlantic/Reykjavik', '(UTC) Monrovia, Reykjavik' => 'Atlantic/Reykjavik',
'W. Europe Standard Time' => 'Europe/Berlin', '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\, Rome\, Stockholm\, Vienna' => 'Europe/Berlin',
'(UTC+01:00) Amsterdam\, Berlin\, Bern\, Rom\, Stockholm\, Wien' => 'Europe/Berlin',
'Central Europe Standard Time' => 'Europe/Budapest', 'Central Europe Standard Time' => 'Europe/Budapest',
'(UTC+01:00) Belgrade, Bratislava, Budapest, Ljubljana, Prague' => 'Europe/Budapest', '(UTC+01:00) Belgrade, Bratislava, Budapest, Ljubljana, Prague' => 'Europe/Budapest',
'Romance Standard Time' => 'Europe/Paris', 'Romance Standard Time' => 'Europe/Paris',
@ -207,4 +210,5 @@ return [
'(UTC+13:00) Nuku\'alofa' => 'Pacific/Tongatapu', '(UTC+13:00) Nuku\'alofa' => 'Pacific/Tongatapu',
'Samoa Standard Time' => 'Pacific/Apia', 'Samoa Standard Time' => 'Pacific/Apia',
'(UTC-11:00)Samoa' => 'Pacific/Apia', '(UTC-11:00)Samoa' => 'Pacific/Apia',
]; 'W. Europe Standard Time 1' => 'Europe/Berlin',
];

View File

@ -1,19 +0,0 @@
<?php
/**
* @author Roman Ozana <ozana@omdesign.cz>
*/
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');

View File

@ -0,0 +1,21 @@
<?php
namespace tests;
/**
* Copyright (c) 2004-2022 Roman Ožana (https://ozana.cz)
*
* @license BSD-3-Clause
* @author Roman Ožana <roman@ozana.cz>
*/
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();

View File

@ -0,0 +1,59 @@
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

View File

@ -0,0 +1,39 @@
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

View File

@ -0,0 +1,67 @@
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

View File

@ -0,0 +1,32 @@
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

View File

@ -0,0 +1,26 @@
<?php
/**
* @author PC Drew <pc@soprisapps.com>
*/
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']);
});

View File

@ -0,0 +1,25 @@
<?php
/**
* Copyright (c) 2004-2022 Roman Ožana (https://ozana.cz)
*
* @license BSD-3-Clause
* @author Roman Ožana <roman@ozana.cz>
*/
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']);
}
});

View File

@ -0,0 +1,25 @@
<?php
/**
* Copyright (c) 2004-2022 Roman Ožana (https://ozana.cz)
*
* @license BSD-3-Clause
* @author Roman Ožana <roman@ozana.cz>
*/
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'));
});

View File

@ -0,0 +1,34 @@
<?php
/**
* Copyright (c) 2004-2022 Roman Ožana (https://ozana.cz)
*
* @license BSD-3-Clause
* @author Roman Ožana <roman@ozana.cz>
*/
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']);
});

View File

@ -0,0 +1,39 @@
<?php
/**
* @author Marc Vachette <marc.vachette@gmail.com>
*/
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());
});

View File

@ -0,0 +1,22 @@
<?php
/**
* Copyright (c) 2004-2022 Roman Ožana (https://ozana.cz)
*
* @license BSD-3-Clause
* @author Roman Ožana <roman@ozana.cz>
*/
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/'));
});

View File

@ -0,0 +1,241 @@
<?php
/**
* @author PC Drew <pc@schoolblocks.com>
* @author Roman Ožana <roman@ozana.cz>
*/
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'));
});

View File

@ -0,0 +1,36 @@
<?php
/**
* Copyright (c) 2004-2022 Roman Ožana (https://ozana.cz)
*
* @license BSD-3-Clause
* @author Roman Ožana <roman@ozana.cz>
*/
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'));
});

View File

@ -1,18 +0,0 @@
<?php
/**
* @author Aaron Parecki <aaron@parecki.com>
*/
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']);

View File

@ -1,20 +0,0 @@
<?php
/**
* @author PC Drew <pc@soprisapps.com>
*/
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']);

View File

@ -1,170 +0,0 @@
<?php
/**
* @author PC Drew <pc@schoolblocks.com>
*/
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'));

View File

@ -1,29 +0,0 @@
<?php
/**
* @author Roman Ozana <ozana@omdesign.cz>
*/
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'));

View File

@ -1,28 +0,0 @@
<?php
/**
* @author Marc Vachette <marc.vachette@gmail.com>
*/
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();

View File

@ -1,19 +0,0 @@
<?php
/**
* @author Roman Ozana <ozana@omdesign.cz>
*/
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'));