diff --git a/.gitattributes b/.gitattributes index c717ebe6..602c2377 100644 --- a/.gitattributes +++ b/.gitattributes @@ -3,5 +3,6 @@ /.gitattributes export-ignore /.gitignore export-ignore /.php_cs.dist export-ignore +/.travis.yml export-ignore /CHANGELOG.md export-ignore /phpstan.neon export-ignore diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3d665e84..93e22978 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -61,3 +61,46 @@ jobs: - name: Code Coverage uses: codecov/codecov-action@v3 if: matrix.coverage != 'none' + dependencies-audit: + name: Dependencies audit (PHP ${{ matrix.php-versions }}) + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + php-versions: ['7.4', '8.0', '8.1', '8.2', '8.3'] + coverage: ['pcov'] + code-analysis: ['no'] + include: + - php-versions: '7.4' + coverage: 'none' + code-analysis: 'yes' + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup PHP, with composer and extensions + uses: shivammathur/setup-php@v2 #https://github.com/shivammathur/setup-php + with: + php-version: ${{ matrix.php-versions }} + extensions: mbstring, dom, fileinfo, mysql, redis, opcache + coverage: ${{ matrix.coverage }} + tools: composer + + - name: Get composer cache directory + id: composer-cache + run: echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT + + - name: Cache composer dependencies + uses: actions/cache@v3 + with: + path: ${{ steps.composer-cache.outputs.dir }} + # Use composer.json for key, if composer.lock is not committed. + # key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.json') }} + key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.json') }} + restore-keys: ${{ runner.os }}-composer- + + - name: Install composer dependencies + run: composer install --no-progress --prefer-dist --optimize-autoloader + + - name: Composer audit + run: composer audit diff --git a/.gitignore b/.gitignore index d0e5169a..d6e0a1d0 100644 --- a/.gitignore +++ b/.gitignore @@ -6,4 +6,4 @@ tests/temp tests/.phpunit.result.cache # Development stuff -.php-cs-fixer.cache \ No newline at end of file +.php-cs-fixer.cache diff --git a/CHANGELOG.md b/CHANGELOG.md index c7891d69..1478325d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,162 @@ ChangeLog ========= +4.29.0 (2023-12-29) +------------------- +* #98 Chore/merge upstream master 20231228 + +4.28.0 (2023-12-19) +------------------- +* #96 use dubai instead of muscat + +4.27.0 (2023-12-18) +------------------- +* #94 Add support for gulf standard time + +4.26.0 (2023-11-30) +------------------- +* #92 Drop malformed or illegal VALUE parameter + +4.25.0 (2023-06-26) +------------------- +* #89 Move America/Yellowknife to deprecated + +4.24.0 (2023-06-19) +------------------- +* #87 Added whitelist for illegal values in parameter + +4.23.0 (2023-04-13) +------------------- +* #82 Modify TZID value for migration timezones + +4.22.0 (2023-03-01) +------------------- +* #80 Allow newly deprecated timezones + +4.21.0 (2022-12-28) +------------------- +* #76 Add mapping for PST timezone + +4.20.0 (2022-12-23) +------------------- +* #74 Handle null tzid + +4.19.0 (2022-12-21) +------------------- +* #72 Deprecate Enderbury timezone + +4.18.0 (2022-09-01) +------------------- +* #69 Add mapping for CDT timezone + +4.17.0 (2022-07-29) +------------------- +* #65 [Calendar] Replace Godthab timezone + +4.16.0 (2022-07-06) +------------------- +* #63 Fix customized timezone guesser + +4.15.0 (2022-06-23) +------------------- +* #57 Handle customized timezone + +4.14.0 (2022-05-31) +------------------- +* #54 Add FindFromOffsetName + +4.13.0 (2022-05-27) +------------------- +* #56 Merge upstream changes from sabre-io/vobject@a595790 into protonlabs/vobject +* #58 Handle version timezone + +4.12.0 (2022-05-05) +------------------- +* #52 Add FindFromOutlookCities timezone finder + +4.11.0 (2022-04-22) +------------------- +* #50 Add lowercase timezone finder + +4.10.0 (2022-03-08) +------------------- +* #48 Block invalid combinations of FREQ with BY rules + +4.9.0 (2022-02-15) +------------------ +* #46 Add support UTC-05:00 timezone + +4.8.0 (2022-02-10) +------------------ +* #42 Add option to fix unfolding issues in ICS + +4.7.2 (2022-01-21) +------------------ +* #41 Add missing microsoft timezones and test with confluence file + +4.7.1 (2022-01-10) +------------------ +* #39 Add support for lowercase timezones + +4.7.0 (2021-12-15) +------------------ +* #34 Merge upstream changes from sabre-io/vobject:4.4.0 into protonlabs/vobject +* #36 Merge upstream changes from sabre-io/vobject:4.4.1 into protonlabs/vobject + +4.6.1 (2021-11-04) +------------------ +* #29 Fix timezone name prefixed with / +* #30 Missing EDT TZID conversion + +4.5.1 (2021-10-11) +------------------ +* #25 Fix duplicate value +* #26 Add php unsupport timezone + +4.4.2 (2021-07-15) +------------------ +* #23 Add microsoft timezone map + +4.4.1 (2021-01-18) +------------------ +* #19 Validate count and until property + +4.4.0 (2020-11-23) +------------------ + +* #18 Merge upstream changes from sabre-io/vobject:4.3.3 into protonlabs/vobject +* #17 Throw exception when getting invalid timezone + +4.3.4 (2020-07-27) +------------------ + +* #16 Merge upstream changes from sabre-io/vobject:4.3.1 into protonlabs/vobject + +4.3.3 (2020-07-22) +------------------ + +* #15 Expose RRULE properties + +4.3.2 (2020-05-20) +------------------ + +* #14 Add timezones data mapping. + +4.3.1 (2020-01-27) +------------------ + +* #6 Add FastForward Before +* #7 Add FastForward to end +* #11 FREQ is mandatory in the RRule +* #12 Validate BYMONTHDAY + +4.2.1 (2019-09-10) +------------------ + +* #2 Fix bug in by year day +* #3 Add daily occurrences to nextMonth and NextYear +* #4 Enhance fast forward speed if no count value has been given + 4.5.2 (2023-01-20) ------------------ diff --git a/README.md b/README.md index 659e3fa8..cc5f34fb 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,9 @@ -sabre/vobject +protonlabs/vobject ============= +![Build Status](https://github.com/ProtonMail/vobject/actions/workflows/actions.yml/badge.svg) +[![codecov](https://codecov.io/gh/ProtonMail/vobject/branch/master/graph/badge.svg?token=ARcwkxCKZn)](https://codecov.io/gh/ProtonMail/vobject) + +Forked from [sabre/vobject](https://github.com/sabre-io/vobject). The VObject library allows you to easily parse and manipulate [iCalendar](https://tools.ietf.org/html/rfc5545) and [vCard](https://tools.ietf.org/html/rfc6350) objects using PHP. @@ -12,12 +16,12 @@ Installation Make sure you have [Composer][1] installed, and then run: - composer require sabre/vobject "^4.0" + composer require protonlabs/vobject "^4.0" This package requires PHP 5.5. If you need the PHP 5.3/5.4 version of this package instead, use: - composer require sabre/vobject "^3.4" + composer require protonlabs/vobject "^3.4" Usage @@ -27,21 +31,6 @@ Usage * [Working with iCalendar](http://sabre.io/vobject/icalendar/) - -Build status ------------- - -| branch | status | -| ------ | ------ | -| master | [![Build Status](https://travis-ci.org/sabre-io/vobject.svg?branch=master)](https://travis-ci.org/sabre-io/vobject) | -| 3.5 | [![Build Status](https://travis-ci.org/sabre-io/vobject.svg?branch=3.5)](https://travis-ci.org/sabre-io/vobject) | -| 3.4 | [![Build Status](https://travis-ci.org/sabre-io/vobject.svg?branch=3.4)](https://travis-ci.org/sabre-io/vobject) | -| 3.1 | [![Build Status](https://travis-ci.org/sabre-io/vobject.svg?branch=3.1)](https://travis-ci.org/sabre-io/vobject) | -| 2.1 | [![Build Status](https://travis-ci.org/sabre-io/vobject.svg?branch=2.1)](https://travis-ci.org/sabre-io/vobject) | -| 2.0 | [![Build Status](https://travis-ci.org/sabre-io/vobject.svg?branch=2.0)](https://travis-ci.org/sabre-io/vobject) | - - - Support ------- diff --git a/codecov.yml b/codecov.yml new file mode 100644 index 00000000..03a09f7d --- /dev/null +++ b/codecov.yml @@ -0,0 +1,19 @@ +comment: + layout: "reach, diff, flags, files" + behavior: default + require_changes: false + +coverage: + range: "70..100" + round: down + precision: 2 + status: + project: + default: + # basic + target: 95% + threshold: 0% + # advanced settings + if_ci_failed: error + informational: false + only_pulls: false \ No newline at end of file diff --git a/composer.json b/composer.json index 67b8b26b..263f6a16 100644 --- a/composer.json +++ b/composer.json @@ -1,5 +1,5 @@ { - "name": "sabre/vobject", + "name": "protonlabs/vobject", "description" : "The VObject library for PHP allows you to easily parse and manipulate iCalendar and vCard objects", "keywords" : [ "iCalendar", diff --git a/lib/Component.php b/lib/Component.php index da2c5ebd..42669547 100644 --- a/lib/Component.php +++ b/lib/Component.php @@ -412,7 +412,7 @@ protected function getDefaults(): array * * $event = $calendar->VEVENT; * - * @return Property|Component + * @return Property|Component|null */ public function __get(string $name): ?Node { diff --git a/lib/Document.php b/lib/Document.php index b311987d..2a249580 100644 --- a/lib/Document.php +++ b/lib/Document.php @@ -191,9 +191,12 @@ public function createProperty(string $name, $value = null, array $parameters = if (is_null($class)) { // If a VALUE parameter is supplied, we should use that. if (isset($parameters['VALUE'])) { - $class = $this->getClassNameForPropertyValue($parameters['VALUE']); - if (is_null($class)) { - throw new InvalidDataException('Unsupported VALUE parameter for '.$name.' property. You supplied "'.$parameters['VALUE'].'"'); + if (is_string($parameters['VALUE'])) { + $class = $this->getClassNameForPropertyValue($parameters['VALUE']); + } + if (is_null($class)) { // VALUE is malformed or illegal, drop it + unset($parameters['VALUE']); + $class = $this->getClassNameForPropertyName($name); } } else { $class = $this->getClassNameForPropertyName($name); diff --git a/lib/Parameter.php b/lib/Parameter.php index 1900cb63..1ef7da88 100644 --- a/lib/Parameter.php +++ b/lib/Parameter.php @@ -46,6 +46,7 @@ class Parameter extends Node */ public function __construct(Document $root, ?string $name, $value = null) { + $this->name = is_null($name) ? '' : strtoupper($name); $this->root = $root; if (is_null($name)) { $this->noName = true; diff --git a/lib/Parser/MimeDir.php b/lib/Parser/MimeDir.php index bbef0ea6..1ba2f1c5 100644 --- a/lib/Parser/MimeDir.php +++ b/lib/Parser/MimeDir.php @@ -10,6 +10,7 @@ use Sabre\VObject\Node; use Sabre\VObject\ParseException; use Sabre\VObject\Property; +use Sabre\VObject\Reader; /** * MimeDir parser. @@ -206,15 +207,32 @@ protected function parseLine(string $line) } $component = $this->root->createComponent(\substr($line, 6), [], false); + $prevNode = null; while (true) { // Reading until we hit END: $line = $this->readLine(); if ('END:' === strtoupper(\substr($line, 0, 4))) { break; } - $result = $this->parseLine($line); + try { + $result = $this->parseLine($line); + } catch (\Exception $e) { + if (isset($prevNode) + && $e instanceof ParseException && str_contains($e->getMessage(), 'Invalid Mimedir file. Line starting at') + && ($this->options & Reader::OPTION_FIX_UNFOLDING) + ) { + // Fix unfolding + $component->remove($prevNode); + $value = $prevNode->getValue().' '.$line.PHP_EOL; + $prevNode->offsetSet('VALUE', $value); + $prevNode->setValue($value); + $component->add($prevNode); + continue; + } + throw $e; + } if ($result) { - $component->add($result); + $prevNode = $component->add($result); } } diff --git a/lib/Property/ICalendar/DateTime.php b/lib/Property/ICalendar/DateTime.php index 7ee90797..1892991a 100644 --- a/lib/Property/ICalendar/DateTime.php +++ b/lib/Property/ICalendar/DateTime.php @@ -2,7 +2,6 @@ namespace Sabre\VObject\Property\ICalendar; -use DateTimeZone; use Sabre\VObject\DateTimeParser; use Sabre\VObject\InvalidDataException; use Sabre\VObject\Property; @@ -128,9 +127,9 @@ public function isFloating(): bool * * @throws InvalidDataException */ - public function getDateTime(\DateTimeZone $timeZone = null): ?\DateTimeImmutable + public function getDateTime(\DateTimeZone $timeZone = null, bool $activeCustomizedGuesser = true): ?\DateTimeImmutable { - $dt = $this->getDateTimes($timeZone); + $dt = $this->getDateTimes($timeZone, $activeCustomizedGuesser); if (!$dt) { return null; } @@ -149,14 +148,14 @@ public function getDateTime(\DateTimeZone $timeZone = null): ?\DateTimeImmutable * * @throws InvalidDataException */ - public function getDateTimes(\DateTimeZone $timeZone = null): array + public function getDateTimes(\DateTimeZone $timeZone = null, bool $activeCustomizedGuesser = true): array { // Does the property have a TZID? /** @var Property\FlatText $tzid */ $tzid = $this['TZID']; if ($tzid) { - $timeZone = TimeZoneUtil::getTimeZone((string) $tzid, $this->root); + $timeZone = TimeZoneUtil::getTimeZone((string) $tzid, $this->root, true, $activeCustomizedGuesser); } $dts = []; diff --git a/lib/Reader.php b/lib/Reader.php index 35318692..fc9c5805 100644 --- a/lib/Reader.php +++ b/lib/Reader.php @@ -28,6 +28,11 @@ class Reader */ public const OPTION_IGNORE_INVALID_LINES = 2; + /** + * If this option is turned on, it will fix unfolding parse error by adding empty space. + */ + public const OPTION_FIX_UNFOLDING = 4; + /** * Parses a vCard or iCalendar object, and returns the top component. * diff --git a/lib/Recur/RRuleIterator.php b/lib/Recur/RRuleIterator.php index 888556ee..ecf1affe 100644 --- a/lib/Recur/RRuleIterator.php +++ b/lib/Recur/RRuleIterator.php @@ -2,8 +2,6 @@ namespace Sabre\VObject\Recur; -use DateTimeImmutable; -use Iterator; use Sabre\VObject\DateTimeParser; use Sabre\VObject\InvalidDataException; use Sabre\VObject\Property; @@ -30,6 +28,8 @@ class RRuleIterator implements \Iterator */ public const dateUpperLimit = 253402300799; + private bool $yearlySkipUpperLimit; + /** * Creates the Iterator. * @@ -37,11 +37,12 @@ class RRuleIterator implements \Iterator * * @throws InvalidDataException */ - public function __construct($rrule, \DateTimeInterface $start) + public function __construct($rrule, \DateTimeInterface $start, bool $yearlySkipUpperLimit = true) { $this->startDate = $start; $this->parseRRule($rrule); $this->currentDate = clone $this->startDate; + $this->yearlySkipUpperLimit = $yearlySkipUpperLimit; } /* Implementation of the Iterator interface {{{ */ @@ -62,7 +63,7 @@ public function current(): ?\DateTimeInterface #[\ReturnTypeWillChange] public function key(): int { - return $this->counter; + return (int) $this->counter; } /** @@ -97,29 +98,25 @@ public function rewind(): void * Goes on to the next iteration. */ #[\ReturnTypeWillChange] - public function next(): void + public function next(int $amount = 1): void { // Otherwise, we find the next event in the normal RRULE // sequence. switch ($this->frequency) { case 'hourly': - $this->nextHourly(); + $this->nextHourly($amount); break; - case 'daily': - $this->nextDaily(); + $this->nextDaily($amount); break; - case 'weekly': - $this->nextWeekly(); + $this->nextWeekly($amount); break; - case 'monthly': - $this->nextMonthly(); + $this->nextMonthly($amount); break; - case 'yearly': - $this->nextYearly(); + $this->nextYearly($amount); break; } ++$this->counter; @@ -141,9 +138,163 @@ public function isInfinite(): bool */ public function fastForward(\DateTimeInterface $dt): void { + // We don't do any jumps if we have a count limit as we have to keep track of the number of occurrences + if (!isset($this->count)) { + $this->jumpForward($dt); + } + + while ($this->valid() && $this->currentDate < $dt) { + $this->next(); + } + } + + /** + * This method allows you to quickly go to the next occurrence before the specified date. + */ + public function fastForwardBefore(\DateTimeInterface $dt): void + { + $hasCount = isset($this->count); + + // We don't do any jumps if we have a count limit as we have to keep track of the number of occurrences + if (!$hasCount) { + $this->jumpForward($dt); + } + + $previousDate = null; while ($this->valid() && $this->currentDate < $dt) { + $previousDate = clone $this->currentDate; $this->next(); } + + if (isset($previousDate)) { + $this->currentDate = $previousDate; + $hasCount && $this->counter--; + } + } + + /** + * This method allows you to quickly go to the last occurrence. + */ + public function fastForwardToEnd(): void + { + if ($this->isInfinite()) { + throw new \LogicException('Cannot fast forward to the end an infinite event.'); + } + + $hasCount = isset($this->count); + + if (isset($this->until) && !$hasCount) { + $this->jumpForward($this->until); + } + + // We fast forward until the last event occurrence + $previous = clone $this->currentDate; + while ($this->valid()) { + $previous = clone $this->currentDate; + $this->next(); + } + + $hasCount && $this->counter--; + $this->currentDate = $previous; + } + + public function getCount(): ?int + { + return $this->count; + } + + public function getInterval(): int + { + return $this->interval; + } + + public function getUntil(): ?\DateTimeInterface + { + return $this->until; + } + + public function getFrequency(): string + { + return $this->frequency; + } + + /** + * Return the frequency in number of days. + * + * @return float|int|null + */ + private function getFrequencyCoeff() + { + $frequencyCoeff = null; + + switch ($this->frequency) { + case 'hourly': + $frequencyCoeff = 1 / 24; + break; + case 'daily': + $frequencyCoeff = 1; + break; + case 'weekly': + $frequencyCoeff = 7; + break; + case 'monthly': + $frequencyCoeff = 30; + break; + case 'yearly': + $frequencyCoeff = 365; + break; + } + + return $frequencyCoeff; + } + + /** + * Perform a fast forward by doing jumps based on the distance of the requested date and the frequency of the + * recurrence rule. Will set the position of the iterator to the last occurrence before the requested date. If the + * fast forwarding failed, the position will be reset. + */ + private function jumpForward(\DateTimeInterface $dt): void + { + $frequencyCoeff = $this->getFrequencyCoeff(); + + do { + // We estimate the number of jumps to reach $dt. This is an estimate as the number of generated event within + // a frequency interval is assumed to be 1 (in reality, it could be anything >= 0) + $diff = $this->currentDate->diff($dt); + $estimatedOccurrences = $diff->days / $frequencyCoeff; + $estimatedOccurrences /= $this->interval; + + // We want to do small jumps to not overshot + $jumpSize = floor($estimatedOccurrences / 4); + $jumpSize = (int) max(1, $jumpSize); + + // If we are too close to the desired occurrence, we abort the jumping + if ($jumpSize <= 4) { + break; + } + + do { + $previousDate = clone $this->currentDate; + $this->next($jumpSize); + } while ($this->valid() && $this->currentDate < $dt); + + $this->currentDate = clone $previousDate; + // Do one step to avoid deadlock + $this->next(); + } while ($this->valid() && $this->currentDate < $dt); + + // We undo the last next as it made the $this->currentDate < $dt false + // we want the last that validate it. + isset($previousDate) && $this->currentDate = clone $previousDate; + + // We don't know the counter at this point anymore + $this->counter = NAN; + + // It's possible that we miss the previous occurrence by jumping too much, in this case we reset the rrule and + // do the normal forward. + if ($this->currentDate >= $dt) { + $this->rewind(); + } } /** @@ -209,7 +360,7 @@ public function fastForward(\DateTimeInterface $dt): void * * You can get this number with the key() method. */ - protected int $counter = 0; + protected float $counter = 0; /** * Which weekdays to recur. @@ -279,18 +430,18 @@ public function fastForward(\DateTimeInterface $dt): void /** * Does the processing for advancing the iterator for hourly frequency. */ - protected function nextHourly(): void + protected function nextHourly($amount = 1): void { - $this->currentDate = $this->currentDate->modify('+'.$this->interval.' hours'); + $this->currentDate = $this->currentDate->modify('+'.$amount * $this->interval.' hours'); } /** * Does the processing for advancing the iterator for daily frequency. */ - protected function nextDaily(): void + protected function nextDaily($amount = 1): void { if (!$this->byHour && !$this->byDay) { - $this->currentDate = $this->currentDate->modify('+'.$this->interval.' days'); + $this->currentDate = $this->currentDate->modify('+'.$amount * $this->interval.' days'); return; } @@ -314,12 +465,14 @@ protected function nextDaily(): void if ($this->byHour) { if ('23' == $this->currentDate->format('G')) { // to obey the interval rule - $this->currentDate = $this->currentDate->modify('+'.($this->interval - 1).' days'); + $this->currentDate = $this->currentDate->modify('+'.(($amount * $this->interval) - 1).' days'); + $amount = 1; } $this->currentDate = $this->currentDate->modify('+1 hours'); } else { - $this->currentDate = $this->currentDate->modify('+'.$this->interval.' days'); + $this->currentDate = $this->currentDate->modify('+'.($amount * $this->interval).' days'); + $amount = 1; } // Current month of the year @@ -346,10 +499,10 @@ protected function nextDaily(): void /** * Does the processing for advancing the iterator for weekly frequency. */ - protected function nextWeekly(): void + protected function nextWeekly($amount = 1): void { if (!$this->byHour && !$this->byDay) { - $this->currentDate = $this->currentDate->modify('+'.$this->interval.' weeks'); + $this->currentDate = $this->currentDate->modify('+'.($amount * $this->interval).' weeks'); return; } @@ -382,8 +535,8 @@ protected function nextWeekly(): void // We need to roll over to the next week if ($currentDay === $firstDay && (!$this->byHour || '0' == $currentHour)) { - $this->currentDate = $this->currentDate->modify('+'.($this->interval - 1).' weeks'); - + $this->currentDate = $this->currentDate->modify('+'.(($amount * $this->interval) - 1).' weeks'); + $amount = 1; // We need to go to the first day of this week, but only if we // are not already on this first day of this week. if ($this->currentDate->format('w') != $firstDay) { @@ -400,17 +553,20 @@ protected function nextWeekly(): void * * @throws \Exception */ - protected function nextMonthly(): void + protected function nextMonthly($amount = 1): void { $currentDayOfMonth = $this->currentDate->format('j'); + $currentHourOfMonth = $this->currentDate->format('G'); + $currentMinuteOfMonth = $this->currentDate->format('i'); + $currentSecondOfMonth = $this->currentDate->format('s'); if (!$this->byMonthDay && !$this->byDay) { // If the current day is higher than the 28th, rollover can // occur to the next month. We Must skip these invalid // entries. if ($currentDayOfMonth < 29) { - $this->currentDate = $this->currentDate->modify('+'.$this->interval.' months'); + $this->currentDate = $this->currentDate->modify('+'.($amount * $this->interval).' months'); } else { - $increase = 0; + $increase = $amount - 1; do { ++$increase; $tempDate = clone $this->currentDate; @@ -429,7 +585,23 @@ protected function nextMonthly(): void foreach ($occurrences as $occurrence) { // The first occurrence that's higher than the current // day of the month wins. - if ($occurrence > $currentDayOfMonth) { + if ($occurrence[0] > $currentDayOfMonth) { + break 2; + } elseif ($occurrence[0] < $currentDayOfMonth) { + continue; + } + if ($occurrence[1] > $currentHourOfMonth) { + break 2; + } elseif ($occurrence[1] < $currentHourOfMonth) { + continue; + } + + if ($occurrence[2] > $currentMinuteOfMonth) { + break 2; + } elseif ($occurrence[2] < $currentMinuteOfMonth) { + continue; + } + if ($occurrence[3] > $currentSecondOfMonth) { break 2; } } @@ -437,20 +609,24 @@ protected function nextMonthly(): void // If we made it all the way here, it means there were no // valid occurrences, and we need to advance to the next // month. - // - // This line does not currently work in hhvm. Temporary workaround - // follows: - // $this->currentDate->modify('first day of this month'); - $this->currentDate = new \DateTimeImmutable($this->currentDate->format('Y-m-1 H:i:s'), $this->currentDate->getTimezone()); + $this->currentDate = $this->currentDate->setDate( + (int) $this->currentDate->format('Y'), + (int) $this->currentDate->format('n'), + 1 + ); // end of workaround - $this->currentDate = $this->currentDate->modify('+ '.$this->interval.' months'); + $this->currentDate = $this->currentDate->modify('+ '.($amount * $this->interval).' months'); + $amount = 1; // This goes to 0 because we need to start counting at the // beginning. $currentDayOfMonth = 0; + $currentHourOfMonth = 0; + $currentMinuteOfMonth = 0; + $currentSecondOfMonth = 0; // For some reason the "until" parameter was not being used here, - // that's why the workaround of the 10000-year bug was needed at all + // that's why the workaround of the 10000 year bug was needed at all // let's stop it before the "until" parameter date if ($this->until && $this->currentDate->getTimestamp() >= $this->until->getTimestamp()) { return; @@ -468,18 +644,21 @@ protected function nextMonthly(): void $this->currentDate = $this->currentDate->setDate( (int) $this->currentDate->format('Y'), (int) $this->currentDate->format('n'), - (int) $occurrence - ); + $occurrence[0] + )->setTime($occurrence[1], $occurrence[2], $occurrence[3]); } /** * Does the processing for advancing the iterator for yearly frequency. */ - protected function nextYearly(): void + protected function nextYearly($amount = 1): void { - $currentMonth = $this->currentDate->format('n'); $currentYear = $this->currentDate->format('Y'); + $currentMonth = $this->currentDate->format('n'); $currentDayOfMonth = $this->currentDate->format('j'); + $currentHourOfMonth = $this->currentDate->format('G'); + $currentMinuteOfMonth = $this->currentDate->format('i'); + $currentSecondOfMonth = $this->currentDate->format('s'); // No sub-rules, so we just advance by year if (empty($this->byMonth)) { @@ -539,7 +718,8 @@ protected function nextYearly(): void } // if there is no date found, check the next year - $currentYear += $this->interval; + $currentYear += $amount * $this->interval; + $amount = 1; } } @@ -581,20 +761,17 @@ protected function nextYearly(): void } // if there is no date found, check the next year - $currentYear += $this->interval; + $currentYear += ($amount * $this->interval); + $amount = 1; } } // The easiest form - $this->currentDate = $this->currentDate->modify('+'.$this->interval.' years'); + $this->currentDate = $this->currentDate->modify('+'.($amount * $this->interval).' years'); return; } - $currentMonth = $this->currentDate->format('n'); - $currentYear = $this->currentDate->format('Y'); - $currentDayOfMonth = $this->currentDate->format('j'); - $advancedToNewMonth = false; // If we got a byDay or getMonthDay filter, we must first expand @@ -602,15 +779,32 @@ protected function nextYearly(): void if ($this->byDay || $this->byMonthDay) { $occurrence = -1; while (true) { - $occurrences = $this->getMonthlyOccurrences(); - - foreach ($occurrences as $occurrence) { - // The first occurrence that's higher than the current - // day of the month wins. - // If we advanced to the next month or year, the first - // occurrence is always correct. - if ($occurrence > $currentDayOfMonth || $advancedToNewMonth) { - break 2; + // If the start date is incorrect we must directly jump to the next value + if (in_array($currentMonth, $this->byMonth)) { + $occurrences = $this->getMonthlyOccurrences(); + foreach ($occurrences as $occurrence) { + // The first occurrence that's higher than the current + // day of the month wins. + // If we advanced to the next month or year, the first + // occurrence is always correct. + if ($occurrence[0] > $currentDayOfMonth || $advancedToNewMonth) { + break 2; + } elseif ($occurrence[0] < $currentDayOfMonth) { + continue; + } + if ($occurrence[1] > $currentHourOfMonth) { + break 2; + } elseif ($occurrence[1] < $currentHourOfMonth) { + continue; + } + if ($occurrence[2] > $currentMinuteOfMonth) { + break 2; + } elseif ($occurrence[2] < $currentMinuteOfMonth) { + continue; + } + if ($occurrence[3] > $currentSecondOfMonth) { + break 2; + } } } @@ -621,7 +815,8 @@ protected function nextYearly(): void do { ++$currentMonth; if ($currentMonth > 12) { - $currentYear += $this->interval; + $currentYear += ($amount * $this->interval); + $amount = 1; $currentMonth = 1; } } while (!in_array($currentMonth, $this->byMonth)); @@ -634,7 +829,7 @@ protected function nextYearly(): void // To prevent running this forever (better: until we hit the max date of DateTimeImmutable) we simply // stop at 9999-12-31. Looks like the year 10000 problem is not solved in php .... - if ($this->currentDate->getTimestamp() > self::dateUpperLimit) { + if (!$this->yearlySkipUpperLimit && ($this->currentDate->getTimestamp() > self::dateUpperLimit)) { $this->currentDate = null; return; @@ -645,8 +840,8 @@ protected function nextYearly(): void $this->currentDate = $this->currentDate->setDate( (int) $currentYear, (int) $currentMonth, - (int) $occurrence - ); + (int) $occurrence[0] + )->setTime($occurrence[1], $occurrence[2], $occurrence[3]); return; } else { @@ -749,6 +944,15 @@ protected function parseRRule($rrule): void case 'BYMONTHDAY': $this->byMonthDay = (array) $value; + foreach ($this->byMonthDay as $byMonthDay) { + if (!is_numeric($byMonthDay)) { + throw new InvalidDataException('BYMONTHDAY in RRULE has a not numeric value(s)!'); + } + $byMonthDay = (int) $byMonthDay; + if ($byMonthDay < -31 || 0 === $byMonthDay || $byMonthDay > 31) { + throw new InvalidDataException('BYMONTHDAY in RRULE must have value(s) from 1 to 31, or -31 to -1!'); + } + } break; case 'BYYEARDAY': @@ -790,6 +994,23 @@ protected function parseRRule($rrule): void throw new InvalidDataException('Not supported: '.strtoupper($key)); } } + + // FREQ is mandatory + if (!isset($this->frequency)) { + throw new InvalidDataException('Unknown value for FREQ'); + } + + if (isset($this->count) && isset($this->until)) { + throw new InvalidDataException('Can not have both UNTIL and COUNT property at the same time'); + } + + if ( + (isset($this->byWeekNo) && 'yearly' !== $this->frequency) + || (isset($this->byYearDay) && in_array($this->frequency, ['daily', 'weekly', 'monthly'], true)) + || (isset($this->byMonthDay) && 'weekly' === $this->frequency) + ) { + throw new InvalidDataException('Invalid combination of FREQ with BY rules'); + } } /** @@ -809,7 +1030,8 @@ protected function parseRRule($rrule): void * Returns all the occurrences for a monthly frequency with a 'byDay' or * 'byMonthDay' expansion for the current month. * - * The returned list is an array of integers with the day of month (1-31). + * The returned list is an array of arrays with as first element the day of month (1-31); + * the hour; the minute and second of the occurence * * @throws \Exception */ @@ -895,8 +1117,23 @@ protected function getMonthlyOccurrences(): array } else { $result = $byDayResults; } - $result = array_unique($result); - sort($result, SORT_NUMERIC); + + $result = $this->addDailyOccurences($result); + $result = array_unique($result, SORT_REGULAR); + $sortLex = function ($a, $b) { + if ($a[0] != $b[0]) { + return $a[0] - $b[0]; + } + if ($a[1] != $b[1]) { + return $a[1] - $b[1]; + } + if ($a[2] != $b[2]) { + return $a[2] - $b[2]; + } + + return $a[3] - $b[3]; + }; + usort($result, $sortLex); // The last thing that needs checking is the BYSETPOS. If it's set, it // means only certain items in the set survive the filter. @@ -914,11 +1151,40 @@ protected function getMonthlyOccurrences(): array } } - sort($filteredResult, SORT_NUMERIC); + usort($result, $sortLex); return $filteredResult; } + /** + * Expends daily occurrences to an array of days that an event occurs on. + * + * @param array $result an array of integers with the day of month (1-31); + * + * @return array an array of arrays with the day of the month, hours, minute and seconds of the occurence + */ + protected function addDailyOccurences(array $result): array + { + $output = []; + $hour = (int) $this->currentDate->format('G'); + $minute = (int) $this->currentDate->format('i'); + $second = (int) $this->currentDate->format('s'); + foreach ($result as $day) { + $seconds = $this->bySecond ? $this->bySecond : [$second]; + $minutes = $this->byMinute ? $this->byMinute : [$minute]; + $hours = $this->byHour ? $this->byHour : [$hour]; + foreach ($hours as $h) { + foreach ($minutes as $m) { + foreach ($seconds as $s) { + $output[] = [(int) $day, (int) $h, (int) $m, (int) $s]; + } + } + } + } + + return $output; + } + /** * Simple mapping from iCalendar day names to day numbers. */ diff --git a/lib/TimeZoneUtil.php b/lib/TimeZoneUtil.php index 8c2a374f..79a5920a 100644 --- a/lib/TimeZoneUtil.php +++ b/lib/TimeZoneUtil.php @@ -2,11 +2,16 @@ namespace Sabre\VObject; +use Sabre\VObject\TimezoneGuesser\FindFromMzVersionTimezone; use Sabre\VObject\TimezoneGuesser\FindFromOffset; +use Sabre\VObject\TimezoneGuesser\FindFromOffsetName; +use Sabre\VObject\TimezoneGuesser\FindFromOutlookCities; use Sabre\VObject\TimezoneGuesser\FindFromTimezoneIdentifier; use Sabre\VObject\TimezoneGuesser\FindFromTimezoneMap; +use Sabre\VObject\TimezoneGuesser\GuessFromCustomizedTimeZone; use Sabre\VObject\TimezoneGuesser\GuessFromLicEntry; use Sabre\VObject\TimezoneGuesser\GuessFromMsTzId; +use Sabre\VObject\TimezoneGuesser\LowercaseTimezoneIdentifier; use Sabre\VObject\TimezoneGuesser\TimezoneFinder; use Sabre\VObject\TimezoneGuesser\TimezoneGuesser; @@ -37,6 +42,10 @@ private function __construct() $this->addFinder('tzid', new FindFromTimezoneIdentifier()); $this->addFinder('tzmap', new FindFromTimezoneMap()); $this->addFinder('offset', new FindFromOffset()); + $this->addFinder('lowercase', new LowercaseTimezoneIdentifier()); + $this->addFinder('outlookCities', new FindFromOutlookCities()); + $this->addFinder('version', new FindFromMzVersionTimezone()); + $this->addFinder('offsetName', new FindFromOffsetName()); } private static function getInstance(): self @@ -72,7 +81,7 @@ private function addFinder(string $key, TimezoneFinder $finder): void * Alternatively, if $failIfUncertain is set to true, it will throw an * exception if we cannot accurately determine the timezone. */ - private function findTimeZone(string $tzid, Component $vcalendar = null, bool $failIfUncertain = false): \DateTimeZone + private function findTimeZone(string $tzid, Component $vcalendar = null, bool $failIfUncertain = false, bool $activeCustomizedGuesser = false): \DateTimeZone { foreach ($this->timezoneFinders as $timezoneFinder) { $timezone = $timezoneFinder->find($tzid, $failIfUncertain); @@ -83,11 +92,21 @@ private function findTimeZone(string $tzid, Component $vcalendar = null, bool $f return $timezone; } + if (!$activeCustomizedGuesser) { + unset($this->timezoneGuessers['customized']); + } + if ($vcalendar) { + // We temporary add the customized timezone guesser if needed + $guessers = $this->timezoneGuessers; + if ($activeCustomizedGuesser) { + $guessers[] = new GuessFromCustomizedTimeZone(); + } + // If that didn't work, we will scan VTIMEZONE objects foreach ($vcalendar->select('VTIMEZONE') as $vtimezone) { if ((string) $vtimezone->TZID === $tzid) { - foreach ($this->timezoneGuessers as $timezoneGuesser) { + foreach ($guessers as $timezoneGuesser) { $timezone = $timezoneGuesser->guess($vtimezone, $failIfUncertain); if (!$timezone instanceof \DateTimeZone) { continue; @@ -117,9 +136,9 @@ public static function addTimezoneFinder(string $key, TimezoneFinder $finder): v self::getInstance()->addFinder($key, $finder); } - public static function getTimeZone(string $tzid, Component $vcalendar = null, bool $failIfUncertain = false): \DateTimeZone + public static function getTimeZone(string $tzid, Component $vcalendar = null, bool $failIfUncertain = false, bool $activeCustomizedGuesser = true): \DateTimeZone { - return self::getInstance()->findTimeZone($tzid, $vcalendar, $failIfUncertain); + return self::getInstance()->findTimeZone($tzid, $vcalendar, $failIfUncertain, $activeCustomizedGuesser); } public static function clean(): void diff --git a/lib/TimezoneGuesser/FindFromMzVersionTimezone.php b/lib/TimezoneGuesser/FindFromMzVersionTimezone.php new file mode 100644 index 00000000..5abdc148 --- /dev/null +++ b/lib/TimezoneGuesser/FindFromMzVersionTimezone.php @@ -0,0 +1,39 @@ + Eastern Standard Time + */ +class FindFromMzVersionTimezone implements TimezoneFinder +{ + public function find(string $tzid, ?bool $failIfUncertain = false): ?\DateTimeZone + { + if (strlen($tzid) < 1) { + return null; + } + + $trailingChar = (int) $tzid[strlen($tzid) - 1]; + if ($trailingChar <= 9 && $trailingChar >= 1) { + $emptySpace = strrpos($tzid, ' '); + if (false === $emptySpace) { + return null; + } + + $tz = TimeZoneUtil::getTimeZone(substr($tzid, 0, $emptySpace)); + if ('UTC' === $tz->getName()) { + return null; + } + + return $tz; + } + + return null; + } +} diff --git a/lib/TimezoneGuesser/FindFromOffsetName.php b/lib/TimezoneGuesser/FindFromOffsetName.php new file mode 100644 index 00000000..0916f128 --- /dev/null +++ b/lib/TimezoneGuesser/FindFromOffsetName.php @@ -0,0 +1,51 @@ + 'Africa/Lagos', + '+02:00' => 'Africa/Cairo', + '+03:00' => 'Europe/Moscow', + '+04:00' => 'Asia/Dubai', + '+05:00' => 'Asia/Karachi', + '+06:00' => 'Asia/Dhaka', + '+07:00' => 'Asia/Jakarta', + '+08:00' => 'Asia/Shanghai', + '+09:00' => 'Asia/Tokyo', + '+10:00' => 'Australia/Sydney', + '+11:00' => 'Pacific/Noumea', + '+12:00' => 'Pacific/Auckland', + '+13:00' => 'Pacific/Apia', + '-01:00' => 'Atlantic/Cape_Verde', + '-02:00' => 'Atlantic/South_Georgia', + '-03:00' => 'America/Sao_Paulo', + '-04:00' => 'America/Manaus', + '-05:00' => 'America/Lima', + '-06:00' => 'America/Guatemala', + '-07:00' => 'America/Hermosillo', + '-08:00' => 'America/Los_Angeles', + '-09:00' => 'Pacific/Gambier', + '-10:00' => 'America/Anchorage', + '-11:00' => 'Pacific/Niue', + ]; + + public function find(string $tzid, ?bool $failIfUncertain = false): ?\DateTimeZone + { + // only handle number timezone + if (strlen($tzid) > 6) { + return null; + } + + try { + $tzid = new \DateTimeZone($tzid); + + return new \DateTimeZone(self::$offsetTimezones[$tzid->getName()]) ?? null; + } catch (\Exception $e) { + return null; + } + } +} diff --git a/lib/TimezoneGuesser/FindFromOutlookCities.php b/lib/TimezoneGuesser/FindFromOutlookCities.php new file mode 100644 index 00000000..657767a3 --- /dev/null +++ b/lib/TimezoneGuesser/FindFromOutlookCities.php @@ -0,0 +1,42 @@ + 'Europe/Kiev', + ]; + public function find(string $tzid, ?bool $failIfUncertain = false): ?\DateTimeZone { // First we will just see if the tzid is a support timezone identifier. @@ -30,6 +32,16 @@ public function find(string $tzid, ?bool $failIfUncertain = false): ?\DateTimeZo if ('(' === $tzid[0]) { return null; } + + // If the timezone is prefixed with a slash we remove the slash for lookup in the maps. + if ('/' === $tzid[0]) { + $tzid = substr($tzid, 1); + } + + if (isset(self::MIGRATION_TIMEZONES[$tzid])) { + $tzid = self::MIGRATION_TIMEZONES[$tzid]; + } + // PHP has a bug that logs PHP warnings even it shouldn't: // https://bugs.php.net/bug.php?id=67881 // diff --git a/lib/TimezoneGuesser/FindFromTimezoneMap.php b/lib/TimezoneGuesser/FindFromTimezoneMap.php index 83466b28..1fe68023 100644 --- a/lib/TimezoneGuesser/FindFromTimezoneMap.php +++ b/lib/TimezoneGuesser/FindFromTimezoneMap.php @@ -4,8 +4,6 @@ namespace Sabre\VObject\TimezoneGuesser; -use DateTimeZone; - /** * Some clients add 'X-LIC-LOCATION' with the olson name. */ @@ -20,6 +18,8 @@ class FindFromTimezoneMap implements TimezoneFinder public function find(string $tzid, ?bool $failIfUncertain = false): ?\DateTimeZone { + $tzid = str_replace('.', '', $tzid); + // Next, we check if the tzid is somewhere in our tzid map. if ($this->hasTzInMap($tzid)) { return new \DateTimeZone($this->getTzFromMap($tzid)); @@ -53,11 +53,16 @@ public function find(string $tzid, ?bool $failIfUncertain = false): ?\DateTimeZo private function getTzMaps(): array { if ([] === $this->map) { - $this->map = array_merge( + $map = array_merge( include __DIR__.'/../timezonedata/windowszones.php', include __DIR__.'/../timezonedata/lotuszones.php', include __DIR__.'/../timezonedata/exchangezones.php', - include __DIR__.'/../timezonedata/php-workaround.php' + include __DIR__.'/../timezonedata/php-workaround.php', + include __DIR__.'/../timezonedata/extrazones.php' + ); + $this->map = array_combine( + array_map(static fn (string $key) => str_replace('.', '', mb_strtolower($key, 'UTF-8')), array_keys($map)), + array_values($map), ); } @@ -66,11 +71,11 @@ private function getTzMaps(): array private function getTzFromMap(string $tzid): string { - return $this->getTzMaps()[$tzid]; + return $this->getTzMaps()[mb_strtolower($tzid, 'UTF-8')]; } private function hasTzInMap(string $tzid): bool { - return isset($this->getTzMaps()[$tzid]); + return isset($this->getTzMaps()[mb_strtolower($tzid, 'UTF-8')]); } } diff --git a/lib/TimezoneGuesser/GuessFromCustomizedTimeZone.php b/lib/TimezoneGuesser/GuessFromCustomizedTimeZone.php new file mode 100644 index 00000000..61d531cd --- /dev/null +++ b/lib/TimezoneGuesser/GuessFromCustomizedTimeZone.php @@ -0,0 +1,95 @@ +TZID || 'Customized Time Zone' !== $vtimezone->TZID->getValue()) { + return null; + } + + $timezones = \DateTimeZone::listIdentifiers(); + $standard = $vtimezone->STANDARD; + $daylight = $vtimezone->DAYLIGHT; + if (!$standard) { + return null; + } + + $standardOffset = $standard->TZOFFSETTO; + if (!$standardOffset) { + return null; + } + $standardOffset = $standardOffset->getValue(); + + $standardRRule = $standard->RRULE ? $standard->RRULE->getValue() : 'FREQ=DAILY'; + // The guess will not be perfectly matched since we use the timezone data of the current year + // It might be wrong if the timezone data changed in the past + $year = (new \DateTimeImmutable('now'))->format('Y'); + $start = new \DateTimeImmutable($year.'-01-01'); + $standardIterator = new RRuleIterator($standardRRule, $start); + $standardIterator->next(); + + if ($daylight && !$daylight->TZOFFSETTO) { + $daylight = null; + } + $daylightOffset = $daylight ? $daylight->TZOFFSETTO->getValue() : ''; + $daylightRRule = $daylight ? ($daylight->RRULE ? $daylight->RRULE->getValue() : 'FREQ=DAILY') : ''; + $daylightIterator = $daylight ? new RRuleIterator($daylightRRule, $standardIterator->current()) : null; + $daylightIterator && $daylightIterator->next(); + + foreach ($timezones as $timezone) { + $tz = new \DateTimeZone($timezone); + // check standard + $timestamp = $standardIterator->current()->getTimestamp(); + $transitions = $tz->getTransitions($timestamp, $timestamp + 1); + if (empty($transitions)) { + continue; + } + + $checkOffset = $transitions[0]['offset']; + + if ($checkOffset !== $this->parseOffsetToInteger($standardOffset)) { + continue; + } + + if (!$daylight) { + return TimeZoneUtil::getTimeZone($timezone, null, $failIfUncertain); + } + + // check daylight + $timestamp = $daylightIterator->current()->getTimestamp(); + $transitions = $tz->getTransitions($timestamp, $timestamp + 1); + if (empty($transitions)) { + continue; + } + + $checkOffset = $transitions[0]['offset']; + if ($checkOffset === $this->parseOffsetToInteger($daylightOffset)) { + return TimeZoneUtil::getTimeZone($timezone, null, $failIfUncertain); + } + } + + return null; + } + + private function parseOffsetToInteger(string $offset): int + { + $time = ((int) ($offset[1].$offset[2]) * 60) + (int) ($offset[3].$offset[4]); + + $time = $time * 60; + + if ('-' === $offset[0]) { + $time = $time * -1; + } + + return $time; + } +} diff --git a/lib/TimezoneGuesser/GuessFromLicEntry.php b/lib/TimezoneGuesser/GuessFromLicEntry.php index 486b3379..fbd846e0 100644 --- a/lib/TimezoneGuesser/GuessFromLicEntry.php +++ b/lib/TimezoneGuesser/GuessFromLicEntry.php @@ -20,6 +20,10 @@ public function guess(VTimeZone $vtimezone, ?bool $failIfUncertain = false): ?\D $lic = (string) $vtimezone->{'X-LIC-LOCATION'}; + if ('Customized Time Zone' === $lic) { + return null; + } + // Libical generators may specify strings like // "SystemV/EST5EDT". For those we must remove the // SystemV part. diff --git a/lib/TimezoneGuesser/LowercaseTimezoneIdentifier.php b/lib/TimezoneGuesser/LowercaseTimezoneIdentifier.php new file mode 100644 index 00000000..a6887b94 --- /dev/null +++ b/lib/TimezoneGuesser/LowercaseTimezoneIdentifier.php @@ -0,0 +1,19 @@ + 'America/Noronha', 'Brasilia' => 'America/Sao_Paulo', // Best guess 'Buenos Aires' => 'America/Argentina/Buenos_Aires', - 'Greenland' => 'America/Godthab', + 'Greenland' => 'Atlantic/Stanley', 'Newfoundland' => 'America/St_Johns', 'Atlantic Time (Canada)' => 'America/Halifax', 'Caracas, La Paz' => 'America/Caracas', @@ -91,4 +91,7 @@ 'Hawaii' => 'Pacific/Honolulu', 'Midway Island, Samoa' => 'Pacific/Midway', 'Eniwetok, Kwajalein, Dateline Time' => 'Pacific/Kwajalein', + + // Localized timezones + 'Amsterdam, Berlin, Berne, Rome, Stockholm, Vienne' => 'Europe/Berlin', ]; diff --git a/lib/timezonedata/extrazones.php b/lib/timezonedata/extrazones.php new file mode 100644 index 00000000..8e477eb9 --- /dev/null +++ b/lib/timezonedata/extrazones.php @@ -0,0 +1,208 @@ + 'America/Rio_Branco', + 'Africa Central' => 'Africa/Maputo', + 'Africa Eastern' => 'Africa/Nairobi', + 'Africa FarWestern' => 'Africa/El_Aaiun', + 'Africa Southern' => 'Africa/Johannesburg', + 'Africa Western' => 'Africa/Lagos', + 'Aktyubinsk' => 'Asia/Aqtobe', + 'Alaska Hawaii' => 'America/Anchorage', + 'Almaty' => 'Asia/Almaty', + 'Amazon' => 'America/Manaus', + 'America Central' => 'America/Chicago', + 'America Eastern' => 'America/New_York', + 'America Mountain' => 'America/Denver', + 'America Pacific' => 'America/Los_Angeles', + 'Anadyr' => 'Asia/Anadyr', + 'Apia' => 'Pacific/Apia', + 'Aqtau' => 'Asia/Aqtau', + 'Aqtobe' => 'Asia/Aqtobe', + 'Argentina Western' => 'America/Argentina/San_Luis', + 'Armenia' => 'Asia/Yerevan', + 'Armenian Standard Time' => 'Asia/Yerevan', + 'Ashkhabad' => 'Asia/Ashgabat', + 'Australia Central' => 'Australia/Adelaide', + 'Australia CentralWestern' => 'Australia/Eucla', + 'Australia Eastern' => 'Australia/Sydney', + 'Australia Western' => 'Australia/Perth', + 'Azerbaijan' => 'Asia/Baku', + 'Baku' => 'Asia/Baku', + 'Bangladesh' => 'Asia/Dhaka', + 'Bering' => 'America/Adak', + 'Bhutan' => 'Asia/Thimphu', + 'Bolivia' => 'America/La_Paz', + 'Borneo' => 'Asia/Kuching', + 'British' => 'Europe/London', + 'Brunei' => 'Asia/Brunei', + 'Casey' => 'Antarctica/Casey', + 'Chamorro' => 'Pacific/Saipan', + 'Chatham' => 'Pacific/Chatham', + 'Chile' => 'America/Santiago', + 'Choibalsan' => 'Asia/Choibalsan', + 'Christmas' => 'Indian/Christmas', + 'Cocos' => 'Indian/Cocos', + 'Colombia' => 'America/Bogota', + 'Cook' => 'Pacific/Rarotonga', + 'Dacca' => 'Asia/Dhaka', + 'Davis' => 'Antarctica/Davis', + 'Dominican' => 'America/Santo_Domingo', + 'DumontDUrville' => 'Antarctica/DumontDUrville', + 'Dushanbe' => 'Asia/Dushanbe', + 'Dutch Guiana' => 'America/Paramaribo', + 'East Timor' => 'Asia/Dili', + 'Easter' => 'Pacific/Easter', + 'Ecuador' => 'America/Guayaquil', + 'Europe Central' => 'Europe/Paris', + 'Europe Eastern' => 'Europe/Bucharest', + 'Europe Further Eastern' => 'Europe/Minsk', + 'Europe Western' => 'Atlantic/Canary', + 'Falkland' => 'Atlantic/Stanley', + 'Fiji Islands Standard Time' => 'Pacific/Fiji', + 'French Guiana' => 'America/Cayenne', + 'French Southern' => 'Indian/Kerguelen', + 'Frunze' => 'Asia/Bishkek', + 'Galapagos' => 'Pacific/Galapagos', + 'Gambier' => 'Pacific/Gambier', + 'Georgia' => 'Asia/Tbilisi', + 'Gilbert Islands' => 'Pacific/Tarawa', + 'GMT' => 'Europe/London', + 'Goose Bay' => 'America/Goose_Bay', + 'Greenland Central' => 'America/Scoresbysund', + 'Greenland Eastern' => 'America/Scoresbysund', + 'Greenland Western' => 'Atlantic/Stanley', + 'Guam' => 'Pacific/Guam', + 'Gulf' => 'Asia/Dubai', + 'Guyana' => 'America/Guyana', + 'Hawaii Aleutian' => 'Pacific/Honolulu', + 'Hong Kong' => 'Asia/Hong_Kong', + 'Hovd' => 'Asia/Hovd', + 'Indian Ocean' => 'Indian/Chagos', + 'Indochina' => 'Asia/Bangkok', + 'Indonesia Central' => 'Asia/Makassar', + 'Indonesia Eastern' => 'Asia/Jayapura', + 'Indonesia Western' => 'Asia/Jakarta', + 'Irish' => 'Europe/Dublin', + 'Irkutsk' => 'Asia/Irkutsk', + 'Kamchatka' => 'Asia/Kamchatka', + 'Kamchatka Standard Time' => 'Asia/Kamchatka', + 'Karachi' => 'Asia/Karachi', + 'Kazakhstan Eastern' => 'Asia/Almaty', + 'Kazakhstan Western' => 'Asia/Aqtobe', + 'Kizilorda' => 'Asia/Qyzylorda', + 'Kosrae' => 'Pacific/Kosrae', + 'Kuybyshev' => 'Europe/Samara', + 'Kyrgystan' => 'Asia/Bishkek', + 'Lanka' => 'Asia/Colombo', + 'Liberia' => 'Africa/Monrovia', + 'Line Islands' => 'Pacific/Kiritimati', + 'Lord Howe' => 'Australia/Lord_Howe', + 'Macau' => 'Asia/Macau', + 'Macquarie' => 'Antarctica/Macquarie', + 'Magadan' => 'Asia/Magadan', + 'Magallanes Standard Time' => 'America/Punta_Arenas', + 'Malaya' => 'Asia/Kuala_Lumpur', + 'Malaysia' => 'Asia/Kuching', + 'Maldives' => 'Indian/Maldives', + 'Marquesas' => 'Pacific/Marquesas', + 'Marshall Islands' => 'Pacific/Majuro', + 'Mawson' => 'Antarctica/Mawson', + 'Mexico Pacific' => 'America/Mazatlan', + 'Mexico Standard Time' => 'America/Mexico_City', + 'Mid-Atlantic Standard Time' => 'Atlantic/Cape_Verde', + 'Mongolia' => 'Asia/Ulaanbaatar', + 'Moscow' => 'Europe/Moscow', + 'Nauru' => 'Pacific/Nauru', + 'New Caledonia' => 'Pacific/Noumea', + 'Newfoundland And Labrador Standard Time' => 'America/St_Johns', + 'Niue' => 'Pacific/Niue', + 'Norfolk' => 'Pacific/Norfolk', + 'Noronha' => 'America/Noronha', + 'North Mariana' => 'Pacific/Saipan', + 'Novosibirsk' => 'Asia/Novosibirsk', + 'Omsk' => 'Asia/Omsk', + 'Omsk Standard Time' => 'Asia/Omsk', + 'Oral' => 'Asia/Oral', + 'Palau' => 'Pacific/Palau', + 'Papua New Guinea' => 'Pacific/Port_Moresby', + 'Paraguay' => 'America/Asuncion', + 'Peru' => 'America/Lima', + 'Philippines' => 'Asia/Manila', + 'Phoenix Islands' => 'Pacific/Fakaofo', + 'Pierre Miquelon' => 'America/Miquelon', + 'Pitcairn' => 'Pacific/Pitcairn', + 'Pyongyang' => 'Asia/Pyongyang', + 'Qyzylorda' => 'Asia/Qyzylorda', + 'Qyzylorda Standard Time' => 'Asia/Qyzylorda', + 'Reunion' => 'Indian/Reunion', + 'Rothera' => 'Antarctica/Rothera', + 'Sakhalin' => 'Asia/Sakhalin', + 'Samara' => 'Europe/Samara', + 'Samarkand' => 'Asia/Samarkand', + 'Sao Tome Standard Time' => 'Africa/Sao_Tome', + 'Saratov Standard Time' => 'Europe/Saratov', + 'Seychelles' => 'Indian/Mahe', + 'Shevchenko' => 'Asia/Aqtau', + 'Solomon' => 'Pacific/Guadalcanal', + 'South Georgia' => 'Atlantic/South_Georgia', + 'Sudan Standard Time' => 'Africa/Khartoum', + 'Suriname' => 'America/Paramaribo', + 'Sverdlovsk' => 'Asia/Yekaterinburg', + 'Syowa' => 'Antarctica/Syowa', + 'Tahiti' => 'Pacific/Tahiti', + 'Tajikistan' => 'Asia/Dushanbe', + 'Tashkent' => 'Asia/Tashkent', + 'Tbilisi' => 'Asia/Tbilisi', + 'Tokelau' => 'Pacific/Fakaofo', + 'Transitional Islamic State Of Afghanistan Standard Time' => 'Asia/Kabul', + 'Turkmenistan' => 'Asia/Ashgabat', + 'Tuvalu' => 'Pacific/Funafuti', + 'Uralsk' => 'Asia/Oral', + 'Uruguay' => 'America/Montevideo', + 'Urumqi' => 'Asia/Urumqi', + 'Uzbekistan' => 'Asia/Tashkent', + 'Vanuatu' => 'Pacific/Efate', + 'Volgograd' => 'Europe/Volgograd', + 'Volgograd Standard Time' => 'Europe/Volgograd', + 'Vostok' => 'Antarctica/Vostok', + 'Wake' => 'Pacific/Wake', + 'Wallis' => 'Pacific/Wallis', + 'Yekaterinburg' => 'Asia/Yekaterinburg', + 'Yerevan' => 'Asia/Yerevan', + 'Yukon' => 'America/Yakutat', + // Overwrite + 'Argentina Standard Time' => 'America/Argentina/Buenos_Aires', + 'Dateline' => 'Pacific/Auckland', + 'Dateline Standard Time' => 'Pacific/Niue', + 'India' => 'Asia/Kolkata', + 'India Standard Time' => 'Asia/Kolkata', + 'Kolkata, Chennai, Mumbai, New Delhi, India Standard Time' => 'Asia/Kolkata', + 'Myanmar' => 'Asia/Yangon', + 'Myanmar Standard Time' => 'Asia/Yangon', + 'Nepal Standard Time' => 'Asia/Kathmandu', + 'Rangoon' => 'Asia/Yangon', + 'Greenwich' => 'Atlantic/Reykjavik', + 'UTC-02' => 'America/Noronha', + 'UTC-08' => 'Pacific/Pitcairn', + 'UTC-09' => 'Pacific/Gambier', + 'UTC-11' => 'Pacific/Niue', + 'UTC+12' => 'Pacific/Auckland', + 'UTC-05:00' => 'America/Lima', + 'US Eastern Standard Time' => 'America/New_York', + 'tzone://Microsoft/Utc' => 'UTC', + 'America/Santa_Isabel' => 'America/Tijuana', + 'Asia/Chongqing' => 'Asia/Shanghai', + 'Asia/Harbin' => 'Asia/Shanghai', + 'Asia/Kashgar' => 'Asia/Urumqi', + 'Pacific/Johnston' => 'Pacific/Honolulu', + 'EDT' => 'America/Manaus', + 'America/Godthab' => 'Atlantic/Stanley', + 'CDT' => 'America/Chicago', + 'PST' => 'America/Los_Angeles', + 'Gulf Standard Time' => 'Asia/Dubai', +]; diff --git a/lib/timezonedata/lotuszones.php b/lib/timezonedata/lotuszones.php index 9115ac74..684a0c97 100644 --- a/lib/timezonedata/lotuszones.php +++ b/lib/timezonedata/lotuszones.php @@ -34,7 +34,7 @@ 'Newfoundland' => 'America/St_Johns', 'Argentina' => 'America/Argentina/Buenos_Aires', 'E. South America' => 'America/Belem', - 'Greenland' => 'America/Godthab', + 'Greenland' => 'Atlantic/Stanley', 'Montevideo' => 'America/Montevideo', 'SA Eastern' => 'America/Belem', // 'Mid-Atlantic' => 'Etc/GMT-2', // conflict with windows timezones. diff --git a/lib/timezonedata/php-bc.php b/lib/timezonedata/php-bc.php index 3116c686..bd60f26d 100644 --- a/lib/timezonedata/php-bc.php +++ b/lib/timezonedata/php-bc.php @@ -16,6 +16,23 @@ * @license http://sabre.io/license/ Modified BSD License */ return [ + // Moved to backward in 2021b + 'Pacific/Enderbury', + // Moved to backward in 2022b + 'Europe/Kiev', + // Moved to backward in 2022d + 'Europe/Uzhgorod', + 'Europe/Zaporozhye', + // Moved to backward in 2022f + 'America/Thunder_Bay', + 'America/Nipigon', + 'America/Rainy_River', + // Moved to backward in 2022g + 'America/Pangnirtung', + // Moved to backward in 2023a + 'America/Yellowknife', + + // Original list 'Africa/Asmera', 'Africa/Timbuktu', 'America/Argentina/ComodRivadavia', diff --git a/lib/timezonedata/windowszones.php b/lib/timezonedata/windowszones.php index 2049a95c..c1bd8026 100644 --- a/lib/timezonedata/windowszones.php +++ b/lib/timezonedata/windowszones.php @@ -60,7 +60,7 @@ 'GMT Standard Time' => 'Europe/London', 'GTB Standard Time' => 'Europe/Bucharest', 'Georgian Standard Time' => 'Asia/Tbilisi', - 'Greenland Standard Time' => 'America/Godthab', + 'Greenland Standard Time' => 'Atlantic/Stanley', 'Greenwich Standard Time' => 'Atlantic/Reykjavik', 'Haiti Standard Time' => 'America/Port-au-Prince', 'Hawaiian Standard Time' => 'Pacific/Honolulu', @@ -149,4 +149,91 @@ 'West Pacific Standard Time' => 'Pacific/Port_Moresby', 'Yakutsk Standard Time' => 'Asia/Yakutsk', 'Yukon Standard Time' => 'America/Whitehorse', + 'coordinated universal time-11' => 'Pacific/Pago_Pago', + 'aleutian islands' => 'America/Adak', + 'marquesas islands' => 'Pacific/Marquesas', + 'coordinated universal time-09' => 'America/Anchorage', + 'baja california' => 'America/Tijuana', + 'coordinated universal time-08' => 'Pacific/Pitcairn', + 'chihuahua, la paz, mazatlan' => 'America/Chihuahua', + 'easter island' => 'Pacific/Easter', + 'guadalajara, mexico city, monterrey' => 'America/Mexico_City', + 'bogota, lima, quito, rio branco' => 'America/Bogota', + 'chetumal' => 'America/Cancun', + 'haiti' => 'America/Port-au-Prince', + 'havana' => 'America/Havana', + 'turks and caicos' => 'America/Grand_Turk', + 'asuncion' => 'America/Asuncion', + 'caracas' => 'America/Caracas', + 'cuiaba' => 'America/Cuiaba', + 'georgetown, la paz, manaus, san juan' => 'America/La_Paz', + 'araguaina' => 'America/Araguaina', + 'cayenne, fortaleza' => 'America/Cayenne', + 'city of buenos aires' => 'America/Argentina/Buenos_Aires', + 'punta arenas' => 'America/Punta_Arenas', + 'saint pierre and miquelon' => 'America/Miquelon', + 'salvador' => 'America/Bahia', + 'coordinated universal time-02' => 'America/Noronha', + 'mid-atlantic - old' => 'America/Noronha', + 'cabo verde is' => 'Atlantic/Cape_Verde', + 'coordinated universal time' => 'UTC', + 'dublin, edinburgh, lisbon, london' => 'Europe/London', + 'monrovia, reykjavik' => 'Atlantic/Reykjavik', + 'belgrade, bratislava, budapest, ljubljana, prague' => 'Europe/Budapest', + 'casablanca' => 'Africa/Casablanca', + 'sao tome' => 'Africa/Sao_Tome', + 'sarajevo, skopje, warsaw, zagreb' => 'Europe/Warsaw', + 'amman' => 'Asia/Amman', + 'athens, bucharest' => 'Europe/Bucharest', + 'beirut' => 'Asia/Beirut', + 'chisinau' => 'Europe/Chisinau', + 'damascus' => 'Asia/Damascus', + 'gaza, hebron' => 'Asia/Hebron', + 'jerusalem' => 'Asia/Jerusalem', + 'kaliningrad' => 'Europe/Kaliningrad', + 'khartoum' => 'Africa/Khartoum', + 'tripoli' => 'Africa/Tripoli', + 'windhoek' => 'Africa/Windhoek', + 'istanbul' => 'Europe/Istanbul', + 'kuwait, riyadh' => 'Asia/Riyadh', + 'minsk' => 'Europe/Minsk', + 'moscow, st petersburg' => 'Europe/Moscow', + 'nairobi' => 'Africa/Nairobi', + 'astrakhan, ulyanovsk' => 'Europe/Astrakhan', + 'izhevsk, samara' => 'Europe/Samara', + 'port louis' => 'Indian/Mauritius', + 'saratov' => 'Europe/Saratov', + 'ashgabat, tashkent' => 'Asia/Tashkent', + 'islamabad, karachi' => 'Asia/Karachi', + 'chennai, kolkata, mumbai, new delhi' => 'Asia/Kolkata', + 'sri jayawardenepura' => 'Asia/Colombo', + 'kathmandu' => 'Asia/Kathmandu', + 'astana' => 'Asia/Almaty', + 'dhaka' => 'Asia/Dhaka', + 'yangon (rangoon)' => 'Asia/Rangoon', + 'barnaul, gorno-altaysk' => 'Asia/Barnaul', + 'tomsk' => 'Asia/Tomsk', + 'beijing, chongqing, hong kong, urumqi' => 'Asia/Shanghai', + 'perth' => 'Australia/Perth', + 'ulaanbaatar' => 'Asia/Ulaanbaatar', + 'eucla' => 'Australia/Eucla', + 'chita' => 'Asia/Chita', + 'seoul' => 'Asia/Seoul', + 'adelaide' => 'Australia/Adelaide', + 'brisbane' => 'Australia/Brisbane', + 'canberra, melbourne, sydney' => 'Australia/Sydney', + 'hobart' => 'Australia/Hobart', + 'lord howe island' => 'Australia/Lord_Howe', + 'bougainville island' => 'Pacific/Bougainville', + 'chokurdakh' => 'Asia/Srednekolymsk', + 'norfolk island' => 'Pacific/Norfolk', + 'solomon is, new caledonia' => 'Pacific/Guadalcanal', + 'anadyr, petropavlovsk-kamchatsky' => 'Asia/Kamchatka', + 'coordinated universal time+12' => 'Pacific/Tarawa', + 'petropavlovsk-kamchatsky - old' => 'Asia/Anadyr', + 'chatham islands' => 'Pacific/Chatham', + 'coordinated universal time+13' => 'Pacific/Fakaofo', + "nuku'alofa" => 'Pacific/Tongatapu', + 'kiritimati island' => 'Pacific/Kiritimati', + 'helsinki, kyiv, riga, sofia, tallinn, vilnius' => 'Europe/Helsinki', ]; diff --git a/tests/VObject/Component/VTimeZoneTest.php b/tests/VObject/Component/VTimeZoneTest.php index 2928ecd0..19a88cb7 100644 --- a/tests/VObject/Component/VTimeZoneTest.php +++ b/tests/VObject/Component/VTimeZoneTest.php @@ -51,4 +51,25 @@ public function testGetTimeZone(): void $obj->VTIMEZONE->getTimeZone() ); } + + public function testGetEmptyTimeZone() + { + $input = <<assertEquals( + $tz, + $obj->VTIMEZONE->getTimeZone() + ); + } } diff --git a/tests/VObject/EmptyParameterTest.php b/tests/VObject/EmptyParameterTest.php index ab197ab3..6739f104 100644 --- a/tests/VObject/EmptyParameterTest.php +++ b/tests/VObject/EmptyParameterTest.php @@ -21,7 +21,7 @@ public function testRead(): void $vcard = Reader::read($input); self::assertInstanceOf(Component\VCard::class, $vcard); - $vcard = $vcard->convert(\Sabre\VObject\Document::VCARD30); + $vcard = $vcard->convert(Document::VCARD30); $vcard = $vcard->serialize(); $converted = Reader::read($vcard); diff --git a/tests/VObject/InvalidValueParamTest.php b/tests/VObject/InvalidValueParamTest.php new file mode 100644 index 00000000..744fb193 --- /dev/null +++ b/tests/VObject/InvalidValueParamTest.php @@ -0,0 +1,54 @@ +assertEquals("LOCATION:EXAMPLE\r\n", $doc->VEVENT->LOCATION->serialize()); + } + + public function testInvalidValue() + { + $event = <<assertEquals("LOCATION:consectetur adipiscing elit\,sed do eiusmod tempor\r\n", $doc->VEVENT->LOCATION->serialize()); + } +} diff --git a/tests/VObject/Parser/UnfoldingTest.php b/tests/VObject/Parser/UnfoldingTest.php new file mode 100644 index 00000000..8d751ae7 --- /dev/null +++ b/tests/VObject/Parser/UnfoldingTest.php @@ -0,0 +1,99 @@ +parse($vcard, Reader::OPTION_FIX_UNFOLDING); + + $this->assertNotNull($vcard->children()[0]->{'X-APPLE-STRUCTURED-LOCATION'}->getValue()); + } + + public function testNotFixUnfolding() + { + $this->expectException(ParseException::class); + + $vcard = <<parse($vcard); + } + + public function testNotFixUnknownProperty() + { + $vcard = <<parse($vcard); + + $this->assertNotNull($vcard->children()[0]->CONFERENCE->getValue()); + } +} diff --git a/tests/VObject/Property/ICalendar/DateTimeTest.php b/tests/VObject/Property/ICalendar/DateTimeTest.php index 00c8be1b..0da557b9 100644 --- a/tests/VObject/Property/ICalendar/DateTimeTest.php +++ b/tests/VObject/Property/ICalendar/DateTimeTest.php @@ -304,12 +304,10 @@ public function testGetDateTimeBadTimeZone(): void $this->vcal->add($event); $this->vcal->add($timezone); - $dt = $elem->getDateTime(); - - self::assertInstanceOf('DateTimeImmutable', $dt); - self::assertEquals('1985-07-04 01:30:00', $dt->format('Y-m-d H:i:s')); - self::assertEquals('Canada/Eastern', $dt->getTimeZone()->getName()); date_default_timezone_set($default); + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('We were unable to determine the correct PHP timezone for tzid: Moon'); + $elem->getDateTime(); } public function testUpdateValueParameter(): void diff --git a/tests/VObject/ReaderTest.php b/tests/VObject/ReaderTest.php index 645ed181..336a291a 100644 --- a/tests/VObject/ReaderTest.php +++ b/tests/VObject/ReaderTest.php @@ -455,4 +455,28 @@ public function testReadXMLStream(): void self::assertEquals('VCALENDAR', $result->name); self::assertCount(0, $result->children()); } + + public function testReadDuplicateValue(): void + { + $input = <<VEVENT->DTSTART->serialize()); + } } diff --git a/tests/VObject/Recur/EventIterator/MainTest.php b/tests/VObject/Recur/EventIterator/MainTest.php index bce4bcd9..022df4a3 100644 --- a/tests/VObject/Recur/EventIterator/MainTest.php +++ b/tests/VObject/Recur/EventIterator/MainTest.php @@ -24,7 +24,7 @@ public function testValues(): void /** @var VEvent $ev */ $ev = $vcal->createComponent('VEVENT'); $ev->UID = 'bla'; - $ev->RRULE = 'FREQ=DAILY;BYHOUR=10;BYMINUTE=5;BYSECOND=16;BYWEEKNO=32;BYYEARDAY=100,200'; + $ev->RRULE = 'FREQ=DAILY;BYHOUR=10;BYMINUTE=5;BYSECOND=16'; /** @var DateTime $dtStart */ $dtStart = $vcal->createProperty('DTSTART'); $dtStart->setDateTime(new \DateTimeImmutable('2011-10-07')); diff --git a/tests/VObject/Recur/FastForwardBeforeTest.php b/tests/VObject/Recur/FastForwardBeforeTest.php new file mode 100644 index 00000000..0426cc5e --- /dev/null +++ b/tests/VObject/Recur/FastForwardBeforeTest.php @@ -0,0 +1,570 @@ +fastForwardBefore($ffDate); + $ru = getrusage(); + $endTime = $ru['ru_utime.tv_sec'] * 1000000 + $ru['ru_utime.tv_usec']; + $this->assertLessThan(self::FF_TIMEOUT, $endTime - $startTime); + } + + public function testFastForwardBeforeYearlyBasic() + { + $startDate = new \DateTime('1970-10-23 00:00:00', new \DateTimeZone('zulu')); + $ffDate = new \DateTime('midnight', new \DateTimeZone('zulu')); + $ffDate->setDate(99999, 1, 1); + $rrule = new RRuleIterator('FREQ=YEARLY', $startDate); + + $this->fastForward($rrule, $ffDate); + + $year = 60 * 60 * 24 * 365; + $expected = (new \DateTime()) + ->setTimezone(new \DateTimeZone('zulu')) + ->setDate(99998, 10, 23) + ->setTime(0, 0, 0) + ->getTimestamp(); + $this->assertEquals($expected, $rrule->current()->getTimestamp()); + + $rrule->next(); + $expected += $year; + $this->assertEquals($expected, $rrule->current()->getTimestamp()); + + $rrule->next(); + // It's a leap + $expected += $year + 24 * 60 * 60; + $this->assertEquals($expected, $rrule->current()->getTimestamp()); + + $rrule->next(); + $expected += $year; + $this->assertEquals($expected, $rrule->current()->getTimestamp()); + + $rrule->next(); + $expected += $year; + $this->assertEquals($expected, $rrule->current()->getTimestamp()); + + $rrule->next(); + $expected += $year; + $this->assertEquals($expected, $rrule->current()->getTimestamp()); + + $rrule->next(); + // leap + $expected += $year + 24 * 60 * 60; + $this->assertEquals($expected, $rrule->current()->getTimestamp()); + + $rrule->next(); + $expected += $year; + $this->assertEquals($expected, $rrule->current()->getTimestamp()); + + $rrule->next(); + $expected += $year; + $this->assertEquals($expected, $rrule->current()->getTimestamp()); + + $rrule->next(); + $expected += $year; + $this->assertEquals($expected, $rrule->current()->getTimestamp()); + + $rrule->next(); + // leap + $expected += $year + 24 * 60 * 60; + $this->assertEquals($expected, $rrule->current()->getTimestamp()); + + $rrule->next(); + } + + public function testFastForwardBeforeYearlyByYearDay() + { + $startDate = new \DateTime('1970-10-23 00:00:00', new \DateTimeZone('zulu')); + $ffDate = new \DateTime('midnight', new \DateTimeZone('zulu')); + $ffDate->setDate(99999, 1, 5); + $rrule = new RRuleIterator('FREQ=YEARLY;BYYEARDAY=1,20,300', $startDate); + + $this->fastForward($rrule, $ffDate); + + // 1st day + $day = 60 * 60 * 24; + $expected = (new \DateTime()) + ->setTimezone(new \DateTimeZone('zulu')) + ->setDate(99999, 1, 1) + ->setTime(0, 0, 0) + ->getTimestamp(); + $this->assertEquals($expected, $rrule->current()->getTimestamp()); + + // 20th day + $rrule->next(); + $expected += 19 * $day; + $this->assertEquals($expected, $rrule->current()->getTimestamp()); + + // 300th day + $rrule->next(); + $expected += 280 * $day; + $this->assertEquals($expected, $rrule->current()->getTimestamp()); + + // 1st day + $expected += 66 * $day; + $rrule->next(); + $this->assertEquals($expected, $rrule->current()->getTimestamp()); + + // 20th day + $rrule->next(); + $expected += 19 * $day; + $this->assertEquals($expected, $rrule->current()->getTimestamp()); + + // 300th day + $rrule->next(); + $expected += 280 * $day; + $this->assertEquals($expected, $rrule->current()->getTimestamp()); + + // 1st day (leap year, we have 366 days in this year) + $rrule->next(); + $expected += 67 * $day; + $this->assertEquals($expected, $rrule->current()->getTimestamp()); + + // 20th day + $rrule->next(); + $expected += 19 * $day; + $this->assertEquals($expected, $rrule->current()->getTimestamp()); + } + + public function testFastForwardBeforeYearlyByWeekNo() + { + $startDate = new \DateTime('1970-10-23 00:00:00', new \DateTimeZone('zulu')); + $ffDate = new \DateTime('midnight', new \DateTimeZone('zulu')); + $ffDate->setDate(99999, 1, 5); + $rrule = new RRuleIterator('FREQ=YEARLY;BYWEEKNO=1,20', $startDate); + + $this->fastForward($rrule, $ffDate); + + $day = 60 * 60 * 24; + $week = 7 * $day; + + // 1st week + $expected = (new \DateTime()) + ->setTimezone(new \DateTimeZone('zulu')) + ->setDate(99999, 1, 4) + ->setTime(0, 0, 0) + ->getTimestamp(); + $this->assertEquals($expected, $rrule->current()->getTimestamp()); + + // 20st week + $rrule->next(); + $expected += $week * 19; + $this->assertEquals($expected, $rrule->current()->getTimestamp()); + } + + public function testFastForwardBeforeYearlyAdvanced() + { + $startDate = new \DateTime('1970-10-23 12:34:56', new \DateTimeZone('zulu')); + $ffDate = new \DateTime('midnight', new \DateTimeZone('zulu')); + $ffDate->setDate(10000, 1, 2)->setTime(8, 44, 13); + $rrule = new RRuleIterator('FREQ=YEARLY;INTERVAL=2;BYMONTH=1;BYDAY=SU;BYHOUR=8,9;BYMINUTE=30', $startDate); + + $this->fastForward($rrule, $ffDate); + + $expected = (new \DateTime('midnight', new \DateTimeZone('zulu'))) + ->setDate(10000, 1, 2) + ->setTime(8, 30, 56) + ->getTimestamp(); + $this->assertEquals($expected, $rrule->current()->getTimestamp()); + + $rrule->next(); + $expected += 60 * 60; + $this->assertEquals($expected, $rrule->current()->getTimestamp()); + + $rrule->next(); + $expected += 7 * 24 * 60 * 60 - 60 * 60; + $this->assertEquals($expected, $rrule->current()->getTimestamp()); + + $rrule->next(); + $expected += 60 * 60; + $this->assertEquals($expected, $rrule->current()->getTimestamp()); + + $rrule->next(); + $expected += 7 * 24 * 60 * 60 - 60 * 60; + $this->assertEquals($expected, $rrule->current()->getTimestamp()); + + $rrule->next(); + $expected += 60 * 60; + $this->assertEquals($expected, $rrule->current()->getTimestamp()); + + $rrule->next(); + $expected += 7 * 24 * 60 * 60 - 60 * 60; + $this->assertEquals($expected, $rrule->current()->getTimestamp()); + + $rrule->next(); + $expected += 60 * 60; + $this->assertEquals($expected, $rrule->current()->getTimestamp()); + + $rrule->next(); + $expected += 7 * 24 * 60 * 60 - 60 * 60; + $this->assertEquals($expected, $rrule->current()->getTimestamp()); + + $rrule->next(); + $expected += 60 * 60; + $this->assertEquals($expected, $rrule->current()->getTimestamp()); + + // jump to 6th january 10002 + $rrule->next(); + $expected = (new \DateTime('midnight', new \DateTimeZone('zulu'))) + ->setDate(10002, 1, 6) + ->setTime(8, 30, 56) + ->getTimestamp(); + $this->assertEquals($expected, $rrule->current()->getTimestamp()); + + $rrule->next(); + $expected += 60 * 60; + $this->assertEquals($expected, $rrule->current()->getTimestamp()); + } + + public function testFastForwardBeforeMonthlyBasic() + { + $startDate = new \DateTime('1970-10-23 22:42:31', new \DateTimeZone('zulu')); + $ffDate = new \DateTime('midnight', new \DateTimeZone('zulu')); + $ffDate->setDate(18000, 1, 30); + $rrule = new RRuleIterator('FREQ=MONTHLY', $startDate); + + $this->fastForward($rrule, $ffDate); + + $expected = (new \DateTime('midnight', new \DateTimeZone('zulu'))) + ->setDate(18000, 1, 23) + ->setTime(22, 42, 31) + ->getTimestamp(); + $this->assertEquals($expected, $rrule->current()->getTimestamp()); + + // february + $rrule->next(); + $expected += 31 * 24 * 60 * 60; + $this->assertEquals($expected, $rrule->current()->getTimestamp()); + // march + $rrule->next(); + $expected += 29 * 24 * 60 * 60; + $this->assertEquals($expected, $rrule->current()->getTimestamp()); + // april + $rrule->next(); + $expected += 31 * 24 * 60 * 60; + $this->assertEquals($expected, $rrule->current()->getTimestamp()); + // may + $rrule->next(); + $expected += 30 * 24 * 60 * 60; + $this->assertEquals($expected, $rrule->current()->getTimestamp()); + // june + $rrule->next(); + $expected += 31 * 24 * 60 * 60; + $this->assertEquals($expected, $rrule->current()->getTimestamp()); + // july + $rrule->next(); + $expected += 30 * 24 * 60 * 60; + $this->assertEquals($expected, $rrule->current()->getTimestamp()); + // august + $rrule->next(); + $expected += 31 * 24 * 60 * 60; + $this->assertEquals($expected, $rrule->current()->getTimestamp()); + } + + public function testFastForwardBeforeMonthly31thDay() + { + $timezone = 'America/New_York'; + $startDate = new \DateTime('1970-01-31 00:00:00', new \DateTimeZone($timezone)); + $ffDate = new \DateTime('midnight', new \DateTimeZone('zulu')); + $ffDate->setDate(18000, 2, 1); + $rrule = new RRuleIterator('FREQ=MONTHLY', $startDate); + + $this->fastForward($rrule, $ffDate); + + $expected = (new \DateTime('midnight', new \DateTimeZone($timezone))) + ->setDate(18000, 1, 31) + ->getTimestamp(); + $this->assertEquals($expected, $rrule->current()->getTimestamp()); + + // march + $rrule->next(); + $expected = (new \DateTime('1970-01-31 00:00:00', new \DateTimeZone($timezone))) + ->setDate(18000, 3, 31) + ->getTimestamp(); + $this->assertEquals($expected, $rrule->current()->getTimestamp()); + + // may + $rrule->next(); + $expected = (new \DateTime('1970-01-31 00:00:00', new \DateTimeZone($timezone))) + ->setDate(18000, 5, 31) + ->getTimestamp(); + $this->assertEquals($expected, $rrule->current()->getTimestamp()); + + // july + $rrule->next(); + $expected = (new \DateTime('1970-01-31 00:00:00', new \DateTimeZone($timezone))) + ->setDate(18000, 7, 31) + ->getTimestamp(); + $this->assertEquals($expected, $rrule->current()->getTimestamp()); + + // august + $rrule->next(); + $expected = (new \DateTime('1970-01-31 00:00:00', new \DateTimeZone($timezone))) + ->setDate(18000, 8, 31) + ->getTimestamp(); + $this->assertEquals($expected, $rrule->current()->getTimestamp()); + + // october + $rrule->next(); + $expected = (new \DateTime('1970-01-31 00:00:00', new \DateTimeZone($timezone))) + ->setDate(18000, 10, 31) + ->getTimestamp(); + $this->assertEquals($expected, $rrule->current()->getTimestamp()); + + // december + $rrule->next(); + $expected = (new \DateTime('1970-01-31 00:00:00', new \DateTimeZone($timezone))) + ->setDate(18000, 12, 31) + ->getTimestamp(); + $this->assertEquals($expected, $rrule->current()->getTimestamp()); + } + + public function testFastForwardBeforeMonthlyAdvanced() + { + $timezone = 'America/New_York'; + $startDate = new \DateTime('1970-01-31 00:00:00', new \DateTimeZone($timezone)); + $ffDate = new \DateTime('midnight', new \DateTimeZone('zulu')); + $ffDate->setDate(8000, 1, 6); + // every 2 months on the 1st Monday, 2nd Tuesday, 3rd Wednesday and 4th Thursday + $rrule = new RRuleIterator('FREQ=MONTHLY;INTERVAL=2;BYDAY=1MO,2TU,3WE,4TH', $startDate); + + $this->fastForward($rrule, $ffDate); + + // monday + $expected = (new \DateTime('midnight', new \DateTimeZone($timezone))) + ->setDate(8000, 1, 3) + ->getTimestamp(); + $this->assertEquals($expected, $rrule->current()->getTimestamp()); + + // tuesday + $expected = (new \DateTime('1970-01-31 00:00:00', new \DateTimeZone($timezone))) + ->setDate(8000, 1, 11) + ->getTimestamp(); + $rrule->next(); + $this->assertEquals($expected, $rrule->current()->getTimestamp()); + + // wednesday + $expected = (new \DateTime('1970-01-31 00:00:00', new \DateTimeZone($timezone))) + ->setDate(8000, 1, 19) + ->getTimestamp(); + $rrule->next(); + $this->assertEquals($expected, $rrule->current()->getTimestamp()); + + // thursday + $expected = (new \DateTime('1970-01-31 00:00:00', new \DateTimeZone($timezone))) + ->setDate(8000, 1, 27) + ->getTimestamp(); + $rrule->next(); + $this->assertEquals($expected, $rrule->current()->getTimestamp()); + + // monday march + $expected = (new \DateTime('1970-01-31 00:00:00', new \DateTimeZone($timezone))) + ->setDate(8000, 3, 6) + ->getTimestamp(); + $rrule->next(); + $this->assertEquals($expected, $rrule->current()->getTimestamp()); + + // tuesday + $expected = (new \DateTime('1970-01-31 00:00:00', new \DateTimeZone($timezone))) + ->setDate(8000, 3, 14) + ->getTimestamp(); + $rrule->next(); + $this->assertEquals($expected, $rrule->current()->getTimestamp()); + + // wednesday (this month starts on wednesday so that's just the next day) + $expected = (new \DateTime('1970-01-31 00:00:00', new \DateTimeZone($timezone))) + ->setDate(8000, 3, 15) + ->getTimestamp(); + $rrule->next(); + $this->assertEquals($expected, $rrule->current()->getTimestamp()); + + // thursday + $expected += 8 * 24 * 60 * 60; + $expected = (new \DateTime('1970-01-31 00:00:00', new \DateTimeZone($timezone))) + ->setDate(8000, 3, 23) + ->getTimestamp(); + $rrule->next(); + $this->assertEquals($expected, $rrule->current()->getTimestamp()); + } + + public function testFastForwardBeforeDailyBasic() + { + $timezone = 'America/New_York'; + $startDate = new \DateTime('1970-10-23 00:00:00', new \DateTimeZone($timezone)); + $ffDate = new \DateTime('midnight', new \DateTimeZone('zulu')); + $ffDate->setDate(4000, 1, 2); + $rrule = new RRuleIterator('FREQ=DAILY', $startDate); + + $this->fastForward($rrule, $ffDate); + + $expected = (new \DateTime('midnight', new \DateTimeZone($timezone))) + ->setDate(4000, 1, 1) + ->getTimestamp(); + $this->assertEquals($expected, $rrule->current()->getTimestamp()); + + $expected += 24 * 60 * 60; + $rrule->next(); + $this->assertEquals($expected, $rrule->current()->getTimestamp()); + + $expected += 24 * 60 * 60; + $rrule->next(); + $this->assertEquals($expected, $rrule->current()->getTimestamp()); + + $expected += 24 * 60 * 60; + $rrule->next(); + $this->assertEquals($expected, $rrule->current()->getTimestamp()); + + $expected += 24 * 60 * 60; + $rrule->next(); + $this->assertEquals($expected, $rrule->current()->getTimestamp()); + + $expected += 24 * 60 * 60; + $rrule->next(); + $this->assertEquals($expected, $rrule->current()->getTimestamp()); + + $expected += 24 * 60 * 60; + $rrule->next(); + $this->assertEquals($expected, $rrule->current()->getTimestamp()); + } + + public function testFastForwardBeforeDailyAdvanced() + { + $timezone = 'America/New_York'; + $startDate = new \DateTime('1970-10-23 00:00:00', new \DateTimeZone($timezone)); + $ffDate = new \DateTime('midnight', new \DateTimeZone($timezone)); + $ffDate->setDate(4000, 1, 4)->setTime(16, 30, 0); + // every 10 days at 16, 17 and 18 + $rrule = new RRuleIterator('FREQ=DAILY;BYHOUR=16,17,18;INTERVAL=10', $startDate); + + $this->fastForward($rrule, $ffDate); + + $expected = (new \DateTime('midnight', new \DateTimeZone($timezone))) + ->setDate(4000, 1, 4) + ->setTime(16, 0, 0) + ->getTimestamp(); + $this->assertEquals($expected, $rrule->current()->getTimestamp()); + + // 17:00 + $expected += 60 * 60; + $rrule->next(); + $this->assertEquals($expected, $rrule->current()->getTimestamp()); + + // 18:00 + $expected += 60 * 60; + $rrule->next(); + $this->assertEquals($expected, $rrule->current()->getTimestamp()); + + // 16:00 + $expected += 10 * 24 * 60 * 60 - 2 * 60 * 60; + $rrule->next(); + $this->assertEquals($expected, $rrule->current()->getTimestamp()); + + // 17:00 + $expected += 60 * 60; + $rrule->next(); + $this->assertEquals($expected, $rrule->current()->getTimestamp()); + + // 18:00 + $expected += 60 * 60; + $rrule->next(); + $this->assertEquals($expected, $rrule->current()->getTimestamp()); + + // 16:00 + $expected += 10 * 24 * 60 * 60 - 2 * 60 * 60; + $rrule->next(); + $this->assertEquals($expected, $rrule->current()->getTimestamp()); + } + + public function testFastForwardBeforeHourlyBasic() + { + $timezone = 'America/New_York'; + $startDate = new \DateTime('1970-10-23 00:12:34', new \DateTimeZone($timezone)); + $ffDate = new \DateTime('midnight', new \DateTimeZone($timezone)); + $ffDate->setDate(4000, 1, 2)->setTime(2, 0, 0); + $rrule = new RRuleIterator('FREQ=HOURLY', $startDate); + + $this->fastForward($rrule, $ffDate); + + $expected = (new \DateTime('midnight', new \DateTimeZone($timezone))) + ->setDate(4000, 1, 2) + ->setTime(1, 12, 34) + ->getTimestamp(); + $this->assertEquals($expected, $rrule->current()->getTimestamp()); + + $expected += 60 * 60; + $rrule->next(); + $this->assertEquals($expected, $rrule->current()->getTimestamp()); + + $expected += 60 * 60; + $rrule->next(); + $this->assertEquals($expected, $rrule->current()->getTimestamp()); + + $expected += 60 * 60; + $rrule->next(); + $this->assertEquals($expected, $rrule->current()->getTimestamp()); + + $expected += 60 * 60; + $rrule->next(); + $this->assertEquals($expected, $rrule->current()->getTimestamp()); + + $expected += 60 * 60; + $rrule->next(); + $this->assertEquals($expected, $rrule->current()->getTimestamp()); + + $expected += 60 * 60; + $rrule->next(); + $this->assertEquals($expected, $rrule->current()->getTimestamp()); + } + + public function testFastForwardBeforeNotInFrequency() + { + $timezone = 'America/New_York'; + $startDate = new \DateTime('1970-10-23 00:00:00', new \DateTimeZone($timezone)); + $ffDate = new \DateTime('midnight', new \DateTimeZone($timezone)); + $ffDate->setDate(2023, 3, 15)->setTime(1, 0, 0); + // every leap years + $rrule = new RRuleIterator('FREQ=YEARLY;BYMONTH=2;BYMONTHDAY=29', $startDate); + + $this->fastForward($rrule, $ffDate); + + $expected = (new \DateTime('midnight', new \DateTimeZone($timezone))) + ->setDate(2020, 2, 29) + ->setTime(0, 0, 0) + ->getTimestamp(); + $this->assertEquals($expected, $rrule->current()->getTimestamp()); + + // the next leap year + $expected = (new \DateTime('midnight', new \DateTimeZone($timezone))) + ->setDate(2024, 2, 29) + ->setTime(0, 0, 0) + ->getTimestamp(); + $rrule->next(); + $this->assertEquals($expected, $rrule->current()->getTimestamp()); + } + + public function testFastForwardBeforeMultipleTimesBasic() + { + $startDate = new \DateTime('2020-01-02 00:00:00', new \DateTimeZone('zulu')); + $ffDate = new \DateTime('2020-01-18 00:00:00', new \DateTimeZone('zulu')); + $rrule = new RRuleIterator('FREQ=WEEKLY', $startDate); + $expected = new \DateTime('2020-01-16 00:00:00', new \DateTimeZone('zulu')); + + $this->fastForward($rrule, $ffDate); + $this->assertEquals($expected->getTimestamp(), $rrule->current()->getTimestamp()); + + $this->fastForward($rrule, $ffDate); + $this->assertEquals($expected->getTimestamp(), $rrule->current()->getTimestamp()); + + $this->fastForward($rrule, $ffDate); + $this->assertEquals($expected->getTimestamp(), $rrule->current()->getTimestamp()); + } +} diff --git a/tests/VObject/Recur/FastForwardTest.php b/tests/VObject/Recur/FastForwardTest.php new file mode 100644 index 00000000..62effc4d --- /dev/null +++ b/tests/VObject/Recur/FastForwardTest.php @@ -0,0 +1,456 @@ +fastForward($ffDate); + $ru = getrusage(); + $endTime = $ru['ru_utime.tv_sec'] * 1000000 + $ru['ru_utime.tv_usec']; + $this->assertLessThan(self::FF_TIMEOUT, $endTime - $startTime); + } + + public function testFastForwardYearlyBasic() + { + $startDate = new \DateTime('1970-10-23 00:00:00', new \DateTimeZone('zulu')); + $ffDate = new \DateTime('midnight', new \DateTimeZone('zulu')); + $ffDate->setDate(99999, 1, 1); + $rrule = new RRuleIterator('FREQ=YEARLY', $startDate); + + $this->fastForward($rrule, $ffDate); + + $year = 60 * 60 * 24 * 365; + $expected = (new \DateTime()) + ->setTimezone(new \DateTimeZone('zulu')) + ->setDate(99999, 10, 23) + ->setTime(0, 0, 0) + ->getTimestamp(); + $this->assertEquals($expected, $rrule->current()->getTimestamp()); + $rrule->next(); + // It's a leap + $expected += $year + 24 * 60 * 60; + $this->assertEquals($expected, $rrule->current()->getTimestamp()); + $rrule->next(); + $expected += $year; + $this->assertEquals($expected, $rrule->current()->getTimestamp()); + $rrule->next(); + $expected += $year; + $this->assertEquals($expected, $rrule->current()->getTimestamp()); + $rrule->next(); + $expected += $year; + $this->assertEquals($expected, $rrule->current()->getTimestamp()); + $rrule->next(); + // leap + $expected += $year + 24 * 60 * 60; + $this->assertEquals($expected, $rrule->current()->getTimestamp()); + $rrule->next(); + $expected += $year; + $this->assertEquals($expected, $rrule->current()->getTimestamp()); + $rrule->next(); + $expected += $year; + $this->assertEquals($expected, $rrule->current()->getTimestamp()); + $rrule->next(); + $expected += $year; + $this->assertEquals($expected, $rrule->current()->getTimestamp()); + $rrule->next(); + // leap + $expected += $year + 24 * 60 * 60; + $this->assertEquals($expected, $rrule->current()->getTimestamp()); + $rrule->next(); + } + + public function testFastForwardYearlyByYearDay() + { + $startDate = new \DateTime('1970-10-23 00:00:00', new \DateTimeZone('zulu')); + $ffDate = new \DateTime('midnight', new \DateTimeZone('zulu')); + $ffDate->setDate(99998, 12, 31); + $rrule = new RRuleIterator('FREQ=YEARLY;BYYEARDAY=1,20,300', $startDate); + + $this->fastForward($rrule, $ffDate); + + $day = 60 * 60 * 24; + $expected = (new \DateTime()) + ->setTimezone(new \DateTimeZone('zulu')) + ->setDate(99999, 1, 1)// 20th day + ->setTime(0, 0, 0) + ->getTimestamp(); + $this->assertEquals($expected, $rrule->current()->getTimestamp()); + $rrule->next(); + // 300th day + $expected += 19 * $day; + $this->assertEquals($expected, $rrule->current()->getTimestamp()); + $rrule->next(); + // 1st day + $expected += 280 * $day; + $this->assertEquals($expected, $rrule->current()->getTimestamp()); + // 20th day + $expected += 66 * $day; + $rrule->next(); + $this->assertEquals($expected, $rrule->current()->getTimestamp()); + // 300th day + $rrule->next(); + $expected += 19 * $day; + $this->assertEquals($expected, $rrule->current()->getTimestamp()); + $rrule->next(); + $expected += 280 * $day; + $this->assertEquals($expected, $rrule->current()->getTimestamp()); + $rrule->next(); + // 1st day (leap year, we have 366 days in this year) + $expected += 67 * $day; + $this->assertEquals($expected, $rrule->current()->getTimestamp()); + $rrule->next(); + $expected += 19 * $day; + $this->assertEquals($expected, $rrule->current()->getTimestamp()); + } + + public function testFastForwardYearlyByWeekNo() + { + $startDate = new \DateTime('1970-10-23 00:00:00', new \DateTimeZone('zulu')); + $ffDate = new \DateTime('midnight', new \DateTimeZone('zulu')); + $ffDate->setDate(99999, 1, 1); + $rrule = new RRuleIterator('FREQ=YEARLY;BYWEEKNO=1,20', $startDate); + + $this->fastForward($rrule, $ffDate); + + $day = 60 * 60 * 24; + $week = 7 * $day; + $expected = (new \DateTime()) + ->setTimezone(new \DateTimeZone('zulu')) + ->setDate(99999, 1, 4)// 1st day + ->setTime(0, 0, 0) + ->getTimestamp(); + $this->assertEquals($expected, $rrule->current()->getTimestamp()); + $rrule->next(); + $expected += $week * 19; + $this->assertEquals($expected, $rrule->current()->getTimestamp()); + } + + public function testFastForwardYearlyAdvanced() + { + $startDate = new \DateTime('1970-10-23 12:34:56', new \DateTimeZone('zulu')); + $ffDate = new \DateTime('midnight', new \DateTimeZone('zulu')); + $ffDate->setDate(9999, 1, 20)->setTime(0, 0, 13); + $rrule = new RRuleIterator('FREQ=YEARLY;INTERVAL=2;BYMONTH=1;BYDAY=SU;BYHOUR=8,9;BYMINUTE=30', $startDate); + + $this->fastForward($rrule, $ffDate); + + $expected = (new \DateTime('midnight', new \DateTimeZone('zulu'))) + ->setDate(10000, 1, 2) + ->setTime(8, 30, 56) + ->getTimestamp(); + $this->assertEquals($expected, $rrule->current()->getTimestamp()); + + $rrule->next(); + $expected += 60 * 60; + $this->assertEquals($expected, $rrule->current()->getTimestamp()); + + $rrule->next(); + $expected += 7 * 24 * 60 * 60 - 60 * 60; + $this->assertEquals($expected, $rrule->current()->getTimestamp()); + + $rrule->next(); + $expected += 60 * 60; + $this->assertEquals($expected, $rrule->current()->getTimestamp()); + + $rrule->next(); + $expected += 7 * 24 * 60 * 60 - 60 * 60; + $this->assertEquals($expected, $rrule->current()->getTimestamp()); + + $rrule->next(); + $expected += 60 * 60; + $this->assertEquals($expected, $rrule->current()->getTimestamp()); + + $rrule->next(); + $expected += 7 * 24 * 60 * 60 - 60 * 60; + $this->assertEquals($expected, $rrule->current()->getTimestamp()); + + $rrule->next(); + $expected += 60 * 60; + $this->assertEquals($expected, $rrule->current()->getTimestamp()); + + $rrule->next(); + $expected += 7 * 24 * 60 * 60 - 60 * 60; + $this->assertEquals($expected, $rrule->current()->getTimestamp()); + + $rrule->next(); + $expected += 60 * 60; + $this->assertEquals($expected, $rrule->current()->getTimestamp()); + + // jump to 6th january 10002 + $rrule->next(); + $expected = (new \DateTime('midnight', new \DateTimeZone('zulu'))) + ->setDate(10002, 1, 6) + ->setTime(8, 30, 56) + ->getTimestamp(); + $this->assertEquals($expected, $rrule->current()->getTimestamp()); + + $rrule->next(); + $expected += 60 * 60; + $this->assertEquals($expected, $rrule->current()->getTimestamp()); + } + + public function testFastForwardMonthlyBasic() + { + $startDate = new \DateTime('1970-10-23 22:42:31', new \DateTimeZone('zulu')); + $ffDate = new \DateTime('midnight', new \DateTimeZone('zulu')); + $ffDate->setDate(18000, 1, 1); + $rrule = new RRuleIterator('FREQ=MONTHLY', $startDate); + + $this->fastForward($rrule, $ffDate); + + $expected = (new \DateTime('midnight', new \DateTimeZone('zulu'))) + ->setDate(18000, 1, 23) + ->setTime(22, 42, 31) + ->getTimestamp(); + $this->assertEquals($expected, $rrule->current()->getTimestamp()); + + // february + $rrule->next(); + $expected += 31 * 24 * 60 * 60; + $this->assertEquals($expected, $rrule->current()->getTimestamp()); + // march + $rrule->next(); + $expected += 29 * 24 * 60 * 60; + $this->assertEquals($expected, $rrule->current()->getTimestamp()); + // april + $rrule->next(); + $expected += 31 * 24 * 60 * 60; + $this->assertEquals($expected, $rrule->current()->getTimestamp()); + // may + $rrule->next(); + $expected += 30 * 24 * 60 * 60; + $this->assertEquals($expected, $rrule->current()->getTimestamp()); + // june + $rrule->next(); + $expected += 31 * 24 * 60 * 60; + $this->assertEquals($expected, $rrule->current()->getTimestamp()); + // july + $rrule->next(); + $expected += 30 * 24 * 60 * 60; + $this->assertEquals($expected, $rrule->current()->getTimestamp()); + // august + $rrule->next(); + $expected += 31 * 24 * 60 * 60; + $this->assertEquals($expected, $rrule->current()->getTimestamp()); + } + + public function testFastForwardMonthly31thDay() + { + $timezone = 'America/New_York'; + $startDate = new \DateTime('1970-01-31 00:00:00', new \DateTimeZone($timezone)); + $ffDate = new \DateTime('midnight', new \DateTimeZone('zulu')); + $ffDate->setDate(18000, 1, 1); + $rrule = new RRuleIterator('FREQ=MONTHLY', $startDate); + + $this->fastForward($rrule, $ffDate); + + $expected = (new \DateTime('midnight', new \DateTimeZone('America/New_York'))) + ->setDate(18000, 1, 31) + ->getTimestamp(); + $this->assertEquals($expected, $rrule->current()->getTimestamp()); + + // march + $rrule->next(); + $expected = (new \DateTime('midnight', new \DateTimeZone('America/New_York'))) + ->setDate(18000, 3, 31) + ->getTimestamp(); + $this->assertEquals($expected, $rrule->current()->getTimestamp()); + + // may + $rrule->next(); + $expected += (30 + 31) * 24 * 60 * 60; + $expected = (new \DateTime('midnight', new \DateTimeZone('America/New_York'))) + ->setDate(18000, 5, 31) + ->getTimestamp(); + $this->assertEquals($expected, $rrule->current()->getTimestamp()); + + // july + $rrule->next(); + $expected = (new \DateTime('midnight', new \DateTimeZone('America/New_York'))) + ->setDate(18000, 7, 31) + ->getTimestamp(); + $this->assertEquals($expected, $rrule->current()->getTimestamp()); + + // august + $rrule->next(); + $expected = (new \DateTime('midnight', new \DateTimeZone('America/New_York'))) + ->setDate(18000, 8, 31) + ->getTimestamp(); + $this->assertEquals($expected, $rrule->current()->getTimestamp()); + + // october + $rrule->next(); + $expected = (new \DateTime('midnight', new \DateTimeZone('America/New_York'))) + ->setDate(18000, 10, 31) + ->getTimestamp(); + $this->assertEquals($expected, $rrule->current()->getTimestamp()); + + // december + $rrule->next(); + $expected = (new \DateTime('midnight', new \DateTimeZone('America/New_York'))) + ->setDate(18000, 12, 31) + ->getTimestamp(); + $this->assertEquals($expected, $rrule->current()->getTimestamp()); + } + + public function testFastForwardMonthlyAdvanced() + { + $timezone = 'America/New_York'; + $startDate = new \DateTime('1970-01-31 00:00:00', new \DateTimeZone($timezone)); + $ffDate = new \DateTime('midnight', new \DateTimeZone('zulu')); + $ffDate->setDate(8000, 1, 1); + $rrule = new RRuleIterator('FREQ=MONTHLY;INTERVAL=2;BYDAY=1MO,2TU,3WE,4TH', $startDate); + + $this->fastForward($rrule, $ffDate); + + // monday + $expected = (new \DateTime('midnight', new \DateTimeZone($timezone))) + ->setDate(8000, 1, 3) + ->getTimestamp(); + $this->assertEquals($expected, $rrule->current()->getTimestamp()); + + // tuesday + $expected = (new \DateTime('midnight', new \DateTimeZone($timezone))) + ->setDate(8000, 1, 11) + ->getTimestamp(); + $rrule->next(); + $this->assertEquals($expected, $rrule->current()->getTimestamp()); + + // wednesday + $expected = (new \DateTime('midnight', new \DateTimeZone($timezone))) + ->setDate(8000, 1, 19) + ->getTimestamp(); + $rrule->next(); + $this->assertEquals($expected, $rrule->current()->getTimestamp()); + + // thursday + $expected = (new \DateTime('midnight', new \DateTimeZone($timezone))) + ->setDate(8000, 1, 27) + ->getTimestamp(); + $rrule->next(); + $this->assertEquals($expected, $rrule->current()->getTimestamp()); + + // monday march + $expected = (new \DateTime('midnight', new \DateTimeZone($timezone))) + ->setDate(8000, 3, 6) + ->getTimestamp(); + $rrule->next(); + $this->assertEquals($expected, $rrule->current()->getTimestamp()); + + // tuesday + $expected = (new \DateTime('midnight', new \DateTimeZone($timezone))) + ->setDate(8000, 3, 14) + ->getTimestamp(); + $rrule->next(); + $this->assertEquals($expected, $rrule->current()->getTimestamp()); + + // wednesday (this month starts on wednesday so that's just the next day) + $expected = (new \DateTime('midnight', new \DateTimeZone($timezone))) + ->setDate(8000, 3, 15) + ->getTimestamp(); + $rrule->next(); + $this->assertEquals($expected, $rrule->current()->getTimestamp()); + + // thursday + $expected = (new \DateTime('midnight', new \DateTimeZone($timezone))) + ->setDate(8000, 3, 23) + ->getTimestamp(); + $rrule->next(); + $this->assertEquals($expected, $rrule->current()->getTimestamp()); + } + + public function testFastForwardDailyBasic() + { + $timezone = 'America/New_York'; + $startDate = new \DateTime('1970-10-23 00:00:00', new \DateTimeZone($timezone)); + $ffDate = new \DateTime('midnight', new \DateTimeZone('zulu')); + $ffDate->setDate(4000, 1, 1); + $rrule = new RRuleIterator('FREQ=DAILY', $startDate); + + $this->fastForward($rrule, $ffDate); + + $expected = (new \DateTime('midnight', new \DateTimeZone($timezone))) + ->setDate(4000, 1, 1) + ->getTimestamp(); + $this->assertEquals($expected, $rrule->current()->getTimestamp()); + + $expected += 24 * 60 * 60; + $rrule->next(); + $this->assertEquals($expected, $rrule->current()->getTimestamp()); + + $expected += 24 * 60 * 60; + $rrule->next(); + $this->assertEquals($expected, $rrule->current()->getTimestamp()); + + $expected += 24 * 60 * 60; + $rrule->next(); + $this->assertEquals($expected, $rrule->current()->getTimestamp()); + + $expected += 24 * 60 * 60; + $rrule->next(); + $this->assertEquals($expected, $rrule->current()->getTimestamp()); + + $expected += 24 * 60 * 60; + $rrule->next(); + $this->assertEquals($expected, $rrule->current()->getTimestamp()); + + $expected += 24 * 60 * 60; + $rrule->next(); + $this->assertEquals($expected, $rrule->current()->getTimestamp()); + } + + public function testFastForwardDailyAdvanced() + { + $timezone = 'America/New_York'; + $startDate = new \DateTime('1970-10-23 00:00:00', new \DateTimeZone($timezone)); + $ffDate = new \DateTime('midnight', new \DateTimeZone('zulu')); + $ffDate->setDate(4000, 1, 1); + $rrule = new RRuleIterator('FREQ=DAILY;BYHOUR=16,17,18;INTERVAL=10', $startDate); + + $this->fastForward($rrule, $ffDate); + + $expected = (new \DateTime('midnight', new \DateTimeZone($timezone))) + ->setDate(4000, 1, 4) + ->setTime(16, 0, 0) + ->getTimestamp(); + $this->assertEquals($expected, $rrule->current()->getTimestamp()); + + // 17:00 + $expected += 60 * 60; + $rrule->next(); + $this->assertEquals($expected, $rrule->current()->getTimestamp()); + + // 18:00 + $expected += 60 * 60; + $rrule->next(); + $this->assertEquals($expected, $rrule->current()->getTimestamp()); + + // 16:00 + $expected += 10 * 24 * 60 * 60 - 2 * 60 * 60; + $rrule->next(); + $this->assertEquals($expected, $rrule->current()->getTimestamp()); + + // 17:00 + $expected += 60 * 60; + $rrule->next(); + $this->assertEquals($expected, $rrule->current()->getTimestamp()); + + // 18:00 + $expected += 60 * 60; + $rrule->next(); + $this->assertEquals($expected, $rrule->current()->getTimestamp()); + + // 16:00 + $expected += 10 * 24 * 60 * 60 - 2 * 60 * 60; + $rrule->next(); + $this->assertEquals($expected, $rrule->current()->getTimestamp()); + } +} diff --git a/tests/VObject/Recur/FastForwardToEndTest.php b/tests/VObject/Recur/FastForwardToEndTest.php new file mode 100644 index 00000000..0b94f394 --- /dev/null +++ b/tests/VObject/Recur/FastForwardToEndTest.php @@ -0,0 +1,356 @@ +fastForwardToEnd(); + $ru = getrusage(); + $endTime = $ru['ru_utime.tv_sec'] * 1000000 + $ru['ru_utime.tv_usec']; + $enfoceTiming && $this->assertLessThan(self::FF_TIMEOUT, $endTime - $startTime); + $this->assertTrue($ruleIterator->valid()); + $this->assertNotNull($ruleIterator->current()); + } + + public function testFastForwardToEndWithoutEndYearlyBasic(): void + { + $startDate = new \DateTime('1970-10-23 00:00:00', new \DateTimeZone('zulu')); + $rrule = new RRuleIterator('FREQ=YEARLY', $startDate); + + $this->expectException(\LogicException::class); + $rrule->fastForwardToEnd(); + } + + public function testFastForwardToEndCountYearlyBasic(): void + { + $startDate = new \DateTime('1970-10-23 00:00:00', new \DateTimeZone('zulu')); + $rrule = new RRuleIterator('FREQ=YEARLY;COUNT=7777', $startDate); + + // We do not enforce the timing in case of a count rule as we cannot optimize it + $this->fastForwardToEnd($rrule, false); + + $expected = (new \DateTime()) + ->setTimezone(new \DateTimeZone('zulu')) + ->setDate(9746, 10, 23) + ->setTime(0, 0, 0) + ->getTimestamp(); + $this->assertEquals($expected, $rrule->current()->getTimestamp()); + } + + public function testFastForwardToEndUntilYearlyBasic(): void + { + $startDate = new \DateTime('1970-10-23 00:00:00', new \DateTimeZone('zulu')); + $rrule = new RRuleIterator('FREQ=YEARLY;UNTIL=97461212T000000', $startDate); + + $this->fastForwardToEnd($rrule); + + $expected = (new \DateTime()) + ->setTimezone(new \DateTimeZone('zulu')) + ->setDate(9746, 10, 23) + ->setTime(0, 0, 0) + ->getTimestamp(); + $this->assertEquals($expected, $rrule->current()->getTimestamp()); + } + + public function testFastForwardToEndCountYearlyByYearDay(): void + { + $startDate = new \DateTime('1970-10-23 00:00:00', new \DateTimeZone('zulu')); + $rrule = new RRuleIterator('FREQ=YEARLY;BYYEARDAY=1,20,300;COUNT=10000', $startDate); + + // We do not enforce the timing in case of a count rule as we cannot optimize it + $this->fastForwardToEnd($rrule, false); + + $expected = (new \DateTime()) + ->setTimezone(new \DateTimeZone('zulu')) + ->setDate(5303, 1, 20) + ->setTime(0, 0, 0) + ->getTimestamp(); + $this->assertEquals($expected, $rrule->current()->getTimestamp()); + } + + public function testFastForwardToEndUntilYearlyByYearDay(): void + { + $startDate = new \DateTime('1970-10-23 00:00:00', new \DateTimeZone('zulu')); + $rrule = new RRuleIterator('FREQ=YEARLY;BYYEARDAY=1,20,300;UNTIL=53030808T000000', $startDate); + + $this->fastForwardToEnd($rrule); + + $expected = (new \DateTime()) + ->setTimezone(new \DateTimeZone('zulu')) + ->setDate(5303, 1, 20) + ->setTime(0, 0, 0) + ->getTimestamp(); + $this->assertEquals($expected, $rrule->current()->getTimestamp()); + } + + /* + * Issue CALENDAR-587 + public function testFastForwardToEndCountYearlyByWeekNo() + { + $startDate = new \DateTime('1970-10-23 00:00:00', new \DateTimeZone('zulu')); + $rrule = new RRuleIterator('FREQ=YEARLY;BYWEEKNO=1,20;COUNT=100', $startDate); + + // We do not enforce the timing in case of a count rule as we cannot optimize it + $this->fastForwardToEnd($rrule, false); + + $expected = (new DateTime()) + ->setTimezone(new DateTimeZone('zulu')) + ->setDate(2019, 12, 30) + ->setTime(0, 0, 0) + ->getTimestamp(); + $this->assertEquals($expected, $rrule->current()->getTimestamp()); + } + + public function testFastForwardToEndUntilYearlyByWeekNo() + { + $startDate = new \DateTime('1970-10-23 00:00:00', new \DateTimeZone('zulu')); + $rrule = new RRuleIterator('FREQ=YEARLY;BYWEEKNO=1,20;UNTIL=20030808T000000', $startDate); + + $this->fastForwardToEnd($rrule); + + $expected = (new DateTime()) + ->setTimezone(new DateTimeZone('zulu')) + ->setDate(2019, 12, 30) + ->setTime(0, 0, 0) + ->getTimestamp(); + $this->assertEquals($expected, $rrule->current()->getTimestamp()); + } + */ + + public function testFastForwardToEndCountYearlyAdvanced() + { + $startDate = new \DateTime('1970-10-23 12:34:56', new \DateTimeZone('zulu')); + $rrule = new RRuleIterator('FREQ=YEARLY;INTERVAL=2;BYMONTH=1;BYDAY=SU;BYHOUR=8,9;BYMINUTE=30;COUNT=10000', $startDate); + + // We do not enforce the timing in case of a count rule as we cannot optimize it + $this->fastForwardToEnd($rrule, false); + + $expected = (new \DateTime('midnight', new \DateTimeZone('zulu'))) + ->setDate(4226, 1, 1) + ->setTime(8, 30, 56) + ->getTimestamp(); + $this->assertEquals($expected, $rrule->current()->getTimestamp()); + } + + public function testFastForwardToEndUntilYearlyAdvanced() + { + $startDate = new \DateTime('1970-10-23 12:34:56', new \DateTimeZone('zulu')); + $rrule = new RRuleIterator('FREQ=YEARLY;INTERVAL=2;BYMONTH=1;BYDAY=SU;BYHOUR=8,9;BYMINUTE=30;UNTIL=42180125T092500', $startDate); + + $this->fastForwardToEnd($rrule); + + $expected = (new \DateTime('midnight', new \DateTimeZone('zulu'))) + ->setDate(4218, 1, 25) + ->setTime(8, 30, 56) + ->getTimestamp(); + $this->assertEquals($expected, $rrule->current()->getTimestamp()); + } + + public function testFastForwardToEndCountMonthlyBasic() + { + $startDate = new \DateTime('1970-10-23 22:42:31', new \DateTimeZone('zulu')); + $rrule = new RRuleIterator('FREQ=MONTHLY;COUNT=10000', $startDate); + + // We do not enforce the timing in case of a count rule as we cannot optimize it + $this->fastForwardToEnd($rrule, false); + + $expected = (new \DateTime('midnight', new \DateTimeZone('zulu'))) + ->setDate(2804, 1, 23) + ->setTime(22, 42, 31) + ->getTimestamp(); + $this->assertEquals($expected, $rrule->current()->getTimestamp()); + } + + public function testFastForwardToEndUntilMonthlyBasic() + { + $startDate = new \DateTime('1970-10-23 22:42:31', new \DateTimeZone('zulu')); + $rrule = new RRuleIterator('FREQ=MONTHLY;UNTIL=28040122T092500', $startDate); + + $this->fastForwardToEnd($rrule); + + $expected = (new \DateTime('midnight', new \DateTimeZone('zulu'))) + ->setDate(2803, 12, 23) + ->setTime(22, 42, 31) + ->getTimestamp(); + $this->assertEquals($expected, $rrule->current()->getTimestamp()); + } + + /** + * @requires PHP < 8.1 + */ + public function testFastForwardToEndCountMonthly31thDay() + { + $startDate = new \DateTime('1970-01-31 00:00:00', new \DateTimeZone('America/New_York')); + $rrule = new RRuleIterator('FREQ=MONTHLY;COUNT=10000', $startDate); + + // We do not enforce the timing in case of a count rule as we cannot optimize it + $this->fastForwardToEnd($rrule, false); + + $expected = (new \DateTime('midnight', new \DateTimeZone('America/New_York'))) + ->setDate(3398, 10, 31) + ->getTimestamp(); + $this->assertEquals($expected, $rrule->current()->getTimestamp()); + } + + /** + * @requires PHP >= 8.1 + */ + public function testFastForwardToEndCountMonthly31thDayPHP81() + { + $startDate = new \DateTime('1970-01-31 00:00:00', new \DateTimeZone('America/New_York')); + $rrule = new RRuleIterator('FREQ=MONTHLY;COUNT=10000', $startDate); + + // We do not enforce the timing in case of a count rule as we cannot optimize it + $this->fastForwardToEnd($rrule, false); + + $expected = (new \DateTime('midnight', new \DateTimeZone('America/New_York'))) + ->setDate(3398, 7, 31); + $this->assertEquals($expected->getTimestamp(), $rrule->current()->getTimestamp()); + } + + public function testFastForwardToEndUntilMonthly31thDay() + { + $startDate = new \DateTime('1970-01-31 00:00:00', new \DateTimeZone('America/New_York')); + $rrule = new RRuleIterator('FREQ=MONTHLY;UNTIL=33980909T092500', $startDate); + + $this->fastForwardToEnd($rrule); + + $expected = (new \DateTime('midnight', new \DateTimeZone('America/New_York'))) + ->setDate(3398, 8, 31) + ->getTimestamp(); + $this->assertEquals($expected, $rrule->current()->getTimestamp()); + } + + /** + * @medium + */ + public function testFastForwardToEndCountMonthlyAdvanced() + { + $startDate = new \DateTime('1970-01-31 00:00:00', new \DateTimeZone('America/New_York')); + // every 2 months on the 1st Monday, 2nd Tuesday, 3rd Wednesday and 4th Thursday + $rrule = new RRuleIterator('FREQ=MONTHLY;INTERVAL=2;BYDAY=1MO,2TU,3WE,4TH;COUNT=10000', $startDate); + + // We do not enforce the timing in case of a count rule as we cannot optimize it + $this->fastForwardToEnd($rrule, false); + + $expected = (new \DateTime('midnight', new \DateTimeZone('America/New_York'))) + ->setDate(2386, 9, 17) + ->getTimestamp(); + $this->assertEquals($expected, $rrule->current()->getTimestamp()); + } + + public function testFastForwardToEndUntilMonthlyAdvanced() + { + $startDate = new \DateTime('1970-01-31 00:00:00', new \DateTimeZone('America/New_York')); + // every 2 months on the 1st Monday, 2nd Tuesday, 3rd Wednesday and 4th Thursday + $rrule = new RRuleIterator('FREQ=MONTHLY;INTERVAL=2;BYDAY=1MO,2TU,3WE,4TH;UNTIL=23860914T092500', $startDate); + + $this->fastForwardToEnd($rrule); + + $expected = (new \DateTime('midnight', new \DateTimeZone('America/New_York'))) + ->setDate(2386, 9, 9) + ->getTimestamp(); + $this->assertEquals($expected, $rrule->current()->getTimestamp()); + } + + public function testFastForwardToEndCountDailyBasic() + { + $timezone = 'America/New_York'; + $startDate = new \DateTime('1970-10-23 00:00:00', new \DateTimeZone($timezone)); + $rrule = new RRuleIterator('FREQ=DAILY;COUNT=100000', $startDate); + + // We do not enforce the timing in case of a count rule as we cannot optimize it + $this->fastForwardToEnd($rrule, false); + + $expected = (new \DateTime('midnight', new \DateTimeZone($timezone))) + ->setDate(2244, 8, 6) + ->getTimestamp(); + $this->assertEquals($expected, $rrule->current()->getTimestamp()); + } + + public function testFastForwardToEndUntilDailyBasic() + { + $timezone = 'America/New_York'; + $startDate = new \DateTime('1970-10-23 00:00:00', new \DateTimeZone($timezone)); + $rrule = new RRuleIterator('FREQ=DAILY;UNTIL=22440806T092500', $startDate); + + $this->fastForwardToEnd($rrule); + + $expected = (new \DateTime('midnight', new \DateTimeZone($timezone))) + ->setDate(2244, 8, 6) + ->getTimestamp(); + $this->assertEquals($expected, $rrule->current()->getTimestamp()); + } + + public function testFastForwardToEndCountDailyAdvanced() + { + $timezone = 'America/New_York'; + $startDate = new \DateTime('1970-10-23 00:00:00', new \DateTimeZone($timezone)); + // every 10 days at 16, 17 and 18 + $rrule = new RRuleIterator('FREQ=DAILY;BYHOUR=16,17,18;INTERVAL=10;COUNT=10000', $startDate); + + // We do not enforce the timing in case of a count rule as we cannot optimize it + $this->fastForwardToEnd($rrule, false); + + $expected = (new \DateTime('midnight', new \DateTimeZone($timezone))) + ->setDate(2062, 1, 13) + ->setTime(18, 0, 0) + ->getTimestamp(); + $this->assertEquals($expected, $rrule->current()->getTimestamp()); + } + + public function testFastForwardToEndUntilDailyAdvanced() + { + $timezone = 'America/New_York'; + $startDate = new \DateTime('1970-10-23 00:00:00', new \DateTimeZone($timezone)); + // every 10 days at 16, 17 and 18 + $rrule = new RRuleIterator('FREQ=DAILY;BYHOUR=16,17,18;INTERVAL=10;UNTIL=20620113T183456', $startDate); + + $this->fastForwardToEnd($rrule); + + $expected = (new \DateTime('midnight', new \DateTimeZone($timezone))) + ->setDate(2062, 1, 13) + ->setTime(18, 0, 0) + ->getTimestamp(); + $this->assertEquals($expected, $rrule->current()->getTimestamp()); + } + + public function testFastForwardToEndCountHourlyBasic() + { + $timezone = 'America/New_York'; + $startDate = new \DateTime('1970-10-23 00:12:34', new \DateTimeZone($timezone)); + $rrule = new RRuleIterator('FREQ=HOURLY;COUNT=100000', $startDate); + + // We do not enforce the timing in case of a count rule as we cannot optimize it + $this->fastForwardToEnd($rrule, false); + + $expected = (new \DateTime('midnight', new \DateTimeZone($timezone))) + ->setDate(1982, 3, 21) + ->setTime(2, 12, 34) + ->getTimestamp(); + $this->assertEquals($expected, $rrule->current()->getTimestamp()); + } + + public function testFastForwardToEndUntilHourlyBasic() + { + $timezone = 'America/New_York'; + $startDate = new \DateTime('1970-10-23 00:12:34', new \DateTimeZone($timezone)); + $rrule = new RRuleIterator('FREQ=HOURLY;UNTIL=19820321T024032', $startDate); + + $this->fastForwardToEnd($rrule); + + $expected = (new \DateTime('midnight', new \DateTimeZone($timezone))) + ->setDate(1982, 3, 21) + ->setTime(2, 12, 34) + ->getTimestamp(); + $this->assertEquals($expected, $rrule->current()->getTimestamp()); + } +} diff --git a/tests/VObject/Recur/RRuleIteratorTest.php b/tests/VObject/Recur/RRuleIteratorTest.php index 2814df66..75222f21 100644 --- a/tests/VObject/Recur/RRuleIteratorTest.php +++ b/tests/VObject/Recur/RRuleIteratorTest.php @@ -25,7 +25,8 @@ public function testHourly(): void '2011-10-08 15:00:00', '2011-10-08 18:00:00', '2011-10-08 21:00:00', - ] + ], + 'hourly', 12, 3, null ); } @@ -42,7 +43,8 @@ public function testDaily(): void '2011-10-19 00:00:00', '2011-10-22 00:00:00', '2011-10-25 00:00:00', - ] + ], + 'daily', null, 3, new \DateTime('2011-10-25') ); } @@ -64,7 +66,8 @@ public function testDailyByDayByHour(): void '2011-10-22 07:00:00', '2011-10-23 06:00:00', '2011-10-23 07:00:00', - ] + ], + 'daily', null, 1, null ); } @@ -86,7 +89,8 @@ public function testDailyByHour(): void '2012-10-13 15:00:00', '2012-10-15 10:00:00', '2012-10-15 11:00:00', - ] + ], + 'daily', null, 2, null ); } @@ -108,7 +112,8 @@ public function testDailyByDay(): void '2011-11-18 12:00:00', '2011-11-22 12:00:00', '2011-11-30 12:00:00', - ] + ], + 'daily', null, 2, null ); } @@ -123,7 +128,8 @@ public function testDailyCount(): void '2014-08-03 18:03:00', '2014-08-04 18:03:00', '2014-08-05 18:03:00', - ] + ], + 'daily', 5, 1, null ); } @@ -140,6 +146,7 @@ public function testDailyByMonth(): void '2013-10-27 16:00:00', '2014-09-07 16:00:00', ], + 'daily', null, 1, null, '2013-09-28' ); } @@ -156,9 +163,8 @@ public function testDailyBySetPosLoop(): void $this->parse( 'FREQ=DAILY;INTERVAL=7;BYDAY=MO', '2022-03-15', - [ - ], - '2022-05-01' + [], + 'daily', null, 7, null, '2022-05-01' ); } @@ -178,7 +184,8 @@ public function testWeekly(): void '2012-01-13 00:00:00', '2012-01-27 00:00:00', '2012-02-10 00:00:00', - ] + ], + 'weekly', 10, 2, null ); } @@ -192,7 +199,8 @@ public function testWeeklyByDay(): void '2014-08-04 00:00:00', '2014-08-11 00:00:00', '2014-08-18 00:00:00', - ] + ], + 'weekly', 4, 1, null ); } @@ -214,7 +222,8 @@ public function testWeeklyByDay2(): void '2011-11-18 00:00:00', '2011-11-29 00:00:00', '2011-11-30 00:00:00', - ] + ], + 'weekly', null, 2, null ); } @@ -239,7 +248,8 @@ public function testWeeklyByDayByHour(): void '2011-11-01 08:00:00', '2011-11-01 09:00:00', '2011-11-01 10:00:00', - ] + ], + 'weekly', null, 2, null ); } @@ -261,7 +271,8 @@ public function testWeeklyByDaySpecificHour(): void '2011-11-18 18:00:00', '2011-11-29 18:00:00', '2011-11-30 18:00:00', - ] + ], + 'weekly', null, 2, null ); } @@ -276,7 +287,8 @@ public function testMonthly(): void '2012-06-05 00:00:00', '2012-09-05 00:00:00', '2012-12-05 00:00:00', - ] + ], + 'monthly', 5, 3, null ); } @@ -298,7 +310,8 @@ public function testMonthlyEndOfMonth(): void '2014-12-31 00:00:00', '2015-08-31 00:00:00', '2015-10-31 00:00:00', - ] + ], + 'monthly', 12, 2, null ); } @@ -317,10 +330,45 @@ public function testMonthlyByMonthDay(): void '2011-11-24 00:00:00', '2012-04-01 00:00:00', '2012-04-24 00:00:00', - ] + ], + 'monthly', 9, 5, null ); } + public function testInvalidByMonthDay(): void + { + $this->expectException(InvalidDataException::class); + $this->parse( + 'FREQ=MONTHLY;COUNT=6;BYMONTHDAY=1,5,10,42', + '2011-04-07 00:00:00', + [] + ); + } + + /** @dataProvider invalidFreqByCombinationProviders */ + public function testInvalidFreqByCombination(string $rule): void + { + $this->expectException(InvalidDataException::class); + $this->parse( + $rule, + '2011-01-01 00:00:00', + [] + ); + } + + public function invalidFreqByCombinationProviders(): iterable + { + return [ + ['FREQ=DAILY;BYWEEKNO=13,15,50'], + ['FREQ=WEEKLY;BYWEEKNO=13,15,50'], + ['FREQ=MONTHLY;BYWEEKNO=13,15,50'], + ['FREQ=DAILY;BYYEARDAY=1'], + ['FREQ=WEEKLY;BYYEARDAY=1'], + ['FREQ=MONTHLY;BYYEARDAY=1'], + ['FREQ=WEEKLY;BYMONTHDAY=1'], + ]; + } + public function testMonthlyByDay(): void { $this->parse( @@ -343,7 +391,8 @@ public function testMonthlyByDay(): void '2011-03-22 00:00:00', '2011-03-28 00:00:00', '2011-05-02 00:00:00', - ] + ], + 'monthly', 16, 2, null ); } @@ -359,7 +408,8 @@ public function testMonthlyByDayUntil(): void '2021-03-03 00:00:00', '2021-03-10 00:00:00', '2021-03-17 00:00:00', - ] + ], + 'monthly', null, 1, new \DateTime('2021-03-17') ); } @@ -370,7 +420,8 @@ public function testMonthlyByDayUntilWithImpossibleNextOccurrence(): void '2021-02-10 00:00:00', [ '2021-02-10 00:00:00', - ] + ], + 'monthly', null, 1, new \DateTime('2021-03-17') ); } @@ -390,7 +441,8 @@ public function testMonthlyByDayByMonthDay(): void '2016-02-01 00:00:00', '2016-08-01 00:00:00', '2017-05-01 00:00:00', - ] + ], + 'monthly', 10, 1, null ); } @@ -410,7 +462,8 @@ public function testMonthlyByDayBySetPos(): void '2011-04-29 00:00:00', '2011-05-02 00:00:00', '2011-05-31 00:00:00', - ] + ], + 'monthly', 10, 1, null ); } @@ -430,7 +483,8 @@ public function testYearly(): void '2032-01-01 00:00:00', '2035-01-01 00:00:00', '2038-01-01 00:00:00', - ] + ], + 'yearly', 10, 3, null ); } @@ -443,7 +497,8 @@ public function testYearlyLeapYear(): void '2012-02-29 00:00:00', '2016-02-29 00:00:00', '2020-02-29 00:00:00', - ] + ], + 'yearly', 3, 1, null ); } @@ -461,7 +516,8 @@ public function testYearlyByMonth(): void '2019-10-07 00:00:00', '2023-04-07 00:00:00', '2023-10-07 00:00:00', - ] + ], + 'yearly', 8, 4, null ); } @@ -519,7 +575,26 @@ public function testYearlyByMonthByDay(): void '2016-04-24 00:00:00', '2016-10-03 00:00:00', '2016-10-30 00:00:00', - ] + ], + 'yearly', 8, 5, null + ); + } + + public function testYearlyNewYearsEve() + { + $this->parse( + 'FREQ=YEARLY;COUNT=7;INTERVAL=2;BYYEARDAY=1', + '2011-01-01 03:07:00', + [ + '2011-01-01 03:07:00', + '2013-01-01 03:07:00', + '2015-01-01 03:07:00', + '2017-01-01 03:07:00', + '2019-01-01 03:07:00', + '2021-01-01 03:07:00', + '2023-01-01 03:07:00', + ], + 'yearly', 7, 2, null ); } @@ -536,7 +611,8 @@ public function testYearlyNewYearsDay(): void '2019-01-01 03:07:00', '2021-01-01 03:07:00', '2023-01-01 03:07:00', - ] + ], + 'yearly', 7, 2 ); } @@ -553,7 +629,8 @@ public function testYearlyByYearDay(): void '2019-07-09 03:07:00', '2021-07-09 03:07:00', '2023-07-09 03:07:00', - ] + ], + 'yearly', 7, 2, null ); } @@ -590,7 +667,8 @@ public function testYearlyByYearDayMultiple(): void '2017-10-28 14:53:11', '2020-07-08 14:53:11', '2020-10-27 14:53:11', - ] + ], + 'yearly', 8, 3, null ); } @@ -606,7 +684,8 @@ public function testYearlyByYearDayByDay(): void '2024-04-06 14:53:11', '2029-04-07 14:53:11', '2035-04-07 14:53:11', - ] + ], + 'yearly', 6, 1, null ); } @@ -624,7 +703,52 @@ public function testYearlyByYearDayNegative(): void '2003-12-27 14:53:11', '2004-09-26 14:53:11', '2004-12-27 14:53:11', - ] + ], + 'yearly', 8, 1, null + ); + } + + public function testFirstLastSundayEveryOtherYearAt1530and1730InJanuary() + { + $this->parse('FREQ=YEARLY;INTERVAL=2;BYMONTH=1;BYDAY=1SU,-1SU;BYHOUR=15,17;BYMINUTE=30,35;BYSECOND=15,56', + '1999-12-01 12:34:56', + [ + '1999-12-01 12:34:56', + '2001-01-07 15:30:15', '2001-01-07 15:30:56', '2001-01-07 15:35:15', '2001-01-07 15:35:56', + '2001-01-07 17:30:15', '2001-01-07 17:30:56', '2001-01-07 17:35:15', '2001-01-07 17:35:56', + + '2001-01-28 15:30:15', '2001-01-28 15:30:56', '2001-01-28 15:35:15', '2001-01-28 15:35:56', + '2001-01-28 17:30:15', '2001-01-28 17:30:56', '2001-01-28 17:35:15', '2001-01-28 17:35:56', + + '2003-01-05 15:30:15', '2003-01-05 15:30:56', '2003-01-05 15:35:15', '2003-01-05 15:35:56', + '2003-01-05 17:30:15', '2003-01-05 17:30:56', '2003-01-05 17:35:15', '2003-01-05 17:35:56', + + '2003-01-26 15:30:15', '2003-01-26 15:30:56', '2003-01-26 15:35:15', '2003-01-26 15:35:56', + '2003-01-26 17:30:15', '2003-01-26 17:30:56', '2003-01-26 17:35:15', '2003-01-26 17:35:56', + ], + 'yearly', null, 2, null + ); + } + + public function testFirstFourthSundayEveryOtherMonthAt830and930() + { + $this->parse('FREQ=MONTHLY;INTERVAL=2;BYDAY=1SU,4SU;BYHOUR=15,17;BYMINUTE=30,32;BYSECOND=11,12', + '2001-01-01 12:34:56', + [ + '2001-01-01 12:34:56', + '2001-01-07 15:30:11', '2001-01-07 15:30:12', '2001-01-07 15:32:11', '2001-01-07 15:32:12', + '2001-01-07 17:30:11', '2001-01-07 17:30:12', '2001-01-07 17:32:11', '2001-01-07 17:32:12', + + '2001-01-28 15:30:11', '2001-01-28 15:30:12', '2001-01-28 15:32:11', '2001-01-28 15:32:12', + '2001-01-28 17:30:11', '2001-01-28 17:30:12', '2001-01-28 17:32:11', '2001-01-28 17:32:12', + + '2001-03-04 15:30:11', '2001-03-04 15:30:12', '2001-03-04 15:32:11', '2001-03-04 15:32:12', + '2001-03-04 17:30:11', '2001-03-04 17:30:12', '2001-03-04 17:32:11', '2001-03-04 17:32:12', + + '2001-03-25 15:30:11', '2001-03-25 15:30:12', '2001-03-25 15:32:11', '2001-03-25 15:32:12', + '2001-03-25 17:30:11', '2001-03-25 17:30:12', '2001-03-25 17:32:11', '2001-03-25 17:32:12', + ], + 'monthly', null, 2, null ); } @@ -646,7 +770,8 @@ public function testYearlyByYearDayLargeNegative(): void '2006-01-01 14:53:11', '2007-01-01 14:53:11', '2008-01-02 14:53:11', - ] + ], + 'yearly', 8, 1 ); } @@ -671,7 +796,8 @@ public function testYearlyByYearDayMaxNegative(): void '2005-12-31 14:53:11', '2006-12-31 14:53:11', '2008-01-01 14:53:11', - ] + ], + 'yearly', 8, 1 ); } @@ -706,7 +832,8 @@ public function testYearlyByDayByWeekNo(): void '2021-01-01 00:00:00', '2021-03-29 00:00:00', '2021-04-12 00:00:00', - ] + ], + 'yearly', 3, 1 ); } @@ -718,6 +845,7 @@ public function testFastForward(): void 'FREQ=YEARLY;COUNT=8;INTERVAL=5;BYMONTH=4,10;BYDAY=1MO,-1SU', '2011-04-04 00:00:00', [], + 'yearly', 8, 5, null, '2020-05-05 00:00:00' ); } @@ -739,7 +867,8 @@ public function testFifthTuesdayProblem(): void '2007-10-04 14:46:42', [ '2007-10-04 14:46:42', - ] + ], + 'monthly', null, 1, new \DateTime('2007-10-30 03:59:59') ); } @@ -764,7 +893,8 @@ public function testFastForwardTooFar(): void '2009-06-15 18:00:00', '2009-06-22 18:00:00', '2009-06-29 18:00:00', - ] + ], + 'weekly', null, 1, new \DateTime('2009-07-04 20:59:59') ); } @@ -786,7 +916,8 @@ public function testValidByWeekNo(): void '2019-05-14 00:00:00', '2020-05-12 00:00:00', '2021-05-18 00:00:00', - ] + ], + 'yearly', null, 1, null ); } @@ -808,7 +939,8 @@ public function testNegativeValidByWeekNo(): void '2016-08-09 00:00:00', '2016-08-12 00:00:00', '2017-08-08 00:00:00', - ] + ], + 'yearly', null, 1, null ); } @@ -830,7 +962,8 @@ public function testTwoValidByWeekNo(): void '2016-05-17 09:00:00', '2016-05-20 09:00:00', '2017-05-16 09:00:00', - ] + ], + 'yearly', null, 1, null ); } @@ -852,7 +985,8 @@ public function testValidByWeekNoByDayDefault(): void '2020-05-11 00:00:00', '2021-05-17 00:00:00', '2022-05-16 00:00:00', - ] + ], + 'yearly', null, 1, null ); } @@ -874,7 +1008,8 @@ public function testMultipleValidByWeekNo(): void '2013-05-14 00:00:00', '2013-05-17 00:00:00', '2013-12-10 00:00:00', - ] + ], + 'yearly', null, 1, null ); } @@ -884,8 +1019,7 @@ public function testInvalidByWeekNo(): void $this->parse( 'FREQ=YEARLY;BYWEEKNO=54', '2011-05-16 00:00:00', - [ - ] + [], ); } @@ -900,6 +1034,7 @@ public function testYearlyByMonthLoop(): void [ '2012-02-01 15:45:00', ], + 'yearly', null, 1, new \DateTime('2012-02-03 22:59:59'), '2012-01-29 23:00:00' ); } @@ -916,9 +1051,15 @@ public function testYearlyBySetPosLoop(): void $this->parse( 'FREQ=YEARLY;BYMONTH=5;BYSETPOS=3;BYMONTHDAY=3', '2022-03-03 15:45:00', - [ - ], - '2022-05-01' + [], + 'yearly', + null, + 1, + null, + '2022-05-01', + 'UTC', + false, + false, ); } @@ -934,6 +1075,7 @@ public function testZeroInterval(): void 'FREQ=YEARLY;INTERVAL=0', '2012-08-24 14:57:00', [], + 'yearly', null, 0, null, '2013-01-01 23:00:00' ); } @@ -948,6 +1090,16 @@ public function testInvalidFreq(): void ); } + public function testInvalidMissingFreq(): void + { + $this->expectException(InvalidDataException::class); + $this->parse( + 'COUNT=6;BYMONTHDAY=24;BYMONTH=1', + '2011-04-07 00:00:00', + [] + ); + } + public function testByDayBadOffset(): void { $this->expectException(InvalidDataException::class); @@ -974,6 +1126,7 @@ public function testUntilBeginHasTimezone(): void '2013-11-11 18:30:00', '2013-11-18 18:30:00', ], + 'weekly', null, 1, new \DateTime('2013-11-18 18:30:00-0500'), null, 'America/New_York' ); @@ -981,24 +1134,39 @@ public function testUntilBeginHasTimezone(): void public function testUntilBeforeDtStart(): void { + $dtstart = '2014-08-02 00:15:00'; $this->parse( 'FREQ=DAILY;UNTIL=20140101T000000Z', - '2014-08-02 00:15:00', + $dtstart, [ - '2014-08-02 00:15:00', - ] + $dtstart, + ], + 'daily', null, 1, new \DateTime($dtstart) + ); + } + + public function testUntilAndCount() + { + $this->expectException(InvalidDataException::class); + $this->expectExceptionMessage('Can not have both UNTIL and COUNT property at the same time'); + + $this->parse( + 'FREQ=DAILY;COUNT=5;UNTIL=20201108T225959Z', + '2021-01-18 00:15:00', + [] ); } public function testIgnoredStuff(): void { $this->parse( - 'FREQ=DAILY;BYSECOND=1;BYMINUTE=1;BYYEARDAY=1;BYWEEKNO=1;COUNT=2', + 'FREQ=DAILY;BYSECOND=1;BYMINUTE=1;COUNT=2', '2014-08-02 00:15:00', [ '2014-08-02 00:15:00', '2014-08-03 00:15:00', - ] + ], + 'daily', 2, 1, null ); } @@ -1012,7 +1180,8 @@ public function testMinusFifthThursday(): void '2015-01-08 00:15:00', '2015-02-05 00:15:00', '2015-03-05 00:15:00', - ] + ], + 'monthly', 4, 1, null ); } @@ -1031,6 +1200,7 @@ public function testNeverEnding(): void [ '2015-01-01 00:15:00', ], + 'monthly', null, 1, null, null, 'UTC', true @@ -1072,10 +1242,26 @@ public function testIteratorFunctions(): void ); } - public function parse($rule, string $start, array $expected, string $fastForward = null, string $tz = 'UTC', bool $runTillTheEnd = false): void - { + public function parse( + $rule, + string $start, + array $expected, + $expectedFreq = null, + $expectedCount = null, + $expectedInterval = null, + $expectedUntil = null, + string $fastForward = null, + string $tz = 'UTC', + bool $runTillTheEnd = false, + bool $yearlySkipUpperLimit = true + ): void { $dt = new \DateTime($start, new \DateTimeZone($tz)); - $parser = new RRuleIterator($rule, $dt); + $parser = new RRuleIterator($rule, $dt, $yearlySkipUpperLimit); + + $this->assertEquals($expectedFreq, $parser->getFrequency()); + $this->assertEquals($expectedCount, $parser->getCount()); + $this->assertEquals($expectedInterval, $parser->getInterval()); + $this->assertEquals($expectedUntil, $parser->getUntil()); if ($fastForward) { $parser->fastForward(new \DateTime($fastForward)); diff --git a/tests/VObject/TimeZoneUtilTest.php b/tests/VObject/TimeZoneUtilTest.php index 462218e9..1069b437 100644 --- a/tests/VObject/TimeZoneUtilTest.php +++ b/tests/VObject/TimeZoneUtilTest.php @@ -34,7 +34,8 @@ public function getMapping(): array include __DIR__.'/../../lib/timezonedata/windowszones.php', include __DIR__.'/../../lib/timezonedata/lotuszones.php', include __DIR__.'/../../lib/timezonedata/exchangezones.php', - include __DIR__.'/../../lib/timezonedata/php-workaround.php' + include __DIR__.'/../../lib/timezonedata/php-workaround.php', + include __DIR__.'/../../lib/timezonedata/extrazones.php', ); // PHPUNit requires an array of arrays @@ -46,6 +47,17 @@ function ($value) { ); } + /** + * @dataProvider getMapping + */ + public function testSlashTZ($timezonename): void + { + $slashTimezone = '/'.$timezonename; + $expected = TimeZoneUtil::getTimeZone($timezonename)->getName(); + $actual = TimeZoneUtil::getTimeZone($slashTimezone)->getName(); + self::assertEquals($expected, $actual); + } + public function testExchangeMap(): void { $vobj = <<getName(), $tz->getName()); } + public function testLowerCaseTimeZone(): void + { + $tz = TimeZoneUtil::getTimeZone('mountain time (us & canada)'); + $ex = new \DateTimeZone('America/Denver'); + self::assertEquals($ex->getName(), $tz->getName()); + } + + public function testDeprecatedTimeZone(): void + { + // Deprecated in 2022b + $tz = TimeZoneUtil::getTimeZone('Europe/Kiev'); + $ex = new \DateTimeZone('Europe/Kiev'); + self::assertSame($ex->getName(), $tz->getName()); + } + + public function testDeprecatedUnsupportedTimeZone(): void + { + // Deprecated and unsupported + $tz = TimeZoneUtil::getTimeZone('America/Godthab'); + $ex = new \DateTimeZone('America/Godthab'); + self::assertNotSame($ex->getName(), $tz->getName()); + } + /** * @dataProvider getPHPTimeZoneIdentifiers */ @@ -205,7 +240,10 @@ public function getPHPTimeZoneIdentifiers(): array function ($value) { return [$value]; }, - \DateTimeZone::listIdentifiers() + // FIXME remove the filter after finishing timezone migration + array_filter(\DateTimeZone::listIdentifiers(), static function (string $timezone) { + return 'Europe/Kyiv' !== $timezone; + }) ); } @@ -220,6 +258,11 @@ function ($value) { ); } + public function testKyivTimezone(): void + { + self::assertSame('Europe/Kiev', TimeZoneUtil::getTimeZone('Europe/Kyiv')->getName()); + } + public function testTimezoneOffset(): void { $tz = TimeZoneUtil::getTimeZone('GMT-0400', null, true); @@ -234,7 +277,7 @@ public function testTimezoneOffset(): void public function testTimezoneFail(): void { - $this->expectException(\InvalidArgumentException::class); + self::expectException(\InvalidArgumentException::class); TimeZoneUtil::getTimeZone('FooBar', null, true); } @@ -376,4 +419,303 @@ public function testPrefixedOffsetExchangeIdentifier(): void $ex = new \DateTimeZone('America/New_York'); self::assertEquals($ex->getName(), $tz->getName()); } + + public function testMicrosoftMap(): void + { + $tz = TimeZoneUtil::getTimeZone('tzone://Microsoft/Utc', null, true); + $ex = new \DateTimeZone('UTC'); + self::assertEquals($ex->getName(), $tz->getName()); + } + + /** + * @dataProvider unSupportTimezoneProvider + */ + public function testPHPUnSupportTimeZone(string $origin, string $expected): void + { + $tz = TimeZoneUtil::getTimeZone($origin, null, true); + $ex = new \DateTimeZone($expected); + self::assertEquals($ex->getName(), $tz->getName()); + } + + public function unSupportTimezoneProvider(): iterable + { + yield 'America/Santa_Isabel' => [ + 'origin' => 'America/Santa_Isabel', + 'expected' => 'America/Tijuana', + ]; + + yield 'Asia/Chongqing' => [ + 'origin' => 'Asia/Chongqing', + 'expected' => 'Asia/Shanghai', + ]; + + yield 'Asia/Harbin' => [ + 'origin' => 'Asia/Harbin', + 'expected' => 'Asia/Shanghai', + ]; + + yield 'Asia/Kashgar' => [ + 'origin' => 'Asia/Kashgar', + 'expected' => 'Asia/Urumqi', + ]; + + yield 'Pacific/Johnston' => [ + 'origin' => 'Pacific/Johnston', + 'expected' => 'Pacific/Honolulu', + ]; + + yield 'EDT' => [ + 'origin' => 'EDT', + 'expected' => 'America/Manaus', + ]; + + yield 'CDT' => [ + 'origin' => 'CDT', + 'expected' => 'America/Chicago', + ]; + + yield 'PST' => [ + 'origin' => 'PST', + 'expected' => 'America/Los_Angeles', + ]; + + yield 'Gulf Standard Time' => [ + 'origin' => 'Gulf Standard Time', + 'expected' => 'Asia/Dubai', + ]; + + if (($handle = fopen(__DIR__.'/microsoft-timezones-confluence.csv', 'r')) !== false) { + $data = fgetcsv($handle); + while (($data = fgetcsv($handle)) !== false) { + yield $data[0] => [ + 'origin' => $data[0], + 'expected' => '' !== $data[2] ? $data[2] : $data[1], + ]; + } + fclose($handle); + } + } + + /** + * @dataProvider offsetTimeZoneProvider + */ + public function testOffsetTimeZones(string $origin, string $expected): void + { + $tz = TimeZoneUtil::getTimeZone($origin, null, true); + $ex = new \DateTimeZone($expected); + self::assertEquals($ex->getName(), $tz->getName()); + } + + public function offsetTimeZoneProvider(): iterable + { + yield 'UTC-05:00' => [ + 'origin' => 'UTC-05:00', + 'expected' => 'America/Lima', + ]; + + yield '-5' => [ + 'origin' => '-5', + 'expected' => 'America/Lima', + ]; + + yield '-05' => [ + 'origin' => '-05', + 'expected' => 'America/Lima', + ]; + + yield '-05:00' => [ + 'origin' => '-05:00', + 'expected' => 'America/Lima', + ]; + } + + /** + * @dataProvider letterCaseTimeZoneProvider + */ + public function testDifferentLetterCaseTimeZone(string $origin, string $expected): void + { + $tz = TimeZoneUtil::getTimeZone($origin, null, true); + $ex = new \DateTimeZone($expected); + self::assertEquals($ex->getName(), $tz->getName()); + } + + public function letterCaseTimeZoneProvider(): iterable + { + yield 'case 1' => [ + 'origin' => 'Europe/paris', + 'expected' => 'Europe/Paris', + ]; + + yield 'case 2' => [ + 'origin' => 'europe/paris', + 'expected' => 'Europe/Paris', + ]; + + yield 'case 3' => [ + 'origin' => 'Europe/pAris', + 'expected' => 'Europe/Paris', + ]; + + yield 'case 4' => [ + 'origin' => 'Asia/taipei', + 'expected' => 'Asia/Taipei', + ]; + } + + /** + * @dataProvider outlookCitiesProvider + */ + public function testOutlookCities(string $origin, bool $failIfUncertain, string $expected): void + { + $tz = TimeZoneUtil::getTimeZone($origin, null, $failIfUncertain); + $ex = new \DateTimeZone($expected); + self::assertEquals($ex->getName(), $tz->getName()); + } + + public function outlookCitiesProvider(): iterable + { + yield 'case 1' => [ + 'origin' => 'TZID:(UTC+01:00) Bruxelles\, København\, Madrid\, Paris', + 'failIfUncertain' => true, + 'expected' => 'Europe/Madrid', + ]; + + yield 'case 2' => [ + 'origin' => 'TZID:(UTC+01:00) Bruxelles, København, Madrid, Paris', + 'failIfUncertain' => true, + 'expected' => 'Europe/Madrid', + ]; + + yield 'case 3' => [ + 'origin' => 'TZID:(UTC+01:00)Bruxelles\, København\, Madrid\, Paris', + 'failIfUncertain' => true, + 'expected' => 'Europe/Madrid', + ]; + + yield 'case 4' => [ + 'origin' => 'Bruxelles\, København\, Madrid\, Paris', + 'failIfUncertain' => false, + 'expected' => 'UTC', + ]; + } + + /** + * @dataProvider versionTzProvider + */ + public function testVersionTz(string $origin, bool $failIfUncertain, string $expected): void + { + $tz = TimeZoneUtil::getTimeZone($origin, null, $failIfUncertain); + $ex = new \DateTimeZone($expected); + self::assertEquals($ex->getName(), $tz->getName()); + } + + public function versionTzProvider(): iterable + { + yield 'case 1' => [ + 'origin' => 'Eastern Standard Time 1', + 'failIfUncertain' => true, + 'expected' => 'America/New_York', + ]; + + yield 'case 2' => [ + 'origin' => 'Eastern Standard Time 2', + 'failIfUncertain' => true, + 'expected' => 'America/New_York', + ]; + } + + public function testCustomizedTimeZone(): void + { + $ics = <<getName()); + $start = new \DateTimeImmutable('2022-04-25'); + self::assertSame(10 * 60 * 60, $tz->getOffset($start)); + + $start = new \DateTimeImmutable('2022-11-10'); + self::assertSame(11 * 60 * 60, $tz->getOffset($start)); + } + + public function testCustomizedTimeZoneWithoutDaylight(): void + { + $ics = $this->getCustomizedICS(); + $tz = TimeZoneUtil::getTimeZone('Customized Time Zone', Reader::read($ics)); + self::assertSame('Asia/Brunei', $tz->getName()); + $start = new \DateTimeImmutable('2022-04-25'); + self::assertSame(8 * 60 * 60, $tz->getOffset($start)); + } + + public function testCustomizedTimeZoneFlag(): void + { + self::expectException(\InvalidArgumentException::class); + $ics = $this->getCustomizedICS(); + $vobject = Reader::read($ics); + $vobject->VEVENT->DTSTART->getDateTime(null, false); + } + + private function getCustomizedICS(): string + { + return <<expectException(\InvalidArgumentException::class); $input = <<convert(Document::VCARD40); - self::assertVObjectEqualsVObject( + $this->assertVObjectEqualsVObject( $output, $vcard ); diff --git a/tests/VObject/microsoft-timezones-confluence.csv b/tests/VObject/microsoft-timezones-confluence.csv new file mode 100644 index 00000000..bc7945f4 --- /dev/null +++ b/tests/VObject/microsoft-timezones-confluence.csv @@ -0,0 +1,548 @@ +Original timezone,Replacement,Proposed,Is manual +"abu dhabi, muscat",Asia/Dubai,Asia/Muscat,TRUE +acre,America/Rio_Branco,America/Rio_Branco,FALSE +"adelaide, central australia",Australia/Adelaide,Australia/Adelaide,FALSE +afghanistan,Asia/Kabul,Asia/Kabul,FALSE +afghanistan standard time,Asia/Kabul,Asia/Kabul,FALSE +africa central,Africa/Maputo,Africa/Maputo,FALSE +africa eastern,Africa/Nairobi,Africa/Nairobi,FALSE +africa farwestern,Africa/El_Aaiun,Africa/El_Aaiun,FALSE +africa southern,Africa/Johannesburg,Africa/Johannesburg,FALSE +africa western,Africa/Lagos,Africa/Lagos,FALSE +aktyubinsk,Asia/Aqtobe,Asia/Aqtobe,FALSE +alaska,America/Anchorage,America/Anchorage,FALSE +alaska hawaii,America/Anchorage,America/Anchorage,FALSE +alaskan,America/Anchorage,America/Anchorage,FALSE +alaskan standard time,America/Anchorage,America/Anchorage,FALSE +aleutian standard time,America/Adak,America/Adak,FALSE +almaty,Asia/Almaty,Asia/Almaty,FALSE +"almaty, novosibirsk, north central asia",Asia/Almaty,Asia/Almaty,FALSE +altai standard time,Asia/Barnaul,Asia/Barnaul,FALSE +amazon,America/Manaus,America/Manaus,FALSE +america central,America/Chicago,America/Chicago,FALSE +america eastern,America/New_York,America/New_York,FALSE +america mountain,America/Denver,America/Denver,FALSE +america pacific,America/Los_Angeles,America/Los_Angeles,FALSE +"amsterdam, berlin, bern, rome, stockholm, vienna",Europe/Berlin,Europe/Berlin,FALSE +anadyr,Asia/Anadyr,Asia/Anadyr,FALSE +apia,Pacific/Apia,Pacific/Apia,FALSE +aqtau,Asia/Aqtau,Asia/Aqtau,FALSE +aqtobe,Asia/Aqtobe,Asia/Aqtobe,FALSE +arab,Asia/Riyadh,Asia/Kuwait,TRUE +arab standard time,Asia/Riyadh,Asia/Riyadh,FALSE +"arab, kuwait, riyadh",Asia/Riyadh,Asia/Kuwait,TRUE +arabian,Asia/Dubai,Asia/Muscat,TRUE +arabian standard time,Asia/Dubai,Asia/Dubai,FALSE +arabic,Asia/Baghdad,Asia/Baghdad,FALSE +arabic standard time,Asia/Baghdad,Asia/Baghdad,FALSE +argentina,America/Argentina/Buenos_Aires,America/Argentina/Buenos_Aires,FALSE +argentina standard time,America/Argentina/Buenos_Aires,America/Argentina/Buenos_Aires,FALSE +argentina western,America/Argentina/San_Luis,America/Argentina/San_Luis,FALSE +arizona,America/Phoenix,America/Phoenix,FALSE +armenia,Asia/Yerevan,Asia/Yerevan,FALSE +armenian,Asia/Yerevan,Asia/Yerevan,FALSE +armenian standard time,Asia/Yerevan,Asia/Yerevan,FALSE +ashkhabad,Asia/Ashgabat,Asia/Ashgabat,FALSE +"astana, dhaka",Asia/Dhaka,Asia/Dhaka,FALSE +astrakhan standard time,Europe/Astrakhan,Europe/Astrakhan,FALSE +"athens, istanbul, minsk",Europe/Athens,Europe/Athens,FALSE +atlantic,America/Halifax,America/Halifax,FALSE +atlantic standard time,America/Halifax,America/Halifax,FALSE +atlantic time (canada),America/Halifax,America/Halifax,FALSE +"auckland, wellington",Pacific/Auckland,Pacific/Auckland,FALSE +aus central,Australia/Darwin,Australia/Darwin,FALSE +aus central standard time,Australia/Darwin,Australia/Darwin,FALSE +aus central w standard time,Australia/Eucla,Australia/Eucla,FALSE +aus eastern,Australia/Sydney,Australia/Sydney,FALSE +aus eastern standard time,Australia/Sydney,Australia/Sydney,FALSE +australia central,Australia/Adelaide,Australia/Adelaide,FALSE +australia centralwestern,Australia/Eucla,Australia/Eucla,FALSE +australia eastern,Australia/Sydney,Australia/Sydney,FALSE +australia western,Australia/Perth,Australia/Perth,FALSE +azerbaijan,Asia/Baku,Asia/Baku,FALSE +azerbaijan standard time,Asia/Baku,Asia/Baku,FALSE +azerbijan,Asia/Baku,Asia/Baku,FALSE +azores,Atlantic/Azores,Atlantic/Azores,FALSE +azores standard time,Atlantic/Azores,Atlantic/Azores,FALSE +baghdad,Asia/Baghdad,Asia/Baghdad,FALSE +bahia standard time,America/Bahia,America/Bahia,FALSE +baku,Asia/Baku,Asia/Baku,FALSE +"baku, tbilisi, yerevan",Asia/Baku,Asia/Baku,FALSE +"bangkok, hanoi, jakarta",Asia/Bangkok,Asia/Bangkok,FALSE +bangladesh,Asia/Dhaka,Asia/Dhaka,FALSE +bangladesh standard time,Asia/Dhaka,Asia/Dhaka,FALSE +"beijing, chongqing, hong kong sar, urumqi",Asia/Shanghai,Asia/Shanghai,FALSE +belarus standard time,Europe/Minsk,Europe/Minsk,FALSE +"belgrade, pozsony, budapest, ljubljana, prague",Europe/Prague,Europe/Prague,FALSE +bering,America/Adak,America/Adak,FALSE +bhutan,Asia/Thimphu,Asia/Thimphu,FALSE +"bogota, lima, quito",America/Bogota,America/Bogota,FALSE +bolivia,America/La_Paz,America/La_Paz,FALSE +borneo,Asia/Kuching,Asia/Kuching,FALSE +bougainville standard time,Pacific/Bougainville,Pacific/Bougainville,FALSE +brasilia,America/Sao_Paulo,America/Sao_Paulo,FALSE +"brisbane, east australia",Australia/Brisbane,Australia/Brisbane,FALSE +british,Europe/London,Europe/London,FALSE +brunei,Asia/Brunei,Asia/Brunei,FALSE +"brussels, copenhagen, madrid, paris",Europe/Paris,Europe/Paris,FALSE +bucharest,Europe/Bucharest,Europe/Bucharest,FALSE +buenos aires,America/Argentina/Buenos_Aires,America/Argentina/Buenos_Aires,FALSE +cairo,Africa/Cairo,Africa/Cairo,FALSE +canada central,America/Edmonton,America/Edmonton,FALSE +canada central standard time,America/Regina,America/Regina,FALSE +"canberra, melbourne, sydney, hobart (year 2000 only)",Australia/Sydney,Australia/Sydney,FALSE +cape verde,Atlantic/Cape_Verde,Atlantic/Cape_Verde,FALSE +cape verde is,Atlantic/Cape_Verde,Atlantic/Cape_Verde,FALSE +cape verde standard time,Atlantic/Cape_Verde,Atlantic/Cape_Verde,FALSE +"caracas, la paz",America/Caracas,America/Caracas,FALSE +"casablanca, monrovia",Africa/Casablanca,Africa/Casablanca,FALSE +casey,Antarctica/Casey,Antarctica/Casey,FALSE +caucasus,Asia/Yerevan,Asia/Yerevan,FALSE +caucasus standard time,Asia/Yerevan,Asia/Yerevan,FALSE +cen australia,Australia/Adelaide,Australia/Adelaide,FALSE +cen australia standard time,Australia/Adelaide,Australia/Adelaide,FALSE +central,America/Chicago,America/Chicago,FALSE +central america,America/Guatemala,America/Guatemala,FALSE +central america standard time,America/Guatemala,America/Guatemala,FALSE +central asia,Asia/Dhaka,Asia/Dhaka,FALSE +central asia standard time,Asia/Almaty,Asia/Almaty,FALSE +central brazilian,America/Manaus,America/Manaus,FALSE +central brazilian standard time,America/Cuiaba,America/Cuiaba,FALSE +central europe,Europe/Prague,Europe/Prague,FALSE +central europe standard time,Europe/Budapest,Europe/Budapest,FALSE +central european,Europe/Belgrade,Europe/Sarajevo,TRUE +central european standard time,Europe/Warsaw,Europe/Warsaw,FALSE +central pacific,Asia/Magadan,Asia/Magadan,FALSE +central pacific standard time,Pacific/Guadalcanal,Pacific/Guadalcanal,FALSE +central standard time,America/Chicago,America/Chicago,FALSE +central standard time (mexico),America/Mexico_City,America/Mexico_City,FALSE +central time (us & canada),America/Chicago,America/Chicago,FALSE +chamorro,Pacific/Guam,Pacific/Saipan,TRUE +chatham,Pacific/Chatham,Pacific/Chatham,FALSE +chatham islands standard time,Pacific/Chatham,Pacific/Chatham,FALSE +chile,America/Santiago,America/Santiago,FALSE +china,Asia/Shanghai,Asia/Shanghai,FALSE +china standard time,Asia/Shanghai,Asia/Shanghai,FALSE +choibalsan,Asia/Choibalsan,Asia/Choibalsan,FALSE +christmas,Indian/Christmas,Indian/Christmas,FALSE +cocos,Indian/Cocos,Indian/Cocos,FALSE +colombia,America/Bogota,America/Bogota,FALSE +cook,Pacific/Rarotonga,Pacific/Rarotonga,FALSE +cuba,America/Havana,America/Havana,FALSE +cuba standard time,America/Havana,America/Havana,FALSE +dacca,Asia/Dhaka,Asia/Dhaka,FALSE +darwin,Australia/Darwin,Australia/Darwin,FALSE +dateline,Pacific/Auckland,Pacific/Auckland,FALSE +dateline standard time,Pacific/Niue,Pacific/Niue,FALSE +davis,Antarctica/Davis,Antarctica/Davis,FALSE +dominican,America/Santo_Domingo,America/Santo_Domingo,FALSE +dumontdurville,Antarctica/DumontDUrville,Antarctica/DumontDUrville,FALSE +dushanbe,Asia/Dushanbe,Asia/Dushanbe,FALSE +dutch guiana,America/Paramaribo,America/Paramaribo,FALSE +e africa,Africa/Nairobi,Africa/Nairobi,FALSE +e africa standard time,Africa/Nairobi,Africa/Nairobi,FALSE +e australia,Australia/Brisbane,Australia/Brisbane,FALSE +e australia standard time,Australia/Brisbane,Australia/Brisbane,FALSE +e europe,Europe/Minsk,Europe/Minsk,FALSE +e europe standard time,Europe/Chisinau,Europe/Chisinau,FALSE +e south america,America/Belem,America/Belem,FALSE +e south america standard time,America/Sao_Paulo,America/Sao_Paulo,FALSE +"east africa, nairobi",Africa/Nairobi,Africa/Nairobi,FALSE +east timor,Asia/Dili,Asia/Dili,FALSE +easter,Pacific/Easter,Pacific/Easter,FALSE +easter island standard time,Pacific/Easter,Pacific/Easter,FALSE +eastern,America/New_York,America/New_York,FALSE +eastern standard time,America/New_York,America/New_York,FALSE +eastern standard time (mexico),America/Cancun,America/Cancun,FALSE +eastern time (us & canada),America/New_York,America/New_York,FALSE +ecuador,America/Guayaquil,America/Guayaquil,FALSE +egypt,Africa/Cairo,Africa/Cairo,FALSE +egypt standard time,Africa/Cairo,Africa/Cairo,FALSE +ekaterinburg,Asia/Yekaterinburg,Asia/Yekaterinburg,FALSE +ekaterinburg standard time,Asia/Yekaterinburg,Asia/Yekaterinburg,FALSE +"eniwetok, kwajalein, dateline time",Pacific/Kwajalein,Pacific/Kwajalein,FALSE +europe central,Europe/Paris,Europe/Paris,FALSE +europe eastern,Europe/Bucharest,Europe/Bucharest,FALSE +europe further eastern,Europe/Minsk,Europe/Minsk,FALSE +europe western,Atlantic/Canary,Atlantic/Canary,FALSE +falkland,Atlantic/Stanley,Atlantic/Stanley,FALSE +fiji,Pacific/Fiji,Pacific/Fiji,FALSE +fiji islands standard time,Pacific/Fiji,Pacific/Fiji,FALSE +"fiji islands, kamchatka, marshall is",Pacific/Fiji,Pacific/Fiji,FALSE +fiji standard time,Pacific/Fiji,Pacific/Fiji,FALSE +fle,Europe/Helsinki,Europe/Helsinki,FALSE +fle standard time,Europe/Kiev,Europe/Kiev,FALSE +french guiana,America/Cayenne,America/Cayenne,FALSE +french southern,Indian/Kerguelen,Indian/Kerguelen,FALSE +frunze,Asia/Bishkek,Asia/Bishkek,FALSE +galapagos,Pacific/Galapagos,Pacific/Galapagos,FALSE +gambier,Pacific/Gambier,Pacific/Gambier,FALSE +georgia,Asia/Tbilisi,Asia/Tbilisi,FALSE +georgian,Asia/Tbilisi,Asia/Tbilisi,FALSE +georgian standard time,Asia/Tbilisi,Asia/Tbilisi,FALSE +gilbert islands,Pacific/Tarawa,Pacific/Tarawa,FALSE +gmt,Europe/London,Europe/London,FALSE +gmt standard time,Europe/London,Europe/London,FALSE +goose bay,America/Goose_Bay,America/Goose_Bay,FALSE +greenland,Atlantic/Stanley,Atlantic/Stanley,FALSE +greenland central,America/Scoresbysund,America/Scoresbysund,FALSE +greenland eastern,America/Scoresbysund,America/Scoresbysund,FALSE +greenland standard time,Atlantic/Stanley,Atlantic/Stanley,FALSE +greenland western,Atlantic/Stanley,Atlantic/Stanley,FALSE +greenwich,Atlantic/Reykjavik,Atlantic/Reykjavik,FALSE +"greenwich mean time; dublin, edinburgh, london",Europe/London,Europe/London,FALSE +"greenwich mean time: dublin, edinburgh, lisbon, london",Europe/Lisbon,Europe/Lisbon,FALSE +greenwich standard time,Atlantic/Reykjavik,Atlantic/Reykjavik,FALSE +gtb,Europe/Athens,Europe/Athens,FALSE +gtb standard time,Europe/Bucharest,Europe/Bucharest,FALSE +guam,Pacific/Guam,Pacific/Guam,FALSE +"guam, port moresby",Pacific/Guam,Pacific/Guam,FALSE +gulf,Asia/Dubai,Asia/Dubai,FALSE +guyana,America/Guyana,America/Guyana,FALSE +haiti standard time,America/Port-au-Prince,America/Port-au-Prince,FALSE +"harare, pretoria",Africa/Maputo,Africa/Harare,TRUE +hawaii,Pacific/Honolulu,Pacific/Honolulu,FALSE +hawaii aleutian,Pacific/Honolulu,Pacific/Honolulu,FALSE +hawaiian,Pacific/Honolulu,Pacific/Honolulu,FALSE +hawaiian standard time,Pacific/Honolulu,Pacific/Honolulu,FALSE +"helsinki, riga, tallinn",Europe/Helsinki,Europe/Helsinki,FALSE +"hobart, tasmania",Australia/Hobart,Australia/Hobart,FALSE +hong kong,Asia/Hong_Kong,Asia/Hong_Kong,FALSE +hovd,Asia/Hovd,Asia/Hovd,FALSE +india,Asia/Kolkata,Asia/Kolkata,FALSE +india standard time,Asia/Kolkata,Asia/Kolkata,FALSE +indian ocean,Indian/Chagos,Indian/Chagos,FALSE +indiana (east),America/New_York,America/Indiana/Indianapolis,TRUE +indochina,Asia/Bangkok,Asia/Bangkok,FALSE +indonesia central,Asia/Makassar,Asia/Makassar,FALSE +indonesia eastern,Asia/Jayapura,Asia/Jayapura,FALSE +indonesia western,Asia/Jakarta,Asia/Jakarta,FALSE +iran,Asia/Tehran,Asia/Tehran,FALSE +iran standard time,Asia/Tehran,Asia/Tehran,FALSE +irish,Europe/Dublin,Europe/Dublin,FALSE +irkutsk,Asia/Irkutsk,Asia/Irkutsk,FALSE +"irkutsk, ulaan bataar",Asia/Irkutsk,Asia/Irkutsk,FALSE +"islamabad, karachi, tashkent",Asia/Karachi,Asia/Karachi,FALSE +israel,Asia/Jerusalem,Asia/Jerusalem,FALSE +israel standard time,Asia/Jerusalem,Asia/Jerusalem,FALSE +"israel, jerusalem standard time",Asia/Jerusalem,Asia/Jerusalem,FALSE +japan,Asia/Tokyo,Asia/Tokyo,FALSE +jordan,Asia/Amman,Asia/Amman,FALSE +jordan standard time,Asia/Amman,Asia/Amman,FALSE +kabul,Asia/Kabul,Asia/Kabul,FALSE +kaliningrad standard time,Europe/Kaliningrad,Europe/Kaliningrad,FALSE +kamchatka,Asia/Kamchatka,Asia/Kamchatka,FALSE +kamchatka standard time,Asia/Kamchatka,Asia/Kamchatka,FALSE +karachi,Asia/Karachi,Asia/Karachi,FALSE +"kathmandu, nepal",Asia/Kathmandu,Asia/Kathmandu,FALSE +kazakhstan eastern,Asia/Almaty,Asia/Almaty,FALSE +kazakhstan western,Asia/Aqtobe,Asia/Aqtobe,FALSE +kizilorda,Asia/Qyzylorda,Asia/Qyzylorda,FALSE +"kolkata, chennai, mumbai, new delhi, india standard time",Asia/Kolkata,Asia/Kolkata,FALSE +korea,Asia/Seoul,Asia/Seoul,FALSE +korea standard time,Asia/Seoul,Asia/Seoul,FALSE +kosrae,Pacific/Kosrae,Pacific/Kosrae,FALSE +krasnoyarsk,Asia/Krasnoyarsk,Asia/Krasnoyarsk,FALSE +"kuala lumpur, singapore",Asia/Shanghai,Asia/Singapore,TRUE +kuybyshev,Europe/Samara,Europe/Samara,FALSE +kwajalein,Pacific/Kwajalein,Pacific/Kwajalein,FALSE +kyrgystan,Asia/Bishkek,Asia/Bishkek,FALSE +lanka,Asia/Colombo,Asia/Colombo,FALSE +liberia,Africa/Monrovia,Africa/Monrovia,FALSE +libya standard time,Africa/Tripoli,Africa/Tripoli,FALSE +line islands,Pacific/Kiritimati,Pacific/Kiritimati,FALSE +line islands standard time,Pacific/Kiritimati,Pacific/Kiritimati,FALSE +lord howe,Australia/Lord_Howe,Australia/Lord_Howe,FALSE +lord howe standard time,Australia/Lord_Howe,Australia/Lord_Howe,FALSE +macau,Asia/Macau,Asia/Macau,FALSE +macquarie,Antarctica/Macquarie,Antarctica/Macquarie,FALSE +magadan,Asia/Magadan,Asia/Magadan,FALSE +magadan standard time,Asia/Magadan,Asia/Magadan,FALSE +"magadan, solomon is, new caledonia",Asia/Magadan,Asia/Magadan,FALSE +magallanes standard time,America/Punta_Arenas,America/Punta_Arenas,FALSE +malaya,Asia/Kuala_Lumpur,Asia/Kuala_Lumpur,FALSE +malaysia,Asia/Kuching,Asia/Kuching,FALSE +maldives,Indian/Maldives,Indian/Maldives,FALSE +marquesas,Pacific/Marquesas,Pacific/Marquesas,FALSE +marquesas standard time,Pacific/Marquesas,Pacific/Marquesas,FALSE +marshall islands,Pacific/Majuro,Pacific/Majuro,FALSE +mauritius,Indian/Mauritius,Indian/Mauritius,FALSE +mauritius standard time,Indian/Mauritius,Indian/Mauritius,FALSE +mawson,Antarctica/Mawson,Antarctica/Mawson,FALSE +mexico,America/Mexico_City,America/Mexico_City,FALSE +"mexico city, tegucigalpa",America/Mexico_City,America/Mexico_City,FALSE +mexico pacific,America/Mazatlan,America/Mazatlan,FALSE +mexico standard time,America/Mexico_City,America/Mexico_City,FALSE +mexico standard time 2,America/Chihuahua,America/Chihuahua,FALSE +mid-atlantic,America/Noronha,America/Noronha,FALSE +mid-atlantic standard time,Atlantic/Cape_Verde,Atlantic/Cape_Verde,FALSE +middle east,Asia/Beirut,Asia/Beirut,FALSE +middle east standard time,Asia/Beirut,Asia/Beirut,FALSE +"midway island, samoa",Pacific/Pago_Pago,Pacific/Midway,TRUE +mongolia,Asia/Ulaanbaatar,Asia/Ulaanbaatar,FALSE +montevideo,America/Montevideo,America/Montevideo,FALSE +montevideo standard time,America/Montevideo,America/Montevideo,FALSE +morocco,Africa/Casablanca,Africa/Casablanca,FALSE +morocco standard time,Africa/Casablanca,Africa/Casablanca,FALSE +moscow,Europe/Moscow,Europe/Moscow,FALSE +"moscow, st petersburg, volgograd",Europe/Moscow,Europe/Moscow,FALSE +mountain,America/Denver,America/Denver,FALSE +mountain standard time,America/Denver,America/Denver,FALSE +mountain standard time (mexico),America/Chihuahua,America/Chihuahua,FALSE +mountain time (us & canada),America/Denver,America/Denver,FALSE +myanmar,Indian/Cocos,Asia/Yangon,TRUE +myanmar standard time,Indian/Cocos,Asia/Yangon,TRUE +n central asia,Asia/Almaty,Asia/Almaty,FALSE +n central asia standard time,Asia/Novosibirsk,Asia/Novosibirsk,FALSE +namibia,Africa/Windhoek,Africa/Windhoek,FALSE +namibia standard time,Africa/Windhoek,Africa/Windhoek,FALSE +nauru,Pacific/Nauru,Pacific/Nauru,FALSE +nepal,Asia/Kathmandu,Asia/Kathmandu,FALSE +nepal standard time,Asia/Kathmandu,Asia/Kathmandu,FALSE +new caledonia,Pacific/Noumea,Pacific/Noumea,FALSE +new zealand,Pacific/Auckland,Pacific/Auckland,FALSE +new zealand standard time,Pacific/Auckland,Pacific/Auckland,FALSE +newfoundland,America/St_Johns,America/St_Johns,FALSE +newfoundland and labrador standard time,America/St_Johns,America/St_Johns,FALSE +newfoundland standard time,America/St_Johns,America/St_Johns,FALSE +niue,Pacific/Niue,Pacific/Niue,FALSE +norfolk,Pacific/Norfolk,Pacific/Norfolk,FALSE +norfolk standard time,Pacific/Norfolk,Pacific/Norfolk,FALSE +noronha,America/Noronha,America/Noronha,FALSE +north asia,Asia/Krasnoyarsk,Asia/Krasnoyarsk,FALSE +north asia east,Asia/Irkutsk,Asia/Irkutsk,FALSE +north asia east standard time,Asia/Irkutsk,Asia/Irkutsk,FALSE +north asia standard time,Asia/Krasnoyarsk,Asia/Krasnoyarsk,FALSE +north korea standard time,Asia/Pyongyang,Asia/Pyongyang,FALSE +north mariana,Pacific/Guam,Pacific/Saipan,TRUE +novosibirsk,Asia/Novosibirsk,Asia/Novosibirsk,FALSE +"nuku'alofa, tonga",Pacific/Tongatapu,Pacific/Tongatapu,FALSE +omsk,Asia/Omsk,Asia/Omsk,FALSE +omsk standard time,Asia/Omsk,Asia/Omsk,FALSE +oral,Asia/Oral,Asia/Oral,FALSE +"osaka, sapporo, tokyo",Asia/Tokyo,Asia/Tokyo,FALSE +pacific,America/Los_Angeles,America/Los_Angeles,FALSE +pacific sa,America/Santiago,America/Santiago,FALSE +pacific sa standard time,America/Santiago,America/Santiago,FALSE +pacific standard time,America/Los_Angeles,America/Los_Angeles,FALSE +pacific standard time (mexico),America/Tijuana,America/Tijuana,FALSE +pacific time (us & canada),America/Los_Angeles,America/Los_Angeles,FALSE +pacific time (us & canada); tijuana,America/Los_Angeles,America/Los_Angeles,FALSE +pakistan,Asia/Karachi,Asia/Karachi,FALSE +pakistan standard time,Asia/Karachi,Asia/Karachi,FALSE +palau,Pacific/Palau,Pacific/Palau,FALSE +papua new guinea,Pacific/Port_Moresby,Pacific/Port_Moresby,FALSE +paraguay,America/Asuncion,America/Asuncion,FALSE +paraguay standard time,America/Asuncion,America/Asuncion,FALSE +"paris, madrid, brussels, copenhagen",Europe/Paris,Europe/Paris,FALSE +"perth, western australia",Australia/Perth,Australia/Perth,FALSE +peru,America/Lima,America/Lima,FALSE +philippines,Asia/Manila,Asia/Manila,FALSE +phoenix islands,Pacific/Fakaofo,Pacific/Fakaofo,FALSE +pierre miquelon,America/Miquelon,America/Miquelon,FALSE +pitcairn,Pacific/Pitcairn,Pacific/Pitcairn,FALSE +"prague, central europe",Europe/Prague,Europe/Prague,FALSE +pyongyang,Asia/Pyongyang,Asia/Pyongyang,FALSE +qyzylorda,Asia/Qyzylorda,Asia/Qyzylorda,FALSE +qyzylorda standard time,Asia/Qyzylorda,Asia/Qyzylorda,FALSE +rangoon,Indian/Cocos,Asia/Yangon,TRUE +reunion,Indian/Reunion,Indian/Reunion,FALSE +romance,Europe/Paris,Europe/Paris,FALSE +romance standard time,Europe/Paris,Europe/Paris,FALSE +rothera,Antarctica/Rothera,Antarctica/Rothera,FALSE +russia time zone 10,Asia/Srednekolymsk,Asia/Srednekolymsk,FALSE +russia time zone 11,Asia/Kamchatka,Asia/Kamchatka,FALSE +russia time zone 3,Europe/Samara,Europe/Samara,FALSE +russian,Europe/Moscow,Europe/Moscow,FALSE +russian standard time,Europe/Moscow,Europe/Moscow,FALSE +sa eastern,America/Belem,America/Belem,FALSE +sa eastern standard time,America/Cayenne,America/Cayenne,FALSE +sa pacific,America/Bogota,America/Bogota,FALSE +sa pacific standard time,America/Bogota,America/Bogota,FALSE +sa western,America/La_Paz,America/La_Paz,FALSE +sa western standard time,America/La_Paz,America/La_Paz,FALSE +saint pierre standard time,America/Miquelon,America/Miquelon,FALSE +sakhalin,Asia/Sakhalin,Asia/Sakhalin,FALSE +sakhalin standard time,Asia/Sakhalin,Asia/Sakhalin,FALSE +samara,Europe/Samara,Europe/Samara,FALSE +samarkand,Asia/Samarkand,Asia/Samarkand,FALSE +samoa,Pacific/Apia,Pacific/Apia,FALSE +samoa standard time,Pacific/Apia,Pacific/Apia,FALSE +santiago,America/Santiago,America/Santiago,FALSE +sao tome standard time,Africa/Sao_Tome,Africa/Sao_Tome,FALSE +"sarajevo, skopje, sofija, vilnius, warsaw, zagreb",Europe/Belgrade,Europe/Sarajevo,TRUE +saratov standard time,Europe/Saratov,Europe/Saratov,FALSE +saskatchewan,America/Edmonton,America/Edmonton,FALSE +se asia,Asia/Bangkok,Asia/Bangkok,FALSE +se asia standard time,Asia/Bangkok,Asia/Bangkok,FALSE +"seoul, korea standard time",Asia/Seoul,Asia/Seoul,FALSE +seychelles,Indian/Mahe,Indian/Mahe,FALSE +shevchenko,Asia/Aqtau,Asia/Aqtau,FALSE +singapore,Asia/Shanghai,Asia/Singapore,TRUE +singapore standard time,Asia/Shanghai,Asia/Singapore,TRUE +solomon,Pacific/Guadalcanal,Pacific/Guadalcanal,FALSE +south africa,Africa/Maputo,Africa/Harare,TRUE +south africa standard time,Africa/Johannesburg,Africa/Johannesburg,FALSE +south georgia,Atlantic/South_Georgia,Atlantic/South_Georgia,FALSE +"sri jayawardenepura, sri lanka",Asia/Colombo,Asia/Colombo,FALSE +sri lanka,Asia/Colombo,Asia/Colombo,FALSE +sri lanka standard time,Asia/Colombo,Asia/Colombo,FALSE +sudan standard time,Africa/Khartoum,Africa/Khartoum,FALSE +suriname,America/Paramaribo,America/Paramaribo,FALSE +sverdlovsk,Asia/Yekaterinburg,Asia/Yekaterinburg,FALSE +syowa,Antarctica/Syowa,Antarctica/Syowa,FALSE +syria standard time,Asia/Damascus,Asia/Damascus,FALSE +tahiti,Pacific/Tahiti,Pacific/Tahiti,FALSE +taipei,Asia/Taipei,Asia/Taipei,FALSE +taipei standard time,Asia/Taipei,Asia/Taipei,FALSE +tajikistan,Asia/Dushanbe,Asia/Dushanbe,FALSE +tashkent,Asia/Tashkent,Asia/Tashkent,FALSE +tasmania,Australia/Hobart,Australia/Hobart,FALSE +tasmania standard time,Australia/Hobart,Australia/Hobart,FALSE +tbilisi,Asia/Tbilisi,Asia/Tbilisi,FALSE +tehran,Asia/Tehran,Asia/Tehran,FALSE +tocantins standard time,America/Araguaina,America/Araguaina,FALSE +tokelau,Pacific/Fakaofo,Pacific/Fakaofo,FALSE +tokyo,Asia/Tokyo,Asia/Tokyo,FALSE +tokyo standard time,Asia/Tokyo,Asia/Tokyo,FALSE +tomsk standard time,Asia/Tomsk,Asia/Tomsk,FALSE +tonga,Pacific/Tongatapu,Pacific/Tongatapu,FALSE +tonga standard time,Pacific/Tongatapu,Pacific/Tongatapu,FALSE +transbaikal standard time,Asia/Chita,Asia/Chita,FALSE +transitional islamic state of afghanistan standard time,Asia/Kabul,Asia/Kabul,FALSE +turkey,Europe/Istanbul,Europe/Istanbul,FALSE +turkey standard time,Europe/Istanbul,Europe/Istanbul,FALSE +turkmenistan,Asia/Ashgabat,Asia/Ashgabat,FALSE +turks and caicos standard time,America/Grand_Turk,America/Grand_Turk,FALSE +tuvalu,Asia/Kamchatka,Pacific/Funafuti,TRUE +ulaanbaatar standard time,Asia/Ulaanbaatar,Asia/Ulaanbaatar,FALSE +universal coordinated time,UTC,UTC,FALSE +uralsk,Asia/Oral,Asia/Oral,FALSE +uruguay,America/Montevideo,America/Montevideo,FALSE +urumqi,Asia/Urumqi,Asia/Urumqi,FALSE +us eastern,America/New_York,America/Indiana/Indianapolis,TRUE +us eastern standard time,America/New_York,America/New_York,FALSE +us mountain,America/Phoenix,America/Phoenix,FALSE +us mountain standard time,America/Phoenix,America/Phoenix,FALSE +utc-02,America/Noronha,America/Noronha,FALSE +utc-08,Pacific/Pitcairn,Pacific/Pitcairn,FALSE +utc-09,Pacific/Gambier,Pacific/Gambier,FALSE +utc-11,Pacific/Niue,Pacific/Niue,FALSE +utc+12,Pacific/Auckland,Pacific/Auckland,FALSE +uzbekistan,Asia/Tashkent,Asia/Tashkent,FALSE +vanuatu,Pacific/Efate,Pacific/Efate,FALSE +venezuela,America/Caracas,America/Caracas,FALSE +venezuela standard time,America/Caracas,America/Caracas,FALSE +vladivostok,Asia/Vladivostok,Asia/Vladivostok,FALSE +vladivostok standard time,Asia/Vladivostok,Asia/Vladivostok,FALSE +volgograd,Europe/Volgograd,Europe/Volgograd,FALSE +volgograd standard time,Europe/Volgograd,Europe/Volgograd,FALSE +vostok,Antarctica/Vostok,Antarctica/Vostok,FALSE +w australia,Australia/Perth,Australia/Perth,FALSE +w australia standard time,Australia/Perth,Australia/Perth,FALSE +w central africa,Africa/Lagos,Africa/Lagos,FALSE +w central africa standard time,Africa/Lagos,Africa/Lagos,FALSE +w europe,Europe/Amsterdam,Europe/Amsterdam,FALSE +w europe standard time,Europe/Berlin,Europe/Berlin,FALSE +w mongolia standard time,Asia/Hovd,Asia/Hovd,FALSE +wake,Asia/Kamchatka,Pacific/Wake,TRUE +wallis,Asia/Kamchatka,Pacific/Wallis,TRUE +west asia,Asia/Tashkent,Asia/Tashkent,FALSE +west asia standard time,Asia/Tashkent,Asia/Tashkent,FALSE +west bank standard time,Asia/Hebron,Asia/Hebron,FALSE +west central africa,Africa/Lagos,Africa/Luanda,TRUE +west pacific,Pacific/Guam,Pacific/Guam,FALSE +west pacific standard time,Pacific/Port_Moresby,Pacific/Port_Moresby,FALSE +yakutsk,Asia/Yakutsk,Asia/Yakutsk,FALSE +yakutsk standard time,Asia/Yakutsk,Asia/Yakutsk,FALSE +yekaterinburg,Asia/Yekaterinburg,Asia/Yekaterinburg,FALSE +yerevan,Asia/Yerevan,Asia/Yerevan,FALSE +yukon,America/Yakutat,America/Yakutat,FALSE +coordinated universal time-11,Pacific/Pago_Pago,,TRUE +aleutian islands,America/Adak,,TRUE +marquesas islands,Pacific/Marquesas,,TRUE +coordinated universal time-09,America/Anchorage,,TRUE +baja california,America/Tijuana,,TRUE +coordinated universal time-08,Pacific/Pitcairn,,TRUE +"chihuahua, la paz, mazatlan",America/Chihuahua,,TRUE +easter island,Pacific/Easter,,TRUE +"guadalajara, mexico city, monterrey",America/Mexico_City,,TRUE +"bogota, lima, quito, rio branco",America/Bogota,,TRUE +chetumal,America/Cancun,,TRUE +haiti,America/Port-au-Prince,,TRUE +havana,America/Havana,,TRUE +turks and caicos,America/Grand_Turk,,TRUE +asuncion,America/Asuncion,,TRUE +caracas,America/Caracas,,TRUE +cuiaba,America/Cuiaba,,TRUE +"georgetown, la paz, manaus, san juan",America/La_Paz,,TRUE +araguaina,America/Araguaina,,TRUE +"cayenne, fortaleza",America/Cayenne,,TRUE +city of buenos aires,America/Argentina/Buenos_Aires,,TRUE +punta arenas,America/Punta_Arenas,,TRUE +saint pierre and miquelon,America/Miquelon,,TRUE +salvador,America/Bahia,,TRUE +coordinated universal time-02,America/Noronha,,TRUE +mid-atlantic - old,America/Noronha,,TRUE +cabo verde is,Atlantic/Cape_Verde,,TRUE +coordinated universal time,UTC,,TRUE +"dublin, edinburgh, lisbon, london",Europe/London,,TRUE +"monrovia, reykjavik",Atlantic/Reykjavik,,TRUE +"belgrade, bratislava, budapest, ljubljana, prague",Europe/Budapest,,TRUE +casablanca,Africa/Casablanca,,TRUE +sao tome,Africa/Sao_Tome,,TRUE +"sarajevo, skopje, warsaw, zagreb",Europe/Warsaw,,TRUE +amman,Asia/Amman,,TRUE +"athens, bucharest",Europe/Bucharest,,TRUE +beirut,Asia/Beirut,,TRUE +chisinau,Europe/Chisinau,,TRUE +damascus,Asia/Damascus,,TRUE +"gaza, hebron",Asia/Hebron,,TRUE +jerusalem,Asia/Jerusalem,,TRUE +kaliningrad,Europe/Kaliningrad,,TRUE +khartoum,Africa/Khartoum,,TRUE +tripoli,Africa/Tripoli,,TRUE +windhoek,Africa/Windhoek,,TRUE +istanbul,Europe/Istanbul,,TRUE +"kuwait, riyadh",Asia/Riyadh,,TRUE +minsk,Europe/Minsk,,TRUE +"moscow, st petersburg",Europe/Moscow,,TRUE +nairobi,Africa/Nairobi,,TRUE +"astrakhan, ulyanovsk",Europe/Astrakhan,,TRUE +"izhevsk, samara",Europe/Samara,,TRUE +port louis,Indian/Mauritius,,TRUE +saratov,Europe/Saratov,,TRUE +"ashgabat, tashkent",Asia/Tashkent,,TRUE +"islamabad, karachi",Asia/Karachi,,TRUE +"chennai, kolkata, mumbai, new delhi",Asia/Kolkata,,TRUE +sri jayawardenepura,Asia/Colombo,,TRUE +kathmandu,Asia/Kathmandu,,TRUE +astana,Asia/Almaty,,TRUE +dhaka,Asia/Dhaka,,TRUE +yangon (rangoon),Indian/Cocos,Asia/Rangoon,TRUE +"barnaul, gorno-altaysk",Asia/Barnaul,,TRUE +tomsk,Asia/Tomsk,,TRUE +"beijing, chongqing, hong kong, urumqi",Asia/Shanghai,,TRUE +perth,Australia/Perth,,TRUE +ulaanbaatar,Asia/Ulaanbaatar,,TRUE +eucla,Australia/Eucla,,TRUE +chita,Asia/Chita,,TRUE +seoul,Asia/Seoul,,TRUE +adelaide,Australia/Adelaide,,TRUE +brisbane,Australia/Brisbane,,TRUE +"canberra, melbourne, sydney",Australia/Sydney,,TRUE +hobart,Australia/Hobart,,TRUE +lord howe island,Australia/Lord_Howe,,TRUE +bougainville island,Pacific/Bougainville,,TRUE +chokurdakh,Asia/Srednekolymsk,,TRUE +norfolk island,Pacific/Norfolk,,TRUE +"solomon is, new caledonia",Pacific/Guadalcanal,,TRUE +"anadyr, petropavlovsk-kamchatsky",Asia/Kamchatka,,TRUE +coordinated universal time+12,Pacific/Tarawa,,TRUE +petropavlovsk-kamchatsky - old,Asia/Anadyr,,TRUE +chatham islands,Pacific/Chatham,,TRUE +coordinated universal time+13,Pacific/Fakaofo,,TRUE +nuku'alofa,Pacific/Tongatapu,,TRUE +kiritimati island,Pacific/Kiritimati,,TRUE +"helsinki, kyiv, riga, sofia, tallinn, vilnius",Europe/Helsinki,,TRUE +"amsterdam, berlin, berne, rome, stockholm, vienne",Europe/Berlin,,TRUE \ No newline at end of file