@@ -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}
0 commit comments