Compare commits
2 commits
2395e88e36
...
b9da2622a4
Author | SHA1 | Date | |
---|---|---|---|
Florian Schlegel | b9da2622a4 | ||
e2541c84e4 |
|
@ -46,7 +46,8 @@
|
||||||
],
|
],
|
||||||
"spacefed": {
|
"spacefed": {
|
||||||
"spacenet": false,
|
"spacenet": false,
|
||||||
"spacesaml": false
|
"spacesaml": false,
|
||||||
|
"spacephone": false
|
||||||
},
|
},
|
||||||
"ext_ccc": "chaostreff"
|
"ext_ccc": "chaostreff"
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
|
|
@ -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
|
||||||
|
|
|
@ -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) . ';');
|
||||||
|
|
|
@ -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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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>
|
|
@ -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
|
||||||
|
|
54
include/icalparser/src/EventsList.php
Normal file
54
include/icalparser/src/EventsList.php
Normal 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 [];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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.
|
||||||
|
|
|
@ -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',
|
||||||
];
|
];
|
|
@ -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');
|
|
21
include/icalparser/tests/bootstrap.php
Normal file
21
include/icalparser/tests/bootstrap.php
Normal 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();
|
|
@ -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
|
39
include/icalparser/tests/cal/missing_RRULE_notice.ics
Normal file
39
include/icalparser/tests/cal/missing_RRULE_notice.ics
Normal 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
|
67
include/icalparser/tests/cal/multiple_categories.ics
Executable file
67
include/icalparser/tests/cal/multiple_categories.ics
Executable 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
|
32
include/icalparser/tests/cal/url.ics
Normal file
32
include/icalparser/tests/cal/url.ics
Normal 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
|
26
include/icalparser/tests/event.attachements.phpt
Normal file
26
include/icalparser/tests/event.attachements.phpt
Normal 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']);
|
||||||
|
});
|
25
include/icalparser/tests/event.categories.phpt
Normal file
25
include/icalparser/tests/event.categories.phpt
Normal 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']);
|
||||||
|
}
|
||||||
|
});
|
25
include/icalparser/tests/event.dates.phpt
Normal file
25
include/icalparser/tests/event.dates.phpt
Normal 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'));
|
||||||
|
});
|
||||||
|
|
34
include/icalparser/tests/event.description.phpt
Normal file
34
include/icalparser/tests/event.description.phpt
Normal 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']);
|
||||||
|
});
|
||||||
|
|
39
include/icalparser/tests/event.timezones.phpt
Normal file
39
include/icalparser/tests/event.timezones.phpt
Normal 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());
|
||||||
|
});
|
22
include/icalparser/tests/event.url.phpt
Normal file
22
include/icalparser/tests/event.url.phpt
Normal 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/'));
|
||||||
|
});
|
241
include/icalparser/tests/events.recurring.phpt
Normal file
241
include/icalparser/tests/events.recurring.phpt
Normal 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'));
|
||||||
|
});
|
36
include/icalparser/tests/events.sorting.phpt
Normal file
36
include/icalparser/tests/events.sorting.phpt
Normal 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'));
|
||||||
|
});
|
|
@ -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']);
|
|
|
@ -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']);
|
|
|
@ -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'));
|
|
|
@ -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'));
|
|
|
@ -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();
|
|
|
@ -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'));
|
|
Loading…
Reference in a new issue