/** * @license * Copyright (C) 2018 The Libphonenumber Authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ /** * @fileoverview Utility for international phone numbers. * Functionality includes formatting, parsing and validation. * (based on the java implementation). * * NOTE: A lot of methods in this class require Region Code strings. These must * be provided using CLDR two-letter region-code format. These should be in * upper-case. The list of the codes can be found here: * http://www.unicode.org/cldr/charts/30/supplemental/territory_information.html * * @author James Wright */ goog.provide('i18n.phonenumbers.ShortNumberInfo'); goog.require('goog.proto2.PbLiteSerializer'); goog.require('i18n.phonenumbers.PhoneMetadata'); goog.require('i18n.phonenumbers.PhoneNumber'); goog.require('i18n.phonenumbers.PhoneNumberDesc'); goog.require('i18n.phonenumbers.PhoneNumberUtil'); goog.require('i18n.phonenumbers.metadata'); goog.require('i18n.phonenumbers.shortnumbermetadata'); /** * @constructor * @private */ i18n.phonenumbers.ShortNumberInfo = function() { /** * A mapping from region code to the short-number metadata for that region. * @type {Object.} */ this.regionToMetadataMap = {}; }; goog.addSingletonGetter(i18n.phonenumbers.ShortNumberInfo); /** * In these countries, if extra digits are added to an emergency number, it no * longer connects to the emergency service. * @const * @type {!Array} * @private */ i18n.phonenumbers.ShortNumberInfo. REGIONS_WHERE_EMERGENCY_NUMBERS_MUST_BE_EXACT_ = [ 'BR', 'CL', 'NI' ]; /** * @enum {number} Cost categories of short numbers. */ i18n.phonenumbers.ShortNumberInfo.ShortNumberCost = { TOLL_FREE: 0, STANDARD_RATE: 1, PREMIUM_RATE: 2, UNKNOWN_COST: 3 }; /** * Returns a list with the region codes that match the specific country calling * code. For non-geographical country calling codes, the region code 001 is * returned. Also, in the case of no region code being found, an empty list * is returned. * @param {number} countryCallingCode * @return {!Array} The region codes that match the given country code. * @private */ i18n.phonenumbers.ShortNumberInfo.prototype.getRegionCodesForCountryCode_ = function(countryCallingCode) { var regionCodes = i18n.phonenumbers.metadata .countryCodeToRegionCodeMap[countryCallingCode]; return regionCodes ? regionCodes : []; }; /** * Helper method to check that the country calling code of the number matches * the region it's being dialed from. * @param {i18n.phonenumbers.PhoneNumber} number * @param {?string} regionDialingFrom * @return {boolean} * @private */ i18n.phonenumbers.ShortNumberInfo.prototype.regionDialingFromMatchesNumber_ = function(number, regionDialingFrom) { var regionCodes = this.getRegionCodesForCountryCode_( number.getCountryCodeOrDefault()); return regionDialingFrom != null && regionCodes.includes(regionDialingFrom); }; /** * Check whether a short number is a possible number when dialed from the given * region. This provides a more lenient check than * {@link #isValidShortNumberForRegion}. * * @param {i18n.phonenumbers.PhoneNumber} number the short number to check * @param {string} regionDialingFrom the region from which the number is dialed * @return {boolean} whether the number is a possible short number */ i18n.phonenumbers.ShortNumberInfo.prototype.isPossibleShortNumberForRegion = function(number, regionDialingFrom) { if (!this.regionDialingFromMatchesNumber_(number, regionDialingFrom)) { return false; } var phoneMetadata = this.getMetadataForRegion_(regionDialingFrom); if (!phoneMetadata) { return false; } var numberLength = this.getNationalSignificantNumber_(number).length; return phoneMetadata.getGeneralDesc().possibleLengthArray().includes( numberLength); }; /** * Check whether a short number is a possible number. If a country calling code * is shared by multiple regions, this returns true if it's possible in any of * them. This provides a more lenient check than {@link #isValidShortNumber}. * See {@link #isPossibleShortNumberForRegion(PhoneNumber, String)} for details. * * @param {i18n.phonenumbers.PhoneNumber} number the short number to check * @return {boolean} whether the number is a possible short number */ i18n.phonenumbers.ShortNumberInfo.prototype.isPossibleShortNumber = function(number) { var regionCodes = this.getRegionCodesForCountryCode_( number.getCountryCodeOrDefault()); var shortNumberLength = this.getNationalSignificantNumber_(number).length; for (var i = 0; i < regionCodes.length; i++) { var region = regionCodes[i]; var phoneMetadata = this.getMetadataForRegion_(region); if (!phoneMetadata) { continue; } var possibleLengths = phoneMetadata.getGeneralDesc().possibleLengthArray(); if (possibleLengths.includes(shortNumberLength)) { return true; } } return false; }; /** * Tests whether a short number matches a valid pattern in a region. Note that * this doesn't verify the number is actually in use, which is impossible to * tell by just looking at the number itself. * * @param {i18n.phonenumbers.PhoneNumber} number the short number for which we * want to test the validity * @param {?string} regionDialingFrom the region from which the number is dialed * @return {boolean} whether the short number matches a valid pattern */ i18n.phonenumbers.ShortNumberInfo.prototype.isValidShortNumberForRegion = function(number, regionDialingFrom) { if (!this.regionDialingFromMatchesNumber_(number, regionDialingFrom)) { return false; } var phoneMetadata = this.getMetadataForRegion_(regionDialingFrom); if (!phoneMetadata) { return false; } var shortNumber = this.getNationalSignificantNumber_(number); var generalDesc = phoneMetadata.getGeneralDesc(); if (!this.matchesPossibleNumberAndNationalNumber_(shortNumber, generalDesc)) { return false; } var shortNumberDesc = phoneMetadata.getShortCode(); return this.matchesPossibleNumberAndNationalNumber_(shortNumber, shortNumberDesc); }; /** * Tests whether a short number matches a valid pattern. If a country calling * code is shared by multiple regions, this returns true if it's valid in any of * them. Note that this doesn't verify the number is actually in use, which is * impossible to tell by just looking at the number itself. See * {@link #isValidShortNumberForRegion(PhoneNumber, String)} for details. * * @param {i18n.phonenumbers.PhoneNumber} number the short number for which we * want to test the validity * @return {boolean} whether the short number matches a valid pattern */ i18n.phonenumbers.ShortNumberInfo.prototype.isValidShortNumber = function(number) { var regionCodes = this.getRegionCodesForCountryCode_( number.getCountryCodeOrDefault()); var regionCode = this.getRegionCodeForShortNumberFromRegionList_(number, regionCodes); if (regionCodes.length > 1 && regionCode != null) { // If a matching region had been found for the phone number from among two // or more regions, then we have already implicitly verified its validity // for that region. return true; } return this.isValidShortNumberForRegion(number, regionCode); }; /** * Gets the expected cost category of a short number when dialed from a region * (however, nothing is implied about its validity). If it is important that the * number is valid, then its validity must first be checked using * {@link #isValidShortNumberForRegion}. Note that emergency numbers are always * considered toll-free. Example usage: *
{@code
 * // The region for which the number was parsed and the region we subsequently
 * // check against need not be the same. Here we parse the number in the US and
 * // check it for Canada.
 * PhoneNumber number = phoneUtil.parse("110", "US");
 * ...
 * String regionCode = "CA";
 * ShortNumberInfo shortInfo = ShortNumberInfo.getInstance();
 * if (shortInfo.isValidShortNumberForRegion(shortNumber, regionCode)) {
 *   ShortNumberCost cost = shortInfo.getExpectedCostForRegion(number,
 *                                                             regionCode);
 *   // Do something with the cost information here.
 * }}
* * @param {i18n.phonenumbers.PhoneNumber} number the short number for which we * want to know the expected cost category * @param {string} regionDialingFrom the region from which the number is dialed * @return {i18n.phonenumbers.ShortNumberInfo.ShortNumberCost} the expected cost * category for that region of the short number. Returns UNKNOWN_COST if the * number does not match a cost category. Note that an invalid number may * match any cost category. * @package */ // @VisibleForTesting i18n.phonenumbers.ShortNumberInfo.prototype.getExpectedCostForRegion = function(number, regionDialingFrom) { var ShortNumberCost = i18n.phonenumbers.ShortNumberInfo.ShortNumberCost; if (!this.regionDialingFromMatchesNumber_(number, regionDialingFrom)) { return ShortNumberCost.UNKNOWN_COST; } var phoneMetadata = this.getMetadataForRegion_(regionDialingFrom); if (!phoneMetadata) { return ShortNumberCost.UNKNOWN_COST; } var shortNumber = this.getNationalSignificantNumber_(number); if (!phoneMetadata.getGeneralDesc().possibleLengthArray().includes( shortNumber.length)) { return ShortNumberCost.UNKNOWN_COST; } if (this.matchesPossibleNumberAndNationalNumber_( shortNumber, phoneMetadata.getPremiumRate())) { return ShortNumberCost.PREMIUM_RATE; } if (this.matchesPossibleNumberAndNationalNumber_( shortNumber, phoneMetadata.getStandardRate())) { return ShortNumberCost.STANDARD_RATE; } if (this.matchesPossibleNumberAndNationalNumber_( shortNumber, phoneMetadata.getTollFree())) { return ShortNumberCost.TOLL_FREE; } if (this.isEmergencyNumber(shortNumber, regionDialingFrom)) { // Emergency numbers are implicitly toll-free return ShortNumberCost.TOLL_FREE; } return ShortNumberCost.UNKNOWN_COST; }; /** * Gets the expected cost category of a short number (however, nothing is * implied about its validity). If the country calling code is unique to a * region, this method behaves exactly the same as * {@link #getExpectedCostForRegion(PhoneNumber, String)}. However, if the * country calling code is shared by multiple regions, then it returns the * highest cost in the sequence PREMIUM_RATE, UNKNOWN_COST, STANDARD_RATE, * TOLL_FREE. The reason for the position of UNKNOWN_COST in this order is that * if a number is UNKNOWN_COST in one region but STANDARD_RATE or TOLL_FREE in * another, its expected cost cannot be estimated as one of the latter since it * might be a PREMIUM_RATE number. *

* For example, if a number is STANDARD_RATE in the US, but TOLL_FREE in Canada, * the expected cost returned by this method will be STANDARD_RATE, since the * NANPA countries share the same country calling code. *

* Note: If the region from which the number is dialed is known, it is highly * preferable to call {@link #getExpectedCostForRegion(PhoneNumber, String)} * instead. * * @param {i18n.phonenumbers.PhoneNumber} number the short number for which we * want to know the expected cost category * @return {i18n.phonenumbers.ShortNumberInfo.ShortNumberCost} the highest * expected cost category of the short number in the region(s) with the * given country calling code * @package */ // @VisibleForTesting i18n.phonenumbers.ShortNumberInfo.prototype.getExpectedCost = function(number) { var ShortNumberCost = i18n.phonenumbers.ShortNumberInfo.ShortNumberCost; var regionCodes = this.getRegionCodesForCountryCode_( number.getCountryCodeOrDefault()); if (regionCodes.length === 0) { return ShortNumberCost.UNKNOWN_COST; } if (regionCodes.length === 1) { return this.getExpectedCostForRegion(number, regionCodes[0]); } var cost = ShortNumberCost.TOLL_FREE; for (var i = 0; i < regionCodes.length; i++) { var regionCode = regionCodes[i]; var costForRegion = this.getExpectedCostForRegion(number, regionCode); switch (costForRegion) { case ShortNumberCost.PREMIUM_RATE: return ShortNumberCost.PREMIUM_RATE; case ShortNumberCost.UNKNOWN_COST: cost = ShortNumberCost.UNKNOWN_COST; break; case ShortNumberCost.STANDARD_RATE: if (cost !== ShortNumberCost.UNKNOWN_COST) { cost = ShortNumberCost.STANDARD_RATE; } break; case ShortNumberCost.TOLL_FREE: // Do nothing. break; default: throw new Error('Unrecognized cost for region: ' + costForRegion); } } return cost; }; /** * Helper method to get the region code for a given phone number, from a list * of possible region codes. If the list contains more than one region, the * first region for which the number is valid is returned. * @param {!i18n.phonenumbers.PhoneNumber} number * @param {Array} regionCodes * @return {?string} * @private */ i18n.phonenumbers.ShortNumberInfo.prototype.getRegionCodeForShortNumberFromRegionList_ = function(number, regionCodes) { if (regionCodes.length === 0) { return null; } else if (regionCodes.length === 1) { return regionCodes[0]; } var nationalNumber = this.getNationalSignificantNumber_(number); for (var i = 0; i < regionCodes.length; i++) { var regionCode = regionCodes[i]; var phoneMetadata = this.getMetadataForRegion_(regionCode); if (phoneMetadata && this.matchesPossibleNumberAndNationalNumber_( nationalNumber, phoneMetadata.getShortCode())) { return regionCode; } } return null; }; /** * Convenience method to get a list of what regions the library has metadata for * @return {!Array} the list of region codes * @package */ i18n.phonenumbers.ShortNumberInfo.prototype.getSupportedRegions = function() { return Object.keys(i18n.phonenumbers.shortnumbermetadata.countryToMetadata) .filter(function(regionCode) { return isNaN(regionCode); }); }; /** * Gets a valid short number for the specified region. * * @param {?string} regionCode the region for which an example short number is * needed * @return {string} a valid short number for the specified region. Returns an * empty string when the metadata does not contain such information. * @package */ i18n.phonenumbers.ShortNumberInfo.prototype.getExampleShortNumber = function(regionCode) { var phoneMetadata = this.getMetadataForRegion_(regionCode); if (!phoneMetadata) { return ''; } var desc = phoneMetadata.getShortCode(); if (desc.hasExampleNumber()) { return desc.getExampleNumber() || ''; } return ''; }; /** * Gets a valid short number for the specified cost category. * * @param {string} regionCode the region for which an example short number is * needed * @param {i18n.phonenumbers.ShortNumberInfo.ShortNumberCost} cost the cost * category of number that is needed * @return {string} a valid short number for the specified region and cost * category. Returns an empty string when the metadata does not contain such * information, or the cost is UNKNOWN_COST. */ i18n.phonenumbers.ShortNumberInfo.prototype.getExampleShortNumberForCost = function(regionCode, cost) { var phoneMetadata = this.getMetadataForRegion_(regionCode); if (!phoneMetadata) { return ''; } var ShortNumberCost = i18n.phonenumbers.ShortNumberInfo.ShortNumberCost; var desc = null; switch (cost) { case ShortNumberCost.TOLL_FREE: desc = phoneMetadata.getTollFree(); break; case ShortNumberCost.STANDARD_RATE: desc = phoneMetadata.getStandardRate(); break; case ShortNumberCost.PREMIUM_RATE: desc = phoneMetadata.getPremiumRate(); break; default: // UNKNOWN_COST numbers are computed by the process of elimination from // the other cost categories. } if (desc && desc.hasExampleNumber()) { return desc.getExampleNumber() || ''; } return ''; }; /** * Returns true if the given number, exactly as dialed, might be used to * connect to an emergency service in the given region. *

* This method accepts a string, rather than a PhoneNumber, because it needs * to distinguish cases such as "+1 911" and "911", where the former may not * connect to an emergency service in all cases but the latter would. This * method takes into account cases where the number might contain formatting, * or might have additional digits appended (when it is okay to do that in * the specified region). * * @param {string} number the phone number to test * @param {string} regionCode the region where the phone number is being * dialed * @return {boolean} whether the number might be used to connect to an * emergency service in the given region */ i18n.phonenumbers.ShortNumberInfo.prototype.connectsToEmergencyNumber = function(number, regionCode) { return this.matchesEmergencyNumberHelper_(number, regionCode, true /* allows prefix match */); }; /** * Returns true if the given number exactly matches an emergency service * number in the given region. *

* This method takes into account cases where the number might contain * formatting, but doesn't allow additional digits to be appended. Note that * {@code isEmergencyNumber(number, region)} implies * {@code connectsToEmergencyNumber(number, region)}. * * @param {string} number the phone number to test * @param {string} regionCode the region where the phone number is being * dialed * @return {boolean} whether the number exactly matches an emergency services * number in the given region. */ i18n.phonenumbers.ShortNumberInfo.prototype.isEmergencyNumber = function(number, regionCode) { return this.matchesEmergencyNumberHelper_(number, regionCode, false /* doesn't allow prefix match */); }; /** * @param {?string} regionCode The region code to get metadata for * @return {?i18n.phonenumbers.PhoneMetadata} The region code's metadata, or * null if it is not available or the region code is invalid. * @private */ i18n.phonenumbers.ShortNumberInfo.prototype.getMetadataForRegion_ = function(regionCode) { if (!regionCode) { return null; } regionCode = regionCode.toUpperCase(); var metadata = this.regionToMetadataMap[regionCode]; if (metadata == null) { /** @type {goog.proto2.PbLiteSerializer} */ var serializer = new goog.proto2.PbLiteSerializer(); var metadataSerialized = i18n.phonenumbers.shortnumbermetadata.countryToMetadata[regionCode]; if (metadataSerialized == null) { return null; } metadata = /** @type {i18n.phonenumbers.PhoneMetadata} */ ( serializer.deserialize(i18n.phonenumbers.PhoneMetadata.getDescriptor(), metadataSerialized)); this.regionToMetadataMap[regionCode] = metadata; } return metadata; }; /** * @param {string} number the number to match against * @param {string} regionCode the region code to check against * @param {boolean} allowPrefixMatch whether to allow prefix matching * @return {boolean} True iff the number matches an emergency number for that * particular region. * @private */ i18n.phonenumbers.ShortNumberInfo.prototype.matchesEmergencyNumberHelper_ = function(number, regionCode, allowPrefixMatch) { var possibleNumber = i18n.phonenumbers.PhoneNumberUtil .extractPossibleNumber(number); if (i18n.phonenumbers.PhoneNumberUtil.LEADING_PLUS_CHARS_PATTERN .test(possibleNumber)) { return false; } var metadata = this.getMetadataForRegion_(regionCode); if (metadata == null || !metadata.hasEmergency()) { return false; } var normalizedNumber = i18n.phonenumbers.PhoneNumberUtil .normalizeDigitsOnly(possibleNumber); var allowPrefixMatchForRegion = allowPrefixMatch && !i18n.phonenumbers.ShortNumberInfo .REGIONS_WHERE_EMERGENCY_NUMBERS_MUST_BE_EXACT_.includes(regionCode); var emergencyNumberPattern = metadata.getEmergency() .getNationalNumberPatternOrDefault(); var result = i18n.phonenumbers.PhoneNumberUtil.matchesEntirely( emergencyNumberPattern, normalizedNumber); return result || (allowPrefixMatchForRegion && i18n.phonenumbers.PhoneNumberUtil .matchesPrefix(emergencyNumberPattern, normalizedNumber)); }; /** * Given a valid short number, determines whether it is carrier-specific * (however, nothing is implied about its validity). Carrier-specific numbers * may connect to a different end-point, or not connect at all, depending on * the user's carrier. If it is important that the number is valid, then its * validity must first be checked using {@link #isValidShortNumber} or * {@link #isValidShortNumberForRegion}. * * @param {i18n.phonenumbers.PhoneNumber} number the valid short number to * check * @return {boolean} whether the short number is carrier-specific, assuming the * input was a valid short number */ i18n.phonenumbers.ShortNumberInfo.prototype.isCarrierSpecific = function(number) { var regionCodes = this.getRegionCodesForCountryCode_( number.getCountryCodeOrDefault()); var regionCode = this.getRegionCodeForShortNumberFromRegionList_(number, regionCodes); var nationalNumber = this.getNationalSignificantNumber_(number); var phoneMetadata = this.getMetadataForRegion_(regionCode); return !!phoneMetadata && this.matchesPossibleNumberAndNationalNumber_( nationalNumber, phoneMetadata.getCarrierSpecific()); }; /** * Given a valid short number, determines whether it is carrier-specific when * dialed from the given region (however, nothing is implied about its * validity). Carrier-specific numbers may connect to a different end-point, or * not connect at all, depending on the user's carrier. If it is important that * the number is valid, then its validity must first be checked using * {@link #isValidShortNumber} or {@link #isValidShortNumberForRegion}. Returns * false if the number doesn't match the region provided. * * @param {i18n.phonenumbers.PhoneNumber} number the valid short number to * check * @param {string} regionDialingFrom the region from which the number is dialed * @return {boolean} whether the short number is carrier-specific in the * provided region, assuming the input was a valid short number */ i18n.phonenumbers.ShortNumberInfo.prototype.isCarrierSpecificForRegion = function(number, regionDialingFrom) { if (!this.regionDialingFromMatchesNumber_(number, regionDialingFrom)) { return false; } var nationalNumber = this.getNationalSignificantNumber_(number); var phoneMetadata = this.getMetadataForRegion_(regionDialingFrom); return !!phoneMetadata && this.matchesPossibleNumberAndNationalNumber_( nationalNumber, phoneMetadata.getCarrierSpecific()); }; /** * Given a valid short number, determines whether it is an SMS service * (however, nothing is implied about its validity). An SMS service is where the * primary or only intended usage is to receive and/or send text messages * (SMSs). This includes MMS as MMS numbers downgrade to SMS if the other party * isn't MMS-capable. If it is important that the number is valid, then its * validity must first be checked using {@link #isValidShortNumber} or {@link * #isValidShortNumberForRegion}. Returns false if the number doesn't match the * region provided. * * @param {i18n.phonenumbers.PhoneNumber} number the valid short number to * check * @param {string} regionDialingFrom the region from which the number is dialed * @return {boolean} whether the short number is an SMS service in the provided * region, assuming the input was a valid short number */ i18n.phonenumbers.ShortNumberInfo.prototype.isSmsServiceForRegion = function(number, regionDialingFrom) { if (!this.regionDialingFromMatchesNumber_(number, regionDialingFrom)) { return false; } var phoneMetadata = this.getMetadataForRegion_(regionDialingFrom); var nationalNumber = this.getNationalSignificantNumber_(number); return !!phoneMetadata && this.matchesPossibleNumberAndNationalNumber_( nationalNumber, phoneMetadata.getSmsServices()); }; /** * Gets the national significant number of a phone number. Note a national * significant number doesn't contain a national prefix or any formatting. *

* This is a temporary duplicate of the {@code getNationalSignificantNumber} * method from {@code PhoneNumberUtil}. Ultimately a canonical static version * should exist in a separate utility class (to prevent {@code ShortNumberInfo} * needing to depend on PhoneNumberUtil). * * @param {i18n.phonenumbers.PhoneNumber} number the phone number for which the * national significant number is needed. * @return {string} the national significant number of the PhoneNumber object * passed in. * @private */ i18n.phonenumbers.ShortNumberInfo.prototype.getNationalSignificantNumber_ = function(number) { if (!number.hasNationalNumber()) { return ''; } /** @type {string} */ var nationalNumber = '' + number.getNationalNumber(); // If leading zero(s) have been set, we prefix this now. Note that a single // leading zero is not the same as a national prefix; leading zeros should be // dialled no matter whether you are dialling from within or outside the // country, national prefixes are added when formatting nationally if // applicable. if (number.hasItalianLeadingZero() && number.getItalianLeadingZero() && number.getNumberOfLeadingZerosOrDefault() > 0) { return Array(number.getNumberOfLeadingZerosOrDefault() + 1).join('0') + nationalNumber; } return nationalNumber; }; /** * Helper method to add in a performance optimization. * TODO: Once we have benchmarked ShortNumberInfo, consider if it is worth * keeping this performance optimization. * @param {string} number * @param {i18n.phonenumbers.PhoneNumberDesc} numberDesc * @return {boolean} * @private */ i18n.phonenumbers.ShortNumberInfo.prototype .matchesPossibleNumberAndNationalNumber_ = function(number, numberDesc) { if (numberDesc.possibleLengthArray().length > 0 && !numberDesc.possibleLengthArray().includes(number.length)) { return false; } return i18n.phonenumbers.PhoneNumberUtil.matchesEntirely( numberDesc.getNationalNumberPatternOrDefault(), number.toString()); };