From 25f894f3f50de30337198b8a39e158f66d0ce670 Mon Sep 17 00:00:00 2001 From: Giuseppe Arcuti Date: Thu, 27 Jun 2024 13:53:01 +0200 Subject: [PATCH] Initial commit on top of a5df3554edc8e8f9b3f976eba5416b7fb0a8fb0e --- .gitattributes | 1 + .gitignore | 2 +- CHANGELOG.md | 159 +++++ README.md | 25 +- bin/bench_freebusygenerator.php | 6 +- codecov.yml | 19 + composer.json | 2 +- lib/Component.php | 5 +- lib/Component/VCalendar.php | 150 +++-- lib/Component/VCard.php | 2 +- lib/DateTimeParser.php | 16 +- lib/Document.php | 17 +- lib/FreeBusyGenerator.php | 4 +- lib/ITip/Broker.php | 8 +- lib/Parameter.php | 1 + lib/Parser/MimeDir.php | 22 +- lib/Property.php | 2 +- lib/Property/ICalendar/DateTime.php | 9 +- lib/Property/Text.php | 2 +- lib/Reader.php | 5 + lib/Recur/EventIterator.php | 2 +- lib/Recur/RRuleIterator.php | 394 ++++++++++-- lib/Splitter/ICalendar.php | 2 +- lib/Splitter/VCard.php | 2 +- lib/TimeZoneUtil.php | 27 +- .../FindFromMzVersionTimezone.php | 39 ++ lib/TimezoneGuesser/FindFromOffsetName.php | 51 ++ lib/TimezoneGuesser/FindFromOutlookCities.php | 42 ++ .../FindFromTimezoneIdentifier.php | 16 +- lib/TimezoneGuesser/FindFromTimezoneMap.php | 17 +- .../GuessFromCustomizedTimeZone.php | 96 +++ lib/TimezoneGuesser/GuessFromLicEntry.php | 4 + .../LowercaseTimezoneIdentifier.php | 19 + lib/VCardConverter.php | 14 +- lib/Version.php | 2 +- lib/timezonedata/exchangezones.php | 5 +- lib/timezonedata/extrazones.php | 208 +++++++ lib/timezonedata/lotuszones.php | 2 +- lib/timezonedata/php-bc.php | 17 + lib/timezonedata/windowszones.php | 363 ++++++----- tests/VObject/Component/VCalendarTest.php | 4 +- tests/VObject/Component/VCardTest.php | 4 +- tests/VObject/Component/VFreeBusyTest.php | 3 +- tests/VObject/Component/VTimeZoneTest.php | 21 + tests/VObject/ComponentTest.php | 12 +- tests/VObject/EmptyParameterTest.php | 2 +- tests/VObject/FreeBusyGeneratorTest.php | 2 +- .../VObject/ITip/BrokerAttendeeReplyTest.php | 18 +- tests/VObject/InvalidValueParamTest.php | 54 ++ tests/VObject/JCardTest.php | 18 +- tests/VObject/Parser/JsonTest.php | 18 +- tests/VObject/Parser/UnfoldingTest.php | 99 +++ .../Property/ICalendar/DateTimeTest.php | 8 +- tests/VObject/PropertyTest.php | 2 +- tests/VObject/ReaderTest.php | 24 + .../VObject/Recur/EventIterator/MainTest.php | 2 +- tests/VObject/Recur/FastForwardBeforeTest.php | 570 ++++++++++++++++++ tests/VObject/Recur/FastForwardTest.php | 456 ++++++++++++++ tests/VObject/Recur/FastForwardToEndTest.php | 356 +++++++++++ tests/VObject/Recur/RRuleIteratorTest.php | 306 ++++++++-- tests/VObject/TimeZoneUtilTest.php | 391 +++++++++++- tests/VObject/VCardConverterTest.php | 8 +- .../microsoft-timezones-confluence.csv | 548 +++++++++++++++++ 63 files changed, 4229 insertions(+), 476 deletions(-) create mode 100644 codecov.yml create mode 100644 lib/TimezoneGuesser/FindFromMzVersionTimezone.php create mode 100644 lib/TimezoneGuesser/FindFromOffsetName.php create mode 100644 lib/TimezoneGuesser/FindFromOutlookCities.php create mode 100644 lib/TimezoneGuesser/GuessFromCustomizedTimeZone.php create mode 100644 lib/TimezoneGuesser/LowercaseTimezoneIdentifier.php create mode 100644 lib/timezonedata/extrazones.php create mode 100644 tests/VObject/InvalidValueParamTest.php create mode 100644 tests/VObject/Parser/UnfoldingTest.php create mode 100644 tests/VObject/Recur/FastForwardBeforeTest.php create mode 100644 tests/VObject/Recur/FastForwardTest.php create mode 100644 tests/VObject/Recur/FastForwardToEndTest.php create mode 100644 tests/VObject/microsoft-timezones-confluence.csv diff --git a/.gitattributes b/.gitattributes index c717ebe6a..602c23776 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/.gitignore b/.gitignore index d0e5169ac..d6e0a1d04 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 c7891d698..1c0ebb9d2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,165 @@ ChangeLog ========= +4.30.0 (2024-05-09) +* #103 Fix customized timezone guesser + +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 659e3fa83..cc5f34fb9 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/bin/bench_freebusygenerator.php b/bin/bench_freebusygenerator.php index 891335190..ae859f487 100644 --- a/bin/bench_freebusygenerator.php +++ b/bin/bench_freebusygenerator.php @@ -24,9 +24,9 @@ $bench->parse->stop(); $repeat = 100; -$start = new \DateTime('2000-01-01'); -$end = new \DateTime('2020-01-01'); -$timeZone = new \DateTimeZone('America/Toronto'); +$start = new DateTime('2000-01-01'); +$end = new DateTime('2020-01-01'); +$timeZone = new DateTimeZone('America/Toronto'); $bench->fb->start(); diff --git a/codecov.yml b/codecov.yml new file mode 100644 index 000000000..03a09f7d8 --- /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 67b8b26b0..263f6a166 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 da2c5ebd4..2bcf21917 100644 --- a/lib/Component.php +++ b/lib/Component.php @@ -2,7 +2,6 @@ namespace Sabre\VObject; -use Sabre\VObject; use Sabre\Xml; /** @@ -15,7 +14,7 @@ * @author Evert Pot (http://evertpot.com/) * @license http://sabre.io/license/ Modified BSD License * - * @property VObject\Property\FlatText UID + * @property Property\FlatText UID */ class Component extends Node { @@ -412,7 +411,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/Component/VCalendar.php b/lib/Component/VCalendar.php index b317e02c8..7b6e66e88 100644 --- a/lib/Component/VCalendar.php +++ b/lib/Component/VCalendar.php @@ -20,8 +20,8 @@ * * @property VEvent VEVENT * @property VJournal VJOURNAL - * @property VObject\Property\Text ORG - * @property VObject\Property\FlatText METHOD + * @property Property\Text ORG + * @property Property\FlatText METHOD */ class VCalendar extends VObject\Document { @@ -51,21 +51,21 @@ class VCalendar extends VObject\Document * List of value-types, and which classes they map to. */ public static array $valueMap = [ - 'BINARY' => VObject\Property\Binary::class, - 'BOOLEAN' => VObject\Property\Boolean::class, - 'CAL-ADDRESS' => VObject\Property\ICalendar\CalAddress::class, - 'DATE' => VObject\Property\ICalendar\Date::class, - 'DATE-TIME' => VObject\Property\ICalendar\DateTime::class, - 'DURATION' => VObject\Property\ICalendar\Duration::class, - 'FLOAT' => VObject\Property\FloatValue::class, - 'INTEGER' => VObject\Property\IntegerValue::class, - 'PERIOD' => VObject\Property\ICalendar\Period::class, - 'RECUR' => VObject\Property\ICalendar\Recur::class, - 'TEXT' => VObject\Property\Text::class, - 'TIME' => VObject\Property\Time::class, - 'UNKNOWN' => VObject\Property\Unknown::class, // jCard / jCal-only. - 'URI' => VObject\Property\Uri::class, - 'UTC-OFFSET' => VObject\Property\UtcOffset::class, + 'BINARY' => Property\Binary::class, + 'BOOLEAN' => Property\Boolean::class, + 'CAL-ADDRESS' => Property\ICalendar\CalAddress::class, + 'DATE' => Property\ICalendar\Date::class, + 'DATE-TIME' => Property\ICalendar\DateTime::class, + 'DURATION' => Property\ICalendar\Duration::class, + 'FLOAT' => Property\FloatValue::class, + 'INTEGER' => Property\IntegerValue::class, + 'PERIOD' => Property\ICalendar\Period::class, + 'RECUR' => Property\ICalendar\Recur::class, + 'TEXT' => Property\Text::class, + 'TIME' => Property\Time::class, + 'UNKNOWN' => Property\Unknown::class, // jCard / jCal-only. + 'URI' => Property\Uri::class, + 'UTC-OFFSET' => Property\UtcOffset::class, ]; /** @@ -73,78 +73,78 @@ class VCalendar extends VObject\Document */ public static array $propertyMap = [ // Calendar properties - 'CALSCALE' => VObject\Property\FlatText::class, - 'METHOD' => VObject\Property\FlatText::class, - 'PRODID' => VObject\Property\FlatText::class, - 'VERSION' => VObject\Property\FlatText::class, + 'CALSCALE' => Property\FlatText::class, + 'METHOD' => Property\FlatText::class, + 'PRODID' => Property\FlatText::class, + 'VERSION' => Property\FlatText::class, // Component properties - 'ATTACH' => VObject\Property\Uri::class, - 'CATEGORIES' => VObject\Property\Text::class, - 'CLASS' => VObject\Property\FlatText::class, - 'COMMENT' => VObject\Property\FlatText::class, - 'DESCRIPTION' => VObject\Property\FlatText::class, - 'GEO' => VObject\Property\FloatValue::class, - 'LOCATION' => VObject\Property\FlatText::class, - 'PERCENT-COMPLETE' => VObject\Property\IntegerValue::class, - 'PRIORITY' => VObject\Property\IntegerValue::class, - 'RESOURCES' => VObject\Property\Text::class, - 'STATUS' => VObject\Property\FlatText::class, - 'SUMMARY' => VObject\Property\FlatText::class, + 'ATTACH' => Property\Uri::class, + 'CATEGORIES' => Property\Text::class, + 'CLASS' => Property\FlatText::class, + 'COMMENT' => Property\FlatText::class, + 'DESCRIPTION' => Property\FlatText::class, + 'GEO' => Property\FloatValue::class, + 'LOCATION' => Property\FlatText::class, + 'PERCENT-COMPLETE' => Property\IntegerValue::class, + 'PRIORITY' => Property\IntegerValue::class, + 'RESOURCES' => Property\Text::class, + 'STATUS' => Property\FlatText::class, + 'SUMMARY' => Property\FlatText::class, // Date and Time Component Properties - 'COMPLETED' => VObject\Property\ICalendar\DateTime::class, - 'DTEND' => VObject\Property\ICalendar\DateTime::class, - 'DUE' => VObject\Property\ICalendar\DateTime::class, - 'DTSTART' => VObject\Property\ICalendar\DateTime::class, - 'DURATION' => VObject\Property\ICalendar\Duration::class, - 'FREEBUSY' => VObject\Property\ICalendar\Period::class, - 'TRANSP' => VObject\Property\FlatText::class, + 'COMPLETED' => Property\ICalendar\DateTime::class, + 'DTEND' => Property\ICalendar\DateTime::class, + 'DUE' => Property\ICalendar\DateTime::class, + 'DTSTART' => Property\ICalendar\DateTime::class, + 'DURATION' => Property\ICalendar\Duration::class, + 'FREEBUSY' => Property\ICalendar\Period::class, + 'TRANSP' => Property\FlatText::class, // Time Zone Component Properties - 'TZID' => VObject\Property\FlatText::class, - 'TZNAME' => VObject\Property\FlatText::class, - 'TZOFFSETFROM' => VObject\Property\UtcOffset::class, - 'TZOFFSETTO' => VObject\Property\UtcOffset::class, - 'TZURL' => VObject\Property\Uri::class, + 'TZID' => Property\FlatText::class, + 'TZNAME' => Property\FlatText::class, + 'TZOFFSETFROM' => Property\UtcOffset::class, + 'TZOFFSETTO' => Property\UtcOffset::class, + 'TZURL' => Property\Uri::class, // Relationship Component Properties - 'ATTENDEE' => VObject\Property\ICalendar\CalAddress::class, - 'CONTACT' => VObject\Property\FlatText::class, - 'ORGANIZER' => VObject\Property\ICalendar\CalAddress::class, - 'RECURRENCE-ID' => VObject\Property\ICalendar\DateTime::class, - 'RELATED-TO' => VObject\Property\FlatText::class, - 'URL' => VObject\Property\Uri::class, - 'UID' => VObject\Property\FlatText::class, + 'ATTENDEE' => Property\ICalendar\CalAddress::class, + 'CONTACT' => Property\FlatText::class, + 'ORGANIZER' => Property\ICalendar\CalAddress::class, + 'RECURRENCE-ID' => Property\ICalendar\DateTime::class, + 'RELATED-TO' => Property\FlatText::class, + 'URL' => Property\Uri::class, + 'UID' => Property\FlatText::class, // Recurrence Component Properties - 'EXDATE' => VObject\Property\ICalendar\DateTime::class, - 'RDATE' => VObject\Property\ICalendar\DateTime::class, - 'RRULE' => VObject\Property\ICalendar\Recur::class, - 'EXRULE' => VObject\Property\ICalendar\Recur::class, // Deprecated since rfc5545 + 'EXDATE' => Property\ICalendar\DateTime::class, + 'RDATE' => Property\ICalendar\DateTime::class, + 'RRULE' => Property\ICalendar\Recur::class, + 'EXRULE' => Property\ICalendar\Recur::class, // Deprecated since rfc5545 // Alarm Component Properties - 'ACTION' => VObject\Property\FlatText::class, - 'REPEAT' => VObject\Property\IntegerValue::class, - 'TRIGGER' => VObject\Property\ICalendar\Duration::class, + 'ACTION' => Property\FlatText::class, + 'REPEAT' => Property\IntegerValue::class, + 'TRIGGER' => Property\ICalendar\Duration::class, // Change Management Component Properties - 'CREATED' => VObject\Property\ICalendar\DateTime::class, - 'DTSTAMP' => VObject\Property\ICalendar\DateTime::class, - 'LAST-MODIFIED' => VObject\Property\ICalendar\DateTime::class, - 'SEQUENCE' => VObject\Property\IntegerValue::class, + 'CREATED' => Property\ICalendar\DateTime::class, + 'DTSTAMP' => Property\ICalendar\DateTime::class, + 'LAST-MODIFIED' => Property\ICalendar\DateTime::class, + 'SEQUENCE' => Property\IntegerValue::class, // Request Status - 'REQUEST-STATUS' => VObject\Property\Text::class, + 'REQUEST-STATUS' => Property\Text::class, // Additions from draft-daboo-valarm-extensions-04 - 'ALARM-AGENT' => VObject\Property\Text::class, - 'ACKNOWLEDGED' => VObject\Property\ICalendar\DateTime::class, - 'PROXIMITY' => VObject\Property\Text::class, - 'DEFAULT-ALARM' => VObject\Property\Boolean::class, + 'ALARM-AGENT' => Property\Text::class, + 'ACKNOWLEDGED' => Property\ICalendar\DateTime::class, + 'PROXIMITY' => Property\Text::class, + 'DEFAULT-ALARM' => Property\Boolean::class, // Additions from draft-daboo-calendar-availability-05 - 'BUSYTYPE' => VObject\Property\Text::class, + 'BUSYTYPE' => Property\Text::class, ]; /** @@ -166,10 +166,10 @@ public function getDocumentType(): int * * @return VObject\Component[] */ - public function getBaseComponents(string $componentName = null): array + public function getBaseComponents(?string $componentName = null): array { $isBaseComponent = function ($component): bool { - if (!$component instanceof VObject\Component) { + if (!$component instanceof Component) { return false; } if ('VTIMEZONE' === $component->name) { @@ -214,13 +214,11 @@ public function getBaseComponents(string $componentName = null): array * If there is no such component, null will be returned. * * @param string|null $componentName filter by component name - * - * @return VObject\Component|null */ - public function getBaseComponent(string $componentName = null): ?Component + public function getBaseComponent(?string $componentName = null): ?Component { $isBaseComponent = function ($component): bool { - if (!$component instanceof VObject\Component) { + if (!$component instanceof Component) { return false; } if ('VTIMEZONE' === $component->name) { @@ -275,7 +273,7 @@ public function getBaseComponent(string $componentName = null): ?Component * @throws InvalidDataException * @throws VObject\Recur\MaxInstancesExceededException */ - public function expand(\DateTimeInterface $start, \DateTimeInterface $end, \DateTimeZone $timeZone = null): VCalendar + public function expand(\DateTimeInterface $start, \DateTimeInterface $end, ?\DateTimeZone $timeZone = null): VCalendar { $newChildren = []; $recurringEvents = []; diff --git a/lib/Component/VCard.php b/lib/Component/VCard.php index 0f2a5e266..6bbec5717 100644 --- a/lib/Component/VCard.php +++ b/lib/Component/VCard.php @@ -82,7 +82,7 @@ class VCard extends VObject\Document 'ROLE' => VObject\Property\FlatText::class, 'LOGO' => VObject\Property\Binary::class, // 'AGENT' => 'Sabre\\VObject\\Property\\', // Todo: is an embedded vCard. Probably rare, so - // not supported at the moment + // not supported at the moment 'ORG' => VObject\Property\Text::class, 'NOTE' => VObject\Property\FlatText::class, 'REV' => VObject\Property\VCard\TimeStamp::class, diff --git a/lib/DateTimeParser.php b/lib/DateTimeParser.php index 5ce9f207a..a8130b8bc 100644 --- a/lib/DateTimeParser.php +++ b/lib/DateTimeParser.php @@ -27,7 +27,7 @@ class DateTimeParser * * @throws InvalidDataException */ - public static function parseDateTime(string $dt, \DateTimeZone $tz = null): \DateTimeImmutable + public static function parseDateTime(string $dt, ?\DateTimeZone $tz = null): \DateTimeImmutable { // Format is YYYYMMDD + "T" + hhmmss $result = preg_match('/^([0-9]{4})([0-1][0-9])([0-3][0-9])T([0-2][0-9])([0-5][0-9])([0-5][0-9])([Z]?)$/', $dt, $matches); @@ -54,7 +54,7 @@ public static function parseDateTime(string $dt, \DateTimeZone $tz = null): \Dat * * @throws InvalidDataException */ - public static function parseDate(string $date, \DateTimeZone $tz = null): \DateTimeImmutable + public static function parseDate(string $date, ?\DateTimeZone $tz = null): \DateTimeImmutable { // Format is YYYYMMDD $result = preg_match('/^([0-9]{4})([0-1][0-9])([0-3][0-9])$/', $date, $matches); @@ -98,12 +98,12 @@ public static function parseDuration(string $duration): \DateInterval } $parts = [ - 'week', - 'day', - 'hour', - 'minute', - 'second', - ]; + 'week', + 'day', + 'hour', + 'minute', + 'second', + ]; foreach ($parts as $part) { $matches[$part] = isset($matches[$part]) && $matches[$part] ? (int) $matches[$part] : 0; diff --git a/lib/Document.php b/lib/Document.php index b311987d7..fa789a8de 100644 --- a/lib/Document.php +++ b/lib/Document.php @@ -2,8 +2,6 @@ namespace Sabre\VObject; -use Sabre\VObject; - /** * Document. * @@ -18,7 +16,7 @@ * @author Evert Pot (http://evertpot.com/) * @license http://sabre.io/license/ Modified BSD License * - * @property VObject\Property\FlatText VERSION + * @property Property\FlatText VERSION */ abstract class Document extends Component { @@ -140,7 +138,7 @@ public function create(string $name) * an iCalendar object, this may be something like CALSCALE:GREGORIAN. To * ensure that this does not happen, set $defaults to false. */ - public function createComponent(string $name, array $children = null, bool $defaults = true): Component + public function createComponent(string $name, ?array $children = null, bool $defaults = true): Component { $name = strtoupper($name); $class = Component::class; @@ -169,7 +167,7 @@ public function createComponent(string $name, array $children = null, bool $defa * * @throws InvalidDataException */ - public function createProperty(string $name, $value = null, array $parameters = null, string $valueType = null): Property + public function createProperty(string $name, $value = null, ?array $parameters = null, ?string $valueType = null): Property { // If there's a . in the name, it means it's prefixed by a group name. if (false !== ($i = strpos($name, '.'))) { @@ -191,9 +189,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/FreeBusyGenerator.php b/lib/FreeBusyGenerator.php index 2f222e4e5..424826034 100644 --- a/lib/FreeBusyGenerator.php +++ b/lib/FreeBusyGenerator.php @@ -72,7 +72,7 @@ class FreeBusyGenerator * Check the setTimeRange and setObjects methods for details about the * arguments. */ - public function __construct(\DateTimeInterface $start = null, \DateTimeInterface $end = null, $objects = null, \DateTimeZone $timeZone = null) + public function __construct(?\DateTimeInterface $start = null, ?\DateTimeInterface $end = null, $objects = null, ?\DateTimeZone $timeZone = null) { $this->setTimeRange($start, $end); @@ -138,7 +138,7 @@ public function setObjects($objects): void * * @throws \Exception */ - public function setTimeRange(\DateTimeInterface $start = null, \DateTimeInterface $end = null): void + public function setTimeRange(?\DateTimeInterface $start = null, ?\DateTimeInterface $end = null): void { if (!$start) { $start = new \DateTimeImmutable(Settings::$minDate); diff --git a/lib/ITip/Broker.php b/lib/ITip/Broker.php index f792fd990..9cbc3f72b 100644 --- a/lib/ITip/Broker.php +++ b/lib/ITip/Broker.php @@ -113,7 +113,7 @@ class Broker * @throws MaxInstancesExceededException * @throws NoInstancesException */ - public function processMessage(Message $itipMessage, VCalendar $existingObject = null) + public function processMessage(Message $itipMessage, ?VCalendar $existingObject = null) { // We only support events at the moment. if ('VEVENT' !== $itipMessage->component) { @@ -268,7 +268,7 @@ public function parseEvent($calendar, $userHref, $oldCalendar = null): array * This is message from an organizer, and is either a new event * invite, or an update to an existing one. */ - protected function processMessageRequest(Message $itipMessage, VCalendar $existingObject = null): ?VCalendar + protected function processMessageRequest(Message $itipMessage, ?VCalendar $existingObject = null): ?VCalendar { if (!$existingObject) { // This is a new invite, and we're just going to copy over @@ -296,7 +296,7 @@ protected function processMessageRequest(Message $itipMessage, VCalendar $existi * attendee got removed from an event, or an event got cancelled * altogether. */ - protected function processMessageCancel(Message $itipMessage, VCalendar $existingObject = null): ?VCalendar + protected function processMessageCancel(Message $itipMessage, ?VCalendar $existingObject = null): ?VCalendar { if (!$existingObject) { // The event didn't exist in the first place, so we're just @@ -321,7 +321,7 @@ protected function processMessageCancel(Message $itipMessage, VCalendar $existin * @throws MaxInstancesExceededException * @throws NoInstancesException */ - protected function processMessageReply(Message $itipMessage, VCalendar $existingObject = null): ?VCalendar + protected function processMessageReply(Message $itipMessage, ?VCalendar $existingObject = null): ?VCalendar { // A reply can only be processed based on an existing object. // If the object is not available, the reply is ignored. diff --git a/lib/Parameter.php b/lib/Parameter.php index 1900cb634..1ef7da88a 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 bbef0ea6b..1ba2f1c59 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.php b/lib/Property.php index 53a252fb2..574099e65 100644 --- a/lib/Property.php +++ b/lib/Property.php @@ -61,7 +61,7 @@ abstract class Property extends Node * @param array $parameters List of parameters * @param string|null $group The vcard property group */ - public function __construct(Component $root, ?string $name, $value = null, array $parameters = [], string $group = null) + public function __construct(Component $root, ?string $name, $value = null, array $parameters = [], ?string $group = null) { $this->name = $name; $this->group = $group; diff --git a/lib/Property/ICalendar/DateTime.php b/lib/Property/ICalendar/DateTime.php index 7ee907972..b506bc6be 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/Property/Text.php b/lib/Property/Text.php index c606ad812..c59029059 100644 --- a/lib/Property/Text.php +++ b/lib/Property/Text.php @@ -64,7 +64,7 @@ class Text extends Property * @param array $parameters List of parameters * @param string|null $group The vcard property group */ - public function __construct(Component $root, string $name, $value = null, array $parameters = [], string $group = null) + public function __construct(Component $root, string $name, $value = null, array $parameters = [], ?string $group = null) { // There's two types of multi-valued text properties: // 1. multivalue properties. diff --git a/lib/Reader.php b/lib/Reader.php index 353186920..fc9c58054 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/EventIterator.php b/lib/Recur/EventIterator.php index d79cf3132..2c93bcf4e 100644 --- a/lib/Recur/EventIterator.php +++ b/lib/Recur/EventIterator.php @@ -89,7 +89,7 @@ class EventIterator implements \Iterator * @throws NoInstancesException * @throws InvalidDataException */ - public function __construct($input, string $uid = null, \DateTimeZone $timeZone = null) + public function __construct($input, ?string $uid = null, ?\DateTimeZone $timeZone = null) { if (is_null($timeZone)) { $timeZone = new \DateTimeZone('UTC'); diff --git a/lib/Recur/RRuleIterator.php b/lib/Recur/RRuleIterator.php index 888556eea..ecf1affeb 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/Splitter/ICalendar.php b/lib/Splitter/ICalendar.php index ccf0d1add..79f0bd443 100644 --- a/lib/Splitter/ICalendar.php +++ b/lib/Splitter/ICalendar.php @@ -46,7 +46,7 @@ public function __construct($input, int $options = 0) { $data = VObject\Reader::read($input, $options); - if (!$data instanceof VObject\Component\VCalendar) { + if (!$data instanceof VCalendar) { throw new VObject\ParseException('Supplied input could not be parsed as VCALENDAR.'); } diff --git a/lib/Splitter/VCard.php b/lib/Splitter/VCard.php index e63ae837d..40775a335 100644 --- a/lib/Splitter/VCard.php +++ b/lib/Splitter/VCard.php @@ -61,7 +61,7 @@ public function getNext(): ?Component try { $object = $this->parser->parse(); - if (!$object instanceof VObject\Component\VCard) { + if (!$object instanceof Component\VCard) { throw new VObject\ParseException('The supplied input contained non-VCARD data.'); } } catch (VObject\EofException $e) { diff --git a/lib/TimeZoneUtil.php b/lib/TimeZoneUtil.php index 8c2a374fb..b75a6310d 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 000000000..5abdc1484 --- /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 000000000..0916f1283 --- /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 000000000..657767a37 --- /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 83466b28e..1fe680230 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 000000000..f1f4f6899 --- /dev/null +++ b/lib/TimezoneGuesser/GuessFromCustomizedTimeZone.php @@ -0,0 +1,96 @@ +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(); + + $day = 24 * 60 * 60; + foreach ($timezones as $timezone) { + $tz = new \DateTimeZone($timezone); + // check standard + $timestamp = $standardIterator->current()->getTimestamp(); + $transitions = $tz->getTransitions($timestamp + $day, $timestamp + $day + 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 + $day, $timestamp + $day + 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 486b33796..fbd846e0f 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 000000000..a6887b941 --- /dev/null +++ b/lib/TimezoneGuesser/LowercaseTimezoneIdentifier.php @@ -0,0 +1,19 @@ +name, ['PHOTO', 'LOGO', 'SOUND'])) { - /** @var Property\Uri $newProperty */ + if ($property instanceof Uri && in_array($property->name, ['PHOTO', 'LOGO', 'SOUND'])) { + /** @var Uri $newProperty */ $newProperty = $this->convertUriToBinary($output, $newProperty); } elseif ($property instanceof Property\VCard\DateAndOrTime) { // In vCard 4, the birth year may be optional. This is not the @@ -153,8 +153,8 @@ protected function convertProperty(Component\VCard $input, Component\VCard $outp return; } - if ($property instanceof Property\Binary) { - /** @var Property\Binary $newProperty */ + if ($property instanceof Binary) { + /** @var Binary $newProperty */ $newProperty = $this->convertBinaryToUri($output, $newProperty, $parameters); } elseif ($property instanceof Property\VCard\DateAndOrTime && isset($parameters['X-APPLE-OMIT-YEAR'])) { // If a property such as BDAY contained 'X-APPLE-OMIT-YEAR', @@ -256,7 +256,7 @@ protected function convertProperty(Component\VCard $input, Component\VCard $outp * * @throws InvalidDataException */ - protected function convertBinaryToUri(Component\VCard $output, Property\Binary $newProperty, array &$parameters): Uri + protected function convertBinaryToUri(Component\VCard $output, Binary $newProperty, array &$parameters): Uri { $value = $newProperty->getValue(); /** @var Uri $newProperty */ @@ -304,11 +304,11 @@ protected function convertBinaryToUri(Component\VCard $output, Property\Binary $ * be valid in vCard 3.0 as well, we should convert those to BINARY if * possible, to improve compatibility. * - * @return Property\Binary|Property\Uri|null + * @return Binary|Uri|null * * @throws InvalidDataException */ - protected function convertUriToBinary(Component\VCard $output, Property\Uri $newProperty): Property + protected function convertUriToBinary(Component\VCard $output, Uri $newProperty): Property { $value = $newProperty->getValue(); diff --git a/lib/Version.php b/lib/Version.php index 893f272d3..7fef9c74f 100644 --- a/lib/Version.php +++ b/lib/Version.php @@ -14,5 +14,5 @@ class Version /** * Full version number. */ - public const VERSION = '4.5.2'; + public const VERSION = '4.30.0'; } diff --git a/lib/timezonedata/exchangezones.php b/lib/timezonedata/exchangezones.php index 89bddc27c..9a92cc433 100644 --- a/lib/timezonedata/exchangezones.php +++ b/lib/timezonedata/exchangezones.php @@ -71,7 +71,7 @@ 'Mid-Atlantic' => '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 000000000..8e477eb99 --- /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 9115ac743..684a0c972 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 3116c6868..bd60f26dc 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 2049a95c1..209a9141b 100644 --- a/lib/timezonedata/windowszones.php +++ b/lib/timezonedata/windowszones.php @@ -11,142 +11,229 @@ */ return [ - 'AUS Central Standard Time' => 'Australia/Darwin', - 'AUS Eastern Standard Time' => 'Australia/Sydney', - 'Afghanistan Standard Time' => 'Asia/Kabul', - 'Alaskan Standard Time' => 'America/Anchorage', - 'Aleutian Standard Time' => 'America/Adak', - 'Altai Standard Time' => 'Asia/Barnaul', - 'Arab Standard Time' => 'Asia/Riyadh', - 'Arabian Standard Time' => 'Asia/Dubai', - 'Arabic Standard Time' => 'Asia/Baghdad', - 'Argentina Standard Time' => 'America/Buenos_Aires', - 'Astrakhan Standard Time' => 'Europe/Astrakhan', - 'Atlantic Standard Time' => 'America/Halifax', - 'Aus Central W. Standard Time' => 'Australia/Eucla', - 'Azerbaijan Standard Time' => 'Asia/Baku', - 'Azores Standard Time' => 'Atlantic/Azores', - 'Bahia Standard Time' => 'America/Bahia', - 'Bangladesh Standard Time' => 'Asia/Dhaka', - 'Belarus Standard Time' => 'Europe/Minsk', - 'Bougainville Standard Time' => 'Pacific/Bougainville', - 'Canada Central Standard Time' => 'America/Regina', - 'Cape Verde Standard Time' => 'Atlantic/Cape_Verde', - 'Caucasus Standard Time' => 'Asia/Yerevan', - 'Cen. Australia Standard Time' => 'Australia/Adelaide', - 'Central America Standard Time' => 'America/Guatemala', - 'Central Asia Standard Time' => 'Asia/Almaty', - 'Central Brazilian Standard Time' => 'America/Cuiaba', - 'Central Europe Standard Time' => 'Europe/Budapest', - 'Central European Standard Time' => 'Europe/Warsaw', - 'Central Pacific Standard Time' => 'Pacific/Guadalcanal', - 'Central Standard Time' => 'America/Chicago', - 'Central Standard Time (Mexico)' => 'America/Mexico_City', - 'Chatham Islands Standard Time' => 'Pacific/Chatham', - 'China Standard Time' => 'Asia/Shanghai', - 'Cuba Standard Time' => 'America/Havana', - 'Dateline Standard Time' => 'Etc/GMT+12', - 'E. Africa Standard Time' => 'Africa/Nairobi', - 'E. Australia Standard Time' => 'Australia/Brisbane', - 'E. Europe Standard Time' => 'Europe/Chisinau', - 'E. South America Standard Time' => 'America/Sao_Paulo', - 'Easter Island Standard Time' => 'Pacific/Easter', - 'Eastern Standard Time' => 'America/New_York', - 'Eastern Standard Time (Mexico)' => 'America/Cancun', - 'Egypt Standard Time' => 'Africa/Cairo', - 'Ekaterinburg Standard Time' => 'Asia/Yekaterinburg', - 'FLE Standard Time' => 'Europe/Kiev', - 'Fiji Standard Time' => 'Pacific/Fiji', - 'GMT Standard Time' => 'Europe/London', - 'GTB Standard Time' => 'Europe/Bucharest', - 'Georgian Standard Time' => 'Asia/Tbilisi', - 'Greenland Standard Time' => 'America/Godthab', - 'Greenwich Standard Time' => 'Atlantic/Reykjavik', - 'Haiti Standard Time' => 'America/Port-au-Prince', - 'Hawaiian Standard Time' => 'Pacific/Honolulu', - 'India Standard Time' => 'Asia/Calcutta', - 'Iran Standard Time' => 'Asia/Tehran', - 'Israel Standard Time' => 'Asia/Jerusalem', - 'Jordan Standard Time' => 'Asia/Amman', - 'Kaliningrad Standard Time' => 'Europe/Kaliningrad', - 'Korea Standard Time' => 'Asia/Seoul', - 'Libya Standard Time' => 'Africa/Tripoli', - 'Line Islands Standard Time' => 'Pacific/Kiritimati', - 'Lord Howe Standard Time' => 'Australia/Lord_Howe', - 'Magadan Standard Time' => 'Asia/Magadan', - 'Magallanes Standard Time' => 'America/Punta_Arenas', - 'Marquesas Standard Time' => 'Pacific/Marquesas', - 'Mauritius Standard Time' => 'Indian/Mauritius', - 'Middle East Standard Time' => 'Asia/Beirut', - 'Montevideo Standard Time' => 'America/Montevideo', - 'Morocco Standard Time' => 'Africa/Casablanca', - 'Mountain Standard Time' => 'America/Denver', - 'Mountain Standard Time (Mexico)' => 'America/Chihuahua', - 'Myanmar Standard Time' => 'Asia/Rangoon', - 'N. Central Asia Standard Time' => 'Asia/Novosibirsk', - 'Namibia Standard Time' => 'Africa/Windhoek', - 'Nepal Standard Time' => 'Asia/Katmandu', - 'New Zealand Standard Time' => 'Pacific/Auckland', - 'Newfoundland Standard Time' => 'America/St_Johns', - 'Norfolk Standard Time' => 'Pacific/Norfolk', - 'North Asia East Standard Time' => 'Asia/Irkutsk', - 'North Asia Standard Time' => 'Asia/Krasnoyarsk', - 'North Korea Standard Time' => 'Asia/Pyongyang', - 'Omsk Standard Time' => 'Asia/Omsk', - 'Pacific SA Standard Time' => 'America/Santiago', - 'Pacific Standard Time' => 'America/Los_Angeles', - 'Pacific Standard Time (Mexico)' => 'America/Tijuana', - 'Pakistan Standard Time' => 'Asia/Karachi', - 'Paraguay Standard Time' => 'America/Asuncion', - 'Qyzylorda Standard Time' => 'Asia/Qyzylorda', - 'Romance Standard Time' => 'Europe/Paris', - 'Russia Time Zone 10' => 'Asia/Srednekolymsk', - 'Russia Time Zone 11' => 'Asia/Kamchatka', - 'Russia Time Zone 3' => 'Europe/Samara', - 'Russian Standard Time' => 'Europe/Moscow', - 'SA Eastern Standard Time' => 'America/Cayenne', - 'SA Pacific Standard Time' => 'America/Bogota', - 'SA Western Standard Time' => 'America/La_Paz', - 'SE Asia Standard Time' => 'Asia/Bangkok', - 'Saint Pierre Standard Time' => 'America/Miquelon', - 'Sakhalin Standard Time' => 'Asia/Sakhalin', - 'Samoa Standard Time' => 'Pacific/Apia', - 'Sao Tome Standard Time' => 'Africa/Sao_Tome', - 'Saratov Standard Time' => 'Europe/Saratov', - 'Singapore Standard Time' => 'Asia/Singapore', - 'South Africa Standard Time' => 'Africa/Johannesburg', - 'Sri Lanka Standard Time' => 'Asia/Colombo', - 'Sudan Standard Time' => 'Africa/Khartoum', - 'Syria Standard Time' => 'Asia/Damascus', - 'Taipei Standard Time' => 'Asia/Taipei', - 'Tasmania Standard Time' => 'Australia/Hobart', - 'Tocantins Standard Time' => 'America/Araguaina', - 'Tokyo Standard Time' => 'Asia/Tokyo', - 'Tomsk Standard Time' => 'Asia/Tomsk', - 'Tonga Standard Time' => 'Pacific/Tongatapu', - 'Transbaikal Standard Time' => 'Asia/Chita', - 'Turkey Standard Time' => 'Europe/Istanbul', - 'Turks And Caicos Standard Time' => 'America/Grand_Turk', - 'US Eastern Standard Time' => 'America/Indianapolis', - 'US Mountain Standard Time' => 'America/Phoenix', - 'UTC' => 'Etc/GMT', - 'UTC+12' => 'Etc/GMT-12', - 'UTC+13' => 'Etc/GMT-13', - 'UTC-02' => 'Etc/GMT+2', - 'UTC-08' => 'Etc/GMT+8', - 'UTC-09' => 'Etc/GMT+9', - 'UTC-11' => 'Etc/GMT+11', - 'Ulaanbaatar Standard Time' => 'Asia/Ulaanbaatar', - 'Venezuela Standard Time' => 'America/Caracas', - 'Vladivostok Standard Time' => 'Asia/Vladivostok', - 'Volgograd Standard Time' => 'Europe/Volgograd', - 'W. Australia Standard Time' => 'Australia/Perth', - 'W. Central Africa Standard Time' => 'Africa/Lagos', - 'W. Europe Standard Time' => 'Europe/Berlin', - 'W. Mongolia Standard Time' => 'Asia/Hovd', - 'West Asia Standard Time' => 'Asia/Tashkent', - 'West Bank Standard Time' => 'Asia/Hebron', - 'West Pacific Standard Time' => 'Pacific/Port_Moresby', - 'Yakutsk Standard Time' => 'Asia/Yakutsk', - 'Yukon Standard Time' => 'America/Whitehorse', + 'AUS Central Standard Time' => 'Australia/Darwin', + 'AUS Eastern Standard Time' => 'Australia/Sydney', + 'Afghanistan Standard Time' => 'Asia/Kabul', + 'Alaskan Standard Time' => 'America/Anchorage', + 'Aleutian Standard Time' => 'America/Adak', + 'Altai Standard Time' => 'Asia/Barnaul', + 'Arab Standard Time' => 'Asia/Riyadh', + 'Arabian Standard Time' => 'Asia/Dubai', + 'Arabic Standard Time' => 'Asia/Baghdad', + 'Argentina Standard Time' => 'America/Buenos_Aires', + 'Astrakhan Standard Time' => 'Europe/Astrakhan', + 'Atlantic Standard Time' => 'America/Halifax', + 'Aus Central W. Standard Time' => 'Australia/Eucla', + 'Azerbaijan Standard Time' => 'Asia/Baku', + 'Azores Standard Time' => 'Atlantic/Azores', + 'Bahia Standard Time' => 'America/Bahia', + 'Bangladesh Standard Time' => 'Asia/Dhaka', + 'Belarus Standard Time' => 'Europe/Minsk', + 'Bougainville Standard Time' => 'Pacific/Bougainville', + 'Canada Central Standard Time' => 'America/Regina', + 'Cape Verde Standard Time' => 'Atlantic/Cape_Verde', + 'Caucasus Standard Time' => 'Asia/Yerevan', + 'Cen. Australia Standard Time' => 'Australia/Adelaide', + 'Central America Standard Time' => 'America/Guatemala', + 'Central Asia Standard Time' => 'Asia/Almaty', + 'Central Brazilian Standard Time' => 'America/Cuiaba', + 'Central Europe Standard Time' => 'Europe/Budapest', + 'Central European Standard Time' => 'Europe/Warsaw', + 'Central Pacific Standard Time' => 'Pacific/Guadalcanal', + 'Central Standard Time' => 'America/Chicago', + 'Central Standard Time (Mexico)' => 'America/Mexico_City', + 'Chatham Islands Standard Time' => 'Pacific/Chatham', + 'China Standard Time' => 'Asia/Shanghai', + 'Cuba Standard Time' => 'America/Havana', + 'Dateline Standard Time' => 'Etc/GMT+12', + 'E. Africa Standard Time' => 'Africa/Nairobi', + 'E. Australia Standard Time' => 'Australia/Brisbane', + 'E. Europe Standard Time' => 'Europe/Chisinau', + 'E. South America Standard Time' => 'America/Sao_Paulo', + 'Easter Island Standard Time' => 'Pacific/Easter', + 'Eastern Standard Time' => 'America/New_York', + 'Eastern Standard Time (Mexico)' => 'America/Cancun', + 'Egypt Standard Time' => 'Africa/Cairo', + 'Ekaterinburg Standard Time' => 'Asia/Yekaterinburg', + 'FLE Standard Time' => 'Europe/Kiev', + 'Fiji Standard Time' => 'Pacific/Fiji', + 'GMT Standard Time' => 'Europe/London', + 'GTB Standard Time' => 'Europe/Bucharest', + 'Georgian Standard Time' => 'Asia/Tbilisi', + 'Greenland Standard Time' => 'Atlantic/Stanley', + 'Greenwich Standard Time' => 'Atlantic/Reykjavik', + 'Haiti Standard Time' => 'America/Port-au-Prince', + 'Hawaiian Standard Time' => 'Pacific/Honolulu', + 'India Standard Time' => 'Asia/Calcutta', + 'Iran Standard Time' => 'Asia/Tehran', + 'Israel Standard Time' => 'Asia/Jerusalem', + 'Jordan Standard Time' => 'Asia/Amman', + 'Kaliningrad Standard Time' => 'Europe/Kaliningrad', + 'Korea Standard Time' => 'Asia/Seoul', + 'Libya Standard Time' => 'Africa/Tripoli', + 'Line Islands Standard Time' => 'Pacific/Kiritimati', + 'Lord Howe Standard Time' => 'Australia/Lord_Howe', + 'Magadan Standard Time' => 'Asia/Magadan', + 'Magallanes Standard Time' => 'America/Punta_Arenas', + 'Marquesas Standard Time' => 'Pacific/Marquesas', + 'Mauritius Standard Time' => 'Indian/Mauritius', + 'Middle East Standard Time' => 'Asia/Beirut', + 'Montevideo Standard Time' => 'America/Montevideo', + 'Morocco Standard Time' => 'Africa/Casablanca', + 'Mountain Standard Time' => 'America/Denver', + 'Mountain Standard Time (Mexico)' => 'America/Chihuahua', + 'Myanmar Standard Time' => 'Asia/Rangoon', + 'N. Central Asia Standard Time' => 'Asia/Novosibirsk', + 'Namibia Standard Time' => 'Africa/Windhoek', + 'Nepal Standard Time' => 'Asia/Katmandu', + 'New Zealand Standard Time' => 'Pacific/Auckland', + 'Newfoundland Standard Time' => 'America/St_Johns', + 'Norfolk Standard Time' => 'Pacific/Norfolk', + 'North Asia East Standard Time' => 'Asia/Irkutsk', + 'North Asia Standard Time' => 'Asia/Krasnoyarsk', + 'North Korea Standard Time' => 'Asia/Pyongyang', + 'Omsk Standard Time' => 'Asia/Omsk', + 'Pacific SA Standard Time' => 'America/Santiago', + 'Pacific Standard Time' => 'America/Los_Angeles', + 'Pacific Standard Time (Mexico)' => 'America/Tijuana', + 'Pakistan Standard Time' => 'Asia/Karachi', + 'Paraguay Standard Time' => 'America/Asuncion', + 'Qyzylorda Standard Time' => 'Asia/Qyzylorda', + 'Romance Standard Time' => 'Europe/Paris', + 'Russia Time Zone 10' => 'Asia/Srednekolymsk', + 'Russia Time Zone 11' => 'Asia/Kamchatka', + 'Russia Time Zone 3' => 'Europe/Samara', + 'Russian Standard Time' => 'Europe/Moscow', + 'SA Eastern Standard Time' => 'America/Cayenne', + 'SA Pacific Standard Time' => 'America/Bogota', + 'SA Western Standard Time' => 'America/La_Paz', + 'SE Asia Standard Time' => 'Asia/Bangkok', + 'Saint Pierre Standard Time' => 'America/Miquelon', + 'Sakhalin Standard Time' => 'Asia/Sakhalin', + 'Samoa Standard Time' => 'Pacific/Apia', + 'Sao Tome Standard Time' => 'Africa/Sao_Tome', + 'Saratov Standard Time' => 'Europe/Saratov', + 'Singapore Standard Time' => 'Asia/Singapore', + 'South Africa Standard Time' => 'Africa/Johannesburg', + 'Sri Lanka Standard Time' => 'Asia/Colombo', + 'Sudan Standard Time' => 'Africa/Khartoum', + 'Syria Standard Time' => 'Asia/Damascus', + 'Taipei Standard Time' => 'Asia/Taipei', + 'Tasmania Standard Time' => 'Australia/Hobart', + 'Tocantins Standard Time' => 'America/Araguaina', + 'Tokyo Standard Time' => 'Asia/Tokyo', + 'Tomsk Standard Time' => 'Asia/Tomsk', + 'Tonga Standard Time' => 'Pacific/Tongatapu', + 'Transbaikal Standard Time' => 'Asia/Chita', + 'Turkey Standard Time' => 'Europe/Istanbul', + 'Turks And Caicos Standard Time' => 'America/Grand_Turk', + 'US Eastern Standard Time' => 'America/Indianapolis', + 'US Mountain Standard Time' => 'America/Phoenix', + 'UTC' => 'Etc/GMT', + 'UTC+12' => 'Etc/GMT-12', + 'UTC+13' => 'Etc/GMT-13', + 'UTC-02' => 'Etc/GMT+2', + 'UTC-08' => 'Etc/GMT+8', + 'UTC-09' => 'Etc/GMT+9', + 'UTC-11' => 'Etc/GMT+11', + 'Ulaanbaatar Standard Time' => 'Asia/Ulaanbaatar', + 'Venezuela Standard Time' => 'America/Caracas', + 'Vladivostok Standard Time' => 'Asia/Vladivostok', + 'Volgograd Standard Time' => 'Europe/Volgograd', + 'W. Australia Standard Time' => 'Australia/Perth', + 'W. Central Africa Standard Time' => 'Africa/Lagos', + 'W. Europe Standard Time' => 'Europe/Berlin', + 'W. Mongolia Standard Time' => 'Asia/Hovd', + 'West Asia Standard Time' => 'Asia/Tashkent', + 'West Bank Standard Time' => 'Asia/Hebron', + '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/VCalendarTest.php b/tests/VObject/Component/VCalendarTest.php index 1893a9ac7..bde6a4fa5 100644 --- a/tests/VObject/Component/VCalendarTest.php +++ b/tests/VObject/Component/VCalendarTest.php @@ -756,7 +756,7 @@ public function testCalDAVMETHOD(): void ); } - public function assertValidate($ics, $options, $expectedLevel, string $expectedMessage = null): void + public function assertValidate($ics, $options, $expectedLevel, ?string $expectedMessage = null): void { $vcal = VObject\Reader::read($ics); $result = $vcal->validate($options); @@ -764,7 +764,7 @@ public function assertValidate($ics, $options, $expectedLevel, string $expectedM self::assertValidateResult($result, $expectedLevel, $expectedMessage); } - public function assertValidateResult($input, $expectedLevel, string $expectedMessage = null): void + public function assertValidateResult($input, $expectedLevel, ?string $expectedMessage = null): void { $messages = []; foreach ($input as $warning) { diff --git a/tests/VObject/Component/VCardTest.php b/tests/VObject/Component/VCardTest.php index b71b30856..3851d97d9 100644 --- a/tests/VObject/Component/VCardTest.php +++ b/tests/VObject/Component/VCardTest.php @@ -283,7 +283,7 @@ public function testVCard21NoCardDAV(): void ); } - public function assertValidate($vcf, $options, int $expectedLevel, string $expectedMessage = null): void + public function assertValidate($vcf, $options, int $expectedLevel, ?string $expectedMessage = null): void { $vcal = VObject\Reader::read($vcf); $result = $vcal->validate($options); @@ -291,7 +291,7 @@ public function assertValidate($vcf, $options, int $expectedLevel, string $expec self::assertValidateResult($result, $expectedLevel, $expectedMessage); } - public function assertValidateResult($input, int $expectedLevel, string $expectedMessage = null): void + public function assertValidateResult($input, int $expectedLevel, ?string $expectedMessage = null): void { $messages = []; foreach ($input as $warning) { diff --git a/tests/VObject/Component/VFreeBusyTest.php b/tests/VObject/Component/VFreeBusyTest.php index f3a521f95..f2f97ebe1 100644 --- a/tests/VObject/Component/VFreeBusyTest.php +++ b/tests/VObject/Component/VFreeBusyTest.php @@ -3,7 +3,6 @@ namespace Sabre\VObject\Component; use PHPUnit\Framework\TestCase; -use Sabre\VObject; use Sabre\VObject\Reader; class VFreeBusyTest extends TestCase @@ -23,7 +22,7 @@ public function testIsFree(): void END:VCALENDAR BLA; - $obj = VObject\Reader::read($input); + $obj = Reader::read($input); $vfb = $obj->VFREEBUSY; $tz = new \DateTimeZone('UTC'); diff --git a/tests/VObject/Component/VTimeZoneTest.php b/tests/VObject/Component/VTimeZoneTest.php index 2928ecd0c..19a88cb74 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/ComponentTest.php b/tests/VObject/ComponentTest.php index 4a247d906..c48480983 100644 --- a/tests/VObject/ComponentTest.php +++ b/tests/VObject/ComponentTest.php @@ -463,7 +463,7 @@ public function testRemoveNotFound(): void */ public function testValidateRules(array $componentList, int $errorCount): void { - $vcard = new Component\VCard(); + $vcard = new VCard(); $component = new FakeComponent($vcard, 'Hi', [], false); foreach ($componentList as $v) { @@ -475,7 +475,7 @@ public function testValidateRules(array $componentList, int $errorCount): void public function testValidateRepair(): void { - $vcard = new Component\VCard(); + $vcard = new VCard(); $component = new FakeComponent($vcard, 'Hi', [], false); $component->validate(Component::REPAIR); @@ -484,7 +484,7 @@ public function testValidateRepair(): void public function testValidateRepairShouldNotDeduplicatePropertiesWhenValuesDiffer(): void { - $vcard = new Component\VCard(); + $vcard = new VCard(); $component = new FakeComponent($vcard, 'WithDuplicateGIR', []); $component->add('BAZ', 'BAZ'); @@ -500,7 +500,7 @@ public function testValidateRepairShouldNotDeduplicatePropertiesWhenValuesDiffer public function testValidateRepairShouldNotDeduplicatePropertiesWhenParametersDiffer(): void { - $vcard = new Component\VCard(); + $vcard = new VCard(); $component = new FakeComponent($vcard, 'WithDuplicateGIR', []); $component->add('BAZ', 'BAZ'); @@ -516,7 +516,7 @@ public function testValidateRepairShouldNotDeduplicatePropertiesWhenParametersDi public function testValidateRepairShouldDeduplicatePropertiesWhenValuesAndParametersAreEqual(): void { - $vcard = new Component\VCard(); + $vcard = new VCard(); $component = new FakeComponent($vcard, 'WithDuplicateGIR', []); $component->add('BAZ', 'BAZ'); @@ -532,7 +532,7 @@ public function testValidateRepairShouldDeduplicatePropertiesWhenValuesAndParame public function testValidateRepairShouldDeduplicatePropertiesWhenValuesAreEqual(): void { - $vcard = new Component\VCard(); + $vcard = new VCard(); $component = new FakeComponent($vcard, 'WithDuplicateGIR', []); $component->add('BAZ', 'BAZ'); diff --git a/tests/VObject/EmptyParameterTest.php b/tests/VObject/EmptyParameterTest.php index ab197ab33..6739f104a 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/FreeBusyGeneratorTest.php b/tests/VObject/FreeBusyGeneratorTest.php index 47283eafb..a4946cc10 100644 --- a/tests/VObject/FreeBusyGeneratorTest.php +++ b/tests/VObject/FreeBusyGeneratorTest.php @@ -46,7 +46,7 @@ public function testInvalidArg(): void * * @throws ParseException */ - public function assertFreeBusyReport(string $expected, $input, \DateTimeZone $timeZone = null, string $vavailability = null): void + public function assertFreeBusyReport(string $expected, $input, ?\DateTimeZone $timeZone = null, ?string $vavailability = null): void { $gen = new FreeBusyGenerator( new \DateTime('20110101T110000Z', new \DateTimeZone('UTC')), diff --git a/tests/VObject/ITip/BrokerAttendeeReplyTest.php b/tests/VObject/ITip/BrokerAttendeeReplyTest.php index 98942af41..6590356bb 100644 --- a/tests/VObject/ITip/BrokerAttendeeReplyTest.php +++ b/tests/VObject/ITip/BrokerAttendeeReplyTest.php @@ -404,15 +404,15 @@ public function testRecurringAllDay(): void $version = \Sabre\VObject\Version::VERSION; $expected = [ - [ - 'uid' => 'foobar', - 'method' => 'REPLY', - 'component' => 'VEVENT', - 'sender' => 'mailto:one@example.org', - 'senderName' => 'One', - 'recipient' => 'mailto:strunk@example.org', - 'recipientName' => 'Strunk', - 'message' => << 'foobar', + 'method' => 'REPLY', + 'component' => 'VEVENT', + 'sender' => 'mailto:one@example.org', + 'senderName' => 'One', + 'recipient' => 'mailto:strunk@example.org', + 'recipientName' => 'Strunk', + 'message' => <<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/JCardTest.php b/tests/VObject/JCardTest.php index cbd56ba97..eb0c0f6fa 100644 --- a/tests/VObject/JCardTest.php +++ b/tests/VObject/JCardTest.php @@ -122,15 +122,15 @@ public function testToJCard(): void 'adr', new \stdClass(), 'text', - [ - '', - '', - ['My Street', 'Left Side', 'Second Shack'], - 'Hometown', - 'PA', - '18252', - 'U.S.A', - ], + [ + '', + '', + ['My Street', 'Left Side', 'Second Shack'], + 'Hometown', + 'PA', + '18252', + 'U.S.A', + ], ], [ 'x-truncated', diff --git a/tests/VObject/Parser/JsonTest.php b/tests/VObject/Parser/JsonTest.php index 996e834e6..392cc7c11 100644 --- a/tests/VObject/Parser/JsonTest.php +++ b/tests/VObject/Parser/JsonTest.php @@ -89,15 +89,15 @@ public function testRoundTripJCard(): void 'adr', new \stdClass(), 'text', - [ - '', - '', - ['My Street', 'Left Side', 'Second Shack'], - 'Hometown', - 'PA', - '18252', - 'U.S.A', - ], + [ + '', + '', + ['My Street', 'Left Side', 'Second Shack'], + 'Hometown', + 'PA', + '18252', + 'U.S.A', + ], ], [ diff --git a/tests/VObject/Parser/UnfoldingTest.php b/tests/VObject/Parser/UnfoldingTest.php new file mode 100644 index 000000000..8d751ae7a --- /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 00c8be1be..0da557b93 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/PropertyTest.php b/tests/VObject/PropertyTest.php index b15e06b27..5a34383f1 100644 --- a/tests/VObject/PropertyTest.php +++ b/tests/VObject/PropertyTest.php @@ -259,7 +259,7 @@ public function testValidateControlChars(): void 0x17, 0x16, 0x15, 0x14, 0x13, 0x12, 0x11, 0x10, 0x0F, 0x0E, 0x0D, 0x0C, 0x0B, 0x0A, 0x09, 0x08, 0x07, 0x06, 0x05, 0x04, 0x03, 0x02, 0x01, 0x00, - ] as $c) { + ] as $c) { $s .= sprintf('%02X(%c)', $c, $c); } $s .= ']end'; diff --git a/tests/VObject/ReaderTest.php b/tests/VObject/ReaderTest.php index 645ed1813..336a291ab 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 bce4bcd92..022df4a3b 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 000000000..0426cc5ec --- /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 000000000..62effc4de --- /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 000000000..0b94f3945 --- /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 2814df66d..7ace7ae4f 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 ); } @@ -271,12 +282,13 @@ public function testMonthly(): void 'FREQ=MONTHLY;INTERVAL=3;COUNT=5', '2011-12-05 00:00:00', [ - '2011-12-05 00:00:00', - '2012-03-05 00:00:00', - '2012-06-05 00:00:00', - '2012-09-05 00:00:00', - '2012-12-05 00:00:00', - ] + '2011-12-05 00:00:00', + '2012-03-05 00:00:00', + '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 462218e9b..b82556219 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,346 @@ 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 testCustomizedTimeZone2(): void + { + $ics = <<getName()); + $start = new \DateTimeImmutable('2022-04-25'); + self::assertSame(2 * 60 * 60, $tz->getOffset($start)); + + $start = new \DateTimeImmutable('2022-11-10'); + self::assertSame(60 * 60, $tz->getOffset($start)); + } + + public function testCustomizedTimeZoneWithoutDaylight(): void + { + $ics = $this->getCustomizedICS(); + $tz = TimeZoneUtil::getTimeZone('Customized Time Zone', Reader::read($ics)); + self::assertSame('Antarctica/Casey', $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); $vcard = $vcard->serialize(); @@ -608,7 +608,7 @@ public function testPhoneNumberValueTypeGetsRemoved(): void $vcard = Reader::read($input); $vcard = $vcard->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 000000000..bc7945f4e --- /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