Skip to content

Commit 5bee119

Browse files
fix: handle UTC offset timezones
Signed-off-by: SebastianKrupinski <krupinskis05@gmail.com>
1 parent 01d1edc commit 5bee119

2 files changed

Lines changed: 88 additions & 0 deletions

File tree

‎lib/TimezoneGuesser/FindFromOffset.php‎

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,24 @@ public function find(string $tzid, ?bool $failIfUncertain = false): ?\DateTimeZo
2424
// @codeCoverageIgnoreEnd
2525
}
2626

27+
if (0 === strncasecmp($tzid, 'UTC', 3)) {
28+
$offset = substr($tzid, 3);
29+
if (6 === strlen($offset) && ':' === $offset[3]) {
30+
$offset = substr($offset, 0, 3).substr($offset, 4);
31+
}
32+
33+
if (5 !== strlen($offset)
34+
|| ('+' !== $offset[0] && '-' !== $offset[0])
35+
|| 4 !== strspn(substr($offset, 1), '0123456789')
36+
|| (int) substr($offset, 1, 2) > 23
37+
|| (int) substr($offset, 3, 2) > 59
38+
) {
39+
return null;
40+
}
41+
42+
return new \DateTimeZone($offset);
43+
}
44+
2745
return null;
2846
}
2947
}

‎tests/VObject/TimeZoneUtilTest.php‎

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -221,6 +221,76 @@ public function testTimezoneOffset(): void
221221
self::assertEquals($ex->getName(), $tz->getName());
222222
}
223223

224+
/**
225+
* @dataProvider getValidUtcOffsets
226+
*/
227+
public function testValidUtcOffset(string $tzid, string $expectedOffset): void
228+
{
229+
$tz = TimeZoneUtil::getTimeZone($tzid, null, true);
230+
231+
self::assertSame($expectedOffset, $tz->getName());
232+
}
233+
234+
public static function getValidUtcOffsets(): array
235+
{
236+
return [
237+
['UTC-04:00', '-04:00'],
238+
['UTC-0400', '-04:00'],
239+
['UTC+05:30', '+05:30'],
240+
['UTC+0545', '+05:45'],
241+
['utc-03:30', '-03:30'],
242+
['UTC+00:00', '+00:00'],
243+
['UTC-00:00', '+00:00'],
244+
['UTC+23:59', '+23:59'],
245+
];
246+
}
247+
248+
/**
249+
* @dataProvider getInvalidUtcOffsets
250+
*/
251+
public function testInvalidUtcOffset(string $tzid): void
252+
{
253+
self::assertSame(date_default_timezone_get(), TimeZoneUtil::getTimeZone($tzid)->getName());
254+
255+
$this->expectException(\InvalidArgumentException::class);
256+
TimeZoneUtil::getTimeZone($tzid, null, true);
257+
}
258+
259+
public static function getInvalidUtcOffsets(): array
260+
{
261+
return [
262+
['UTC-24:00'],
263+
['UTC+04:60'],
264+
['UTC+4:00'],
265+
['UTC+04'],
266+
['UTC04:00'],
267+
['UTC+ab:cd'],
268+
['UTC+04:00extra'],
269+
["UTC+04:00\n"],
270+
['UTC+04::00'],
271+
['UTC+'],
272+
];
273+
}
274+
275+
public function testParseUtcOffsetDateTime(): void
276+
{
277+
$calendar = Reader::read(<<<ICS
278+
BEGIN:VCALENDAR
279+
VERSION:2.0
280+
BEGIN:VEVENT
281+
UID:fixed-offset
282+
DTSTART;TZID="UTC-04:00":20260728T111500
283+
END:VEVENT
284+
END:VCALENDAR
285+
ICS
286+
);
287+
288+
$start = $calendar->VEVENT->DTSTART->getDateTime();
289+
290+
self::assertSame('2026-07-28 11:15:00 -04:00', $start->format('Y-m-d H:i:s P'));
291+
self::assertSame((new \DateTimeImmutable('2026-07-28T15:15:00Z'))->getTimestamp(), $start->getTimestamp());
292+
}
293+
224294
public function testTimezoneFail(): void
225295
{
226296
$this->expectException(\InvalidArgumentException::class);

0 commit comments

Comments
 (0)