Skip to content

Commit 243f8f9

Browse files
Crovitche-1623nicolas-grekas
authored andcommitted
[Intl] Add metadata about currencies' validtity dates
1 parent 56e7101 commit 243f8f9

File tree

3 files changed

+2328
-1
lines changed

3 files changed

+2328
-1
lines changed

src/Symfony/Component/Intl/Data/Generator/CurrencyDataGenerator.php

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,7 @@ protected function generateDataForMeta(BundleEntryReaderInterface $reader, strin
102102
$data = [
103103
'Currencies' => $this->currencyCodes,
104104
'Meta' => $this->generateCurrencyMeta($supplementalDataBundle),
105+
'Map' => $this->generateCurrencyMap($supplementalDataBundle),
105106
'Alpha3ToNumeric' => $this->generateAlpha3ToNumericMapping($numericCodesBundle, $this->currencyCodes),
106107
];
107108

@@ -127,6 +128,70 @@ private function generateCurrencyMeta(ArrayAccessibleResourceBundle $supplementa
127128
return iterator_to_array($supplementalDataBundle['CurrencyMeta']);
128129
}
129130

131+
/**
132+
* @return array<string, array>
133+
*/
134+
private function generateCurrencyMap(mixed $supplementalDataBundle): array
135+
{
136+
/**
137+
* @var list<string, list<string, array{from?: string, to?: string, tender?: false}>> $regionsData
138+
*/
139+
$regionsData = [];
140+
141+
foreach ($supplementalDataBundle['CurrencyMap'] as $regionId => $region) {
142+
foreach ($region as $metadata) {
143+
/**
144+
* Note 1: The "to" property (if present) is always greater than "from".
145+
* Note 2: The "to" property may be missing if the currency is still in use.
146+
* Note 3: The "tender" property indicates whether the country legally recognizes the currency within
147+
* its borders. This property is explicitly set to `false` only if that is not the case;
148+
* otherwise, it is `true` by default.
149+
* Note 4: The "from" and "to" dates are not stored as strings; they are stored as a pair of integers.
150+
* Note 5: The "to" property may be missing if "tender" is set to `false`.
151+
*
152+
* @var array{
153+
* from?: array{0: int, 1: int},
154+
* to?: array{0: int, 2: int},
155+
* tender?: bool,
156+
* id: string
157+
* } $metadata
158+
*/
159+
$metadata = iterator_to_array($metadata);
160+
161+
$id = $metadata['id'];
162+
163+
unset($metadata['id']);
164+
165+
if (\array_key_exists($id, self::DENYLIST)) {
166+
continue;
167+
}
168+
169+
if (\array_key_exists('from', $metadata)) {
170+
$metadata['from'] = self::icuPairToDate($metadata['from']);
171+
}
172+
173+
if (\array_key_exists('to', $metadata)) {
174+
$metadata['to'] = self::icuPairToDate($metadata['to']);
175+
}
176+
177+
if (\array_key_exists('tender', $metadata)) {
178+
$metadata['tender'] = filter_var($metadata['tender'], \FILTER_VALIDATE_BOOLEAN, \FILTER_NULL_ON_FAILURE);
179+
180+
if (null === $metadata['tender']) {
181+
throw new \RuntimeException('Unexpected boolean value for tender attribute.');
182+
}
183+
}
184+
185+
$regionsData[$regionId][$id] = $metadata;
186+
}
187+
188+
// Do not exclude countries with no currencies or excluded currencies (e.g. Antartica)
189+
$regionsData[$regionId] ??= [];
190+
}
191+
192+
return $regionsData;
193+
}
194+
130195
private function generateAlpha3ToNumericMapping(ArrayAccessibleResourceBundle $numericCodesBundle, array $currencyCodes): array
131196
{
132197
$alpha3ToNumericMapping = iterator_to_array($numericCodesBundle['codeMap']);
@@ -156,4 +221,41 @@ private function generateNumericToAlpha3Mapping(array $alpha3ToNumericMapping):
156221

157222
return $numericToAlpha3Mapping;
158223
}
224+
225+
/**
226+
* Decodes ICU "date pair" into a DateTimeImmutable (UTC).
227+
*
228+
* ICU stores UDate = milliseconds since 1970-01-01T00:00:00Z in a signed 64-bit.
229+
*
230+
* @param array{0: int, 1: int} $pair
231+
*/
232+
private static function icuPairToDate(array $pair): string
233+
{
234+
[$highBits32, $lowBits32] = $pair;
235+
236+
// Recompose a 64-bit unsigned integer from two 32-bit chunks.
237+
$unsigned64 = ((($highBits32 & 0xFFFFFFFF) << 32) | ($lowBits32 & 0xFFFFFFFF));
238+
239+
// Convert to signed 64-bit (two's complement) if sign bit is set.
240+
if ($unsigned64 >= (1 << 63)) {
241+
$unsigned64 -= (1 << 64);
242+
}
243+
244+
// Split into seconds and milliseconds.
245+
$seconds = intdiv($unsigned64, 1000);
246+
$millisecondsRemainder = $unsigned64 - $seconds * 1000;
247+
248+
// Normalize negative millisecond remainders (e.g., for pre-1970 values)
249+
if (0 > $millisecondsRemainder) {
250+
--$seconds;
251+
}
252+
253+
$datetime = \DateTimeImmutable::createFromFormat('U', $seconds, new \DateTimeZone('Etc/UTC'));
254+
255+
if (false === $datetime) {
256+
throw new \RuntimeException('Unable to parse ICU milliseconds pair.');
257+
}
258+
259+
return $datetime->format('Y-m-d');
260+
}
159261
}

src/Symfony/Component/Intl/Resources/bin/compile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,5 +8,5 @@ docker run \
88
-v /tmp/symfony/icu:/tmp \
99
-v $(pwd):/symfony \
1010
-w /symfony \
11-
jakzal/php-intl:8.3-74.1 \
11+
jakzal/php-intl:8.4-77.1 \
1212
php src/Symfony/Component/Intl/Resources/bin/update-data.php

0 commit comments

Comments
 (0)