diff --git a/IbanTools.php b/IbanTools.php new file mode 100644 index 0000000..4d079d9 --- /dev/null +++ b/IbanTools.php @@ -0,0 +1,618 @@ + + */ +class IbanTools +{ + private string $iban; + private string $normalized; + private ?array $countryInfo = null; + + /** + * @var array + */ + private const REGISTRY = [ + 'AD' => ['n' => 'Andorra', 'il' => 24, 'bl' => 20, 'r' => '^AD(\d{2})(\d{4})(\d{4})([A-Za-z0-9]{12})$', 'bks' => 0, 'bke' => 3, 'brs' => 4, 'bre' => 7, 's' => true, 'cs' => NULL, 'ce' => NULL], + 'AE' => ['n' => 'United Arab Emirates (The)', 'il' => 23, 'bl' => 19, 'r' => '^AE(\d{2})(\d{3})(\d{16})$', 'bks' => 0, 'bke' => 2, 'brs' => NULL, 'bre' => NULL, 's' => false, 'cs' => NULL, 'ce' => NULL], + 'AL' => ['n' => 'Albania', 'il' => 28, 'bl' => 24, 'r' => '^AL(\d{2})(\d{8})([A-Za-z0-9]{16})$', 'bks' => 0, 'bke' => 2, 'brs' => NULL, 'bre' => NULL, 's' => false, 'cs' => NULL, 'ce' => NULL], + 'AT' => ['n' => 'Austria', 'il' => 20, 'bl' => 16, 'r' => '^AT(\d{2})(\d{5})(\d{11})$', 'bks' => 0, 'bke' => 4, 'brs' => NULL, 'bre' => NULL, 's' => true, 'cs' => NULL, 'ce' => NULL], + 'AZ' => ['n' => 'Azerbaijan', 'il' => 28, 'bl' => 24, 'r' => '^AZ(\d{2})([A-Z]{4})([A-Za-z0-9]{20})$', 'bks' => 0, 'bke' => 3, 'brs' => NULL, 'bre' => NULL, 's' => false, 'cs' => NULL, 'ce' => NULL], + 'BA' => ['n' => 'Bosnia and Herzegovina', 'il' => 20, 'bl' => 16, 'r' => '^BA(\d{2})(\d{3})(\d{3})(\d{8})(\d{2})$', 'bks' => 0, 'bke' => 2, 'brs' => 3, 'bre' => 5, 's' => false, 'cs' => 14, 'ce' => 15], + 'BE' => ['n' => 'Belgium', 'il' => 16, 'bl' => 12, 'r' => '^BE(\d{2})(\d{3})(\d{7})(\d{2})$', 'bks' => 0, 'bke' => 2, 'brs' => NULL, 'bre' => NULL, 's' => true, 'cs' => 10, 'ce' => 11], + 'BG' => ['n' => 'Bulgaria', 'il' => 22, 'bl' => 18, 'r' => '^BG(\d{2})([A-Z]{4})(\d{4})(\d{2})([A-Za-z0-9]{8})$', 'bks' => 0, 'bke' => 3, 'brs' => 4, 'bre' => 7, 's' => true, 'cs' => NULL, 'ce' => NULL], + 'BH' => ['n' => 'Bahrain', 'il' => 22, 'bl' => 18, 'r' => '^BH(\d{2})([A-Z]{4})([A-Za-z0-9]{14})$', 'bks' => 0, 'bke' => 3, 'brs' => NULL, 'bre' => NULL, 's' => false, 'cs' => NULL, 'ce' => NULL], + 'BI' => ['n' => 'Burundi', 'il' => 27, 'bl' => 23, 'r' => '^BI(\d{2})(\d{5})(\d{5})(\d{11})(\d{2})$', 'bks' => 0, 'bke' => 4, 'brs' => 5, 'bre' => 9, 's' => false, 'cs' => NULL, 'ce' => NULL], + 'BL' => ['n' => 'Saint Barthelemy', 'il' => 27, 'bl' => 23, 'r' => '^BL(\d{2})(\d{5})(\d{5})([A-Za-z0-9]{11})(\d{2})$', 'bks' => 0, 'bke' => 4, 'brs' => 5, 'bre' => 9, 's' => true, 'cs' => 21, 'ce' => 22], + 'BR' => ['n' => 'Brazil', 'il' => 29, 'bl' => 25, 'r' => '^BR(\d{2})(\d{8})(\d{5})(\d{10})([A-Z]{1})([A-Za-z0-9]{1})$', 'bks' => 0, 'bke' => 7, 'brs' => 8, 'bre' => 12, 's' => false, 'cs' => NULL, 'ce' => NULL], + 'BY' => ['n' => 'Belarus', 'il' => 28, 'bl' => 24, 'r' => '^BY(\d{2})([A-Za-z0-9]{4})(\d{4})([A-Za-z0-9]{16})$', 'bks' => 0, 'bke' => 3, 'brs' => 4, 'bre' => 7, 's' => false, 'cs' => NULL, 'ce' => NULL], + 'CH' => ['n' => 'Switzerland', 'il' => 21, 'bl' => 17, 'r' => '^CH(\d{2})(\d{5})([A-Za-z0-9]{12})$', 'bks' => 0, 'bke' => 4, 'brs' => NULL, 'bre' => NULL, 's' => true, 'cs' => NULL, 'ce' => NULL], + 'CR' => ['n' => 'Costa Rica', 'il' => 22, 'bl' => 18, 'r' => '^CR(\d{2})(\d{4})(\d{14})$', 'bks' => 0, 'bke' => 3, 'brs' => NULL, 'bre' => NULL, 's' => false, 'cs' => NULL, 'ce' => NULL], + 'CY' => ['n' => 'Cyprus', 'il' => 28, 'bl' => 24, 'r' => '^CY(\d{2})(\d{3})(\d{5})([A-Za-z0-9]{16})$', 'bks' => 0, 'bke' => 2, 'brs' => 3, 'bre' => 7, 's' => true, 'cs' => NULL, 'ce' => NULL], + 'CZ' => ['n' => 'Czechia', 'il' => 24, 'bl' => 20, 'r' => '^CZ(\d{2})(\d{4})(\d{6})(\d{10})$', 'bks' => 0, 'bke' => 3, 'brs' => NULL, 'bre' => NULL, 's' => true, 'cs' => NULL, 'ce' => NULL], + 'DE' => ['n' => 'Germany', 'il' => 22, 'bl' => 18, 'r' => '^DE(\d{2})(\d{8})(\d{10})$', 'bks' => 0, 'bke' => 7, 'brs' => NULL, 'bre' => NULL, 's' => true, 'cs' => NULL, 'ce' => NULL], + 'DJ' => ['n' => 'Djibouti', 'il' => 27, 'bl' => 23, 'r' => '^DJ(\d{2})(\d{5})(\d{5})(\d{11})(\d{2})$', 'bks' => 0, 'bke' => 4, 'brs' => 5, 'bre' => 9, 's' => false, 'cs' => 21, 'ce' => 23], + 'DK' => ['n' => 'Denmark', 'il' => 18, 'bl' => 14, 'r' => '^DK(\d{2})(\d{4})(\d{9})(\d{1})$', 'bks' => 0, 'bke' => 3, 'brs' => NULL, 'bre' => NULL, 's' => true, 'cs' => NULL, 'ce' => NULL], + 'DO' => ['n' => 'Dominican Republic', 'il' => 28, 'bl' => 24, 'r' => '^DO(\d{2})([A-Za-z0-9]{4})(\d{20})$', 'bks' => 0, 'bke' => 3, 'brs' => NULL, 'bre' => NULL, 's' => false, 'cs' => NULL, 'ce' => NULL], + 'EE' => ['n' => 'Estonia', 'il' => 20, 'bl' => 16, 'r' => '^EE(\d{2})(\d{2})(\d{14})$', 'bks' => 0, 'bke' => 1, 'brs' => NULL, 'bre' => NULL, 's' => true, 'cs' => 15, 'ce' => 15], + 'EG' => ['n' => 'Egypt', 'il' => 29, 'bl' => 25, 'r' => '^EG(\d{2})(\d{4})(\d{4})(\d{17})$', 'bks' => 0, 'bke' => 3, 'brs' => 4, 'bre' => 7, 's' => false, 'cs' => NULL, 'ce' => NULL], + 'ES' => ['n' => 'Spain', 'il' => 24, 'bl' => 20, 'r' => '^ES(\d{2})(\d{4})(\d{4})(\d{1})(\d{1})(\d{10})$', 'bks' => 0, 'bke' => 3, 'brs' => 4, 'bre' => 7, 's' => true, 'cs' => 8, 'ce' => 9], + 'FI' => ['n' => 'Finland', 'il' => 18, 'bl' => 14, 'r' => '^FI(\d{2})(\d{3})(\d{11})$', 'bks' => 0, 'bke' => 2, 'brs' => NULL, 'bre' => NULL, 's' => true, 'cs' => 13, 'ce' => 13], + 'FK' => ['n' => 'Falkland Islands (Malvinas)', 'il' => 18, 'bl' => 14, 'r' => '^FK(\d{2})([A-Z]{2})(\d{12})$', 'bks' => 0, 'bke' => 1, 'brs' => NULL, 'bre' => NULL, 's' => false, 'cs' => NULL, 'ce' => NULL], + 'FO' => ['n' => 'Faroe Islands', 'il' => 18, 'bl' => 14, 'r' => '^FO(\d{2})(\d{4})(\d{9})(\d{1})$', 'bks' => 0, 'bke' => 3, 'brs' => NULL, 'bre' => NULL, 's' => false, 'cs' => 13, 'ce' => 13], + 'FR' => ['n' => 'France', 'il' => 27, 'bl' => 23, 'r' => '^FR(\d{2})(\d{5})(\d{5})([A-Za-z0-9]{11})(\d{2})$', 'bks' => 0, 'bke' => 4, 'brs' => 5, 'bre' => 9, 's' => true, 'cs' => 21, 'ce' => 22], + 'GB' => ['n' => 'United Kingdom', 'il' => 22, 'bl' => 18, 'r' => '^GB(\d{2})([A-Z]{4})(\d{6})(\d{8})$', 'bks' => 0, 'bke' => 3, 'brs' => 4, 'bre' => 9, 's' => true, 'cs' => NULL, 'ce' => NULL], + 'GE' => ['n' => 'Georgia', 'il' => 22, 'bl' => 18, 'r' => '^GE(\d{2})([A-Z]{2})(\d{16})$', 'bks' => 0, 'bke' => 1, 'brs' => NULL, 'bre' => NULL, 's' => false, 'cs' => NULL, 'ce' => NULL], + 'GF' => ['n' => 'French Guyana', 'il' => 27, 'bl' => 23, 'r' => '^GF(\d{2})(\d{5})(\d{5})([A-Za-z0-9]{11})(\d{2})$', 'bks' => 0, 'bke' => 4, 'brs' => 5, 'bre' => 9, 's' => true, 'cs' => 21, 'ce' => 22], + 'GI' => ['n' => 'Gibraltar', 'il' => 23, 'bl' => 19, 'r' => '^GI(\d{2})([A-Z]{4})([A-Za-z0-9]{15})$', 'bks' => 0, 'bke' => 3, 'brs' => NULL, 'bre' => NULL, 's' => true, 'cs' => NULL, 'ce' => NULL], + 'GL' => ['n' => 'Greenland', 'il' => 18, 'bl' => 14, 'r' => '^GL(\d{2})(\d{4})(\d{9})(\d{1})$', 'bks' => 0, 'bke' => 3, 'brs' => NULL, 'bre' => NULL, 's' => false, 'cs' => NULL, 'ce' => NULL], + 'GP' => ['n' => 'Guadelope', 'il' => 27, 'bl' => 23, 'r' => '^GP(\d{2})(\d{5})(\d{5})([A-Za-z0-9]{11})(\d{2})$', 'bks' => 0, 'bke' => 4, 'brs' => 5, 'bre' => 9, 's' => true, 'cs' => 21, 'ce' => 22], + 'GR' => ['n' => 'Greece', 'il' => 27, 'bl' => 23, 'r' => '^GR(\d{2})(\d{3})(\d{4})([A-Za-z0-9]{16})$', 'bks' => 0, 'bke' => 2, 'brs' => 3, 'bre' => 6, 's' => true, 'cs' => NULL, 'ce' => NULL], + 'GT' => ['n' => 'Guatemala', 'il' => 28, 'bl' => 24, 'r' => '^GT(\d{2})([A-Za-z0-9]{4})([A-Za-z0-9]{20})$', 'bks' => 0, 'bke' => 3, 'brs' => NULL, 'bre' => NULL, 's' => false, 'cs' => NULL, 'ce' => NULL], + 'HN' => ['n' => 'Honduras', 'il' => 28, 'bl' => 24, 'r' => '^HN(\d{2})([A-Z]{4})(\d{20})$', 'bks' => 0, 'bke' => 3, 'brs' => NULL, 'bre' => NULL, 's' => false, 'cs' => NULL, 'ce' => NULL], + 'HR' => ['n' => 'Croatia', 'il' => 21, 'bl' => 17, 'r' => '^HR(\d{2})(\d{7})(\d{10})$', 'bks' => 0, 'bke' => 6, 'brs' => NULL, 'bre' => NULL, 's' => true, 'cs' => NULL, 'ce' => NULL], + 'HU' => ['n' => 'Hungary', 'il' => 28, 'bl' => 24, 'r' => '^HU(\d{2})(\d{3})(\d{4})(\d{1})(\d{15})(\d{1})$', 'bks' => 0, 'bke' => 2, 'brs' => 3, 'bre' => 6, 's' => true, 'cs' => 23, 'ce' => 23], + 'IE' => ['n' => 'Ireland', 'il' => 22, 'bl' => 18, 'r' => '^IE(\d{2})([A-Z]{4})(\d{6})(\d{8})$', 'bks' => 0, 'bke' => 3, 'brs' => 4, 'bre' => 9, 's' => true, 'cs' => NULL, 'ce' => NULL], + 'IL' => ['n' => 'Israel', 'il' => 23, 'bl' => 19, 'r' => '^IL(\d{2})(\d{3})(\d{3})(\d{13})$', 'bks' => 0, 'bke' => 2, 'brs' => 3, 'bre' => 5, 's' => false, 'cs' => NULL, 'ce' => NULL], + 'IQ' => ['n' => 'Iraq', 'il' => 23, 'bl' => 19, 'r' => '^IQ(\d{2})([A-Z]{4})(\d{3})(\d{12})$', 'bks' => 0, 'bke' => 3, 'brs' => 4, 'bre' => 6, 's' => false, 'cs' => NULL, 'ce' => NULL], + 'IS' => ['n' => 'Iceland', 'il' => 26, 'bl' => 22, 'r' => '^IS(\d{2})(\d{4})(\d{2})(\d{6})(\d{10})$', 'bks' => 0, 'bke' => 1, 'brs' => 4, 'bre' => 9, 's' => true, 'cs' => NULL, 'ce' => NULL], + 'IT' => ['n' => 'Italy', 'il' => 27, 'bl' => 23, 'r' => '^IT(\d{2})([A-Z]{1})(\d{5})(\d{5})([A-Za-z0-9]{12})$', 'bks' => 0, 'bke' => 5, 'brs' => 6, 'bre' => 10, 's' => true, 'cs' => 0, 'ce' => 0], + 'JO' => ['n' => 'Jordan', 'il' => 30, 'bl' => 26, 'r' => '^JO(\d{2})([A-Z]{4})(\d{4})([A-Za-z0-9]{18})$', 'bks' => 0, 'bke' => 3, 'brs' => 4, 'bre' => 7, 's' => false, 'cs' => NULL, 'ce' => NULL], + 'KW' => ['n' => 'Kuwait', 'il' => 30, 'bl' => 26, 'r' => '^KW(\d{2})([A-Z]{4})([A-Za-z0-9]{22})$', 'bks' => 0, 'bke' => 3, 'brs' => NULL, 'bre' => NULL, 's' => false, 'cs' => NULL, 'ce' => NULL], + 'KZ' => ['n' => 'Kazakhstan', 'il' => 20, 'bl' => 16, 'r' => '^KZ(\d{2})(\d{3})([A-Za-z0-9]{13})$', 'bks' => 0, 'bke' => 2, 'brs' => NULL, 'bre' => NULL, 's' => false, 'cs' => NULL, 'ce' => NULL], + 'LB' => ['n' => 'Lebanon', 'il' => 28, 'bl' => 24, 'r' => '^LB(\d{2})(\d{4})([A-Za-z0-9]{20})$', 'bks' => 0, 'bke' => 3, 'brs' => NULL, 'bre' => NULL, 's' => false, 'cs' => NULL, 'ce' => NULL], + 'LC' => ['n' => 'Saint Lucia', 'il' => 32, 'bl' => 28, 'r' => '^LC(\d{2})([A-Z]{4})([A-Za-z0-9]{24})$', 'bks' => 0, 'bke' => 3, 'brs' => NULL, 'bre' => NULL, 's' => false, 'cs' => NULL, 'ce' => NULL], + 'LI' => ['n' => 'Liechtenstein', 'il' => 21, 'bl' => 17, 'r' => '^LI(\d{2})(\d{5})([A-Za-z0-9]{12})$', 'bks' => 0, 'bke' => 4, 'brs' => NULL, 'bre' => NULL, 's' => true, 'cs' => NULL, 'ce' => NULL], + 'LT' => ['n' => 'Lithuania', 'il' => 20, 'bl' => 16, 'r' => '^LT(\d{2})(\d{5})(\d{11})$', 'bks' => 0, 'bke' => 4, 'brs' => NULL, 'bre' => NULL, 's' => true, 'cs' => NULL, 'ce' => NULL], + 'LU' => ['n' => 'Luxembourg', 'il' => 20, 'bl' => 16, 'r' => '^LU(\d{2})(\d{3})([A-Za-z0-9]{13})$', 'bks' => 0, 'bke' => 2, 'brs' => NULL, 'bre' => NULL, 's' => true, 'cs' => 14, 'ce' => 15], + 'LV' => ['n' => 'Latvia', 'il' => 21, 'bl' => 17, 'r' => '^LV(\d{2})([A-Z]{4})([A-Za-z0-9]{13})$', 'bks' => 0, 'bke' => 3, 'brs' => NULL, 'bre' => NULL, 's' => true, 'cs' => NULL, 'ce' => NULL], + 'LY' => ['n' => 'Libya', 'il' => 25, 'bl' => 21, 'r' => '^LY(\d{2})(\d{3})(\d{3})(\d{15})$', 'bks' => 0, 'bke' => 2, 'brs' => 3, 'bre' => 5, 's' => false, 'cs' => NULL, 'ce' => NULL], + 'MC' => ['n' => 'Monaco', 'il' => 27, 'bl' => 23, 'r' => '^MC(\d{2})(\d{5})(\d{5})([A-Za-z0-9]{11})(\d{2})$', 'bks' => 0, 'bke' => 4, 'brs' => 5, 'bre' => 9, 's' => true, 'cs' => 21, 'ce' => 22], + 'MD' => ['n' => 'Moldova, Republic of', 'il' => 24, 'bl' => 20, 'r' => '^MD(\d{2})([A-Za-z0-9]{2})([A-Za-z0-9]{18})$', 'bks' => 0, 'bke' => 1, 'brs' => NULL, 'bre' => NULL, 's' => false, 'cs' => NULL, 'ce' => NULL], + 'ME' => ['n' => 'Montenegro', 'il' => 22, 'bl' => 18, 'r' => '^ME(\d{2})(\d{3})(\d{13})(\d{2})$', 'bks' => 0, 'bke' => 2, 'brs' => NULL, 'bre' => NULL, 's' => false, 'cs' => 16, 'ce' => 17], + 'MF' => ['n' => 'Saint Martin (French Part)', 'il' => 27, 'bl' => 23, 'r' => '^MF(\d{2})(\d{5})(\d{5})([A-Za-z0-9]{11})(\d{2})$', 'bks' => 0, 'bke' => 4, 'brs' => 5, 'bre' => 9, 's' => true, 'cs' => 21, 'ce' => 22], + 'MK' => ['n' => 'North Macedonia', 'il' => 19, 'bl' => 15, 'r' => '^MK(\d{2})(\d{3})([A-Za-z0-9]{10})(\d{2})$', 'bks' => 0, 'bke' => 2, 'brs' => NULL, 'bre' => NULL, 's' => false, 'cs' => 13, 'ce' => 14], + 'MN' => ['n' => 'Mongolia', 'il' => 20, 'bl' => 16, 'r' => '^MN(\d{2})(\d{4})(\d{12})$', 'bks' => 0, 'bke' => 3, 'brs' => NULL, 'bre' => NULL, 's' => false, 'cs' => NULL, 'ce' => NULL], + 'MQ' => ['n' => 'Martinique', 'il' => 27, 'bl' => 23, 'r' => '^MQ(\d{2})(\d{5})(\d{5})([A-Za-z0-9]{11})(\d{2})$', 'bks' => 0, 'bke' => 4, 'brs' => 5, 'bre' => 9, 's' => true, 'cs' => 21, 'ce' => 22], + 'MR' => ['n' => 'Mauritania', 'il' => 27, 'bl' => 23, 'r' => '^MR(\d{2})(\d{5})(\d{5})(\d{11})(\d{2})$', 'bks' => 0, 'bke' => 4, 'brs' => 5, 'bre' => 9, 's' => false, 'cs' => 21, 'ce' => 22], + 'MT' => ['n' => 'Malta', 'il' => 31, 'bl' => 27, 'r' => '^MT(\d{2})([A-Z]{4})(\d{5})([A-Za-z0-9]{18})$', 'bks' => 0, 'bke' => 3, 'brs' => 4, 'bre' => 8, 's' => true, 'cs' => NULL, 'ce' => NULL], + 'MU' => ['n' => 'Mauritius', 'il' => 30, 'bl' => 26, 'r' => '^MU(\d{2})([A-Z]{4})(\d{2})(\d{2})(\d{12})(\d{3})([A-Z]{3})$', 'bks' => 0, 'bke' => 5, 'brs' => 10, 'bre' => 21, 's' => false, 'cs' => NULL, 'ce' => NULL], + 'NC' => ['n' => 'New Caledonia', 'il' => 27, 'bl' => 23, 'r' => '^NC(\d{2})(\d{5})(\d{5})([A-Za-z0-9]{11})(\d{2})$', 'bks' => 0, 'bke' => 4, 'brs' => 5, 'bre' => 9, 's' => true, 'cs' => 21, 'ce' => 22], + 'NI' => ['n' => 'Nicaragua', 'il' => 28, 'bl' => 24, 'r' => '^NI(\d{2})([A-Z]{4})(\d{20})$', 'bks' => 0, 'bke' => 3, 'brs' => NULL, 'bre' => NULL, 's' => false, 'cs' => NULL, 'ce' => NULL], + 'NL' => ['n' => 'Netherlands (The)', 'il' => 18, 'bl' => 14, 'r' => '^NL(\d{2})([A-Z]{4})(\d{10})$', 'bks' => 0, 'bke' => 3, 'brs' => NULL, 'bre' => NULL, 's' => true, 'cs' => NULL, 'ce' => NULL], + 'NO' => ['n' => 'Norway', 'il' => 15, 'bl' => 11, 'r' => '^NO(\d{2})(\d{4})(\d{6})(\d{1})$', 'bks' => 0, 'bke' => 3, 'brs' => NULL, 'bre' => NULL, 's' => true, 'cs' => 10, 'ce' => 10], + 'OM' => ['n' => 'Oman', 'il' => 23, 'bl' => 19, 'r' => '^OM(\d{2})(\d{3})([A-Za-z0-9]{16})$', 'bks' => 0, 'bke' => 2, 'brs' => NULL, 'bre' => NULL, 's' => false, 'cs' => NULL, 'ce' => NULL], + 'PF' => ['n' => 'French Polynesia', 'il' => 27, 'bl' => 23, 'r' => '^PF(\d{2})(\d{5})(\d{5})([A-Za-z0-9]{11})(\d{2})$', 'bks' => 0, 'bke' => 4, 'brs' => 5, 'bre' => 9, 's' => true, 'cs' => 21, 'ce' => 22], + 'PK' => ['n' => 'Pakistan', 'il' => 24, 'bl' => 20, 'r' => '^PK(\d{2})([A-Z]{4})([A-Za-z0-9]{16})$', 'bks' => 0, 'bke' => 3, 'brs' => NULL, 'bre' => NULL, 's' => false, 'cs' => NULL, 'ce' => NULL], + 'PL' => ['n' => 'Poland', 'il' => 28, 'bl' => 24, 'r' => '^PL(\d{2})(\d{8})(\d{16})$', 'bks' => 0, 'bke' => 7, 'brs' => NULL, 'bre' => NULL, 's' => true, 'cs' => 7, 'ce' => 7], + 'PM' => ['n' => 'Saint Pierre et Miquelon', 'il' => 27, 'bl' => 23, 'r' => '^PM(\d{2})(\d{5})(\d{5})([A-Za-z0-9]{11})(\d{2})$', 'bks' => 0, 'bke' => 4, 'brs' => 5, 'bre' => 9, 's' => true, 'cs' => 21, 'ce' => 22], + 'PS' => ['n' => 'Palestine, State of', 'il' => 29, 'bl' => 25, 'r' => '^PS(\d{2})([A-Z]{4})([A-Za-z0-9]{21})$', 'bks' => 0, 'bke' => 3, 'brs' => NULL, 'bre' => NULL, 's' => false, 'cs' => NULL, 'ce' => NULL], + 'PT' => ['n' => 'Portugal', 'il' => 25, 'bl' => 21, 'r' => '^PT(\d{2})(\d{4})(\d{4})(\d{11})(\d{2})$', 'bks' => 0, 'bke' => 3, 'brs' => 4, 'bre' => 7, 's' => true, 'cs' => 19, 'ce' => 20], + 'QA' => ['n' => 'Qatar', 'il' => 29, 'bl' => 25, 'r' => '^QA(\d{2})([A-Z]{4})(\d{4})([A-Za-z0-9]{17})$', 'bks' => 0, 'bke' => 3, 'brs' => 4, 'bre' => 7, 's' => false, 'cs' => NULL, 'ce' => NULL], + 'RE' => ['n' => 'Reunion', 'il' => 27, 'bl' => 23, 'r' => '^RE(\d{2})(\d{5})(\d{5})([A-Za-z0-9]{11})(\d{2})$', 'bks' => 0, 'bke' => 4, 'brs' => 5, 'bre' => 9, 's' => true, 'cs' => 21, 'ce' => 22], + 'RO' => ['n' => 'Romania', 'il' => 24, 'bl' => 20, 'r' => '^RO(\d{2})([A-Z]{4})([A-Za-z0-9]{16})$', 'bks' => 0, 'bke' => 3, 'brs' => NULL, 'bre' => NULL, 's' => true, 'cs' => NULL, 'ce' => NULL], + 'RS' => ['n' => 'Serbia', 'il' => 22, 'bl' => 18, 'r' => '^RS(\d{2})(\d{3})(\d{13})(\d{2})$', 'bks' => 0, 'bke' => 2, 'brs' => NULL, 'bre' => NULL, 's' => false, 'cs' => 16, 'ce' => 17], + 'RU' => ['n' => 'Russian Federation', 'il' => 33, 'bl' => 29, 'r' => '^RU(\d{2})(\d{9})(\d{5})([A-Za-z0-9]{15})$', 'bks' => 0, 'bke' => 8, 'brs' => 9, 'bre' => 13, 's' => false, 'cs' => NULL, 'ce' => NULL], + 'SA' => ['n' => 'Saudi Arabia', 'il' => 24, 'bl' => 20, 'r' => '^SA(\d{2})(\d{2})([A-Za-z0-9]{18})$', 'bks' => 0, 'bke' => 1, 'brs' => NULL, 'bre' => NULL, 's' => false, 'cs' => NULL, 'ce' => NULL], + 'SC' => ['n' => 'Seychelles', 'il' => 31, 'bl' => 27, 'r' => '^SC(\d{2})([A-Z]{4})(\d{2})(\d{2})(\d{16})([A-Z]{3})$', 'bks' => 0, 'bke' => 5, 'brs' => 10, 'bre' => 25, 's' => false, 'cs' => NULL, 'ce' => NULL], + 'SD' => ['n' => 'Sudan', 'il' => 18, 'bl' => 14, 'r' => '^SD(\d{2})(\d{2})(\d{12})$', 'bks' => 0, 'bke' => 1, 'brs' => NULL, 'bre' => NULL, 's' => false, 'cs' => NULL, 'ce' => NULL], + 'SE' => ['n' => 'Sweden', 'il' => 24, 'bl' => 20, 'r' => '^SE(\d{2})(\d{3})(\d{16})(\d{1})$', 'bks' => 0, 'bke' => 2, 'brs' => NULL, 'bre' => NULL, 's' => true, 'cs' => 19, 'ce' => 19], + 'SI' => ['n' => 'Slovenia', 'il' => 19, 'bl' => 15, 'r' => '^SI(\d{2})(\d{5})(\d{8})(\d{2})$', 'bks' => 0, 'bke' => 4, 'brs' => NULL, 'bre' => NULL, 's' => true, 'cs' => 13, 'ce' => 14], + 'SK' => ['n' => 'Slovakia', 'il' => 24, 'bl' => 20, 'r' => '^SK(\d{2})(\d{4})(\d{6})(\d{10})$', 'bks' => 0, 'bke' => 3, 'brs' => 4, 'bre' => 9, 's' => true, 'cs' => 19, 'ce' => 19], + 'SM' => ['n' => 'San Marino', 'il' => 27, 'bl' => 23, 'r' => '^SM(\d{2})([A-Z]{1})(\d{5})(\d{5})([A-Za-z0-9]{12})$', 'bks' => 0, 'bke' => 5, 'brs' => 6, 'bre' => 10, 's' => true, 'cs' => 0, 'ce' => 0], + 'SO' => ['n' => 'Somalia', 'il' => 23, 'bl' => 19, 'r' => '^SO(\d{2})(\d{4})(\d{3})(\d{12})$', 'bks' => 0, 'bke' => 3, 'brs' => 4, 'bre' => 6, 's' => false, 'cs' => NULL, 'ce' => NULL], + 'ST' => ['n' => 'Sao Tome and Principe', 'il' => 25, 'bl' => 21, 'r' => '^ST(\d{2})(\d{4})(\d{4})(\d{11})(\d{2})$', 'bks' => 0, 'bke' => 3, 'brs' => 4, 'bre' => 7, 's' => false, 'cs' => NULL, 'ce' => NULL], + 'SV' => ['n' => 'El Salvador', 'il' => 28, 'bl' => 24, 'r' => '^SV(\d{2})([A-Z]{4})(\d{20})$', 'bks' => 0, 'bke' => 3, 'brs' => NULL, 'bre' => NULL, 's' => false, 'cs' => NULL, 'ce' => NULL], + 'TF' => ['n' => 'French Southern Territories', 'il' => 27, 'bl' => 23, 'r' => '^TF(\d{2})(\d{5})(\d{5})([A-Za-z0-9]{11})(\d{2})$', 'bks' => 0, 'bke' => 4, 'brs' => 5, 'bre' => 9, 's' => true, 'cs' => 21, 'ce' => 22], + 'TL' => ['n' => 'Timor-Leste', 'il' => 23, 'bl' => 19, 'r' => '^TL(\d{2})(\d{3})(\d{14})(\d{2})$', 'bks' => 0, 'bke' => 2, 'brs' => NULL, 'bre' => NULL, 's' => false, 'cs' => 17, 'ce' => 18], + 'TN' => ['n' => 'Tunisia', 'il' => 24, 'bl' => 20, 'r' => '^TN(\d{2})(\d{2})(\d{3})(\d{13})(\d{2})$', 'bks' => 0, 'bke' => 1, 'brs' => 2, 'bre' => 4, 's' => false, 'cs' => 18, 'ce' => 19], + 'TR' => ['n' => 'Turkiye', 'il' => 26, 'bl' => 22, 'r' => '^TR(\d{2})(\d{5})(\d{1})([A-Za-z0-9]{16})$', 'bks' => 0, 'bke' => 4, 'brs' => NULL, 'bre' => NULL, 's' => false, 'cs' => NULL, 'ce' => NULL], + 'UA' => ['n' => 'Ukraine', 'il' => 29, 'bl' => 25, 'r' => '^UA(\d{2})(\d{6})([A-Za-z0-9]{19})$', 'bks' => 0, 'bke' => 5, 'brs' => NULL, 'bre' => NULL, 's' => false, 'cs' => NULL, 'ce' => NULL], + 'VA' => ['n' => 'Holy See', 'il' => 22, 'bl' => 18, 'r' => '^VA(\d{2})(\d{3})(\d{15})$', 'bks' => 0, 'bke' => 2, 'brs' => NULL, 'bre' => NULL, 's' => true, 'cs' => NULL, 'ce' => NULL], + 'VG' => ['n' => 'Virgin Islands (British)', 'il' => 24, 'bl' => 20, 'r' => '^VG(\d{2})([A-Z]{4})(\d{16})$', 'bks' => 0, 'bke' => 3, 'brs' => NULL, 'bre' => NULL, 's' => false, 'cs' => NULL, 'ce' => NULL], + 'WF' => ['n' => 'Wallis and Futuna Islands', 'il' => 27, 'bl' => 23, 'r' => '^WF(\d{2})(\d{5})(\d{5})([A-Za-z0-9]{11})(\d{2})$', 'bks' => 0, 'bke' => 4, 'brs' => 5, 'bre' => 9, 's' => true, 'cs' => 21, 'ce' => 22], + 'XK' => ['n' => 'Kosovo', 'il' => 20, 'bl' => 16, 'r' => '^XK(\d{2})(\d{4})(\d{10})(\d{2})$', 'bks' => 0, 'bke' => 1, 'brs' => NULL, 'bre' => NULL, 's' => false, 'cs' => NULL, 'ce' => NULL], + 'YE' => ['n' => 'Yemen', 'il' => 30, 'bl' => 26, 'r' => '^YE(\d{2})([A-Z]{4})(\d{4})([A-Za-z0-9]{18})$', 'bks' => 0, 'bke' => 3, 'brs' => 4, 'bre' => 7, 's' => false, 'cs' => NULL, 'ce' => NULL], + 'YT' => ['n' => 'Mayotte', 'il' => 27, 'bl' => 23, 'r' => '^YT(\d{2})(\d{5})(\d{5})([A-Za-z0-9]{11})(\d{2})$', 'bks' => 0, 'bke' => 4, 'brs' => 5, 'bre' => 9, 's' => true, 'cs' => 21, 'ce' => 22], + ]; + + private const MISTRANSCRIPTIONS = [ + '0' => ['O', '6', 'D', 'G'], '1' => ['I', 'L', '7', '2', 'Z'], '2' => ['Z', '7', 'P', 'E', '1'], + '3' => ['8', 'B'], '4' => ['G', 'U'], '5' => ['S', '7'], '6' => ['O', '8', 'G', 'C', 'B', 'D'], + '7' => ['J', 'I', '1', 'L'], '8' => ['B', '3', '6'], '9' => ['G', 'Y', 'O', '0', 'D'], + 'A' => ['G', 'Q', 'O'], 'B' => ['6', '3', '8', 'P', 'O'], 'C' => ['R', '6', 'I', 'L', 'O'], + 'D' => ['O', '9', 'Q', 'G', '6', 'A'], 'E' => ['F', 'G', '2', 'K', 'Z', 'S', 'O'], + 'F' => ['E', 'K', 'T', 'P', 'Y', '4', 'B', '7', '1'], 'G' => ['9', 'Q', '8', '6', 'C', '4', 'O'], + 'H' => ['B', 'N', 'A', '4', '6', 'M', 'W', 'F', 'R', 'T', 'X'], 'I' => ['1', 'L', '7', 'J', '2', 'T', 'Z'], + 'J' => ['I', '7', '2', '9', '1', 'U', 'T', 'Q', 'P', 'Y', 'Z', 'L', 'S'], 'K' => ['F', 'X', 'H', 'R'], + 'L' => ['1', '2', '7', 'C', 'I', 'J', 'R', 'T', 'Y', 'Z'], 'M' => ['H', '8', 'E', '3', 'N', 'V', 'W'], + 'N' => ['H', 'R', 'C', '2', '4', 'M', 'O', 'P', 'K', 'T', 'Z'], 'O' => ['6', '9', 'A', 'D', 'G', 'C', 'E', 'B', 'N', 'P'], + 'P' => ['F', '4', '8', '2', 'B', 'J', 'R', 'N', 'O', 'T', 'Y'], 'Q' => ['O', 'G', '9', 'Y', '1', '7', 'L'], + 'R' => ['K', 'B', 'V', 'C', '1', 'L', '2'], 'S' => ['5', '6', '9', 'B', 'G', 'Q', 'A', 'Y'], + 'T' => ['1', '4', '7', 'F', 'I', 'J', 'L', 'P', 'X', 'Y'], 'U' => ['V', 'N', 'A', '4', '9', 'W', 'Y'], + 'V' => ['U', 'R', 'N'], 'W' => ['M', 'N', 'U', 'V'], 'X' => ['K', 'F', '4', 'T', 'V', 'Y'], + 'Y' => ['G', 'V', 'J', 'I', '4', '9', 'T', 'F', 'Q', '1'], 'Z' => ['2', '1', 'L', 'R', 'I', '7', 'V', '3', '4'], + ]; + + public function __construct(string $iban) + { + $this->iban = $iban; + $this->normalized = self::normalize($iban); + $countryCode = $this->getCountryCode(); + $this->countryInfo = self::REGISTRY[$countryCode] ?? null; + } + + public static function validate(string $iban): bool + { + return (new self($iban))->isValid(); + } + + public static function normalize(string $iban): string + { + $iban = ltrim(strtoupper($iban)); + $iban = preg_replace('/^I?IBAN/', '', $iban) ?? ''; + return preg_replace('/[^A-Z0-9]/', '', $iban) ?? ''; + } + + public function isValid(): bool + { + if (!$this->countryInfo) { + return false; + } + + if (strlen($this->normalized) !== $this->countryInfo['il']) { + return false; + } + + if (!preg_match('/' . $this->countryInfo['r'] . '/', $this->normalized)) { + return false; + } + + if (!$this->verifyChecksum()) { + return false; + } + + $nationalCheck = $this->verifyNationalChecksum(); + if ($nationalCheck === false) { + return false; + } + + return true; + } + + public function verifyChecksum(): bool + { + $temp = substr($this->normalized, 4) . substr($this->normalized, 0, 4); + $numeric = ''; + foreach (str_split($temp) as $char) { + $numeric .= ctype_alpha($char) ? (ord($char) - 55) : $char; + } + return self::bigModulo97($numeric) === 1; + } + + public function calculateChecksum(): string + { + $tmp = substr($this->normalized, 4) . substr($this->normalized, 0, 2) . '00'; + $numeric = ''; + foreach (str_split($tmp) as $char) { + $numeric .= ctype_alpha($char) ? (ord($char) - 55) : $char; + } + $checksum = self::bigModulo97($numeric, true); + return str_pad((string)(98 - $checksum), 2, '0', STR_PAD_LEFT); + } + + public function setChecksum(): string + { + return substr($this->normalized, 0, 2) . $this->calculateChecksum() . substr($this->normalized, 4); + } + + public function getCountryCode(): string + { + return substr($this->normalized, 0, 2); + } + + public function getCountryName(): ?string + { + return $this->countryInfo['n'] ?? null; + } + + public function isSepa(): bool + { + return $this->countryInfo['s'] ?? false; + } + + public function getBban(): string + { + return substr($this->normalized, 4); + } + + public function getBankCode(): ?string + { + if (!$this->countryInfo || $this->countryInfo['bks'] === null) return null; + return substr($this->getBban(), $this->countryInfo['bks'], $this->countryInfo['bke'] - $this->countryInfo['bks'] + 1); + } + + public function getBranchCode(): ?string + { + if (!$this->countryInfo || $this->countryInfo['brs'] === null) return null; + return substr($this->getBban(), $this->countryInfo['brs'], $this->countryInfo['bre'] - $this->countryInfo['brs'] + 1); + } + + public function getAccountNumber(): ?string + { + if (!$this->countryInfo) return null; + $start = ($this->countryInfo['bre'] ?? $this->countryInfo['bke'] ?? -1) + 1; + return substr($this->getBban(), $start); + } + + public function getNationalChecksum(): ?string + { + if (!$this->countryInfo || $this->countryInfo['cs'] === null) return null; + return substr($this->getBban(), $this->countryInfo['cs'], $this->countryInfo['ce'] - $this->countryInfo['cs'] + 1); + } + + public function format(bool $machine = false): string + { + if ($machine) return $this->normalized; + return wordwrap($this->normalized, 4, ' ', true); + } + + public function obfuscate(): string + { + $len = strlen($this->normalized); + if ($len < 8) return $this->normalized; + $prefix = substr($this->normalized, 0, 2); + $suffix = substr($this->normalized, -4); + return wordwrap($prefix . str_repeat('*', $len - 6) . $suffix, 4, ' ', true); + } + + public function getParts(): array + { + return [ + 'country' => $this->getCountryCode(), + 'checksum' => substr($this->normalized, 2, 2), + 'bban' => $this->getBban(), + 'bank' => $this->getBankCode(), + 'branch' => $this->getBranchCode(), + 'account' => $this->getAccountNumber(), + 'national_checksum' => $this->getNationalChecksum(), + ]; + } + + public static function getSuggestions(string $incorrectIban): array + { + $normalized = self::normalize($incorrectIban); + $length = strlen($normalized); + if ($length < 5 || $length > 34) return []; + + $suggestions = []; + for ($i = 0; $i < $length; $i++) { + $char = $normalized[$i]; + if (!isset(self::MISTRANSCRIPTIONS[$char])) continue; + + foreach (self::MISTRANSCRIPTIONS[$char] as $possible) { + if (($i < 2 && !ctype_alpha($possible)) || ($i >= 2 && $i <= 3 && !ctype_digit($possible))) continue; + $candidate = substr($normalized, 0, $i) . $possible . substr($normalized, $i + 1); + if (self::validate($candidate)) $suggestions[] = $candidate; + } + } + return array_unique($suggestions); + } + + // --- Validation Rules Helpers --- // + + private function verifyNationalChecksum(): ?bool + { + $countryCode = $this->getCountryCode(); + $method = 'verify' . $countryCode; + if (method_exists($this, $method)) return $this->$method(); + + $mod9710Countries = ['ME', 'MK', 'PT', 'RS', 'SI', 'TL']; + if (in_array($countryCode, $mod9710Countries)) return $this->verifyMod9710(); + + return null; + } + + private function verifyMod9710(): bool + { + $bban = $this->getBban(); + $checksum = substr($bban, -2); + $base = substr($bban, 0, -2); + $numeric = ''; + foreach (str_split($base) as $char) { + $numeric .= ctype_digit($char) ? $char : (ord(strtoupper($char)) - 55); + } + $p = 0; + foreach (str_split($numeric) as $digit) { + $p = (($p + (int)$digit) * 10) % 97; + } + $p = ($p * 10) % 97; + $expected = 98 - ($p % 97); + if ($expected >= 97) $expected -= 97; + return (int)$checksum === $expected; + } + + private function verifyIT(): bool + { + $bban = $this->getBban(); + $evenList = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28]; + $oddList = [1, 0, 5, 7, 9, 13, 15, 17, 19, 21, 2, 4, 18, 20, 11, 3, 6, 8, 12, 14, 16, 10, 22, 25, 24, 23, 27, 28, 26]; + $chars = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ-. '; + $sum = 0; + for ($i = 0; $i < 22; $i++) { + $pos = strpos($chars, $bban[$i + 1]); + if ($pos === false) return false; + $sum += ($i % 2 === 0 ? $oddList[$pos] : $evenList[$pos]); + } + return $bban[0] === chr(65 + ($sum % 26)); + } + + private function verifySM(): bool { return $this->verifyIT(); } + + private function verifyNL(): bool + { + if ($this->getBankCode() === 'INGB') return true; + $account = $this->getAccountNumber(); + if (!$account || strlen($account) < 10) return false; + $sum = 0; + for ($i = 0; $i < 10; $i++) { $sum += (int)$account[$i] * (10 - $i); } + return ($sum % 11) === 0; + } + + private function verifyBE(): bool + { + $bban = $this->getBban(); + return ((int)substr($bban, 0, -2) % 97) === (int)substr($bban, -2); + } + + private function verifyES(): bool + { + $bban = $this->getBban(); + if (strlen($bban) !== 20) return false; + $bank = substr($bban, 0, 4); + $branch = substr($bban, 4, 4); + $checksum = substr($bban, 8, 2); + $account = substr($bban, 10, 10); + + $calculateMod11 = function(string $num): string { + $weights = [1, 2, 4, 8, 5, 10, 9, 7, 3, 6]; + $sum = 0; + for ($i = 0; $i < 10; $i++) { $sum += (int)$num[$i] * $weights[$i]; } + $check = 11 - ($sum % 11); + if ($check === 11) $check = 0; + if ($check === 10) $check = 1; + return (string)$check; + }; + + return $checksum === ($calculateMod11("00" . $bank . $branch) . $calculateMod11($account)); + } + + private function verifyFR(): bool + { + $numeric = ''; + $conversion = ['A'=>1,'B'=>2,'C'=>3,'D'=>4,'E'=>5,'F'=>6,'G'=>7,'H'=>8,'I'=>9,'J'=>1,'K'=>2,'L'=>3,'M'=>4,'N'=>5,'O'=>6,'P'=>7,'Q'=>8,'R'=>9,'S'=>2,'T'=>3,'U'=>4,'V'=>5,'W'=>6,'X'=>7,'Y'=>8,'Z'=>9]; + foreach (str_split($this->getBban()) as $char) { + $numeric .= ctype_digit($char) ? $char : ($conversion[$char] ?? ''); + } + if (strlen($numeric) !== 23) return false; + $sum = (89 * (int)substr($numeric, 0, 5)) + (15 * (int)substr($numeric, 5, 5)) + (3 * (int)substr($numeric, 10, 11)); + return (int)substr($numeric, 21, 2) === (97 - ($sum % 97)); + } + + private function verifyRS(): bool + { + if ($this->getBankCode() === '908') return true; + return $this->verifyMod9710(); + } + + private function verifySI(): bool + { + if (substr($this->getBban(), 0, 2) === '01') return true; + return $this->verifyMod9710(); + } + + private static function bigModulo97(string $bigInt): int + { + if (function_exists('gmp_init')) { + return (int)gmp_strval(gmp_mod(gmp_init($bigInt, 10), gmp_init('97', 10))); + } + $rest = 0; + foreach (str_split($bigInt, 7) as $part) { + $rest = (int)($rest . $part) % 97; + } + return $rest; + } + + public function __toString(): string + { + return $this->normalized; + } + + // --- Auto Updater Subsystem --- // + + /** + * Parses official SWIFT IBAN_Registry.txt (TSV format) and updates this class file in place. + */ + public static function updateRegistry(string $txtFilePath): void + { + if (!file_exists($txtFilePath)) { + throw new RuntimeException("Registry file not found: $txtFilePath"); + } + + $raw_data = file_get_contents($txtFilePath); + if (function_exists('mb_convert_encoding')) { + $raw_data = mb_convert_encoding($raw_data, 'UTF-8', 'Windows-1252'); + } + + $fp = fopen('php://temp', 'r+'); + fwrite($fp, $raw_data); + rewind($fp); + + $registry = []; + while (($row = fgetcsv($fp, 0, "\t")) !== false) { + if (empty($row[0])) continue; + $fieldName = trim($row[0]); + array_shift($row); + $registry[$fieldName] = $row; + } + fclose($fp); + + $countries = $registry['IBAN prefix country code (ISO 3166)'] ?? []; + if (empty($countries)) { + throw new RuntimeException("Could not find country codes in TSV. Ensure you have the raw SWIFT IBAN_Registry.txt."); + } + + $newRegistryBlocks = []; + + foreach ($countries as $i => $code) { + $country_code = strtoupper(substr(trim((string)$code), 0, 2)); + if (empty($country_code) || $country_code === 'IB') continue; + + $country_name = $registry['Name of country'][$i] ?? ''; + $bban_structure = preg_replace('/[:;]/', '', $registry['BBAN structure'][$i] ?? ''); + $bban_length = (int)preg_replace('/[^\d]/', '', $registry['BBAN length'][$i] ?? ''); + $iban_structure = preg_replace('/, .*$/', '', $registry['IBAN structure'][$i] ?? ''); + $iban_length = (int)preg_replace('/[^\d]/', '', $registry['IBAN length'][$i] ?? ''); + $sepa_raw = $registry['SEPA country'][$i] ?? ''; + $bban_bi_position = $registry['Bank identifier position within the BBAN'][$i] ?? ''; + + if ($country_code === 'KZ') $iban_structure = '2!a2!n3!n13!c'; + if ($country_code === 'QA') { + $bban_structure = '4!a4!n17!c'; + $iban_structure = 'QA2!n4!a4!n17!c'; + } + if ($country_code === 'JO') { + $bban_bi_position = '1-4'; // Hardcode fix + } + + if (str_starts_with($iban_structure, '2!a')) { + $iban_structure = $country_code . substr($iban_structure, 3); + } + + $iban_regex = self::swiftToRegex($iban_structure, $country_code); + [$bks, $bke, $brs, $bre] = self::parseOffsets($bban_bi_position, $bban_structure); + $isSepa = strtolower(trim($sepa_raw)) === 'yes' ? 'true' : 'false'; + + $to_generate = [$country_code => $country_name]; + if ($country_code === 'DK') { + $to_generate = ['DK' => $country_name, 'FO' => 'Faroe Islands', 'GL' => 'Greenland']; + } elseif ($country_code === 'FR') { + $to_generate = ['FR' => $country_name, 'BL' => 'Saint Barthelemy', 'GF' => 'French Guyana', 'GP' => 'Guadelope', 'MF' => 'Saint Martin (French Part)', 'MQ' => 'Martinique', 'RE' => 'Reunion', 'PF' => 'French Polynesia', 'TF' => 'French Southern Territories', 'YT' => 'Mayotte', 'NC' => 'New Caledonia', 'PM' => 'Saint Pierre et Miquelon', 'WF' => 'Wallis and Futuna Islands']; + } + + foreach ($to_generate as $cCode => $cName) { + // Carry over custom CS/CE values + $cs = self::REGISTRY[$cCode]['cs'] ?? 'NULL'; + $ce = self::REGISTRY[$cCode]['ce'] ?? 'NULL'; + + $cNameClean = str_replace("'", "\\'", $cName); + $bksStr = $bks ?? 'NULL'; + $bkeStr = $bke ?? 'NULL'; + $brsStr = $brs ?? 'NULL'; + $breStr = $bre ?? 'NULL'; + $csStr = $cs === 'NULL' || $cs === null ? 'NULL' : $cs; + $ceStr = $ce === 'NULL' || $ce === null ? 'NULL' : $ce; + + // Adjust Regex for cloned territories (e.g. FR -> GF) + $clonedRegex = $iban_regex; + if ($cCode !== $country_code) { + $clonedRegex = '^' . $cCode . substr($iban_regex, 3); + } + + $newRegistryBlocks[$cCode] = " '$cCode' => ['n' => '$cNameClean', 'il' => $iban_length, 'bl' => $bban_length, 'r' => '$clonedRegex', 'bks' => $bksStr, 'bke' => $bkeStr, 'brs' => $brsStr, 'bre' => $breStr, 's' => $isSepa, 'cs' => $csStr, 'ce' => $ceStr],"; + } + } + + ksort($newRegistryBlocks); + $newRegistryString = "[\n" . implode("\n", $newRegistryBlocks) . "\n ]"; + + $classFile = (new ReflectionClass(self::class))->getFileName(); + $content = file_get_contents($classFile); + $startToken = 'private const REGISTRY = ['; + $endToken = ' ];'; + $startPos = strpos($content, $startToken); + $endPos = strpos($content, $endToken, $startPos); + + if ($startPos !== false && $endPos !== false) { + $updatedContent = substr_replace($content, "private const REGISTRY = " . $newRegistryString . ";", $startPos, $endPos - $startPos + 6); + file_put_contents($classFile, $updatedContent); + echo "✅ Successfully synced " . count($newRegistryBlocks) . " countries and updated " . basename($classFile) . "!\n"; + } else { + throw new RuntimeException("Failed to locate REGISTRY array block in source file."); + } + } + + private static function swiftToRegex(string $structure, string $countryCode): string + { + $regex = '^'; + if (preg_match('/^[A-Z]{2}/', $structure)) { + $regex .= substr($structure, 0, 2); + $structure = substr($structure, 2); + } else { + $regex .= $countryCode; + } + + preg_match_all('/(\d+)!([anc])/', $structure, $matches, PREG_SET_ORDER); + foreach ($matches as $m) { + $len = $m[1]; + $type = $m[2]; + if ($type === 'n') $char = '\d'; + elseif ($type === 'c') $char = '[A-Za-z0-9]'; + elseif ($type === 'a') $char = '[A-Z]'; + else $char = '.'; + + $regex .= "($char{{$len}})"; + } + return $regex . '$'; + } + + private static function parseOffsets(string $positionText, string $bbanStructure): array + { + $bks = null; $bke = null; $brs = null; $bre = null; + preg_match_all('/(\d)-(\d\d?)/', $positionText, $matches, PREG_SET_ORDER); + + $tokens = []; + foreach ($matches as $m) { + $from = (int)$m[1]; + $to = (int)$m[2]; + if (!isset($tokens[$from]) || $to < $tokens[$from]) { + $tokens[$from] = $to; + } + } + + if (isset($tokens[1])) { $bks = 0; $bke = $tokens[1] - 1; unset($tokens[1]); } + elseif (isset($tokens[2])) { $bks = 0; $bke = $tokens[2] - 1; unset($tokens[2]); } + + $keys = array_keys($tokens); + if (!empty($keys)) { + $start = $keys[0]; + $brs = $start - 1; + $bre = $tokens[$start] - 1; + } else { + $reduced = preg_replace('/^\d+![nac]/', '', $bbanStructure); + preg_match_all('/(\d+)!([anc])/', $reduced ?? '', $structMatches, PREG_SET_ORDER); + $validTokens = []; + $currentOffset = $bke !== null ? $bke + 1 : 0; + + foreach ($structMatches as $m) { + $len = (int)$m[1]; + if ($len >= 3) { + $validTokens[] = ['s' => $currentOffset, 'e' => $currentOffset + $len - 1]; + } + $currentOffset += $len; + } + if (count($validTokens) >= 2) { + $brs = $validTokens[0]['s']; + $bre = $validTokens[0]['e']; + } + } + return [$bks, $bke, $brs, $bre]; + } +} \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..65c5ca8 --- /dev/null +++ b/LICENSE @@ -0,0 +1,165 @@ + GNU LESSER GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + + This version of the GNU Lesser General Public License incorporates +the terms and conditions of version 3 of the GNU General Public +License, supplemented by the additional permissions listed below. + + 0. Additional Definitions. + + As used herein, "this License" refers to version 3 of the GNU Lesser +General Public License, and the "GNU GPL" refers to version 3 of the GNU +General Public License. + + "The Library" refers to a covered work governed by this License, +other than an Application or a Combined Work as defined below. + + An "Application" is any work that makes use of an interface provided +by the Library, but which is not otherwise based on the Library. +Defining a subclass of a class defined by the Library is deemed a mode +of using an interface provided by the Library. + + A "Combined Work" is a work produced by combining or linking an +Application with the Library. The particular version of the Library +with which the Combined Work was made is also called the "Linked +Version". + + The "Minimal Corresponding Source" for a Combined Work means the +Corresponding Source for the Combined Work, excluding any source code +for portions of the Combined Work that, considered in isolation, are +based on the Application, and not on the Linked Version. + + The "Corresponding Application Code" for a Combined Work means the +object code and/or source code for the Application, including any data +and utility programs needed for reproducing the Combined Work from the +Application, but excluding the System Libraries of the Combined Work. + + 1. Exception to Section 3 of the GNU GPL. + + You may convey a covered work under sections 3 and 4 of this License +without being bound by section 3 of the GNU GPL. + + 2. Conveying Modified Versions. + + If you modify a copy of the Library, and, in your modifications, a +facility refers to a function or data to be supplied by an Application +that uses the facility (other than as an argument passed when the +facility is invoked), then you may convey a copy of the modified +version: + + a) under this License, provided that you make a good faith effort to + ensure that, in the event an Application does not supply the + function or data, the facility still operates, and performs + whatever part of its purpose remains meaningful, or + + b) under the GNU GPL, with none of the additional permissions of + this License applicable to that copy. + + 3. Object Code Incorporating Material from Library Header Files. + + The object code form of an Application may incorporate material from +a header file that is part of the Library. You may convey such object +code under terms of your choice, provided that, if the incorporated +material is not limited to numerical parameters, data structure +layouts and accessors, or small macros, inline functions and templates +(ten or fewer lines in length), you do both of the following: + + a) Give prominent notice with each copy of the object code that the + Library is used in it and that the Library and its use are + covered by this License. + + b) Accompany the object code with a copy of the GNU GPL and this license + document. + + 4. Combined Works. + + You may convey a Combined Work under terms of your choice that, +taken together, effectively do not restrict modification of the +portions of the Library contained in the Combined Work and reverse +engineering for debugging such modifications, if you also do each of +the following: + + a) Give prominent notice with each copy of the Combined Work that + the Library is used in it and that the Library and its use are + covered by this License. + + b) Accompany the Combined Work with a copy of the GNU GPL and this license + document. + + c) For a Combined Work that displays copyright notices during + execution, include the copyright notice for the Library among + these notices, as well as a reference directing the user to the + copies of the GNU GPL and this license document. + + d) Do one of the following: + + 0) Convey the Minimal Corresponding Source under the terms of this + License, and the Corresponding Application Code in a form + suitable for, and under terms that permit, the user to + recombine or relink the Application with a modified version of + the Linked Version to produce a modified Combined Work, in the + manner specified by section 6 of the GNU GPL for conveying + Corresponding Source. + + 1) Use a suitable shared library mechanism for linking with the + Library. A suitable mechanism is one that (a) uses at run time + a copy of the Library already present on the user's computer + system, and (b) will operate properly with a modified version + of the Library that is interface-compatible with the Linked + Version. + + e) Provide Installation Information, but only if you would otherwise + be required to provide such information under section 6 of the + GNU GPL, and only to the extent that such information is + necessary to install and execute a modified version of the + Combined Work produced by recombining or relinking the + Application with a modified version of the Linked Version. (If + you use option 4d0, the Installation Information must accompany + the Minimal Corresponding Source and Corresponding Application + Code. If you use option 4d1, you must provide the Installation + Information in the manner specified by section 6 of the GNU GPL + for conveying Corresponding Source.) + + 5. Combined Libraries. + + You may place library facilities that are a work based on the +Library side by side in a single library together with other library +facilities that are not Applications and are not covered by this +License, and convey such a combined library under terms of your +choice, if you do both of the following: + + a) Accompany the combined library with a copy of the same work based + on the Library, uncombined with any other library facilities, + conveyed under the terms of this License. + + b) Give prominent notice with the combined library that part of it + is a work based on the Library, and explaining where to find the + accompanying uncombined form of the same work. + + 6. Revised Versions of the GNU Lesser General Public License. + + The Free Software Foundation may publish revised and/or new versions +of the GNU Lesser General Public License from time to time. Such new +versions will be similar in spirit to the present version, but may +differ in detail to address new problems or concerns. + + Each version is given a distinguishing version number. If the +Library as you received it specifies that a certain numbered version +of the GNU Lesser General Public License "or any later version" +applies to it, you have the option of following the terms and +conditions either of that published version or of any later version +published by the Free Software Foundation. If the Library as you +received it does not specify a version number of the GNU Lesser +General Public License, you may choose any version of the GNU Lesser +General Public License ever published by the Free Software Foundation. + + If the Library as you received it specifies that a proxy can decide +whether future versions of the GNU Lesser General Public License shall +apply, that proxy's public statement of acceptance of any version is +permanent authorization for you to choose that version for the +Library. diff --git a/test.php b/test.php new file mode 100644 index 0000000..d4ead48 --- /dev/null +++ b/test.php @@ -0,0 +1,90 @@ +