Skip to content

Commit 0955438

Browse files
bug symfony#48998 [Validator] Sync IBAN formats with Swift IBAN registry (smelesh)
This PR was merged into the 5.4 branch. Discussion ---------- [Validator] Sync IBAN formats with Swift IBAN registry | Q | A | ------------- | --- | Branch? | 5.4 | Bug fix? | yes | New feature? | no | Deprecations? | no | Tickets | n/a | License | MIT | Doc PR | n/a Gathered IBAN formats from [IBAN Registry provided by SWIFT](https://www.swift.com/standards/data-standards/iban-international-bank-account-number). Some countries don't exist in the registry (Angola, Burkina Faso, Benin, Congo, Ivory Coast, Cameron, Cape Verde, Algeria, Iran, Madagascar, Mali, Mozambique, Senegal). I can't verify the format, but they are marked experimental here: https://www.iban.com/structure Some formats were changed (Burundi, Costa Rica, Kuwait, Turkey). Is it a BC break? Commits ------- d4e3047 [Validator] Sync IBAN formats with Swift IBAN registry
2 parents ddfd2ac + d4e3047 commit 0955438

File tree

4 files changed

+295
-53
lines changed

4 files changed

+295
-53
lines changed

src/Symfony/Component/Validator/.gitattributes

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,4 @@
22
/phpunit.xml.dist export-ignore
33
/.gitattributes export-ignore
44
/.gitignore export-ignore
5+
/Resources/bin/sync-iban-formats.php export-ignore

src/Symfony/Component/Validator/Constraints/IbanValidator.php

Lines changed: 71 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,6 @@
2020
* @author Manuel Reinhard <[email protected]>
2121
* @author Michael Schummel
2222
* @author Bernhard Schussek <[email protected]>
23-
*
24-
* @see http://www.michael-schummel.de/2007/10/05/iban-prufung-mit-php/
2523
*/
2624
class IbanValidator extends ConstraintValidator
2725
{
@@ -34,107 +32,135 @@ class IbanValidator extends ConstraintValidator
3432
* a BBAN (Basic Bank Account Number) which has a fixed length per country and,
3533
* included within it, a bank identifier with a fixed position and a fixed length per country
3634
*
37-
* @see https://www.swift.com/sites/default/files/resources/iban_registry.pdf
35+
* @see Resources/bin/sync-iban-formats.php
36+
* @see https://www.swift.com/swift-resource/11971/download?language=en
37+
* @see https://en.wikipedia.org/wiki/International_Bank_Account_Number
3838
*/
3939
private const FORMATS = [
40+
// auto-generated
4041
'AD' => 'AD\d{2}\d{4}\d{4}[\dA-Z]{12}', // Andorra
41-
'AE' => 'AE\d{2}\d{3}\d{16}', // United Arab Emirates
42+
'AE' => 'AE\d{2}\d{3}\d{16}', // United Arab Emirates (The)
4243
'AL' => 'AL\d{2}\d{8}[\dA-Z]{16}', // Albania
4344
'AO' => 'AO\d{2}\d{21}', // Angola
4445
'AT' => 'AT\d{2}\d{5}\d{11}', // Austria
45-
'AX' => 'FI\d{2}\d{6}\d{7}\d{1}', // Aland Islands
46+
'AX' => 'FI\d{2}\d{3}\d{11}', // Finland
4647
'AZ' => 'AZ\d{2}[A-Z]{4}[\dA-Z]{20}', // Azerbaijan
4748
'BA' => 'BA\d{2}\d{3}\d{3}\d{8}\d{2}', // Bosnia and Herzegovina
4849
'BE' => 'BE\d{2}\d{3}\d{7}\d{2}', // Belgium
49-
'BF' => 'BF\d{2}\d{23}', // Burkina Faso
50+
'BF' => 'BF\d{2}[\dA-Z]{2}\d{22}', // Burkina Faso
5051
'BG' => 'BG\d{2}[A-Z]{4}\d{4}\d{2}[\dA-Z]{8}', // Bulgaria
5152
'BH' => 'BH\d{2}[A-Z]{4}[\dA-Z]{14}', // Bahrain
52-
'BI' => 'BI\d{2}\d{12}', // Burundi
53-
'BJ' => 'BJ\d{2}[A-Z]{1}\d{23}', // Benin
54-
'BY' => 'BY\d{2}[\dA-Z]{4}\d{4}[\dA-Z]{16}', // Belarus - https://bank.codes/iban/structure/belarus/
55-
'BL' => 'FR\d{2}\d{5}\d{5}[\dA-Z]{11}\d{2}', // Saint Barthelemy
56-
'BR' => 'BR\d{2}\d{8}\d{5}\d{10}[A-Z][\dA-Z]', // Brazil
57-
'CG' => 'CG\d{2}\d{23}', // Congo
53+
'BI' => 'BI\d{2}\d{5}\d{5}\d{11}\d{2}', // Burundi
54+
'BJ' => 'BJ\d{2}[\dA-Z]{2}\d{22}', // Benin
55+
'BL' => 'FR\d{2}\d{5}\d{5}[\dA-Z]{11}\d{2}', // France
56+
'BR' => 'BR\d{2}\d{8}\d{5}\d{10}[A-Z]{1}[\dA-Z]{1}', // Brazil
57+
'BY' => 'BY\d{2}[\dA-Z]{4}\d{4}[\dA-Z]{16}', // Republic of Belarus
58+
'CF' => 'CF\d{2}\d{23}', // Central African Republic
59+
'CG' => 'CG\d{2}\d{23}', // Congo, Republic of the
5860
'CH' => 'CH\d{2}\d{5}[\dA-Z]{12}', // Switzerland
59-
'CI' => 'CI\d{2}[A-Z]{1}\d{23}', // Ivory Coast
60-
'CM' => 'CM\d{2}\d{23}', // Cameron
61-
'CR' => 'CR\d{2}0\d{3}\d{14}', // Costa Rica
62-
'CV' => 'CV\d{2}\d{21}', // Cape Verde
61+
'CI' => 'CI\d{2}[A-Z]{1}\d{23}', // Côte d'Ivoire
62+
'CM' => 'CM\d{2}\d{23}', // Cameroon
63+
'CR' => 'CR\d{2}\d{4}\d{14}', // Costa Rica
64+
'CV' => 'CV\d{2}\d{21}', // Cabo Verde
6365
'CY' => 'CY\d{2}\d{3}\d{5}[\dA-Z]{16}', // Cyprus
64-
'CZ' => 'CZ\d{2}\d{20}', // Czech Republic
66+
'CZ' => 'CZ\d{2}\d{4}\d{6}\d{10}', // Czechia
6567
'DE' => 'DE\d{2}\d{8}\d{10}', // Germany
68+
'DJ' => 'DJ\d{2}\d{5}\d{5}\d{11}\d{2}', // Djibouti
69+
'DK' => 'DK\d{2}\d{4}\d{9}\d{1}', // Denmark
6670
'DO' => 'DO\d{2}[\dA-Z]{4}\d{20}', // Dominican Republic
67-
'DK' => 'DK\d{2}\d{4}\d{10}', // Denmark
68-
'DZ' => 'DZ\d{2}\d{20}', // Algeria
71+
'DZ' => 'DZ\d{2}\d{22}', // Algeria
6972
'EE' => 'EE\d{2}\d{2}\d{2}\d{11}\d{1}', // Estonia
70-
'ES' => 'ES\d{2}\d{4}\d{4}\d{1}\d{1}\d{10}', // Spain (also includes Canary Islands, Ceuta and Melilla)
71-
'FI' => 'FI\d{2}\d{6}\d{7}\d{1}', // Finland
73+
'EG' => 'EG\d{2}\d{4}\d{4}\d{17}', // Egypt
74+
'ES' => 'ES\d{2}\d{4}\d{4}\d{1}\d{1}\d{10}', // Spain
75+
'FI' => 'FI\d{2}\d{3}\d{11}', // Finland
7276
'FO' => 'FO\d{2}\d{4}\d{9}\d{1}', // Faroe Islands
7377
'FR' => 'FR\d{2}\d{5}\d{5}[\dA-Z]{11}\d{2}', // France
74-
'GF' => 'FR\d{2}\d{5}\d{5}[\dA-Z]{11}\d{2}', // French Guyana
75-
'GB' => 'GB\d{2}[A-Z]{4}\d{6}\d{8}', // United Kingdom of Great Britain and Northern Ireland
78+
'GA' => 'GA\d{2}\d{23}', // Gabon
79+
'GB' => 'GB\d{2}[A-Z]{4}\d{6}\d{8}', // United Kingdom
7680
'GE' => 'GE\d{2}[A-Z]{2}\d{16}', // Georgia
81+
'GF' => 'FR\d{2}\d{5}\d{5}[\dA-Z]{11}\d{2}', // France
82+
'GG' => 'GB\d{2}[A-Z]{4}\d{6}\d{8}', // United Kingdom
7783
'GI' => 'GI\d{2}[A-Z]{4}[\dA-Z]{15}', // Gibraltar
7884
'GL' => 'GL\d{2}\d{4}\d{9}\d{1}', // Greenland
79-
'GP' => 'FR\d{2}\d{5}\d{5}[\dA-Z]{11}\d{2}', // Guadeloupe
85+
'GP' => 'FR\d{2}\d{5}\d{5}[\dA-Z]{11}\d{2}', // France
86+
'GQ' => 'GQ\d{2}\d{23}', // Equatorial Guinea
8087
'GR' => 'GR\d{2}\d{3}\d{4}[\dA-Z]{16}', // Greece
8188
'GT' => 'GT\d{2}[\dA-Z]{4}[\dA-Z]{20}', // Guatemala
89+
'GW' => 'GW\d{2}[\dA-Z]{2}\d{19}', // Guinea-Bissau
90+
'HN' => 'HN\d{2}[A-Z]{4}\d{20}', // Honduras
8291
'HR' => 'HR\d{2}\d{7}\d{10}', // Croatia
8392
'HU' => 'HU\d{2}\d{3}\d{4}\d{1}\d{15}\d{1}', // Hungary
8493
'IE' => 'IE\d{2}[A-Z]{4}\d{6}\d{8}', // Ireland
8594
'IL' => 'IL\d{2}\d{3}\d{3}\d{13}', // Israel
95+
'IM' => 'GB\d{2}[A-Z]{4}\d{6}\d{8}', // United Kingdom
96+
'IQ' => 'IQ\d{2}[A-Z]{4}\d{3}\d{12}', // Iraq
8697
'IR' => 'IR\d{2}\d{22}', // Iran
8798
'IS' => 'IS\d{2}\d{4}\d{2}\d{6}\d{10}', // Iceland
8899
'IT' => 'IT\d{2}[A-Z]{1}\d{5}\d{5}[\dA-Z]{12}', // Italy
100+
'JE' => 'GB\d{2}[A-Z]{4}\d{6}\d{8}', // United Kingdom
89101
'JO' => 'JO\d{2}[A-Z]{4}\d{4}[\dA-Z]{18}', // Jordan
90-
'KW' => 'KW\d{2}[A-Z]{4}\d{22}', // KUWAIT
102+
'KM' => 'KM\d{2}\d{23}', // Comoros
103+
'KW' => 'KW\d{2}[A-Z]{4}[\dA-Z]{22}', // Kuwait
91104
'KZ' => 'KZ\d{2}\d{3}[\dA-Z]{13}', // Kazakhstan
92-
'LB' => 'LB\d{2}\d{4}[\dA-Z]{20}', // LEBANON
93-
'LI' => 'LI\d{2}\d{5}[\dA-Z]{12}', // Liechtenstein (Principality of)
105+
'LB' => 'LB\d{2}\d{4}[\dA-Z]{20}', // Lebanon
106+
'LC' => 'LC\d{2}[A-Z]{4}[\dA-Z]{24}', // Saint Lucia
107+
'LI' => 'LI\d{2}\d{5}[\dA-Z]{12}', // Liechtenstein
94108
'LT' => 'LT\d{2}\d{5}\d{11}', // Lithuania
95109
'LU' => 'LU\d{2}\d{3}[\dA-Z]{13}', // Luxembourg
96110
'LV' => 'LV\d{2}[A-Z]{4}[\dA-Z]{13}', // Latvia
111+
'LY' => 'LY\d{2}\d{3}\d{3}\d{15}', // Libya
112+
'MA' => 'MA\d{2}\d{24}', // Morocco
97113
'MC' => 'MC\d{2}\d{5}\d{5}[\dA-Z]{11}\d{2}', // Monaco
98114
'MD' => 'MD\d{2}[\dA-Z]{2}[\dA-Z]{18}', // Moldova
99115
'ME' => 'ME\d{2}\d{3}\d{13}\d{2}', // Montenegro
100-
'MF' => 'FR\d{2}\d{5}\d{5}[\dA-Z]{11}\d{2}', // Saint Martin (French part)
116+
'MF' => 'FR\d{2}\d{5}\d{5}[\dA-Z]{11}\d{2}', // France
101117
'MG' => 'MG\d{2}\d{23}', // Madagascar
102-
'MK' => 'MK\d{2}\d{3}[\dA-Z]{10}\d{2}', // Macedonia, Former Yugoslav Republic of
103-
'ML' => 'ML\d{2}[A-Z]{1}\d{23}', // Mali
104-
'MQ' => 'FR\d{2}\d{5}\d{5}[\dA-Z]{11}\d{2}', // Martinique
118+
'MK' => 'MK\d{2}\d{3}[\dA-Z]{10}\d{2}', // Macedonia
119+
'ML' => 'ML\d{2}[\dA-Z]{2}\d{22}', // Mali
120+
'MQ' => 'FR\d{2}\d{5}\d{5}[\dA-Z]{11}\d{2}', // France
105121
'MR' => 'MR\d{2}\d{5}\d{5}\d{11}\d{2}', // Mauritania
106122
'MT' => 'MT\d{2}[A-Z]{4}\d{5}[\dA-Z]{18}', // Malta
107123
'MU' => 'MU\d{2}[A-Z]{4}\d{2}\d{2}\d{12}\d{3}[A-Z]{3}', // Mauritius
108124
'MZ' => 'MZ\d{2}\d{21}', // Mozambique
109-
'NC' => 'FR\d{2}\d{5}\d{5}[\dA-Z]{11}\d{2}', // New Caledonia
110-
'NL' => 'NL\d{2}[A-Z]{4}\d{10}', // The Netherlands
125+
'NC' => 'FR\d{2}\d{5}\d{5}[\dA-Z]{11}\d{2}', // France
126+
'NE' => 'NE\d{2}[A-Z]{2}\d{22}', // Niger
127+
'NI' => 'NI\d{2}[A-Z]{4}\d{24}', // Nicaragua
128+
'NL' => 'NL\d{2}[A-Z]{4}\d{10}', // Netherlands (The)
111129
'NO' => 'NO\d{2}\d{4}\d{6}\d{1}', // Norway
112-
'PF' => 'FR\d{2}\d{5}\d{5}[\dA-Z]{11}\d{2}', // French Polynesia
130+
'PF' => 'FR\d{2}\d{5}\d{5}[\dA-Z]{11}\d{2}', // France
113131
'PK' => 'PK\d{2}[A-Z]{4}[\dA-Z]{16}', // Pakistan
114132
'PL' => 'PL\d{2}\d{8}\d{16}', // Poland
115-
'PM' => 'FR\d{2}\d{5}\d{5}[\dA-Z]{11}\d{2}', // Saint Pierre et Miquelon
133+
'PM' => 'FR\d{2}\d{5}\d{5}[\dA-Z]{11}\d{2}', // France
116134
'PS' => 'PS\d{2}[A-Z]{4}[\dA-Z]{21}', // Palestine, State of
117-
'PT' => 'PT\d{2}\d{4}\d{4}\d{11}\d{2}', // Portugal (plus Azores and Madeira)
135+
'PT' => 'PT\d{2}\d{4}\d{4}\d{11}\d{2}', // Portugal
118136
'QA' => 'QA\d{2}[A-Z]{4}[\dA-Z]{21}', // Qatar
119-
'RE' => 'FR\d{2}\d{5}\d{5}[\dA-Z]{11}\d{2}', // Reunion
137+
'RE' => 'FR\d{2}\d{5}\d{5}[\dA-Z]{11}\d{2}', // France
120138
'RO' => 'RO\d{2}[A-Z]{4}[\dA-Z]{16}', // Romania
121139
'RS' => 'RS\d{2}\d{3}\d{13}\d{2}', // Serbia
140+
'RU' => 'RU\d{2}\d{9}\d{5}[\dA-Z]{15}', // Russia
122141
'SA' => 'SA\d{2}\d{2}[\dA-Z]{18}', // Saudi Arabia
142+
'SC' => 'SC\d{2}[A-Z]{4}\d{2}\d{2}\d{16}[A-Z]{3}', // Seychelles
143+
'SD' => 'SD\d{2}\d{2}\d{12}', // Sudan
123144
'SE' => 'SE\d{2}\d{3}\d{16}\d{1}', // Sweden
124145
'SI' => 'SI\d{2}\d{5}\d{8}\d{2}', // Slovenia
125-
'SK' => 'SK\d{2}\d{4}\d{6}\d{10}', // Slovak Republic
146+
'SK' => 'SK\d{2}\d{4}\d{6}\d{10}', // Slovakia
126147
'SM' => 'SM\d{2}[A-Z]{1}\d{5}\d{5}[\dA-Z]{12}', // San Marino
127-
'SN' => 'SN\d{2}[A-Z]{1}\d{23}', // Senegal
128-
'TF' => 'FR\d{2}\d{5}\d{5}[\dA-Z]{11}\d{2}', // French Southern Territories
148+
'SN' => 'SN\d{2}[A-Z]{2}\d{22}', // Senegal
149+
'SO' => 'SO\d{2}\d{4}\d{3}\d{12}', // Somalia
150+
'ST' => 'ST\d{2}\d{4}\d{4}\d{11}\d{2}', // Sao Tome and Principe
151+
'SV' => 'SV\d{2}[A-Z]{4}\d{20}', // El Salvador
152+
'TD' => 'TD\d{2}\d{23}', // Chad
153+
'TF' => 'FR\d{2}\d{5}\d{5}[\dA-Z]{11}\d{2}', // France
154+
'TG' => 'TG\d{2}[A-Z]{2}\d{22}', // Togo
129155
'TL' => 'TL\d{2}\d{3}\d{14}\d{2}', // Timor-Leste
130156
'TN' => 'TN\d{2}\d{2}\d{3}\d{13}\d{2}', // Tunisia
131-
'TR' => 'TR\d{2}\d{5}[\dA-Z]{1}[\dA-Z]{16}', // Turkey
157+
'TR' => 'TR\d{2}\d{5}\d{1}[\dA-Z]{16}', // Turkey
132158
'UA' => 'UA\d{2}\d{6}[\dA-Z]{19}', // Ukraine
133159
'VA' => 'VA\d{2}\d{3}\d{15}', // Vatican City State
134-
'VG' => 'VG\d{2}[A-Z]{4}\d{16}', // Virgin Islands, British
135-
'WF' => 'FR\d{2}\d{5}\d{5}[\dA-Z]{11}\d{2}', // Wallis and Futuna Islands
136-
'XK' => 'XK\d{2}\d{4}\d{10}\d{2}', // Republic of Kosovo
137-
'YT' => 'FR\d{2}\d{5}\d{5}[\dA-Z]{11}\d{2}', // Mayotte
160+
'VG' => 'VG\d{2}[A-Z]{4}\d{16}', // Virgin Islands
161+
'WF' => 'FR\d{2}\d{5}\d{5}[\dA-Z]{11}\d{2}', // France
162+
'XK' => 'XK\d{2}\d{4}\d{10}\d{2}', // Kosovo
163+
'YT' => 'FR\d{2}\d{5}\d{5}[\dA-Z]{11}\d{2}', // France
138164
];
139165

140166
/**
Lines changed: 204 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,204 @@
1+
#!/usr/bin/env php
2+
<?php
3+
4+
/*
5+
* This file is part of the Symfony package.
6+
*
7+
* (c) Fabien Potencier <[email protected]>
8+
*
9+
* For the full copyright and license information, please view the LICENSE
10+
* file that was distributed with this source code.
11+
*/
12+
13+
if ('cli' !== \PHP_SAPI) {
14+
throw new \Exception('This script must be run from the command line.');
15+
}
16+
17+
/*
18+
* This script syncs IBAN formats from the upstream and updates them into IbanValidator.
19+
*
20+
* Usage:
21+
* php Resources/bin/sync-iban-formats.php
22+
*/
23+
24+
error_reporting(\E_ALL);
25+
26+
set_error_handler(static function (int $type, string $msg, string $file, int $line): void {
27+
throw new \ErrorException($msg, 0, $type, $file, $line);
28+
});
29+
30+
echo "Collecting IBAN formats...\n";
31+
32+
$formats = array_merge(
33+
(new UncyclopediaIbanProvider())->getIbanFormats(),
34+
(new SwiftRegistryIbanProvider())->getIbanFormats()
35+
);
36+
37+
printf("Collected %d IBAN formats\n", count($formats));
38+
39+
echo "Updating validator...\n";
40+
41+
updateValidatorFormats(__DIR__.'/../../Constraints/IbanValidator.php', $formats);
42+
43+
echo "Done.\n";
44+
45+
exit(0);
46+
47+
function updateValidatorFormats(string $validatorPath, array $formats): void
48+
{
49+
ksort($formats);
50+
51+
$formatsContent = "[\n";
52+
$formatsContent .= " // auto-generated\n";
53+
54+
foreach ($formats as $countryCode => [$format, $country]) {
55+
$formatsContent .= " '{$countryCode}' => '{$format}', // {$country}\n";
56+
}
57+
58+
$formatsContent .= ' ]';
59+
60+
$validatorContent = file_get_contents($validatorPath);
61+
62+
$validatorContent = preg_replace(
63+
'/FORMATS = \[.*?\];/s',
64+
"FORMATS = {$formatsContent};",
65+
$validatorContent
66+
);
67+
68+
file_put_contents($validatorPath, $validatorContent);
69+
}
70+
71+
final class SwiftRegistryIbanProvider
72+
{
73+
/**
74+
* @return array<string, array{string, string}>
75+
*/
76+
public function getIbanFormats(): array
77+
{
78+
$items = $this->readPropertiesFromRegistry([
79+
'Name of country' => 'country',
80+
'IBAN prefix country code (ISO 3166)' => 'country_code',
81+
'IBAN structure' => 'iban_structure',
82+
'Country code includes other countries/territories' => 'included_country_codes',
83+
]);
84+
85+
$formats = [];
86+
87+
foreach ($items as $item) {
88+
$formats[$item['country_code']] = [$this->buildIbanRegexp($item['iban_structure']), $item['country']];
89+
90+
foreach ($this->parseCountryCodesList($item['included_country_codes']) as $includedCountryCode) {
91+
$formats[$includedCountryCode] = $formats[$item['country_code']];
92+
}
93+
}
94+
95+
return $formats;
96+
}
97+
98+
/**
99+
* @return list<string>
100+
*/
101+
private function parseCountryCodesList(string $countryCodesList): array
102+
{
103+
if ('N/A' === $countryCodesList) {
104+
return [];
105+
}
106+
107+
$countryCodes = [];
108+
109+
foreach (explode(',', $countryCodesList) as $countryCode) {
110+
$countryCodes[] = preg_replace('/^([A-Z]{2})(\s+\(.+?\))?$/', '$1', trim($countryCode));
111+
}
112+
113+
return $countryCodes;
114+
}
115+
116+
/**
117+
* @param array<string, string> $properties
118+
*
119+
* @return list<array<string, string>>
120+
*/
121+
private function readPropertiesFromRegistry(array $properties): array
122+
{
123+
$items = [];
124+
125+
$registryContent = file_get_contents('https://www.swift.com/swift-resource/11971/download');
126+
$lines = explode("\n", $registryContent);
127+
128+
// skip header line
129+
array_shift($lines);
130+
131+
foreach ($lines as $line) {
132+
$columns = str_getcsv($line, "\t");
133+
$propertyLabel = array_shift($columns);
134+
135+
if (!isset($properties[$propertyLabel])) {
136+
continue;
137+
}
138+
139+
$propertyField = $properties[$propertyLabel];
140+
141+
foreach ($columns as $index => $value) {
142+
$items[$index][$propertyField] = $value;
143+
}
144+
}
145+
146+
return array_values($items);
147+
}
148+
149+
private function buildIbanRegexp(string $ibanStructure): string
150+
{
151+
$pattern = $ibanStructure;
152+
153+
$pattern = preg_replace('/(\d+)!n/', '\\d{$1}', $pattern);
154+
$pattern = preg_replace('/(\d+)!a/', '[A-Z]{$1}', $pattern);
155+
$pattern = preg_replace('/(\d+)!c/', '[\\dA-Z]{$1}', $pattern);
156+
157+
return $pattern;
158+
}
159+
}
160+
161+
final class UncyclopediaIbanProvider
162+
{
163+
/**
164+
* @return array<string, array{string, string}>
165+
*/
166+
public function getIbanFormats(): array
167+
{
168+
$formats = [];
169+
170+
foreach ($this->readIbanFormatsTable() as $item) {
171+
if (!preg_match('/^([A-Z]{2})/', $item['Example'], $matches)) {
172+
continue;
173+
}
174+
175+
$countryCode = $matches[1];
176+
177+
$formats[$countryCode] = [$this->buildIbanRegexp($countryCode, $item['BBAN Format']), $item['Country']];
178+
}
179+
180+
return $formats;
181+
}
182+
183+
/**
184+
* @return list<array<string, string|int>>
185+
*/
186+
private function readIbanFormatsTable(): array
187+
{
188+
$tablesResponse = file_get_contents('https://www.wikitable2json.com/api/International_Bank_Account_Number?table=3&keyRows=1&clearRef=true');
189+
190+
return json_decode($tablesResponse, true, 512, JSON_THROW_ON_ERROR)[0];
191+
}
192+
193+
private function buildIbanRegexp(string $countryCode, string $bbanFormat): string
194+
{
195+
$pattern = $bbanFormat;
196+
197+
$pattern = preg_replace('/\s*,\s*/', '', $pattern);
198+
$pattern = preg_replace('/(\d+)n/', '\\d{$1}', $pattern);
199+
$pattern = preg_replace('/(\d+)a/', '[A-Z]{$1}', $pattern);
200+
$pattern = preg_replace('/(\d+)c/', '[\\dA-Z]{$1}', $pattern);
201+
202+
return $countryCode.'\\d{2}'.$pattern;
203+
}
204+
}

0 commit comments

Comments
 (0)