UNPKG

libphonenumber-js

Version:

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

647 lines (537 loc) 21.9 kB
'use strict'; Object.defineProperty(exports, "__esModule", { value: true }); exports.VALID_PUNCTUATION = exports.VALID_DIGITS = exports.PLUS_CHARS = undefined; var _getIterator2 = require('babel-runtime/core-js/get-iterator'); var _getIterator3 = _interopRequireDefault(_getIterator2); var _extends2 = require('babel-runtime/helpers/extends'); var _extends3 = _interopRequireDefault(_extends2); exports.default = parse; exports.normalize = normalize; exports.replace_characters = replace_characters; exports.is_viable_phone_number = is_viable_phone_number; exports.extract_formatted_phone_number = extract_formatted_phone_number; exports.parse_phone_number = parse_phone_number; exports.parse_phone_number_and_country_phone_code = parse_phone_number_and_country_phone_code; exports.strip_national_prefix = strip_national_prefix; exports.find_country_code = find_country_code; var _common = require('./common'); var _metadata = require('./metadata'); var _format = require('./format'); var _getNumberType = require('./get number type'); var _getNumberType2 = _interopRequireDefault(_getNumberType); function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } // This is a port of Google Android `libphonenumber`'s // `phonenumberutil.js` of 17th November, 2016. // // https://github.com/googlei18n/libphonenumber/commits/master/javascript/i18n/phonenumbers/phonenumberutil.js var PLUS_CHARS = exports.PLUS_CHARS = '+\uFF0B'; // Digits accepted in phone numbers // (ascii, fullwidth, arabic-indic, and eastern arabic digits). var VALID_DIGITS = exports.VALID_DIGITS = '0-9\uFF10-\uFF19\u0660-\u0669\u06F0-\u06F9'; // `DASHES` will be right after the opening square bracket of the "character class" var DASHES = '-\u2010-\u2015\u2212\u30FC\uFF0D'; var SLASHES = '\uFF0F/'; var DOTS = '\uFF0E.'; var WHITESPACE = ' \xA0\xAD\u200B\u2060\u3000'; var BRACKETS = '()\uFF08\uFF09\uFF3B\uFF3D\\[\\]'; var TILDES = '~\u2053\u223C\uFF5E'; // Regular expression of acceptable punctuation found in phone numbers. This // excludes punctuation found as a leading character only. This consists of dash // characters, white space characters, full stops, slashes, square brackets, // parentheses and tildes. Full-width variants are also present. var VALID_PUNCTUATION = exports.VALID_PUNCTUATION = '' + DASHES + SLASHES + DOTS + WHITESPACE + BRACKETS + TILDES; // Regular expression of viable phone numbers. This is location independent. // Checks we have at least three leading digits, and only valid punctuation, // alpha characters and digits in the phone number. Does not include extension // data. The symbol 'x' is allowed here as valid punctuation since it is often // used as a placeholder for carrier codes, for example in Brazilian phone // numbers. We also allow multiple '+' characters at the start. // // Corresponds to the following: // [digits]{minLengthNsn}| // plus_sign* // (([punctuation]|[star])*[digits]){3,}([punctuation]|[star]|[digits]|[alpha])* // // The first reg-ex is to allow short numbers (two digits long) to be parsed if // they are entered as "15" etc, but only if there is no punctuation in them. // The second expression restricts the number of digits to three or more, but // then allows them to be in international form, and to have alpha-characters // and punctuation. We split up the two reg-exes here and combine them when // creating the reg-ex VALID_PHONE_NUMBER_PATTERN itself so we can prefix it // with ^ and append $ to each branch. // // Note VALID_PUNCTUATION starts with a -, so must be the first in the range. // (wtf did they mean by saying that; probably nothing) // var MIN_LENGTH_PHONE_NUMBER_PATTERN = '[' + VALID_DIGITS + ']{' + MIN_LENGTH_FOR_NSN + '}'; // // And this is the second reg-exp: // (see MIN_LENGTH_PHONE_NUMBER_PATTERN for a full description of this reg-exp) // var VALID_PHONE_NUMBER = '[' + PLUS_CHARS + ']{0,1}' + '(?:' + '[' + VALID_PUNCTUATION + ']*' + '[' + VALID_DIGITS + ']' + '){3,}' + '[' + VALID_PUNCTUATION + VALID_DIGITS + ']*'; // The combined regular expression for valid phone numbers: // var VALID_PHONE_NUMBER_PATTERN = new RegExp( // Either a short two-digit-only phone number '^' + MIN_LENGTH_PHONE_NUMBER_PATTERN + '$' + '|' + // Or a longer fully parsed phone number (min 3 characters) '^' + VALID_PHONE_NUMBER + // screw phone number extensions // '(?:' + EXTN_PATTERNS_FOR_PARSING + ')?' + '$', 'i'); // This consists of the plus symbol, digits, and arabic-indic digits. var PHONE_NUMBER_START_PATTERN = new RegExp('[' + PLUS_CHARS + VALID_DIGITS + ']'); // Regular expression of trailing characters that we want to remove. var AFTER_PHONE_NUMBER_END_PATTERN = new RegExp('[^' + VALID_DIGITS + ']+$'); var LEADING_PLUS_CHARS_PATTERN = new RegExp('^[' + PLUS_CHARS + ']+'); // These mappings map a character (key) to a specific digit that should // replace it for normalization purposes. Non-European digits that // may be used in phone numbers are mapped to a European equivalent. var DIGIT_MAPPINGS = { '0': '0', '1': '1', '2': '2', '3': '3', '4': '4', '5': '5', '6': '6', '7': '7', '8': '8', '9': '9', '\uFF10': '0', // Fullwidth digit 0 '\uFF11': '1', // Fullwidth digit 1 '\uFF12': '2', // Fullwidth digit 2 '\uFF13': '3', // Fullwidth digit 3 '\uFF14': '4', // Fullwidth digit 4 '\uFF15': '5', // Fullwidth digit 5 '\uFF16': '6', // Fullwidth digit 6 '\uFF17': '7', // Fullwidth digit 7 '\uFF18': '8', // Fullwidth digit 8 '\uFF19': '9', // Fullwidth digit 9 '\u0660': '0', // Arabic-indic digit 0 '\u0661': '1', // Arabic-indic digit 1 '\u0662': '2', // Arabic-indic digit 2 '\u0663': '3', // Arabic-indic digit 3 '\u0664': '4', // Arabic-indic digit 4 '\u0665': '5', // Arabic-indic digit 5 '\u0666': '6', // Arabic-indic digit 6 '\u0667': '7', // Arabic-indic digit 7 '\u0668': '8', // Arabic-indic digit 8 '\u0669': '9', // Arabic-indic digit 9 '\u06F0': '0', // Eastern-Arabic digit 0 '\u06F1': '1', // Eastern-Arabic digit 1 '\u06F2': '2', // Eastern-Arabic digit 2 '\u06F3': '3', // Eastern-Arabic digit 3 '\u06F4': '4', // Eastern-Arabic digit 4 '\u06F5': '5', // Eastern-Arabic digit 5 '\u06F6': '6', // Eastern-Arabic digit 6 '\u06F7': '7', // Eastern-Arabic digit 7 '\u06F8': '8', // Eastern-Arabic digit 8 '\u06F9': '9' // Eastern-Arabic digit 9 }; // The maximum length of the country calling code. var MAX_LENGTH_COUNTRY_CODE = 3; // The minimum length of the national significant number. var MIN_LENGTH_FOR_NSN = 2; // The ITU says the maximum length should be 15, // but one can find longer numbers in Germany. var MAX_LENGTH_FOR_NSN = 17; // We don't allow input strings for parsing to be longer than 250 chars. // This prevents malicious input from consuming CPU. var MAX_INPUT_STRING_LENGTH = 250; var default_options = { country: {} }; // `options`: // { // country: // { // restrict - (a two-letter country code) // the phone number must be in this country // // default - (a two-letter country code) // default country to use for phone number parsing and validation // (if no country code could be derived from the phone number) // } // } // // Returns `{ country, number }` // // Example use cases: // // ```js // parse('8 (800) 555-35-35', 'RU') // parse('8 (800) 555-35-35', 'RU', metadata) // parse('8 (800) 555-35-35', { country: { default: 'RU' } }) // parse('8 (800) 555-35-35', { country: { default: 'RU' } }, metadata) // parse('+7 800 555 35 35') // parse('+7 800 555 35 35', metadata) // ``` // function parse(first_argument, second_argument, third_argument) { var _sort_out_arguments = sort_out_arguments(first_argument, second_argument, third_argument), text = _sort_out_arguments.text, options = _sort_out_arguments.options, metadata = _sort_out_arguments.metadata; if (!options) { options = (0, _extends3.default)({}, default_options); } // Validate country codes // Validate `default` country if (options.country.default && !metadata.countries[options.country.default]) { throw new Error('Unknown country code: ' + options.country.default); } // Validate `restrict` country if (options.country.restrict && !metadata.countries[options.country.restrict]) { throw new Error('Unknown country code: ' + options.country.restrict); } // Parse the phone number var formatted_phone_number = extract_formatted_phone_number(text); // If the phone number is not viable, then abort. if (!is_viable_phone_number(formatted_phone_number)) { return {}; } var _parse_phone_number_a = parse_phone_number_and_country_phone_code(formatted_phone_number, metadata), country_phone_code = _parse_phone_number_a.country_phone_code, number = _parse_phone_number_a.number; // Maybe invalid country phone code encountered if (!number) { return {}; } var country = void 0; var country_metadata = void 0; // Whether the phone number is formatted as an international phone number var is_international = false; if (country_phone_code) { is_international = true; // Check country restriction if (options.country.restrict && country_phone_code !== (0, _metadata.get_phone_code)(metadata.countries[options.country.restrict])) { return {}; } // Formatting information for regions which share // a country calling code is contained by only one region // for performance reasons. For example, for NANPA region // ("North American Numbering Plan Administration", // which includes USA, Canada, Cayman Islands, Bahamas, etc) // it will be contained in the metadata for `US`. country_metadata = (0, _metadata.get_metadata_by_country_phone_code)(country_phone_code, metadata); // `country` will be set later, // because, for example, for NANPA countries // there are several countries corresponding // to the same `1` country phone code. // Therefore, to reliably determine the exact country, // national (significant) number should be parsed first. } else if (options.country.restrict || options.country.default) { country = options.country.restrict || options.country.default; country_metadata = metadata.countries[country]; number = normalize(text); } if (!country_metadata) { return {}; } var national_number = strip_national_prefix(number, country_metadata); var did_have_national_prefix = national_number !== number; // https://github.com/halt-hammerzeit/libphonenumber-js/issues/67 // if (!is_international && !did_have_national_prefix && // is_national_prefix_required(national_number, country_metadata)) // { // return {} // } // Sometimes there are several countries // corresponding to the same country phone code // (e.g. NANPA countries all having `1` country phone code). // Therefore, to reliably determine the exact country, // national (significant) number should have been parsed first. // if (!country) { // When `metadata.json` is generated, all "ambiguous" country phone codes // get their countries populated with the full set of // "phone number type" regular expressions. country = find_country_code(country_phone_code, national_number, metadata); // Just in case there appears to be a bug in Google's metadata // and the exact country could not be extracted from the phone number. /* istanbul ignore if */ if (!country) { return {}; } // Update metadata to be for this specific country country_metadata = metadata.countries[country]; } // Validate national (significant) number length. // // A sidenote: // // They say that sometimes national (significant) numbers // can be longer than `MAX_LENGTH_FOR_NSN` (e.g. in Germany). // https://github.com/googlei18n/libphonenumber/blob/7e1748645552da39c4e1ba731e47969d97bdb539/resources/phonenumber.proto#L36 // Such numbers will just be discarded. // if (national_number.length > MAX_LENGTH_FOR_NSN) { return {}; } // National number pattern is different for each country, // even for those ones which are part of the "NANPA" group. var national_number_rule = new RegExp((0, _metadata.get_national_number_pattern)(country_metadata)); // Check if national phone number pattern matches the number if (!(0, _common.matches_entirely)(national_number, national_number_rule)) { return {}; } return { country: country, phone: national_number }; } // Normalizes a string of characters representing a phone number. // This converts wide-ascii and arabic-indic numerals to European numerals, // and strips punctuation and alpha characters. function normalize(number) { return replace_characters(number, DIGIT_MAPPINGS); } // For any character not being part of `replacements` // it is removed from the phone number. function replace_characters(text, replacements) { var replaced = ''; var _iteratorNormalCompletion = true; var _didIteratorError = false; var _iteratorError = undefined; try { for (var _iterator = (0, _getIterator3.default)(text), _step; !(_iteratorNormalCompletion = (_step = _iterator.next()).done); _iteratorNormalCompletion = true) { var character = _step.value; var replacement = replacements[character.toUpperCase()]; if (replacement !== undefined) { replaced += replacement; } } } catch (err) { _didIteratorError = true; _iteratorError = err; } finally { try { if (!_iteratorNormalCompletion && _iterator.return) { _iterator.return(); } } finally { if (_didIteratorError) { throw _iteratorError; } } } return replaced; } // Checks to see if the string of characters could possibly be a phone number at // all. At the moment, checks to see that the string begins with at least 2 // digits, ignoring any punctuation commonly found in phone numbers. This method // does not require the number to be normalized in advance - but does assume // that leading non-number symbols have been removed, such as by the method // `extract_possible_number`. // function is_viable_phone_number(number) { return number.length >= MIN_LENGTH_FOR_NSN && (0, _common.matches_entirely)(number, VALID_PHONE_NUMBER_PATTERN); } function extract_formatted_phone_number(text) { if (!text || text.length > MAX_INPUT_STRING_LENGTH) { return ''; } // Attempt to extract a possible number from the string passed in var starts_at = text.search(PHONE_NUMBER_START_PATTERN); if (starts_at < 0) { return ''; } return text // Trim everything to the left of the phone number .slice(starts_at) // Remove trailing non-numerical characters .replace(AFTER_PHONE_NUMBER_END_PATTERN, ''); } // Parses a formatted phone number. function parse_phone_number(number) { if (!number) { return ''; } var is_international = LEADING_PLUS_CHARS_PATTERN.test(number); // Remove non-digits // (and strip the possible leading '+') number = normalize(number); if (is_international) { return '+' + number; } return number; } // Parses a formatted phone number // and returns `{ country_phone_code, number }` // where `number` is the national (significant) phone number. // // (aka `maybeExtractCountryPhoneCode`) // function parse_phone_number_and_country_phone_code(number, metadata) { number = parse_phone_number(number); if (!number) { return {}; } // If this is not an international phone number, // then don't extract country phone code. if (number[0] !== '+') { return { number: number }; } // Strip the leading '+' sign number = number.slice(1); // Fast abortion: country codes do not begin with a '0' if (number[0] === '0') { return {}; } // The thing with country phone codes // is that they are orthogonal to each other // i.e. there's no such country phone code A // for which country phone code B exists // where B starts with A. // Therefore, while scanning digits, // if a valid country code is found, // that means that it is the country code. // var i = 1; while (i <= MAX_LENGTH_COUNTRY_CODE && i <= number.length) { var country_phone_code = number.slice(0, i); if (metadata.country_phone_code_to_countries[country_phone_code]) { return { country_phone_code: country_phone_code, number: number.slice(i) }; } i++; } return {}; } // Strips any national prefix (such as 0, 1) present in the number provided function strip_national_prefix(number, country_metadata) { var national_prefix_for_parsing = (0, _metadata.get_national_prefix_for_parsing)(country_metadata); if (!number || !national_prefix_for_parsing) { return number; } // Attempt to parse the first digits as a national prefix var national_prefix_pattern = new RegExp('^(?:' + national_prefix_for_parsing + ')'); var national_prefix_matcher = national_prefix_pattern.exec(number); // If no national prefix is present in the phone number, // but if the national prefix is optional for this country, // then consider this phone number valid. // // Google's reference `libphonenumber` implementation // wouldn't recognize such phone numbers as valid, // but I think it would perfectly make sense // to consider such phone numbers as valid // because if a national phone number was originally // formatted without the national prefix // then it must be parseable back into the original national number. // In other words, `parse(format(number))` // must always be equal to `number`. // if (!national_prefix_matcher) { return number; } var national_significant_number = void 0; // `national_prefix_for_parsing` capturing groups // (used only for really messy cases: Argentina, Brazil, Mexico, Somalia) var any_groups_were_captured = national_prefix_matcher[national_prefix_matcher.length - 1]; var national_prefix_transform_rule = (0, _metadata.get_national_prefix_transform_rule)(country_metadata); // If the national number tranformation is needed then do it if (national_prefix_transform_rule && any_groups_were_captured) { national_significant_number = number.replace(national_prefix_pattern, national_prefix_transform_rule); } // Else, no transformation is necessary, // and just strip the national prefix. else { national_significant_number = number.slice(national_prefix_matcher[0].length); } // Verify the parsed national (significant) number for this country var national_number_rule = new RegExp((0, _metadata.get_national_number_pattern)(country_metadata)); // If the original number (before stripping national prefix) was viable, // and the resultant number is not, then prefer the original phone number. // This is because for some countries (e.g. Russia) the same digit could be both // a national prefix and a leading digit of a valid national phone number, // like `8` is the national prefix for Russia and both // `8 800 555 35 35` and `800 555 35 35` are valid numbers. if ((0, _common.matches_entirely)(number, national_number_rule) && !(0, _common.matches_entirely)(national_significant_number, national_number_rule)) { return number; } // Return the parsed national (significant) number return national_significant_number; } function find_country_code(country_phone_code, national_phone_number, metadata) { // Is always non-empty, because `country_phone_code` is always valid var possible_countries = metadata.country_phone_code_to_countries[country_phone_code]; // If there's just one country corresponding to the country code, // then just return it, without further phone number digits validation. if (possible_countries.length === 1) { return possible_countries[0]; } var _iteratorNormalCompletion2 = true; var _didIteratorError2 = false; var _iteratorError2 = undefined; try { for (var _iterator2 = (0, _getIterator3.default)(possible_countries), _step2; !(_iteratorNormalCompletion2 = (_step2 = _iterator2.next()).done); _iteratorNormalCompletion2 = true) { var country_code = _step2.value; var country = metadata.countries[country_code]; // Leading digits check would be the simplest one if ((0, _metadata.get_leading_digits)(country)) { if (national_phone_number && national_phone_number.search((0, _metadata.get_leading_digits)(country)) === 0) { return country_code; } } // Else perform full validation with all of those bulky // fixed-line/mobile/etc regular expressions. else if ((0, _getNumberType2.default)({ phone: national_phone_number, country: country_code }, metadata)) { return country_code; } } } catch (err) { _didIteratorError2 = true; _iteratorError2 = err; } finally { try { if (!_iteratorNormalCompletion2 && _iterator2.return) { _iterator2.return(); } } finally { if (_didIteratorError2) { throw _iteratorError2; } } } } // export function is_national_prefix_required(national_number, country_metadata) // { // const format = choose_format_for_number(get_formats(country_metadata), national_number) // // if (format) // { // return get_format_national_prefix_is_mandatory_when_formatting(format, country_metadata) // } // } // Sort out arguments function sort_out_arguments(first_argument, second_argument, third_argument) { var text = void 0; var options = void 0; var metadata = void 0; if (typeof first_argument === 'string') { text = first_argument; } // Covert `resrict` country to an `options` object if (typeof second_argument === 'string') { var restrict_to_country = second_argument; options = (0, _extends3.default)({}, default_options, { country: { restrict: restrict_to_country } }); metadata = third_argument; } else { // Differentiate `metadata` from `options` if (second_argument && second_argument.countries) { metadata = second_argument; } else { options = second_argument; metadata = third_argument; } } // Sanity check if (!metadata) { throw new Error('Metadata not passed'); } return { text: text, options: options, metadata: metadata }; } //# sourceMappingURL=parse.js.map