diff --git a/lib/TimezoneGuesser/FindFromOffset.php b/lib/TimezoneGuesser/FindFromOffset.php index d50cb969c..418fd6ba9 100644 --- a/lib/TimezoneGuesser/FindFromOffset.php +++ b/lib/TimezoneGuesser/FindFromOffset.php @@ -24,6 +24,24 @@ public function find(string $tzid, ?bool $failIfUncertain = false): ?\DateTimeZo // @codeCoverageIgnoreEnd } + if (0 === strncasecmp($tzid, 'UTC', 3)) { + $offset = substr($tzid, 3); + if (6 === strlen($offset) && ':' === $offset[3]) { + $offset = substr($offset, 0, 3).substr($offset, 4); + } + + if (5 !== strlen($offset) + || ('+' !== $offset[0] && '-' !== $offset[0]) + || 4 !== strspn(substr($offset, 1), '0123456789') + || (int) substr($offset, 1, 2) > 23 + || (int) substr($offset, 3, 2) > 59 + ) { + return null; + } + + return new \DateTimeZone($offset); + } + return null; } } diff --git a/tests/VObject/TimeZoneUtilTest.php b/tests/VObject/TimeZoneUtilTest.php index 3690e64c2..43294585f 100644 --- a/tests/VObject/TimeZoneUtilTest.php +++ b/tests/VObject/TimeZoneUtilTest.php @@ -221,6 +221,72 @@ public function testTimezoneOffset(): void self::assertEquals($ex->getName(), $tz->getName()); } + #[DataProvider('getValidUtcOffsets')] + public function testValidUtcOffset(string $tzid, string $expectedOffset): void + { + $tz = TimeZoneUtil::getTimeZone($tzid, null, true); + + self::assertSame($expectedOffset, $tz->getName()); + } + + public static function getValidUtcOffsets(): array + { + return [ + ['UTC-04:00', '-04:00'], + ['UTC-0400', '-04:00'], + ['UTC+05:30', '+05:30'], + ['UTC+0545', '+05:45'], + ['utc-03:30', '-03:30'], + ['UTC+00:00', '+00:00'], + ['UTC-00:00', '+00:00'], + ['UTC+23:59', '+23:59'], + ]; + } + + #[DataProvider('getInvalidUtcOffsets')] + public function testInvalidUtcOffset(string $tzid): void + { + self::assertSame(date_default_timezone_get(), TimeZoneUtil::getTimeZone($tzid)->getName()); + + $this->expectException(\InvalidArgumentException::class); + TimeZoneUtil::getTimeZone($tzid, null, true); + } + + public static function getInvalidUtcOffsets(): array + { + return [ + ['UTC-24:00'], + ['UTC+04:60'], + ['UTC+4:00'], + ['UTC+04'], + ['UTC04:00'], + ['UTC+ab:cd'], + ['UTC+04:00extra'], + ["UTC+04:00\n"], + ['UTC+04::00'], + ['UTC+'], + ]; + } + + public function testParseUtcOffsetDateTime(): void + { + $calendar = Reader::read(<<VEVENT->DTSTART->getDateTime(); + + self::assertSame('2026-07-28 11:15:00 -04:00', $start->format('Y-m-d H:i:s P')); + self::assertSame((new \DateTimeImmutable('2026-07-28T15:15:00Z'))->getTimestamp(), $start->getTimestamp()); + } + public function testTimezoneFail(): void { $this->expectException(\InvalidArgumentException::class);