Java: Fix to Javadoc comments.
[platform/upstream/libphonenumber.git] / java / libphonenumber / src / com / google / i18n / phonenumbers / ShortNumberInfo.java
1 /*
2  * Copyright (C) 2013 The Libphonenumber Authors
3  *
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
7  *
8  * http://www.apache.org/licenses/LICENSE-2.0
9  *
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.
15  */
16
17 package com.google.i18n.phonenumbers;
18
19 import com.google.i18n.phonenumbers.Phonemetadata.PhoneMetadata;
20 import com.google.i18n.phonenumbers.Phonemetadata.PhoneNumberDesc;
21 import com.google.i18n.phonenumbers.Phonenumber.PhoneNumber;
22
23 import java.util.Collections;
24 import java.util.HashSet;
25 import java.util.List;
26 import java.util.Set;
27 import java.util.logging.Level;
28 import java.util.logging.Logger;
29 import java.util.regex.Pattern;
30
31 /**
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}.
35  *
36  * @author Shaopeng Jia
37  * @author David Yonge-Mallo
38  */
39 public class ShortNumberInfo {
40   private static final Logger logger = Logger.getLogger(ShortNumberInfo.class.getName());
41
42   private static final ShortNumberInfo INSTANCE =
43       new ShortNumberInfo(PhoneNumberUtil.getInstance());
44
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>();
49   static {
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");
53   }
54
55   /** Cost categories of short numbers. */
56   public enum ShortNumberCost {
57     TOLL_FREE,
58     STANDARD_RATE,
59     PREMIUM_RATE,
60     UNKNOWN_COST
61   }
62
63   /** Returns the singleton instance of the ShortNumberInfo. */
64   public static ShortNumberInfo getInstance() {
65     return INSTANCE;
66   }
67
68   private final PhoneNumberUtil phoneUtil;
69
70   // @VisibleForTesting
71   ShortNumberInfo(PhoneNumberUtil util) {
72     phoneUtil = util;
73   }
74
75   /**
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}.
79    *
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
83    */
84   public boolean isPossibleShortNumberForRegion(String shortNumber, String regionDialingFrom) {
85     PhoneMetadata phoneMetadata =
86         MetadataManager.getShortNumberMetadataForRegion(regionDialingFrom);
87     if (phoneMetadata == null) {
88       return false;
89     }
90     PhoneNumberDesc generalDesc = phoneMetadata.getGeneralDesc();
91     return phoneUtil.isNumberPossibleForDesc(shortNumber, generalDesc);
92   }
93
94   /**
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.
99    *
100    * @param number the short number to check
101    * @return whether the number is a possible short number
102    */
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())) {
109         return true;
110       }
111     }
112     return false;
113   }
114
115   /**
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
118    * itself.
119    *
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
123    */
124   public boolean isValidShortNumberForRegion(String shortNumber, String regionDialingFrom) {
125     PhoneMetadata phoneMetadata =
126         MetadataManager.getShortNumberMetadataForRegion(regionDialingFrom);
127     if (phoneMetadata == null) {
128       return false;
129     }
130     PhoneNumberDesc generalDesc = phoneMetadata.getGeneralDesc();
131     if (!generalDesc.hasNationalNumberPattern() ||
132         !phoneUtil.isNumberMatchingDesc(shortNumber, generalDesc)) {
133       return false;
134     }
135     PhoneNumberDesc shortNumberDesc = phoneMetadata.getShortCode();
136     if (!shortNumberDesc.hasNationalNumberPattern()) {
137       logger.log(Level.WARNING, "No short code national number pattern found for region: " +
138           regionDialingFrom);
139       return false;
140     }
141     return phoneUtil.isNumberMatchingDesc(shortNumber, shortNumberDesc);
142   }
143
144   /**
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.
149    *
150    * @param number the short number for which we want to test the validity
151    * @return whether the short number matches a valid pattern
152    */
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.
160       return true;
161     }
162     return isValidShortNumberForRegion(shortNumber, regionCode);
163   }
164
165   /**
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:
170    * <pre>{@code
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,
176    *       regionCode);
177    *   // Do something with the cost information here.
178    * }}</pre>
179    *
180    * @param shortNumber the short number for which we want to know the expected cost category,
181    *     as a string
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
185    *     category.
186    */
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(
190         regionDialingFrom);
191     if (phoneMetadata == null) {
192       return ShortNumberCost.UNKNOWN_COST;
193     }
194
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;
199     }
200     if (phoneUtil.isNumberMatchingDesc(shortNumber, phoneMetadata.getStandardRate())) {
201       return ShortNumberCost.STANDARD_RATE;
202     }
203     if (phoneUtil.isNumberMatchingDesc(shortNumber, phoneMetadata.getTollFree())) {
204       return ShortNumberCost.TOLL_FREE;
205     }
206     if (isEmergencyNumber(shortNumber, regionDialingFrom)) {
207       // Emergency numbers are implicitly toll-free.
208       return ShortNumberCost.TOLL_FREE;
209     }
210     return ShortNumberCost.UNKNOWN_COST;
211   }
212
213   /**
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.
222    *
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
225    * calling code.
226    *
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.
229    *
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
233    */
234   public ShortNumberCost getExpectedCost(PhoneNumber number) {
235     List<String> regionCodes = phoneUtil.getRegionCodesForCountryCode(number.getCountryCode());
236     if (regionCodes.size() == 0) {
237       return ShortNumberCost.UNKNOWN_COST;
238     }
239     String shortNumber = phoneUtil.getNationalSignificantNumber(number);
240     if (regionCodes.size() == 1) {
241       return getExpectedCostForRegion(shortNumber, regionCodes.get(0));
242     }
243     ShortNumberCost cost = ShortNumberCost.TOLL_FREE;
244     for (String regionCode : regionCodes) {
245       ShortNumberCost costForRegion = getExpectedCostForRegion(shortNumber, regionCode);
246       switch (costForRegion) {
247         case PREMIUM_RATE:
248           return ShortNumberCost.PREMIUM_RATE;
249         case UNKNOWN_COST:
250           cost = ShortNumberCost.UNKNOWN_COST;
251           break;
252         case STANDARD_RATE:
253           if (cost != ShortNumberCost.UNKNOWN_COST) {
254             cost = ShortNumberCost.STANDARD_RATE;
255           }
256           break;
257         case TOLL_FREE:
258           // Do nothing.
259           break;
260         default:
261           logger.log(Level.SEVERE, "Unrecognised cost for region: " + costForRegion);
262       }
263     }
264     return cost;
265   }
266
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) {
273       return null;
274     } else if (regionCodes.size() == 1) {
275       return regionCodes.get(0);
276     }
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.
283         return regionCode;
284       }
285     }
286     return null;
287   }
288
289   /**
290    * Convenience method to get a list of what regions the library has metadata for.
291    */
292   Set<String> getSupportedRegions() {
293     return Collections.unmodifiableSet(MetadataManager.getShortNumberMetadataSupportedRegions());
294   }
295
296   /**
297    * Gets a valid short number for the specified region.
298    *
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.
302    */
303   // @VisibleForTesting
304   String getExampleShortNumber(String regionCode) {
305     PhoneMetadata phoneMetadata = MetadataManager.getShortNumberMetadataForRegion(regionCode);
306     if (phoneMetadata == null) {
307       return "";
308     }
309     PhoneNumberDesc desc = phoneMetadata.getShortCode();
310     if (desc.hasExampleNumber()) {
311       return desc.getExampleNumber();
312     }
313     return "";
314   }
315
316   /**
317    * Gets a valid short number for the specified cost category.
318    *
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.
323    */
324   // @VisibleForTesting
325   String getExampleShortNumberForCost(String regionCode, ShortNumberCost cost) {
326     PhoneMetadata phoneMetadata = MetadataManager.getShortNumberMetadataForRegion(regionCode);
327     if (phoneMetadata == null) {
328       return "";
329     }
330     PhoneNumberDesc desc = null;
331     switch (cost) {
332       case TOLL_FREE:
333         desc = phoneMetadata.getTollFree();
334         break;
335       case STANDARD_RATE:
336         desc = phoneMetadata.getStandardRate();
337         break;
338       case PREMIUM_RATE:
339         desc = phoneMetadata.getPremiumRate();
340         break;
341       default:
342         // UNKNOWN_COST numbers are computed by the process of elimination from the other cost
343         // categories.
344     }
345     if (desc != null && desc.hasExampleNumber()) {
346       return desc.getExampleNumber();
347     }
348     return "";
349   }
350
351   /**
352    * Returns true if the number might be used to connect to an emergency service in the given
353    * region.
354    *
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).
357    *
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
361    */
362   public boolean connectsToEmergencyNumber(String number, String regionCode) {
363     return matchesEmergencyNumberHelper(number, regionCode, true /* allows prefix match */);
364   }
365
366   /**
367    * Returns true if the number exactly matches an emergency service number in the given region.
368    *
369    * This method takes into account cases where the number might contain formatting, but doesn't
370    * allow additional digits to be appended.
371    *
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
375    */
376   public boolean isEmergencyNumber(String number, String regionCode) {
377     return matchesEmergencyNumberHelper(number, regionCode, false /* doesn't allow prefix match */);
378   }
379
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.
387       return false;
388     }
389     PhoneMetadata metadata = MetadataManager.getShortNumberMetadataForRegion(regionCode);
390     if (metadata == null || !metadata.hasEmergency()) {
391       return false;
392     }
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();
399   }
400
401   /**
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}.
406    *
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
409    *     number).
410    */
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()));
418   }
419 }