From 4726a9403ffb3d373ede3f5cf18ef1a3f5726b79 Mon Sep 17 00:00:00 2001 From: cod3ddy Date: Tue, 2 Jun 2026 22:57:12 +0200 Subject: [PATCH 1/6] chore: remove hardcoded slice of countries' details --- countries.go | 266 +----- countries.json | 2234 ++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 2246 insertions(+), 254 deletions(-) create mode 100644 countries.json diff --git a/countries.go b/countries.go index ef7e746..c2c9347 100644 --- a/countries.go +++ b/countries.go @@ -1,259 +1,17 @@ package sanja -var countries = []Country{ - {"Afghanistan", "AF", "AFG", 4, "93"}, - {"Albania", "AL", "ALB", 8, "355"}, - {"Algeria", "DZ", "DZA", 12, "213"}, - {"American Samoa", "AS", "ASM", 16, "1-684"}, - {"Andorra", "AD", "AND", 20, "376"}, - {"Angola", "AO", "AGO", 24, "244"}, - {"Anguilla", "AI", "AIA", 660, "1-264"}, - {"Antarctica", "AQ", "ATA", 10, "672"}, - {"Antigua and Barbuda", "AG", "ATG", 28, "1-268"}, - {"Argentina", "AR", "ARG", 32, "54"}, - {"Armenia", "AM", "ARM", 51, "374"}, - {"Aruba", "AW", "ABW", 533, "297"}, - {"Australia", "AU", "AUS", 36, "61"}, - {"Austria", "AT", "AUT", 40, "43"}, - {"Azerbaijan", "AZ", "AZE", 31, "994"}, - {"Bahamas", "BS", "BHS", 44, "1-242"}, - {"Bahrain", "BH", "BHR", 48, "973"}, - {"Bangladesh", "BD", "BGD", 50, "880"}, - {"Barbados", "BB", "BRB", 52, "1-246"}, - {"Belarus", "BY", "BLR", 112, "375"}, - {"Belgium", "BE", "BEL", 56, "32"}, - {"Belize", "BZ", "BLZ", 84, "501"}, - {"Benin", "BJ", "BEN", 204, "229"}, - {"Bermuda", "BM", "BMU", 60, "1-441"}, - {"Bhutan", "BT", "BTN", 64, "975"}, - {"Bolivia", "BO", "BOL", 68, "591"}, - {"Bonaire", "BQ", "BES", 535, "599"}, - {"Bosnia and Herzegovina", "BA", "BIH", 70, "387"}, - {"Botswana", "BW", "BWA", 72, "267"}, - {"Bouvet Island", "BV", "BVT", 74, "47"}, - {"Brazil", "BR", "BRA", 76, "55"}, - {"British Indian Ocean Territory", "IO", "IOT", 86, "246"}, - {"Brunei Darussalam", "BN", "BRN", 96, "673"}, - {"Bulgaria", "BG", "BGR", 100, "359"}, - {"Burkina Faso", "BF", "BFA", 854, "226"}, - {"Burundi", "BI", "BDI", 108, "257"}, - {"Cambodia", "KH", "KHM", 116, "855"}, - {"Cameroon", "CM", "CMR", 120, "237"}, - {"Canada", "CA", "CAN", 124, "1"}, - {"Cape Verde", "CV", "CPV", 132, "238"}, - {"Cayman Islands", "KY", "CYM", 136, "1-345"}, - {"Central African Republic", "CF", "CAF", 140, "236"}, - {"Chad", "TD", "TCD", 148, "235"}, - {"Chile", "CL", "CHL", 152, "56"}, - {"China", "CN", "CHN", 156, "86"}, - {"Christmas Island", "CX", "CXR", 162, "61"}, - {"Cocos (Keeling) Islands", "CC", "CCK", 166, "61"}, - {"Colombia", "CO", "COL", 170, "57"}, - {"Comoros", "KM", "COM", 174, "269"}, - {"Congo", "CG", "COG", 178, "242"}, - {"Democratic Republic of the Congo", "CD", "COD", 180, "243"}, - {"Cook Islands", "CK", "COK", 184, "682"}, - {"Costa Rica", "CR", "CRI", 188, "506"}, - {"Croatia", "HR", "HRV", 191, "385"}, - {"Cuba", "CU", "CUB", 192, "53"}, - {"Curacao", "CW", "CUW", 531, "599"}, - {"Cyprus", "CY", "CYP", 196, "357"}, - {"Czech Republic", "CZ", "CZE", 203, "420"}, - {"Cote d'Ivoire", "CI", "CIV", 384, "225"}, - {"Denmark", "DK", "DNK", 208, "45"}, - {"Djibouti", "DJ", "DJI", 262, "253"}, - {"Dominica", "DM", "DMA", 212, "1-767"}, - {"Dominican Republic", "DO", "DOM", 214, "1-809,1-829,1-849"}, - {"Ecuador", "EC", "ECU", 218, "593"}, - {"Egypt", "EG", "EGY", 818, "20"}, - {"El Salvador", "SV", "SLV", 222, "503"}, - {"Equatorial Guinea", "GQ", "GNQ", 226, "240"}, - {"Eritrea", "ER", "ERI", 232, "291"}, - {"Estonia", "EE", "EST", 233, "372"}, - {"Ethiopia", "ET", "ETH", 231, "251"}, - {"Falkland Islands (Malvinas)", "FK", "FLK", 238, "500"}, - {"Faroe Islands", "FO", "FRO", 234, "298"}, - {"Fiji", "FJ", "FJI", 242, "679"}, - {"Finland", "FI", "FIN", 246, "358"}, - {"France", "FR", "FRA", 250, "33"}, - {"French Guiana", "GF", "GUF", 254, "594"}, - {"French Polynesia", "PF", "PYF", 258, "689"}, - {"French Southern Territories", "TF", "ATF", 260, "262"}, - {"Gabon", "GA", "GAB", 266, "241"}, - {"Gambia", "GM", "GMB", 270, "220"}, - {"Georgia", "GE", "GEO", 268, "995"}, - {"Germany", "DE", "DEU", 276, "49"}, - {"Ghana", "GH", "GHA", 288, "233"}, - {"Gibraltar", "GI", "GIB", 292, "350"}, - {"Greece", "GR", "GRC", 300, "30"}, - {"Greenland", "GL", "GRL", 304, "299"}, - {"Grenada", "GD", "GRD", 308, "1-473"}, - {"Guadeloupe", "GP", "GLP", 312, "590"}, - {"Guam", "GU", "GUM", 316, "1-671"}, - {"Guatemala", "GT", "GTM", 320, "502"}, - {"Guernsey", "GG", "GGY", 831, "44"}, - {"Guinea", "GN", "GIN", 324, "224"}, - {"Guinea-Bissau", "GW", "GNB", 624, "245"}, - {"Guyana", "GY", "GUY", 328, "592"}, - {"Haiti", "HT", "HTI", 332, "509"}, - {"Heard Island and McDonald Islands", "HM", "HMD", 334, "672"}, - {"Holy See (Vatican City State)", "VA", "VAT", 336, "379"}, - {"Honduras", "HN", "HND", 340, "504"}, - {"Hong Kong", "HK", "HKG", 344, "852"}, - {"Hungary", "HU", "HUN", 348, "36"}, - {"Iceland", "IS", "ISL", 352, "354"}, - {"India", "IN", "IND", 356, "91"}, - {"Indonesia", "ID", "IDN", 360, "62"}, - {"Iran, Islamic Republic of", "IR", "IRN", 364, "98"}, - {"Iraq", "IQ", "IRQ", 368, "964"}, - {"Ireland", "IE", "IRL", 372, "353"}, - {"Isle of Man", "IM", "IMN", 833, "44"}, - {"Israel", "IL", "ISR", 376, "972"}, - {"Italy", "IT", "ITA", 380, "39"}, - {"Jamaica", "JM", "JAM", 388, "1-876"}, - {"Japan", "JP", "JPN", 392, "81"}, - {"Jersey", "JE", "JEY", 832, "44"}, - {"Jordan", "JO", "JOR", 400, "962"}, - {"Kazakhstan", "KZ", "KAZ", 398, "7"}, - {"Kenya", "KE", "KEN", 404, "254"}, - {"Kiribati", "KI", "KIR", 296, "686"}, - {"Korea, Democratic People's Republic of", "KP", "PRK", 408, "850"}, - {"Korea, Republic of", "KR", "KOR", 410, "82"}, - {"Kuwait", "KW", "KWT", 414, "965"}, - {"Kyrgyzstan", "KG", "KGZ", 417, "996"}, - {"Lao People's Democratic Republic", "LA", "LAO", 418, "856"}, - {"Latvia", "LV", "LVA", 428, "371"}, - {"Lebanon", "LB", "LBN", 422, "961"}, - {"Lesotho", "LS", "LSO", 426, "266"}, - {"Liberia", "LR", "LBR", 430, "231"}, - {"Libya", "LY", "LBY", 434, "218"}, - {"Liechtenstein", "LI", "LIE", 438, "423"}, - {"Lithuania", "LT", "LTU", 440, "370"}, - {"Luxembourg", "LU", "LUX", 442, "352"}, - {"Macao", "MO", "MAC", 446, "853"}, - {"Macedonia, the Former Yugoslav Republic of", "MK", "MKD", 807, "389"}, - {"Madagascar", "MG", "MDG", 450, "261"}, - {"Malawi", "MW", "MWI", 454, "265"}, - {"Malaysia", "MY", "MYS", 458, "60"}, - {"Maldives", "MV", "MDV", 462, "960"}, - {"Mali", "ML", "MLI", 466, "223"}, - {"Malta", "MT", "MLT", 470, "356"}, - {"Marshall Islands", "MH", "MHL", 584, "692"}, - {"Martinique", "MQ", "MTQ", 474, "596"}, - {"Mauritania", "MR", "MRT", 478, "222"}, - {"Mauritius", "MU", "MUS", 480, "230"}, - {"Mayotte", "YT", "MYT", 175, "262"}, - {"Mexico", "MX", "MEX", 484, "52"}, - {"Micronesia, Federated States of", "FM", "FSM", 583, "691"}, - {"Moldova, Republic of", "MD", "MDA", 498, "373"}, - {"Monaco", "MC", "MCO", 492, "377"}, - {"Mongolia", "MN", "MNG", 496, "976"}, - {"Montenegro", "ME", "MNE", 499, "382"}, - {"Montserrat", "MS", "MSR", 500, "1-664"}, - {"Morocco", "MA", "MAR", 504, "212"}, - {"Mozambique", "MZ", "MOZ", 508, "258"}, - {"Myanmar", "MM", "MMR", 104, "95"}, - {"Namibia", "NA", "NAM", 516, "264"}, - {"Nauru", "NR", "NRU", 520, "674"}, - {"Nepal", "NP", "NPL", 524, "977"}, - {"Netherlands", "NL", "NLD", 528, "31"}, - {"New Caledonia", "NC", "NCL", 540, "687"}, - {"New Zealand", "NZ", "NZL", 554, "64"}, - {"Nicaragua", "NI", "NIC", 558, "505"}, - {"Niger", "NE", "NER", 562, "227"}, - {"Nigeria", "NG", "NGA", 566, "234"}, - {"Niue", "NU", "NIU", 570, "683"}, - {"Norfolk Island", "NF", "NFK", 574, "672"}, - {"Northern Mariana Islands", "MP", "MNP", 580, "1-670"}, - {"Norway", "NO", "NOR", 578, "47"}, - {"Oman", "OM", "OMN", 512, "968"}, - {"Pakistan", "PK", "PAK", 586, "92"}, - {"Palau", "PW", "PLW", 585, "680"}, - {"Palestine, State of", "PS", "PSE", 275, "970"}, - {"Panama", "PA", "PAN", 591, "507"}, - {"Papua New Guinea", "PG", "PNG", 598, "675"}, - {"Paraguay", "PY", "PRY", 600, "595"}, - {"Peru", "PE", "PER", 604, "51"}, - {"Philippines", "PH", "PHL", 608, "63"}, - {"Pitcairn", "PN", "PCN", 612, "870"}, - {"Poland", "PL", "POL", 616, "48"}, - {"Portugal", "PT", "PRT", 620, "351"}, - {"Puerto Rico", "PR", "PRI", 630, "1"}, - {"Qatar", "QA", "QAT", 634, "974"}, - {"Romania", "RO", "ROU", 642, "40"}, - {"Russian Federation", "RU", "RUS", 643, "7"}, - {"Rwanda", "RW", "RWA", 646, "250"}, - {"Reunion", "RE", "REU", 638, "262"}, - {"Saint Barthelemy", "BL", "BLM", 652, "590"}, - {"Saint Helena", "SH", "SHN", 654, "290"}, - {"Saint Kitts and Nevis", "KN", "KNA", 659, "1-869"}, - {"Saint Lucia", "LC", "LCA", 662, "1-758"}, - {"Saint Martin (French part)", "MF", "MAF", 663, "590"}, - {"Saint Pierre and Miquelon", "PM", "SPM", 666, "508"}, - {"Saint Vincent and the Grenadines", "VC", "VCT", 670, "1-784"}, - {"Samoa", "WS", "WSM", 882, "685"}, - {"San Marino", "SM", "SMR", 674, "378"}, - {"Sao Tome and Principe", "ST", "STP", 678, "239"}, - {"Saudi Arabia", "SA", "SAU", 682, "966"}, - {"Senegal", "SN", "SEN", 686, "221"}, - {"Serbia", "RS", "SRB", 688, "381"}, - {"Seychelles", "SC", "SYC", 690, "248"}, - {"Sierra Leone", "SL", "SLE", 694, "232"}, - {"Singapore", "SG", "SGP", 702, "65"}, - {"Sint Maarten (Dutch part)", "SX", "SXM", 534, "1-721"}, - {"Slovakia", "SK", "SVK", 703, "421"}, - {"Slovenia", "SI", "SVN", 705, "386"}, - {"Solomon Islands", "SB", "SLB", 90, "677"}, - {"Somalia", "SO", "SOM", 706, "252"}, - {"South Africa", "ZA", "ZAF", 710, "27"}, - {"South Georgia and the South Sandwich Islands", "GS", "SGS", 239, "500"}, - {"South Sudan", "SS", "SSD", 728, "211"}, - {"Spain", "ES", "ESP", 724, "34"}, - {"Sri Lanka", "LK", "LKA", 144, "94"}, - {"Sudan", "SD", "SDN", 729, "249"}, - {"Suriname", "SR", "SUR", 740, "597"}, - {"Svalbard and Jan Mayen", "SJ", "SJM", 744, "47"}, - {"Swaziland", "SZ", "SWZ", 748, "268"}, - {"Sweden", "SE", "SWE", 752, "46"}, - {"Switzerland", "CH", "CHE", 756, "41"}, - {"Syrian Arab Republic", "SY", "SYR", 760, "963"}, - {"Taiwan", "TW", "TWN", 158, "886"}, - {"Tajikistan", "TJ", "TJK", 762, "992"}, - {"United Republic of Tanzania", "TZ", "TZA", 834, "255"}, - {"Thailand", "TH", "THA", 764, "66"}, - {"Timor-Leste", "TL", "TLS", 626, "670"}, - {"Togo", "TG", "TGO", 768, "228"}, - {"Tokelau", "TK", "TKL", 772, "690"}, - {"Tonga", "TO", "TON", 776, "676"}, - {"Trinidad and Tobago", "TT", "TTO", 780, "1-868"}, - {"Tunisia", "TN", "TUN", 788, "216"}, - {"Turkey", "TR", "TUR", 792, "90"}, - {"Turkmenistan", "TM", "TKM", 795, "993"}, - {"Turks and Caicos Islands", "TC", "TCA", 796, "1-649"}, - {"Tuvalu", "TV", "TUV", 798, "688"}, - {"Uganda", "UG", "UGA", 800, "256"}, - {"Ukraine", "UA", "UKR", 804, "380"}, - {"United Arab Emirates", "AE", "ARE", 784, "971"}, - {"United Kingdom", "GB", "GBR", 826, "44"}, - {"United States", "US", "USA", 840, "1"}, - {"United States Minor Outlying Islands", "UM", "UMI", 581, "1"}, - {"Uruguay", "UY", "URY", 858, "598"}, - {"Uzbekistan", "UZ", "UZB", 860, "998"}, - {"Vanuatu", "VU", "VUT", 548, "678"}, - {"Venezuela", "VE", "VEN", 862, "58"}, - {"Viet Nam", "VN", "VNM", 704, "84"}, - {"British Virgin Islands", "VG", "VGB", 92, "1-284"}, - {"US Virgin Islands", "VI", "VIR", 850, "1-340"}, - {"Wallis and Futuna", "WF", "WLF", 876, "681"}, - {"Western Sahara", "EH", "ESH", 732, "212"}, - {"Yemen", "YE", "YEM", 887, "967"}, - {"Zambia", "ZM", "ZMB", 894, "260"}, - {"Zimbabwe", "ZW", "ZWE", 716, "263"}, -} +import ( + _ "embed" + "encoding/json" +) + +//go:embed countries.json +var countriesData []byte -// getCountries returns a full list of countries with dialing codes func getCountries() []Country { - return countries + var result []Country + if err := json.Unmarshal(countriesData, &result); err != nil { + panic("sanja: failed to parse countries data: " + err.Error()) + } + return result } - -// TODO: fetch or parse countries from file e.g, JSON, CSV Or excel. diff --git a/countries.json b/countries.json new file mode 100644 index 0000000..fae8560 --- /dev/null +++ b/countries.json @@ -0,0 +1,2234 @@ +[ + { + "name": "Afghanistan", + "a2": "AF", + "a3": "AFG", + "numCode": 4, + "dialingCode": "93", + "minLocalDigits": 9, + "maxLocalDigits": 9 + }, + { + "name": "Albania", + "a2": "AL", + "a3": "ALB", + "numCode": 8, + "dialingCode": "355", + "minLocalDigits": 9, + "maxLocalDigits": 9 + }, + { + "name": "Algeria", + "a2": "DZ", + "a3": "DZA", + "numCode": 12, + "dialingCode": "213", + "minLocalDigits": 9, + "maxLocalDigits": 9 + }, + { + "name": "American Samoa", + "a2": "AS", + "a3": "ASM", + "numCode": 16, + "dialingCode": "1-684", + "minLocalDigits": 7, + "maxLocalDigits": 7 + }, + { + "name": "Andorra", + "a2": "AD", + "a3": "AND", + "numCode": 20, + "dialingCode": "376", + "minLocalDigits": 6, + "maxLocalDigits": 9 + }, + { + "name": "Angola", + "a2": "AO", + "a3": "AGO", + "numCode": 24, + "dialingCode": "244", + "minLocalDigits": 9, + "maxLocalDigits": 9 + }, + { + "name": "Anguilla", + "a2": "AI", + "a3": "AIA", + "numCode": 660, + "dialingCode": "1-264", + "minLocalDigits": 7, + "maxLocalDigits": 7 + }, + { + "name": "Antarctica", + "a2": "AQ", + "a3": "ATA", + "numCode": 10, + "dialingCode": "672", + "minLocalDigits": 6, + "maxLocalDigits": 12 + }, + { + "name": "Antigua and Barbuda", + "a2": "AG", + "a3": "ATG", + "numCode": 28, + "dialingCode": "1-268", + "minLocalDigits": 7, + "maxLocalDigits": 7 + }, + { + "name": "Argentina", + "a2": "AR", + "a3": "ARG", + "numCode": 32, + "dialingCode": "54", + "minLocalDigits": 10, + "maxLocalDigits": 11 + }, + { + "name": "Armenia", + "a2": "AM", + "a3": "ARM", + "numCode": 51, + "dialingCode": "374", + "minLocalDigits": 8, + "maxLocalDigits": 8 + }, + { + "name": "Aruba", + "a2": "AW", + "a3": "ABW", + "numCode": 533, + "dialingCode": "297", + "minLocalDigits": 7, + "maxLocalDigits": 7 + }, + { + "name": "Australia", + "a2": "AU", + "a3": "AUS", + "numCode": 36, + "dialingCode": "61", + "minLocalDigits": 9, + "maxLocalDigits": 9 + }, + { + "name": "Austria", + "a2": "AT", + "a3": "AUT", + "numCode": 40, + "dialingCode": "43", + "minLocalDigits": 7, + "maxLocalDigits": 13 + }, + { + "name": "Azerbaijan", + "a2": "AZ", + "a3": "AZE", + "numCode": 31, + "dialingCode": "994", + "minLocalDigits": 9, + "maxLocalDigits": 9 + }, + { + "name": "Bahamas", + "a2": "BS", + "a3": "BHS", + "numCode": 44, + "dialingCode": "1-242", + "minLocalDigits": 7, + "maxLocalDigits": 7 + }, + { + "name": "Bahrain", + "a2": "BH", + "a3": "BHR", + "numCode": 48, + "dialingCode": "973", + "minLocalDigits": 8, + "maxLocalDigits": 8 + }, + { + "name": "Bangladesh", + "a2": "BD", + "a3": "BGD", + "numCode": 50, + "dialingCode": "880", + "minLocalDigits": 10, + "maxLocalDigits": 10 + }, + { + "name": "Barbados", + "a2": "BB", + "a3": "BRB", + "numCode": 52, + "dialingCode": "1-246", + "minLocalDigits": 7, + "maxLocalDigits": 7 + }, + { + "name": "Belarus", + "a2": "BY", + "a3": "BLR", + "numCode": 112, + "dialingCode": "375", + "minLocalDigits": 9, + "maxLocalDigits": 9 + }, + { + "name": "Belgium", + "a2": "BE", + "a3": "BEL", + "numCode": 56, + "dialingCode": "32", + "minLocalDigits": 8, + "maxLocalDigits": 9 + }, + { + "name": "Belize", + "a2": "BZ", + "a3": "BLZ", + "numCode": 84, + "dialingCode": "501", + "minLocalDigits": 7, + "maxLocalDigits": 7 + }, + { + "name": "Benin", + "a2": "BJ", + "a3": "BEN", + "numCode": 204, + "dialingCode": "229", + "minLocalDigits": 8, + "maxLocalDigits": 8 + }, + { + "name": "Bermuda", + "a2": "BM", + "a3": "BMU", + "numCode": 60, + "dialingCode": "1-441", + "minLocalDigits": 7, + "maxLocalDigits": 7 + }, + { + "name": "Bhutan", + "a2": "BT", + "a3": "BTN", + "numCode": 64, + "dialingCode": "975", + "minLocalDigits": 7, + "maxLocalDigits": 8 + }, + { + "name": "Bolivia", + "a2": "BO", + "a3": "BOL", + "numCode": 68, + "dialingCode": "591", + "minLocalDigits": 8, + "maxLocalDigits": 8 + }, + { + "name": "Bonaire", + "a2": "BQ", + "a3": "BES", + "numCode": 535, + "dialingCode": "599", + "minLocalDigits": 7, + "maxLocalDigits": 8 + }, + { + "name": "Bosnia and Herzegovina", + "a2": "BA", + "a3": "BIH", + "numCode": 70, + "dialingCode": "387", + "minLocalDigits": 8, + "maxLocalDigits": 9 + }, + { + "name": "Botswana", + "a2": "BW", + "a3": "BWA", + "numCode": 72, + "dialingCode": "267", + "minLocalDigits": 7, + "maxLocalDigits": 8 + }, + { + "name": "Bouvet Island", + "a2": "BV", + "a3": "BVT", + "numCode": 74, + "dialingCode": "47", + "minLocalDigits": 8, + "maxLocalDigits": 8 + }, + { + "name": "Brazil", + "a2": "BR", + "a3": "BRA", + "numCode": 76, + "dialingCode": "55", + "minLocalDigits": 10, + "maxLocalDigits": 11 + }, + { + "name": "British Indian Ocean Territory", + "a2": "IO", + "a3": "IOT", + "numCode": 86, + "dialingCode": "246", + "minLocalDigits": 7, + "maxLocalDigits": 7 + }, + { + "name": "Brunei Darussalam", + "a2": "BN", + "a3": "BRN", + "numCode": 96, + "dialingCode": "673", + "minLocalDigits": 7, + "maxLocalDigits": 8 + }, + { + "name": "Bulgaria", + "a2": "BG", + "a3": "BGR", + "numCode": 100, + "dialingCode": "359", + "minLocalDigits": 9, + "maxLocalDigits": 9 + }, + { + "name": "Burkina Faso", + "a2": "BF", + "a3": "BFA", + "numCode": 854, + "dialingCode": "226", + "minLocalDigits": 8, + "maxLocalDigits": 8 + }, + { + "name": "Burundi", + "a2": "BI", + "a3": "BDI", + "numCode": 108, + "dialingCode": "257", + "minLocalDigits": 8, + "maxLocalDigits": 8 + }, + { + "name": "Cambodia", + "a2": "KH", + "a3": "KHM", + "numCode": 116, + "dialingCode": "855", + "minLocalDigits": 8, + "maxLocalDigits": 9 + }, + { + "name": "Cameroon", + "a2": "CM", + "a3": "CMR", + "numCode": 120, + "dialingCode": "237", + "minLocalDigits": 9, + "maxLocalDigits": 9 + }, + { + "name": "Canada", + "a2": "CA", + "a3": "CAN", + "numCode": 124, + "dialingCode": "1", + "minLocalDigits": 10, + "maxLocalDigits": 10 + }, + { + "name": "Cape Verde", + "a2": "CV", + "a3": "CPV", + "numCode": 132, + "dialingCode": "238", + "minLocalDigits": 7, + "maxLocalDigits": 7 + }, + { + "name": "Cayman Islands", + "a2": "KY", + "a3": "CYM", + "numCode": 136, + "dialingCode": "1-345", + "minLocalDigits": 7, + "maxLocalDigits": 7 + }, + { + "name": "Central African Republic", + "a2": "CF", + "a3": "CAF", + "numCode": 140, + "dialingCode": "236", + "minLocalDigits": 8, + "maxLocalDigits": 8 + }, + { + "name": "Chad", + "a2": "TD", + "a3": "TCD", + "numCode": 148, + "dialingCode": "235", + "minLocalDigits": 8, + "maxLocalDigits": 8 + }, + { + "name": "Chile", + "a2": "CL", + "a3": "CHL", + "numCode": 152, + "dialingCode": "56", + "minLocalDigits": 9, + "maxLocalDigits": 9 + }, + { + "name": "China", + "a2": "CN", + "a3": "CHN", + "numCode": 156, + "dialingCode": "86", + "minLocalDigits": 11, + "maxLocalDigits": 11 + }, + { + "name": "Christmas Island", + "a2": "CX", + "a3": "CXR", + "numCode": 162, + "dialingCode": "61", + "minLocalDigits": 9, + "maxLocalDigits": 9 + }, + { + "name": "Cocos (Keeling) Islands", + "a2": "CC", + "a3": "CCK", + "numCode": 166, + "dialingCode": "61", + "minLocalDigits": 9, + "maxLocalDigits": 9 + }, + { + "name": "Colombia", + "a2": "CO", + "a3": "COL", + "numCode": 170, + "dialingCode": "57", + "minLocalDigits": 10, + "maxLocalDigits": 10 + }, + { + "name": "Comoros", + "a2": "KM", + "a3": "COM", + "numCode": 174, + "dialingCode": "269", + "minLocalDigits": 7, + "maxLocalDigits": 7 + }, + { + "name": "Congo", + "a2": "CG", + "a3": "COG", + "numCode": 178, + "dialingCode": "242", + "minLocalDigits": 9, + "maxLocalDigits": 9 + }, + { + "name": "Democratic Republic of the Congo", + "a2": "CD", + "a3": "COD", + "numCode": 180, + "dialingCode": "243", + "minLocalDigits": 9, + "maxLocalDigits": 9 + }, + { + "name": "Cook Islands", + "a2": "CK", + "a3": "COK", + "numCode": 184, + "dialingCode": "682", + "minLocalDigits": 5, + "maxLocalDigits": 8 + }, + { + "name": "Costa Rica", + "a2": "CR", + "a3": "CRI", + "numCode": 188, + "dialingCode": "506", + "minLocalDigits": 8, + "maxLocalDigits": 8 + }, + { + "name": "Croatia", + "a2": "HR", + "a3": "HRV", + "numCode": 191, + "dialingCode": "385", + "minLocalDigits": 8, + "maxLocalDigits": 9 + }, + { + "name": "Cuba", + "a2": "CU", + "a3": "CUB", + "numCode": 192, + "dialingCode": "53", + "minLocalDigits": 8, + "maxLocalDigits": 8 + }, + { + "name": "Curacao", + "a2": "CW", + "a3": "CUW", + "numCode": 531, + "dialingCode": "599", + "minLocalDigits": 7, + "maxLocalDigits": 8 + }, + { + "name": "Cyprus", + "a2": "CY", + "a3": "CYP", + "numCode": 196, + "dialingCode": "357", + "minLocalDigits": 8, + "maxLocalDigits": 8 + }, + { + "name": "Czech Republic", + "a2": "CZ", + "a3": "CZE", + "numCode": 203, + "dialingCode": "420", + "minLocalDigits": 9, + "maxLocalDigits": 9 + }, + { + "name": "Cote d'Ivoire", + "a2": "CI", + "a3": "CIV", + "numCode": 384, + "dialingCode": "225", + "minLocalDigits": 10, + "maxLocalDigits": 10 + }, + { + "name": "Denmark", + "a2": "DK", + "a3": "DNK", + "numCode": 208, + "dialingCode": "45", + "minLocalDigits": 8, + "maxLocalDigits": 8 + }, + { + "name": "Djibouti", + "a2": "DJ", + "a3": "DJI", + "numCode": 262, + "dialingCode": "253", + "minLocalDigits": 8, + "maxLocalDigits": 8 + }, + { + "name": "Dominica", + "a2": "DM", + "a3": "DMA", + "numCode": 212, + "dialingCode": "1-767", + "minLocalDigits": 7, + "maxLocalDigits": 7 + }, + { + "name": "Dominican Republic", + "a2": "DO", + "a3": "DOM", + "numCode": 214, + "dialingCode": "1-809,1-829,1-849", + "minLocalDigits": 7, + "maxLocalDigits": 7 + }, + { + "name": "Ecuador", + "a2": "EC", + "a3": "ECU", + "numCode": 218, + "dialingCode": "593", + "minLocalDigits": 9, + "maxLocalDigits": 9 + }, + { + "name": "Egypt", + "a2": "EG", + "a3": "EGY", + "numCode": 818, + "dialingCode": "20", + "minLocalDigits": 10, + "maxLocalDigits": 10 + }, + { + "name": "El Salvador", + "a2": "SV", + "a3": "SLV", + "numCode": 222, + "dialingCode": "503", + "minLocalDigits": 8, + "maxLocalDigits": 8 + }, + { + "name": "Equatorial Guinea", + "a2": "GQ", + "a3": "GNQ", + "numCode": 226, + "dialingCode": "240", + "minLocalDigits": 9, + "maxLocalDigits": 9 + }, + { + "name": "Eritrea", + "a2": "ER", + "a3": "ERI", + "numCode": 232, + "dialingCode": "291", + "minLocalDigits": 7, + "maxLocalDigits": 7 + }, + { + "name": "Estonia", + "a2": "EE", + "a3": "EST", + "numCode": 233, + "dialingCode": "372", + "minLocalDigits": 7, + "maxLocalDigits": 8 + }, + { + "name": "Ethiopia", + "a2": "ET", + "a3": "ETH", + "numCode": 231, + "dialingCode": "251", + "minLocalDigits": 9, + "maxLocalDigits": 9 + }, + { + "name": "Falkland Islands (Malvinas)", + "a2": "FK", + "a3": "FLK", + "numCode": 238, + "dialingCode": "500", + "minLocalDigits": 5, + "maxLocalDigits": 5 + }, + { + "name": "Faroe Islands", + "a2": "FO", + "a3": "FRO", + "numCode": 234, + "dialingCode": "298", + "minLocalDigits": 6, + "maxLocalDigits": 6 + }, + { + "name": "Fiji", + "a2": "FJ", + "a3": "FJI", + "numCode": 242, + "dialingCode": "679", + "minLocalDigits": 7, + "maxLocalDigits": 7 + }, + { + "name": "Finland", + "a2": "FI", + "a3": "FIN", + "numCode": 246, + "dialingCode": "358", + "minLocalDigits": 5, + "maxLocalDigits": 12 + }, + { + "name": "France", + "a2": "FR", + "a3": "FRA", + "numCode": 250, + "dialingCode": "33", + "minLocalDigits": 9, + "maxLocalDigits": 9 + }, + { + "name": "French Guiana", + "a2": "GF", + "a3": "GUF", + "numCode": 254, + "dialingCode": "594", + "minLocalDigits": 9, + "maxLocalDigits": 9 + }, + { + "name": "French Polynesia", + "a2": "PF", + "a3": "PYF", + "numCode": 258, + "dialingCode": "689", + "minLocalDigits": 8, + "maxLocalDigits": 8 + }, + { + "name": "French Southern Territories", + "a2": "TF", + "a3": "ATF", + "numCode": 260, + "dialingCode": "262", + "minLocalDigits": 9, + "maxLocalDigits": 9 + }, + { + "name": "Gabon", + "a2": "GA", + "a3": "GAB", + "numCode": 266, + "dialingCode": "241", + "minLocalDigits": 7, + "maxLocalDigits": 8 + }, + { + "name": "Gambia", + "a2": "GM", + "a3": "GMB", + "numCode": 270, + "dialingCode": "220", + "minLocalDigits": 7, + "maxLocalDigits": 7 + }, + { + "name": "Georgia", + "a2": "GE", + "a3": "GEO", + "numCode": 268, + "dialingCode": "995", + "minLocalDigits": 9, + "maxLocalDigits": 9 + }, + { + "name": "Germany", + "a2": "DE", + "a3": "DEU", + "numCode": 276, + "dialingCode": "49", + "minLocalDigits": 3, + "maxLocalDigits": 12 + }, + { + "name": "Ghana", + "a2": "GH", + "a3": "GHA", + "numCode": 288, + "dialingCode": "233", + "minLocalDigits": 9, + "maxLocalDigits": 9 + }, + { + "name": "Gibraltar", + "a2": "GI", + "a3": "GIB", + "numCode": 292, + "dialingCode": "350", + "minLocalDigits": 8, + "maxLocalDigits": 8 + }, + { + "name": "Greece", + "a2": "GR", + "a3": "GRC", + "numCode": 300, + "dialingCode": "30", + "minLocalDigits": 10, + "maxLocalDigits": 10 + }, + { + "name": "Greenland", + "a2": "GL", + "a3": "GRL", + "numCode": 304, + "dialingCode": "299", + "minLocalDigits": 6, + "maxLocalDigits": 6 + }, + { + "name": "Grenada", + "a2": "GD", + "a3": "GRD", + "numCode": 308, + "dialingCode": "1-473", + "minLocalDigits": 7, + "maxLocalDigits": 7 + }, + { + "name": "Guadeloupe", + "a2": "GP", + "a3": "GLP", + "numCode": 312, + "dialingCode": "590", + "minLocalDigits": 9, + "maxLocalDigits": 9 + }, + { + "name": "Guam", + "a2": "GU", + "a3": "GUM", + "numCode": 316, + "dialingCode": "1-671", + "minLocalDigits": 7, + "maxLocalDigits": 7 + }, + { + "name": "Guatemala", + "a2": "GT", + "a3": "GTM", + "numCode": 320, + "dialingCode": "502", + "minLocalDigits": 8, + "maxLocalDigits": 8 + }, + { + "name": "Guernsey", + "a2": "GG", + "a3": "GGY", + "numCode": 831, + "dialingCode": "44", + "minLocalDigits": 10, + "maxLocalDigits": 10 + }, + { + "name": "Guinea", + "a2": "GN", + "a3": "GIN", + "numCode": 324, + "dialingCode": "224", + "minLocalDigits": 9, + "maxLocalDigits": 9 + }, + { + "name": "Guinea-Bissau", + "a2": "GW", + "a3": "GNB", + "numCode": 624, + "dialingCode": "245", + "minLocalDigits": 7, + "maxLocalDigits": 7 + }, + { + "name": "Guyana", + "a2": "GY", + "a3": "GUY", + "numCode": 328, + "dialingCode": "592", + "minLocalDigits": 7, + "maxLocalDigits": 7 + }, + { + "name": "Haiti", + "a2": "HT", + "a3": "HTI", + "numCode": 332, + "dialingCode": "509", + "minLocalDigits": 8, + "maxLocalDigits": 8 + }, + { + "name": "Heard Island and McDonald Islands", + "a2": "HM", + "a3": "HMD", + "numCode": 334, + "dialingCode": "672", + "minLocalDigits": 6, + "maxLocalDigits": 12 + }, + { + "name": "Holy See (Vatican City State)", + "a2": "VA", + "a3": "VAT", + "numCode": 336, + "dialingCode": "379", + "minLocalDigits": 10, + "maxLocalDigits": 10 + }, + { + "name": "Honduras", + "a2": "HN", + "a3": "HND", + "numCode": 340, + "dialingCode": "504", + "minLocalDigits": 8, + "maxLocalDigits": 8 + }, + { + "name": "Hong Kong", + "a2": "HK", + "a3": "HKG", + "numCode": 344, + "dialingCode": "852", + "minLocalDigits": 8, + "maxLocalDigits": 8 + }, + { + "name": "Hungary", + "a2": "HU", + "a3": "HUN", + "numCode": 348, + "dialingCode": "36", + "minLocalDigits": 8, + "maxLocalDigits": 9 + }, + { + "name": "Iceland", + "a2": "IS", + "a3": "ISL", + "numCode": 352, + "dialingCode": "354", + "minLocalDigits": 7, + "maxLocalDigits": 9 + }, + { + "name": "India", + "a2": "IN", + "a3": "IND", + "numCode": 356, + "dialingCode": "91", + "minLocalDigits": 10, + "maxLocalDigits": 10 + }, + { + "name": "Indonesia", + "a2": "ID", + "a3": "IDN", + "numCode": 360, + "dialingCode": "62", + "minLocalDigits": 7, + "maxLocalDigits": 12 + }, + { + "name": "Iran, Islamic Republic of", + "a2": "IR", + "a3": "IRN", + "numCode": 364, + "dialingCode": "98", + "minLocalDigits": 10, + "maxLocalDigits": 10 + }, + { + "name": "Iraq", + "a2": "IQ", + "a3": "IRQ", + "numCode": 368, + "dialingCode": "964", + "minLocalDigits": 10, + "maxLocalDigits": 10 + }, + { + "name": "Ireland", + "a2": "IE", + "a3": "IRL", + "numCode": 372, + "dialingCode": "353", + "minLocalDigits": 9, + "maxLocalDigits": 9 + }, + { + "name": "Isle of Man", + "a2": "IM", + "a3": "IMN", + "numCode": 833, + "dialingCode": "44", + "minLocalDigits": 10, + "maxLocalDigits": 10 + }, + { + "name": "Israel", + "a2": "IL", + "a3": "ISR", + "numCode": 376, + "dialingCode": "972", + "minLocalDigits": 9, + "maxLocalDigits": 9 + }, + { + "name": "Italy", + "a2": "IT", + "a3": "ITA", + "numCode": 380, + "dialingCode": "39", + "minLocalDigits": 6, + "maxLocalDigits": 11 + }, + { + "name": "Jamaica", + "a2": "JM", + "a3": "JAM", + "numCode": 388, + "dialingCode": "1-876", + "minLocalDigits": 7, + "maxLocalDigits": 7 + }, + { + "name": "Japan", + "a2": "JP", + "a3": "JPN", + "numCode": 392, + "dialingCode": "81", + "minLocalDigits": 9, + "maxLocalDigits": 11 + }, + { + "name": "Jersey", + "a2": "JE", + "a3": "JEY", + "numCode": 832, + "dialingCode": "44", + "minLocalDigits": 10, + "maxLocalDigits": 10 + }, + { + "name": "Jordan", + "a2": "JO", + "a3": "JOR", + "numCode": 400, + "dialingCode": "962", + "minLocalDigits": 9, + "maxLocalDigits": 9 + }, + { + "name": "Kazakhstan", + "a2": "KZ", + "a3": "KAZ", + "numCode": 398, + "dialingCode": "7", + "minLocalDigits": 10, + "maxLocalDigits": 10 + }, + { + "name": "Kenya", + "a2": "KE", + "a3": "KEN", + "numCode": 404, + "dialingCode": "254", + "minLocalDigits": 9, + "maxLocalDigits": 9 + }, + { + "name": "Kiribati", + "a2": "KI", + "a3": "KIR", + "numCode": 296, + "dialingCode": "686", + "minLocalDigits": 8, + "maxLocalDigits": 8 + }, + { + "name": "Korea, Democratic People's Republic of", + "a2": "KP", + "a3": "PRK", + "numCode": 408, + "dialingCode": "850", + "minLocalDigits": 8, + "maxLocalDigits": 10 + }, + { + "name": "Korea, Republic of", + "a2": "KR", + "a3": "KOR", + "numCode": 410, + "dialingCode": "82", + "minLocalDigits": 9, + "maxLocalDigits": 11 + }, + { + "name": "Kuwait", + "a2": "KW", + "a3": "KWT", + "numCode": 414, + "dialingCode": "965", + "minLocalDigits": 8, + "maxLocalDigits": 8 + }, + { + "name": "Kyrgyzstan", + "a2": "KG", + "a3": "KGZ", + "numCode": 417, + "dialingCode": "996", + "minLocalDigits": 9, + "maxLocalDigits": 9 + }, + { + "name": "Lao People's Democratic Republic", + "a2": "LA", + "a3": "LAO", + "numCode": 418, + "dialingCode": "856", + "minLocalDigits": 8, + "maxLocalDigits": 9 + }, + { + "name": "Latvia", + "a2": "LV", + "a3": "LVA", + "numCode": 428, + "dialingCode": "371", + "minLocalDigits": 8, + "maxLocalDigits": 8 + }, + { + "name": "Lebanon", + "a2": "LB", + "a3": "LBN", + "numCode": 422, + "dialingCode": "961", + "minLocalDigits": 7, + "maxLocalDigits": 8 + }, + { + "name": "Lesotho", + "a2": "LS", + "a3": "LSO", + "numCode": 426, + "dialingCode": "266", + "minLocalDigits": 8, + "maxLocalDigits": 8 + }, + { + "name": "Liberia", + "a2": "LR", + "a3": "LBR", + "numCode": 430, + "dialingCode": "231", + "minLocalDigits": 7, + "maxLocalDigits": 8 + }, + { + "name": "Libya", + "a2": "LY", + "a3": "LBY", + "numCode": 434, + "dialingCode": "218", + "minLocalDigits": 9, + "maxLocalDigits": 10 + }, + { + "name": "Liechtenstein", + "a2": "LI", + "a3": "LIE", + "numCode": 438, + "dialingCode": "423", + "minLocalDigits": 7, + "maxLocalDigits": 9 + }, + { + "name": "Lithuania", + "a2": "LT", + "a3": "LTU", + "numCode": 440, + "dialingCode": "370", + "minLocalDigits": 8, + "maxLocalDigits": 8 + }, + { + "name": "Luxembourg", + "a2": "LU", + "a3": "LUX", + "numCode": 442, + "dialingCode": "352", + "minLocalDigits": 6, + "maxLocalDigits": 9 + }, + { + "name": "Macao", + "a2": "MO", + "a3": "MAC", + "numCode": 446, + "dialingCode": "853", + "minLocalDigits": 8, + "maxLocalDigits": 8 + }, + { + "name": "Macedonia, the Former Yugoslav Republic of", + "a2": "MK", + "a3": "MKD", + "numCode": 807, + "dialingCode": "389", + "minLocalDigits": 8, + "maxLocalDigits": 8 + }, + { + "name": "Madagascar", + "a2": "MG", + "a3": "MDG", + "numCode": 450, + "dialingCode": "261", + "minLocalDigits": 9, + "maxLocalDigits": 9 + }, + { + "name": "Malawi", + "a2": "MW", + "a3": "MWI", + "numCode": 454, + "dialingCode": "265", + "minLocalDigits": 9, + "maxLocalDigits": 9 + }, + { + "name": "Malaysia", + "a2": "MY", + "a3": "MYS", + "numCode": 458, + "dialingCode": "60", + "minLocalDigits": 9, + "maxLocalDigits": 10 + }, + { + "name": "Maldives", + "a2": "MV", + "a3": "MDV", + "numCode": 462, + "dialingCode": "960", + "minLocalDigits": 7, + "maxLocalDigits": 7 + }, + { + "name": "Mali", + "a2": "ML", + "a3": "MLI", + "numCode": 466, + "dialingCode": "223", + "minLocalDigits": 8, + "maxLocalDigits": 8 + }, + { + "name": "Malta", + "a2": "MT", + "a3": "MLT", + "numCode": 470, + "dialingCode": "356", + "minLocalDigits": 8, + "maxLocalDigits": 8 + }, + { + "name": "Marshall Islands", + "a2": "MH", + "a3": "MHL", + "numCode": 584, + "dialingCode": "692", + "minLocalDigits": 7, + "maxLocalDigits": 7 + }, + { + "name": "Martinique", + "a2": "MQ", + "a3": "MTQ", + "numCode": 474, + "dialingCode": "596", + "minLocalDigits": 9, + "maxLocalDigits": 9 + }, + { + "name": "Mauritania", + "a2": "MR", + "a3": "MRT", + "numCode": 478, + "dialingCode": "222", + "minLocalDigits": 8, + "maxLocalDigits": 8 + }, + { + "name": "Mauritius", + "a2": "MU", + "a3": "MUS", + "numCode": 480, + "dialingCode": "230", + "minLocalDigits": 8, + "maxLocalDigits": 8 + }, + { + "name": "Mayotte", + "a2": "YT", + "a3": "MYT", + "numCode": 175, + "dialingCode": "262", + "minLocalDigits": 9, + "maxLocalDigits": 9 + }, + { + "name": "Mexico", + "a2": "MX", + "a3": "MEX", + "numCode": 484, + "dialingCode": "52", + "minLocalDigits": 10, + "maxLocalDigits": 10 + }, + { + "name": "Micronesia, Federated States of", + "a2": "FM", + "a3": "FSM", + "numCode": 583, + "dialingCode": "691", + "minLocalDigits": 7, + "maxLocalDigits": 7 + }, + { + "name": "Moldova, Republic of", + "a2": "MD", + "a3": "MDA", + "numCode": 498, + "dialingCode": "373", + "minLocalDigits": 8, + "maxLocalDigits": 8 + }, + { + "name": "Monaco", + "a2": "MC", + "a3": "MCO", + "numCode": 492, + "dialingCode": "377", + "minLocalDigits": 8, + "maxLocalDigits": 9 + }, + { + "name": "Mongolia", + "a2": "MN", + "a3": "MNG", + "numCode": 496, + "dialingCode": "976", + "minLocalDigits": 8, + "maxLocalDigits": 8 + }, + { + "name": "Montenegro", + "a2": "ME", + "a3": "MNE", + "numCode": 499, + "dialingCode": "382", + "minLocalDigits": 8, + "maxLocalDigits": 8 + }, + { + "name": "Montserrat", + "a2": "MS", + "a3": "MSR", + "numCode": 500, + "dialingCode": "1-664", + "minLocalDigits": 7, + "maxLocalDigits": 7 + }, + { + "name": "Morocco", + "a2": "MA", + "a3": "MAR", + "numCode": 504, + "dialingCode": "212", + "minLocalDigits": 9, + "maxLocalDigits": 9 + }, + { + "name": "Mozambique", + "a2": "MZ", + "a3": "MOZ", + "numCode": 508, + "dialingCode": "258", + "minLocalDigits": 9, + "maxLocalDigits": 9 + }, + { + "name": "Myanmar", + "a2": "MM", + "a3": "MMR", + "numCode": 104, + "dialingCode": "95", + "minLocalDigits": 7, + "maxLocalDigits": 10 + }, + { + "name": "Namibia", + "a2": "NA", + "a3": "NAM", + "numCode": 516, + "dialingCode": "264", + "minLocalDigits": 9, + "maxLocalDigits": 9 + }, + { + "name": "Nauru", + "a2": "NR", + "a3": "NRU", + "numCode": 520, + "dialingCode": "674", + "minLocalDigits": 7, + "maxLocalDigits": 7 + }, + { + "name": "Nepal", + "a2": "NP", + "a3": "NPL", + "numCode": 524, + "dialingCode": "977", + "minLocalDigits": 10, + "maxLocalDigits": 10 + }, + { + "name": "Netherlands", + "a2": "NL", + "a3": "NLD", + "numCode": 528, + "dialingCode": "31", + "minLocalDigits": 9, + "maxLocalDigits": 9 + }, + { + "name": "New Caledonia", + "a2": "NC", + "a3": "NCL", + "numCode": 540, + "dialingCode": "687", + "minLocalDigits": 6, + "maxLocalDigits": 6 + }, + { + "name": "New Zealand", + "a2": "NZ", + "a3": "NZL", + "numCode": 554, + "dialingCode": "64", + "minLocalDigits": 8, + "maxLocalDigits": 10 + }, + { + "name": "Nicaragua", + "a2": "NI", + "a3": "NIC", + "numCode": 558, + "dialingCode": "505", + "minLocalDigits": 8, + "maxLocalDigits": 8 + }, + { + "name": "Niger", + "a2": "NE", + "a3": "NER", + "numCode": 562, + "dialingCode": "227", + "minLocalDigits": 8, + "maxLocalDigits": 8 + }, + { + "name": "Nigeria", + "a2": "NG", + "a3": "NGA", + "numCode": 566, + "dialingCode": "234", + "minLocalDigits": 7, + "maxLocalDigits": 10 + }, + { + "name": "Niue", + "a2": "NU", + "a3": "NIU", + "numCode": 570, + "dialingCode": "683", + "minLocalDigits": 4, + "maxLocalDigits": 7 + }, + { + "name": "Norfolk Island", + "a2": "NF", + "a3": "NFK", + "numCode": 574, + "dialingCode": "672", + "minLocalDigits": 6, + "maxLocalDigits": 12 + }, + { + "name": "Northern Mariana Islands", + "a2": "MP", + "a3": "MNP", + "numCode": 580, + "dialingCode": "1-670", + "minLocalDigits": 7, + "maxLocalDigits": 7 + }, + { + "name": "Norway", + "a2": "NO", + "a3": "NOR", + "numCode": 578, + "dialingCode": "47", + "minLocalDigits": 8, + "maxLocalDigits": 8 + }, + { + "name": "Oman", + "a2": "OM", + "a3": "OMN", + "numCode": 512, + "dialingCode": "968", + "minLocalDigits": 8, + "maxLocalDigits": 8 + }, + { + "name": "Pakistan", + "a2": "PK", + "a3": "PAK", + "numCode": 586, + "dialingCode": "92", + "minLocalDigits": 10, + "maxLocalDigits": 10 + }, + { + "name": "Palau", + "a2": "PW", + "a3": "PLW", + "numCode": 585, + "dialingCode": "680", + "minLocalDigits": 7, + "maxLocalDigits": 7 + }, + { + "name": "Palestine, State of", + "a2": "PS", + "a3": "PSE", + "numCode": 275, + "dialingCode": "970", + "minLocalDigits": 9, + "maxLocalDigits": 9 + }, + { + "name": "Panama", + "a2": "PA", + "a3": "PAN", + "numCode": 591, + "dialingCode": "507", + "minLocalDigits": 8, + "maxLocalDigits": 8 + }, + { + "name": "Papua New Guinea", + "a2": "PG", + "a3": "PNG", + "numCode": 598, + "dialingCode": "675", + "minLocalDigits": 7, + "maxLocalDigits": 8 + }, + { + "name": "Paraguay", + "a2": "PY", + "a3": "PRY", + "numCode": 600, + "dialingCode": "595", + "minLocalDigits": 9, + "maxLocalDigits": 9 + }, + { + "name": "Peru", + "a2": "PE", + "a3": "PER", + "numCode": 604, + "dialingCode": "51", + "minLocalDigits": 9, + "maxLocalDigits": 9 + }, + { + "name": "Philippines", + "a2": "PH", + "a3": "PHL", + "numCode": 608, + "dialingCode": "63", + "minLocalDigits": 10, + "maxLocalDigits": 10 + }, + { + "name": "Pitcairn", + "a2": "PN", + "a3": "PCN", + "numCode": 612, + "dialingCode": "870", + "minLocalDigits": 6, + "maxLocalDigits": 12 + }, + { + "name": "Poland", + "a2": "PL", + "a3": "POL", + "numCode": 616, + "dialingCode": "48", + "minLocalDigits": 9, + "maxLocalDigits": 9 + }, + { + "name": "Portugal", + "a2": "PT", + "a3": "PRT", + "numCode": 620, + "dialingCode": "351", + "minLocalDigits": 9, + "maxLocalDigits": 9 + }, + { + "name": "Puerto Rico", + "a2": "PR", + "a3": "PRI", + "numCode": 630, + "dialingCode": "1", + "minLocalDigits": 10, + "maxLocalDigits": 10 + }, + { + "name": "Qatar", + "a2": "QA", + "a3": "QAT", + "numCode": 634, + "dialingCode": "974", + "minLocalDigits": 8, + "maxLocalDigits": 8 + }, + { + "name": "Romania", + "a2": "RO", + "a3": "ROU", + "numCode": 642, + "dialingCode": "40", + "minLocalDigits": 9, + "maxLocalDigits": 9 + }, + { + "name": "Russian Federation", + "a2": "RU", + "a3": "RUS", + "numCode": 643, + "dialingCode": "7", + "minLocalDigits": 10, + "maxLocalDigits": 10 + }, + { + "name": "Rwanda", + "a2": "RW", + "a3": "RWA", + "numCode": 646, + "dialingCode": "250", + "minLocalDigits": 9, + "maxLocalDigits": 9 + }, + { + "name": "Reunion", + "a2": "RE", + "a3": "REU", + "numCode": 638, + "dialingCode": "262", + "minLocalDigits": 9, + "maxLocalDigits": 9 + }, + { + "name": "Saint Barthelemy", + "a2": "BL", + "a3": "BLM", + "numCode": 652, + "dialingCode": "590", + "minLocalDigits": 9, + "maxLocalDigits": 9 + }, + { + "name": "Saint Helena", + "a2": "SH", + "a3": "SHN", + "numCode": 654, + "dialingCode": "290", + "minLocalDigits": 4, + "maxLocalDigits": 4 + }, + { + "name": "Saint Kitts and Nevis", + "a2": "KN", + "a3": "KNA", + "numCode": 659, + "dialingCode": "1-869", + "minLocalDigits": 7, + "maxLocalDigits": 7 + }, + { + "name": "Saint Lucia", + "a2": "LC", + "a3": "LCA", + "numCode": 662, + "dialingCode": "1-758", + "minLocalDigits": 7, + "maxLocalDigits": 7 + }, + { + "name": "Saint Martin (French part)", + "a2": "MF", + "a3": "MAF", + "numCode": 663, + "dialingCode": "590", + "minLocalDigits": 9, + "maxLocalDigits": 9 + }, + { + "name": "Saint Pierre and Miquelon", + "a2": "PM", + "a3": "SPM", + "numCode": 666, + "dialingCode": "508", + "minLocalDigits": 6, + "maxLocalDigits": 6 + }, + { + "name": "Saint Vincent and the Grenadines", + "a2": "VC", + "a3": "VCT", + "numCode": 670, + "dialingCode": "1-784", + "minLocalDigits": 7, + "maxLocalDigits": 7 + }, + { + "name": "Samoa", + "a2": "WS", + "a3": "WSM", + "numCode": 882, + "dialingCode": "685", + "minLocalDigits": 7, + "maxLocalDigits": 7 + }, + { + "name": "San Marino", + "a2": "SM", + "a3": "SMR", + "numCode": 674, + "dialingCode": "378", + "minLocalDigits": 6, + "maxLocalDigits": 10 + }, + { + "name": "Sao Tome and Principe", + "a2": "ST", + "a3": "STP", + "numCode": 678, + "dialingCode": "239", + "minLocalDigits": 7, + "maxLocalDigits": 7 + }, + { + "name": "Saudi Arabia", + "a2": "SA", + "a3": "SAU", + "numCode": 682, + "dialingCode": "966", + "minLocalDigits": 9, + "maxLocalDigits": 9 + }, + { + "name": "Senegal", + "a2": "SN", + "a3": "SEN", + "numCode": 686, + "dialingCode": "221", + "minLocalDigits": 9, + "maxLocalDigits": 9 + }, + { + "name": "Serbia", + "a2": "RS", + "a3": "SRB", + "numCode": 688, + "dialingCode": "381", + "minLocalDigits": 8, + "maxLocalDigits": 9 + }, + { + "name": "Seychelles", + "a2": "SC", + "a3": "SYC", + "numCode": 690, + "dialingCode": "248", + "minLocalDigits": 7, + "maxLocalDigits": 7 + }, + { + "name": "Sierra Leone", + "a2": "SL", + "a3": "SLE", + "numCode": 694, + "dialingCode": "232", + "minLocalDigits": 8, + "maxLocalDigits": 8 + }, + { + "name": "Singapore", + "a2": "SG", + "a3": "SGP", + "numCode": 702, + "dialingCode": "65", + "minLocalDigits": 8, + "maxLocalDigits": 8 + }, + { + "name": "Sint Maarten (Dutch part)", + "a2": "SX", + "a3": "SXM", + "numCode": 534, + "dialingCode": "1-721", + "minLocalDigits": 7, + "maxLocalDigits": 7 + }, + { + "name": "Slovakia", + "a2": "SK", + "a3": "SVK", + "numCode": 703, + "dialingCode": "421", + "minLocalDigits": 9, + "maxLocalDigits": 9 + }, + { + "name": "Slovenia", + "a2": "SI", + "a3": "SVN", + "numCode": 705, + "dialingCode": "386", + "minLocalDigits": 8, + "maxLocalDigits": 8 + }, + { + "name": "Solomon Islands", + "a2": "SB", + "a3": "SLB", + "numCode": 90, + "dialingCode": "677", + "minLocalDigits": 7, + "maxLocalDigits": 7 + }, + { + "name": "Somalia", + "a2": "SO", + "a3": "SOM", + "numCode": 706, + "dialingCode": "252", + "minLocalDigits": 8, + "maxLocalDigits": 9 + }, + { + "name": "South Africa", + "a2": "ZA", + "a3": "ZAF", + "numCode": 710, + "dialingCode": "27", + "minLocalDigits": 9, + "maxLocalDigits": 9 + }, + { + "name": "South Georgia and the South Sandwich Islands", + "a2": "GS", + "a3": "SGS", + "numCode": 239, + "dialingCode": "500", + "minLocalDigits": 5, + "maxLocalDigits": 5 + }, + { + "name": "South Sudan", + "a2": "SS", + "a3": "SSD", + "numCode": 728, + "dialingCode": "211", + "minLocalDigits": 9, + "maxLocalDigits": 9 + }, + { + "name": "Spain", + "a2": "ES", + "a3": "ESP", + "numCode": 724, + "dialingCode": "34", + "minLocalDigits": 9, + "maxLocalDigits": 9 + }, + { + "name": "Sri Lanka", + "a2": "LK", + "a3": "LKA", + "numCode": 144, + "dialingCode": "94", + "minLocalDigits": 9, + "maxLocalDigits": 9 + }, + { + "name": "Sudan", + "a2": "SD", + "a3": "SDN", + "numCode": 729, + "dialingCode": "249", + "minLocalDigits": 9, + "maxLocalDigits": 9 + }, + { + "name": "Suriname", + "a2": "SR", + "a3": "SUR", + "numCode": 740, + "dialingCode": "597", + "minLocalDigits": 6, + "maxLocalDigits": 7 + }, + { + "name": "Svalbard and Jan Mayen", + "a2": "SJ", + "a3": "SJM", + "numCode": 744, + "dialingCode": "47", + "minLocalDigits": 8, + "maxLocalDigits": 8 + }, + { + "name": "Swaziland", + "a2": "SZ", + "a3": "SWZ", + "numCode": 748, + "dialingCode": "268", + "minLocalDigits": 8, + "maxLocalDigits": 8 + }, + { + "name": "Sweden", + "a2": "SE", + "a3": "SWE", + "numCode": 752, + "dialingCode": "46", + "minLocalDigits": 7, + "maxLocalDigits": 10 + }, + { + "name": "Switzerland", + "a2": "CH", + "a3": "CHE", + "numCode": 756, + "dialingCode": "41", + "minLocalDigits": 9, + "maxLocalDigits": 9 + }, + { + "name": "Syrian Arab Republic", + "a2": "SY", + "a3": "SYR", + "numCode": 760, + "dialingCode": "963", + "minLocalDigits": 9, + "maxLocalDigits": 9 + }, + { + "name": "Taiwan", + "a2": "TW", + "a3": "TWN", + "numCode": 158, + "dialingCode": "886", + "minLocalDigits": 9, + "maxLocalDigits": 9 + }, + { + "name": "Tajikistan", + "a2": "TJ", + "a3": "TJK", + "numCode": 762, + "dialingCode": "992", + "minLocalDigits": 9, + "maxLocalDigits": 9 + }, + { + "name": "United Republic of Tanzania", + "a2": "TZ", + "a3": "TZA", + "numCode": 834, + "dialingCode": "255", + "minLocalDigits": 9, + "maxLocalDigits": 9 + }, + { + "name": "Thailand", + "a2": "TH", + "a3": "THA", + "numCode": 764, + "dialingCode": "66", + "minLocalDigits": 9, + "maxLocalDigits": 9 + }, + { + "name": "Timor-Leste", + "a2": "TL", + "a3": "TLS", + "numCode": 626, + "dialingCode": "670", + "minLocalDigits": 7, + "maxLocalDigits": 8 + }, + { + "name": "Togo", + "a2": "TG", + "a3": "TGO", + "numCode": 768, + "dialingCode": "228", + "minLocalDigits": 8, + "maxLocalDigits": 8 + }, + { + "name": "Tokelau", + "a2": "TK", + "a3": "TKL", + "numCode": 772, + "dialingCode": "690", + "minLocalDigits": 4, + "maxLocalDigits": 4 + }, + { + "name": "Tonga", + "a2": "TO", + "a3": "TON", + "numCode": 776, + "dialingCode": "676", + "minLocalDigits": 5, + "maxLocalDigits": 7 + }, + { + "name": "Trinidad and Tobago", + "a2": "TT", + "a3": "TTO", + "numCode": 780, + "dialingCode": "1-868", + "minLocalDigits": 7, + "maxLocalDigits": 7 + }, + { + "name": "Tunisia", + "a2": "TN", + "a3": "TUN", + "numCode": 788, + "dialingCode": "216", + "minLocalDigits": 8, + "maxLocalDigits": 8 + }, + { + "name": "Turkey", + "a2": "TR", + "a3": "TUR", + "numCode": 792, + "dialingCode": "90", + "minLocalDigits": 10, + "maxLocalDigits": 10 + }, + { + "name": "Turkmenistan", + "a2": "TM", + "a3": "TKM", + "numCode": 795, + "dialingCode": "993", + "minLocalDigits": 8, + "maxLocalDigits": 8 + }, + { + "name": "Turks and Caicos Islands", + "a2": "TC", + "a3": "TCA", + "numCode": 796, + "dialingCode": "1-649", + "minLocalDigits": 7, + "maxLocalDigits": 7 + }, + { + "name": "Tuvalu", + "a2": "TV", + "a3": "TUV", + "numCode": 798, + "dialingCode": "688", + "minLocalDigits": 5, + "maxLocalDigits": 7 + }, + { + "name": "Uganda", + "a2": "UG", + "a3": "UGA", + "numCode": 800, + "dialingCode": "256", + "minLocalDigits": 9, + "maxLocalDigits": 9 + }, + { + "name": "Ukraine", + "a2": "UA", + "a3": "UKR", + "numCode": 804, + "dialingCode": "380", + "minLocalDigits": 9, + "maxLocalDigits": 9 + }, + { + "name": "United Arab Emirates", + "a2": "AE", + "a3": "ARE", + "numCode": 784, + "dialingCode": "971", + "minLocalDigits": 9, + "maxLocalDigits": 9 + }, + { + "name": "United Kingdom", + "a2": "GB", + "a3": "GBR", + "numCode": 826, + "dialingCode": "44", + "minLocalDigits": 9, + "maxLocalDigits": 10 + }, + { + "name": "United States", + "a2": "US", + "a3": "USA", + "numCode": 840, + "dialingCode": "1", + "minLocalDigits": 10, + "maxLocalDigits": 10 + }, + { + "name": "United States Minor Outlying Islands", + "a2": "UM", + "a3": "UMI", + "numCode": 581, + "dialingCode": "1", + "minLocalDigits": 10, + "maxLocalDigits": 10 + }, + { + "name": "Uruguay", + "a2": "UY", + "a3": "URY", + "numCode": 858, + "dialingCode": "598", + "minLocalDigits": 8, + "maxLocalDigits": 9 + }, + { + "name": "Uzbekistan", + "a2": "UZ", + "a3": "UZB", + "numCode": 860, + "dialingCode": "998", + "minLocalDigits": 9, + "maxLocalDigits": 9 + }, + { + "name": "Vanuatu", + "a2": "VU", + "a3": "VUT", + "numCode": 548, + "dialingCode": "678", + "minLocalDigits": 7, + "maxLocalDigits": 7 + }, + { + "name": "Venezuela", + "a2": "VE", + "a3": "VEN", + "numCode": 862, + "dialingCode": "58", + "minLocalDigits": 10, + "maxLocalDigits": 10 + }, + { + "name": "Viet Nam", + "a2": "VN", + "a3": "VNM", + "numCode": 704, + "dialingCode": "84", + "minLocalDigits": 9, + "maxLocalDigits": 10 + }, + { + "name": "British Virgin Islands", + "a2": "VG", + "a3": "VGB", + "numCode": 92, + "dialingCode": "1-284", + "minLocalDigits": 7, + "maxLocalDigits": 7 + }, + { + "name": "US Virgin Islands", + "a2": "VI", + "a3": "VIR", + "numCode": 850, + "dialingCode": "1-340", + "minLocalDigits": 7, + "maxLocalDigits": 7 + }, + { + "name": "Wallis and Futuna", + "a2": "WF", + "a3": "WLF", + "numCode": 876, + "dialingCode": "681", + "minLocalDigits": 6, + "maxLocalDigits": 6 + }, + { + "name": "Western Sahara", + "a2": "EH", + "a3": "ESH", + "numCode": 732, + "dialingCode": "212", + "minLocalDigits": 9, + "maxLocalDigits": 9 + }, + { + "name": "Yemen", + "a2": "YE", + "a3": "YEM", + "numCode": 887, + "dialingCode": "967", + "minLocalDigits": 9, + "maxLocalDigits": 9 + }, + { + "name": "Zambia", + "a2": "ZM", + "a3": "ZMB", + "numCode": 894, + "dialingCode": "260", + "minLocalDigits": 9, + "maxLocalDigits": 9 + }, + { + "name": "Zimbabwe", + "a2": "ZW", + "a3": "ZWE", + "numCode": 716, + "dialingCode": "263", + "minLocalDigits": 9, + "maxLocalDigits": 9 + } +] \ No newline at end of file From f000188e9429fcba2882bf63ae3683fe8ee5c2e1 Mon Sep 17 00:00:00 2001 From: cod3ddy Date: Tue, 2 Jun 2026 22:57:42 +0200 Subject: [PATCH 2/6] chore: parse embedded countries' data from json --- country.go | 63 ++++++++++++++++++++++++++++++++++++++++-------------- 1 file changed, 47 insertions(+), 16 deletions(-) diff --git a/country.go b/country.go index 44a4aa1..6f953a1 100644 --- a/country.go +++ b/country.go @@ -5,16 +5,18 @@ import ( "strings" ) -// Country represents a country with its dialing codes +// Country represents a country with its dialing codes and E.164 digit constraints. type Country struct { - Name string - A2 string - A3 string - NumCode int - DialingCode string + Name string `json:"name"` + A2 string `json:"a2"` + A3 string `json:"a3"` + NumCode int `json:"numCode"` + DialingCode string `json:"dialingCode"` + MinLocalDigits int `json:"minLocalDigits"` + MaxLocalDigits int `json:"maxLocalDigits"` } -// GetCountryByA2 returns a countyr by its A2 code +// GetCountryByA2 returns a country by its ISO 3166-1 alpha-2 code, or nil if not found. func (n *Normalizer) GetCountryByA2(a2 string) *Country { for i := range n.countries { if n.countries[i].A2 == a2 { @@ -25,26 +27,55 @@ func (n *Normalizer) GetCountryByA2(a2 string) *Country { return nil } -// GetCountryByCode returns a country by its dialing code +// GetCountryByCode returns a country by its dialing code, or nil if not found. func (n *Normalizer) GetCountryByCode(code string) *Country { return n.codeMap[code] } -// ValidatePhoneNumber validates if a phone number matches a specific country +// ValidatePhoneNumber validates that phone is a valid E.164 number for the given country. +// The number must include the country code prefix (with or without leading +). +// It checks E.164 total length (<= 15 digits) and the country's known subscriber digit range. func (n *Normalizer) ValidatePhoneNumber(phone, countryA2 string) error { country := n.GetCountryByA2(countryA2) if country == nil { - return fmt.Errorf("%w: country: %s ", ErrUnknownCountry, countryA2) + return fmt.Errorf("%w: %s", ErrUnknownCountry, countryA2) } - cleanPhone := cleanPhone(phone) - expectedCodes := splitDialingCodes(country.DialingCode) + cleaned := cleanPhone(phone) + if cleaned == "" { + return ErrInvalidPhoneNumber + } - for _, code := range expectedCodes { - if strings.HasPrefix(cleanPhone, fmt.Sprintf("+%s", code)) || strings.HasPrefix(cleanPhone, code) { - return nil + + var matchedCode string + for _, code := range splitDialingCodes(country.DialingCode) { + if strings.HasPrefix(cleaned, "+"+code) || strings.HasPrefix(cleaned, code) { + matchedCode = code + break } } - return fmt.Errorf("%w: country name: %s", ErrPhoneNumberAndCountryCodeMismatch, country.Name) + if matchedCode == "" { + return fmt.Errorf("%w: expected code for %s", ErrPhoneNumberAndCountryCodeMismatch, country.Name) + } + + + withoutPlus := strings.TrimPrefix(cleaned, "+") + subscriber := withoutPlus[len(matchedCode):] + + if len(matchedCode)+len(subscriber) > 15 { + return fmt.Errorf("%w: exceeds E.164 maximum of 15 digits", ErrPhoneNumberTooLong) + } + + if len(subscriber) < country.MinLocalDigits { + return fmt.Errorf("%w: %s requires at least %d local digits, got %d", + ErrInvalidPhoneNumber, country.Name, country.MinLocalDigits, len(subscriber)) + } + + if len(subscriber) > country.MaxLocalDigits { + return fmt.Errorf("%w: %s allows at most %d local digits, got %d", + ErrInvalidPhoneNumber, country.Name, country.MaxLocalDigits, len(subscriber)) + } + + return nil } From e0f77ce385216a83c3beea649165ca2b64bf16f6 Mon Sep 17 00:00:00 2001 From: cod3ddy Date: Tue, 2 Jun 2026 22:58:27 +0200 Subject: [PATCH 3/6] fix: updat the doc, sanja has now migrated and started adhering to the E.164 format --- doc.go | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/doc.go b/doc.go index 08d785d..aa5b421 100644 --- a/doc.go +++ b/doc.go @@ -1,18 +1,22 @@ -// Package sanja provides phone number normalization and formatting. +// Package sanja provides phone number normalization to E.164 format. // // Overview: -// Sanja helps normalize phone numbers to international format with -// country code handling. It supports 250+ countries and bulk processing. +// Sanja normalizes phone numbers to international E.164 format (+countryCodeSubscriber) +// with support for 250+ countries and bulk processing. // // Example: // -// norm := sanja.NewNormalizer("US") +// norm, err := sanja.NewNormalizer("US") +// if err != nil { +// log.Fatal(err) +// } // normalized, err := norm.Normalize("555-123-4567") // // Result: "+15551234567" // // Features: -// - Local to international number conversion +// - Local to international (E.164) conversion // - Bulk phone number processing -// - Country code detection -// - Comprehensive country data +// - Country code detection for 250+ countries +// - E.164 length validation (max 15 digits) +// - Per-country subscriber digit validation via ValidatePhoneNumber package sanja From a982e7acbcdae68fce6cd0a9d535390ac1969454 Mon Sep 17 00:00:00 2001 From: cod3ddy Date: Tue, 2 Jun 2026 23:10:32 +0200 Subject: [PATCH 4/6] chore: add an errror for when a phone number execeeds the max e.164 length --- errors.go | 1 + 1 file changed, 1 insertion(+) diff --git a/errors.go b/errors.go index 4d16fda..650df04 100644 --- a/errors.go +++ b/errors.go @@ -6,3 +6,4 @@ var ErrInvalidPhoneNumber = errors.New("invalid phone number") var ErrDefaultCountryNotFound = errors.New("no default country set") var ErrPhoneNumberAndCountryCodeMismatch = errors.New("phone number does not match country code") var ErrUnknownCountry = errors.New("invalid or unknown country") +var ErrPhoneNumberTooLong = errors.New("phone number exceeds E.164 maximum length") From 8cb9f4110babdb89604f9c2d58ac35e904d48f9d Mon Sep 17 00:00:00 2001 From: cod3ddy Date: Tue, 2 Jun 2026 23:12:03 +0200 Subject: [PATCH 5/6] fix: the normalizer now conforms to the e.164 standard --- normalizer.go | 104 +++++++++------ normalizer_test.go | 320 ++++++++++++++++++++++++--------------------- 2 files changed, 239 insertions(+), 185 deletions(-) diff --git a/normalizer.go b/normalizer.go index 277344a..f55f876 100644 --- a/normalizer.go +++ b/normalizer.go @@ -6,90 +6,120 @@ import ( ) const ( + // MinimumLocalDigitsLength is the minimum number of subscriber digits accepted. MinimumLocalDigitsLength int = 7 + // MaximumLocalDigitsLength is the E.164 maximum subscriber digits (15 total minus a 1-digit country code minimum). MaximumLocalDigitsLength int = 12 + // e164MaxDigits is the E.164 hard cap: country code + subscriber <= 15 digits. + e164MaxDigits int = 15 ) -// Normalizer hanldes phone numbder normalization +// Normalizer handles phone number normalization to E.164 format. type Normalizer struct { countries []Country defaultCountry *Country codeMap map[string]*Country } -// NewNormalizer creatres a new phone number normalizer -func NewNormalizer(defaultCountryA2 string) *Normalizer { - var n = Normalizer{ +// NewNormalizer creates a Normalizer with the given ISO 3166-1 alpha-2 code as the default country. +// Returns an error if the country code is not recognised. +func NewNormalizer(defaultCountryA2 string) (*Normalizer, error) { + n := &Normalizer{ countries: getCountries(), codeMap: make(map[string]*Country), } for i := range n.countries { country := &n.countries[i] - - codes := splitDialingCodes(country.DialingCode) - - for _, code := range codes { + for _, code := range splitDialingCodes(country.DialingCode) { n.codeMap[code] = country } } - // set default country n.defaultCountry = n.GetCountryByA2(defaultCountryA2) + if n.defaultCountry == nil { + return nil, fmt.Errorf("%w: %s", ErrUnknownCountry, defaultCountryA2) + } - return &n + return n, nil } -// Normalize adds the country code prefix to a phone number if missing +// Normalize returns phone in E.164 format (+countryCodeSubscriber). +// +// Resolution order: +// 1. Numbers already prefixed with "+" are returned as-is (with length validation). +// 2. Numbers that begin with the default country's dialing code (no "+" or leading zeros) +// receive a "+" prefix. +// 3. Everything else is treated as a local number: leading zeros are stripped and the +// default country code is prepended. +// +// Returns ErrInvalidPhoneNumber for numbers that are too short, +// and ErrPhoneNumberTooLong for numbers that exceed the E.164 15-digit cap. func (n *Normalizer) Normalize(phone string) (string, error) { - if n.defaultCountry == nil { - return phone, ErrDefaultCountryNotFound - } + cleaned := cleanPhone(phone) - phone = cleanPhone(phone) + if strings.HasPrefix(cleaned, "+") { + digits := cleaned[1:] + if len(digits) < MinimumLocalDigitsLength { + return "", ErrInvalidPhoneNumber + } + if len(digits) > e164MaxDigits { + return "", ErrPhoneNumberTooLong + } + return cleaned, nil + } - if phone == "" || len(phone) < MinimumLocalDigitsLength { + if cleaned == "" || len(cleaned) < MinimumLocalDigitsLength { return "", ErrInvalidPhoneNumber } - if strings.HasPrefix(phone, "+") { - return phone, nil + if n.hasCountryCode(cleaned) { + if len(cleaned) > e164MaxDigits { + return "", ErrPhoneNumberTooLong + } + return "+" + cleaned, nil } - // Check it already starts with a country code without + - if n.hasCountryCode(phone) { - return fmt.Sprintf("+%s", phone), nil + local := strings.TrimLeft(cleaned, "0") + if len(local) < MinimumLocalDigitsLength { + return "", ErrInvalidPhoneNumber + } + if len(local) > MaximumLocalDigitsLength { + return "", ErrPhoneNumberTooLong } - - phone = strings.TrimLeft(phone, "0") // removes leading zeros in local numbers defaultCode := splitDialingCodes(n.defaultCountry.DialingCode)[0] + result := "+" + defaultCode + local + if len(result)-1 > e164MaxDigits { + return "", ErrPhoneNumberTooLong + } - return fmt.Sprintf("+%s%s", defaultCode, phone), nil + return result, nil } -// NormalizeBulk normalizes multiple phone numbers +// NormalizeBulk normalizes a slice of phone numbers, returning a result and error per entry. func (n *Normalizer) NormalizeBulk(phones []string) ([]string, []error) { - var normalized = make([]string, len(phones)) - var errors = make([]error, len(phones)) + normalized := make([]string, len(phones)) + errs := make([]error, len(phones)) for i, phone := range phones { - normalized[i], errors[i] = n.Normalize(phone) + normalized[i], errs[i] = n.Normalize(phone) } - return normalized, errors + return normalized, errs } -// hasCountryCode checks if a phone number already starts with a valid country code +// hasCountryCode reports whether phone (no "+" prefix) begins with the default country's +// dialing code. the leading "0" is treated as a local dial prefix and returns false immediately, +// preventing false matches against country codes that share digits with local number prefixes. func (n *Normalizer) hasCountryCode(phone string) bool { - cleanPhone := strings.TrimLeft(phone, "0") - for i := 1; i <= 4 && i <= len(phone); i++ { - prefix := cleanPhone[:i] - - if country, exists := n.codeMap[prefix]; exists { - return country.A2 == n.defaultCountry.A2 + if strings.HasPrefix(phone, "0") { + return false + } + for _, code := range splitDialingCodes(n.defaultCountry.DialingCode) { + if strings.HasPrefix(phone, code) { + return true } } - return false } diff --git a/normalizer_test.go b/normalizer_test.go index c8bb74a..b95719e 100644 --- a/normalizer_test.go +++ b/normalizer_test.go @@ -4,109 +4,66 @@ import ( "testing" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) +func mustNormalizer(t *testing.T, a2 string) *Normalizer { + t.Helper() + n, err := NewNormalizer(a2) + require.NoError(t, err) + return n +} + +func TestNewNormalizer_InvalidCountry(t *testing.T) { + _, err := NewNormalizer("XX") + assert.ErrorIs(t, err, ErrUnknownCountry) +} + +func TestNewNormalizer_ValidCountry(t *testing.T) { + n, err := NewNormalizer("MW") + assert.NoError(t, err) + assert.NotNil(t, n) +} + func TestNormalize(t *testing.T) { - norm := NewNormalizer("MW") + norm := mustNormalizer(t, "MW") tests := []struct { - name string - input string - expected string - expectError bool - errorContains string + name string + input string + expected string + expectError bool + errorIs error }{ - { - name: "local number with leading zero", - input: "0886392814", - expected: "+265886392814", - }, - { - name: "local number without leading zero", - input: "886392814", - expected: "+265886392814", - }, - { - name: "already normalized with plus", - input: "+265886392814", - expected: "+265886392814", - }, - { - name: "number with country code without plus", - input: "265886392814", - expected: "+265886392814", - }, - { - name: "number with multiple leading zeros", - input: "000886392814", - expected: "+265886392814", - }, + {name: "local number with leading zero", input: "0886392814", expected: "+265886392814"}, + {name: "local number without leading zero", input: "886392814", expected: "+265886392814"}, + {name: "already normalized with plus", input: "+265886392814", expected: "+265886392814"}, + {name: "number with country code without plus", input: "265886392814", expected: "+265886392814"}, + {name: "number with multiple leading zeros", input: "000886392814", expected: "+265886392814"}, + {name: "number with spaces", input: "088 639 2814", expected: "+265886392814"}, + {name: "number with dashes", input: "088-639-2814", expected: "+265886392814"}, + {name: "number with parentheses", input: "(088)6392814", expected: "+265886392814"}, + {name: "number with mixed formatting", input: "+265 (88) 639-2814", expected: "+265886392814"}, - { - name: "number with spaces", - input: "088 639 2814", - expected: "+265886392814", - }, - { - name: "number with dashes", - input: "088-639-2814", - expected: "+265886392814", - }, - { - name: "number with parentheses", - input: "(088)6392814", - expected: "+265886392814", - }, - { - name: "number with mixed formatting", - input: "+265 (88) 639-2814", - expected: "+265886392814", - }, + {name: "number with letters stripped", input: "088-639A2814", expected: "+265886392814"}, - { - name: "US number with country code", - input: "+12025550123", - expected: "+12025550123", - }, - { - name: "UK number with country code", - input: "+442079460000", - expected: "+442079460000", - }, - { - name: "South Africa number", - input: "+27821234567", - expected: "+27821234567", - }, + {name: "US number with country code", input: "+12025550123", expected: "+12025550123"}, + {name: "UK number with country code", input: "+442079460000", expected: "+442079460000"}, + {name: "South Africa number", input: "+27821234567", expected: "+27821234567"}, - { - name: "empty string", - input: "", - expectError: true, - errorContains: "invalid phone number", - }, - { - name: "only special characters", - input: "+-() ", - expectError: true, - errorContains: "invalid phone number", - }, - { - name: "too short number", - input: "123", - expectError: true, - errorContains: "invalid phone number", - }, + {name: "empty string", input: "", expectError: true, errorIs: ErrInvalidPhoneNumber}, + {name: "only special characters", input: "+-() ", expectError: true, errorIs: ErrInvalidPhoneNumber}, + {name: "too short number", input: "123", expectError: true, errorIs: ErrInvalidPhoneNumber}, + {name: "exceeds E.164 max", input: "08863928149999999", expectError: true, errorIs: ErrPhoneNumberTooLong}, } for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { result, err := norm.Normalize(tc.input) - if tc.expectError { assert.Error(t, err) - if tc.errorContains != "" { - assert.Contains(t, err.Error(), tc.errorContains) + if tc.errorIs != nil { + assert.ErrorIs(t, err, tc.errorIs) } } else { assert.NoError(t, err) @@ -117,12 +74,11 @@ func TestNormalize(t *testing.T) { } func TestNormalizeBulk(t *testing.T) { - norm := NewNormalizer("MW") + norm := mustNormalizer(t, "MW") tests := []struct { name string input []string - expectedCount int errorCount int expectedOutput []string }{ @@ -137,8 +93,7 @@ func TestNormalizeBulk(t *testing.T) { "123", "+447911123456", }, - expectedCount: 7, - errorCount: 2, + errorCount: 2, expectedOutput: []string{ "+265886392814", "+265886392814", @@ -157,8 +112,7 @@ func TestNormalizeBulk(t *testing.T) { "+265886392814", "265886392814", }, - expectedCount: 4, - errorCount: 0, + errorCount: 0, expectedOutput: []string{ "+265886392814", "+265886392814", @@ -167,22 +121,14 @@ func TestNormalizeBulk(t *testing.T) { }, }, { - name: "all invalid numbers", - input: []string{ - "", - "abc", - "123", - }, - expectedCount: 3, - errorCount: 3, - expectedOutput: []string{ - "", "", "", - }, + name: "all invalid numbers", + input: []string{"", "abc", "123"}, + errorCount: 3, + expectedOutput: []string{"", "", ""}, }, { name: "empty slice", input: []string{}, - expectedCount: 0, errorCount: 0, expectedOutput: []string{}, }, @@ -190,23 +136,22 @@ func TestNormalizeBulk(t *testing.T) { for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { - normalizedNumbers, errors := norm.NormalizeBulk(tc.input) + results, errs := norm.NormalizeBulk(tc.input) - assert.Equal(t, len(tc.input), len(normalizedNumbers)) - assert.Equal(t, len(tc.input), len(errors)) + assert.Equal(t, len(tc.input), len(results)) + assert.Equal(t, len(tc.input), len(errs)) - actualErrorCount := 0 - for _, err := range errors { + actualErrors := 0 + for _, err := range errs { if err != nil { - actualErrorCount++ + actualErrors++ } } - - assert.Equal(t, tc.errorCount, actualErrorCount) + assert.Equal(t, tc.errorCount, actualErrors) for i, expected := range tc.expectedOutput { - if i < len(normalizedNumbers) { - assert.Equal(t, expected, normalizedNumbers[i]) + if i < len(results) { + assert.Equal(t, expected, results[i]) } } }) @@ -215,59 +160,138 @@ func TestNormalizeBulk(t *testing.T) { func TestNormalizeWithDifferentDefaultCountries(t *testing.T) { tests := []struct { - name string defaultCountry string input string expected string }{ + {defaultCountry: "US", input: "5550123456", expected: "+15550123456"}, + {defaultCountry: "GB", input: "2079460000", expected: "+442079460000"}, + {defaultCountry: "ZA", input: "821234567", expected: "+27821234567"}, + {defaultCountry: "KE", input: "712345678", expected: "+254712345678"}, + } + + for _, tc := range tests { + t.Run(tc.defaultCountry, func(t *testing.T) { + norm := mustNormalizer(t, tc.defaultCountry) + result, err := norm.Normalize(tc.input) + assert.NoError(t, err) + assert.Equal(t, tc.expected, result) + }) + } +} + +func TestGetCountryByA2(t *testing.T) { + norm := mustNormalizer(t, "MW") + + t.Run("found", func(t *testing.T) { + c := norm.GetCountryByA2("US") + require.NotNil(t, c) + assert.Equal(t, "United States", c.Name) + assert.Equal(t, "1", c.DialingCode) + }) + + t.Run("not found returns nil", func(t *testing.T) { + c := norm.GetCountryByA2("XX") + assert.Nil(t, c) + }) +} + +func TestGetCountryByCode(t *testing.T) { + norm := mustNormalizer(t, "MW") + + t.Run("found by single code", func(t *testing.T) { + c := norm.GetCountryByCode("265") + require.NotNil(t, c) + assert.Equal(t, "MW", c.A2) + }) + + t.Run("found by multi-code country", func(t *testing.T) { + c := norm.GetCountryByCode("1829") + require.NotNil(t, c) + assert.Equal(t, "DO", c.A2) + }) + + t.Run("not found returns nil", func(t *testing.T) { + c := norm.GetCountryByCode("9999") + assert.Nil(t, c) + }) +} + +func TestValidatePhoneNumber(t *testing.T) { + norm := mustNormalizer(t, "MW") + + tests := []struct { + name string + phone string + country string + wantErr error + }{ + { + name: "valid Malawi number", + phone: "+265886392814", + country: "MW", + }, + { + name: "valid US number", + phone: "+12025550123", + country: "US", + }, + { + name: "valid UK number", + phone: "+447911123456", + country: "GB", + }, + { + name: "wrong country code", + phone: "+27821234567", + country: "MW", + wantErr: ErrPhoneNumberAndCountryCodeMismatch, + }, + { + name: "unknown country", + phone: "+265886392814", + country: "XX", + wantErr: ErrUnknownCountry, + }, { - name: "US default country", - defaultCountry: "US", - input: "5550123", - expected: "+15550123", + name: "too few local digits for Malawi", + phone: "+26512345", + country: "MW", + wantErr: ErrInvalidPhoneNumber, }, { - name: "UK default country", - defaultCountry: "GB", - input: "2079460000", - expected: "+442079460000", + name: "exceeds E.164 max", + phone: "+2658863928149999", + country: "MW", + wantErr: ErrPhoneNumberTooLong, }, { - name: "ZA default country", - defaultCountry: "ZA", - input: "821234567", - expected: "+27821234567", + name: "valid Dominican Republic secondary code", + phone: "+18291234567", + country: "DO", }, { - name: "KE default country", - defaultCountry: "KE", - input: "712345678", - expected: "+254712345678", + name: "empty phone", + phone: "", + country: "MW", + wantErr: ErrInvalidPhoneNumber, }, } for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { - norm := NewNormalizer(tc.defaultCountry) - result, err := norm.Normalize(tc.input) - assert.NoError(t, err) - assert.Equal(t, tc.expected, result) + err := norm.ValidatePhoneNumber(tc.phone, tc.country) + if tc.wantErr != nil { + assert.ErrorIs(t, err, tc.wantErr) + } else { + assert.NoError(t, err) + } }) } } -func TestNormalizer_NoDefaultCountry(t *testing.T) { - - norm := NewNormalizer("INVALID") - - _, err := norm.Normalize("0886392814") - assert.Error(t, err) - assert.Contains(t, err.Error(), "no default country set") -} - -// Benchmark tests func BenchmarkNormalize(b *testing.B) { - norm := NewNormalizer("MW") + norm, _ := NewNormalizer("MW") phoneNumbers := []string{ "0886392814", "265886392814", @@ -283,7 +307,7 @@ func BenchmarkNormalize(b *testing.B) { } func BenchmarkNormalizeBulk(b *testing.B) { - norm := NewNormalizer("MW") + norm, _ := NewNormalizer("MW") phoneNumbers := []string{ "0886392814", "265886392814", "+265886392814", "886392814", "0999123456", "265999123456", "+265999123456", "999123456", From 6142642cbc3bb3a439d8b9331e3822b9960f5b39 Mon Sep 17 00:00:00 2001 From: cod3ddy Date: Tue, 2 Jun 2026 23:13:09 +0200 Subject: [PATCH 6/6] fix: remove typo, remove 'g' and replace with '9' --- utils.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/utils.go b/utils.go index a2dfc86..6f41ae6 100644 --- a/utils.go +++ b/utils.go @@ -6,7 +6,7 @@ import "strings" func cleanPhone(phone string) string { var result strings.Builder for _, r := range phone { - if r >= '0' && r <= 'g' || r == '+' { + if (r >= '0' && r <= '9') || r == '+' { result.WriteRune(r) } }