2 * Copyright (C) 2013 The Libphonenumber Authors
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
8 * http://www.apache.org/licenses/LICENSE-2.0
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
17 package com.google.i18n.phonenumbers;
19 import com.google.i18n.phonenumbers.Phonemetadata.PhoneMetadata;
20 import com.google.i18n.phonenumbers.Phonemetadata.PhoneNumberDesc;
21 import com.google.i18n.phonenumbers.Phonenumber.PhoneNumber;
23 import java.util.Collections;
24 import java.util.HashSet;
25 import java.util.List;
27 import java.util.logging.Level;
28 import java.util.logging.Logger;
29 import java.util.regex.Pattern;
32 * Methods for getting information about short phone numbers, such as short codes and emergency
33 * numbers. Note that most commercial short numbers are not handled here, but by the
34 * {@link PhoneNumberUtil}.
36 * @author Shaopeng Jia
37 * @author David Yonge-Mallo
39 public class ShortNumberInfo {
40 private static final Logger logger = Logger.getLogger(ShortNumberInfo.class.getName());
42 private static final ShortNumberInfo INSTANCE =
43 new ShortNumberInfo(PhoneNumberUtil.getInstance());
45 // In these countries, if extra digits are added to an emergency number, it no longer connects
46 // to the emergency service.
47 private static final Set<String> REGIONS_WHERE_EMERGENCY_NUMBERS_MUST_BE_EXACT =
48 new HashSet<String>();
50 REGIONS_WHERE_EMERGENCY_NUMBERS_MUST_BE_EXACT.add("BR");
51 REGIONS_WHERE_EMERGENCY_NUMBERS_MUST_BE_EXACT.add("CL");
52 REGIONS_WHERE_EMERGENCY_NUMBERS_MUST_BE_EXACT.add("NI");
55 /** Cost categories of short numbers. */
56 public enum ShortNumberCost {
63 /** Returns the singleton instance of the ShortNumberInfo. */
64 public static ShortNumberInfo getInstance() {
68 private final PhoneNumberUtil phoneUtil;
71 ShortNumberInfo(PhoneNumberUtil util) {
76 * Check whether a short number is a possible number when dialled from a region, given the number
77 * in the form of a string, and the region where the number is dialed from. This provides a more
78 * lenient check than {@link #isValidShortNumberForRegion}.
80 * @param shortNumber the short number to check as a string
81 * @param regionDialingFrom the region from which the number is dialed
82 * @return whether the number is a possible short number
84 public boolean isPossibleShortNumberForRegion(String shortNumber, String regionDialingFrom) {
85 PhoneMetadata phoneMetadata =
86 MetadataManager.getShortNumberMetadataForRegion(regionDialingFrom);
87 if (phoneMetadata == null) {
90 PhoneNumberDesc generalDesc = phoneMetadata.getGeneralDesc();
91 return phoneUtil.isNumberPossibleForDesc(shortNumber, generalDesc);
95 * Check whether a short number is a possible number. If a country calling code is shared by
96 * multiple regions, this returns true if it's possible in any of them. This provides a more
97 * lenient check than {@link #isValidShortNumber}. See {@link
98 * #isPossibleShortNumberForRegion(String, String)} for details.
100 * @param number the short number to check
101 * @return whether the number is a possible short number
103 public boolean isPossibleShortNumber(PhoneNumber number) {
104 List<String> regionCodes = phoneUtil.getRegionCodesForCountryCode(number.getCountryCode());
105 String shortNumber = phoneUtil.getNationalSignificantNumber(number);
106 for (String region : regionCodes) {
107 PhoneMetadata phoneMetadata = MetadataManager.getShortNumberMetadataForRegion(region);
108 if (phoneUtil.isNumberPossibleForDesc(shortNumber, phoneMetadata.getGeneralDesc())) {
116 * Tests whether a short number matches a valid pattern in a region. Note that this doesn't verify
117 * the number is actually in use, which is impossible to tell by just looking at the number
120 * @param shortNumber the short number to check as a string
121 * @param regionDialingFrom the region from which the number is dialed
122 * @return whether the short number matches a valid pattern
124 public boolean isValidShortNumberForRegion(String shortNumber, String regionDialingFrom) {
125 PhoneMetadata phoneMetadata =
126 MetadataManager.getShortNumberMetadataForRegion(regionDialingFrom);
127 if (phoneMetadata == null) {
130 PhoneNumberDesc generalDesc = phoneMetadata.getGeneralDesc();
131 if (!generalDesc.hasNationalNumberPattern() ||
132 !phoneUtil.isNumberMatchingDesc(shortNumber, generalDesc)) {
135 PhoneNumberDesc shortNumberDesc = phoneMetadata.getShortCode();
136 if (!shortNumberDesc.hasNationalNumberPattern()) {
137 logger.log(Level.WARNING, "No short code national number pattern found for region: " +
141 return phoneUtil.isNumberMatchingDesc(shortNumber, shortNumberDesc);
145 * Tests whether a short number matches a valid pattern. If a country calling code is shared by
146 * multiple regions, this returns true if it's valid in any of them. Note that this doesn't verify
147 * the number is actually in use, which is impossible to tell by just looking at the number
148 * itself. See {@link #isValidShortNumberForRegion(String, String)} for details.
150 * @param number the short number for which we want to test the validity
151 * @return whether the short number matches a valid pattern
153 public boolean isValidShortNumber(PhoneNumber number) {
154 List<String> regionCodes = phoneUtil.getRegionCodesForCountryCode(number.getCountryCode());
155 String shortNumber = phoneUtil.getNationalSignificantNumber(number);
156 String regionCode = getRegionCodeForShortNumberFromRegionList(number, regionCodes);
157 if (regionCodes.size() > 1 && regionCode != null) {
158 // If a matching region had been found for the phone number from among two or more regions,
159 // then we have already implicitly verified its validity for that region.
162 return isValidShortNumberForRegion(shortNumber, regionCode);
166 * Gets the expected cost category of a short number when dialled from a region (however, nothing
167 * is implied about its validity). If it is important that the number is valid, then its validity
168 * must first be checked using {@link isValidShortNumberForRegion}. Note that emergency numbers
169 * are always considered toll-free. Example usage:
171 * ShortNumberInfo shortInfo = ShortNumberInfo.getInstance();
172 * String shortNumber = "110";
173 * String regionCode = "FR";
174 * if (shortInfo.isValidShortNumberForRegion(shortNumber, regionCode)) {
175 * ShortNumberInfo.ShortNumberCost cost = shortInfo.getExpectedCostForRegion(shortNumber,
177 * // Do something with the cost information here.
180 * @param shortNumber the short number for which we want to know the expected cost category,
182 * @param regionDialingFrom the region from which the number is dialed
183 * @return the expected cost category for that region of the short number. Returns UNKNOWN_COST if
184 * the number does not match a cost category. Note that an invalid number may match any cost
187 public ShortNumberCost getExpectedCostForRegion(String shortNumber, String regionDialingFrom) {
188 // Note that regionDialingFrom may be null, in which case phoneMetadata will also be null.
189 PhoneMetadata phoneMetadata = MetadataManager.getShortNumberMetadataForRegion(
191 if (phoneMetadata == null) {
192 return ShortNumberCost.UNKNOWN_COST;
195 // The cost categories are tested in order of decreasing expense, since if for some reason the
196 // patterns overlap the most expensive matching cost category should be returned.
197 if (phoneUtil.isNumberMatchingDesc(shortNumber, phoneMetadata.getPremiumRate())) {
198 return ShortNumberCost.PREMIUM_RATE;
200 if (phoneUtil.isNumberMatchingDesc(shortNumber, phoneMetadata.getStandardRate())) {
201 return ShortNumberCost.STANDARD_RATE;
203 if (phoneUtil.isNumberMatchingDesc(shortNumber, phoneMetadata.getTollFree())) {
204 return ShortNumberCost.TOLL_FREE;
206 if (isEmergencyNumber(shortNumber, regionDialingFrom)) {
207 // Emergency numbers are implicitly toll-free.
208 return ShortNumberCost.TOLL_FREE;
210 return ShortNumberCost.UNKNOWN_COST;
214 * Gets the expected cost category of a short number (however, nothing is implied about its
215 * validity). If the country calling code is unique to a region, this method behaves exactly the
216 * same as {@link #getExpectedCostForRegion(String, String)}. However, if the country calling
217 * code is shared by multiple regions, then it returns the highest cost in the sequence
218 * PREMIUM_RATE, UNKNOWN_COST, STANDARD_RATE, TOLL_FREE. The reason for the position of
219 * UNKNOWN_COST in this order is that if a number is UNKNOWN_COST in one region but STANDARD_RATE
220 * or TOLL_FREE in another, its expected cost cannot be estimated as one of the latter since it
221 * might be a PREMIUM_RATE number.
223 * For example, if a number is STANDARD_RATE in the US, but TOLL_FREE in Canada, the expected cost
224 * returned by this method will be STANDARD_RATE, since the NANPA countries share the same country
227 * Note: If the region from which the number is dialed is known, it is highly preferable to call
228 * {@link #getExpectedCostForRegion(String, String)} instead.
230 * @param number the short number for which we want to know the expected cost category
231 * @return the highest expected cost category of the short number in the region(s) with the given
232 * country calling code
234 public ShortNumberCost getExpectedCost(PhoneNumber number) {
235 List<String> regionCodes = phoneUtil.getRegionCodesForCountryCode(number.getCountryCode());
236 if (regionCodes.size() == 0) {
237 return ShortNumberCost.UNKNOWN_COST;
239 String shortNumber = phoneUtil.getNationalSignificantNumber(number);
240 if (regionCodes.size() == 1) {
241 return getExpectedCostForRegion(shortNumber, regionCodes.get(0));
243 ShortNumberCost cost = ShortNumberCost.TOLL_FREE;
244 for (String regionCode : regionCodes) {
245 ShortNumberCost costForRegion = getExpectedCostForRegion(shortNumber, regionCode);
246 switch (costForRegion) {
248 return ShortNumberCost.PREMIUM_RATE;
250 cost = ShortNumberCost.UNKNOWN_COST;
253 if (cost != ShortNumberCost.UNKNOWN_COST) {
254 cost = ShortNumberCost.STANDARD_RATE;
261 logger.log(Level.SEVERE, "Unrecognised cost for region: " + costForRegion);
267 // Helper method to get the region code for a given phone number, from a list of possible region
268 // codes. If the list contains more than one region, the first region for which the number is
269 // valid is returned.
270 private String getRegionCodeForShortNumberFromRegionList(PhoneNumber number,
271 List<String> regionCodes) {
272 if (regionCodes.size() == 0) {
274 } else if (regionCodes.size() == 1) {
275 return regionCodes.get(0);
277 String nationalNumber = phoneUtil.getNationalSignificantNumber(number);
278 for (String regionCode : regionCodes) {
279 PhoneMetadata phoneMetadata = MetadataManager.getShortNumberMetadataForRegion(regionCode);
280 if (phoneMetadata != null &&
281 phoneUtil.isNumberMatchingDesc(nationalNumber, phoneMetadata.getShortCode())) {
282 // The number is valid for this region.
290 * Convenience method to get a list of what regions the library has metadata for.
292 Set<String> getSupportedRegions() {
293 return Collections.unmodifiableSet(MetadataManager.getShortNumberMetadataSupportedRegions());
297 * Gets a valid short number for the specified region.
299 * @param regionCode the region for which an example short number is needed
300 * @return a valid short number for the specified region. Returns an empty string when the
301 * metadata does not contain such information.
303 // @VisibleForTesting
304 String getExampleShortNumber(String regionCode) {
305 PhoneMetadata phoneMetadata = MetadataManager.getShortNumberMetadataForRegion(regionCode);
306 if (phoneMetadata == null) {
309 PhoneNumberDesc desc = phoneMetadata.getShortCode();
310 if (desc.hasExampleNumber()) {
311 return desc.getExampleNumber();
317 * Gets a valid short number for the specified cost category.
319 * @param regionCode the region for which an example short number is needed
320 * @param cost the cost category of number that is needed
321 * @return a valid short number for the specified region and cost category. Returns an empty
322 * string when the metadata does not contain such information, or the cost is UNKNOWN_COST.
324 // @VisibleForTesting
325 String getExampleShortNumberForCost(String regionCode, ShortNumberCost cost) {
326 PhoneMetadata phoneMetadata = MetadataManager.getShortNumberMetadataForRegion(regionCode);
327 if (phoneMetadata == null) {
330 PhoneNumberDesc desc = null;
333 desc = phoneMetadata.getTollFree();
336 desc = phoneMetadata.getStandardRate();
339 desc = phoneMetadata.getPremiumRate();
342 // UNKNOWN_COST numbers are computed by the process of elimination from the other cost
345 if (desc != null && desc.hasExampleNumber()) {
346 return desc.getExampleNumber();
352 * Returns true if the number might be used to connect to an emergency service in the given
355 * This method takes into account cases where the number might contain formatting, or might have
356 * additional digits appended (when it is okay to do that in the region specified).
358 * @param number the phone number to test
359 * @param regionCode the region where the phone number is being dialed
360 * @return whether the number might be used to connect to an emergency service in the given region
362 public boolean connectsToEmergencyNumber(String number, String regionCode) {
363 return matchesEmergencyNumberHelper(number, regionCode, true /* allows prefix match */);
367 * Returns true if the number exactly matches an emergency service number in the given region.
369 * This method takes into account cases where the number might contain formatting, but doesn't
370 * allow additional digits to be appended.
372 * @param number the phone number to test
373 * @param regionCode the region where the phone number is being dialed
374 * @return whether the number exactly matches an emergency services number in the given region
376 public boolean isEmergencyNumber(String number, String regionCode) {
377 return matchesEmergencyNumberHelper(number, regionCode, false /* doesn't allow prefix match */);
380 private boolean matchesEmergencyNumberHelper(String number, String regionCode,
381 boolean allowPrefixMatch) {
382 number = PhoneNumberUtil.extractPossibleNumber(number);
383 if (PhoneNumberUtil.PLUS_CHARS_PATTERN.matcher(number).lookingAt()) {
384 // Returns false if the number starts with a plus sign. We don't believe dialing the country
385 // code before emergency numbers (e.g. +1911) works, but later, if that proves to work, we can
386 // add additional logic here to handle it.
389 PhoneMetadata metadata = MetadataManager.getShortNumberMetadataForRegion(regionCode);
390 if (metadata == null || !metadata.hasEmergency()) {
393 Pattern emergencyNumberPattern =
394 Pattern.compile(metadata.getEmergency().getNationalNumberPattern());
395 String normalizedNumber = PhoneNumberUtil.normalizeDigitsOnly(number);
396 return (!allowPrefixMatch || REGIONS_WHERE_EMERGENCY_NUMBERS_MUST_BE_EXACT.contains(regionCode))
397 ? emergencyNumberPattern.matcher(normalizedNumber).matches()
398 : emergencyNumberPattern.matcher(normalizedNumber).lookingAt();
402 * Given a valid short number, determines whether it is carrier-specific (however, nothing is
403 * implied about its validity). If it is important that the number is valid, then its validity
404 * must first be checked using {@link #isValidShortNumber} or
405 * {@link #isValidShortNumberForRegion}.
407 * @param number the valid short number to check
408 * @return whether the short number is carrier-specific (assuming the input was a valid short
411 public boolean isCarrierSpecific(PhoneNumber number) {
412 List<String> regionCodes = phoneUtil.getRegionCodesForCountryCode(number.getCountryCode());
413 String regionCode = getRegionCodeForShortNumberFromRegionList(number, regionCodes);
414 String nationalNumber = phoneUtil.getNationalSignificantNumber(number);
415 PhoneMetadata phoneMetadata = MetadataManager.getShortNumberMetadataForRegion(regionCode);
416 return (phoneMetadata != null) &&
417 (phoneUtil.isNumberMatchingDesc(nationalNumber, phoneMetadata.getCarrierSpecific()));