UNPKG

libphonenumber-js

Version:

A simpler (and smaller) rewrite of Google Android's popular libphonenumber library

530 lines (471 loc) 20.8 kB
import _Object$keys from 'babel-runtime/core-js/object/keys'; import _getIterator from 'babel-runtime/core-js/get-iterator'; import { parseString } from 'xml2js'; import Promise from 'bluebird'; import { DIGIT_PLACEHOLDER } from '../as you type'; var phone_number_types = ['premium_rate', 'toll_free', 'shared_cost', 'voip', 'personal_number', 'pager', 'uan', 'voice_mail', 'fixed_line', 'mobile']; // Excessive fields from "PhoneNumberMetadata.xml" // aren't included to reduce code complexity and size: // // * `<references>` — a link to ITU (International Telecommunication Union) // document describing phone numbering plan for a country // // * `<noInternationalDialling>` — who needs to input non-internationally-dialable phones // // * `<areaCodeOptional>` — we aren't in the XXth century, // it's a globalized world, so write your // phone numbers with area codes. // // * `<fixedLine>`, `<mobile>`, `<pager>`, // `<tollFree>`, `<premiumRate>`, // `<sharedCost>`, `<personalNumber>`, // `<voip>`, `<uan>`, `<voicemail>` — who needs that in the XXIst century. // just go mobile and stop talking nonsense. // // * `internationalPrefix`, // `preferredInternationalPrefix` — who needs to parse (or format) those weird // "internationally dialed" phone numbers // like "011 ..." in the USA. // this isn't XXth century, just use mobile phones. // // * `preferredExtnPrefix` — screw phone number extensions // // * `leadingZeroPossible` — (aka "italian leading zero") // who needs to parse a phone number into an integer. // just keep it as a string. // // * `carrierCodeFormattingRule` — only used in Brazil and Colombia // when dialing from within those countries // from mobile phones to fixed line phone numbers. // i guess brazilians and colombians // already know when to add those carrier codes // by themselves (and when not to add them) // // * `mobileNumberPortableRegion` — is only used to disable phone number type detection // // * `<possibleLengths>` — is a redundant field to speed up testing of // whether a phone number format can be used to format // a particular national (significant) phone number. // // `libphonenumber/BuildMetadataFromXml.java` was used as a reference. // https://github.com/googlei18n/libphonenumber/blob/master/tools/java/common/src/com/google/i18n/phonenumbers/BuildMetadataFromXml.java // // There are three Xml metadata files in Google's `libphonenumber`: // // * PhoneNumberMetadata.xml — core data, used both for parse/format and "as you type" // // * PhoneNumberAlternateFormats.xml — alternative phone number formats. // is presumably used for parsing phone numbers // written in "alternative" formats. // is not used by "as you type" // presumably because of formats ambiguity // when combined with the core data. // this metadata is not used in this library // as there's no clear description on what to do with it // and how it works in the original `libphonenumber` code. // // * ShortNumberMetadata.xml — emergency numbers, etc. not used in this library. // export default function (input, included_countries, extended) { return Promise.promisify(parseString)(input).then(function (xml) { // https://github.com/googlei18n/libphonenumber/blob/master/resources/PhoneNumberMetadata.xml // https://github.com/googlei18n/libphonenumber/blob/master/resources/phonemetadata.proto // https://github.com/googlei18n/libphonenumber/blob/master/javascript/i18n/phonenumbers/phonenumberutil.js // https://github.com/googlei18n/libphonenumber/blob/master/javascript/i18n/phonenumbers/asyoutypeformatter.js var country_phone_code_to_countries = {}; var countries = {}; var _iteratorNormalCompletion = true; var _didIteratorError = false; var _iteratorError = undefined; try { var _loop = function _loop() { var territory = _step.value; // A two-letter country code var country_code = territory.$.id; // Skip this country if it has not been explicitly included if (included_countries && !included_countries.has(country_code)) { return 'continue'; } // Country metadata var country = { // Phone code related fields: // Phone code for phone numbers in this country. // // E.g. `1` for both USA and Canada. // phone_code: territory.$.countryCode, // In case of several countries // having the same country phone code, // these leading digits are the means // of classifying an international phone number // whether it belongs to a certain country. // // E.g. for Antigua and Barbuda // country phone code is `1` (same as USA) // and leading digits are `268`. // leading_digits: territory.$.leadingDigits, // The regular expression of all possible // national (significant) numbers for this country. national_number_pattern: territory.generalDesc[0].nationalNumberPattern[0].replace(/\s/g, ''), // National prefix related fields: // aka "trunk code". // This is the prefix prepended to a // national (significant) phone number // when dialed from within the country. // E.g. `0` for UK. national_prefix: territory.$.nationalPrefix, // In some (many) countries the national prefix // is not just a constant digit (like `0` in UK) // but can be different depending on the phone number // (and can be also absent for some phone numbers). // // So `national_prefix_for_parsing` is used when parsing // a national-prefixed (local) phone number // into a national significant phone number // extracting that possible national prefix out of it. // national_prefix_for_parsing: territory.$.nationalPrefixForParsing ? territory.$.nationalPrefixForParsing.replace(/\s/g, '') : undefined, // If `national_prefix_for_parsing` regular expression // contains "captured groups", then `national_prefix_transform_rule` // defines how the national-prefixed (local) phone number is // parsed into a national significant phone number. // // Pseudocode: // // national_prefix_pattern = regular_expression('^(?:' + national_prefix_for_parsing + ')') // national_significant_number = all_digits.replace(national_prefix_pattern, national_prefix_transform_rule) // // E.g. if a country's national numbers are 6-digit // and national prefix is always `0`, // then `national_prefix_for_parsing` could be `0(\d{6})` // and the corresponding `national_prefix_transform_rule` would be `$1` // (which is the default behaviour). // // Currently this feature is only used in // Argentina, Brazil, Mexico and San Marino // due to their messy telephone numbering plans. // // For example, mobile numbers in Argentina are written in two completely // different ways when dialed in-country and out-of-country // (e.g. 0343 15 555 1212 is exactly the same number as +54 9 343 555 1212). // Therefore for Argentina `national_prefix_transform_rule` is `9$1`. // national_prefix_transform_rule: territory.$.nationalPrefixTransformRule, // Controls how national prefix is written // in a formatted local phone number. // // E.g. in Armenia national prefix is `0` // and `national_prefix_formatting_rule` is `($NP$FG)` // which means that a national significant phone number `xxxxxxxx` // matching phone number pattern `(\d{2})(\d{6})` with format `$1 $2` // is written as a local phone number `(0xx) xxxxxx`. // national_prefix_formatting_rule: national_prefix_formatting_rule(territory.$.nationalPrefixFormattingRule, territory.$.nationalPrefix), // Is it possible that a national (significant) // phone number has leading zeroes? // // E.g. in Gabon some numbers start with a `0` // while the national prefix is also `0` // which is optional for mobile numbers. // national_prefix_is_optional_when_formatting: territory.$.nationalPrefixOptionalWhenFormatting ? Boolean(territory.$.nationalPrefixOptionalWhenFormatting) : undefined, // I suppose carrier codes can be omitted. // They are required only for Brazil and Columbia, // and only when calling to fixed line numbers // from mobile phones within those countries. // I guess people living in those countries // would know that they need to add carrier codes. // Other people don't need to know that. // Anyway, if someone sends a Pull Request // implementing carrier codes as Google's `libphonenumber` does // then such Pull Request will likely be merged. // // // In some countries carrier code is required // // to dial certain phone numbers. // // // // E.g. in Colombia calling to fixed line numbers // // from mobile phones requires a carrier code when called within Colombia. // // Or, for example, Brazilian fixed line and mobile numbers // // need to be dialed with a carrier code when called within Brazil. // // Without that, most of the carriers won't connect the call. // // These are the only two cases when "carrier codes" are required. // // // carrier_code_formatting_rule: territory.$.carrierCodeFormattingRule, // These `types` will be purged later, // if they're not needed (which is most likely). // See `country_phone_code_to_countries` ambiguity for more info. // types: { fixed_line: phone_type_pattern(territory, 'fixedLine'), mobile: phone_type_pattern(territory, 'mobile'), toll_free: phone_type_pattern(territory, 'tollFree'), premium_rate: phone_type_pattern(territory, 'premiumRate'), personal_number: phone_type_pattern(territory, 'personalNumber'), voice_mail: phone_type_pattern(territory, 'voicemail'), uan: phone_type_pattern(territory, 'uan'), pager: phone_type_pattern(territory, 'pager'), voip: phone_type_pattern(territory, 'voip'), shared_cost: phone_type_pattern(territory, 'sharedCost') } }; // Check that national (significant) phone number pattern // is set for this country (no "default" value here) if (!country.national_number_pattern) { throw new Error('"generalDesc.nationalNumberPattern" is missing for country ' + country_code + ' metadata'); } // Some countries don't have `availableFormats` specified, // because those formats are inherited from the "main country for region". if (territory.availableFormats) { country.formats = territory.availableFormats[0].numberFormat.map(function (number_format) { return { pattern: number_format.$.pattern, leading_digits_patterns: number_format.leadingDigits ? number_format.leadingDigits.map(function (leading_digits) { return leading_digits.replace(/\s/g, ''); }) : undefined, national_prefix_formatting_rule: national_prefix_formatting_rule(number_format.$.nationalPrefixFormattingRule, territory.$.nationalPrefix), national_prefix_is_optional_when_formatting: number_format.$.nationalPrefixOptionalWhenFormatting, format: number_format.format[0], international_format: number_format.intlFormat ? number_format.intlFormat[0] : undefined }; }) // Screw local-only formats .filter(function (format) { return format.international_format !== 'NA'; }); // Sanity check (using no "default" for this field) var _iteratorNormalCompletion4 = true; var _didIteratorError4 = false; var _iteratorError4 = undefined; try { for (var _iterator4 = _getIterator(country.formats), _step4; !(_iteratorNormalCompletion4 = (_step4 = _iterator4.next()).done); _iteratorNormalCompletion4 = true) { var format = _step4.value; // Never happens if (!format.format) { throw new Error('No phone number format "format" supplied for pattern ' + format.pattern + ' for ' + country_code); } // Never happens if (format.format.indexOf(DIGIT_PLACEHOLDER) >= 0) { throw new Error('Phone number format "' + format.format + '" contains a reserved "' + DIGIT_PLACEHOLDER + '" symbol for pattern ' + format.pattern + ' for ' + country_code); } } } catch (err) { _didIteratorError4 = true; _iteratorError4 = err; } finally { try { if (!_iteratorNormalCompletion4 && _iterator4.return) { _iterator4.return(); } } finally { if (_didIteratorError4) { throw _iteratorError4; } } } } // Add this country's metadata // to the metadata map. countries[country_code] = country; // Register this country's "country phone code" if (!country_phone_code_to_countries[country.phone_code]) { country_phone_code_to_countries[country.phone_code] = []; } // In case of several countries // having the same country phone code. // // E.g. for USA and Canada, USA is the // "main country for phone code 1". // // (maybe this field is not used at all // in which case this field is to be removed) // if (territory.$.mainCountryForCode === "true") { country_phone_code_to_countries[country.phone_code].unshift(country_code); } else { country_phone_code_to_countries[country.phone_code].push(country_code); } }; for (var _iterator = _getIterator(xml.phoneNumberMetadata.territories[0].territory), _step; !(_iteratorNormalCompletion = (_step = _iterator.next()).done); _iteratorNormalCompletion = true) { var _ret = _loop(); if (_ret === 'continue') continue; } // Some countries don't have `availableFormats` specified, // because those formats are meant to be copied // from the "main country for region". } catch (err) { _didIteratorError = true; _iteratorError = err; } finally { try { if (!_iteratorNormalCompletion && _iterator.return) { _iterator.return(); } } finally { if (_didIteratorError) { throw _iteratorError; } } } var _iteratorNormalCompletion2 = true; var _didIteratorError2 = false; var _iteratorError2 = undefined; try { for (var _iterator2 = _getIterator(_Object$keys(countries)), _step2; !(_iteratorNormalCompletion2 = (_step2 = _iterator2.next()).done); _iteratorNormalCompletion2 = true) { var _country_code = _step2.value; var _country = countries[_country_code]; var main_country_for_region_code = country_phone_code_to_countries[_country.phone_code][0]; var main_country_for_region = countries[main_country_for_region_code]; _country.formats = main_country_for_region.formats; // Some countries like Saint Helena and Falkland Islands // ('AC', 'FK', 'KI', 'NU', 'SH', 'TA', ...) // don't have any phone number formats // and phone numbers are formatted as a block in those countries. if (!_country.formats) { _country.formats = []; } } // Turns out that `<generalDesc><nationalNumberPattern/></generalDesc>` // is not preemptive at all: it's too unspecific for the cases // when several countries correspond to the same country phone code // (e.g. NANPA: US, Canada, etc — all correspond to the same `1` country phone code). // For these cases all those bulky `<fixedLine/>`, `<mobile/>`, etc // patterns are required. Therefore retain them for these rare cases. // // This inncreases metadata size by 5 KiloBytes. // } catch (err) { _didIteratorError2 = true; _iteratorError2 = err; } finally { try { if (!_iteratorNormalCompletion2 && _iterator2.return) { _iterator2.return(); } } finally { if (_didIteratorError2) { throw _iteratorError2; } } } var _iteratorNormalCompletion3 = true; var _didIteratorError3 = false; var _iteratorError3 = undefined; try { for (var _iterator3 = _getIterator(_Object$keys(country_phone_code_to_countries)), _step3; !(_iteratorNormalCompletion3 = (_step3 = _iterator3.next()).done); _iteratorNormalCompletion3 = true) { var country_phone_code = _step3.value; var country_codes = country_phone_code_to_countries[country_phone_code]; // Purge `types` regular expressions (they are huge) // when they're not needed for resolving country phone code // to country phone number matching. // E.g. when there's a one-to-one correspondence // between a country phone code and a country code if (!extended) { if (country_codes.length === 1) { delete countries[country_codes[0]].types; continue; } } var _iteratorNormalCompletion5 = true; var _didIteratorError5 = false; var _iteratorError5 = undefined; try { var _loop2 = function _loop2() { var country_code = _step5.value; // Leading digits for a country are sufficient // to resolve country phone code ambiguity. if (!extended) { if (countries[country_code].leading_digits) { delete countries[country_code].types; return 'continue'; } } var types = countries[country_code].types; // Find duplicate regular expressions for types // and just discard such duplicate types // to reduce metadata size (by 5 KiloBytes). var _iteratorNormalCompletion6 = true; var _didIteratorError6 = false; var _iteratorError6 = undefined; try { var _loop3 = function _loop3() { var type = _step6.value; if (!types[type]) { return 'continue'; } // Remove redundant types // (other types having the same regular expressions as this one) phone_number_types.filter(function (key) { return key !== type && types[key] === types[type]; }).forEach(function (key) { return delete types[key]; }); }; for (var _iterator6 = _getIterator(phone_number_types), _step6; !(_iteratorNormalCompletion6 = (_step6 = _iterator6.next()).done); _iteratorNormalCompletion6 = true) { var _ret3 = _loop3(); if (_ret3 === 'continue') continue; } } catch (err) { _didIteratorError6 = true; _iteratorError6 = err; } finally { try { if (!_iteratorNormalCompletion6 && _iterator6.return) { _iterator6.return(); } } finally { if (_didIteratorError6) { throw _iteratorError6; } } } }; for (var _iterator5 = _getIterator(country_codes), _step5; !(_iteratorNormalCompletion5 = (_step5 = _iterator5.next()).done); _iteratorNormalCompletion5 = true) { var _ret2 = _loop2(); if (_ret2 === 'continue') continue; } } catch (err) { _didIteratorError5 = true; _iteratorError5 = err; } finally { try { if (!_iteratorNormalCompletion5 && _iterator5.return) { _iterator5.return(); } } finally { if (_didIteratorError5) { throw _iteratorError5; } } } } } catch (err) { _didIteratorError3 = true; _iteratorError3 = err; } finally { try { if (!_iteratorNormalCompletion3 && _iterator3.return) { _iterator3.return(); } } finally { if (_didIteratorError3) { throw _iteratorError3; } } } return { countries: countries, country_phone_code_to_countries: country_phone_code_to_countries }; }); } // Replaces $NP with national prefix and $FG with the first group ($1) function national_prefix_formatting_rule(rule, national_prefix) { if (!rule) { return; } // Replace $NP with national prefix and $FG with the first group ($1) return rule.replace('$NP', national_prefix).replace('$FG', '$1'); } // Gets phone type pattern function phone_type_pattern(territory, type) { return territory[type] ? territory[type][0].nationalNumberPattern[0].replace(/\s/g, '') : undefined; } //# sourceMappingURL=generate.js.map