@@ -102,6 +102,7 @@ protected function generateDataForMeta(BundleEntryReaderInterface $reader, strin
102
102
$ data = [
103
103
'Currencies ' => $ this ->currencyCodes ,
104
104
'Meta ' => $ this ->generateCurrencyMeta ($ supplementalDataBundle ),
105
+ 'Map ' => $ this ->generateCurrencyMap ($ supplementalDataBundle ),
105
106
'Alpha3ToNumeric ' => $ this ->generateAlpha3ToNumericMapping ($ numericCodesBundle , $ this ->currencyCodes ),
106
107
];
107
108
@@ -127,6 +128,70 @@ private function generateCurrencyMeta(ArrayAccessibleResourceBundle $supplementa
127
128
return iterator_to_array ($ supplementalDataBundle ['CurrencyMeta ' ]);
128
129
}
129
130
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
+
130
195
private function generateAlpha3ToNumericMapping (ArrayAccessibleResourceBundle $ numericCodesBundle , array $ currencyCodes ): array
131
196
{
132
197
$ alpha3ToNumericMapping = iterator_to_array ($ numericCodesBundle ['codeMap ' ]);
@@ -156,4 +221,41 @@ private function generateNumericToAlpha3Mapping(array $alpha3ToNumericMapping):
156
221
157
222
return $ numericToAlpha3Mapping ;
158
223
}
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
+ }
159
261
}
0 commit comments