Skip to content

Commit

Permalink
feature #31262 [Intl] Update timezones to ICU 64.2 + compile zone to …
Browse files Browse the repository at this point in the history
…country mapping (ro0NL)

This PR was squashed before being merged into the 4.3-dev branch (closes #31262).

Discussion
----------

[Intl] Update timezones to ICU 64.2 + compile zone to country mapping

| Q             | A
| ------------- | ---
| Branch?       | master
| Bug fix?      | no
| New feature?  | yes
| BC breaks?    | no     <!-- see https://symfony.com/bc -->
| Deprecations? | no
| Tests pass?   | yes    <!-- please add some, will be required by reviewers -->
| Fixed tickets | #...   <!-- #-prefixed issue number(s), if any -->
| License       | MIT
| Doc PR        | symfony/symfony-docs#... <!-- required for new features -->

This PR compiles the zone to country mapping (and vice versa) from ICU data:
https://github.com/unicode-org/icu/blob/master/icu4c/source/data/misc/windowsZones.txt

I've recompiled timezones on master due #31162, i should do it once more when it's merged upstream.

Having this data allows compatibility between PHP timezones and ICU;
- https://www.php.net/manual/en/intltimezone.getregion.php
- https://www.php.net/manual/en/class.datetimezone.php#datetimezone.constants.per-country

For the timezone validator in Symfony, this would be required to have a compatible "regions" option, once it supports ICU as well (#28836 (comment))

Commits
-------

3018a7a [Intl] Update timezones to ICU 64.2 + compile zone to country mapping
  • Loading branch information
fabpot committed Apr 29, 2019
2 parents 0cd5c18 + 3018a7a commit 1f388ae
Show file tree
Hide file tree
Showing 207 changed files with 17,872 additions and 9,249 deletions.
148 changes: 130 additions & 18 deletions src/Symfony/Component/Intl/Data/Generator/TimezoneDataGenerator.php
Expand Up @@ -13,8 +13,11 @@

use Symfony\Component\Intl\Data\Bundle\Compiler\GenrbCompiler;
use Symfony\Component\Intl\Data\Bundle\Reader\BundleReaderInterface;
use Symfony\Component\Intl\Data\Provider\RegionDataProvider;
use Symfony\Component\Intl\Data\Util\ArrayAccessibleResourceBundle;
use Symfony\Component\Intl\Data\Util\LocaleScanner;
use Symfony\Component\Intl\Exception\MissingResourceException;
use Symfony\Component\Intl\Locale;

/**
* The rule for compiling the zone bundle.
Expand All @@ -26,11 +29,19 @@
class TimezoneDataGenerator extends AbstractDataGenerator
{
/**
* Collects all available zone codes.
* Collects all available zone IDs.
*
* @var string[]
*/
private $zoneCodes = [];
private $zoneIds = [];
private $regionDataProvider;

public function __construct(GenrbCompiler $compiler, string $dirName, RegionDataProvider $regionDataProvider)
{
parent::__construct($compiler, $dirName);

$this->regionDataProvider = $regionDataProvider;
}

/**
* {@inheritdoc}
Expand All @@ -48,14 +59,15 @@ protected function compileTemporaryBundles(GenrbCompiler $compiler, $sourceDir,
$compiler->compile($sourceDir.'/zone', $tempDir);
$compiler->compile($sourceDir.'/misc/timezoneTypes.txt', $tempDir);
$compiler->compile($sourceDir.'/misc/metaZones.txt', $tempDir);
$compiler->compile($sourceDir.'/misc/windowsZones.txt', $tempDir);
}

/**
* {@inheritdoc}
*/
protected function preGenerate()
{
$this->zoneCodes = [];
$this->zoneIds = [];
}

/**
Expand All @@ -66,17 +78,30 @@ protected function generateDataForLocale(BundleReaderInterface $reader, $tempDir
$localeBundle = $reader->read($tempDir, $displayLocale);

if (isset($localeBundle['zoneStrings']) && null !== $localeBundle['zoneStrings']) {
$localeBundles = [$localeBundle];
$fallback = $displayLocale;
while (null !== ($fallback = Locale::getFallback($fallback))) {
$localeBundles[] = $reader->read($tempDir, $fallback);
}
if ('root' !== $displayLocale) {
$localeBundles[] = $reader->read($tempDir, 'root');
}
$data = [
'Version' => $localeBundle['Version'],
'Names' => self::generateZones(
'Names' => $this->generateZones(
$displayLocale,
$reader->read($tempDir, 'timezoneTypes'),
$reader->read($tempDir, 'metaZones'),
$reader->read($tempDir, 'root'),
$localeBundle
$reader->read($tempDir, 'windowsZones'),
...$localeBundles
),
];

$this->zoneCodes = array_merge($this->zoneCodes, array_keys($data['Names']));
if (!$data['Names']) {
return;
}

$this->zoneIds = array_merge($this->zoneIds, array_keys($data['Names']));

return $data;
}
Expand All @@ -96,20 +121,63 @@ protected function generateDataForMeta(BundleReaderInterface $reader, $tempDir)
{
$rootBundle = $reader->read($tempDir, 'root');

$this->zoneCodes = array_unique($this->zoneCodes);
$this->zoneIds = array_unique($this->zoneIds);

sort($this->zoneCodes);
sort($this->zoneIds);

$data = [
'Version' => $rootBundle['Version'],
'Zones' => $this->zoneCodes,
'Zones' => $this->zoneIds,
'ZoneToCountry' => self::generateZoneToCountryMapping($reader->read($tempDir, 'windowsZones')),
];

$data['CountryToZone'] = self::generateCountryToZoneMapping($data['ZoneToCountry']);

return $data;
}

private static function generateZones(ArrayAccessibleResourceBundle $typeBundle, ArrayAccessibleResourceBundle $metaBundle, ArrayAccessibleResourceBundle $rootBundle, ArrayAccessibleResourceBundle $localeBundle): array
private function generateZones(string $locale, ArrayAccessibleResourceBundle $typeBundle, ArrayAccessibleResourceBundle $metaBundle, ArrayAccessibleResourceBundle $windowsZonesBundle, ArrayAccessibleResourceBundle ...$localeBundles): array
{
$accessor = static function (ArrayAccessibleResourceBundle $resourceBundle, array $indices) {
$result = $resourceBundle;
foreach ($indices as $indice) {
$result = $result[$indice] ?? null;
}

return $result;
};
$accessor = static function (array $indices, &$inherited = false) use ($localeBundles, $accessor) {
$inherited = false;
foreach ($localeBundles as $i => $localeBundle) {
$nextLocaleBundle = $localeBundles[$i + 1] ?? null;
$result = $accessor($localeBundle, $indices);
if (null !== $result && (null === $nextLocaleBundle || $result !== $accessor($nextLocaleBundle, $indices))) {
$inherited = 0 !== $i;

return $result;
}
}

return null;
};
$regionFormat = $accessor(['zoneStrings', 'regionFormat']) ?? '{0}';
$fallbackFormat = $accessor(['zoneStrings', 'fallbackFormat']) ?? '{1} ({0})';
$zoneToCountry = self::generateZoneToCountryMapping($windowsZonesBundle);
$resolveName = function (string $id, string $city = null) use ($locale, $regionFormat, $fallbackFormat, $zoneToCountry): string {
if (isset($zoneToCountry[$id])) {
try {
$country = $this->regionDataProvider->getName($zoneToCountry[$id], $locale);
} catch (MissingResourceException $e) {
$country = $this->regionDataProvider->getName($zoneToCountry[$id], 'en');
}

return null === $city ? str_replace('{0}', $country, $regionFormat) : str_replace(['{0}', '{1}'], [$city, $country], $fallbackFormat);
} elseif (null !== $city) {
return str_replace('{0}', $city, $regionFormat);
} else {
return str_replace(['/', '_'], ' ', 0 === strrpos($id, 'Etc/') ? substr($id, 4) : $id);
}
};
$available = [];
foreach ($typeBundle['typeMap']['timezone'] as $zone => $_) {
if ('Etc:Unknown' === $zone || preg_match('~^Etc:GMT[-+]\d+$~', $zone)) {
Expand All @@ -126,33 +194,77 @@ private static function generateZones(ArrayAccessibleResourceBundle $typeBundle,
}
}

$isBase = false === strpos($locale, '_');
$zones = [];
foreach (array_keys($available) as $zone) {
// lg: long generic, e.g. "Central European Time"
// ls: long specific (not DST), e.g. "Central European Standard Time"
// ld: long DST, e.g. "Central European Summer Time"
// ec: example city, e.g. "Amsterdam"
$name = $localeBundle['zoneStrings'][$zone]['lg'] ?? $rootBundle['zoneStrings'][$zone]['lg'] ?? $localeBundle['zoneStrings'][$zone]['ls'] ?? $rootBundle['zoneStrings'][$zone]['ls'] ?? null;
$city = $localeBundle['zoneStrings'][$zone]['ec'] ?? $rootBundle['zoneStrings'][$zone]['ec'] ?? null;
$name = $accessor(['zoneStrings', $zone, 'lg'], $nameInherited) ?? $accessor(['zoneStrings', $zone, 'ls'], $nameInherited);
$city = $accessor(['zoneStrings', $zone, 'ec'], $cityInherited);
$id = str_replace(':', '/', $zone);

if (null === $name && isset($metazones[$zone])) {
$meta = 'meta:'.$metazones[$zone];
$name = $localeBundle['zoneStrings'][$meta]['lg'] ?? $rootBundle['zoneStrings'][$meta]['lg'] ?? $localeBundle['zoneStrings'][$meta]['ls'] ?? $rootBundle['zoneStrings'][$meta]['ls'] ?? null;
$name = $accessor(['zoneStrings', $meta, 'lg'], $nameInherited) ?? $accessor(['zoneStrings', $meta, 'ls'], $nameInherited);
}
if (null === $city && 0 !== strrpos($zone, 'Etc:') && false !== $i = strrpos($zone, ':')) {
$city = str_replace('_', ' ', substr($zone, $i + 1));
$cityInherited = !$isBase;
}
if (null === $name) {
if ($isBase && null === $name) {
$name = $resolveName($id, $city);
$city = null;
}
if (
($nameInherited && $cityInherited)
|| (null === $name && null === $city)
|| ($nameInherited && null === $city)
|| ($cityInherited && null === $name)
) {
continue;
}
if (null !== $city) {
$name .= ' ('.$city.')';
if (null === $name) {
$name = $resolveName($id, $city);
} elseif (null !== $city && false === mb_stripos(str_replace('-', ' ', $name), str_replace('-', ' ', $city))) {
$name = str_replace(['{0}', '{1}'], [$city, $name], $fallbackFormat);
}

$id = str_replace(':', '/', $zone);
$zones[$id] = $name;
}

return $zones;
}

private static function generateZoneToCountryMapping(ArrayAccessibleResourceBundle $windowsZoneBundle): array
{
$mapping = [];

foreach ($windowsZoneBundle['mapTimezones'] as $zoneInfo) {
foreach ($zoneInfo as $region => $zones) {
if (\in_array($region, ['001', 'ZZ'], true)) {
continue;
}
$mapping += array_fill_keys(explode(' ', $zones), $region);
}
}

ksort($mapping);

return $mapping;
}

private static function generateCountryToZoneMapping(array $zoneToCountryMapping): array
{
$mapping = [];

foreach ($zoneToCountryMapping as $zone => $country) {
$mapping[$country][] = $zone;
}

ksort($mapping);

return $mapping;
}
}
2 changes: 1 addition & 1 deletion src/Symfony/Component/Intl/Resources/bin/update-data.php
Expand Up @@ -260,7 +260,7 @@

echo "Generating zone data...\n";

$generator = new TimezoneDataGenerator($compiler, Intl::TIMEZONE_DIR);
$generator = new TimezoneDataGenerator($compiler, Intl::TIMEZONE_DIR, new RegionDataProvider($jsonDir.'/'.Intl::REGION_DIR, $reader));
$generator->generateData($config);

echo "Resource bundle compilation complete.\n";
Expand Down

0 comments on commit 1f388ae

Please sign in to comment.