Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions lib/TimezoneGuesser/FindFromOffset.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
}
66 changes: 66 additions & 0 deletions tests/VObject/TimeZoneUtilTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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(<<<ICS
BEGIN:VCALENDAR
VERSION:2.0
BEGIN:VEVENT
UID:fixed-offset
DTSTART;TZID="UTC-04:00":20260728T111500
END:VEVENT
END:VCALENDAR
ICS
);

$start = $calendar->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);
Expand Down