2020-02-22 15:38:54 +04:00

3908 lines
169 KiB
Objective-C
Executable File
Raw Permalink Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

//
// NBPhoneNumberUtil.m
// libPhoneNumber
//
// Created by tabby on 2015. 2. 8..
// Copyright (c) 2015년 ohtalk.me. All rights reserved.
//
#import <libphonenumber/NBPhoneNumberUtil.h>
#import <libphonenumber/NBPhoneNumberDefines.h>
#import <libphonenumber/NBPhoneNumber.h>
#import "NBNumberFormat.h"
#import "NBPhoneNumberDesc.h"
#import "NBPhoneMetaData.h"
#import "NBMetadataHelper.h"
#import <math.h>
#if TARGET_OS_IPHONE && !TARGET_OS_WATCH
#import <CoreTelephony/CTTelephonyNetworkInfo.h>
#import <CoreTelephony/CTCarrier.h>
#endif
#pragma mark - NBPhoneNumberUtil interface -
@interface NBPhoneNumberUtil ()
@property (nonatomic, strong) NSLock *entireStringCacheLock;
@property (nonatomic, strong) NSMutableDictionary *entireStringRegexCache;
@property (nonatomic, strong) NSLock *lockPatternCache;
@property (nonatomic, strong) NSMutableDictionary *regexPatternCache;
@property (nonatomic, strong, readwrite) NSMutableDictionary *i18nNumberFormat;
@property (nonatomic, strong, readwrite) NSMutableDictionary *i18nPhoneNumberDesc;
@property (nonatomic, strong, readwrite) NSMutableDictionary *i18nPhoneMetadata;
@property (nonatomic, strong) NSRegularExpression *PLUS_CHARS_PATTERN;
@property (nonatomic, strong) NSRegularExpression *CAPTURING_DIGIT_PATTERN;
@property (nonatomic, strong) NSRegularExpression *VALID_ALPHA_PHONE_PATTERN;
#if TARGET_OS_IPHONE && !TARGET_OS_WATCH
@property (nonatomic, readonly) CTTelephonyNetworkInfo *telephonyNetworkInfo;
#endif
@end
@implementation NBPhoneNumberUtil
#pragma mark - Static Int variables -
const static NSUInteger NANPA_COUNTRY_CODE_ = 1;
const static int MIN_LENGTH_FOR_NSN_ = 2;
const static int MAX_LENGTH_FOR_NSN_ = 16;
const static int MAX_LENGTH_COUNTRY_CODE_ = 3;
const static int MAX_INPUT_STRING_LENGTH_ = 250;
#pragma mark - Static String variables -
static NSString *VALID_PUNCTUATION = @"-x-―−ー-- ­ ().\\[\\]/~";
static NSString *INVALID_COUNTRY_CODE_STR = @"Invalid country calling code";
static NSString *NOT_A_NUMBER_STR = @"The string supplied did not seem to be a phone number";
static NSString *TOO_SHORT_AFTER_IDD_STR = @"Phone number too short after IDD";
static NSString *TOO_SHORT_NSN_STR = @"The string supplied is too short to be a phone number";
static NSString *TOO_LONG_STR = @"The string supplied is too long to be a phone number";
static NSString *COLOMBIA_MOBILE_TO_FIXED_LINE_PREFIX = @"3";
static NSString *PLUS_SIGN = @"+";
static NSString *STAR_SIGN = @"*";
static NSString *RFC3966_EXTN_PREFIX = @";ext=";
static NSString *RFC3966_PREFIX = @"tel:";
static NSString *RFC3966_PHONE_CONTEXT = @";phone-context=";
static NSString *RFC3966_ISDN_SUBADDRESS = @";isub=";
static NSString *DEFAULT_EXTN_PREFIX = @" ext. ";
static NSString *VALID_ALPHA = @"A-Za-z";
#pragma mark - Static regular expression strings -
static NSString *NON_DIGITS_PATTERN = @"\\D+";
static NSString *CC_PATTERN = @"\\$CC";
static NSString *FIRST_GROUP_PATTERN = @"(\\$\\d)";
static NSString *FIRST_GROUP_ONLY_PREFIX_PATTERN = @"^\\(?\\$1\\)?";
static NSString *NP_PATTERN = @"\\$NP";
static NSString *FG_PATTERN = @"\\$FG";
static NSString *VALID_ALPHA_PHONE_PATTERN_STRING = @"(?:.*?[A-Za-z]){3}.*";
static NSString *UNIQUE_INTERNATIONAL_PREFIX = @"[\\d]+(?:[~\\u2053\\u223C\\uFF5E][\\d]+)?";
static NSString *LEADING_PLUS_CHARS_PATTERN;
static NSString *EXTN_PATTERN;
static NSString *SEPARATOR_PATTERN;
static NSString *VALID_PHONE_NUMBER_PATTERN;
static NSString *VALID_START_CHAR_PATTERN;
static NSString *UNWANTED_END_CHAR_PATTERN;
static NSString *SECOND_NUMBER_START_PATTERN;
static NSDictionary *ALPHA_MAPPINGS;
static NSDictionary *ALL_NORMALIZATION_MAPPINGS;
static NSDictionary *DIALLABLE_CHAR_MAPPINGS;
static NSDictionary *ALL_PLUS_NUMBER_GROUPING_SYMBOLS;
static NSDictionary *DIGIT_MAPPINGS;
static NSArray *GEO_MOBILE_COUNTRIES;
#pragma mark - Deprecated methods
+ (NBPhoneNumberUtil *)sharedInstance
{
static NBPhoneNumberUtil *sharedOnceInstance = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{ sharedOnceInstance = [[self alloc] init]; });
return sharedOnceInstance;
}
#pragma mark - NSError
- (NSError*)errorWithObject:(id)obj withDomain:(NSString *)domain
{
NSDictionary *userInfo = [NSDictionary dictionaryWithObject:obj forKey:NSLocalizedDescriptionKey];
NSError *error = [NSError errorWithDomain:domain code:0 userInfo:userInfo];
return error;
}
- (NSRegularExpression *)entireRegularExpressionWithPattern:(NSString *)regexPattern
options:(NSRegularExpressionOptions)options
error:(NSError **)error
{
[_entireStringCacheLock lock];
@try {
if (!_entireStringRegexCache) {
_entireStringRegexCache = [[NSMutableDictionary alloc] init];
}
NSRegularExpression *regex = [_entireStringRegexCache objectForKey:regexPattern];
if (! regex)
{
NSString *finalRegexString = regexPattern;
if ([regexPattern rangeOfString:@"^"].location == NSNotFound) {
finalRegexString = [NSString stringWithFormat:@"^(?:%@)$", regexPattern];
}
regex = [self regularExpressionWithPattern:finalRegexString options:0 error:error];
[_entireStringRegexCache setObject:regex forKey:regexPattern];
}
return regex;
}
@finally {
[_entireStringCacheLock unlock];
}
}
- (NSRegularExpression *)regularExpressionWithPattern:(NSString *)pattern options:(NSRegularExpressionOptions)options error:(NSError **)error
{
[_lockPatternCache lock];
@try {
if (!_regexPatternCache) {
_regexPatternCache = [[NSMutableDictionary alloc] init];
}
NSRegularExpression *regex = [_regexPatternCache objectForKey:pattern];
if (!regex) {
regex = [NSRegularExpression regularExpressionWithPattern:pattern options:options error:error];
[_regexPatternCache setObject:regex forKey:pattern];
}
return regex;
}
@finally {
[_lockPatternCache unlock];
}
}
- (NSMutableArray*)componentsSeparatedByRegex:(NSString *)sourceString regex:(NSString *)pattern
{
NSString *replacedString = [self replaceStringByRegex:sourceString regex:pattern withTemplate:@"<SEP>"];
NSMutableArray *resArray = [[replacedString componentsSeparatedByString:@"<SEP>"] mutableCopy];
[resArray removeObject:@""];
return resArray;
}
- (int)stringPositionByRegex:(NSString *)sourceString regex:(NSString *)pattern
{
if (sourceString == nil || sourceString.length <= 0 || pattern == nil || pattern.length <= 0) {
return -1;
}
NSError *error = nil;
NSRegularExpression *currentPattern = [self regularExpressionWithPattern:pattern options:0 error:&error];
NSArray *matches = [currentPattern matchesInString:sourceString options:0 range:NSMakeRange(0, sourceString.length)];
int foundPosition = -1;
if (matches.count > 0) {
NSTextCheckingResult *match = [matches objectAtIndex:0];
return (int)match.range.location;
}
return foundPosition;
}
- (int)indexOfStringByString:(NSString *)sourceString target:(NSString *)targetString
{
NSRange finded = [sourceString rangeOfString:targetString];
if (finded.location != NSNotFound) {
return (int)finded.location;
}
return -1;
}
- (NSString *)replaceFirstStringByRegex:(NSString *)sourceString regex:(NSString *)pattern withTemplate:(NSString *)templateString
{
NSString *replacementResult = [sourceString copy];
NSError *error = nil;
NSRegularExpression *currentPattern = [self regularExpressionWithPattern:pattern options:0 error:&error];
NSRange replaceRange = [currentPattern rangeOfFirstMatchInString:sourceString options:0 range:NSMakeRange(0, sourceString.length)];
if (replaceRange.location != NSNotFound) {
replacementResult = [currentPattern stringByReplacingMatchesInString:[sourceString mutableCopy] options:0
range:replaceRange
withTemplate:templateString];
}
return replacementResult;
}
- (NSString *)replaceStringByRegex:(NSString *)sourceString regex:(NSString *)pattern withTemplate:(NSString *)templateString
{
NSString *replacementResult = [sourceString copy];
NSError *error = nil;
NSRegularExpression *currentPattern = [self regularExpressionWithPattern:pattern options:0 error:&error];
NSArray *matches = [currentPattern matchesInString:sourceString options:0 range:NSMakeRange(0, sourceString.length)];
if ([matches count] == 1) {
NSRange replaceRange = [currentPattern rangeOfFirstMatchInString:sourceString options:0 range:NSMakeRange(0, sourceString.length)];
if (replaceRange.location != NSNotFound) {
replacementResult = [currentPattern stringByReplacingMatchesInString:[sourceString mutableCopy] options:0
range:replaceRange
withTemplate:templateString];
}
return replacementResult;
}
if ([matches count] > 1) {
replacementResult = [currentPattern stringByReplacingMatchesInString:[replacementResult mutableCopy] options:0
range:NSMakeRange(0, sourceString.length) withTemplate:templateString];
return replacementResult;
}
return replacementResult;
}
- (NSTextCheckingResult*)matcheFirstByRegex:(NSString *)sourceString regex:(NSString *)pattern
{
NSError *error = nil;
NSRegularExpression *currentPattern = [self regularExpressionWithPattern:pattern options:0 error:&error];
NSArray *matches = [currentPattern matchesInString:sourceString options:0 range:NSMakeRange(0, sourceString.length)];
if ([matches count] > 0)
return [matches objectAtIndex:0];
return nil;
}
- (NSArray*)matchesByRegex:(NSString *)sourceString regex:(NSString *)pattern
{
NSError *error = nil;
NSRegularExpression *currentPattern = [self regularExpressionWithPattern:pattern options:0 error:&error];
NSArray *matches = [currentPattern matchesInString:sourceString options:0 range:NSMakeRange(0, sourceString.length)];
return matches;
}
- (NSArray*)matchedStringByRegex:(NSString *)sourceString regex:(NSString *)pattern
{
NSArray *matches = [self matchesByRegex:sourceString regex:pattern];
NSMutableArray *matchString = [[NSMutableArray alloc] init];
for (NSTextCheckingResult *match in matches) {
NSString *curString = [sourceString substringWithRange:match.range];
[matchString addObject:curString];
}
return matchString;
}
- (BOOL)isStartingStringByRegex:(NSString *)sourceString regex:(NSString *)pattern
{
NSError *error = nil;
NSRegularExpression *currentPattern = [self regularExpressionWithPattern:pattern options:0 error:&error];
NSArray *matches = [currentPattern matchesInString:sourceString options:0 range:NSMakeRange(0, sourceString.length)];
for (NSTextCheckingResult *match in matches) {
if (match.range.location == 0) {
return YES;
}
}
return NO;
}
- (NSString *)stringByReplacingOccurrencesString:(NSString *)sourceString withMap:(NSDictionary *)dicMap removeNonMatches:(BOOL)bRemove
{
NSMutableString *targetString = [[NSMutableString alloc] initWithString:@""];
for(unsigned int i=0; i<sourceString.length; i++)
{
unichar oneChar = [sourceString characterAtIndex:i];
NSString *keyString = [NSString stringWithCharacters:&oneChar length:1];
NSString *mappedValue = [dicMap objectForKey:keyString];
if (mappedValue != nil) {
[targetString appendString:mappedValue];
} else {
if (bRemove == NO) {
[targetString appendString:keyString];
}
}
}
return targetString;
}
- (BOOL)isAllDigits:(NSString *)sourceString
{
NSCharacterSet *nonNumbers = [[NSCharacterSet decimalDigitCharacterSet] invertedSet];
NSRange r = [sourceString rangeOfCharacterFromSet:nonNumbers];
return r.location == NSNotFound;
}
- (BOOL)isNumeric:(NSString *)sourceString
{
NSScanner *sc = [NSScanner scannerWithString:sourceString];
if ([sc scanFloat:NULL]) {
return [sc isAtEnd];
}
return NO;
}
- (BOOL)isNaN:(NSString *)sourceString
{
if ([self isNumeric:sourceString]) return NO;
return YES;
}
- (NSString *)getNationalSignificantNumber:(NBPhoneNumber *)phoneNumber
{
if (phoneNumber.italianLeadingZero) {
return [NSString stringWithFormat:@"0%@", phoneNumber.nationalNumber];
}
return [phoneNumber.nationalNumber stringValue];
}
#pragma mark - Initializations -
- (id)init
{
self = [super init];
if (self)
{
_lockPatternCache = [[NSLock alloc] init];
_entireStringCacheLock = [[NSLock alloc] init];
/**
* Set of country calling codes that have geographically assigned mobile
* numbers. This may not be complete; we add calling codes case by case, as we
* find geographical mobile numbers or hear from user reports.
*
* @const
* @type {!Array.<number>}
* @private
*/
// @[ Mexico, Argentina, Brazil ]
GEO_MOBILE_COUNTRIES = @[ @52, @54, @55 ];
[self initRegularExpressionSet];
[self initNormalizationMappings];
}
return self;
}
- (void)initRegularExpressionSet
{
NSString *EXTN_PATTERNS_FOR_PARSING = @"(?:;ext=([0-9-٠-٩۰-۹]{1,7})|[ \\t,]*(?:e?xt(?:ensi(?:ó?|ó))?n?|??|[,xX#~]|int|anexo|)[:\\.]?[ \\t,-]*([0-9-٠-٩۰-۹]{1,7})#?|[- ]+([0-9-٠-٩۰-۹]{1,5})#)$";
NSError *error = nil;
if (!_PLUS_CHARS_PATTERN) {
_PLUS_CHARS_PATTERN = [self regularExpressionWithPattern:[NSString stringWithFormat:@"[%@]+", NB_PLUS_CHARS] options:0 error:&error];
}
if (!LEADING_PLUS_CHARS_PATTERN) {
LEADING_PLUS_CHARS_PATTERN = [NSString stringWithFormat:@"^[%@]+", NB_PLUS_CHARS];
}
if (!_CAPTURING_DIGIT_PATTERN) {
_CAPTURING_DIGIT_PATTERN = [self regularExpressionWithPattern:[NSString stringWithFormat:@"([%@])", NB_VALID_DIGITS_STRING] options:0 error:&error];
}
if (!VALID_START_CHAR_PATTERN) {
VALID_START_CHAR_PATTERN = [NSString stringWithFormat:@"[%@%@]", NB_PLUS_CHARS, NB_VALID_DIGITS_STRING];
}
if (!SECOND_NUMBER_START_PATTERN) {
SECOND_NUMBER_START_PATTERN = @"[\\\\\\/] *x";
}
if (!_VALID_ALPHA_PHONE_PATTERN) {
_VALID_ALPHA_PHONE_PATTERN = [self regularExpressionWithPattern:VALID_ALPHA_PHONE_PATTERN_STRING options:0 error:&error];
}
if (!UNWANTED_END_CHAR_PATTERN) {
UNWANTED_END_CHAR_PATTERN = [NSString stringWithFormat:@"[^%@%@#]+$", NB_VALID_DIGITS_STRING, VALID_ALPHA];
}
if (!EXTN_PATTERN) {
EXTN_PATTERN = [NSString stringWithFormat:@"(?:%@)$", EXTN_PATTERNS_FOR_PARSING];
}
if (!SEPARATOR_PATTERN) {
SEPARATOR_PATTERN = [NSString stringWithFormat:@"[%@]+", VALID_PUNCTUATION];
}
if (!VALID_PHONE_NUMBER_PATTERN) {
VALID_PHONE_NUMBER_PATTERN = @"^[0-9-٠-٩۰-۹]{2}$|^[+]*(?:[-x-―−ー-- ­ ().\\[\\]/~*]*[0-9-٠-٩۰-۹]){3,}[-x-―−ー-- ­ ().\\[\\]/~*A-Za-z0-9-٠-٩۰-۹]*(?:;ext=([0-9-٠-٩۰-۹]{1,7})|[ \\t,]*(?:e?xt(?:ensi(?:ó?|ó))?n?|??|[,x#~]|int|anexo|)[:\\.]?[ \\t,-]*([0-9-٠-٩۰-۹]{1,7})#?|[- ]+([0-9-٠-٩۰-۹]{1,5})#)?$";
}
}
- (NSDictionary *)DIGIT_MAPPINGS
{
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
DIGIT_MAPPINGS = [NSDictionary dictionaryWithObjectsAndKeys:
@"0", @"0", @"1", @"1", @"2", @"2", @"3", @"3", @"4", @"4", @"5", @"5", @"6", @"6", @"7", @"7", @"8", @"8", @"9", @"9",
// Fullwidth digit 0 to 9
@"0", @"\uFF10", @"1", @"\uFF11", @"2", @"\uFF12", @"3", @"\uFF13", @"4", @"\uFF14", @"5", @"\uFF15", @"6", @"\uFF16", @"7", @"\uFF17", @"8", @"\uFF18", @"9", @"\uFF19",
// Arabic-indic digit 0 to 9
@"0", @"\u0660", @"1", @"\u0661", @"2", @"\u0662", @"3", @"\u0663", @"4", @"\u0664", @"5", @"\u0665", @"6", @"\u0666", @"7", @"\u0667", @"8", @"\u0668", @"9", @"\u0669",
// Eastern-Arabic digit 0 to 9
@"0", @"\u06F0", @"1", @"\u06F1", @"2", @"\u06F2", @"3", @"\u06F3", @"4", @"\u06F4", @"5", @"\u06F5", @"6", @"\u06F6", @"7", @"\u06F7", @"8", @"\u06F8", @"9", @"\u06F9", nil];
});
return DIGIT_MAPPINGS;
}
- (void)initNormalizationMappings
{
if (!DIALLABLE_CHAR_MAPPINGS) {
DIALLABLE_CHAR_MAPPINGS = [NSDictionary dictionaryWithObjectsAndKeys:
@"0", @"0", @"1", @"1", @"2", @"2", @"3", @"3", @"4", @"4", @"5", @"5", @"6", @"6", @"7", @"7", @"8", @"8", @"9", @"9",
@"+", @"+", @"*", @"*", nil];
}
if (!ALPHA_MAPPINGS) {
ALPHA_MAPPINGS = [NSDictionary dictionaryWithObjectsAndKeys:
@"2", @"A", @"2", @"B", @"2", @"C", @"3", @"D", @"3", @"E", @"3", @"F", @"4", @"G", @"4", @"H", @"4", @"I", @"5", @"J",
@"5", @"K", @"5", @"L", @"6", @"M", @"6", @"N", @"6", @"O", @"7", @"P", @"7", @"Q", @"7", @"R", @"7", @"S", @"8", @"T",
@"8", @"U", @"8", @"V", @"9", @"W", @"9", @"X", @"9", @"Y", @"9", @"Z", nil];
}
if (!ALL_NORMALIZATION_MAPPINGS) {
ALL_NORMALIZATION_MAPPINGS = [NSDictionary dictionaryWithObjectsAndKeys:
@"0", @"0", @"1", @"1", @"2", @"2", @"3", @"3", @"4", @"4", @"5", @"5", @"6", @"6", @"7", @"7", @"8", @"8", @"9", @"9",
// Fullwidth digit 0 to 9
@"0", @"\uFF10", @"1", @"\uFF11", @"2", @"\uFF12", @"3", @"\uFF13", @"4", @"\uFF14", @"5", @"\uFF15", @"6", @"\uFF16", @"7", @"\uFF17", @"8", @"\uFF18", @"9", @"\uFF19",
// Arabic-indic digit 0 to 9
@"0", @"\u0660", @"1", @"\u0661", @"2", @"\u0662", @"3", @"\u0663", @"4", @"\u0664", @"5", @"\u0665", @"6", @"\u0666", @"7", @"\u0667", @"8", @"\u0668", @"9", @"\u0669",
// Eastern-Arabic digit 0 to 9
@"0", @"\u06F0", @"1", @"\u06F1", @"2", @"\u06F2", @"3", @"\u06F3", @"4", @"\u06F4", @"5", @"\u06F5", @"6", @"\u06F6", @"7", @"\u06F7", @"8", @"\u06F8", @"9", @"\u06F9",
@"2", @"A", @"2", @"B", @"2", @"C", @"3", @"D", @"3", @"E", @"3", @"F", @"4", @"G", @"4", @"H", @"4", @"I", @"5", @"J",
@"5", @"K", @"5", @"L", @"6", @"M", @"6", @"N", @"6", @"O", @"7", @"P", @"7", @"Q", @"7", @"R", @"7", @"S", @"8", @"T",
@"8", @"U", @"8", @"V", @"9", @"W", @"9", @"X", @"9", @"Y", @"9", @"Z", nil];
}
if (!ALL_PLUS_NUMBER_GROUPING_SYMBOLS) {
ALL_PLUS_NUMBER_GROUPING_SYMBOLS = [NSDictionary dictionaryWithObjectsAndKeys:
@"0", @"0", @"1", @"1", @"2", @"2", @"3", @"3", @"4", @"4", @"5", @"5", @"6", @"6", @"7", @"7", @"8", @"8", @"9", @"9",
@"A", @"A", @"B", @"B", @"C", @"C", @"D", @"D", @"E", @"E", @"F", @"F", @"G", @"G", @"H", @"H", @"I", @"I", @"J", @"J",
@"K", @"K", @"L", @"L", @"M", @"M", @"N", @"N", @"O", @"O", @"P", @"P", @"Q", @"Q", @"R", @"R", @"S", @"S", @"T", @"T",
@"U", @"U", @"V", @"V", @"W", @"W", @"X", @"X", @"Y", @"Y", @"Z", @"Z", @"A", @"a", @"B", @"b", @"C", @"c", @"D", @"d",
@"E", @"e", @"F", @"f", @"G", @"g", @"H", @"h", @"I", @"i", @"J", @"j", @"K", @"k", @"L", @"l", @"M", @"m", @"N", @"n",
@"O", @"o", @"P", @"p", @"Q", @"q", @"R", @"r", @"S", @"s", @"T", @"t", @"U", @"u", @"V", @"v", @"W", @"w", @"X", @"x",
@"Y", @"y", @"Z", @"z", @"-", @"-", @"-", @"\uFF0D", @"-", @"\u2010", @"-", @"\u2011", @"-", @"\u2012", @"-", @"\u2013", @"-", @"\u2014", @"-", @"\u2015",
@"-", @"\u2212", @"/", @"/", @"/", @"\uFF0F", @" ", @" ", @" ", @"\u3000", @" ", @"\u2060", @".", @".", @".", @"\uFF0E", nil];
}
}
#pragma mark - Metadata manager (phonenumberutil.js) functions -
/**
* Attempts to extract a possible number from the string passed in. This
* currently strips all leading characters that cannot be used to start a phone
* number. Characters that can be used to start a phone number are defined in
* the VALID_START_CHAR_PATTERN. If none of these characters are found in the
* number passed in, an empty string is returned. This function also attempts to
* strip off any alternative extensions or endings if two or more are present,
* such as in the case of: (530) 583-6985 x302/x2303. The second extension here
* makes this actually two phone numbers, (530) 583-6985 x302 and (530) 583-6985
* x2303. We remove the second extension so that the first number is parsed
* correctly.
*
* @param {string} number the string that might contain a phone number.
* @return {string} the number, stripped of any non-phone-number prefix (such as
* 'Tel:') or an empty string if no character used to start phone numbers
* (such as + or any digit) is found in the number.
*/
- (NSString *)extractPossibleNumber:(NSString *)number
{
NBMetadataHelper *helper = [[NBMetadataHelper alloc] init];
number = [helper normalizeNonBreakingSpace:number];
NSString *possibleNumber = @"";
int start = [self stringPositionByRegex:number regex:VALID_START_CHAR_PATTERN];
if (start >= 0)
{
possibleNumber = [number substringFromIndex:start];
// Remove trailing non-alpha non-numerical characters.
possibleNumber = [self replaceStringByRegex:possibleNumber regex:UNWANTED_END_CHAR_PATTERN withTemplate:@""];
// Check for extra numbers at the end.
int secondNumberStart = [self stringPositionByRegex:number regex:SECOND_NUMBER_START_PATTERN];
if (secondNumberStart > 0)
{
possibleNumber = [possibleNumber substringWithRange:NSMakeRange(0, secondNumberStart - 1)];
}
}
else
{
possibleNumber = @"";
}
return possibleNumber;
}
/**
* Checks to see if the string of characters could possibly be a phone number at
* all. At the moment, checks to see that the string begins with at least 2
* digits, ignoring any punctuation commonly found in phone numbers. This method
* does not require the number to be normalized in advance - but does assume
* that leading non-number symbols have been removed, such as by the method
* extractPossibleNumber.
*
* @param {string} number string to be checked for viability as a phone number.
* @return {boolean} NO if the number could be a phone number of some sort,
* otherwise NO.
*/
- (BOOL)isViablePhoneNumber:(NSString *)phoneNumber
{
NBMetadataHelper *helper = [[NBMetadataHelper alloc] init];
phoneNumber = [helper normalizeNonBreakingSpace:phoneNumber];
if (phoneNumber.length < MIN_LENGTH_FOR_NSN_)
{
return NO;
}
return [self matchesEntirely:VALID_PHONE_NUMBER_PATTERN string:phoneNumber];
}
/**
* Normalizes a string of characters representing a phone number. This performs
* the following conversions:
* Punctuation is stripped.
* For ALPHA/VANITY numbers:
* Letters are converted to their numeric representation on a telephone
* keypad. The keypad used here is the one defined in ITU Recommendation
* E.161. This is only done if there are 3 or more letters in the number,
* to lessen the risk that such letters are typos.
* For other numbers:
* Wide-ascii digits are converted to normal ASCII (European) digits.
* Arabic-Indic numerals are converted to European numerals.
* Spurious alpha characters are stripped.
*
* @param {string} number a string of characters representing a phone number.
* @return {string} the normalized string version of the phone number.
*/
- (NSString *)normalizePhoneNumber:(NSString *)number
{
NBMetadataHelper *helper = [[NBMetadataHelper alloc] init];
number = [helper normalizeNonBreakingSpace:number];
if ([self matchesEntirely:VALID_ALPHA_PHONE_PATTERN_STRING string:number])
{
return [self normalizeHelper:number normalizationReplacements:ALL_NORMALIZATION_MAPPINGS removeNonMatches:true];
}
else
{
return [self normalizeDigitsOnly:number];
}
return nil;
}
/**
* Normalizes a string of characters representing a phone number. This is a
* wrapper for normalize(String number) but does in-place normalization of the
* StringBuffer provided.
*
* @param {!goog.string.StringBuffer} number a StringBuffer of characters
* representing a phone number that will be normalized in place.
* @private
*/
- (void)normalizeSB:(NSString **)number
{
if (number == NULL) {
return;
}
(*number) = [self normalizePhoneNumber:(*number)];
}
/**
* Normalizes a string of characters representing a phone number. This converts
* wide-ascii and arabic-indic numerals to European numerals, and strips
* punctuation and alpha characters.
*
* @param {string} number a string of characters representing a phone number.
* @return {string} the normalized string version of the phone number.
*/
- (NSString *)normalizeDigitsOnly:(NSString *)number
{
NBMetadataHelper *helper = [[NBMetadataHelper alloc] init];
number = [helper normalizeNonBreakingSpace:number];
return [self stringByReplacingOccurrencesString:number withMap:self.DIGIT_MAPPINGS removeNonMatches:YES];
}
/**
* Converts all alpha characters in a number to their respective digits on a
* keypad, but retains existing formatting. Also converts wide-ascii digits to
* normal ascii digits, and converts Arabic-Indic numerals to European numerals.
*
* @param {string} number a string of characters representing a phone number.
* @return {string} the normalized string version of the phone number.
*/
- (NSString *)convertAlphaCharactersInNumber:(NSString *)number
{
NBMetadataHelper *helper = [[NBMetadataHelper alloc] init];
number = [helper normalizeNonBreakingSpace:number];
return [self stringByReplacingOccurrencesString:number withMap:ALL_NORMALIZATION_MAPPINGS removeNonMatches:NO];
}
/**
* Gets the length of the geographical area code from the
* {@code national_number} field of the PhoneNumber object passed in, so that
* clients could use it to split a national significant number into geographical
* area code and subscriber number. It works in such a way that the resultant
* subscriber number should be diallable, at least on some devices. An example
* of how this could be used:
*
* <pre>
* var phoneUtil = getInstance();
* var number = phoneUtil.parse('16502530000', 'US');
* var nationalSignificantNumber =
* phoneUtil.getNationalSignificantNumber(number);
* var areaCode;
* var subscriberNumber;
*
* var areaCodeLength = phoneUtil.getLengthOfGeographicalAreaCode(number);
* if (areaCodeLength > 0) {
* areaCode = nationalSignificantNumber.substring(0, areaCodeLength);
* subscriberNumber = nationalSignificantNumber.substring(areaCodeLength);
* } else {
* areaCode = '';
* subscriberNumber = nationalSignificantNumber;
* }
* </pre>
*
* N.B.: area code is a very ambiguous concept, so the I18N team generally
* recommends against using it for most purposes, but recommends using the more
* general {@code national_number} instead. Read the following carefully before
* deciding to use this method:
* <ul>
* <li> geographical area codes change over time, and this method honors those
* changes; therefore, it doesn't guarantee the stability of the result it
* produces.
* <li> subscriber numbers may not be diallable from all devices (notably
* mobile devices, which typically requires the full national_number to be
* dialled in most regions).
* <li> most non-geographical numbers have no area codes, including numbers
* from non-geographical entities.
* <li> some geographical numbers have no area codes.
* </ul>
*
* @param {i18n.phonenumbers.PhoneNumber} number the PhoneNumber object for
* which clients want to know the length of the area code.
* @return {number} the length of area code of the PhoneNumber object passed in.
*/
- (int)getLengthOfGeographicalAreaCode:(NBPhoneNumber*)phoneNumber error:(NSError **)error
{
int res = 0;
@try {
res = [self getLengthOfGeographicalAreaCode:phoneNumber];
}
@catch (NSException *exception) {
NSDictionary *userInfo = [NSDictionary dictionaryWithObject:exception.reason
forKey:NSLocalizedDescriptionKey];
if (error != NULL) {
(*error) = [NSError errorWithDomain:exception.name code:0 userInfo:userInfo];
}
}
return res;
}
- (int)getLengthOfGeographicalAreaCode:(NBPhoneNumber*)phoneNumber
{
NSString *regionCode = [self getRegionCodeForNumber:phoneNumber];
NBMetadataHelper *helper = [[NBMetadataHelper alloc] init];
NBPhoneMetaData *metadata = [helper getMetadataForRegion:regionCode];
if (metadata == nil) {
return 0;
}
// If a country doesn't use a national prefix, and this number doesn't have
// an Italian leading zero, we assume it is a closed dialling plan with no
// area codes.
if (metadata.nationalPrefix == nil && phoneNumber.italianLeadingZero == NO) {
return 0;
}
if ([self isNumberGeographical:phoneNumber] == NO) {
return 0;
}
return [self getLengthOfNationalDestinationCode:phoneNumber];
}
/**
* Gets the length of the national destination code (NDC) from the PhoneNumber
* object passed in, so that clients could use it to split a national
* significant number into NDC and subscriber number. The NDC of a phone number
* is normally the first group of digit(s) right after the country calling code
* when the number is formatted in the international format, if there is a
* subscriber number part that follows. An example of how this could be used:
*
* <pre>
* var phoneUtil = getInstance();
* var number = phoneUtil.parse('18002530000', 'US');
* var nationalSignificantNumber =
* phoneUtil.getNationalSignificantNumber(number);
* var nationalDestinationCode;
* var subscriberNumber;
*
* var nationalDestinationCodeLength =
* phoneUtil.getLengthOfNationalDestinationCode(number);
* if (nationalDestinationCodeLength > 0) {
* nationalDestinationCode =
* nationalSignificantNumber.substring(0, nationalDestinationCodeLength);
* subscriberNumber =
* nationalSignificantNumber.substring(nationalDestinationCodeLength);
* } else {
* nationalDestinationCode = '';
* subscriberNumber = nationalSignificantNumber;
* }
* </pre>
*
* Refer to the unittests to see the difference between this function and
* {@link #getLengthOfGeographicalAreaCode}.
*
* @param {i18n.phonenumbers.PhoneNumber} number the PhoneNumber object for
* which clients want to know the length of the NDC.
* @return {number} the length of NDC of the PhoneNumber object passed in.
*/
- (int)getLengthOfNationalDestinationCode:(NBPhoneNumber*)phoneNumber error:(NSError **)error
{
int res = 0;
@try {
res = [self getLengthOfNationalDestinationCode:phoneNumber];
}
@catch (NSException *exception) {
NSDictionary *userInfo = [NSDictionary dictionaryWithObject:exception.reason
forKey:NSLocalizedDescriptionKey];
if (error != NULL) {
(*error) = [NSError errorWithDomain:exception.name code:0 userInfo:userInfo];
}
}
return res;
}
- (int)getLengthOfNationalDestinationCode:(NBPhoneNumber*)phoneNumber
{
NBPhoneNumber *copiedProto = nil;
NBMetadataHelper *helper = [[NBMetadataHelper alloc] init];
if ([NBMetadataHelper hasValue:phoneNumber.extension]) {
copiedProto = [phoneNumber copy];
copiedProto.extension = nil;
} else {
copiedProto = phoneNumber;
}
NSString *nationalSignificantNumber = [self format:copiedProto numberFormat:NBEPhoneNumberFormatINTERNATIONAL];
NSMutableArray *numberGroups = [[self componentsSeparatedByRegex:nationalSignificantNumber regex:NON_DIGITS_PATTERN] mutableCopy];
// The pattern will start with '+COUNTRY_CODE ' so the first group will always
// be the empty string (before the + symbol) and the second group will be the
// country calling code. The third group will be area code if it is not the
// last group.
// NOTE: On IE the first group that is supposed to be the empty string does
// not appear in the array of number groups... so make the result on non-IE
// browsers to be that of IE.
if ([numberGroups count] > 0 && ((NSString *)[numberGroups objectAtIndex:0]).length <= 0) {
[numberGroups removeObjectAtIndex:0];
}
if ([numberGroups count] <= 2) {
return 0;
}
NSArray *regionCodes = [helper regionCodeFromCountryCode:phoneNumber.countryCode];
BOOL isExists = NO;
for (NSString *regCode in regionCodes)
{
if ([regCode isEqualToString:@"AR"])
{
isExists = YES;
break;
}
}
if (isExists && [self getNumberType:phoneNumber] == NBEPhoneNumberTypeMOBILE)
{
// Argentinian mobile numbers, when formatted in the international format,
// are in the form of +54 9 NDC XXXX.... As a result, we take the length of
// the third group (NDC) and add 1 for the digit 9, which also forms part of
// the national significant number.
//
// TODO: Investigate the possibility of better modeling the metadata to make
// it easier to obtain the NDC.
return (int)((NSString *)[numberGroups objectAtIndex:2]).length + 1;
}
return (int)((NSString *)[numberGroups objectAtIndex:1]).length;
}
/**
* Normalizes a string of characters representing a phone number by replacing
* all characters found in the accompanying map with the values therein, and
* stripping all other characters if removeNonMatches is NO.
*
* @param {string} number a string of characters representing a phone number.
* @param {!Object.<string, string>} normalizationReplacements a mapping of
* characters to what they should be replaced by in the normalized version
* of the phone number.
* @param {boolean} removeNonMatches indicates whether characters that are not
* able to be replaced should be stripped from the number. If this is NO,
* they will be left unchanged in the number.
* @return {string} the normalized string version of the phone number.
* @private
*/
- (NSString *)normalizeHelper:(NSString *)sourceString normalizationReplacements:(NSDictionary*)normalizationReplacements
removeNonMatches:(BOOL)removeNonMatches
{
NSMutableString *normalizedNumber = [[NSMutableString alloc] init];
unichar character = 0;
NSString *newDigit = @"";
unsigned int numberLength = (unsigned int)sourceString.length;
for (unsigned int i = 0; i<numberLength; ++i)
{
character = [sourceString characterAtIndex:i];
newDigit = [normalizationReplacements objectForKey:[[NSString stringWithFormat: @"%C", character] uppercaseString]];
if (newDigit != nil)
{
[normalizedNumber appendString:newDigit];
}
else if (removeNonMatches == NO)
{
[normalizedNumber appendString:[NSString stringWithFormat: @"%C", character]];
}
// If neither of the above are NO, we remove this character.
//NSLog(@"[%@]", normalizedNumber);
}
return normalizedNumber;
}
/**
* Helper function to check if the national prefix formatting rule has the first
* group only, i.e., does not start with the national prefix.
*
* @param {string} nationalPrefixFormattingRule The formatting rule for the
* national prefix.
* @return {boolean} NO if the national prefix formatting rule has the first
* group only.
*/
- (BOOL)formattingRuleHasFirstGroupOnly:(NSString *)nationalPrefixFormattingRule
{
BOOL hasFound = NO;
if ([self stringPositionByRegex:nationalPrefixFormattingRule regex:FIRST_GROUP_ONLY_PREFIX_PATTERN] >= 0)
{
hasFound = YES;
}
return (([nationalPrefixFormattingRule length] == 0) || hasFound);
}
/**
* Tests whether a phone number has a geographical association. It checks if
* the number is associated to a certain region in the country where it belongs
* to. Note that this doesn't verify if the number is actually in use.
*
* @param {i18n.phonenumbers.PhoneNumber} phoneNumber The phone number to test.
* @return {boolean} NO if the phone number has a geographical association.
* @private
*/
- (BOOL)isNumberGeographical:(NBPhoneNumber*)phoneNumber
{
NBEPhoneNumberType numberType = [self getNumberType:phoneNumber];
// TODO: Include mobile phone numbers from countries like Indonesia, which
// has some mobile numbers that are geographical.
BOOL containGeoMobileContries =
[GEO_MOBILE_COUNTRIES containsObject:phoneNumber.countryCode] && numberType == NBEPhoneNumberTypeMOBILE;
BOOL isFixedLine = (numberType == NBEPhoneNumberTypeFIXED_LINE);
BOOL isFixedLineOrMobile = (numberType == NBEPhoneNumberTypeFIXED_LINE_OR_MOBILE);
return isFixedLine || isFixedLineOrMobile || containGeoMobileContries;
}
/**
* Helper function to check region code is not unknown or nil.
*
* @param {?string} regionCode the ISO 3166-1 two-letter region code.
* @return {boolean} NO if region code is valid.
* @private
*/
- (BOOL)isValidRegionCode:(NSString *)regionCode
{
// In Java we check whether the regionCode is contained in supportedRegions
// that is built out of all the values of countryCallingCodeToRegionCodeMap
// (countryCodeToRegionCodeMap in JS) minus REGION_CODE_FOR_NON_GEO_ENTITY.
// In JS we check whether the regionCode is contained in the keys of
// countryToMetadata but since for non-geographical country calling codes
// (e.g. +800) we use the country calling codes instead of the region code as
// key in the map we have to make sure regionCode is not a number to prevent
// returning NO for non-geographical country calling codes.
NBMetadataHelper *helper = [[NBMetadataHelper alloc] init];
return [NBMetadataHelper hasValue:regionCode] && [self isNaN:regionCode] && [helper getMetadataForRegion:regionCode.uppercaseString] != nil;
}
/**
* Helper function to check the country calling code is valid.
*
* @param {number} countryCallingCode the country calling code.
* @return {boolean} NO if country calling code code is valid.
* @private
*/
- (BOOL)hasValidCountryCallingCode:(NSNumber*)countryCallingCode
{
NBMetadataHelper *helper = [[NBMetadataHelper alloc] init];
id res = [helper regionCodeFromCountryCode:countryCallingCode];
if (res != nil) {
return YES;
}
return NO;
}
/**
* Formats a phone number in the specified format using default rules. Note that
* this does not promise to produce a phone number that the user can dial from
* where they are - although we do format in either 'national' or
* 'international' format depending on what the client asks for, we do not
* currently support a more abbreviated format, such as for users in the same
* 'area' who could potentially dial the number without area code. Note that if
* the phone number has a country calling code of 0 or an otherwise invalid
* country calling code, we cannot work out which formatting rules to apply so
* we return the national significant number with no formatting applied.
*
* @param {i18n.phonenumbers.PhoneNumber} number the phone number to be
* formatted.
* @param {i18n.phonenumbers.PhoneNumberFormat} numberFormat the format the
* phone number should be formatted into.
* @return {string} the formatted phone number.
*/
- (NSString *)format:(NBPhoneNumber*)phoneNumber numberFormat:(NBEPhoneNumberFormat)numberFormat error:(NSError**)error
{
NSString *res = nil;
@try {
res = [self format:phoneNumber numberFormat:numberFormat];
}
@catch (NSException *exception) {
NSDictionary *userInfo = [NSDictionary dictionaryWithObject:exception.reason
forKey:NSLocalizedDescriptionKey];
if (error != NULL)
(*error) = [NSError errorWithDomain:exception.name code:0 userInfo:userInfo];
}
return res;
}
- (NSString *)format:(NBPhoneNumber*)phoneNumber numberFormat:(NBEPhoneNumberFormat)numberFormat
{
NBMetadataHelper *helper = [[NBMetadataHelper alloc] init];
if ([phoneNumber.nationalNumber isEqualToNumber:@0] && [NBMetadataHelper hasValue:phoneNumber.rawInput]) {
// Unparseable numbers that kept their raw input just use that.
// This is the only case where a number can be formatted as E164 without a
// leading '+' symbol (but the original number wasn't parseable anyway).
// TODO: Consider removing the 'if' above so that unparseable strings
// without raw input format to the empty string instead of "+00"
/** @type {string} */
NSString *rawInput = phoneNumber.rawInput;
if ([NBMetadataHelper hasValue:rawInput]) {
return rawInput;
}
}
NSNumber *countryCallingCode = phoneNumber.countryCode;
NSString *nationalSignificantNumber = [self getNationalSignificantNumber:phoneNumber];
if (numberFormat == NBEPhoneNumberFormatE164)
{
// Early exit for E164 case (even if the country calling code is invalid)
// since no formatting of the national number needs to be applied.
// Extensions are not formatted.
return [self prefixNumberWithCountryCallingCode:countryCallingCode phoneNumberFormat:NBEPhoneNumberFormatE164
formattedNationalNumber:nationalSignificantNumber formattedExtension:@""];
}
if ([self hasValidCountryCallingCode:countryCallingCode] == NO)
{
return nationalSignificantNumber;
}
// Note getRegionCodeForCountryCode() is used because formatting information
// for regions which share a country calling code is contained by only one
// region for performance reasons. For example, for NANPA regions it will be
// contained in the metadata for US.
NSArray *regionCodeArray = [helper regionCodeFromCountryCode:countryCallingCode];
NSString *regionCode = [regionCodeArray objectAtIndex:0];
// Metadata cannot be nil because the country calling code is valid (which
// means that the region code cannot be ZZ and must be one of our supported
// region codes).
NBPhoneMetaData *metadata = [self getMetadataForRegionOrCallingCode:countryCallingCode regionCode:regionCode];
NSString *formattedExtension = [self maybeGetFormattedExtension:phoneNumber metadata:metadata numberFormat:numberFormat];
NSString *formattedNationalNumber = [self formatNsn:nationalSignificantNumber metadata:metadata phoneNumberFormat:numberFormat carrierCode:nil];
return [self prefixNumberWithCountryCallingCode:countryCallingCode phoneNumberFormat:numberFormat
formattedNationalNumber:formattedNationalNumber formattedExtension:formattedExtension];
}
/**
* Formats a phone number in the specified format using client-defined
* formatting rules. Note that if the phone number has a country calling code of
* zero or an otherwise invalid country calling code, we cannot work out things
* like whether there should be a national prefix applied, or how to format
* extensions, so we return the national significant number with no formatting
* applied.
*
* @param {i18n.phonenumbers.PhoneNumber} number the phone number to be
* formatted.
* @param {i18n.phonenumbers.PhoneNumberFormat} numberFormat the format the
* phone number should be formatted into.
* @param {Array.<i18n.phonenumbers.NumberFormat>} userDefinedFormats formatting
* rules specified by clients.
* @return {string} the formatted phone number.
*/
- (NSString *)formatByPattern:(NBPhoneNumber*)number numberFormat:(NBEPhoneNumberFormat)numberFormat userDefinedFormats:(NSArray*)userDefinedFormats error:(NSError**)error
{
NSString *res = nil;
@try {
res = [self formatByPattern:number numberFormat:numberFormat userDefinedFormats:userDefinedFormats];
}
@catch (NSException *exception) {
NSDictionary *userInfo = [NSDictionary dictionaryWithObject:exception.reason
forKey:NSLocalizedDescriptionKey];
if (error != NULL)
(*error) = [NSError errorWithDomain:exception.name code:0 userInfo:userInfo];
}
return res;
}
- (NSString *)formatByPattern:(NBPhoneNumber*)number numberFormat:(NBEPhoneNumberFormat)numberFormat userDefinedFormats:(NSArray*)userDefinedFormats
{
NSNumber *countryCallingCode = number.countryCode;
NSString *nationalSignificantNumber = [self getNationalSignificantNumber:number];
if ([self hasValidCountryCallingCode:countryCallingCode] == NO) {
return nationalSignificantNumber;
}
// Note getRegionCodeForCountryCode() is used because formatting information
// for regions which share a country calling code is contained by only one
// region for performance reasons. For example, for NANPA regions it will be
// contained in the metadata for US.
NBMetadataHelper *helper = [[NBMetadataHelper alloc] init];
NSArray *regionCodes = [helper regionCodeFromCountryCode:countryCallingCode];
NSString *regionCode = nil;
if (regionCodes != nil && regionCodes.count > 0) {
regionCode = [regionCodes objectAtIndex:0];
}
// Metadata cannot be nil because the country calling code is valid
/** @type {i18n.phonenumbers.PhoneMetadata} */
NBPhoneMetaData *metadata = [self getMetadataForRegionOrCallingCode:countryCallingCode regionCode:regionCode];
NSString *formattedNumber = @"";
NBNumberFormat *formattingPattern = [self chooseFormattingPatternForNumber:userDefinedFormats
nationalNumber:nationalSignificantNumber];
if (formattingPattern == nil) {
// If no pattern above is matched, we format the number as a whole.
formattedNumber = nationalSignificantNumber;
} else {
// Before we do a replacement of the national prefix pattern $NP with the
// national prefix, we need to copy the rule so that subsequent replacements
// for different numbers have the appropriate national prefix.
NBNumberFormat *numFormatCopy = [formattingPattern copy];
NSString *nationalPrefixFormattingRule = formattingPattern.nationalPrefixFormattingRule;
if (nationalPrefixFormattingRule.length > 0) {
NSString *nationalPrefix = metadata.nationalPrefix;
if (nationalPrefix.length > 0) {
// Replace $NP with national prefix and $FG with the first group ($1).
nationalPrefixFormattingRule = [self replaceStringByRegex:nationalPrefixFormattingRule
regex:NP_PATTERN withTemplate:nationalPrefix];
nationalPrefixFormattingRule = [self replaceStringByRegex:nationalPrefixFormattingRule
regex:FG_PATTERN withTemplate:@"\\$1"];
numFormatCopy.nationalPrefixFormattingRule = nationalPrefixFormattingRule;
} else {
// We don't want to have a rule for how to format the national prefix if
// there isn't one.
numFormatCopy.nationalPrefixFormattingRule = @"";
}
}
formattedNumber = [self formatNsnUsingPattern:nationalSignificantNumber
formattingPattern:numFormatCopy numberFormat:numberFormat carrierCode:nil];
}
NSString *formattedExtension = [self maybeGetFormattedExtension:number metadata:metadata numberFormat:numberFormat];
//NSLog(@"!@# prefixNumberWithCountryCallingCode called [%@]", formattedExtension);
return [self prefixNumberWithCountryCallingCode:countryCallingCode phoneNumberFormat:numberFormat
formattedNationalNumber:formattedNumber formattedExtension:formattedExtension];
}
/**
* Formats a phone number in national format for dialing using the carrier as
* specified in the {@code carrierCode}. The {@code carrierCode} will always be
* used regardless of whether the phone number already has a preferred domestic
* carrier code stored. If {@code carrierCode} contains an empty string, returns
* the number in national format without any carrier code.
*
* @param {i18n.phonenumbers.PhoneNumber} number the phone number to be
* formatted.
* @param {string} carrierCode the carrier selection code to be used.
* @return {string} the formatted phone number in national format for dialing
* using the carrier as specified in the {@code carrierCode}.
*/
- (NSString *)formatNationalNumberWithCarrierCode:(NBPhoneNumber*)number carrierCode:(NSString *)carrierCode error:(NSError **)error
{
NSString *res = nil;
@try {
res = [self formatNationalNumberWithCarrierCode:number carrierCode:carrierCode];
}
@catch (NSException *exception) {
NSDictionary *userInfo = [NSDictionary dictionaryWithObject:exception.reason
forKey:NSLocalizedDescriptionKey];
if (error != NULL) {
(*error) = [NSError errorWithDomain:exception.name code:0 userInfo:userInfo];
}
}
return res;
}
- (NSString *)formatNationalNumberWithCarrierCode:(NBPhoneNumber*)number carrierCode:(NSString *)carrierCode
{
NSNumber *countryCallingCode = number.countryCode;
NSString *nationalSignificantNumber = [self getNationalSignificantNumber:number];
if ([self hasValidCountryCallingCode:countryCallingCode] == NO) {
return nationalSignificantNumber;
}
// Note getRegionCodeForCountryCode() is used because formatting information
// for regions which share a country calling code is contained by only one
// region for performance reasons. For example, for NANPA regions it will be
// contained in the metadata for US.
NSString *regionCode = [self getRegionCodeForCountryCode:countryCallingCode];
// Metadata cannot be nil because the country calling code is valid.
NBPhoneMetaData *metadata = [self getMetadataForRegionOrCallingCode:countryCallingCode regionCode:regionCode];
NSString *formattedExtension = [self maybeGetFormattedExtension:number metadata:metadata numberFormat:NBEPhoneNumberFormatNATIONAL];
NSString *formattedNationalNumber = [self formatNsn:nationalSignificantNumber metadata:metadata
phoneNumberFormat:NBEPhoneNumberFormatNATIONAL carrierCode:carrierCode];
return [self prefixNumberWithCountryCallingCode:countryCallingCode phoneNumberFormat:NBEPhoneNumberFormatNATIONAL
formattedNationalNumber:formattedNationalNumber formattedExtension:formattedExtension];
}
/**
* @param {number} countryCallingCode
* @param {?string} regionCode
* @return {i18n.phonenumbers.PhoneMetadata}
* @private
*/
- (NBPhoneMetaData*)getMetadataForRegionOrCallingCode:(NSNumber*)countryCallingCode regionCode:(NSString *)regionCode
{
NBMetadataHelper *helper = [[NBMetadataHelper alloc] init];
return [NB_REGION_CODE_FOR_NON_GEO_ENTITY isEqualToString:regionCode] ?
[helper getMetadataForNonGeographicalRegion:countryCallingCode] : [helper getMetadataForRegion:regionCode];
}
/**
* Formats a phone number in national format for dialing using the carrier as
* specified in the preferred_domestic_carrier_code field of the PhoneNumber
* object passed in. If that is missing, use the {@code fallbackCarrierCode}
* passed in instead. If there is no {@code preferred_domestic_carrier_code},
* and the {@code fallbackCarrierCode} contains an empty string, return the
* number in national format without any carrier code.
*
* <p>Use {@link #formatNationalNumberWithCarrierCode} instead if the carrier
* code passed in should take precedence over the number's
* {@code preferred_domestic_carrier_code} when formatting.
*
* @param {i18n.phonenumbers.PhoneNumber} number the phone number to be
* formatted.
* @param {string} fallbackCarrierCode the carrier selection code to be used, if
* none is found in the phone number itself.
* @return {string} the formatted phone number in national format for dialing
* using the number's preferred_domestic_carrier_code, or the
* {@code fallbackCarrierCode} passed in if none is found.
*/
- (NSString *)formatNationalNumberWithPreferredCarrierCode:(NBPhoneNumber*)number
fallbackCarrierCode:(NSString *)fallbackCarrierCode error:(NSError **)error
{
NSString *res = nil;
@try {
res = [self formatNationalNumberWithCarrierCode:number carrierCode:fallbackCarrierCode];
}
@catch (NSException *exception) {
NSDictionary *userInfo = [NSDictionary dictionaryWithObject:exception.reason
forKey:NSLocalizedDescriptionKey];
if (error != NULL) {
(*error) = [NSError errorWithDomain:exception.name code:0 userInfo:userInfo];
}
}
return res;
}
- (NSString *)formatNationalNumberWithPreferredCarrierCode:(NBPhoneNumber*)number fallbackCarrierCode:(NSString *)fallbackCarrierCode
{
NSString *domesticCarrierCode = number.preferredDomesticCarrierCode != nil ? number.preferredDomesticCarrierCode : fallbackCarrierCode;
return [self formatNationalNumberWithCarrierCode:number carrierCode:domesticCarrierCode];
}
/**
* Returns a number formatted in such a way that it can be dialed from a mobile
* phone in a specific region. If the number cannot be reached from the region
* (e.g. some countries block toll-free numbers from being called outside of the
* country), the method returns an empty string.
*
* @param {i18n.phonenumbers.PhoneNumber} number the phone number to be
* formatted.
* @param {string} regionCallingFrom the region where the call is being placed.
* @param {boolean} withFormatting whether the number should be returned with
* formatting symbols, such as spaces and dashes.
* @return {string} the formatted phone number.
*/
- (NSString *)formatNumberForMobileDialing:(NBPhoneNumber*)number regionCallingFrom:(NSString *)regionCallingFrom withFormatting:(BOOL)withFormatting
error:(NSError**)error
{
NSString *res = nil;
@try {
res = [self formatNumberForMobileDialing:number regionCallingFrom:regionCallingFrom withFormatting:withFormatting];
}
@catch (NSException *exception) {
NSDictionary *userInfo = [NSDictionary dictionaryWithObject:exception.reason
forKey:NSLocalizedDescriptionKey];
if (error != NULL)
(*error) = [NSError errorWithDomain:exception.name code:0 userInfo:userInfo];
}
return res;
}
- (NSString *)formatNumberForMobileDialing:(NBPhoneNumber*)number regionCallingFrom:(NSString *)regionCallingFrom withFormatting:(BOOL)withFormatting
{
NSNumber *countryCallingCode = number.countryCode;
if ([self hasValidCountryCallingCode:countryCallingCode] == NO) {
return [NBMetadataHelper hasValue:number.rawInput] ? number.rawInput : @"";
}
NSString *formattedNumber = @"";
// Clear the extension, as that part cannot normally be dialed together with
// the main number.
NBPhoneNumber *numberNoExt = [number copy];
numberNoExt.extension = @"";
NSString *regionCode = [self getRegionCodeForCountryCode:countryCallingCode];
if ([regionCallingFrom isEqualToString:regionCode]) {
NBEPhoneNumberType numberType = [self getNumberType:numberNoExt];
BOOL isFixedLineOrMobile = (numberType == NBEPhoneNumberTypeFIXED_LINE) || (numberType == NBEPhoneNumberTypeMOBILE) ||
(numberType == NBEPhoneNumberTypeFIXED_LINE_OR_MOBILE);
// Carrier codes may be needed in some countries. We handle this here.
if ([regionCode isEqualToString:@"CO"] && numberType == NBEPhoneNumberTypeFIXED_LINE) {
formattedNumber = [self formatNationalNumberWithCarrierCode:numberNoExt
carrierCode:COLOMBIA_MOBILE_TO_FIXED_LINE_PREFIX];
} else if ([regionCode isEqualToString:@"BR"] && isFixedLineOrMobile) {
formattedNumber = [NBMetadataHelper hasValue:numberNoExt.preferredDomesticCarrierCode] ?
[self formatNationalNumberWithPreferredCarrierCode:numberNoExt fallbackCarrierCode:@""] : @"";
// Brazilian fixed line and mobile numbers need to be dialed with a
// carrier code when called within Brazil. Without that, most of the
// carriers won't connect the call. Because of that, we return an
// empty string here.
} else {
// For NANPA countries, non-geographical countries, and Mexican fixed
// line and mobile numbers, we output international format for numbersi
// that can be dialed internationally as that always works.
if ((countryCallingCode.unsignedIntegerValue == NANPA_COUNTRY_CODE_ ||
[regionCode isEqualToString:NB_REGION_CODE_FOR_NON_GEO_ENTITY] ||
// MX fixed line and mobile numbers should always be formatted in
// international format, even when dialed within MX. For national
// format to work, a carrier code needs to be used, and the correct
// carrier code depends on if the caller and callee are from the
// same local area. It is trickier to get that to work correctly than
// using international format, which is tested to work fine on all
// carriers.
([regionCode isEqualToString:@"MX"] && isFixedLineOrMobile)) && [self canBeInternationallyDialled:numberNoExt]) {
formattedNumber = [self format:numberNoExt numberFormat:NBEPhoneNumberFormatINTERNATIONAL];
} else {
formattedNumber = [self format:numberNoExt numberFormat:NBEPhoneNumberFormatNATIONAL];
}
}
} else if ([self canBeInternationallyDialled:numberNoExt]) {
return withFormatting ? [self format:numberNoExt numberFormat:NBEPhoneNumberFormatINTERNATIONAL] :
[self format:numberNoExt numberFormat:NBEPhoneNumberFormatE164];
}
return withFormatting ?
formattedNumber : [self normalizeHelper:formattedNumber normalizationReplacements:DIALLABLE_CHAR_MAPPINGS removeNonMatches:YES];
}
/**
* Formats a phone number for out-of-country dialing purposes. If no
* regionCallingFrom is supplied, we format the number in its INTERNATIONAL
* format. If the country calling code is the same as that of the region where
* the number is from, then NATIONAL formatting will be applied.
*
* <p>If the number itself has a country calling code of zero or an otherwise
* invalid country calling code, then we return the number with no formatting
* applied.
*
* <p>Note this function takes care of the case for calling inside of NANPA and
* between Russia and Kazakhstan (who share the same country calling code). In
* those cases, no international prefix is used. For regions which have multiple
* international prefixes, the number in its INTERNATIONAL format will be
* returned instead.
*
* @param {i18n.phonenumbers.PhoneNumber} number the phone number to be
* formatted.
* @param {string} regionCallingFrom the region where the call is being placed.
* @return {string} the formatted phone number.
*/
- (NSString *)formatOutOfCountryCallingNumber:(NBPhoneNumber*)number regionCallingFrom:(NSString *)regionCallingFrom error:(NSError**)error
{
NSString *res = nil;
@try {
res = [self formatOutOfCountryCallingNumber:number regionCallingFrom:regionCallingFrom];
}
@catch (NSException *exception) {
NSDictionary *userInfo = [NSDictionary dictionaryWithObject:exception.reason
forKey:NSLocalizedDescriptionKey];
if (error != NULL)
(*error) = [NSError errorWithDomain:exception.name code:0 userInfo:userInfo];
}
return res;
}
- (NSString *)formatOutOfCountryCallingNumber:(NBPhoneNumber*)number regionCallingFrom:(NSString *)regionCallingFrom
{
if ([self isValidRegionCode:regionCallingFrom] == NO) {
return [self format:number numberFormat:NBEPhoneNumberFormatINTERNATIONAL];
}
NSNumber *countryCallingCode = [number.countryCode copy];
NSString *nationalSignificantNumber = [self getNationalSignificantNumber:number];
if ([self hasValidCountryCallingCode:countryCallingCode] == NO) {
return nationalSignificantNumber;
}
if (countryCallingCode.unsignedIntegerValue == NANPA_COUNTRY_CODE_) {
if ([self isNANPACountry:regionCallingFrom]) {
// For NANPA regions, return the national format for these regions but
// prefix it with the country calling code.
return [NSString stringWithFormat:@"%@ %@", countryCallingCode, [self format:number numberFormat:NBEPhoneNumberFormatNATIONAL]];
}
} else if ([countryCallingCode isEqualToNumber:[self getCountryCodeForValidRegion:regionCallingFrom error:nil]]) {
// If regions share a country calling code, the country calling code need
// not be dialled. This also applies when dialling within a region, so this
// if clause covers both these cases. Technically this is the case for
// dialling from La Reunion to other overseas departments of France (French
// Guiana, Martinique, Guadeloupe), but not vice versa - so we don't cover
// this edge case for now and for those cases return the version including
// country calling code. Details here:
// http://www.petitfute.com/voyage/225-info-pratiques-reunion
return [self format:number numberFormat:NBEPhoneNumberFormatNATIONAL];
}
// Metadata cannot be nil because we checked 'isValidRegionCode()' above.
NBMetadataHelper *helper = [[NBMetadataHelper alloc] init];
NBPhoneMetaData *metadataForRegionCallingFrom = [helper getMetadataForRegion:regionCallingFrom];
NSString *internationalPrefix = metadataForRegionCallingFrom.internationalPrefix;
// For regions that have multiple international prefixes, the international
// format of the number is returned, unless there is a preferred international
// prefix.
NSString *internationalPrefixForFormatting = @"";
if ([self matchesEntirely:UNIQUE_INTERNATIONAL_PREFIX string:internationalPrefix]) {
internationalPrefixForFormatting = internationalPrefix;
} else if ([NBMetadataHelper hasValue:metadataForRegionCallingFrom.preferredInternationalPrefix]) {
internationalPrefixForFormatting = metadataForRegionCallingFrom.preferredInternationalPrefix;
}
NSString *regionCode = [self getRegionCodeForCountryCode:countryCallingCode];
// Metadata cannot be nil because the country calling code is valid.
NBPhoneMetaData *metadataForRegion = [self getMetadataForRegionOrCallingCode:countryCallingCode regionCode:regionCode];
NSString *formattedNationalNumber = [self formatNsn:nationalSignificantNumber metadata:metadataForRegion
phoneNumberFormat:NBEPhoneNumberFormatINTERNATIONAL carrierCode:nil];
NSString *formattedExtension = [self maybeGetFormattedExtension:number metadata:metadataForRegion numberFormat:NBEPhoneNumberFormatINTERNATIONAL];
NSString *hasLenth = [NSString stringWithFormat:@"%@ %@ %@%@", internationalPrefixForFormatting, countryCallingCode, formattedNationalNumber, formattedExtension];
NSString *hasNotLength = [self prefixNumberWithCountryCallingCode:countryCallingCode phoneNumberFormat:NBEPhoneNumberFormatINTERNATIONAL
formattedNationalNumber:formattedNationalNumber formattedExtension:formattedExtension];
return internationalPrefixForFormatting.length > 0 ? hasLenth:hasNotLength;
}
/**
* A helper function that is used by format and formatByPattern.
*
* @param {number} countryCallingCode the country calling code.
* @param {i18n.phonenumbers.PhoneNumberFormat} numberFormat the format the
* phone number should be formatted into.
* @param {string} formattedNationalNumber
* @param {string} formattedExtension
* @return {string} the formatted phone number.
* @private
*/
- (NSString *)prefixNumberWithCountryCallingCode:(NSNumber*)countryCallingCode phoneNumberFormat:(NBEPhoneNumberFormat)numberFormat
formattedNationalNumber:(NSString *)formattedNationalNumber
formattedExtension:(NSString *)formattedExtension
{
switch (numberFormat)
{
case NBEPhoneNumberFormatE164:
return [NSString stringWithFormat:@"+%@%@%@", countryCallingCode, formattedNationalNumber, formattedExtension];
case NBEPhoneNumberFormatINTERNATIONAL:
return [NSString stringWithFormat:@"+%@ %@%@", countryCallingCode, formattedNationalNumber, formattedExtension];
case NBEPhoneNumberFormatRFC3966:
return [NSString stringWithFormat:@"%@+%@-%@%@", RFC3966_PREFIX, countryCallingCode, formattedNationalNumber, formattedExtension];
case NBEPhoneNumberFormatNATIONAL:
default:
return [NSString stringWithFormat:@"%@%@", formattedNationalNumber, formattedExtension];
}
}
/**
* Formats a phone number using the original phone number format that the number
* is parsed from. The original format is embedded in the country_code_source
* field of the PhoneNumber object passed in. If such information is missing,
* the number will be formatted into the NATIONAL format by default. When the
* number contains a leading zero and this is unexpected for this country, or we
* don't have a formatting pattern for the number, the method returns the raw
* input when it is available.
*
* Note this method guarantees no digit will be inserted, removed or modified as
* a result of formatting.
*
* @param {i18n.phonenumbers.PhoneNumber} number the phone number that needs to
* be formatted in its original number format.
* @param {string} regionCallingFrom the region whose IDD needs to be prefixed
* if the original number has one.
* @return {string} the formatted phone number in its original number format.
*/
- (NSString *)formatInOriginalFormat:(NBPhoneNumber*)number regionCallingFrom:(NSString *)regionCallingFrom error:(NSError **)error
{
NSString *res = nil;
@try {
res = [self formatInOriginalFormat:number regionCallingFrom:regionCallingFrom];
}
@catch (NSException *exception) {
NSDictionary *userInfo = [NSDictionary dictionaryWithObject:exception.reason
forKey:NSLocalizedDescriptionKey];
if (error != NULL)
(*error) = [NSError errorWithDomain:exception.name code:0 userInfo:userInfo];
}
return res;
}
- (NSString *)formatInOriginalFormat:(NBPhoneNumber*)number regionCallingFrom:(NSString *)regionCallingFrom
{
NBMetadataHelper *helper = [[NBMetadataHelper alloc] init];
if ([NBMetadataHelper hasValue:number.rawInput] && ([self hasUnexpectedItalianLeadingZero:number] || [self hasFormattingPatternForNumber:number] == NO)) {
// We check if we have the formatting pattern because without that, we might
// format the number as a group without national prefix.
return number.rawInput;
}
if (number.countryCodeSource == nil) {
return [self format:number numberFormat:NBEPhoneNumberFormatNATIONAL];
}
NSString *formattedNumber = @"";
switch ([number.countryCodeSource integerValue])
{
case NBECountryCodeSourceFROM_NUMBER_WITH_PLUS_SIGN:
formattedNumber = [self format:number numberFormat:NBEPhoneNumberFormatINTERNATIONAL];
break;
case NBECountryCodeSourceFROM_NUMBER_WITH_IDD:
formattedNumber = [self formatOutOfCountryCallingNumber:number regionCallingFrom:regionCallingFrom];
break;
case NBECountryCodeSourceFROM_NUMBER_WITHOUT_PLUS_SIGN:
formattedNumber = [[self format:number numberFormat:NBEPhoneNumberFormatINTERNATIONAL] substringFromIndex:1];
break;
case NBECountryCodeSourceFROM_DEFAULT_COUNTRY:
// Fall-through to default case.
default:
{
NSString *regionCode = [self getRegionCodeForCountryCode:number.countryCode];
// We strip non-digits from the NDD here, and from the raw input later,
// so that we can compare them easily.
NSString *nationalPrefix = [self getNddPrefixForRegion:regionCode stripNonDigits:YES];
NSString *nationalFormat = [self format:number numberFormat:NBEPhoneNumberFormatNATIONAL];
if (nationalPrefix == nil || nationalPrefix.length == 0)
{
// If the region doesn't have a national prefix at all, we can safely
// return the national format without worrying about a national prefix
// being added.
formattedNumber = nationalFormat;
break;
}
// Otherwise, we check if the original number was entered with a national
// prefix.
if ([self rawInputContainsNationalPrefix:number.rawInput nationalPrefix:nationalPrefix regionCode:regionCode])
{
// If so, we can safely return the national format.
formattedNumber = nationalFormat;
break;
}
// Metadata cannot be nil here because getNddPrefixForRegion() (above)
// returns nil if there is no metadata for the region.
NBPhoneMetaData *metadata = [helper getMetadataForRegion:regionCode];
NSString *nationalNumber = [self getNationalSignificantNumber:number];
NBNumberFormat *formatRule = [self chooseFormattingPatternForNumber:metadata.numberFormats nationalNumber:nationalNumber];
// The format rule could still be nil here if the national number was 0
// and there was no raw input (this should not be possible for numbers
// generated by the phonenumber library as they would also not have a
// country calling code and we would have exited earlier).
if (formatRule == nil)
{
formattedNumber = nationalFormat;
break;
}
// When the format we apply to this number doesn't contain national
// prefix, we can just return the national format.
// TODO: Refactor the code below with the code in
// isNationalPrefixPresentIfRequired.
NSString *candidateNationalPrefixRule = formatRule.nationalPrefixFormattingRule;
// We assume that the first-group symbol will never be _before_ the
// national prefix.
NSRange firstGroupRange = [candidateNationalPrefixRule rangeOfString:@"$1"];
if (firstGroupRange.location == NSNotFound)
{
formattedNumber = nationalFormat;
break;
}
if (firstGroupRange.location <= 0)
{
formattedNumber = nationalFormat;
break;
}
candidateNationalPrefixRule = [candidateNationalPrefixRule substringWithRange:NSMakeRange(0, firstGroupRange.location)];
candidateNationalPrefixRule = [self normalizeDigitsOnly:candidateNationalPrefixRule];
if (candidateNationalPrefixRule.length == 0)
{
// National prefix not used when formatting this number.
formattedNumber = nationalFormat;
break;
}
// Otherwise, we need to remove the national prefix from our output.
NBNumberFormat *numFormatCopy = [formatRule copy];
numFormatCopy.nationalPrefixFormattingRule = nil;
formattedNumber = [self formatByPattern:number numberFormat:NBEPhoneNumberFormatNATIONAL userDefinedFormats:@[numFormatCopy]];
break;
}
}
NSString *rawInput = number.rawInput;
// If no digit is inserted/removed/modified as a result of our formatting, we
// return the formatted phone number; otherwise we return the raw input the
// user entered.
if (formattedNumber != nil && rawInput.length > 0)
{
NSString *normalizedFormattedNumber = [self normalizeHelper:formattedNumber normalizationReplacements:DIALLABLE_CHAR_MAPPINGS removeNonMatches:YES];
/** @type {string} */
NSString *normalizedRawInput = [self normalizeHelper:rawInput normalizationReplacements:DIALLABLE_CHAR_MAPPINGS removeNonMatches:YES];
if ([normalizedFormattedNumber isEqualToString:normalizedRawInput] == NO)
{
formattedNumber = rawInput;
}
}
return formattedNumber;
}
/**
* Check if rawInput, which is assumed to be in the national format, has a
* national prefix. The national prefix is assumed to be in digits-only form.
* @param {string} rawInput
* @param {string} nationalPrefix
* @param {string} regionCode
* @return {boolean}
* @private
*/
- (BOOL)rawInputContainsNationalPrefix:(NSString *)rawInput nationalPrefix:(NSString *)nationalPrefix regionCode:(NSString *)regionCode
{
BOOL isValid = NO;
NSString *normalizedNationalNumber = [self normalizeDigitsOnly:rawInput];
if ([self isStartingStringByRegex:normalizedNationalNumber regex:nationalPrefix])
{
// Some Japanese numbers (e.g. 00777123) might be mistaken to contain the
// national prefix when written without it (e.g. 0777123) if we just do
// prefix matching. To tackle that, we check the validity of the number if
// the assumed national prefix is removed (777123 won't be valid in
// Japan).
NSString *subString = [normalizedNationalNumber substringFromIndex:nationalPrefix.length];
NSError *anError = nil;
isValid = [self isValidNumber:[self parse:subString defaultRegion:regionCode error:&anError]];
if (anError != nil)
return NO;
}
return isValid;
}
/**
* Returns NO if a number is from a region whose national significant number
* couldn't contain a leading zero, but has the italian_leading_zero field set
* to NO.
* @param {i18n.phonenumbers.PhoneNumber} number
* @return {boolean}
* @private
*/
- (BOOL)hasUnexpectedItalianLeadingZero:(NBPhoneNumber*)number
{
return number.italianLeadingZero && [self isLeadingZeroPossible:number.countryCode] == NO;
}
/**
* @param {i18n.phonenumbers.PhoneNumber} number
* @return {boolean}
* @private
*/
- (BOOL)hasFormattingPatternForNumber:(NBPhoneNumber*)number
{
NSNumber *countryCallingCode = number.countryCode;
NSString *phoneNumberRegion = [self getRegionCodeForCountryCode:countryCallingCode];
NBPhoneMetaData *metadata = [self getMetadataForRegionOrCallingCode:countryCallingCode regionCode:phoneNumberRegion];
if (metadata == nil)
{
return NO;
}
NSString *nationalNumber = [self getNationalSignificantNumber:number];
NBNumberFormat *formatRule = [self chooseFormattingPatternForNumber:metadata.numberFormats nationalNumber:nationalNumber];
return formatRule != nil;
}
/**
* Formats a phone number for out-of-country dialing purposes.
*
* Note that in this version, if the number was entered originally using alpha
* characters and this version of the number is stored in raw_input, this
* representation of the number will be used rather than the digit
* representation. Grouping information, as specified by characters such as '-'
* and ' ', will be retained.
*
* <p><b>Caveats:</b></p>
* <ul>
* <li>This will not produce good results if the country calling code is both
* present in the raw input _and_ is the start of the national number. This is
* not a problem in the regions which typically use alpha numbers.
* <li>This will also not produce good results if the raw input has any grouping
* information within the first three digits of the national number, and if the
* function needs to strip preceding digits/words in the raw input before these
* digits. Normally people group the first three digits together so this is not
* a huge problem - and will be fixed if it proves to be so.
* </ul>
*
* @param {i18n.phonenumbers.PhoneNumber} number the phone number that needs to
* be formatted.
* @param {string} regionCallingFrom the region where the call is being placed.
* @return {string} the formatted phone number.
*/
- (NSString *)formatOutOfCountryKeepingAlphaChars:(NBPhoneNumber*)number regionCallingFrom:(NSString *)regionCallingFrom error:(NSError **)error
{
NSString *res = nil;
@try {
res = [self formatOutOfCountryKeepingAlphaChars:number regionCallingFrom:regionCallingFrom];
}
@catch (NSException *exception) {
NSDictionary *userInfo = [NSDictionary dictionaryWithObject:exception.reason
forKey:NSLocalizedDescriptionKey];
if (error != NULL)
(*error) = [NSError errorWithDomain:exception.name code:0 userInfo:userInfo];
}
return res;
}
- (NSString *)formatOutOfCountryKeepingAlphaChars:(NBPhoneNumber*)number regionCallingFrom:(NSString *)regionCallingFrom
{
NSString *rawInput = number.rawInput;
// If there is no raw input, then we can't keep alpha characters because there
// aren't any. In this case, we return formatOutOfCountryCallingNumber.
if (rawInput == nil || rawInput.length == 0)
{
return [self formatOutOfCountryCallingNumber:number regionCallingFrom:regionCallingFrom];
}
NSNumber *countryCode = number.countryCode;
if ([self hasValidCountryCallingCode:countryCode] == NO)
{
return rawInput;
}
// Strip any prefix such as country calling code, IDD, that was present. We do
// this by comparing the number in raw_input with the parsed number. To do
// this, first we normalize punctuation. We retain number grouping symbols
// such as ' ' only.
rawInput = [self normalizeHelper:rawInput normalizationReplacements:ALL_PLUS_NUMBER_GROUPING_SYMBOLS removeNonMatches:NO];
//NSLog(@"---- formatOutOfCountryKeepingAlphaChars normalizeHelper rawInput [%@]", rawInput);
// Now we trim everything before the first three digits in the parsed number.
// We choose three because all valid alpha numbers have 3 digits at the start
// - if it does not, then we don't trim anything at all. Similarly, if the
// national number was less than three digits, we don't trim anything at all.
NSString *nationalNumber = [self getNationalSignificantNumber:number];
if (nationalNumber.length > 3)
{
int firstNationalNumberDigit = [self indexOfStringByString:rawInput target:[nationalNumber substringWithRange:NSMakeRange(0, 3)]];
if (firstNationalNumberDigit != -1)
{
rawInput = [rawInput substringFromIndex:firstNationalNumberDigit];
}
}
NBMetadataHelper *helper = [[NBMetadataHelper alloc] init];
NBPhoneMetaData *metadataForRegionCallingFrom = [helper getMetadataForRegion:regionCallingFrom];
if (countryCode.unsignedIntegerValue == NANPA_COUNTRY_CODE_) {
if ([self isNANPACountry:regionCallingFrom]) {
return [NSString stringWithFormat:@"%@ %@", countryCode, rawInput];
}
} else if (metadataForRegionCallingFrom != nil && [countryCode isEqualToNumber:[self getCountryCodeForValidRegion:regionCallingFrom error:nil]]) {
NBNumberFormat *formattingPattern = [self chooseFormattingPatternForNumber:metadataForRegionCallingFrom.numberFormats
nationalNumber:nationalNumber];
if (formattingPattern == nil) {
// If no pattern above is matched, we format the original input.
return rawInput;
}
NBNumberFormat *newFormat = [formattingPattern copy];
// The first group is the first group of digits that the user wrote
// together.
newFormat.pattern = @"(\\d+)(.*)";
// Here we just concatenate them back together after the national prefix
// has been fixed.
newFormat.format = @"$1$2";
// Now we format using this pattern instead of the default pattern, but
// with the national prefix prefixed if necessary.
// This will not work in the cases where the pattern (and not the leading
// digits) decide whether a national prefix needs to be used, since we have
// overridden the pattern to match anything, but that is not the case in the
// metadata to date.
return [self formatNsnUsingPattern:rawInput formattingPattern:newFormat numberFormat:NBEPhoneNumberFormatNATIONAL carrierCode:nil];
}
NSString *internationalPrefixForFormatting = @"";
// If an unsupported region-calling-from is entered, or a country with
// multiple international prefixes, the international format of the number is
// returned, unless there is a preferred international prefix.
if (metadataForRegionCallingFrom != nil) {
NSString *internationalPrefix = metadataForRegionCallingFrom.internationalPrefix;
internationalPrefixForFormatting =
[self matchesEntirely:UNIQUE_INTERNATIONAL_PREFIX string:internationalPrefix] ? internationalPrefix : metadataForRegionCallingFrom.preferredInternationalPrefix;
}
NSString *regionCode = [self getRegionCodeForCountryCode:countryCode];
// Metadata cannot be nil because the country calling code is valid.
NBPhoneMetaData *metadataForRegion = [self getMetadataForRegionOrCallingCode:countryCode regionCode:regionCode];
NSString *formattedExtension = [self maybeGetFormattedExtension:number metadata:metadataForRegion numberFormat:NBEPhoneNumberFormatINTERNATIONAL];
if (internationalPrefixForFormatting.length > 0) {
return [NSString stringWithFormat:@"%@ %@ %@%@", internationalPrefixForFormatting, countryCode, rawInput, formattedExtension];
} else {
// Invalid region entered as country-calling-from (so no metadata was found
// for it) or the region chosen has multiple international dialling
// prefixes.
return [self prefixNumberWithCountryCallingCode:countryCode phoneNumberFormat:NBEPhoneNumberFormatINTERNATIONAL formattedNationalNumber:rawInput formattedExtension:formattedExtension];
}
}
/**
* Note in some regions, the national number can be written in two completely
* different ways depending on whether it forms part of the NATIONAL format or
* INTERNATIONAL format. The numberFormat parameter here is used to specify
* which format to use for those cases. If a carrierCode is specified, this will
* be inserted into the formatted string to replace $CC.
*
* @param {string} number a string of characters representing a phone number.
* @param {i18n.phonenumbers.PhoneMetadata} metadata the metadata for the
* region that we think this number is from.
* @param {i18n.phonenumbers.PhoneNumberFormat} numberFormat the format the
* phone number should be formatted into.
* @param {string=} opt_carrierCode
* @return {string} the formatted phone number.
* @private
*/
- (NSString *)formatNsn:(NSString *)phoneNumber metadata:(NBPhoneMetaData*)metadata phoneNumberFormat:(NBEPhoneNumberFormat)numberFormat carrierCode:(NSString *)opt_carrierCode
{
NSMutableArray *intlNumberFormats = metadata.intlNumberFormats;
// When the intlNumberFormats exists, we use that to format national number
// for the INTERNATIONAL format instead of using the numberDesc.numberFormats.
NSArray *availableFormats = ([intlNumberFormats count] <= 0 || numberFormat == NBEPhoneNumberFormatNATIONAL) ? metadata.numberFormats : intlNumberFormats;
NBNumberFormat *formattingPattern = [self chooseFormattingPatternForNumber:availableFormats nationalNumber:phoneNumber];
if (formattingPattern == nil) {
return phoneNumber;
}
return [self formatNsnUsingPattern:phoneNumber formattingPattern:formattingPattern numberFormat:numberFormat carrierCode:opt_carrierCode];
}
/**
* @param {Array.<i18n.phonenumbers.NumberFormat>} availableFormats the
* available formats the phone number could be formatted into.
* @param {string} nationalNumber a string of characters representing a phone
* number.
* @return {i18n.phonenumbers.NumberFormat}
* @private
*/
- (NBNumberFormat*)chooseFormattingPatternForNumber:(NSArray*)availableFormats nationalNumber:(NSString *)nationalNumber
{
for (NBNumberFormat *numFormat in availableFormats) {
unsigned int size = (unsigned int)[numFormat.leadingDigitsPatterns count];
// We always use the last leading_digits_pattern, as it is the most detailed.
if (size == 0 || [self stringPositionByRegex:nationalNumber regex:[numFormat.leadingDigitsPatterns lastObject]] == 0) {
if ([self matchesEntirely:numFormat.pattern string:nationalNumber]) {
return numFormat;
}
}
}
return nil;
}
/**
* Note that carrierCode is optional - if nil or an empty string, no carrier
* code replacement will take place.
*
* @param {string} nationalNumber a string of characters representing a phone
* number.
* @param {i18n.phonenumbers.NumberFormat} formattingPattern the formatting rule
* the phone number should be formatted into.
* @param {i18n.phonenumbers.PhoneNumberFormat} numberFormat the format the
* phone number should be formatted into.
* @param {string=} opt_carrierCode
* @return {string} the formatted phone number.
* @private
*/
- (NSString *)formatNsnUsingPattern:(NSString *)nationalNumber formattingPattern:(NBNumberFormat*)formattingPattern numberFormat:(NBEPhoneNumberFormat)numberFormat carrierCode:(NSString *)opt_carrierCode
{
NSString *numberFormatRule = formattingPattern.format;
NSString *domesticCarrierCodeFormattingRule = formattingPattern.domesticCarrierCodeFormattingRule;
NSString *formattedNationalNumber = @"";
if (numberFormat == NBEPhoneNumberFormatNATIONAL && [NBMetadataHelper hasValue:opt_carrierCode] && domesticCarrierCodeFormattingRule.length > 0) {
// Replace the $CC in the formatting rule with the desired carrier code.
NSString *carrierCodeFormattingRule = [self replaceStringByRegex:domesticCarrierCodeFormattingRule regex:CC_PATTERN withTemplate:opt_carrierCode];
// Now replace the $FG in the formatting rule with the first group and
// the carrier code combined in the appropriate way.
numberFormatRule = [self replaceFirstStringByRegex:numberFormatRule regex:FIRST_GROUP_PATTERN
withTemplate:carrierCodeFormattingRule];
formattedNationalNumber = [self replaceStringByRegex:nationalNumber regex:formattingPattern.pattern withTemplate:numberFormatRule];
} else {
// Use the national prefix formatting rule instead.
NSString *nationalPrefixFormattingRule = formattingPattern.nationalPrefixFormattingRule;
if (numberFormat == NBEPhoneNumberFormatNATIONAL && [NBMetadataHelper hasValue:nationalPrefixFormattingRule]) {
NSString *replacePattern = [self replaceFirstStringByRegex:numberFormatRule regex:FIRST_GROUP_PATTERN withTemplate:nationalPrefixFormattingRule];
formattedNationalNumber = [self replaceStringByRegex:nationalNumber regex:formattingPattern.pattern withTemplate:replacePattern];
} else {
formattedNationalNumber = [self replaceStringByRegex:nationalNumber regex:formattingPattern.pattern withTemplate:numberFormatRule];
}
}
if (numberFormat == NBEPhoneNumberFormatRFC3966) {
// Strip any leading punctuation.
formattedNationalNumber = [self replaceStringByRegex:formattedNationalNumber regex:[NSString stringWithFormat:@"^%@", SEPARATOR_PATTERN] withTemplate:@""];
// Replace the rest with a dash between each number group.
formattedNationalNumber = [self replaceStringByRegex:formattedNationalNumber regex:SEPARATOR_PATTERN withTemplate:@"-"];
}
return formattedNationalNumber;
}
/**
* Gets a valid number for the specified region.
*
* @param {string} regionCode the region for which an example number is needed.
* @return {i18n.phonenumbers.PhoneNumber} a valid fixed-line number for the
* specified region. Returns nil when the metadata does not contain such
* information, or the region 001 is passed in. For 001 (representing non-
* geographical numbers), call {@link #getExampleNumberForNonGeoEntity}
* instead.
*/
- (NBPhoneNumber*)getExampleNumber:(NSString *)regionCode error:(NSError *__autoreleasing *)error
{
NBPhoneNumber *res = [self getExampleNumberForType:regionCode type:NBEPhoneNumberTypeFIXED_LINE error:error];
return res;
}
/**
* Gets a valid number for the specified region and number type.
*
* @param {string} regionCode the region for which an example number is needed.
* @param {i18n.phonenumbers.PhoneNumberType} type the type of number that is
* needed.
* @return {i18n.phonenumbers.PhoneNumber} a valid number for the specified
* region and type. Returns nil when the metadata does not contain such
* information or if an invalid region or region 001 was entered.
* For 001 (representing non-geographical numbers), call
* {@link #getExampleNumberForNonGeoEntity} instead.
*/
- (NBPhoneNumber*)getExampleNumberForType:(NSString *)regionCode type:(NBEPhoneNumberType)type error:(NSError *__autoreleasing *)error
{
NBPhoneNumber *res = nil;
if ([self isValidRegionCode:regionCode] == NO) {
return nil;
}
NBMetadataHelper *helper = [[NBMetadataHelper alloc] init];
NBPhoneNumberDesc *desc = [self getNumberDescByType:[helper getMetadataForRegion:regionCode] type:type];
if ([NBMetadataHelper hasValue:desc.exampleNumber ]) {
return [self parse:desc.exampleNumber defaultRegion:regionCode error:error];
}
return res;
}
/**
* Gets a valid number for the specified country calling code for a
* non-geographical entity.
*
* @param {number} countryCallingCode the country calling code for a
* non-geographical entity.
* @return {i18n.phonenumbers.PhoneNumber} a valid number for the
* non-geographical entity. Returns nil when the metadata does not contain
* such information, or the country calling code passed in does not belong
* to a non-geographical entity.
*/
- (NBPhoneNumber*)getExampleNumberForNonGeoEntity:(NSNumber *)countryCallingCode error:(NSError *__autoreleasing *)error
{
NBPhoneNumber *res = nil;
NBMetadataHelper *helper = [[NBMetadataHelper alloc] init];
NBPhoneMetaData *metadata = [helper getMetadataForNonGeographicalRegion:countryCallingCode];
if (metadata != nil) {
NBPhoneNumberDesc *desc = metadata.generalDesc;
if ([NBMetadataHelper hasValue:desc.exampleNumber]) {
NSString *callCode = [NSString stringWithFormat:@"+%@%@", countryCallingCode, desc.exampleNumber];
return [self parse:callCode defaultRegion:NB_UNKNOWN_REGION error:error];
}
}
return res;
}
/**
* Gets the formatted extension of a phone number, if the phone number had an
* extension specified. If not, it returns an empty string.
*
* @param {i18n.phonenumbers.PhoneNumber} number the PhoneNumber that might have
* an extension.
* @param {i18n.phonenumbers.PhoneMetadata} metadata the metadata for the
* region that we think this number is from.
* @param {i18n.phonenumbers.PhoneNumberFormat} numberFormat the format the
* phone number should be formatted into.
* @return {string} the formatted extension if any.
* @private
*/
- (NSString *)maybeGetFormattedExtension:(NBPhoneNumber*)number metadata:(NBPhoneMetaData*)metadata numberFormat:(NBEPhoneNumberFormat)numberFormat
{
if ([NBMetadataHelper hasValue:number.extension] == NO) {
return @"";
} else {
if (numberFormat == NBEPhoneNumberFormatRFC3966) {
return [NSString stringWithFormat:@"%@%@", RFC3966_EXTN_PREFIX, number.extension];
} else {
if ([NBMetadataHelper hasValue:metadata.preferredExtnPrefix]) {
return [NSString stringWithFormat:@"%@%@", metadata.preferredExtnPrefix, number.extension];
} else {
return [NSString stringWithFormat:@"%@%@", DEFAULT_EXTN_PREFIX, number.extension];
}
}
}
}
/**
* @param {i18n.phonenumbers.PhoneMetadata} metadata
* @param {i18n.phonenumbers.PhoneNumberType} type
* @return {i18n.phonenumbers.PhoneNumberDesc}
* @private
*/
- (NBPhoneNumberDesc*)getNumberDescByType:(NBPhoneMetaData*)metadata type:(NBEPhoneNumberType)type
{
switch (type)
{
case NBEPhoneNumberTypePREMIUM_RATE:
return metadata.premiumRate;
case NBEPhoneNumberTypeTOLL_FREE:
return metadata.tollFree;
case NBEPhoneNumberTypeMOBILE:
if (metadata.mobile == nil) return metadata.generalDesc;
return metadata.mobile;
case NBEPhoneNumberTypeFIXED_LINE:
case NBEPhoneNumberTypeFIXED_LINE_OR_MOBILE:
if (metadata.fixedLine == nil) return metadata.generalDesc;
return metadata.fixedLine;
case NBEPhoneNumberTypeSHARED_COST:
return metadata.sharedCost;
case NBEPhoneNumberTypeVOIP:
return metadata.voip;
case NBEPhoneNumberTypePERSONAL_NUMBER:
return metadata.personalNumber;
case NBEPhoneNumberTypePAGER:
return metadata.pager;
case NBEPhoneNumberTypeUAN:
return metadata.uan;
case NBEPhoneNumberTypeVOICEMAIL:
return metadata.voicemail;
default:
return metadata.generalDesc;
}
}
/**
* Gets the type of a phone number.
*
* @param {i18n.phonenumbers.PhoneNumber} number the phone number that we want
* to know the type.
* @return {i18n.phonenumbers.PhoneNumberType} the type of the phone number.
*/
- (NBEPhoneNumberType)getNumberType:(NBPhoneNumber*)phoneNumber
{
NSString *regionCode = [self getRegionCodeForNumber:phoneNumber];
NBPhoneMetaData *metadata = [self getMetadataForRegionOrCallingCode:phoneNumber.countryCode regionCode:regionCode];
if (metadata == nil)
{
return NBEPhoneNumberTypeUNKNOWN;
}
NSString *nationalSignificantNumber = [self getNationalSignificantNumber:phoneNumber];
return [self getNumberTypeHelper:nationalSignificantNumber metadata:metadata];
}
/**
* @param {string} nationalNumber
* @param {i18n.phonenumbers.PhoneMetadata} metadata
* @return {i18n.phonenumbers.PhoneNumberType}
* @private
*/
- (NBEPhoneNumberType)getNumberTypeHelper:(NSString *)nationalNumber metadata:(NBPhoneMetaData*)metadata
{
NBPhoneNumberDesc *generalNumberDesc = metadata.generalDesc;
//NSLog(@"getNumberTypeHelper - UNKNOWN 1");
if ([NBMetadataHelper hasValue:generalNumberDesc.nationalNumberPattern] == NO ||
[self isNumberMatchingDesc:nationalNumber numberDesc:generalNumberDesc] == NO)
{
//NSLog(@"getNumberTypeHelper - UNKNOWN 2");
return NBEPhoneNumberTypeUNKNOWN;
}
//NSLog(@"getNumberTypeHelper - PREMIUM_RATE 1");
if ([self isNumberMatchingDesc:nationalNumber numberDesc:metadata.premiumRate])
{
//NSLog(@"getNumberTypeHelper - PREMIUM_RATE 2");
return NBEPhoneNumberTypePREMIUM_RATE;
}
//NSLog(@"getNumberTypeHelper - TOLL_FREE 1");
if ([self isNumberMatchingDesc:nationalNumber numberDesc:metadata.tollFree])
{
//NSLog(@"getNumberTypeHelper - TOLL_FREE 2");
return NBEPhoneNumberTypeTOLL_FREE;
}
//NSLog(@"getNumberTypeHelper - SHARED_COST 1");
if ([self isNumberMatchingDesc:nationalNumber numberDesc:metadata.sharedCost])
{
//NSLog(@"getNumberTypeHelper - SHARED_COST 2");
return NBEPhoneNumberTypeSHARED_COST;
}
//NSLog(@"getNumberTypeHelper - VOIP 1");
if ([self isNumberMatchingDesc:nationalNumber numberDesc:metadata.voip])
{
//NSLog(@"getNumberTypeHelper - VOIP 2");
return NBEPhoneNumberTypeVOIP;
}
//NSLog(@"getNumberTypeHelper - PERSONAL_NUMBER 1");
if ([self isNumberMatchingDesc:nationalNumber numberDesc:metadata.personalNumber])
{
//NSLog(@"getNumberTypeHelper - PERSONAL_NUMBER 2");
return NBEPhoneNumberTypePERSONAL_NUMBER;
}
//NSLog(@"getNumberTypeHelper - PAGER 1");
if ([self isNumberMatchingDesc:nationalNumber numberDesc:metadata.pager])
{
//NSLog(@"getNumberTypeHelper - PAGER 2");
return NBEPhoneNumberTypePAGER;
}
//NSLog(@"getNumberTypeHelper - UAN 1");
if ([self isNumberMatchingDesc:nationalNumber numberDesc:metadata.uan])
{
//NSLog(@"getNumberTypeHelper - UAN 2");
return NBEPhoneNumberTypeUAN;
}
//NSLog(@"getNumberTypeHelper - VOICEMAIL 1");
if ([self isNumberMatchingDesc:nationalNumber numberDesc:metadata.voicemail])
{
//NSLog(@"getNumberTypeHelper - VOICEMAIL 2");
return NBEPhoneNumberTypeVOICEMAIL;
}
if ([self isNumberMatchingDesc:nationalNumber numberDesc:metadata.fixedLine])
{
if (metadata.sameMobileAndFixedLinePattern)
{
//NSLog(@"getNumberTypeHelper - FIXED_LINE_OR_MOBILE");
return NBEPhoneNumberTypeFIXED_LINE_OR_MOBILE;
}
else if ([self isNumberMatchingDesc:nationalNumber numberDesc:metadata.mobile])
{
//NSLog(@"getNumberTypeHelper - FIXED_LINE_OR_MOBILE");
return NBEPhoneNumberTypeFIXED_LINE_OR_MOBILE;
}
//NSLog(@"getNumberTypeHelper - FIXED_LINE");
return NBEPhoneNumberTypeFIXED_LINE;
}
// Otherwise, test to see if the number is mobile. Only do this if certain
// that the patterns for mobile and fixed line aren't the same.
if ([metadata sameMobileAndFixedLinePattern] == NO && [self isNumberMatchingDesc:nationalNumber numberDesc:metadata.mobile]) {
return NBEPhoneNumberTypeMOBILE;
}
return NBEPhoneNumberTypeUNKNOWN;
}
/**
* @param {string} nationalNumber
* @param {i18n.phonenumbers.PhoneNumberDesc} numberDesc
* @return {boolean}
* @private
*/
- (BOOL)isNumberMatchingDesc:(NSString *)nationalNumber numberDesc:(NBPhoneNumberDesc*)numberDesc
{
if (numberDesc == nil) {
return NO;
}
if ([NBMetadataHelper hasValue:numberDesc.possibleNumberPattern] == NO || [numberDesc.possibleNumberPattern isEqual:@"NA"]) {
return [self matchesEntirely:numberDesc.nationalNumberPattern string:nationalNumber];
}
if ([NBMetadataHelper hasValue:numberDesc.nationalNumberPattern] == NO || [numberDesc.nationalNumberPattern isEqual:@"NA"]) {
return [self matchesEntirely:numberDesc.possibleNumberPattern string:nationalNumber];
}
return [self matchesEntirely:numberDesc.possibleNumberPattern string:nationalNumber] &&
[self matchesEntirely:numberDesc.nationalNumberPattern string:nationalNumber];
}
/**
* Tests whether a phone number matches a valid pattern. Note this doesn't
* verify the number is actually in use, which is impossible to tell by just
* looking at a number itself.
*
* @param {i18n.phonenumbers.PhoneNumber} number the phone number that we want
* to validate.
* @return {boolean} a boolean that indicates whether the number is of a valid
* pattern.
*/
- (BOOL)isValidNumber:(NBPhoneNumber*)number
{
NSString *regionCode = [self getRegionCodeForNumber:number];
return [self isValidNumberForRegion:number regionCode:regionCode];
}
/**
* Tests whether a phone number is valid for a certain region. Note this doesn't
* verify the number is actually in use, which is impossible to tell by just
* looking at a number itself. If the country calling code is not the same as
* the country calling code for the region, this immediately exits with NO.
* After this, the specific number pattern rules for the region are examined.
* This is useful for determining for example whether a particular number is
* valid for Canada, rather than just a valid NANPA number.
* Warning: In most cases, you want to use {@link #isValidNumber} instead. For
* example, this method will mark numbers from British Crown dependencies such
* as the Isle of Man as invalid for the region "GB" (United Kingdom), since it
* has its own region code, "IM", which may be undesirable.
*
* @param {i18n.phonenumbers.PhoneNumber} number the phone number that we want
* to validate.
* @param {?string} regionCode the region that we want to validate the phone
* number for.
* @return {boolean} a boolean that indicates whether the number is of a valid
* pattern.
*/
- (BOOL)isValidNumberForRegion:(NBPhoneNumber*)number regionCode:(NSString *)regionCode
{
NSNumber *countryCode = [number.countryCode copy];
NBPhoneMetaData *metadata = [self getMetadataForRegionOrCallingCode:countryCode regionCode:regionCode];
if (metadata == nil ||
([NB_REGION_CODE_FOR_NON_GEO_ENTITY isEqualToString:regionCode] == NO &&
![countryCode isEqualToNumber:[self getCountryCodeForValidRegion:regionCode error:nil]])) {
// Either the region code was invalid, or the country calling code for this
// number does not match that of the region code.
return NO;
}
NBPhoneNumberDesc *generalNumDesc = metadata.generalDesc;
NSString *nationalSignificantNumber = [self getNationalSignificantNumber:number];
// For regions where we don't have metadata for PhoneNumberDesc, we treat any
// number passed in as a valid number if its national significant number is
// between the minimum and maximum lengths defined by ITU for a national
// significant number.
if ([NBMetadataHelper hasValue:generalNumDesc.nationalNumberPattern] == NO) {
unsigned int numberLength = (unsigned int)nationalSignificantNumber.length;
return numberLength > MIN_LENGTH_FOR_NSN_ && numberLength <= MAX_LENGTH_FOR_NSN_;
}
return [self getNumberTypeHelper:nationalSignificantNumber metadata:metadata] != NBEPhoneNumberTypeUNKNOWN;
}
/**
* Returns the region where a phone number is from. This could be used for
* geocoding at the region level.
*
* @param {i18n.phonenumbers.PhoneNumber} number the phone number whose origin
* we want to know.
* @return {?string} the region where the phone number is from, or nil
* if no region matches this calling code.
*/
- (NSString *)getRegionCodeForNumber:(NBPhoneNumber*)phoneNumber
{
if (phoneNumber == nil) {
return nil;
}
NBMetadataHelper *helper = [[NBMetadataHelper alloc] init];
NSArray *regionCodes = [helper regionCodeFromCountryCode:phoneNumber.countryCode];
if (regionCodes == nil || [regionCodes count] <= 0) {
return nil;
}
if ([regionCodes count] == 1) {
return [regionCodes objectAtIndex:0];
} else {
return [self getRegionCodeForNumberFromRegionList:phoneNumber regionCodes:regionCodes];
}
}
/**
* @param {i18n.phonenumbers.PhoneNumber} number
* @param {Array.<string>} regionCodes
* @return {?string}
* @private
*/
- (NSString *)getRegionCodeForNumberFromRegionList:(NBPhoneNumber*)phoneNumber regionCodes:(NSArray*)regionCodes
{
NSString *nationalNumber = [self getNationalSignificantNumber:phoneNumber];
unsigned int regionCodesCount = (unsigned int)[regionCodes count];
NBMetadataHelper *helper = [[NBMetadataHelper alloc] init];
for (unsigned int i = 0; i<regionCodesCount; i++) {
NSString *regionCode = [regionCodes objectAtIndex:i];
NBPhoneMetaData *metadata = [helper getMetadataForRegion:regionCode];
if ([NBMetadataHelper hasValue:metadata.leadingDigits]) {
if ([self stringPositionByRegex:nationalNumber regex:metadata.leadingDigits] == 0) {
return regionCode;
}
} else if ([self getNumberTypeHelper:nationalNumber metadata:metadata] != NBEPhoneNumberTypeUNKNOWN) {
return regionCode;
}
}
return nil;
}
/**
* Returns the region code that matches the specific country calling code. In
* the case of no region code being found, ZZ will be returned. In the case of
* multiple regions, the one designated in the metadata as the 'main' region for
* this calling code will be returned.
*
* @param {number} countryCallingCode the country calling code.
* @return {string}
*/
- (NSString *)getRegionCodeForCountryCode:(NSNumber *)countryCallingCode
{
NBMetadataHelper *helper = [[NBMetadataHelper alloc] init];
NSArray *regionCodes = [helper regionCodeFromCountryCode:countryCallingCode];
return regionCodes == nil ? NB_UNKNOWN_REGION : [regionCodes objectAtIndex:0];
}
/**
* 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 the country calling code.
* @return {Array.<string>}
*/
- (NSArray*)getRegionCodesForCountryCode:(NSNumber *)countryCallingCode
{
NBMetadataHelper *helper = [[NBMetadataHelper alloc] init];
NSArray *regionCodes = [helper regionCodeFromCountryCode:countryCallingCode];
return regionCodes == nil ? nil : regionCodes;
}
/**
* Returns the country calling code for a specific region. For example, this
* would be 1 for the United States, and 64 for New Zealand.
*
* @param {?string} regionCode the region that we want to get the country
* calling code for.
* @return {number} the country calling code for the region denoted by
* regionCode.
*/
- (NSNumber*)getCountryCodeForRegion:(NSString *)regionCode
{
if ([self isValidRegionCode:regionCode] == NO) {
return @0;
}
NSError *error = nil;
NSNumber *res = [self getCountryCodeForValidRegion:regionCode error:&error];
if (error != nil) {
return @0;
}
return res;
}
/**
* Returns the country calling code for a specific region. For example, this
* would be 1 for the United States, and 64 for New Zealand. Assumes the region
* is already valid.
*
* @param {?string} regionCode the region that we want to get the country
* calling code for.
* @return {number} the country calling code for the region denoted by
* regionCode.
* @throws {string} if the region is invalid
* @private
*/
- (NSNumber*)getCountryCodeForValidRegion:(NSString *)regionCode error:(NSError**)error
{
NBMetadataHelper *helper = [[NBMetadataHelper alloc] init];
NBPhoneMetaData *metadata = [helper getMetadataForRegion:regionCode];
if (metadata == nil) {
NSDictionary *userInfo = [NSDictionary dictionaryWithObject:[NSString stringWithFormat:@"Invalid region code:%@", regionCode]
forKey:NSLocalizedDescriptionKey];
if (error != NULL) {
(*error) = [NSError errorWithDomain:@"INVALID_REGION_CODE" code:0 userInfo:userInfo];
}
return @-1;
}
return metadata.countryCode;
}
/**
* Returns the national dialling prefix for a specific region. For example, this
* would be 1 for the United States, and 0 for New Zealand. Set stripNonDigits
* to NO to strip symbols like '~' (which indicates a wait for a dialling
* tone) from the prefix returned. If no national prefix is present, we return
* nil.
*
* <p>Warning: Do not use this method for do-your-own formatting - for some
* regions, the national dialling prefix is used only for certain types of
* numbers. Use the library's formatting functions to prefix the national prefix
* when required.
*
* @param {?string} regionCode the region that we want to get the dialling
* prefix for.
* @param {boolean} stripNonDigits NO to strip non-digits from the national
* dialling prefix.
* @return {?string} the dialling prefix for the region denoted by
* regionCode.
*/
- (NSString *)getNddPrefixForRegion:(NSString *)regionCode stripNonDigits:(BOOL)stripNonDigits
{
NBMetadataHelper *helper = [[NBMetadataHelper alloc] init];
NBPhoneMetaData *metadata = [helper getMetadataForRegion:regionCode];
if (metadata == nil) {
return nil;
}
NSString *nationalPrefix = metadata.nationalPrefix;
// If no national prefix was found, we return nil.
if (nationalPrefix.length == 0) {
return nil;
}
if (stripNonDigits) {
// Note: if any other non-numeric symbols are ever used in national
// prefixes, these would have to be removed here as well.
nationalPrefix = [nationalPrefix stringByReplacingOccurrencesOfString:@"~" withString:@""];
}
return nationalPrefix;
}
/**
* Checks if this is a region under the North American Numbering Plan
* Administration (NANPA).
*
* @param {?string} regionCode the ISO 3166-1 two-letter region code.
* @return {boolean} NO if regionCode is one of the regions under NANPA.
*/
- (BOOL)isNANPACountry:(NSString *)regionCode
{
BOOL isExists = NO;
NBMetadataHelper *helper = [[NBMetadataHelper alloc] init];
NSArray *res = [helper regionCodeFromCountryCode:[NSNumber numberWithUnsignedInteger:NANPA_COUNTRY_CODE_]];
for (NSString *inRegionCode in res) {
if ([inRegionCode isEqualToString:regionCode.uppercaseString]) {
isExists = YES;
}
}
return regionCode != nil && isExists;
}
/**
* Checks whether countryCode represents the country calling code from a region
* whose national significant number could contain a leading zero. An example of
* such a region is Italy. Returns NO if no metadata for the country is
* found.
*
* @param {number} countryCallingCode the country calling code.
* @return {boolean}
*/
- (BOOL)isLeadingZeroPossible:(NSNumber *)countryCallingCode
{
NBPhoneMetaData *mainMetadataForCallingCode = [self getMetadataForRegionOrCallingCode:countryCallingCode
regionCode:[self getRegionCodeForCountryCode:countryCallingCode]];
return mainMetadataForCallingCode != nil && mainMetadataForCallingCode.leadingZeroPossible;
}
/**
* Checks if the number is a valid vanity (alpha) number such as 800 MICROSOFT.
* A valid vanity number will start with at least 3 digits and will have three
* or more alpha characters. This does not do region-specific checks - to work
* out if this number is actually valid for a region, it should be parsed and
* methods such as {@link #isPossibleNumberWithReason} and
* {@link #isValidNumber} should be used.
*
* @param {string} number the number that needs to be checked.
* @return {boolean} NO if the number is a valid vanity number.
*/
- (BOOL)isAlphaNumber:(NSString *)number
{
if ([self isViablePhoneNumber:number] == NO) {
// Number is too short, or doesn't match the basic phone number pattern.
return NO;
}
NBMetadataHelper *helper = [[NBMetadataHelper alloc] init];
number = [helper normalizeNonBreakingSpace:number];
/** @type {!goog.string.StringBuffer} */
NSString *strippedNumber = [number copy];
[self maybeStripExtension:&strippedNumber];
return [self matchesEntirely:VALID_ALPHA_PHONE_PATTERN_STRING string:strippedNumber];
}
/**
* Convenience wrapper around {@link #isPossibleNumberWithReason}. Instead of
* returning the reason for failure, this method returns a boolean value.
*
* @param {i18n.phonenumbers.PhoneNumber} number the number that needs to be
* checked.
* @return {boolean} NO if the number is possible.
*/
- (BOOL)isPossibleNumber:(NBPhoneNumber*)number error:(NSError**)error
{
BOOL res = NO;
@try {
res = [self isPossibleNumber:number];
}
@catch (NSException *exception) {
NSDictionary *userInfo = [NSDictionary dictionaryWithObject:exception.reason
forKey:NSLocalizedDescriptionKey];
if (error != NULL)
(*error) = [NSError errorWithDomain:exception.name code:0 userInfo:userInfo];
}
return res;
}
- (BOOL)isPossibleNumber:(NBPhoneNumber*)number
{
return [self isPossibleNumberWithReason:number] == NBEValidationResultIS_POSSIBLE;
}
/**
* Helper method to check a number against a particular pattern and determine
* whether it matches, or is too short or too long. Currently, if a number
* pattern suggests that numbers of length 7 and 10 are possible, and a number
* in between these possible lengths is entered, such as of length 8, this will
* return TOO_LONG.
*
* @param {string} numberPattern
* @param {string} number
* @return {ValidationResult}
* @private
*/
- (NBEValidationResult)testNumberLengthAgainstPattern:(NSString *)numberPattern number:(NSString *)number
{
if ([self matchesEntirely:numberPattern string:number]) {
return NBEValidationResultIS_POSSIBLE;
}
if ([self stringPositionByRegex:number regex:numberPattern] == 0) {
return NBEValidationResultTOO_LONG;
} else {
return NBEValidationResultTOO_SHORT;
}
}
/**
* Check whether a phone number is a possible number. It provides a more lenient
* check than {@link #isValidNumber} in the following sense:
* <ol>
* <li>It only checks the length of phone numbers. In particular, it doesn't
* check starting digits of the number.
* <li>It doesn't attempt to figure out the type of the number, but uses general
* rules which applies to all types of phone numbers in a region. Therefore, it
* is much faster than isValidNumber.
* <li>For fixed line numbers, many regions have the concept of area code, which
* together with subscriber number constitute the national significant number.
* It is sometimes okay to dial the subscriber number only when dialing in the
* same area. This function will return NO if the subscriber-number-only
* version is passed in. On the other hand, because isValidNumber validates
* using information on both starting digits (for fixed line numbers, that would
* most likely be area codes) and length (obviously includes the length of area
* codes for fixed line numbers), it will return NO for the
* subscriber-number-only version.
* </ol>
*
* @param {i18n.phonenumbers.PhoneNumber} number the number that needs to be
* checked.
* @return {ValidationResult} a
* ValidationResult object which indicates whether the number is possible.
*/
- (NBEValidationResult)isPossibleNumberWithReason:(NBPhoneNumber*)number error:(NSError *__autoreleasing *)error
{
NBEValidationResult res = NBEValidationResultUNKNOWN;
@try {
res = [self isPossibleNumberWithReason:number];
}
@catch (NSException *exception) {
NSDictionary *userInfo = [NSDictionary dictionaryWithObject:exception.reason
forKey:NSLocalizedDescriptionKey];
if (error != NULL)
(*error) = [NSError errorWithDomain:exception.name code:0 userInfo:userInfo];
}
return res;
}
- (NBEValidationResult)isPossibleNumberWithReason:(NBPhoneNumber*)number
{
NSString *nationalNumber = [self getNationalSignificantNumber:number];
NSNumber *countryCode = number.countryCode;
// Note: For Russian Fed and NANPA numbers, we just use the rules from the
// default region (US or Russia) since the getRegionCodeForNumber will not
// work if the number is possible but not valid. This would need to be
// revisited if the possible number pattern ever differed between various
// regions within those plans.
if ([self hasValidCountryCallingCode:countryCode] == NO) {
return NBEValidationResultINVALID_COUNTRY_CODE;
}
NSString *regionCode = [self getRegionCodeForCountryCode:countryCode];
// Metadata cannot be nil because the country calling code is valid.
NBPhoneMetaData *metadata = [self getMetadataForRegionOrCallingCode:countryCode regionCode:regionCode];
NBPhoneNumberDesc *generalNumDesc = metadata.generalDesc;
// Handling case of numbers with no metadata.
if ([NBMetadataHelper hasValue:generalNumDesc.nationalNumberPattern] == NO) {
unsigned int numberLength = (unsigned int)nationalNumber.length;
if (numberLength < MIN_LENGTH_FOR_NSN_) {
return NBEValidationResultTOO_SHORT;
} else if (numberLength > MAX_LENGTH_FOR_NSN_) {
return NBEValidationResultTOO_LONG;
} else {
return NBEValidationResultIS_POSSIBLE;
}
}
NSString *possibleNumberPattern = generalNumDesc.possibleNumberPattern;
return [self testNumberLengthAgainstPattern:possibleNumberPattern number:nationalNumber];
}
/**
* Check whether a phone number is a possible number given a number in the form
* of a string, and the region where the number could be dialed from. It
* provides a more lenient check than {@link #isValidNumber}. See
* {@link #isPossibleNumber} for details.
*
* <p>This method first parses the number, then invokes
* {@link #isPossibleNumber} with the resultant PhoneNumber object.
*
* @param {string} number the number that needs to be checked, in the form of a
* string.
* @param {string} regionDialingFrom the region that we are expecting the number
* to be dialed from.
* Note this is different from the region where the number belongs.
* For example, the number +1 650 253 0000 is a number that belongs to US.
* When written in this form, it can be dialed from any region. When it is
* written as 00 1 650 253 0000, it can be dialed from any region which uses
* an international dialling prefix of 00. When it is written as
* 650 253 0000, it can only be dialed from within the US, and when written
* as 253 0000, it can only be dialed from within a smaller area in the US
* (Mountain View, CA, to be more specific).
* @return {boolean} NO if the number is possible.
*/
- (BOOL)isPossibleNumberString:(NSString *)number regionDialingFrom:(NSString *)regionDialingFrom error:(NSError**)error
{
NBMetadataHelper *helper = [[NBMetadataHelper alloc] init];
number = [helper normalizeNonBreakingSpace:number];
BOOL res = [self isPossibleNumber:[self parse:number defaultRegion:regionDialingFrom error:error]];
return res;
}
/**
* Attempts to extract a valid number from a phone number that is too long to be
* valid, and resets the PhoneNumber object passed in to that valid version. If
* no valid number could be extracted, the PhoneNumber object passed in will not
* be modified.
* @param {i18n.phonenumbers.PhoneNumber} number a PhoneNumber object which
* contains a number that is too long to be valid.
* @return {boolean} NO if a valid phone number can be successfully extracted.
*/
- (BOOL)truncateTooLongNumber:(NBPhoneNumber*)number
{
if ([self isValidNumber:number]) {
return YES;
}
NBPhoneNumber *numberCopy = [number copy];
NSNumber *nationalNumber = number.nationalNumber;
do {
nationalNumber = [NSNumber numberWithLongLong:(long long)floor(nationalNumber.unsignedLongLongValue / 10)];
numberCopy.nationalNumber = [nationalNumber copy];
if ([nationalNumber isEqualToNumber:@0] || [self isPossibleNumberWithReason:numberCopy] == NBEValidationResultTOO_SHORT) {
return NO;
}
}
while ([self isValidNumber:numberCopy] == NO);
number.nationalNumber = nationalNumber;
return YES;
}
/**
* Extracts country calling code from fullNumber, returns it and places the
* remaining number in nationalNumber. It assumes that the leading plus sign or
* IDD has already been removed. Returns 0 if fullNumber doesn't start with a
* valid country calling code, and leaves nationalNumber unmodified.
*
* @param {!goog.string.StringBuffer} fullNumber
* @param {!goog.string.StringBuffer} nationalNumber
* @return {number}
*/
- (NSNumber *)extractCountryCode:(NSString *)fullNumber nationalNumber:(NSString **)nationalNumber
{
NBMetadataHelper *helper = [[NBMetadataHelper alloc] init];
fullNumber = [helper normalizeNonBreakingSpace:fullNumber];
if ((fullNumber.length == 0) || ([[fullNumber substringToIndex:1] isEqualToString:@"0"])) {
// Country codes do not begin with a '0'.
return @0;
}
unsigned int numberLength = (unsigned int)fullNumber.length;
unsigned int maxCountryCode = MAX_LENGTH_COUNTRY_CODE_;
if ([fullNumber hasPrefix:@"+"]) {
maxCountryCode = MAX_LENGTH_COUNTRY_CODE_ + 1;
}
for (unsigned int i = 1; i <= maxCountryCode && i <= numberLength; ++i) {
NSString *subNumber = [fullNumber substringWithRange:NSMakeRange(0, i)];
NSNumber *potentialCountryCode = [NSNumber numberWithInteger:[subNumber integerValue]];
NSArray *regionCodes = [helper regionCodeFromCountryCode:potentialCountryCode];
if (regionCodes != nil && regionCodes.count > 0) {
if (nationalNumber != NULL) {
if ((*nationalNumber) == nil) {
(*nationalNumber) = [NSString stringWithFormat:@"%@", [fullNumber substringFromIndex:i]];
} else {
(*nationalNumber) = [NSString stringWithFormat:@"%@%@", (*nationalNumber), [fullNumber substringFromIndex:i]];
}
}
return potentialCountryCode;
}
}
return @0;
}
/**
* Convenience method to get a list of what regions the library has metadata
* for.
* @return {!Array.<string>} region codes supported by the library.
*/
- (NSArray *)getSupportedRegions
{
NBMetadataHelper *helper = [[NBMetadataHelper alloc] init];
NSArray *allKeys = [[helper CCode2CNMap] allKeys];
NSPredicate *predicateIsNaN = [NSPredicate predicateWithBlock:^BOOL(id evaluatedObject, NSDictionary *bindings) {
return [self isNaN:evaluatedObject];
}];
NSArray *supportedRegions = [allKeys filteredArrayUsingPredicate:predicateIsNaN];
return supportedRegions;
}
/*
i18n.phonenumbers.PhoneNumberUtil.prototype.getSupportedRegions = function() {
return goog.array.filter(
Object.keys(i18n.phonenumbers.metadata.countryToMetadata),
function(regionCode) {
return isNaN(regionCode);
});
};
*/
/**
* Convenience method to get a list of what global network calling codes the
* library has metadata for.
* @return {!Array.<number>} global network calling codes supported by the
* library.
*/
/*
i18n.phonenumbers.PhoneNumberUtil.prototype.
getSupportedGlobalNetworkCallingCodes = function() {
var callingCodesAsStrings = goog.array.filter(
Object.keys(i18n.phonenumbers.metadata.countryToMetadata),
function(regionCode) {
return !isNaN(regionCode);
});
return goog.array.map(callingCodesAsStrings,
function(callingCode) {
return parseInt(callingCode, 10);
});
};
*/
/**
* Tries to extract a country calling code from a number. This method will
* return zero if no country calling code is considered to be present. Country
* calling codes are extracted in the following ways:
* <ul>
* <li>by stripping the international dialing prefix of the region the person is
* dialing from, if this is present in the number, and looking at the next
* digits
* <li>by stripping the '+' sign if present and then looking at the next digits
* <li>by comparing the start of the number and the country calling code of the
* default region. If the number is not considered possible for the numbering
* plan of the default region initially, but starts with the country calling
* code of this region, validation will be reattempted after stripping this
* country calling code. If this number is considered a possible number, then
* the first digits will be considered the country calling code and removed as
* such.
* </ul>
*
* It will throw a i18n.phonenumbers.Error if the number starts with a '+' but
* the country calling code supplied after this does not match that of any known
* region.
*
* @param {string} number non-normalized telephone number that we wish to
* extract a country calling code from - may begin with '+'.
* @param {i18n.phonenumbers.PhoneMetadata} defaultRegionMetadata metadata
* about the region this number may be from.
* @param {!goog.string.StringBuffer} nationalNumber a string buffer to store
* the national significant number in, in the case that a country calling
* code was extracted. The number is appended to any existing contents. If
* no country calling code was extracted, this will be left unchanged.
* @param {boolean} keepRawInput NO if the country_code_source and
* preferred_carrier_code fields of phoneNumber should be populated.
* @param {i18n.phonenumbers.PhoneNumber} phoneNumber the PhoneNumber object
* where the country_code and country_code_source need to be populated.
* Note the country_code is always populated, whereas country_code_source is
* only populated when keepCountryCodeSource is NO.
* @return {number} the country calling code extracted or 0 if none could be
* extracted.
* @throws {i18n.phonenumbers.Error}
*/
- (NSNumber *)maybeExtractCountryCode:(NSString *)number metadata:(NBPhoneMetaData*)defaultRegionMetadata
nationalNumber:(NSString **)nationalNumber keepRawInput:(BOOL)keepRawInput
phoneNumber:(NBPhoneNumber**)phoneNumber error:(NSError**)error
{
if (nationalNumber == NULL || phoneNumber == NULL || number.length <= 0) {
return @0;
}
NSString *fullNumber = [number copy];
// Set the default prefix to be something that will never match.
NSString *possibleCountryIddPrefix = @"";
if (defaultRegionMetadata != nil) {
possibleCountryIddPrefix = defaultRegionMetadata.internationalPrefix;
}
if (possibleCountryIddPrefix == nil) {
possibleCountryIddPrefix = @"NonMatch";
}
/** @type {i18n.phonenumbers.PhoneNumber.CountryCodeSource} */
NBECountryCodeSource countryCodeSource = [self maybeStripInternationalPrefixAndNormalize:&fullNumber
possibleIddPrefix:possibleCountryIddPrefix];
if (keepRawInput) {
(*phoneNumber).countryCodeSource = [NSNumber numberWithInteger:countryCodeSource];
}
if (countryCodeSource != NBECountryCodeSourceFROM_DEFAULT_COUNTRY) {
if (fullNumber.length <= MIN_LENGTH_FOR_NSN_) {
NSDictionary *userInfo = [NSDictionary dictionaryWithObject:[NSString stringWithFormat:@"TOO_SHORT_AFTER_IDD:%@", fullNumber]
forKey:NSLocalizedDescriptionKey];
if (error != NULL) {
(*error) = [NSError errorWithDomain:@"TOO_SHORT_AFTER_IDD" code:0 userInfo:userInfo];
}
return @0;
}
NSNumber *potentialCountryCode = [self extractCountryCode:fullNumber nationalNumber:nationalNumber];
if (![potentialCountryCode isEqualToNumber:@0]) {
(*phoneNumber).countryCode = potentialCountryCode;
return potentialCountryCode;
}
// If this fails, they must be using a strange country calling code that we
// don't recognize, or that doesn't exist.
NSDictionary *userInfo = [NSDictionary dictionaryWithObject:[NSString stringWithFormat:@"INVALID_COUNTRY_CODE:%@", potentialCountryCode]
forKey:NSLocalizedDescriptionKey];
if (error != NULL) {
(*error) = [NSError errorWithDomain:@"INVALID_COUNTRY_CODE" code:0 userInfo:userInfo];
}
return @0;
} else if (defaultRegionMetadata != nil) {
// Check to see if the number starts with the country calling code for the
// default region. If so, we remove the country calling code, and do some
// checks on the validity of the number before and after.
NSNumber *defaultCountryCode = defaultRegionMetadata.countryCode;
NSString *defaultCountryCodeString = [NSString stringWithFormat:@"%@", defaultCountryCode];
NSString *normalizedNumber = [fullNumber copy];
if ([normalizedNumber hasPrefix:defaultCountryCodeString]) {
NSString *potentialNationalNumber = [normalizedNumber substringFromIndex:defaultCountryCodeString.length];
NBPhoneNumberDesc *generalDesc = defaultRegionMetadata.generalDesc;
NSString *validNumberPattern = generalDesc.nationalNumberPattern;
// Passing null since we don't need the carrier code.
[self maybeStripNationalPrefixAndCarrierCode:&potentialNationalNumber metadata:defaultRegionMetadata carrierCode:nil];
NSString *potentialNationalNumberStr = [potentialNationalNumber copy];
NSString *possibleNumberPattern = generalDesc.possibleNumberPattern;
// If the number was not valid before but is valid now, or if it was too
// long before, we consider the number with the country calling code
// stripped to be a better result and keep that instead.
if ((![self matchesEntirely:validNumberPattern string:fullNumber] &&
[self matchesEntirely:validNumberPattern string:potentialNationalNumberStr]) ||
[self testNumberLengthAgainstPattern:possibleNumberPattern number:fullNumber] == NBEValidationResultTOO_LONG) {
(*nationalNumber) = [(*nationalNumber) stringByAppendingString:potentialNationalNumberStr];
if (keepRawInput) {
(*phoneNumber).countryCodeSource = [NSNumber numberWithInteger:NBECountryCodeSourceFROM_NUMBER_WITHOUT_PLUS_SIGN];
}
(*phoneNumber).countryCode = defaultCountryCode;
return defaultCountryCode;
}
}
}
// No country calling code present.
(*phoneNumber).countryCode = @0;
return @0;
}
/**
* Strips the IDD from the start of the number if present. Helper function used
* by maybeStripInternationalPrefixAndNormalize.
*
* @param {!RegExp} iddPattern the regular expression for the international
* prefix.
* @param {!goog.string.StringBuffer} number the phone number that we wish to
* strip any international dialing prefix from.
* @return {boolean} NO if an international prefix was present.
* @private
*/
- (BOOL)parsePrefixAsIdd:(NSString *)iddPattern sourceString:(NSString **)number
{
if (number == NULL) {
return NO;
}
NSString *numberStr = [(*number) copy];
if ([self stringPositionByRegex:numberStr regex:iddPattern] == 0) {
NSTextCheckingResult *matched = [[self matchesByRegex:numberStr regex:iddPattern] objectAtIndex:0];
NSString *matchedString = [numberStr substringWithRange:matched.range];
unsigned int matchEnd = (unsigned int)matchedString.length;
NSString *remainString = [numberStr substringFromIndex:matchEnd];
NSArray *matchedGroups = [_CAPTURING_DIGIT_PATTERN matchesInString:remainString options:0 range:NSMakeRange(0, remainString.length)];
if (matchedGroups && [matchedGroups count] > 0 && [matchedGroups objectAtIndex:0] != nil) {
NSString *digitMatched = [remainString substringWithRange:((NSTextCheckingResult*)[matchedGroups objectAtIndex:0]).range];
if (digitMatched.length > 0) {
NSString *normalizedGroup = [self normalizeDigitsOnly:digitMatched];
if ([normalizedGroup isEqualToString:@"0"]) {
return NO;
}
}
}
(*number) = [remainString copy];
return YES;
}
return NO;
}
/**
* Strips any international prefix (such as +, 00, 011) present in the number
* provided, normalizes the resulting number, and indicates if an international
* prefix was present.
*
* @param {!goog.string.StringBuffer} number the non-normalized telephone number
* that we wish to strip any international dialing prefix from.
* @param {string} possibleIddPrefix the international direct dialing prefix
* from the region we think this number may be dialed in.
* @return {CountryCodeSource} the corresponding
* CountryCodeSource if an international dialing prefix could be removed
* from the number, otherwise CountryCodeSource.FROM_DEFAULT_COUNTRY if
* the number did not seem to be in international format.
*/
- (NBECountryCodeSource)maybeStripInternationalPrefixAndNormalize:(NSString **)numberStr possibleIddPrefix:(NSString *)possibleIddPrefix
{
if (numberStr == NULL || (*numberStr).length == 0) {
return NBECountryCodeSourceFROM_DEFAULT_COUNTRY;
}
// Check to see if the number begins with one or more plus signs.
if ([self isStartingStringByRegex:(*numberStr) regex:LEADING_PLUS_CHARS_PATTERN]) {
(*numberStr) = [self replaceStringByRegex:(*numberStr) regex:LEADING_PLUS_CHARS_PATTERN withTemplate:@""];
// Can now normalize the rest of the number since we've consumed the '+'
// sign at the start.
(*numberStr) = [self normalizePhoneNumber:(*numberStr)];
return NBECountryCodeSourceFROM_NUMBER_WITH_PLUS_SIGN;
}
// Attempt to parse the first digits as an international prefix.
NSString *iddPattern = [possibleIddPrefix copy];
[self normalizeSB:numberStr];
return [self parsePrefixAsIdd:iddPattern sourceString:numberStr] ? NBECountryCodeSourceFROM_NUMBER_WITH_IDD : NBECountryCodeSourceFROM_DEFAULT_COUNTRY;
}
/**
* Strips any national prefix (such as 0, 1) present in the number provided.
*
* @param {!goog.string.StringBuffer} number the normalized telephone number
* that we wish to strip any national dialing prefix from.
* @param {i18n.phonenumbers.PhoneMetadata} metadata the metadata for the
* region that we think this number is from.
* @param {goog.string.StringBuffer} carrierCode a place to insert the carrier
* code if one is extracted.
* @return {boolean} NO if a national prefix or carrier code (or both) could
* be extracted.
*/
- (BOOL)maybeStripNationalPrefixAndCarrierCode:(NSString **)number metadata:(NBPhoneMetaData*)metadata carrierCode:(NSString **)carrierCode
{
if (number == NULL) {
return NO;
}
NSString *numberStr = [(*number) copy];
unsigned int numberLength = (unsigned int)numberStr.length;
NSString *possibleNationalPrefix = metadata.nationalPrefixForParsing;
if (numberLength == 0 || [NBMetadataHelper hasValue:possibleNationalPrefix] == NO) {
// Early return for numbers of zero length.
return NO;
}
// Attempt to parse the first digits as a national prefix.
NSString *prefixPattern = [NSString stringWithFormat:@"^(?:%@)", possibleNationalPrefix];
NSError *error = nil;
NSRegularExpression *currentPattern = [self regularExpressionWithPattern:prefixPattern options:0 error:&error];
NSArray *prefixMatcher = [currentPattern matchesInString:numberStr options:0 range:NSMakeRange(0, numberLength)];
if (prefixMatcher && [prefixMatcher count] > 0) {
NSString *nationalNumberRule = metadata.generalDesc.nationalNumberPattern;
NSTextCheckingResult *firstMatch = [prefixMatcher objectAtIndex:0];
NSString *firstMatchString = [numberStr substringWithRange:firstMatch.range];
// prefixMatcher[numOfGroups] == null implies nothing was captured by the
// capturing groups in possibleNationalPrefix; therefore, no transformation
// is necessary, and we just remove the national prefix.
unsigned int numOfGroups = (unsigned int)firstMatch.numberOfRanges - 1;
NSString *transformRule = metadata.nationalPrefixTransformRule;
NSString *transformedNumber = @"";
NSRange firstRange = [firstMatch rangeAtIndex:numOfGroups];
NSString *firstMatchStringWithGroup = (firstRange.location != NSNotFound && firstRange.location < numberStr.length) ? [numberStr substringWithRange:firstRange] : nil;
BOOL noTransform = (transformRule == nil || transformRule.length == 0 || [NBMetadataHelper hasValue:firstMatchStringWithGroup] == NO);
if (noTransform) {
transformedNumber = [numberStr substringFromIndex:firstMatchString.length];
} else {
transformedNumber = [self replaceFirstStringByRegex:numberStr regex:prefixPattern withTemplate:transformRule];
}
// If the original number was viable, and the resultant number is not,
// we return.
if ([NBMetadataHelper hasValue:nationalNumberRule ] && [self matchesEntirely:nationalNumberRule string:numberStr] &&
[self matchesEntirely:nationalNumberRule string:transformedNumber] == NO) {
return NO;
}
if ((noTransform && numOfGroups > 0 && [NBMetadataHelper hasValue:firstMatchStringWithGroup]) || (!noTransform && numOfGroups > 1)) {
if (carrierCode != NULL && (*carrierCode) != nil) {
(*carrierCode) = [(*carrierCode) stringByAppendingString:firstMatchStringWithGroup];
}
} else if ((noTransform && numOfGroups > 0 && [NBMetadataHelper hasValue:firstMatchString]) || (!noTransform && numOfGroups > 1)) {
if (carrierCode != NULL && (*carrierCode) != nil) {
(*carrierCode) = [(*carrierCode) stringByAppendingString:firstMatchString];
}
}
(*number) = transformedNumber;
return YES;
}
return NO;
}
/**
* Strips any extension (as in, the part of the number dialled after the call is
* connected, usually indicated with extn, ext, x or similar) from the end of
* the number, and returns it.
*
* @param {!goog.string.StringBuffer} number the non-normalized telephone number
* that we wish to strip the extension from.
* @return {string} the phone extension.
*/
- (NSString *)maybeStripExtension:(NSString **)number
{
if (number == NULL) {
return @"";
}
NSString *numberStr = [(*number) copy];
int mStart = [self stringPositionByRegex:numberStr regex:EXTN_PATTERN];
// If we find a potential extension, and the number preceding this is a viable
// number, we assume it is an extension.
if (mStart >= 0 && [self isViablePhoneNumber:[numberStr substringWithRange:NSMakeRange(0, mStart)]]) {
// The numbers are captured into groups in the regular expression.
NSTextCheckingResult *firstMatch = [self matcheFirstByRegex:numberStr regex:EXTN_PATTERN];
unsigned int matchedGroupsLength = (unsigned int)[firstMatch numberOfRanges];
for (unsigned int i=1; i<matchedGroupsLength; i++) {
NSRange curRange = [firstMatch rangeAtIndex:i];
if (curRange.location != NSNotFound && curRange.location < numberStr.length) {
NSString *matchString = [(*number) substringWithRange:curRange];
// We go through the capturing groups until we find one that captured
// some digits. If none did, then we will return the empty string.
NSString *tokenedString = [numberStr substringWithRange:NSMakeRange(0, mStart)];
(*number) = @"";
(*number) = [(*number) stringByAppendingString:tokenedString];
return matchString;
}
}
}
return @"";
}
/**
* Checks to see that the region code used is valid, or if it is not valid, that
* the number to parse starts with a + symbol so that we can attempt to infer
* the region from the number.
* @param {string} numberToParse number that we are attempting to parse.
* @param {?string} defaultRegion region that we are expecting the number to be
* from.
* @return {boolean} NO if it cannot use the region provided and the region
* cannot be inferred.
* @private
*/
- (BOOL)checkRegionForParsing:(NSString *)numberToParse defaultRegion:(NSString *)defaultRegion
{
// If the number is nil or empty, we can't infer the region.
return [self isValidRegionCode:defaultRegion] ||
(numberToParse != nil && numberToParse.length > 0 && [self isStartingStringByRegex:numberToParse regex:LEADING_PLUS_CHARS_PATTERN]);
}
/**
* Parses a string and returns it in proto buffer format. This method will throw
* a {@link i18n.phonenumbers.Error} if the number is not considered to be a
* possible number. Note that validation of whether the number is actually a
* valid number for a particular region is not performed. This can be done
* separately with {@link #isValidNumber}.
*
* @param {?string} numberToParse number that we are attempting to parse. This
* can contain formatting such as +, ( and -, as well as a phone number
* extension. It can also be provided in RFC3966 format.
* @param {?string} defaultRegion region that we are expecting the number to be
* from. This is only used if the number being parsed is not written in
* international format. The country_code for the number in this case would
* be stored as that of the default region supplied. If the number is
* guaranteed to start with a '+' followed by the country calling code, then
* 'ZZ' or nil can be supplied.
* @return {i18n.phonenumbers.PhoneNumber} a phone number proto buffer filled
* with the parsed number.
* @throws {i18n.phonenumbers.Error} if the string is not considered to be a
* viable phone number or if no default region was supplied and the number
* is not in international format (does not start with +).
*/
- (NBPhoneNumber*)parse:(NSString *)numberToParse defaultRegion:(NSString *)defaultRegion error:(NSError**)error
{
NSError *anError = nil;
NBPhoneNumber *phoneNumber = [self parseHelper:numberToParse defaultRegion:defaultRegion keepRawInput:NO checkRegion:YES error:&anError];
if (anError != nil) {
if (error != NULL) {
(*error) = [self errorWithObject:anError.description withDomain:anError.domain];
}
}
return phoneNumber;
}
/**
* Parses a string using the phone's carrier region (when available, ZZ otherwise).
* This uses the country the sim card in the phone is registered with.
* For example if you have an AT&T sim card but are in Europe, this will parse the
* number using +1 (AT&T is a US Carrier) as the default country code.
* This also works for CDMA phones which don't have a sim card.
*/
- (NBPhoneNumber*)parseWithPhoneCarrierRegion:(NSString *)numberToParse error:(NSError**)error
{
NBMetadataHelper *helper = [[NBMetadataHelper alloc] init];
numberToParse = [helper normalizeNonBreakingSpace:numberToParse];
NSString *defaultRegion = nil;
#if TARGET_OS_IPHONE && !TARGET_OS_WATCH
defaultRegion = [self countryCodeByCarrier];
#else
defaultRegion = [[NSLocale currentLocale] objectForKey: NSLocaleCountryCode];
#endif
if ([NB_UNKNOWN_REGION isEqualToString:defaultRegion]) {
// get region from device as a failover (e.g. iPad)
NSLocale *currentLocale = [NSLocale currentLocale];
defaultRegion = [currentLocale objectForKey:NSLocaleCountryCode];
}
return [self parse:numberToParse defaultRegion:defaultRegion error:error];
}
#if TARGET_OS_IPHONE && !TARGET_OS_WATCH
static CTTelephonyNetworkInfo* _telephonyNetworkInfo;
- (CTTelephonyNetworkInfo*)telephonyNetworkInfo{
// cache telephony network info;
// CTTelephonyNetworkInfo objects are unnecessarily created for every call to parseWithPhoneCarrierRegion:error:
// when in reality this information not change while an app lives in memory
// real-world performance test while parsing 93 phone numbers:
// before change: 126ms
// after change: 32ms
// using static instance prevents deallocation crashes due to ios bug
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
_telephonyNetworkInfo = [[CTTelephonyNetworkInfo alloc] init];
});
return _telephonyNetworkInfo;
}
- (NSString *)countryCodeByCarrier
{
NSString *isoCode = [[self.telephonyNetworkInfo subscriberCellularProvider] isoCountryCode];
// The 2nd part of the if is working around an iOS 7 bug
// If the SIM card is missing, iOS 7 returns an empty string instead of nil
if (!isoCode || [isoCode isEqualToString:@""]) {
isoCode = NB_UNKNOWN_REGION;
}
return isoCode;
}
#endif
/**
* Parses a string and returns it in proto buffer format. This method differs
* from {@link #parse} in that it always populates the raw_input field of the
* protocol buffer with numberToParse as well as the country_code_source field.
*
* @param {string} numberToParse number that we are attempting to parse. This
* can contain formatting such as +, ( and -, as well as a phone number
* extension.
* @param {?string} defaultRegion region that we are expecting the number to be
* from. This is only used if the number being parsed is not written in
* international format. The country calling code for the number in this
* case would be stored as that of the default region supplied.
* @return {i18n.phonenumbers.PhoneNumber} a phone number proto buffer filled
* with the parsed number.
* @throws {i18n.phonenumbers.Error} if the string is not considered to be a
* viable phone number or if no default region was supplied.
*/
- (NBPhoneNumber*)parseAndKeepRawInput:(NSString *)numberToParse defaultRegion:(NSString *)defaultRegion error:(NSError**)error
{
if ([self isValidRegionCode:defaultRegion] == NO) {
if (numberToParse.length > 0 && [numberToParse hasPrefix:@"+"] == NO) {
NSDictionary *userInfo = [NSDictionary dictionaryWithObject:[NSString stringWithFormat:@"Invalid country code:%@", numberToParse]
forKey:NSLocalizedDescriptionKey];
if (error != NULL) {
(*error) = [NSError errorWithDomain:@"INVALID_COUNTRY_CODE" code:0 userInfo:userInfo];
}
}
}
return [self parseHelper:numberToParse defaultRegion:defaultRegion keepRawInput:YES checkRegion:YES error:error];
}
/**
* Parses a string and returns it in proto buffer format. This method is the
* same as the public {@link #parse} method, with the exception that it allows
* the default region to be nil, for use by {@link #isNumberMatch}.
*
* @param {?string} numberToParse number that we are attempting to parse. This
* can contain formatting such as +, ( and -, as well as a phone number
* extension.
* @param {?string} defaultRegion region that we are expecting the number to be
* from. This is only used if the number being parsed is not written in
* international format. The country calling code for the number in this
* case would be stored as that of the default region supplied.
* @param {boolean} keepRawInput whether to populate the raw_input field of the
* phoneNumber with numberToParse.
* @param {boolean} checkRegion should be set to NO if it is permitted for
* the default coregion to be nil or unknown ('ZZ').
* @return {i18n.phonenumbers.PhoneNumber} a phone number proto buffer filled
* with the parsed number.
* @throws {i18n.phonenumbers.Error}
* @private
*/
- (NBPhoneNumber*)parseHelper:(NSString *)numberToParse defaultRegion:(NSString *)defaultRegion
keepRawInput:(BOOL)keepRawInput checkRegion:(BOOL)checkRegion error:(NSError**)error
{
NBMetadataHelper *helper = [[NBMetadataHelper alloc] init];
numberToParse = [helper normalizeNonBreakingSpace:numberToParse];
if (numberToParse == nil) {
if (error != NULL) {
(*error) = [self errorWithObject:[NSString stringWithFormat:@"NOT_A_NUMBER:%@", numberToParse] withDomain:@"NOT_A_NUMBER"];
}
return nil;
} else if (numberToParse.length > MAX_INPUT_STRING_LENGTH_) {
if (error != NULL) {
(*error) = [self errorWithObject:[NSString stringWithFormat:@"TOO_LONG:%@", numberToParse] withDomain:@"TOO_LONG"];
}
return nil;
}
NSString *nationalNumber = @"";
[self buildNationalNumberForParsing:numberToParse nationalNumber:&nationalNumber];
if ([self isViablePhoneNumber:nationalNumber] == NO) {
if (error != NULL) {
(*error) = [self errorWithObject:[NSString stringWithFormat:@"NOT_A_NUMBER:%@", nationalNumber] withDomain:@"NOT_A_NUMBER"];
}
return nil;
}
// Check the region supplied is valid, or that the extracted number starts
// with some sort of + sign so the number's region can be determined.
if (checkRegion && [self checkRegionForParsing:nationalNumber defaultRegion:defaultRegion] == NO) {
if (error != NULL) {
(*error) = [self errorWithObject:[NSString stringWithFormat:@"INVALID_COUNTRY_CODE:%@", defaultRegion]
withDomain:@"INVALID_COUNTRY_CODE"];
}
return nil;
}
NBPhoneNumber *phoneNumber = [[NBPhoneNumber alloc] init];
if (keepRawInput) {
phoneNumber.rawInput = [numberToParse copy];
}
// Attempt to parse extension first, since it doesn't require region-specific
// data and we want to have the non-normalised number here.
NSString *extension = [self maybeStripExtension:&nationalNumber];
if (extension.length > 0) {
phoneNumber.extension = [extension copy];
}
NBPhoneMetaData *regionMetadata = [helper getMetadataForRegion:defaultRegion];
// Check to see if the number is given in international format so we know
// whether this number is from the default region or not.
NSString *normalizedNationalNumber = @"";
NSNumber *countryCode = nil;
NSString *nationalNumberStr = [nationalNumber copy];
{
NSError *anError = nil;
countryCode = [self maybeExtractCountryCode:nationalNumberStr
metadata:regionMetadata
nationalNumber:&normalizedNationalNumber
keepRawInput:keepRawInput
phoneNumber:&phoneNumber error:&anError];
if (anError != nil) {
if ([anError.domain isEqualToString:@"INVALID_COUNTRY_CODE"] && [self stringPositionByRegex:nationalNumberStr
regex:LEADING_PLUS_CHARS_PATTERN] >= 0)
{
// Strip the plus-char, and try again.
NSError *aNestedError = nil;
nationalNumberStr = [self replaceStringByRegex:nationalNumberStr regex:LEADING_PLUS_CHARS_PATTERN withTemplate:@""];
countryCode = [self maybeExtractCountryCode:nationalNumberStr
metadata:regionMetadata
nationalNumber:&normalizedNationalNumber
keepRawInput:keepRawInput
phoneNumber:&phoneNumber error:&aNestedError];
if ([countryCode isEqualToNumber:@0]) {
if (error != NULL)
(*error) = [self errorWithObject:anError.description withDomain:anError.domain];
return nil;
}
} else {
if (error != NULL)
(*error) = [self errorWithObject:anError.description withDomain:anError.domain];
return nil;
}
}
}
if (![countryCode isEqualToNumber:@0]) {
NSString *phoneNumberRegion = [self getRegionCodeForCountryCode:countryCode];
if (phoneNumberRegion != defaultRegion) {
// Metadata cannot be nil because the country calling code is valid.
regionMetadata = [self getMetadataForRegionOrCallingCode:countryCode regionCode:phoneNumberRegion];
}
} else {
// If no extracted country calling code, use the region supplied instead.
// The national number is just the normalized version of the number we were
// given to parse.
[self normalizeSB:&nationalNumber];
normalizedNationalNumber = [normalizedNationalNumber stringByAppendingString:nationalNumber];
if (defaultRegion != nil) {
countryCode = regionMetadata.countryCode;
phoneNumber.countryCode = countryCode;
} else if (keepRawInput) {
[phoneNumber clearCountryCodeSource];
}
}
if (normalizedNationalNumber.length < MIN_LENGTH_FOR_NSN_){
if (error != NULL) {
(*error) = [self errorWithObject:[NSString stringWithFormat:@"TOO_SHORT_NSN:%@", normalizedNationalNumber] withDomain:@"TOO_SHORT_NSN"];
}
return nil;
}
if (regionMetadata != nil) {
NSString *carrierCode = @"";
[self maybeStripNationalPrefixAndCarrierCode:&normalizedNationalNumber metadata:regionMetadata carrierCode:&carrierCode];
if (keepRawInput) {
phoneNumber.preferredDomesticCarrierCode = [carrierCode copy];
}
}
NSString *normalizedNationalNumberStr = [normalizedNationalNumber copy];
unsigned int lengthOfNationalNumber = (unsigned int)normalizedNationalNumberStr.length;
if (lengthOfNationalNumber < MIN_LENGTH_FOR_NSN_) {
if (error != NULL) {
(*error) = [self errorWithObject:[NSString stringWithFormat:@"TOO_SHORT_NSN:%@", normalizedNationalNumber] withDomain:@"TOO_SHORT_NSN"];
}
return nil;
}
if (lengthOfNationalNumber > MAX_LENGTH_FOR_NSN_) {
if (error != NULL) {
(*error) = [self errorWithObject:[NSString stringWithFormat:@"TOO_LONG:%@", normalizedNationalNumber] withDomain:@"TOO_LONG"];
}
return nil;
}
if ([normalizedNationalNumberStr hasPrefix:@"0"]) {
phoneNumber.italianLeadingZero = YES;
}
phoneNumber.nationalNumber = [NSNumber numberWithLongLong:[normalizedNationalNumberStr longLongValue]];
return phoneNumber;
}
/**
* Converts numberToParse to a form that we can parse and write it to
* nationalNumber if it is written in RFC3966; otherwise extract a possible
* number out of it and write to nationalNumber.
*
* @param {?string} numberToParse number that we are attempting to parse. This
* can contain formatting such as +, ( and -, as well as a phone number
* extension.
* @param {!goog.string.StringBuffer} nationalNumber a string buffer for storing
* the national significant number.
* @private
*/
- (void)buildNationalNumberForParsing:(NSString *)numberToParse nationalNumber:(NSString **)nationalNumber
{
if (nationalNumber == NULL)
return;
int indexOfPhoneContext = [self indexOfStringByString:numberToParse target:RFC3966_PHONE_CONTEXT];
if (indexOfPhoneContext > 0)
{
unsigned int phoneContextStart = indexOfPhoneContext + (unsigned int)RFC3966_PHONE_CONTEXT.length;
// If the phone context contains a phone number prefix, we need to capture
// it, whereas domains will be ignored.
if ([numberToParse characterAtIndex:phoneContextStart] == '+')
{
// Additional parameters might follow the phone context. If so, we will
// remove them here because the parameters after phone context are not
// important for parsing the phone number.
NSRange foundRange = [numberToParse rangeOfString:@";" options:NSLiteralSearch range:NSMakeRange(phoneContextStart, numberToParse.length - phoneContextStart)];
if (foundRange.location != NSNotFound)
{
NSRange subRange = NSMakeRange(phoneContextStart, foundRange.location - phoneContextStart);
(*nationalNumber) = [(*nationalNumber) stringByAppendingString:[numberToParse substringWithRange:subRange]];
}
else
{
(*nationalNumber) = [(*nationalNumber) stringByAppendingString:[numberToParse substringFromIndex:phoneContextStart]];
}
}
// Now append everything between the "tel:" prefix and the phone-context.
// This should include the national number, an optional extension or
// isdn-subaddress component.
unsigned int rfc3966Start = [self indexOfStringByString:numberToParse target:RFC3966_PREFIX] + (unsigned int)RFC3966_PREFIX.length;
NSString *subString = [numberToParse substringWithRange:NSMakeRange(rfc3966Start, indexOfPhoneContext - rfc3966Start)];
(*nationalNumber) = [(*nationalNumber) stringByAppendingString:subString];
}
else
{
// Extract a possible number from the string passed in (this strips leading
// characters that could not be the start of a phone number.)
(*nationalNumber) = [(*nationalNumber) stringByAppendingString:[self extractPossibleNumber:numberToParse]];
}
// Delete the isdn-subaddress and everything after it if it is present.
// Note extension won't appear at the same time with isdn-subaddress
// according to paragraph 5.3 of the RFC3966 spec,
NSString *nationalNumberStr = [(*nationalNumber) copy];
int indexOfIsdn = [self indexOfStringByString:nationalNumberStr target:RFC3966_ISDN_SUBADDRESS];
if (indexOfIsdn > 0)
{
(*nationalNumber) = @"";
(*nationalNumber) = [(*nationalNumber) stringByAppendingString:[nationalNumberStr substringWithRange:NSMakeRange(0, indexOfIsdn)]];
}
// If both phone context and isdn-subaddress are absent but other
// parameters are present, the parameters are left in nationalNumber. This
// is because we are concerned about deleting content from a potential
// number string when there is no strong evidence that the number is
// actually written in RFC3966.
}
/**
* Takes two phone numbers and compares them for equality.
*
* <p>Returns EXACT_MATCH if the country_code, NSN, presence of a leading zero
* for Italian numbers and any extension present are the same. Returns NSN_MATCH
* if either or both has no region specified, and the NSNs and extensions are
* the same. Returns SHORT_NSN_MATCH if either or both has no region specified,
* or the region specified is the same, and one NSN could be a shorter version
* of the other number. This includes the case where one has an extension
* specified, and the other does not. Returns NO_MATCH otherwise. For example,
* the numbers +1 345 657 1234 and 657 1234 are a SHORT_NSN_MATCH. The numbers
* +1 345 657 1234 and 345 657 are a NO_MATCH.
*
* @param {i18n.phonenumbers.PhoneNumber|string} firstNumberIn first number to
* compare. If it is a string it can contain formatting, and can have
* country calling code specified with + at the start.
* @param {i18n.phonenumbers.PhoneNumber|string} secondNumberIn second number to
* compare. If it is a string it can contain formatting, and can have
* country calling code specified with + at the start.
* @return {MatchType} NOT_A_NUMBER, NO_MATCH,
* SHORT_NSN_MATCH, NSN_MATCH or EXACT_MATCH depending on the level of
* equality of the two numbers, described in the method definition.
*/
- (NBEMatchType)isNumberMatch:(id)firstNumberIn second:(id)secondNumberIn error:(NSError**)error
{
NBEMatchType res = 0;
@try {
res = [self isNumberMatch:firstNumberIn second:secondNumberIn];
}
@catch (NSException *exception) {
NSDictionary *userInfo = [NSDictionary dictionaryWithObject:exception.reason
forKey:NSLocalizedDescriptionKey];
if (error != NULL)
(*error) = [NSError errorWithDomain:exception.name code:0 userInfo:userInfo];
}
return res;
}
- (NBEMatchType)isNumberMatch:(id)firstNumberIn second:(id)secondNumberIn
{
// If the input arguements are strings parse them to a proto buffer format.
// Else make copies of the phone numbers so that the numbers passed in are not
// edited.
/** @type {i18n.phonenumbers.PhoneNumber} */
NBPhoneNumber *firstNumber = nil, *secondNumber = nil;
if ([firstNumberIn isKindOfClass:[NSString class]]) {
// First see if the first number has an implicit country calling code, by
// attempting to parse it.
NSError *anError;
firstNumber = [self parse:firstNumberIn defaultRegion:NB_UNKNOWN_REGION error:&anError];
if (anError != nil) {
if ([anError.domain isEqualToString:@"INVALID_COUNTRY_CODE"] == NO) {
return NBEMatchTypeNOT_A_NUMBER;
}
// The first number has no country calling code. EXACT_MATCH is no longer
// possible. We parse it as if the region was the same as that for the
// second number, and if EXACT_MATCH is returned, we replace this with
// NSN_MATCH.
if ([secondNumberIn isKindOfClass:[NSString class]] == NO) {
NSString *secondNumberRegion = [self getRegionCodeForCountryCode:((NBPhoneNumber*)secondNumberIn).countryCode];
if (secondNumberRegion != NB_UNKNOWN_REGION) {
NSError *aNestedError;
firstNumber = [self parse:firstNumberIn defaultRegion:secondNumberRegion error:&aNestedError];
if (aNestedError != nil) {
return NBEMatchTypeNOT_A_NUMBER;
}
NBEMatchType match = [self isNumberMatch:firstNumber second:secondNumberIn];
if (match == NBEMatchTypeEXACT_MATCH) {
return NBEMatchTypeNSN_MATCH;
}
return match;
}
}
// If the second number is a string or doesn't have a valid country
// calling code, we parse the first number without country calling code.
NSError *aNestedError;
firstNumber = [self parseHelper:firstNumberIn defaultRegion:nil keepRawInput:NO checkRegion:NO error:&aNestedError];
if (aNestedError != nil) {
return NBEMatchTypeNOT_A_NUMBER;
}
}
} else {
firstNumber = [firstNumberIn copy];
}
if ([secondNumberIn isKindOfClass:[NSString class]]) {
NSError *parseError;
secondNumber = [self parse:secondNumberIn defaultRegion:NB_UNKNOWN_REGION error:&parseError];
if (parseError != nil) {
if ([parseError.domain isEqualToString:@"INVALID_COUNTRY_CODE"] == NO) {
return NBEMatchTypeNOT_A_NUMBER;
}
return [self isNumberMatch:secondNumberIn second:firstNumber];
} else {
return [self isNumberMatch:firstNumberIn second:secondNumber];
}
}
else {
secondNumber = [secondNumberIn copy];
}
// First clear raw_input, country_code_source and
// preferred_domestic_carrier_code fields and any empty-string extensions so
// that we can use the proto-buffer equality method.
firstNumber.rawInput = @"";
[firstNumber clearCountryCodeSource];
firstNumber.preferredDomesticCarrierCode = @"";
secondNumber.rawInput = @"";
[secondNumber clearCountryCodeSource];
secondNumber.preferredDomesticCarrierCode = @"";
if (firstNumber.extension != nil && firstNumber.extension.length == 0) {
firstNumber.extension = nil;
}
if (secondNumber.extension != nil && secondNumber.extension.length == 0) {
secondNumber.extension = nil;
}
// Early exit if both had extensions and these are different.
if ([NBMetadataHelper hasValue:firstNumber.extension] && [NBMetadataHelper hasValue:secondNumber.extension] &&
[firstNumber.extension isEqualToString:secondNumber.extension] == NO) {
return NBEMatchTypeNO_MATCH;
}
NSNumber *firstNumberCountryCode = firstNumber.countryCode;
NSNumber *secondNumberCountryCode = secondNumber.countryCode;
// Both had country_code specified.
if (![firstNumberCountryCode isEqualToNumber:@0] && ![secondNumberCountryCode isEqualToNumber:@0]) {
if ([firstNumber isEqual:secondNumber]) {
return NBEMatchTypeEXACT_MATCH;
} else if ([firstNumberCountryCode isEqualToNumber:secondNumberCountryCode] && [self isNationalNumberSuffixOfTheOther:firstNumber second:secondNumber]) {
// A SHORT_NSN_MATCH occurs if there is a difference because of the
// presence or absence of an 'Italian leading zero', the presence or
// absence of an extension, or one NSN being a shorter variant of the
// other.
return NBEMatchTypeSHORT_NSN_MATCH;
}
// This is not a match.
return NBEMatchTypeNO_MATCH;
}
// Checks cases where one or both country_code fields were not specified. To
// make equality checks easier, we first set the country_code fields to be
// equal.
firstNumber.countryCode = @0;
secondNumber.countryCode = @0;
// If all else was the same, then this is an NSN_MATCH.
if ([firstNumber isEqual:secondNumber]) {
return NBEMatchTypeNSN_MATCH;
}
if ([self isNationalNumberSuffixOfTheOther:firstNumber second:secondNumber]) {
return NBEMatchTypeSHORT_NSN_MATCH;
}
return NBEMatchTypeNO_MATCH;
}
/**
* Returns NO when one national number is the suffix of the other or both are
* the same.
*
* @param {i18n.phonenumbers.PhoneNumber} firstNumber the first PhoneNumber
* object.
* @param {i18n.phonenumbers.PhoneNumber} secondNumber the second PhoneNumber
* object.
* @return {boolean} NO if one PhoneNumber is the suffix of the other one.
* @private
*/
- (BOOL)isNationalNumberSuffixOfTheOther:(NBPhoneNumber*)firstNumber second:(NBPhoneNumber*)secondNumber
{
NSString *firstNumberNationalNumber = [NSString stringWithFormat:@"%@", firstNumber.nationalNumber];
NSString *secondNumberNationalNumber = [NSString stringWithFormat:@"%@", secondNumber.nationalNumber];
// Note that endsWith returns NO if the numbers are equal.
return [firstNumberNationalNumber hasSuffix:secondNumberNationalNumber] ||
[secondNumberNationalNumber hasSuffix:firstNumberNationalNumber];
}
/**
* Returns NO if the number can be dialled from outside the region, or
* unknown. If the number can only be dialled from within the region, returns
* NO. Does not check the number is a valid number.
* TODO: Make this method public when we have enough metadata to make it
* worthwhile. Currently visible for testing purposes only.
*
* @param {i18n.phonenumbers.PhoneNumber} number the phone-number for which we
* want to know whether it is diallable from outside the region.
* @return {boolean} NO if the number can only be dialled from within the
* country.
*/
- (BOOL)canBeInternationallyDialled:(NBPhoneNumber*)number error:(NSError**)error
{
BOOL res = NO;
@try {
res = [self canBeInternationallyDialled:number];
}
@catch (NSException *exception) {
NSDictionary *userInfo = [NSDictionary dictionaryWithObject:exception.reason
forKey:NSLocalizedDescriptionKey];
if (error != NULL)
(*error) = [NSError errorWithDomain:exception.name code:0 userInfo:userInfo];
}
return res;
}
- (BOOL)canBeInternationallyDialled:(NBPhoneNumber*)number
{
NBMetadataHelper *helper = [[NBMetadataHelper alloc] init];
NBPhoneMetaData *metadata = [helper getMetadataForRegion:[self getRegionCodeForNumber:number]];
if (metadata == nil) {
// Note numbers belonging to non-geographical entities (e.g. +800 numbers)
// are always internationally diallable, and will be caught here.
return YES;
}
NSString *nationalSignificantNumber = [self getNationalSignificantNumber:number];
return [self isNumberMatchingDesc:nationalSignificantNumber numberDesc:metadata.noInternationalDialling] == NO;
}
/**
* Check whether the entire input sequence can be matched against the regular
* expression.
*
* @param {!RegExp|string} regex the regular expression to match against.
* @param {string} str the string to test.
* @return {boolean} NO if str can be matched entirely against regex.
* @private
*/
- (BOOL)matchesEntirely:(NSString *)regex string:(NSString *)str
{
if ([regex isEqualToString:@"NA"]) {
return NO;
}
NSError *error = nil;
NSRegularExpression *currentPattern = [self entireRegularExpressionWithPattern:regex options:0 error:&error];
NSRange stringRange = NSMakeRange(0, str.length);
NSTextCheckingResult *matchResult = [currentPattern firstMatchInString:str options:NSMatchingAnchored range:stringRange];
if (matchResult != nil) {
BOOL matchIsEntireString = NSEqualRanges(matchResult.range, stringRange);
if (matchIsEntireString)
{
return YES;
}
}
return NO;
}
@end