1: <?php
2: declare(strict_types=1);
3:
4: namespace om\ICal\Timezone;
5:
6: use DateTimeZone;
7: use Exception;
8: use IntlTimeZone;
9: use om\ICal\Component;
10: use om\TimezoneResolver as NameResolver;
11:
12: /**
13: * Resolves Windows timezone names ("W. Europe Standard Time"), Outlook display names
14: * ("(UTC+01:00) Amsterdam, Berlin, ..."), prefixed names ("/mozilla.org/20070129_1/Europe/Paris"),
15: * quoted names and IANA names without their first part ("Argentina/Buenos_Aires").
16: *
17: * The bundled map (CLDR plus aliases) is used first, so results do not depend on the ICU version;
18: * IntlTimeZone::getIDForWindowsID() of the intl extension is a fallback for newer Windows names.
19: */
20: final class AliasTimezoneResolver implements TimezoneResolver {
21: private readonly NameResolver $names;
22:
23: /**
24: * @param ?array<string, string> $windowsTimezones Windows name => IANA name, the bundled mapping by default
25: */
26: public function __construct(?array $windowsTimezones = null) {
27: $this->names = new NameResolver($windowsTimezones ?? self::windowsTimezones());
28: }
29:
30: public function resolve(string $tzid, Component $calendar): ?ResolvedTimezone {
31: $timezone = $this->names->resolve($tzid) ?? self::fromIntl($tzid) ?? self::bySuffix($tzid);
32: return $timezone === null ? null : new ResolvedTimezone($tzid, $timezone, TimezoneSource::Alias);
33: }
34:
35: private static function fromIntl(string $tzid): ?DateTimeZone {
36: if (!class_exists(IntlTimeZone::class)) {
37: return null;
38: }
39: // returns false for unknown names, although the stubs say string
40: $name = (string) IntlTimeZone::getIDForWindowsID(trim($tzid, " \t'\""));
41: try {
42: return $name !== '' ? new DateTimeZone($name) : null;
43: } catch (Exception) {
44: return null;
45: }
46: }
47:
48: /**
49: * An IANA name missing its first part, e.g. "Argentina/Buenos_Aires", when the match is unique.
50: */
51: private static function bySuffix(string $tzid): ?DateTimeZone {
52: $suffix = '/' . strtolower(trim($tzid, " \t'\"/"));
53: if (strlen($suffix) < 4) {
54: return null;
55: }
56: $matches = array_filter(DateTimeZone::listIdentifiers(DateTimeZone::ALL_WITH_BC), static fn(string $name): bool => str_ends_with(strtolower($name), $suffix));
57: return count($matches) === 1 ? new DateTimeZone(reset($matches)) : null;
58: }
59:
60: /**
61: * @return array<string, string>
62: */
63: public static function windowsTimezones(): array {
64: static $map;
65: return $map ??= require __DIR__ . '/../../WindowsTimezones.php';
66: }
67: }
68: