libphonenumber-js
Version:
A simpler (and smaller) rewrite of Google Android's popular libphonenumber library
647 lines (537 loc) • 21.9 kB
JavaScript
'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