NBPhoneNumberUtil+ShortNumber.m 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326
  1. //
  2. // NBPhoneNumberUtil+ShortNumber.m
  3. // libPhoneNumberiOS
  4. //
  5. // Created by Paween Itthipalkul on 11/29/17.
  6. // Copyright © 2017 Google LLC. All rights reserved.
  7. //
  8. #import "NBPhoneNumberUtil+ShortNumber.h"
  9. #import <Foundation/Foundation.h>
  10. #import "NBMetadataHelper.h"
  11. #import "NBPhoneMetaData.h"
  12. #import "NBPhoneNumber.h"
  13. #import "NBPhoneNumberDesc.h"
  14. #import "NBRegExMatcher.h"
  15. #import "NBRegularExpressionCache.h"
  16. #if SHORT_NUMBER_SUPPORT
  17. static NSString * const PLUS_CHARS_PATTERN = @"[+\uFF0B]+";
  18. @interface NBPhoneNumberUtil()
  19. @property(nonatomic, strong, readonly) NBMetadataHelper *helper;
  20. @property(nonatomic, strong, readonly) NBRegExMatcher *matcher;
  21. @property (nonatomic) NSDictionary<NSNumber *, NSArray<NSString *> *> *countryToRegionCodeMap;
  22. @end
  23. @implementation NBPhoneNumberUtil (ShortNumber)
  24. - (BOOL)isPossibleShortNumber:(NBPhoneNumber *)phoneNumber forRegion:(NSString *)regionDialingFrom {
  25. if (![self doesPhoneNumber:phoneNumber matchesRegion:regionDialingFrom]) {
  26. return NO;
  27. }
  28. NBPhoneMetaData *metadata = [self.helper shortNumberMetadataForRegion:regionDialingFrom];
  29. if (metadata == nil) {
  30. return NO;
  31. }
  32. NSUInteger length = [[self getNationalSignificantNumber:phoneNumber] length];
  33. return [metadata.generalDesc.possibleLength containsObject:@(length)];
  34. }
  35. - (BOOL)isPossibleShortNumber:(NBPhoneNumber *)phoneNumber {
  36. NSArray<NSString *> *regionCodes = [self getRegionCodesForCountryCode:phoneNumber.countryCode];
  37. NSUInteger shortNumberLength = [[self getNationalSignificantNumber:phoneNumber] length];
  38. for (NSString *region in regionCodes) {
  39. NBPhoneMetaData *metadata = [self.helper shortNumberMetadataForRegion:region];
  40. if (metadata == nil) {
  41. continue;
  42. }
  43. if ([metadata.generalDesc.possibleLength containsObject:@(shortNumberLength)]) {
  44. return YES;
  45. }
  46. }
  47. return NO;
  48. }
  49. - (BOOL)isValidShortNumber:(NBPhoneNumber *)phoneNumber
  50. forRegion:(NSString *)regionDialingFrom {
  51. if (![self doesPhoneNumber:phoneNumber matchesRegion:regionDialingFrom]) {
  52. return NO;
  53. }
  54. NBPhoneMetaData *metadata = [self.helper shortNumberMetadataForRegion:regionDialingFrom];
  55. if (metadata == nil) {
  56. return NO;
  57. }
  58. NSString *shortNumber = [self getNationalSignificantNumber:phoneNumber];
  59. NBPhoneNumberDesc *generalDesc = metadata.generalDesc;
  60. if (![self matchesPossibleNumber:shortNumber andNationalNumber:generalDesc]) {
  61. return NO;
  62. }
  63. NBPhoneNumberDesc *shortNumberDesc = metadata.shortCode;
  64. return [self matchesPossibleNumber:shortNumber andNationalNumber:shortNumberDesc];
  65. }
  66. - (BOOL)isValidShortNumber:(NBPhoneNumber *)phoneNumber {
  67. NSArray<NSString *> *regionCodes = [self getRegionCodesForCountryCode:phoneNumber.countryCode];
  68. NSString *regionCode = [self regionCodeForShortNumber:phoneNumber fromRegionList:regionCodes];
  69. if (regionCodes.count > 1 && regionCode != nil) {
  70. // If a matching region had been found for the phone number from among two or more regions,
  71. // then we have already implicitly verified its validity for that region.
  72. return YES;
  73. }
  74. return [self isValidShortNumber:phoneNumber forRegion:regionCode];
  75. }
  76. - (NBEShortNumberCost)expectedCostOfPhoneNumber:(NBPhoneNumber *)phoneNumber
  77. forRegion:(NSString *)regionDialingFrom {
  78. if (![self doesRegionDialingFrom:regionDialingFrom matchesPhoneNumber:phoneNumber]) {
  79. return NBEShortNumberCostUnknown;
  80. }
  81. NBPhoneMetaData *metadata = [self.helper shortNumberMetadataForRegion:regionDialingFrom];
  82. if (metadata == nil) {
  83. return NBEShortNumberCostUnknown;
  84. }
  85. NSString *shortNumber = [self getNationalSignificantNumber:phoneNumber];
  86. // The possible lengths are not present for a particular sub-type if they match the general
  87. // description; for this reason, we check the possible lengths against the general description
  88. // first to allow an early exit if possible.
  89. if (![metadata.generalDesc.possibleLength containsObject:@(shortNumber.length)]) {
  90. return NBEShortNumberCostUnknown;
  91. }
  92. // The cost categories are tested in order of decreasing expense, since if for some reason the
  93. // patterns overlap the most expensive matching cost category should be returned.
  94. if ([self matchesPossibleNumber:shortNumber andNationalNumber:metadata.premiumRate]) {
  95. return NBEShortNumberCostPremiumRate;
  96. } else if ([self matchesPossibleNumber:shortNumber andNationalNumber:metadata.standardRate]) {
  97. return NBEShortNumberCostStandardRate;
  98. } else if ([self matchesPossibleNumber:shortNumber andNationalNumber:metadata.tollFree]) {
  99. return NBEShortNumberCostTollFree;
  100. }
  101. if ([self isEmergencyNumber:shortNumber forRegion:regionDialingFrom]) {
  102. // Emergency numbers are implicitly toll-free.
  103. return NBEShortNumberCostTollFree;
  104. }
  105. return NBEShortNumberCostUnknown;
  106. }
  107. - (NBEShortNumberCost)expectedCostOfPhoneNumber:(NBPhoneNumber *)phoneNumber {
  108. NSArray<NSString *> *regionCodes = [self getRegionCodesForCountryCode:phoneNumber.countryCode];
  109. if (regionCodes.count == 0) {
  110. return NBEShortNumberCostUnknown;
  111. }
  112. if (regionCodes.count == 1) {
  113. return [self expectedCostOfPhoneNumber:phoneNumber forRegion:regionCodes[0]];
  114. }
  115. NBEShortNumberCost cost = NBEShortNumberCostTollFree;
  116. for (NSString *regionCode in regionCodes) {
  117. NBEShortNumberCost costForRegion = [self expectedCostOfPhoneNumber:phoneNumber
  118. forRegion:regionCode];
  119. switch (costForRegion) {
  120. case NBEShortNumberCostPremiumRate:
  121. return NBEShortNumberCostPremiumRate;
  122. case NBEShortNumberCostUnknown:
  123. cost = NBEShortNumberCostUnknown;
  124. break;
  125. case NBEShortNumberCostStandardRate:
  126. if (cost != NBEShortNumberCostUnknown) {
  127. cost = NBEShortNumberCostStandardRate;
  128. }
  129. break;
  130. case NBEShortNumberCostTollFree:
  131. // Do nothing.
  132. break;
  133. }
  134. }
  135. return cost;
  136. }
  137. - (BOOL)isPhoneNumberCarrierSpecific:(NBPhoneNumber *)phoneNumber {
  138. NSArray<NSString *> *regionCodes = [self getRegionCodesForCountryCode:phoneNumber.countryCode];
  139. NSString *regionCode = [self regionCodeForShortNumber:phoneNumber fromRegionList:regionCodes];
  140. NSString *nationalNumber = [self getNationalSignificantNumber:phoneNumber];
  141. NBPhoneMetaData *metadata = [self.helper shortNumberMetadataForRegion:regionCode];
  142. return (metadata != nil &&
  143. ([self matchesPossibleNumber:nationalNumber andNationalNumber:metadata.carrierSpecific]));
  144. }
  145. - (BOOL)isPhoneNumberCarrierSpecific:(NBPhoneNumber *)phoneNumber forRegion:(NSString *)regionCode {
  146. if (![self doesRegionDialingFrom:regionCode matchesPhoneNumber:phoneNumber]) {
  147. return NO;
  148. }
  149. NSString *nationalNumber = [self getNationalSignificantNumber:phoneNumber];
  150. NBPhoneMetaData *metadata = [self.helper shortNumberMetadataForRegion:regionCode];
  151. return (metadata != nil
  152. && ([self matchesPossibleNumber:nationalNumber andNationalNumber:metadata.carrierSpecific]));
  153. }
  154. - (BOOL)isPhoneNumberSMSService:(NBPhoneNumber *)phoneNumber forRegion:(NSString *)regionCode {
  155. if (![self doesRegionDialingFrom:regionCode matchesPhoneNumber:phoneNumber]) {
  156. return NO;
  157. }
  158. NSString *nationalNumber = [self getNationalSignificantNumber:phoneNumber];
  159. NBPhoneMetaData *metadata = [self.helper shortNumberMetadataForRegion:regionCode];
  160. return (metadata != nil
  161. && ([self matchesPossibleNumber:nationalNumber andNationalNumber:metadata.smsServices]));
  162. }
  163. - (BOOL)connectsToEmergencyNumberFromString:(NSString *)number forRegion:(NSString *)regionCode {
  164. return [self matchesEmergencyNumberHelper:number regionCode:regionCode allowsPrefixMatch:YES];
  165. }
  166. - (BOOL)isEmergencyNumber:(NSString *)number forRegion:(NSString *)regionCode {
  167. return [self matchesEmergencyNumberHelper:number regionCode:regionCode allowsPrefixMatch:NO];
  168. }
  169. // MARK: - Private
  170. // In these countries, if extra digits are added to an emergency number, it no longer connects
  171. // to the emergency service.
  172. + (NSSet<NSString *> *)regionsWhereEmergencyNumbersMustBeExact {
  173. static NSSet<NSString *> *regions;
  174. static dispatch_once_t onceToken;
  175. dispatch_once(&onceToken, ^{
  176. regions = [NSSet setWithObjects:@"BR", @"CL", @"NI", nil];
  177. });
  178. return regions;
  179. }
  180. /**
  181. * Helper method to check that the country calling code of the number matches the region it's
  182. * being dialed from.
  183. */
  184. - (BOOL)doesPhoneNumber:(NBPhoneNumber *)phoneNumber matchesRegion:(NSString *)regionCode {
  185. NSArray<NSString *> *regionCodes = [self getRegionCodesForCountryCode:phoneNumber.countryCode];
  186. return [regionCodes containsObject:regionCode];
  187. }
  188. /**
  189. * Gets the national significant number of the a phone number. Note a national significant number
  190. * doesn't contain a national prefix or any formatting.
  191. * <p>
  192. * This is a temporary duplicate of the {@code getNationalSignificantNumber} method from
  193. * {@code PhoneNumberUtil}. Ultimately a canonical static version should exist in a separate
  194. * utility class (to prevent {@code ShortNumberInfo} needing to depend on PhoneNumberUtil).
  195. *
  196. * @param number the phone number for which the national significant number is needed
  197. * @return the national significant number of the PhoneNumber object passed in
  198. */
  199. + (NSString *)nationalSignificantNumberFromPhoneNumber:(NBPhoneNumber *)phoneNumber {
  200. // If leading zero(s) have been set, we prefix this now. Note this is not a national prefix.
  201. NSMutableString *nationalNumber = [[NSMutableString alloc] init];
  202. if (phoneNumber.italianLeadingZero) {
  203. [nationalNumber appendFormat:@"%*d", [phoneNumber.numberOfLeadingZeros intValue], 0];
  204. }
  205. [nationalNumber appendString:[phoneNumber.nationalNumber stringValue]];
  206. return [nationalNumber copy];
  207. }
  208. - (BOOL)matchesPossibleNumber:(NSString *)number andNationalNumber:(NBPhoneNumberDesc *)numberDesc {
  209. if (numberDesc.possibleLength.count > 0
  210. && ![numberDesc.possibleLength containsObject:@(number.length)]) {
  211. return NO;
  212. }
  213. return [self.matcher matchNationalNumber:number phoneNumberDesc:numberDesc allowsPrefixMatch:NO];
  214. }
  215. // Helper method to get the region code for a given phone number, from a list of possible region
  216. // codes. If the list contains more than one region, the first region for which the number is
  217. // valid is returned.
  218. - (NSString *)regionCodeForShortNumber:(NBPhoneNumber *)number
  219. fromRegionList:(NSArray<NSString *> *)regionCodes {
  220. if (regionCodes.count == 0) {
  221. return nil;
  222. } else if (regionCodes.count == 1) {
  223. return regionCodes[0];
  224. }
  225. NSString *nationalNumber = [self getNationalSignificantNumber:number];
  226. for (NSString *regionCode in regionCodes) {
  227. NBPhoneMetaData *metadata = [self.helper shortNumberMetadataForRegion:regionCode];
  228. if (metadata != nil && [self matchesPossibleNumber:nationalNumber
  229. andNationalNumber:metadata.shortCode]) {
  230. // The number is valid for this region.
  231. return regionCode;
  232. }
  233. }
  234. return nil;
  235. }
  236. - (BOOL)doesRegionDialingFrom:(NSString *)regionCode
  237. matchesPhoneNumber:(NBPhoneNumber *)phoneNumber {
  238. NSArray<NSString *> *regionCodes = [self getRegionCodesForCountryCode:phoneNumber.countryCode];
  239. return [regionCodes containsObject:regionCode];
  240. }
  241. - (BOOL)matchesEmergencyNumberHelper:(NSString *)number regionCode:(NSString *)regionCode
  242. allowsPrefixMatch:(BOOL)allowsPrefixMatch {
  243. NSString *possibleNumber = [self extractPossibleNumber:number];
  244. NSRegularExpression *regex =
  245. [[NBRegularExpressionCache sharedInstance] regularExpressionForPattern:PLUS_CHARS_PATTERN
  246. error:NULL];
  247. NSTextCheckingResult *result = [regex firstMatchInString:possibleNumber
  248. options:kNilOptions
  249. range:NSMakeRange(0, possibleNumber.length)];
  250. if (result != nil) {
  251. // Returns false if the number starts with a plus sign. We don't believe dialing the country
  252. // code before emergency numbers (e.g. +1911) works, but later, if that proves to work, we can
  253. // add additional logic here to handle it.
  254. return NO;
  255. }
  256. NBPhoneMetaData *metadata = [self.helper shortNumberMetadataForRegion:regionCode];
  257. if (metadata == nil || metadata.emergency == nil) {
  258. return NO;
  259. }
  260. NSString *normalizedNumber = [self normalizeDigitsOnly:possibleNumber];
  261. NSSet<NSString *> *exactRegions = [NBPhoneNumberUtil regionsWhereEmergencyNumbersMustBeExact];
  262. BOOL allowsPrefixMatchForRegion = allowsPrefixMatch && ![exactRegions containsObject:regionCode];
  263. return [self.matcher matchNationalNumber:normalizedNumber
  264. phoneNumberDesc:metadata.emergency
  265. allowsPrefixMatch:allowsPrefixMatchForRegion];
  266. }
  267. @end
  268. #endif // SHORT_NUMBER_SUPPORT