@@ -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
@@ -125,6 +126,70 @@ private function generateCurrencyMeta(ArrayAccessibleResourceBundle $supplementa
125126 return iterator_to_array ($ supplementalDataBundle ['CurrencyMeta ' ]);
126127 }
127128
129+ /**
130+ * @return array<string, array>
131+ */
132+ private function generateCurrencyMap (mixed $ supplementalDataBundle ): array
133+ {
134+ /**
135+ * @var list<string, list<string, array{from?: string, to?: string, tender?: false}>> $regionsData
136+ */
137+ $ regionsData = [];
138+
139+ foreach ($ supplementalDataBundle ['CurrencyMap ' ] as $ regionId => $ region ) {
140+ foreach ($ region as $ metadata ) {
141+ /**
142+ * Note 1: The "to" property (if present) is always greater than "from".
143+ * Note 2: The "to" property may be missing if the currency is still in use.
144+ * Note 3: The "tender" property indicates whether the country legally recognizes the currency within
145+ * its borders. This property is explicitly set to `false` only if that is not the case;
146+ * otherwise, it is `true` by default.
147+ * Note 4: The "from" and "to" dates are not stored as strings; they are stored as a pair of integers.
148+ * Note 5: The "to" property may be missing if "tender" is set to `false`.
149+ *
150+ * @var array{
151+ * from?: array{0: int, 1: int},
152+ * to?: array{0: int, 2: int},
153+ * tender?: bool,
154+ * id: string
155+ * } $metadata
156+ */
157+ $ metadata = iterator_to_array ($ metadata );
158+
159+ $ id = $ metadata ['id ' ];
160+
161+ unset($ metadata ['id ' ]);
162+
163+ if (\array_key_exists ($ id , self ::DENYLIST )) {
164+ continue ;
165+ }
166+
167+ if (\array_key_exists ('from ' , $ metadata )) {
168+ $ metadata ['from ' ] = self ::icuPairToDate ($ metadata ['from ' ]);
169+ }
170+
171+ if (\array_key_exists ('to ' , $ metadata )) {
172+ $ metadata ['to ' ] = self ::icuPairToDate ($ metadata ['to ' ]);
173+ }
174+
175+ if (\array_key_exists ('tender ' , $ metadata )) {
176+ $ metadata ['tender ' ] = filter_var ($ metadata ['tender ' ], \FILTER_VALIDATE_BOOLEAN , \FILTER_NULL_ON_FAILURE );
177+
178+ if (null === $ metadata ['tender ' ]) {
179+ throw new \RuntimeException ('Unexpected boolean value for tender attribute. ' );
180+ }
181+ }
182+
183+ $ regionsData [$ regionId ][$ id ] = $ metadata ;
184+ }
185+
186+ // Do not exclude countries with no currencies or excluded currencies (e.g. Antartica)
187+ $ regionsData [$ regionId ] ??= [];
188+ }
189+
190+ return $ regionsData ;
191+ }
192+
128193 private function generateAlpha3ToNumericMapping (ArrayAccessibleResourceBundle $ numericCodesBundle , array $ currencyCodes ): array
129194 {
130195 $ alpha3ToNumericMapping = iterator_to_array ($ numericCodesBundle ['codeMap ' ]);
@@ -152,4 +217,41 @@ private function generateNumericToAlpha3Mapping(array $alpha3ToNumericMapping):
152217
153218 return $ numericToAlpha3Mapping ;
154219 }
220+
221+ /**
222+ * Decodes ICU "date pair" into a DateTimeImmutable (UTC).
223+ *
224+ * ICU stores UDate = milliseconds since 1970-01-01T00:00:00Z in a signed 64-bit.
225+ *
226+ * @param array{0: int, 1: int} $pair
227+ */
228+ private static function icuPairToDate (array $ pair ): string
229+ {
230+ [$ highBits32 , $ lowBits32 ] = $ pair ;
231+
232+ // Recompose a 64-bit unsigned integer from two 32-bit chunks.
233+ $ unsigned64 = ((($ highBits32 & 0xFFFFFFFF ) << 32 ) | ($ lowBits32 & 0xFFFFFFFF ));
234+
235+ // Convert to signed 64-bit (two's complement) if sign bit is set.
236+ if ($ unsigned64 >= (1 << 63 )) {
237+ $ unsigned64 -= (1 << 64 );
238+ }
239+
240+ // Split into seconds and milliseconds.
241+ $ seconds = intdiv ($ unsigned64 , 1000 );
242+ $ millisecondsRemainder = $ unsigned64 - $ seconds * 1000 ;
243+
244+ // Normalize negative millisecond remainders (e.g., for pre-1970 values)
245+ if (0 > $ millisecondsRemainder ) {
246+ --$ seconds ;
247+ }
248+
249+ $ datetime = \DateTimeImmutable::createFromFormat ('U ' , $ seconds , new \DateTimeZone ('Etc/UTC ' ));
250+
251+ if (false === $ datetime ) {
252+ throw new \RuntimeException ('Unable to parse ICU milliseconds pair. ' );
253+ }
254+
255+ return $ datetime ->format ('Y-m-d ' );
256+ }
155257}
0 commit comments