UNPKG

libphonenumber-js

Version:

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

838 lines (690 loc) 28.6 kB
import _getIterator from 'babel-runtime/core-js/get-iterator'; import _classCallCheck from 'babel-runtime/helpers/classCallCheck'; import _createClass from 'babel-runtime/helpers/createClass'; // This is an enhanced port of Google Android `libphonenumber`'s // `asyoutypeformatter.js` of 17th November, 2016. // // https://github.com/googlei18n/libphonenumber/blob/8d21a365061de2ba0675c878a710a7b24f74d2ae/javascript/i18n/phonenumbers/asyoutypeformatter.js import { get_phone_code, get_national_prefix, get_national_prefix_for_parsing, get_formats, get_format_pattern, get_format_format as _get_format_format, get_format_international_format, get_format_national_prefix_formatting_rule, get_format_national_prefix_is_mandatory_when_formatting, get_format_leading_digits_patterns, get_metadata_by_country_phone_code } from './metadata'; import { VALID_PUNCTUATION, PLUS_SIGN, PLUS_CHARS, VALID_DIGITS, extract_formatted_phone_number, parse_phone_number, parse_phone_number_and_country_phone_code, find_country_code, strip_national_prefix } from './parse'; import { FIRST_GROUP_PATTERN, format_national_number_using_format, local_to_international_style } from './format'; import { matches_entirely } from './common'; // Used in phone number format template creation. // Could be any digit, I guess. var DUMMY_DIGIT = '9'; var DUMMY_DIGIT_MATCHER = new RegExp(DUMMY_DIGIT, 'g'); // I don't know why is it exactly `15` var LONGEST_NATIONAL_PHONE_NUMBER_LENGTH = 15; // Create a phone number consisting only of the digit 9 that matches the // `number_pattern` by applying the pattern to the "longest phone number" string. var LONGEST_DUMMY_PHONE_NUMBER = repeat(DUMMY_DIGIT, LONGEST_NATIONAL_PHONE_NUMBER_LENGTH); // The digits that have not been entered yet will be represented by a \u2008, // the punctuation space. export var DIGIT_PLACEHOLDER = 'x'; // '\u2008' (punctuation space) var DIGIT_PLACEHOLDER_MATCHER = new RegExp(DIGIT_PLACEHOLDER); var DIGIT_PLACEHOLDER_MATCHER_GLOBAL = new RegExp(DIGIT_PLACEHOLDER, 'g'); // A pattern that is used to match character classes in regular expressions. // An example of a character class is "[1-4]". var CHARACTER_CLASS_PATTERN = /\[([^\[\]])*\]/g; // Any digit in a regular expression that actually denotes a digit. For // example, in the regular expression "80[0-2]\d{6,10}", the first 2 digits // (8 and 0) are standalone digits, but the rest are not. // Two look-aheads are needed because the number following \\d could be a // two-digit number, since the phone number can be as long as 15 digits. var STANDALONE_DIGIT_PATTERN = /\d(?=[^,}][^,}])/g; // A pattern that is used to determine if a `format` is eligible // to be used by the "as you type formatter". // It is eligible when the `format` contains groups of the dollar sign // followed by a single digit, separated by valid phone number punctuation. // This prevents invalid punctuation (such as the star sign in Israeli star numbers) // getting into the output of the "as you type formatter". var ELIGIBLE_FORMAT_PATTERN = new RegExp('^' + '[' + VALID_PUNCTUATION + ']*' + '(\\$\\d[' + VALID_PUNCTUATION + ']*)+' + '$'); // This is the minimum length of the leading digits of a phone number // to guarantee the first "leading digits pattern" for a phone number format // to be preemptive. var MIN_LEADING_DIGITS_LENGTH = 3; var VALID_INCOMPLETE_PHONE_NUMBER = '[' + PLUS_CHARS + ']{0,1}' + '[' + VALID_PUNCTUATION + VALID_DIGITS + ']*'; var VALID_INCOMPLETE_PHONE_NUMBER_PATTERN = new RegExp('^' + VALID_INCOMPLETE_PHONE_NUMBER + '$', 'i'); var as_you_type = function () { function as_you_type(country_code, metadata) { _classCallCheck(this, as_you_type); // Sanity check if (!metadata) { throw new Error('Metadata not passed'); } if (country_code && metadata.countries[country_code]) { this.default_country = country_code; } this.metadata = metadata; this.reset(); } _createClass(as_you_type, [{ key: 'input', value: function input(text) { // Parse input var extracted_number = extract_formatted_phone_number(text); // Special case for a lone '+' sign // since it's not considered a possible phone number. if (!extracted_number) { if (text && text.indexOf('+') >= 0) { extracted_number = '+'; } } // Validate possible first part of a phone number if (!matches_entirely(extracted_number, VALID_INCOMPLETE_PHONE_NUMBER_PATTERN)) { return this.current_output; } return this.process_input(parse_phone_number(extracted_number)); } }, { key: 'process_input', value: function process_input(input) { // If an out of position '+' sign detected // (or a second '+' sign), // then just drop it from the input. if (input[0] === '+') { if (!this.parsed_input) { this.parsed_input += '+'; // If a default country was set // then reset it because an explicitly international // phone number is being entered this.reset_countriness(); } input = input.slice(1); } // Raw phone number this.parsed_input += input; // // Reset phone number validation state // this.valid = false // Add digits to the national number this.national_number += input; // Try to format the parsed input if (this.is_international()) { if (!this.country_phone_code) { // If one looks at country phone codes // then he can notice that no one country phone code // is ever a (leftmost) substring of another country phone code. // So if a valid country code is extracted so far // then it means that this is the country code. // If no country phone code could be extracted so far, // then just return the raw phone number, // because it has no way of knowing // how to format the phone number so far. if (!this.extract_country_phone_code()) { // Return raw phone number return this.parsed_input; } // Initialize country-specific data this.initialize_phone_number_formats_for_this_country_phone_code(); this.reset_format(); this.determine_the_country(); } // `this.country` could be `undefined`, // for instance, when there is ambiguity // in a form of several different countries // each corresponding to the same country phone code // (e.g. NANPA: USA, Canada, etc), // and there's not enough digits entered // to reliably determine the country // the phone number belongs to. // Therefore, in cases of such ambiguity, // each time something is input, // try to determine the country // (if it's not determined yet). else if (!this.country) { this.determine_the_country(); } } else { // Some national prefixes are substrings of other national prefixes // (for the same country), therefore try to extract national prefix each time // because a longer national prefix might be available at some point in time. var previous_national_prefix = this.national_prefix; this.national_number = this.national_prefix + this.national_number; // Possibly extract a national prefix this.extract_national_prefix(); if (this.national_prefix !== previous_national_prefix) { // National number has changed // (due to another national prefix been extracted) // therefore national number has changed // therefore reset all previous formatting data. // (and leading digits matching state) this.matching_formats = this.available_formats; this.reset_format(); } } // Check the available phone number formats // based on the currently available leading digits. this.match_formats_by_leading_digits(); // Format the phone number (given the next digits) var formatted_national_phone_number = this.format_national_phone_number(input); // If the phone number could be formatted, // then return it, possibly prepending with country phone code // (for international phone numbers only) if (formatted_national_phone_number) { return this.full_phone_number(formatted_national_phone_number); } // If the phone number couldn't be formatted, // then just fall back to the raw phone number. return this.parsed_input; } }, { key: 'format_national_phone_number', value: function format_national_phone_number(next_digits) { // Format the next phone number digits // using the previously chosen phone number format. // // This is done here because if `attempt_to_format_complete_phone_number` // was placed before this call then the `template` // wouldn't reflect the situation correctly (and would therefore be inconsistent) // var national_number_formatted_with_previous_format = void 0; if (this.chosen_format) { national_number_formatted_with_previous_format = this.format_next_national_number_digits(next_digits); } // See if the input digits can be formatted properly already. If not, // use the results from format_next_national_number_digits(), which does formatting // based on the formatting pattern chosen. var formatted_number = this.attempt_to_format_complete_phone_number(); // Just because a phone number doesn't have a suitable format // that doesn't mean that the phone is invalid // because phone number formats only format phone numbers, // they don't validate them and some (rare) phone numbers // are meant to stay non-formatted. if (formatted_number) { // if (this.country) // { // this.valid = true // } return formatted_number; } // For some phone number formats national prefix // If the previously chosen phone number format // didn't match the next (current) digit being input // (leading digits pattern didn't match). if (this.choose_another_format()) { // And a more appropriate phone number format // has been chosen for these `leading digits`, // then format the national phone number (so far) // using the newly selected phone number pattern. // Will return `undefined` if it couldn't format // the supplied national number // using the selected phone number pattern. return this.reformat_national_number(); } // If could format the next (current) digit // using the previously chosen phone number format // then return the formatted number so far. // If no new phone number format could be chosen, // and couldn't format the supplied national number // using the selected phone number pattern, // then it will return `undefined`. return national_number_formatted_with_previous_format; } }, { key: 'reset', value: function reset() { // Input stripped of non-phone-number characters. // Can only contain a possible leading '+' sign and digits. this.parsed_input = ''; this.current_output = ''; // This contains the national prefix that has been extracted. It contains only // digits without formatting. this.national_prefix = ''; this.national_number = ''; this.reset_countriness(); this.reset_format(); // this.valid = false return this; } }, { key: 'reset_country', value: function reset_country() { if (this.default_country && !this.is_international()) { this.country = this.default_country; } else { this.country = undefined; } } }, { key: 'reset_countriness', value: function reset_countriness() { this.reset_country(); if (this.default_country && !this.is_international()) { this.country_metadata = this.metadata.countries[this.default_country]; this.country_phone_code = this.country_metadata.phone_code; this.initialize_phone_number_formats_for_this_country_phone_code(); } else { this.country_metadata = undefined; this.country_phone_code = undefined; this.available_formats = []; this.matching_formats = this.available_formats; } } }, { key: 'reset_format', value: function reset_format() { this.chosen_format = undefined; this.template = undefined; this.partially_populated_template = undefined; this.last_match_position = -1; } // Format each digit of national phone number (so far) // using the newly selected phone number pattern. }, { key: 'reformat_national_number', value: function reformat_national_number() { // Format each digit of national phone number (so far) // using the selected phone number pattern. return this.format_next_national_number_digits(this.national_number); } }, { key: 'initialize_phone_number_formats_for_this_country_phone_code', value: function initialize_phone_number_formats_for_this_country_phone_code() { // Get all "eligible" phone number formats for this country this.available_formats = get_formats(this.country_metadata).filter(function (format) { return ELIGIBLE_FORMAT_PATTERN.test(get_format_international_format(format)); }); this.matching_formats = this.available_formats; } }, { key: 'match_formats_by_leading_digits', value: function match_formats_by_leading_digits() { var leading_digits = this.national_number; // "leading digits" patterns start with a maximum 3 digits, // and then with each additional digit // a more precise "leading digits" pattern is specified. // They could make "leading digits" patterns start // with a maximum of a single digit, but they didn't, // so it's possible that some phone number formats // will be falsely rejected until there are at least // 3 digits in the national (significant) number being input. var index_of_leading_digits_pattern = leading_digits.length - MIN_LEADING_DIGITS_LENGTH; if (index_of_leading_digits_pattern < 0) { index_of_leading_digits_pattern = 0; } this.matching_formats = this.get_relevant_phone_number_formats().filter(function (format) { var leading_digits_pattern_count = get_format_leading_digits_patterns(format).length; // Keep everything that isn't restricted by leading digits. if (leading_digits_pattern_count === 0) { return true; } var leading_digits_pattern_index = Math.min(index_of_leading_digits_pattern, leading_digits_pattern_count - 1); var leading_digits_pattern = get_format_leading_digits_patterns(format)[leading_digits_pattern_index]; // Brackets are required for `^` to be applied to // all or-ed (`|`) parts, not just the first one. return new RegExp('^(' + leading_digits_pattern + ')').test(leading_digits); }); // If there was a phone number format chosen // and it no longer holds given the new leading digits then reset it if (this.chosen_format && this.matching_formats.indexOf(this.chosen_format) === -1) { this.reset_format(); } } }, { key: 'get_relevant_phone_number_formats', value: function get_relevant_phone_number_formats() { var leading_digits = this.national_number; // "leading digits" patterns start with a maximum 3 digits, // and then with each additional digit // a more precise "leading digits" pattern is specified. // They could make "leading digits" patterns start // with a maximum of a single digit, but they didn't, // so it's possible that some phone number formats // will be falsely rejected until there are at least // 3 digits in the national (significant) number being input. if (leading_digits.length <= MIN_LEADING_DIGITS_LENGTH) { return this.available_formats; } return this.matching_formats; } // Check to see if there is an exact pattern match for these digits. If so, we // should use this instead of any other formatting template whose // leadingDigitsPattern also matches the input. }, { key: 'attempt_to_format_complete_phone_number', value: function attempt_to_format_complete_phone_number() { var _iteratorNormalCompletion = true; var _didIteratorError = false; var _iteratorError = undefined; try { for (var _iterator = _getIterator(this.get_relevant_phone_number_formats()), _step; !(_iteratorNormalCompletion = (_step = _iterator.next()).done); _iteratorNormalCompletion = true) { var format = _step.value; var matcher = new RegExp('^(?:' + get_format_pattern(format) + ')$'); if (!matcher.test(this.national_number)) { continue; } if (!this.validate_format(format)) { continue; } // To leave the formatter in a consistent state this.reset_format(); this.chosen_format = format; var formatted_number = format_national_number_using_format(this.national_number, format, this.is_international(), this.national_prefix.length > 0, this.country_metadata); // Set `this.template` and `this.partially_populated_template` // // `else` case doesn't ever happen // with the current metadata, // but just in case. // /* istanbul ignore else */ if (this.create_formatting_template(format)) { // Populate `this.partially_populated_template` this.reformat_national_number(); } else { var full_number = this.full_phone_number(formatted_number); this.template = full_number.replace(/[\d\+]/g, DIGIT_PLACEHOLDER); this.partially_populated_template = full_number; } return formatted_number; } } catch (err) { _didIteratorError = true; _iteratorError = err; } finally { try { if (!_iteratorNormalCompletion && _iterator.return) { _iterator.return(); } } finally { if (_didIteratorError) { throw _iteratorError; } } } } // Combines the national number with the appropriate prefix }, { key: 'full_phone_number', value: function full_phone_number(formatted_national_number) { if (this.is_international()) { return '+' + this.country_phone_code + ' ' + formatted_national_number; } return formatted_national_number; } // Extracts the country calling code from the beginning // of the entered `national_number` (so far), // and places the remaining input into the `national_number`. }, { key: 'extract_country_phone_code', value: function extract_country_phone_code() { if (!this.national_number) { return; } var _parse_phone_number_a = parse_phone_number_and_country_phone_code(this.parsed_input, this.metadata), country_phone_code = _parse_phone_number_a.country_phone_code, number = _parse_phone_number_a.number; if (!country_phone_code) { return; } this.country_phone_code = country_phone_code; this.national_number = number; return this.country_metadata = get_metadata_by_country_phone_code(country_phone_code, this.metadata); } }, { key: 'extract_national_prefix', value: function extract_national_prefix() { this.national_prefix = ''; if (!this.country_metadata) { return; } var national_number = strip_national_prefix(this.national_number, this.country_metadata); if (national_number !== this.national_number) { this.national_prefix = this.national_number.slice(0, this.national_number.length - national_number.length); this.national_number = national_number; } return this.national_prefix; } }, { key: 'choose_another_format', value: function choose_another_format() { // When there are multiple available formats, the formatter uses the first // format where a formatting template could be created. var _iteratorNormalCompletion2 = true; var _didIteratorError2 = false; var _iteratorError2 = undefined; try { for (var _iterator2 = _getIterator(this.get_relevant_phone_number_formats()), _step2; !(_iteratorNormalCompletion2 = (_step2 = _iterator2.next()).done); _iteratorNormalCompletion2 = true) { var format = _step2.value; // If this format is currently being used // and is still possible, then stick to it. if (this.chosen_format === format) { return; } // If this `format` is suitable for "as you type", // then extract the template from this format // and use it to format the phone number being input. if (!this.validate_format(format)) { continue; } if (!this.create_formatting_template(format)) { continue; } this.chosen_format = format; // With a new formatting template, the matched position // using the old template needs to be reset. this.last_match_position = -1; return true; } // No format matches the phone number, // therefore set `country` to `undefined` // (or to the default country). } catch (err) { _didIteratorError2 = true; _iteratorError2 = err; } finally { try { if (!_iteratorNormalCompletion2 && _iterator2.return) { _iterator2.return(); } } finally { if (_didIteratorError2) { throw _iteratorError2; } } } this.reset_country(); // No format matches the national phone number entered this.reset_format(); } }, { key: 'validate_format', value: function validate_format(format) { // If national prefix is mandatory for this phone number format // and the user didn't input the national prefix, // then this phone number format isn't suitable. if (!this.is_international() && !this.national_prefix && get_format_national_prefix_is_mandatory_when_formatting(format, this.country_metadata)) { return; } return true; } }, { key: 'create_formatting_template', value: function create_formatting_template(format) { // The formatter doesn't format numbers when numberPattern contains '|', e.g. // (20|3)\d{4}. In those cases we quickly return. // (Though there's no such format in current metadata) /* istanbul ignore if */ if (get_format_pattern(format).indexOf('|') >= 0) { return; } var national_prefix_formatting_rule = get_format_national_prefix_formatting_rule(format, this.country_metadata); // A very smart trick by the guys at Google var number_pattern = get_format_pattern(format) // Replace anything in the form of [..] with \d .replace(CHARACTER_CLASS_PATTERN, '\\d') // Replace any standalone digit (not the one in `{}`) with \d .replace(STANDALONE_DIGIT_PATTERN, '\\d'); // This match will always succeed, // because the "longest dummy phone number" // has enough length to accomodate any possible // national phone number format pattern. var dummy_phone_number_matching_format_pattern = LONGEST_DUMMY_PHONE_NUMBER.match(number_pattern)[0]; // If the national number entered is too long // for any phone number format, then abort. if (this.national_number.length > dummy_phone_number_matching_format_pattern.length) { return; } // Now prepare phone number format var number_format = this.get_format_format(format); // If the user did input the national prefix // then maybe make it a part of the phone number template if (this.national_prefix) { var _national_prefix_formatting_rule = get_format_national_prefix_formatting_rule(format, this.country_metadata); // If national prefix formatting rule is set // for this phone number format if (_national_prefix_formatting_rule) { // Make the national prefix a part of the phone number template number_format = number_format.replace(FIRST_GROUP_PATTERN, _national_prefix_formatting_rule); } } // Get a formatting template which can be used to efficiently format // a partial number where digits are added one by one. // Create formatting template for this phone number format var template = dummy_phone_number_matching_format_pattern // Format the dummy phone number according to the format .replace(new RegExp(number_pattern, 'g'), number_format) // Replace each dummy digit with a DIGIT_PLACEHOLDER .replace(DUMMY_DIGIT_MATCHER, DIGIT_PLACEHOLDER); // This one is for national number only this.partially_populated_template = template; // For convenience, the public `.template` property // is gonna contain the whole international number // if the phone number being input is international. if (this.is_international()) { template = DIGIT_PLACEHOLDER + repeat(DIGIT_PLACEHOLDER, this.country_phone_code.length) + ' ' + template; } // For local numbers, replace national prefix // with a digit placeholder. else { template = template.replace(/\d/g, DIGIT_PLACEHOLDER); } // This one is for the full phone number return this.template = template; } }, { key: 'format_next_national_number_digits', value: function format_next_national_number_digits(digits) { var _iteratorNormalCompletion3 = true; var _didIteratorError3 = false; var _iteratorError3 = undefined; try { for (var _iterator3 = _getIterator(digits), _step3; !(_iteratorNormalCompletion3 = (_step3 = _iterator3.next()).done); _iteratorNormalCompletion3 = true) { var digit = _step3.value; // If there is room for more digits in current `template`, // then set the next digit in the `template`, // and return the formatted digits so far. // If more digits are entered than the current format could handle if (this.partially_populated_template.slice(this.last_match_position + 1).search(DIGIT_PLACEHOLDER_MATCHER) === -1) { // Reset the current format, // so that the new format will be chosen // in a subsequent `this.choose_another_format()` call // later in code. this.chosen_format = undefined; this.template = undefined; this.partially_populated_template = undefined; return; } this.last_match_position = this.partially_populated_template.search(DIGIT_PLACEHOLDER_MATCHER); this.partially_populated_template = this.partially_populated_template.replace(DIGIT_PLACEHOLDER_MATCHER, digit); } // Return the formatted phone number so far } catch (err) { _didIteratorError3 = true; _iteratorError3 = err; } finally { try { if (!_iteratorNormalCompletion3 && _iterator3.return) { _iterator3.return(); } } finally { if (_didIteratorError3) { throw _iteratorError3; } } } return close_dangling_braces(this.partially_populated_template, this.last_match_position + 1).replace(DIGIT_PLACEHOLDER_MATCHER_GLOBAL, ' '); } }, { key: 'is_international', value: function is_international() { return this.parsed_input && this.parsed_input[0] === '+'; } }, { key: 'get_format_format', value: function get_format_format(format) { if (this.is_international()) { return local_to_international_style(get_format_international_format(format)); } return _get_format_format(format); } // Determines the country of the phone number // entered so far based on the country phone code // and the national phone number. }, { key: 'determine_the_country', value: function determine_the_country() { this.country = find_country_code(this.country_phone_code, this.national_number, this.metadata); } }]); return as_you_type; }(); export default as_you_type; export function close_dangling_braces(template, cut_before) { var retained_template = template.slice(0, cut_before); var opening_braces = count_occurences('(', retained_template); var closing_braces = count_occurences(')', retained_template); var dangling_braces = opening_braces - closing_braces; while (dangling_braces > 0 && cut_before < template.length) { if (template[cut_before] === ')') { dangling_braces--; } cut_before++; } return template.slice(0, cut_before); } // Counts all occurences of a symbol in a string export function count_occurences(symbol, string) { var count = 0; var _iteratorNormalCompletion4 = true; var _didIteratorError4 = false; var _iteratorError4 = undefined; try { for (var _iterator4 = _getIterator(string), _step4; !(_iteratorNormalCompletion4 = (_step4 = _iterator4.next()).done); _iteratorNormalCompletion4 = true) { var character = _step4.value; if (character === symbol) { count++; } } } catch (err) { _didIteratorError4 = true; _iteratorError4 = err; } finally { try { if (!_iteratorNormalCompletion4 && _iterator4.return) { _iterator4.return(); } } finally { if (_didIteratorError4) { throw _iteratorError4; } } } return count; } // Repeats a string (or a symbol) N times. // http://stackoverflow.com/questions/202605/repeat-string-javascript export function repeat(string, times) { if (times < 1) { return ''; } var result = ''; while (times > 1) { if (times & 1) { result += string; } times >>= 1; string += string; } return result + string; } //# sourceMappingURL=as you type.js.map