Project import
diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..d9a10c0 --- /dev/null +++ b/LICENSE
@@ -0,0 +1,176 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS
diff --git a/README.md b/README.md new file mode 100644 index 0000000..532bd0a --- /dev/null +++ b/README.md
@@ -0,0 +1,76 @@ +# libPhoneNumber for iOS (ARC) + +* NBPhoneNumber, NBNumberFormat, NBPhoneNumberDesc, NBPhoneMetaData (Metadata classes) + +* NBPhoneNumberUtil (from phonenumberutil.js) +* NBAsYouTypeFormatter + +## Install with CocoaPods +### Use [CocoaPods](http://cocoapods.org/?q=libPhoneNumber-iOS) + +## Install without CocoaPods +### Add source files to your projects from libPhoneNumber + - NBPhoneNumberUtil.h, .m + - NBAsYouTypeFormatter.h, .m + + - NBNumberFormat.h, .m + - NBPhoneNumber.h, .m + - NBPhoneNumberDesc.h, .m + - NBPhoneNumberDefines.h + + - NBPhoneNumberMetadata.h, .m + - NBPhoneNumberMetadataForTesting.h, .m + + - Add "NBPhoneNumberMetadata.plist" and "NBPhoneNumberMetadataForTesting.plist" to bundle resources + - Add "CoreTelephony.framework" + + See sample test code from "libPhoneNumber-iOS / libPhoneNumberTests / libPhoneNumberTests.m" + +## Sample Usage + NBPhoneNumberUtil *phoneUtil = [NBPhoneNumberUtil sharedInstance]; + + NSError *aError = nil; + NBPhoneNumber *myNumber = [phoneUtil parse:@"6766077303" defaultRegion:@"AT" error:&aError]; + + if (aError == nil) { + // Should check error + NSLog(@"isValidPhoneNumber ? [%@]", [phoneUtil isValidNumber:myNumber] ? @"YES":@"NO"); + NSLog(@"E164 : %@", [phoneUtil format:myNumber numberFormat:NBEPhoneNumberFormatE164 + error:&aError]); + NSLog(@"INTERNATIONAL : %@", [phoneUtil format:myNumber numberFormat:NBEPhoneNumberFormatINTERNATIONAL + error:&aError]); + NSLog(@"NATIONAL : %@", [phoneUtil format:myNumber numberFormat:NBEPhoneNumberFormatNATIONAL + error:&aError]); + NSLog(@"RFC3966 : %@", [phoneUtil format:myNumber numberFormat:NBEPhoneNumberFormatRFC3966 + error:&aError]); + } + else { + NSLog(@"Error : %@", [aError localizedDescription]); + } + + NSLog (@"extractCountryCode [%ld]", [phoneUtil extractCountryCode:@"823213123123" + nationalNumber:nil]); + + NSString *res = nil; + UInt32 dRes = [phoneUtil extractCountryCode:@"823213123123" nationalNumber:&res]; + + NSLog (@"extractCountryCode [%lu] [%@]", dRes, res); + + +### Visit http://code.google.com/p/libphonenumber/ for more information +#### or zen.isis@gmail.com + +## Metadata managing (updating metadata) +#### Step1. Fetch "metadata.js" and "metadatafortesting.js" from Repositories + svn checkout http://libphonenumber.googlecode.com/svn/trunk/ libphonenumber-read-only + +#### Step2. Convert Javascript Object to JSON using PHP scripts + Output - "PhoneNumberMetaData.json" and "PhoneNumberMetaDataForTesting.json" + +#### Step3. Generate binary file from NBPhoneMetaDataGenerator + Output - "NBPhoneNumberMetadata.plist" and "NBPhoneNumberMetadataForTesting.plist" + +#### Step4. Update exists "NBPhoneNumberMetadata.plist" and "NBPhoneNumberMetadataForTesting.plist" files + +## Porting Rules +* **Remain** javascript libPhonenumber code logic (and comments etc) as possible for maintenance with *.js
diff --git a/libPhoneNumber/NBAsYouTypeFormatter.h b/libPhoneNumber/NBAsYouTypeFormatter.h new file mode 100644 index 0000000..c994fd5 --- /dev/null +++ b/libPhoneNumber/NBAsYouTypeFormatter.h
@@ -0,0 +1,21 @@ +// +// NBAsYouTypeFormatter.h +// libPhoneNumber +// +// Created by ishtar on 13. 2. 25.. +// Copyright (c) 2013년 NHN. All rights reserved. +// + +#import <Foundation/Foundation.h> + +@interface NBAsYouTypeFormatter : NSObject + +- (id)initWithRegionCode:(NSString*)regionCode; +- (id)initWithRegionCodeForTest:(NSString*)regionCode; + +- (NSString*)inputDigit:(NSString*)nextChar; +- (NSString*)inputDigitAndRememberPosition:(NSString*)nextChar; +- (NSInteger)getRememberedPosition; +- (void)clear; + +@end
diff --git a/libPhoneNumber/NBAsYouTypeFormatter.m b/libPhoneNumber/NBAsYouTypeFormatter.m new file mode 100644 index 0000000..742199d --- /dev/null +++ b/libPhoneNumber/NBAsYouTypeFormatter.m
@@ -0,0 +1,1146 @@ +// +// NBAsYouTypeFormatter.m +// libPhoneNumber +// +// Created by ishtar on 13. 2. 25.. +// Copyright (c) 2013년 NHN. All rights reserved. +// + +#import "NBAsYouTypeFormatter.h" + +#import "NBPhoneNumberUtil.h" +#import "NBPhoneMetaData.h" +#import "NBNumberFormat.h" + + +@interface NBAsYouTypeFormatter () + +@property (nonatomic, strong, readwrite) NSString *DIGIT_PLACEHOLDER_; +@property (nonatomic, assign, readwrite) NSString *SEPARATOR_BEFORE_NATIONAL_NUMBER_; +@property (nonatomic, strong, readwrite) NSString *currentOutput_, *currentFormattingPattern_; +@property (nonatomic, strong, readwrite) NSString *defaultCountry_; +@property (nonatomic, strong, readwrite) NSString *nationalPrefixExtracted_; +@property (nonatomic, strong, readwrite) NSMutableString *formattingTemplate_, *accruedInput_, *prefixBeforeNationalNumber_, *accruedInputWithoutFormatting_, *nationalNumber_; +@property (nonatomic, strong, readwrite) NSRegularExpression *DIGIT_PATTERN_, *NATIONAL_PREFIX_SEPARATORS_PATTERN_, *CHARACTER_CLASS_PATTERN_, *STANDALONE_DIGIT_PATTERN_; +@property (nonatomic, strong, readwrite) NSRegularExpression *ELIGIBLE_FORMAT_PATTERN_; +@property (nonatomic, assign, readwrite) BOOL ableToFormat_, inputHasFormatting_, isCompleteNumber_, isExpectingCountryCallingCode_, shouldAddSpaceAfterNationalPrefix_; +@property (nonatomic, strong, readwrite) NBPhoneNumberUtil *phoneUtil_; +@property (nonatomic, assign, readwrite) NSUInteger lastMatchPosition_, originalPosition_, positionToRemember_; +@property (nonatomic, assign, readwrite) NSUInteger MIN_LEADING_DIGITS_LENGTH_; +@property (nonatomic, strong, readwrite) NSMutableArray *possibleFormats_; +@property (nonatomic, strong, readwrite) NBPhoneMetaData *currentMetaData_, *defaultMetaData_, *EMPTY_METADATA_; + +@end + + +@implementation NBAsYouTypeFormatter + +- (id)init +{ + self = [super init]; + + if (self) { + /** + * @type {i18n.phonenumbers.PhoneNumberUtil} + * @private + */ + self.phoneUtil_ = [NBPhoneNumberUtil sharedInstance]; + + /** + * The digits that have not been entered yet will be represented by a \u2008, + * the punctuation space. + * @const + * @type {string} + * @private + */ + self.DIGIT_PLACEHOLDER_ = @"\u2008"; + + /** + * Character used when appropriate to separate a prefix, such as a long NDD or a + * country calling code, from the national number. + * @const + * @type {string} + * @private + */ + self.SEPARATOR_BEFORE_NATIONAL_NUMBER_ = @" "; + + /** + * This is the minimum length of national number accrued that is required to + * trigger the formatter. The first element of the leadingDigitsPattern of + * each numberFormat contains a regular expression that matches up to this + * number of digits. + * @const + * @type {number} + * @private + */ + self.MIN_LEADING_DIGITS_LENGTH_ = 3; + + /** + * @type {string} + * @private + */ + self.currentOutput_ = @""; + + /** + * @type {!goog.string.StringBuffer} + * @private + */ + self.formattingTemplate_ = [NSMutableString stringWithString:@""]; + + NSError *aError = nil; + + /** + * @type {RegExp} + * @private + */ + self.DIGIT_PATTERN_ = [NSRegularExpression regularExpressionWithPattern:self.DIGIT_PLACEHOLDER_ options:0 error:&aError]; + + /** + * A set of characters that, if found in a national prefix formatting rules, are + * an indicator to us that we should separate the national prefix from the + * number when formatting. + * @const + * @type {RegExp} + * @private + */ + self.NATIONAL_PREFIX_SEPARATORS_PATTERN_ = [NSRegularExpression regularExpressionWithPattern:@"[- ]" options:0 error:&aError]; + + /** + * A pattern that is used to match character classes in regular expressions. + * An example of a character class is [1-4]. + * @const + * @type {RegExp} + * @private + */ + self.CHARACTER_CLASS_PATTERN_ = [NSRegularExpression regularExpressionWithPattern:@"\\[([^\\[\\]])*\\]" options:0 error:&aError]; + + /** + * Any digit in a regular expression that actually denotes a digit. For + * example, in the regular expression 80[0-2]\d{6,10}, the first 2 digits + * (8 and 0) are standalone digits, but the rest are not. + * Two look-aheads are needed because the number following \\d could be a + * two-digit number, since the phone number can be as long as 15 digits. + * @const + * @type {RegExp} + * @private + */ + self.STANDALONE_DIGIT_PATTERN_ = [NSRegularExpression regularExpressionWithPattern:@"\\d(?=[^,}][^,}])" options:0 error:&aError]; + + /** + * A pattern that is used to determine if a numberFormat under availableFormats + * is eligible to be used by the AYTF. It is eligible when the format element + * under numberFormat contains groups of the dollar sign followed by a single + * digit, separated by valid phone number punctuation. This prevents invalid + * punctuation (such as the star sign in Israeli star numbers) getting into the + * output of the AYTF. + * @const + * @type {RegExp} + * @private + */ + NSString *eligible_format = @"^[-x‐-―−ー--/ ()()[].\\[\\]/~⁓∼~]*(\\$\\d[-x‐-―−ー--/ ()()[].\\[\\]/~⁓∼~]*)+$"; + self.ELIGIBLE_FORMAT_PATTERN_ = [NSRegularExpression regularExpressionWithPattern:eligible_format options:0 error:&aError]; + + /** + * The pattern from numberFormat that is currently used to create + * formattingTemplate. + * @type {string} + * @private + */ + self.currentFormattingPattern_ = @""; + + /** + * @type {!goog.string.StringBuffer} + * @private + */ + self.accruedInput_ = [NSMutableString stringWithString:@""]; + + /** + * @type {!goog.string.StringBuffer} + * @private + */ + self.accruedInputWithoutFormatting_ = [NSMutableString stringWithString:@""]; + + /** + * This indicates whether AsYouTypeFormatter is currently doing the + * formatting. + * @type {BOOL} + * @private + */ + self.ableToFormat_ = YES; + + /** + * Set to YES when users enter their own formatting. AsYouTypeFormatter will + * do no formatting at all when this is set to YES. + * @type {BOOL} + * @private + */ + self.inputHasFormatting_ = NO; + + /** + * This is set to YES when we know the user is entering a full national + * significant number, since we have either detected a national prefix or an + * international dialing prefix. When this is YES, we will no longer use + * local number formatting patterns. + * @type {BOOL} + * @private + */ + self.isCompleteNumber_ = NO; + + /** + * @type {BOOL} + * @private + */ + self.isExpectingCountryCallingCode_ = NO; + + /** + * @type {number} + * @private + */ + self.lastMatchPosition_ = 0; + + /** + * The position of a digit upon which inputDigitAndRememberPosition is most + * recently invoked, as found in the original sequence of characters the user + * entered. + * @type {number} + * @private + */ + self.originalPosition_ = 0; + + /** + * The position of a digit upon which inputDigitAndRememberPosition is most + * recently invoked, as found in accruedInputWithoutFormatting. + * entered. + * @type {number} + * @private + */ + self.positionToRemember_ = 0; + + /** + * This contains anything that has been entered so far preceding the national + * significant number, and it is formatted (e.g. with space inserted). For + * example, this can contain IDD, country code, and/or NDD, etc. + * @type {!goog.string.StringBuffer} + * @private + */ + self.prefixBeforeNationalNumber_ = [NSMutableString stringWithString:@""]; + + /** + * @type {BOOL} + * @private + */ + self.shouldAddSpaceAfterNationalPrefix_ = NO; + + /** + * This contains the national prefix that has been extracted. It contains only + * digits without formatting. + * @type {string} + * @private + */ + self.nationalPrefixExtracted_ = @""; + + /** + * @type {!goog.string.StringBuffer} + * @private + */ + self.nationalNumber_ = [NSMutableString stringWithString:@""]; + + /** + * @type {Array.<i18n.phonenumbers.NumberFormat>} + * @private + */ + self.possibleFormats_ = [[NSMutableArray alloc] init]; + } + + return self; +} + +/** + * Constructs an AsYouTypeFormatter for the specific region. + * + * @param {string} regionCode the ISO 3166-1 two-letter region code that denotes + * the region where the phone number is being entered. + * @constructor + */ + +- (id)initWithRegionCode:(NSString*)regionCode +{ + self = [self init]; + + if (self) { + /** + * @type {string} + * @private + */ + self.defaultCountry_ = regionCode; + self.currentMetaData_ = [self getMetadataForRegion_:self.defaultCountry_]; + /** + * @type {i18n.phonenumbers.PhoneMetadata} + * @private + */ + self.defaultMetaData_ = self.currentMetaData_; + + /** + * @const + * @type {i18n.phonenumbers.PhoneMetadata} + * @private + */ + self.EMPTY_METADATA_ = [[NBPhoneMetaData alloc] init]; + [self.EMPTY_METADATA_ setInternationalPrefix:@"NA"]; + } + + return self; +} + +- (id)initWithRegionCodeForTest:(NSString*)regionCode +{ + self = [self init]; + + if (self) { + self.phoneUtil_ = [NBPhoneNumberUtil sharedInstanceForTest]; + + self.defaultCountry_ = regionCode; + self.currentMetaData_ = [self getMetadataForRegion_:self.defaultCountry_]; + self.defaultMetaData_ = self.currentMetaData_; + self.EMPTY_METADATA_ = [[NBPhoneMetaData alloc] init]; + [self.EMPTY_METADATA_ setInternationalPrefix:@"NA"]; + } + + return self; +} + +/** + * The metadata needed by this class is the same for all regions sharing the + * same country calling code. Therefore, we return the metadata for "main" + * region for this country calling code. + * @param {string} regionCode an ISO 3166-1 two-letter region code. + * @return {i18n.phonenumbers.PhoneMetadata} main metadata for this region. + * @private + */ +- (NBPhoneMetaData*)getMetadataForRegion_:(NSString*)regionCode +{ + + /** @type {number} */ + NSNumber *countryCallingCode = [self.phoneUtil_ getCountryCodeForRegion:regionCode]; + /** @type {string} */ + NSString *mainCountry = [self.phoneUtil_ getRegionCodeForCountryCode:countryCallingCode]; + /** @type {i18n.phonenumbers.PhoneMetadata} */ + NBPhoneMetaData *metadata = [self.phoneUtil_ getMetadataForRegion:mainCountry]; + if (metadata != nil) { + return metadata; + } + // Set to a default instance of the metadata. This allows us to function with + // an incorrect region code, even if formatting only works for numbers + // specified with '+'. + return self.EMPTY_METADATA_; +}; + + +/** + * @return {BOOL} YES if a new template is created as opposed to reusing the + * existing template. + * @private + */ +- (BOOL)maybeCreateNewTemplate_ +{ + // When there are multiple available formats, the formatter uses the first + // format where a formatting template could be created. + /** @type {number} */ + NSUInteger possibleFormatsLength = [self.possibleFormats_ count]; + for (NSUInteger i = 0; i < possibleFormatsLength; ++i) + { + /** @type {i18n.phonenumbers.NumberFormat} */ + NBNumberFormat *numberFormat = [self.possibleFormats_ safeObjectAtIndex:i]; + /** @type {string} */ + NSString *pattern = numberFormat.pattern; + + if ([self.currentFormattingPattern_ isEqualToString:pattern]) { + return NO; + } + + if ([self createFormattingTemplate_:numberFormat ]) + { + self.currentFormattingPattern_ = pattern; + NSRange nationalPrefixRange = NSMakeRange(0, [numberFormat.nationalPrefixFormattingRule length]); + if (nationalPrefixRange.length > 0) { + NSTextCheckingResult *matchResult = + [self.NATIONAL_PREFIX_SEPARATORS_PATTERN_ firstMatchInString:numberFormat.nationalPrefixFormattingRule + options:0 + range:nationalPrefixRange]; + self.shouldAddSpaceAfterNationalPrefix_ = (matchResult != nil); + } else { + self.shouldAddSpaceAfterNationalPrefix_ = NO; + } + // With a new formatting template, the matched position using the old + // template needs to be reset. + self.lastMatchPosition_ = 0; + return YES; + } + } + self.ableToFormat_ = NO; + return NO; +}; + + +/** + * @param {string} leadingThreeDigits first three digits of entered number. + * @private + */ +- (void)getAvailableFormats_:(NSString*)leadingThreeDigits +{ + /** @type {Array.<i18n.phonenumbers.NumberFormat>} */ + BOOL isIntlNumberFormats = (self.isCompleteNumber_ && self.currentMetaData_.intlNumberFormats.count > 0); + NSMutableArray *formatList = isIntlNumberFormats ? self.currentMetaData_.intlNumberFormats : self.currentMetaData_.numberFormats; + + /** @type {number} */ + NSUInteger formatListLength = formatList.count; + + for (NSUInteger i = 0; i < formatListLength; ++i) + { + /** @type {i18n.phonenumbers.NumberFormat} */ + NBNumberFormat *format = [formatList safeObjectAtIndex:i]; + /** @type {BOOL} */ + BOOL nationalPrefixIsUsedByCountry = (self.currentMetaData_.nationalPrefix && self.currentMetaData_.nationalPrefix.length > 0); + + if (!nationalPrefixIsUsedByCountry || self.isCompleteNumber_ || format.nationalPrefixOptionalWhenFormatting || + [self.phoneUtil_ formattingRuleHasFirstGroupOnly:format.nationalPrefixFormattingRule]) + { + if ([self isFormatEligible_:format.format]) { + [self.possibleFormats_ addObject:format]; + } + } + } + + [self narrowDownPossibleFormats_:leadingThreeDigits]; +}; + + +/** + * @param {string} format + * @return {BOOL} + * @private + */ +- (BOOL)isFormatEligible_:(NSString*)format +{ + NSTextCheckingResult *matchResult = + [self.ELIGIBLE_FORMAT_PATTERN_ firstMatchInString:format options:0 range:NSMakeRange(0, [format length])]; + return (matchResult != nil); +}; + + +/** + * @param {string} leadingDigits + * @private + */ +- (void)narrowDownPossibleFormats_:(NSString *)leadingDigits +{ + /** @type {Array.<i18n.phonenumbers.NumberFormat>} */ + NSMutableArray *possibleFormats = [[NSMutableArray alloc] init]; + /** @type {number} */ + NSUInteger indexOfLeadingDigitsPattern = leadingDigits.length - self.MIN_LEADING_DIGITS_LENGTH_; + /** @type {number} */ + NSUInteger possibleFormatsLength = self.possibleFormats_.count; + for (NSUInteger i = 0; i < possibleFormatsLength; ++i) + { + /** @type {i18n.phonenumbers.NumberFormat} */ + NBNumberFormat *format = [self.possibleFormats_ safeObjectAtIndex:i]; + if (format.leadingDigitsPatterns.count > indexOfLeadingDigitsPattern) + { + /** @type {string} */ + NSString *leadingDigitsPattern = [format.leadingDigitsPatterns safeObjectAtIndex:indexOfLeadingDigitsPattern]; + + if ([self.phoneUtil_ stringPositionByRegex:leadingDigits regex:leadingDigitsPattern] == 0) + { + [possibleFormats addObject:format]; + } + } else { + // else the particular format has no more specific leadingDigitsPattern, + // and it should be retained. + [possibleFormats addObject:[self.possibleFormats_ safeObjectAtIndex:i]]; + } + } + self.possibleFormats_ = possibleFormats; +}; + + +/** + * @param {i18n.phonenumbers.NumberFormat} format + * @return {BOOL} + * @private + */ +- (BOOL)createFormattingTemplate_:(NBNumberFormat*)format +{ + /** @type {string} */ + NSString *numberPattern = format.pattern; + + // The formatter doesn't format numbers when numberPattern contains '|', e.g. + // (20|3)\d{4}. In those cases we quickly return. + NSRange stringRange = [numberPattern rangeOfString:@"|"]; + if (stringRange.location != NSNotFound) { + return NO; + } + + // Replace anything in the form of [..] with \d + numberPattern = [self.CHARACTER_CLASS_PATTERN_ stringByReplacingMatchesInString:numberPattern + options:0 range:NSMakeRange(0, [numberPattern length]) + withTemplate:@"\\\\d"]; + + // Replace any standalone digit (not the one in d{}) with \d + numberPattern = [self.STANDALONE_DIGIT_PATTERN_ stringByReplacingMatchesInString:numberPattern + options:0 range:NSMakeRange(0, [numberPattern length]) + withTemplate:@"\\\\d"]; + self.formattingTemplate_ = [NSMutableString stringWithString:@""]; + + /** @type {string} */ + NSString *tempTemplate = [self getFormattingTemplate_:numberPattern numberFormat:format.format]; + if (tempTemplate.length > 0) { + [self.formattingTemplate_ appendString:tempTemplate]; + return YES; + } + return NO; +}; + + +/** + * Gets a formatting template which can be used to efficiently format a + * partial number where digits are added one by one. + * + * @param {string} numberPattern + * @param {string} numberFormat + * @return {string} + * @private + */ +- (NSString*)getFormattingTemplate_:(NSString*)numberPattern numberFormat:(NSString*)numberFormat +{ + // Creates a phone number consisting only of the digit 9 that matches the + // numberPattern by applying the pattern to the longestPhoneNumber string. + /** @type {string} */ + NSString *longestPhoneNumber = @"999999999999999"; + + /** @type {Array.<string>} */ + NSArray *m = [self.phoneUtil_ matchedStringByRegex:longestPhoneNumber regex:numberPattern]; + + // this match will always succeed + /** @type {string} */ + NSString *aPhoneNumber = [m safeObjectAtIndex:0]; + // No formatting template can be created if the number of digits entered so + // far is longer than the maximum the current formatting rule can accommodate. + if (aPhoneNumber.length < self.nationalNumber_.length) { + return @""; + } + // Formats the number according to numberFormat + /** @type {string} */ + NSString *template = [self.phoneUtil_ replaceStringByRegex:aPhoneNumber regex:numberPattern withTemplate:numberFormat]; + + // Replaces each digit with character DIGIT_PLACEHOLDER + template = [self.phoneUtil_ replaceStringByRegex:template regex:@"9" withTemplate:self.DIGIT_PLACEHOLDER_]; + return template; +}; + + +/** + * Clears the internal state of the formatter, so it can be reused. + */ +- (void)clear +{ + self.currentOutput_ = @""; + self.accruedInput_ = [NSMutableString stringWithString:@""]; + self.accruedInputWithoutFormatting_ = [NSMutableString stringWithString:@""]; + self.formattingTemplate_ = [NSMutableString stringWithString:@""]; + self.lastMatchPosition_ = 0; + self.currentFormattingPattern_ = @""; + self.prefixBeforeNationalNumber_ = [NSMutableString stringWithString:@""]; + self.nationalPrefixExtracted_ = @""; + self.nationalNumber_ = [NSMutableString stringWithString:@""]; + self.ableToFormat_ = YES; + self.inputHasFormatting_ = NO; + self.positionToRemember_ = 0; + self.originalPosition_ = 0; + self.isCompleteNumber_ = NO; + self.isExpectingCountryCallingCode_ = NO; + [self.possibleFormats_ removeAllObjects]; + self.shouldAddSpaceAfterNationalPrefix_ = NO; + + if (self.currentMetaData_ != self.defaultMetaData_) { + self.currentMetaData_ = [self getMetadataForRegion_:self.defaultCountry_]; + } +} + + +/** + * Formats a phone number on-the-fly as each digit is entered. + * + * @param {string} nextChar the most recently entered digit of a phone number. + * Formatting characters are allowed, but as soon as they are encountered + * this method formats the number as entered and not 'as you type' anymore. + * Full width digits and Arabic-indic digits are allowed, and will be shown + * as they are. + * @return {string} the partially formatted phone number. + */ +- (NSString*)inputDigit:(NSString*)nextChar +{ + self.currentOutput_ = [self inputDigitWithOptionToRememberPosition_:nextChar rememberPosition:NO]; + return self.currentOutput_; +} + + +/** + * Same as {@link #inputDigit}, but remembers the position where + * {@code nextChar} is inserted, so that it can be retrieved later by using + * {@link #getRememberedPosition}. The remembered position will be automatically + * adjusted if additional formatting characters are later inserted/removed in + * front of {@code nextChar}. + * + * @param {string} nextChar + * @return {string} + */ +- (NSString*)inputDigitAndRememberPosition:(NSString*)nextChar +{ + self.currentOutput_ = [self inputDigitWithOptionToRememberPosition_:nextChar rememberPosition:YES]; + return self.currentOutput_; +}; + + +/** + * @param {string} nextChar + * @param {BOOL} rememberPosition + * @return {string} + * @private + */ +- (NSString*)inputDigitWithOptionToRememberPosition_:(NSString*)nextChar rememberPosition:(BOOL)rememberPosition +{ + [self.accruedInput_ appendString:nextChar]; + + if (rememberPosition) { + self.originalPosition_ = self.accruedInput_.length; + } + + // We do formatting on-the-fly only when each character entered is either a + // digit, or a plus sign (accepted at the start of the number only). + if (![self isDigitOrLeadingPlusSign_:nextChar]) + { + self.ableToFormat_ = NO; + self.inputHasFormatting_ = YES; + } else { + nextChar = [self normalizeAndAccrueDigitsAndPlusSign_:nextChar rememberPosition:rememberPosition]; + } + + if (!self.ableToFormat_) { + // When we are unable to format because of reasons other than that + // formatting chars have been entered, it can be due to really long IDDs or + // NDDs. If that is the case, we might be able to do formatting again after + // extracting them. + if (self.inputHasFormatting_) { + return [NSString stringWithString:self.accruedInput_]; + } + else if ([self attemptToExtractIdd_]) { + if ([self attemptToExtractCountryCallingCode_]) { + return [self attemptToChoosePatternWithPrefixExtracted_]; + } + } + else if ([self ableToExtractLongerNdd_]) { + // Add an additional space to separate long NDD and national significant + // number for readability. We don't set shouldAddSpaceAfterNationalPrefix_ + // to YES, since we don't want this to change later when we choose + // formatting templates. + [self.prefixBeforeNationalNumber_ appendString:[NSString stringWithFormat: @"%@", self.SEPARATOR_BEFORE_NATIONAL_NUMBER_]]; + return [self attemptToChoosePatternWithPrefixExtracted_]; + } + return self.accruedInput_; + } + + // We start to attempt to format only when at least MIN_LEADING_DIGITS_LENGTH + // digits (the plus sign is counted as a digit as well for this purpose) have + // been entered. + switch (self.accruedInputWithoutFormatting_.length) + { + case 0: + case 1: + case 2: + return self.accruedInput_; + case 3: + if ([self attemptToExtractIdd_]) { + self.isExpectingCountryCallingCode_ = YES; + } else { + // No IDD or plus sign is found, might be entering in national format. + self.nationalPrefixExtracted_ = [self removeNationalPrefixFromNationalNumber_]; + return [self attemptToChooseFormattingPattern_]; + } + default: + if (self.isExpectingCountryCallingCode_) { + if ([self attemptToExtractCountryCallingCode_]) { + self.isExpectingCountryCallingCode_ = NO; + } + return [NSString stringWithFormat:@"%@%@", self.prefixBeforeNationalNumber_, self.nationalNumber_]; + } + + if (self.possibleFormats_.count > 0) { + // The formatting pattern is already chosen. + /** @type {string} */ + NSString *tempNationalNumber = [self inputDigitHelper_:nextChar]; + // See if the accrued digits can be formatted properly already. If not, + // use the results from inputDigitHelper, which does formatting based on + // the formatting pattern chosen. + /** @type {string} */ + NSString *formattedNumber = [self attemptToFormatAccruedDigits_]; + if (formattedNumber.length > 0) { + return formattedNumber; + } + + [self narrowDownPossibleFormats_:self.nationalNumber_]; + + if ([self maybeCreateNewTemplate_]) { + return [self inputAccruedNationalNumber_]; + } + + return self.ableToFormat_ ? [self appendNationalNumber_:tempNationalNumber] : self.accruedInput_; + } + else { + return [self attemptToChooseFormattingPattern_]; + } + } +}; + + +/** + * @return {string} + * @private + */ +- (NSString*)attemptToChoosePatternWithPrefixExtracted_ +{ + self.ableToFormat_ = YES; + self.isExpectingCountryCallingCode_ = NO; + [self.possibleFormats_ removeAllObjects]; + return [self attemptToChooseFormattingPattern_]; +}; + + +/** + * Some national prefixes are a substring of others. If extracting the shorter + * NDD doesn't result in a number we can format, we try to see if we can extract + * a longer version here. + * @return {BOOL} + * @private + */ +- (BOOL)ableToExtractLongerNdd_ +{ + if (self.nationalPrefixExtracted_.length > 0) + { + // Put the extracted NDD back to the national number before attempting to + // extract a new NDD. + /** @type {string} */ + NSString *nationalNumberStr = [NSString stringWithString:self.nationalNumber_]; + self.nationalNumber_ = [NSMutableString stringWithString:@""]; + [self.nationalNumber_ appendString:self.nationalPrefixExtracted_]; + [self.nationalNumber_ appendString:nationalNumberStr]; + // Remove the previously extracted NDD from prefixBeforeNationalNumber. We + // cannot simply set it to empty string because people sometimes incorrectly + // enter national prefix after the country code, e.g. +44 (0)20-1234-5678. + /** @type {string} */ + NSString *prefixBeforeNationalNumberStr = [NSString stringWithString:self.prefixBeforeNationalNumber_]; + NSRange lastRange = [prefixBeforeNationalNumberStr rangeOfString:self.nationalPrefixExtracted_ options:NSBackwardsSearch]; + /** @type {number} */ + NSUInteger indexOfPreviousNdd = lastRange.location; + self.prefixBeforeNationalNumber_ = [NSMutableString stringWithString:@""]; + [self.prefixBeforeNationalNumber_ appendString:[prefixBeforeNationalNumberStr substringWithRange:NSMakeRange(0, indexOfPreviousNdd)]]; + } + + return self.nationalPrefixExtracted_ != [self removeNationalPrefixFromNationalNumber_]; +}; + + +/** + * @param {string} nextChar + * @return {BOOL} + * @private + */ +- (BOOL)isDigitOrLeadingPlusSign_:(NSString*)nextChar +{ + NSString *digitPattern = [NSString stringWithFormat:@"([%@])", [self.phoneUtil_ VALID_DIGITS_STRING]]; + NSString *plusPattern = [NSString stringWithFormat:@"[%@]+", [self.phoneUtil_ PLUS_CHARS_]]; + + BOOL isDigitPattern = [[self.phoneUtil_ matchesByRegex:nextChar regex:digitPattern] count] > 0; + BOOL isPlusPattern = [[self.phoneUtil_ matchesByRegex:nextChar regex:plusPattern] count] > 0; + + return isDigitPattern || (self.accruedInput_.length == 1 && isPlusPattern); +}; + + +/** + * Check to see if there is an exact pattern match for these digits. If so, we + * should use this instead of any other formatting template whose + * leadingDigitsPattern also matches the input. + * @return {string} + * @private + */ +- (NSString*)attemptToFormatAccruedDigits_ +{ + /** @type {string} */ + NSString *nationalNumber = [NSString stringWithString:self.nationalNumber_]; + + /** @type {number} */ + NSUInteger possibleFormatsLength = self.possibleFormats_.count; + for (NSUInteger i = 0; i < possibleFormatsLength; ++i) + { + /** @type {i18n.phonenumbers.NumberFormat} */ + NBNumberFormat *numberFormat = self.possibleFormats_[i]; + /** @type {string} */ + NSString * pattern = numberFormat.pattern; + /** @type {RegExp} */ + NSString *patternRegExp = [NSString stringWithFormat:@"^(?:%@)$", pattern]; + BOOL isPatternRegExp = [[self.phoneUtil_ matchesByRegex:nationalNumber regex:patternRegExp] count] > 0; + if (isPatternRegExp) { + if (numberFormat.nationalPrefixFormattingRule.length > 0) { + NSArray *matches = [self.NATIONAL_PREFIX_SEPARATORS_PATTERN_ matchesInString:numberFormat.nationalPrefixFormattingRule + options:0 + range:NSMakeRange(0, numberFormat.nationalPrefixFormattingRule.length)]; + self.shouldAddSpaceAfterNationalPrefix_ = [matches count] > 0; + } else { + self.shouldAddSpaceAfterNationalPrefix_ = NO; + } + + /** @type {string} */ + NSString *formattedNumber = [self.phoneUtil_ replaceStringByRegex:nationalNumber + regex:pattern + withTemplate:numberFormat.format]; + return [self appendNationalNumber_:formattedNumber]; + } + } + return @""; +}; + + +/** + * Combines the national number with any prefix (IDD/+ and country code or + * national prefix) that was collected. A space will be inserted between them if + * the current formatting template indicates this to be suitable. + * @param {string} nationalNumber The number to be appended. + * @return {string} The combined number. + * @private + */ +- (NSString*)appendNationalNumber_:(NSString*)nationalNumber +{ + /** @type {number} */ + NSUInteger prefixBeforeNationalNumberLength = self.prefixBeforeNationalNumber_.length; + unichar blank_char = [self.SEPARATOR_BEFORE_NATIONAL_NUMBER_ characterAtIndex:0]; + if (self.shouldAddSpaceAfterNationalPrefix_ && prefixBeforeNationalNumberLength > 0 && + [self.prefixBeforeNationalNumber_ characterAtIndex:prefixBeforeNationalNumberLength - 1] != blank_char) + { + // We want to add a space after the national prefix if the national prefix + // formatting rule indicates that this would normally be done, with the + // exception of the case where we already appended a space because the NDD + // was surprisingly long. + + return [NSString stringWithFormat:@"%@%@%@", self.prefixBeforeNationalNumber_, self.SEPARATOR_BEFORE_NATIONAL_NUMBER_, nationalNumber]; + } else { + return [NSString stringWithFormat:@"%@%@", self.prefixBeforeNationalNumber_, nationalNumber]; + } +}; + + +/** + * Returns the current position in the partially formatted phone number of the + * character which was previously passed in as the parameter of + * {@link #inputDigitAndRememberPosition}. + * + * @return {number} + */ +- (NSInteger)getRememberedPosition +{ + if (!self.ableToFormat_) { + return self.originalPosition_; + } + /** @type {number} */ + NSUInteger accruedInputIndex = 0; + /** @type {number} */ + NSUInteger currentOutputIndex = 0; + /** @type {string} */ + NSString *accruedInputWithoutFormatting = self.accruedInputWithoutFormatting_; + /** @type {string} */ + NSString *currentOutput = self.currentOutput_; + + while (accruedInputIndex < self.positionToRemember_ && currentOutputIndex < currentOutput.length) + { + if ([accruedInputWithoutFormatting characterAtIndex:accruedInputIndex] == [currentOutput characterAtIndex:currentOutputIndex]) + { + accruedInputIndex++; + } + currentOutputIndex++; + } + return currentOutputIndex; +}; + + +/** + * Attempts to set the formatting template and returns a string which contains + * the formatted version of the digits entered so far. + * + * @return {string} + * @private + */ +- (NSString*)attemptToChooseFormattingPattern_ +{ + /** @type {string} */ + NSString *nationalNumber = [self.nationalNumber_ copy]; + // We start to attempt to format only when as least MIN_LEADING_DIGITS_LENGTH + // digits of national number (excluding national prefix) have been entered. + if (nationalNumber.length >= self.MIN_LEADING_DIGITS_LENGTH_) { + [self getAvailableFormats_:[nationalNumber substringWithRange:NSMakeRange(0, self.MIN_LEADING_DIGITS_LENGTH_)]]; + return [self maybeCreateNewTemplate_] ? [self inputAccruedNationalNumber_] : self.accruedInput_; + } else { + return [self appendNationalNumber_:nationalNumber]; + } +} + + +/** + * Invokes inputDigitHelper on each digit of the national number accrued, and + * returns a formatted string in the end. + * + * @return {string} + * @private + */ +- (NSString*)inputAccruedNationalNumber_ +{ + /** @type {string} */ + NSString *nationalNumber = [self.nationalNumber_ copy]; + /** @type {number} */ + NSUInteger lengthOfNationalNumber = nationalNumber.length; + if (lengthOfNationalNumber > 0) { + /** @type {string} */ + NSString *tempNationalNumber = @""; + for (NSUInteger i = 0; i < lengthOfNationalNumber; i++) + { + tempNationalNumber = [self inputDigitHelper_:[NSString stringWithFormat: @"%C", [nationalNumber characterAtIndex:i]]]; + } + return self.ableToFormat_ ? [self appendNationalNumber_:tempNationalNumber] : self.accruedInput_; + } else { + return self.prefixBeforeNationalNumber_; + } +}; + + +/** + * @return {BOOL} YES if the current country is a NANPA country and the + * national number begins with the national prefix. + * @private + */ +- (BOOL)isNanpaNumberWithNationalPrefix_ +{ + // For NANPA numbers beginning with 1[2-9], treat the 1 as the national + // prefix. The reason is that national significant numbers in NANPA always + // start with [2-9] after the national prefix. Numbers beginning with 1[01] + // can only be short/emergency numbers, which don't need the national prefix. + if (![self.currentMetaData_.countryCode isEqualToNumber:@1]) { + return NO; + } + + /** @type {string} */ + NSString *nationalNumber = [self.nationalNumber_ copy]; + return ([nationalNumber characterAtIndex:0] == '1') && ([nationalNumber characterAtIndex:1] != '0') && + ([nationalNumber characterAtIndex:1] != '1'); +}; + + +/** + * Returns the national prefix extracted, or an empty string if it is not + * present. + * @return {string} + * @private + */ +- (NSString*)removeNationalPrefixFromNationalNumber_ +{ + /** @type {string} */ + NSString *nationalNumber = [self.nationalNumber_ copy]; + /** @type {number} */ + NSUInteger startOfNationalNumber = 0; + + if ([self isNanpaNumberWithNationalPrefix_]) { + startOfNationalNumber = 1; + [self.prefixBeforeNationalNumber_ appendString:@"1"]; + [self.prefixBeforeNationalNumber_ appendFormat:@"%@", self.SEPARATOR_BEFORE_NATIONAL_NUMBER_]; + self.isCompleteNumber_ = YES; + } + else if (self.currentMetaData_.nationalPrefixForParsing != nil && self.currentMetaData_.nationalPrefixForParsing.length > 0) + { + /** @type {RegExp} */ + NSString *nationalPrefixForParsing = [NSString stringWithFormat:@"^(?:%@)", self.currentMetaData_.nationalPrefixForParsing]; + /** @type {Array.<string>} */ + NSArray *m = [self.phoneUtil_ matchedStringByRegex:nationalNumber regex:nationalPrefixForParsing]; + NSString *firstString = [m safeObjectAtIndex:0]; + if (m != nil && firstString != nil && firstString.length > 0) { + // When the national prefix is detected, we use international formatting + // rules instead of national ones, because national formatting rules could + // contain local formatting rules for numbers entered without area code. + self.isCompleteNumber_ = YES; + startOfNationalNumber = firstString.length; + [self.prefixBeforeNationalNumber_ appendString:[nationalNumber substringWithRange:NSMakeRange(0, startOfNationalNumber)]]; + } + } + + self.nationalNumber_ = [NSMutableString stringWithString:@""]; + [self.nationalNumber_ appendString:[nationalNumber substringFromIndex:startOfNationalNumber]]; + return [nationalNumber substringWithRange:NSMakeRange(0, startOfNationalNumber)]; +}; + + +/** + * Extracts IDD and plus sign to prefixBeforeNationalNumber when they are + * available, and places the remaining input into nationalNumber. + * + * @return {BOOL} YES when accruedInputWithoutFormatting begins with the + * plus sign or valid IDD for defaultCountry. + * @private + */ +- (BOOL)attemptToExtractIdd_ +{ + /** @type {string} */ + NSString *accruedInputWithoutFormatting = [self.accruedInputWithoutFormatting_ copy]; + /** @type {RegExp} */ + NSString *internationalPrefix = [NSString stringWithFormat:@"^(?:\\+|%@)", self.currentMetaData_.internationalPrefix]; + /** @type {Array.<string>} */ + NSArray *m = [self.phoneUtil_ matchedStringByRegex:accruedInputWithoutFormatting regex:internationalPrefix]; + + NSString *firstString = [m safeObjectAtIndex:0]; + + if (m != nil && firstString != nil && firstString.length > 0) { + self.isCompleteNumber_ = YES; + /** @type {number} */ + NSUInteger startOfCountryCallingCode = firstString.length; + self.nationalNumber_ = [NSMutableString stringWithString:@""]; + [self.nationalNumber_ appendString:[accruedInputWithoutFormatting substringFromIndex:startOfCountryCallingCode]]; + self.prefixBeforeNationalNumber_ = [NSMutableString stringWithString:@""]; + [self.prefixBeforeNationalNumber_ appendString:[accruedInputWithoutFormatting substringWithRange:NSMakeRange(0, startOfCountryCallingCode)]]; + + if ([accruedInputWithoutFormatting characterAtIndex:0] != '+') + { + [self.prefixBeforeNationalNumber_ appendString:[NSString stringWithFormat: @"%@", self.SEPARATOR_BEFORE_NATIONAL_NUMBER_]]; + } + return YES; + } + return NO; +}; + + +/** + * Extracts the country calling code from the beginning of nationalNumber to + * prefixBeforeNationalNumber when they are available, and places the remaining + * input into nationalNumber. + * + * @return {BOOL} YES when a valid country calling code can be found. + * @private + */ +- (BOOL)attemptToExtractCountryCallingCode_ +{ + if (self.nationalNumber_.length == 0) { + return NO; + } + + /** @type {!goog.string.StringBuffer} */ + NSString *numberWithoutCountryCallingCode = @""; + + /** @type {number} */ + NSNumber *countryCode = [self.phoneUtil_ extractCountryCode:self.nationalNumber_ nationalNumber:&numberWithoutCountryCallingCode]; + + if ([countryCode isEqualToNumber:@0]) { + return NO; + } + + self.nationalNumber_ = [NSMutableString stringWithString:@""]; + [self.nationalNumber_ appendString:numberWithoutCountryCallingCode]; + + /** @type {string} */ + NSString *newRegionCode = [self.phoneUtil_ getRegionCodeForCountryCode:countryCode]; + + if ([[self.phoneUtil_ REGION_CODE_FOR_NON_GEO_ENTITY] isEqualToString:newRegionCode]) { + self.currentMetaData_ = [self.phoneUtil_ getMetadataForNonGeographicalRegion:countryCode]; + } else if (newRegionCode != self.defaultCountry_) + { + self.currentMetaData_ = [self getMetadataForRegion_:newRegionCode]; + } + + /** @type {string} */ + NSString *countryCodeString = [NSString stringWithFormat:@"%@", countryCode]; + [self.prefixBeforeNationalNumber_ appendString:countryCodeString]; + [self.prefixBeforeNationalNumber_ appendString:[NSString stringWithFormat: @"%@", self.SEPARATOR_BEFORE_NATIONAL_NUMBER_]]; + return YES; +}; + + +/** + * Accrues digits and the plus sign to accruedInputWithoutFormatting for later + * use. If nextChar contains a digit in non-ASCII format (e.g. the full-width + * version of digits), it is first normalized to the ASCII version. The return + * value is nextChar itself, or its normalized version, if nextChar is a digit + * in non-ASCII format. This method assumes its input is either a digit or the + * plus sign. + * + * @param {string} nextChar + * @param {BOOL} rememberPosition + * @return {string} + * @private + */ +- (NSString*)normalizeAndAccrueDigitsAndPlusSign_:(NSString *)nextChar rememberPosition:(BOOL)rememberPosition +{ + /** @type {string} */ + NSString *normalizedChar; + + if ([nextChar isEqualToString:@"+"]) { + normalizedChar = nextChar; + [self.accruedInputWithoutFormatting_ appendString:nextChar]; + } else { + normalizedChar = [[self.phoneUtil_ DIGIT_MAPPINGS] objectForKey:nextChar]; + [self.accruedInputWithoutFormatting_ appendString:normalizedChar]; + [self.nationalNumber_ appendString:normalizedChar]; + } + + if (rememberPosition) { + self.positionToRemember_ = self.accruedInputWithoutFormatting_.length; + } + + return normalizedChar; +}; + + +/** + * @param {string} nextChar + * @return {string} + * @private + */ +- (NSString*)inputDigitHelper_:(NSString *)nextChar +{ + /** @type {string} */ + NSString *formattingTemplate = [self.formattingTemplate_ copy]; + NSString *subedString = @""; + + if (formattingTemplate.length > self.lastMatchPosition_) { + subedString = [formattingTemplate substringFromIndex:self.lastMatchPosition_]; + } + + if ([self.phoneUtil_ stringPositionByRegex:subedString regex:self.DIGIT_PLACEHOLDER_] >= 0) { + /** @type {number} */ + NSUInteger digitPatternStart = [self.phoneUtil_ stringPositionByRegex:formattingTemplate regex:self.DIGIT_PLACEHOLDER_]; + + /** @type {string} */ + NSRange tempRange = [formattingTemplate rangeOfString:self.DIGIT_PLACEHOLDER_]; + NSString *tempTemplate = [formattingTemplate stringByReplacingOccurrencesOfString:self.DIGIT_PLACEHOLDER_ + withString:nextChar + options:NSLiteralSearch + range:tempRange]; + self.formattingTemplate_ = [NSMutableString stringWithString:@""]; + [self.formattingTemplate_ appendString:tempTemplate]; + self.lastMatchPosition_ = digitPatternStart; + return [tempTemplate substringWithRange:NSMakeRange(0, self.lastMatchPosition_ + 1)]; + } else { + if (self.possibleFormats_.count == 1) + { + // More digits are entered than we could handle, and there are no other + // valid patterns to try. + self.ableToFormat_ = NO; + } // else, we just reset the formatting pattern. + self.currentFormattingPattern_ = @""; + return self.accruedInput_; + } +}; + +@end
diff --git a/libPhoneNumber/NBNumberFormat.h b/libPhoneNumber/NBNumberFormat.h new file mode 100644 index 0000000..d2e8749 --- /dev/null +++ b/libPhoneNumber/NBNumberFormat.h
@@ -0,0 +1,22 @@ +// +// NBPhoneNumberFormat.h +// libPhoneNumber +// +// Created by NHN Corp. Last Edited by BAND dev team (band_dev@nhn.com) +// + +#import <Foundation/Foundation.h> + +@interface NBNumberFormat : NSObject + +// from phonemetadata.pb.js +/* 1 */ @property (nonatomic, strong, readwrite) NSString *pattern; +/* 2 */ @property (nonatomic, strong, readwrite) NSString *format; +/* 3 */ @property (nonatomic, strong, readwrite) NSMutableArray *leadingDigitsPatterns; +/* 4 */ @property (nonatomic, strong, readwrite) NSString *nationalPrefixFormattingRule; +/* 6 */ @property (nonatomic, assign, readwrite) BOOL nationalPrefixOptionalWhenFormatting; +/* 5 */ @property (nonatomic, strong, readwrite) NSString *domesticCarrierCodeFormattingRule; + +- (id)initWithData:(id)data; + +@end
diff --git a/libPhoneNumber/NBNumberFormat.m b/libPhoneNumber/NBNumberFormat.m new file mode 100644 index 0000000..5f261a7 --- /dev/null +++ b/libPhoneNumber/NBNumberFormat.m
@@ -0,0 +1,139 @@ +// +// NBPhoneNumberFormat.m +// libPhoneNumber +// +// Created by NHN Corp. Last Edited by BAND dev team (band_dev@nhn.com) +// + +#import "NBNumberFormat.h" +#import "NBPhoneNumberDefines.h" + +@implementation NBNumberFormat + +@synthesize pattern, format, leadingDigitsPatterns, nationalPrefixFormattingRule, nationalPrefixOptionalWhenFormatting, domesticCarrierCodeFormattingRule; + + +- (id)initWithData:(id)data +{ + self = [self init]; + + if (self && data != nil && [data isKindOfClass:[NSArray class]]) + { + /* 1 */ self.pattern = [data safeObjectAtIndex:1]; + /* 2 */ self.format = [data safeObjectAtIndex:2]; + /* 3 */ self.leadingDigitsPatterns = [self stringArrayFromData:[data safeObjectAtIndex:3]]; // NSString array + /* 4 */ self.nationalPrefixFormattingRule = [data safeObjectAtIndex:4]; + /* 6 */ self.nationalPrefixOptionalWhenFormatting = [[data safeObjectAtIndex:6] boolValue]; + /* 5 */ self.domesticCarrierCodeFormattingRule = [data safeObjectAtIndex:5]; + } + + return self; +} + + +- (id)init +{ + self = [super init]; + + if (self) + { + self.nationalPrefixOptionalWhenFormatting = NO; + self.leadingDigitsPatterns = [[NSMutableArray alloc] init]; + } + + return self; +} + + +- (NSMutableArray*)stringArrayFromData:(id)data +{ + NSMutableArray *resArray = [[NSMutableArray alloc] init]; + if (data != nil && [data isKindOfClass:[NSArray class]]) + { + for (id numFormat in data) + { + if ([numFormat isKindOfClass:[NSString class]]) + { + [resArray addObject:numFormat]; + } + else + { + [resArray addObject:[numFormat stringValue]]; + } + } + } + + return resArray; +} + + +- (NSString *)description +{ + return [NSString stringWithFormat:@"[pattern:%@, format:%@, leadingDigitsPattern:%@, nationalPrefixFormattingRule:%@, nationalPrefixOptionalWhenFormatting:%@, domesticCarrierCodeFormattingRule:%@]", + self.pattern, self.format, self.leadingDigitsPatterns, self.nationalPrefixFormattingRule, self.nationalPrefixOptionalWhenFormatting?@"Y":@"N", self.domesticCarrierCodeFormattingRule]; +} + + +- (id)copyWithZone:(NSZone *)zone +{ + NBNumberFormat *phoneFormatCopy = [[NBNumberFormat allocWithZone:zone] init]; + + /* + 1 @property (nonatomic, strong, readwrite) NSString *pattern; + 2 @property (nonatomic, strong, readwrite) NSString *format; + 3 @property (nonatomic, strong, readwrite) NSString *leadingDigitsPattern; + 4 @property (nonatomic, strong, readwrite) NSString *nationalPrefixFormattingRule; + 6 @property (nonatomic, assign, readwrite) BOOL nationalPrefixOptionalWhenFormatting; + 5 @property (nonatomic, strong, readwrite) NSString *domesticCarrierCodeFormattingRule; + */ + + phoneFormatCopy.pattern = [self.pattern copy]; + phoneFormatCopy.format = [self.format copy]; + phoneFormatCopy.leadingDigitsPatterns = [self.leadingDigitsPatterns copy]; + phoneFormatCopy.nationalPrefixFormattingRule = [self.nationalPrefixFormattingRule copy]; + phoneFormatCopy.nationalPrefixOptionalWhenFormatting = self.nationalPrefixOptionalWhenFormatting; + phoneFormatCopy.domesticCarrierCodeFormattingRule = [self.domesticCarrierCodeFormattingRule copy]; + + return phoneFormatCopy; +} + + +- (id)initWithCoder:(NSCoder*)coder +{ + if (self = [super init]) + { + self.pattern = [coder decodeObjectForKey:@"pattern"]; + self.format = [coder decodeObjectForKey:@"format"]; + self.leadingDigitsPatterns = [coder decodeObjectForKey:@"leadingDigitsPatterns"]; + self.nationalPrefixFormattingRule = [coder decodeObjectForKey:@"nationalPrefixFormattingRule"]; + self.nationalPrefixOptionalWhenFormatting = [[coder decodeObjectForKey:@"nationalPrefixOptionalWhenFormatting"] boolValue]; + self.domesticCarrierCodeFormattingRule = [coder decodeObjectForKey:@"domesticCarrierCodeFormattingRule"]; + } + return self; +} + + +- (void)encodeWithCoder:(NSCoder*)coder +{ + [coder encodeObject:self.pattern forKey:@"pattern"]; + [coder encodeObject:self.format forKey:@"format"]; + [coder encodeObject:self.leadingDigitsPatterns forKey:@"leadingDigitsPatterns"]; + [coder encodeObject:self.nationalPrefixFormattingRule forKey:@"nationalPrefixFormattingRule"]; + [coder encodeObject:[NSNumber numberWithBool:self.nationalPrefixOptionalWhenFormatting] forKey:@"nationalPrefixOptionalWhenFormatting"]; + [coder encodeObject:self.domesticCarrierCodeFormattingRule forKey:@"domesticCarrierCodeFormattingRule"]; +} + + +- (void)setData:(id)data +{ + if ([data isKindOfClass:[NSArray class]] || [data isKindOfClass:[NSMutableArray class]]) + { + + } + else if ([data isKindOfClass:[NSDictionary class]] || [data isKindOfClass:[NSMutableDictionary class]]) + { + + } +} + +@end
diff --git a/libPhoneNumber/NBPhoneMetaData.h b/libPhoneNumber/NBPhoneMetaData.h new file mode 100644 index 0000000..6767492 --- /dev/null +++ b/libPhoneNumber/NBPhoneMetaData.h
@@ -0,0 +1,45 @@ +// +// M2PhoneMetaData.h +// libPhoneNumber +// +// Created by NHN Corp. Last Edited by BAND dev team (band_dev@nhn.com) +// + +#import <Foundation/Foundation.h> + +@class NBPhoneNumberDesc, NBNumberFormat; + +@interface NBPhoneMetaData : NSObject + +// from phonemetadata.pb.js +/* 1 */ @property (nonatomic, strong, readwrite) NBPhoneNumberDesc *generalDesc; +/* 2 */ @property (nonatomic, strong, readwrite) NBPhoneNumberDesc *fixedLine; +/* 3 */ @property (nonatomic, strong, readwrite) NBPhoneNumberDesc *mobile; +/* 4 */ @property (nonatomic, strong, readwrite) NBPhoneNumberDesc *tollFree; +/* 5 */ @property (nonatomic, strong, readwrite) NBPhoneNumberDesc *premiumRate; +/* 6 */ @property (nonatomic, strong, readwrite) NBPhoneNumberDesc *sharedCost; +/* 7 */ @property (nonatomic, strong, readwrite) NBPhoneNumberDesc *personalNumber; +/* 8 */ @property (nonatomic, strong, readwrite) NBPhoneNumberDesc *voip; +/* 21 */ @property (nonatomic, strong, readwrite) NBPhoneNumberDesc *pager; +/* 25 */ @property (nonatomic, strong, readwrite) NBPhoneNumberDesc *uan; +/* 27 */ @property (nonatomic, strong, readwrite) NBPhoneNumberDesc *emergency; +/* 28 */ @property (nonatomic, strong, readwrite) NBPhoneNumberDesc *voicemail; +/* 24 */ @property (nonatomic, strong, readwrite) NBPhoneNumberDesc *noInternationalDialling; +/* 9 */ @property (nonatomic, strong, readwrite) NSString *codeID; +/* 10 */ @property (nonatomic, strong, readwrite) NSNumber *countryCode; +/* 11 */ @property (nonatomic, strong, readwrite) NSString *internationalPrefix; +/* 17 */ @property (nonatomic, strong, readwrite) NSString *preferredInternationalPrefix; +/* 12 */ @property (nonatomic, strong, readwrite) NSString *nationalPrefix; +/* 13 */ @property (nonatomic, strong, readwrite) NSString *preferredExtnPrefix; +/* 15 */ @property (nonatomic, strong, readwrite) NSString *nationalPrefixForParsing; +/* 16 */ @property (nonatomic, strong, readwrite) NSString *nationalPrefixTransformRule; +/* 18 */ @property (nonatomic, assign, readwrite) BOOL sameMobileAndFixedLinePattern; +/* 19 */ @property (nonatomic, strong, readwrite) NSMutableArray *numberFormats; +/* 20 */ @property (nonatomic, strong, readwrite) NSMutableArray *intlNumberFormats; +/* 22 */ @property (nonatomic, assign, readwrite) BOOL mainCountryForCode; +/* 23 */ @property (nonatomic, strong, readwrite) NSString *leadingDigits; +/* 26 */ @property (nonatomic, assign, readwrite) BOOL leadingZeroPossible; + +- (void)buildData:(id)data; + +@end
diff --git a/libPhoneNumber/NBPhoneMetaData.m b/libPhoneNumber/NBPhoneMetaData.m new file mode 100644 index 0000000..8aa30d8 --- /dev/null +++ b/libPhoneNumber/NBPhoneMetaData.m
@@ -0,0 +1,458 @@ +// +// NBPhoneMetaData.m +// libPhoneNumber +// +// Created by NHN Corp. Last Edited by BAND dev team (band_dev@nhn.com) +// + +#import "NBPhoneMetaData.h" +#import "NBPhoneNumberDesc.h" +#import "NBNumberFormat.h" +#import "NBPhoneNumberDefines.h" + +@implementation NBPhoneMetaData + +@synthesize generalDesc, fixedLine, mobile, tollFree, premiumRate, sharedCost, personalNumber, voip, pager, uan, emergency, voicemail, noInternationalDialling; +@synthesize codeID, countryCode; +@synthesize internationalPrefix, preferredInternationalPrefix, nationalPrefix, preferredExtnPrefix, nationalPrefixForParsing, nationalPrefixTransformRule, sameMobileAndFixedLinePattern, numberFormats, intlNumberFormats, mainCountryForCode, leadingDigits, leadingZeroPossible; + +- (id)init +{ + self = [super init]; + + if (self) + { + [self setNumberFormats:[[NSMutableArray alloc] init]]; + [self setIntlNumberFormats:[[NSMutableArray alloc] init]]; + + self.leadingZeroPossible = NO; + self.mainCountryForCode = NO; + } + + return self; +} + + +- (NSString *)description +{ + return [NSString stringWithFormat:@"* codeID[%@] countryCode[%@] generalDesc[%@] fixedLine[%@] mobile[%@] tollFree[%@] premiumRate[%@] sharedCost[%@] personalNumber[%@] voip[%@] pager[%@] uan[%@] emergency[%@] voicemail[%@] noInternationalDialling[%@] internationalPrefix[%@] preferredInternationalPrefix[%@] nationalPrefix[%@] preferredExtnPrefix[%@] nationalPrefixForParsing[%@] nationalPrefixTransformRule[%@] sameMobileAndFixedLinePattern[%@] numberFormats[%@] intlNumberFormats[%@] mainCountryForCode[%@] leadingDigits[%@] leadingZeroPossible[%@]", + self.codeID, self.countryCode, self.generalDesc, self.fixedLine, self.mobile, self.tollFree, self.premiumRate, self.sharedCost, self.personalNumber, self.voip, self.pager, self.uan, self.emergency, self.voicemail, self.noInternationalDialling, self.internationalPrefix, self.preferredInternationalPrefix, self.nationalPrefix, self.preferredExtnPrefix, self.nationalPrefixForParsing, self.nationalPrefixTransformRule, self.sameMobileAndFixedLinePattern?@"Y":@"N", self.numberFormats, self.intlNumberFormats, self.mainCountryForCode?@"Y":@"N", self.leadingDigits, self.leadingZeroPossible?@"Y":@"N"]; +} + + +- (void)buildData:(id)data +{ + if (data != nil && [data isKindOfClass:[NSArray class]] ) + { + /* 1 */ self.generalDesc = [[NBPhoneNumberDesc alloc] initWithData:[data safeObjectAtIndex:1]]; + /* 2 */ self.fixedLine = [[NBPhoneNumberDesc alloc] initWithData:[data safeObjectAtIndex:2]]; + /* 3 */ self.mobile = [[NBPhoneNumberDesc alloc] initWithData:[data safeObjectAtIndex:3]]; + /* 4 */ self.tollFree = [[NBPhoneNumberDesc alloc] initWithData:[data safeObjectAtIndex:4]]; + /* 5 */ self.premiumRate = [[NBPhoneNumberDesc alloc] initWithData:[data safeObjectAtIndex:5]]; + /* 6 */ self.sharedCost = [[NBPhoneNumberDesc alloc] initWithData:[data safeObjectAtIndex:6]]; + /* 7 */ self.personalNumber = [[NBPhoneNumberDesc alloc] initWithData:[data safeObjectAtIndex:7]]; + /* 8 */ self.voip = [[NBPhoneNumberDesc alloc] initWithData:[data safeObjectAtIndex:8]]; + /* 21 */ self.pager = [[NBPhoneNumberDesc alloc] initWithData:[data safeObjectAtIndex:21]]; + /* 25 */ self.uan = [[NBPhoneNumberDesc alloc] initWithData:[data safeObjectAtIndex:25]]; + /* 27 */ self.emergency = [[NBPhoneNumberDesc alloc] initWithData:[data safeObjectAtIndex:27]]; + /* 28 */ self.voicemail = [[NBPhoneNumberDesc alloc] initWithData:[data safeObjectAtIndex:28]]; + /* 24 */ self.noInternationalDialling = [[NBPhoneNumberDesc alloc] initWithData:[data safeObjectAtIndex:24]]; + /* 9 */ self.codeID = [data safeObjectAtIndex:9]; + /* 10 */ self.countryCode = [data safeObjectAtIndex:10]; + /* 11 */ self.internationalPrefix = [data safeObjectAtIndex:11]; + /* 17 */ self.preferredInternationalPrefix = [data safeObjectAtIndex:17]; + /* 12 */ self.nationalPrefix = [data safeObjectAtIndex:12]; + /* 13 */ self.preferredExtnPrefix = [data safeObjectAtIndex:13]; + /* 15 */ self.nationalPrefixForParsing = [data safeObjectAtIndex:15]; + /* 16 */ self.nationalPrefixTransformRule = [data safeObjectAtIndex:16]; + /* 18 */ self.sameMobileAndFixedLinePattern = [[data safeObjectAtIndex:18] boolValue]; + /* 19 */ self.numberFormats = [self numberFormatArrayFromData:[data safeObjectAtIndex:19]]; // NBNumberFormat array + /* 20 */ self.intlNumberFormats = [self numberFormatArrayFromData:[data safeObjectAtIndex:20]]; // NBNumberFormat array + /* 22 */ self.mainCountryForCode = [[data safeObjectAtIndex:22] boolValue]; + /* 23 */ self.leadingDigits = [data safeObjectAtIndex:23]; + /* 26 */ self.leadingZeroPossible = [[data safeObjectAtIndex:26] boolValue]; + } + else + { + NSLog(@"nil data or wrong data type"); + } +} + + +- (id)initWithCoder:(NSCoder*)coder +{ + if (self = [super init]) + { + self.generalDesc = [coder decodeObjectForKey:@"generalDesc"]; + self.fixedLine = [coder decodeObjectForKey:@"fixedLine"]; + self.mobile = [coder decodeObjectForKey:@"mobile"]; + self.tollFree = [coder decodeObjectForKey:@"tollFree"]; + self.premiumRate = [coder decodeObjectForKey:@"premiumRate"]; + self.sharedCost = [coder decodeObjectForKey:@"sharedCost"]; + self.personalNumber = [coder decodeObjectForKey:@"personalNumber"]; + self.voip = [coder decodeObjectForKey:@"voip"]; + self.pager = [coder decodeObjectForKey:@"pager"]; + self.uan = [coder decodeObjectForKey:@"uan"]; + self.emergency = [coder decodeObjectForKey:@"emergency"]; + self.voicemail = [coder decodeObjectForKey:@"voicemail"]; + self.noInternationalDialling = [coder decodeObjectForKey:@"noInternationalDialling"]; + self.codeID = [coder decodeObjectForKey:@"codeID"]; + self.countryCode = [coder decodeObjectForKey:@"countryCode"]; + self.internationalPrefix = [coder decodeObjectForKey:@"internationalPrefix"]; + self.preferredInternationalPrefix = [coder decodeObjectForKey:@"preferredInternationalPrefix"]; + self.nationalPrefix = [coder decodeObjectForKey:@"nationalPrefix"]; + self.preferredExtnPrefix = [coder decodeObjectForKey:@"preferredExtnPrefix"]; + self.nationalPrefixForParsing = [coder decodeObjectForKey:@"nationalPrefixForParsing"]; + self.nationalPrefixTransformRule = [coder decodeObjectForKey:@"nationalPrefixTransformRule"]; + self.sameMobileAndFixedLinePattern = [[coder decodeObjectForKey:@"sameMobileAndFixedLinePattern"] boolValue]; + self.numberFormats = [coder decodeObjectForKey:@"numberFormats"]; + self.intlNumberFormats = [coder decodeObjectForKey:@"intlNumberFormats"]; + self.mainCountryForCode = [[coder decodeObjectForKey:@"mainCountryForCode"] boolValue]; + self.leadingDigits = [coder decodeObjectForKey:@"leadingDigits"]; + self.leadingZeroPossible = [[coder decodeObjectForKey:@"leadingZeroPossible"] boolValue]; + } + return self; +} + + +- (void)encodeWithCoder:(NSCoder*)coder +{ + [coder encodeObject:self.generalDesc forKey:@"generalDesc"]; + [coder encodeObject:self.fixedLine forKey:@"fixedLine"]; + [coder encodeObject:self.mobile forKey:@"mobile"]; + [coder encodeObject:self.tollFree forKey:@"tollFree"]; + [coder encodeObject:self.premiumRate forKey:@"premiumRate"]; + [coder encodeObject:self.sharedCost forKey:@"sharedCost"]; + [coder encodeObject:self.personalNumber forKey:@"personalNumber"]; + [coder encodeObject:self.voip forKey:@"voip"]; + [coder encodeObject:self.pager forKey:@"pager"]; + [coder encodeObject:self.uan forKey:@"uan"]; + [coder encodeObject:self.emergency forKey:@"emergency"]; + [coder encodeObject:self.voicemail forKey:@"voicemail"]; + [coder encodeObject:self.noInternationalDialling forKey:@"noInternationalDialling"]; + [coder encodeObject:self.codeID forKey:@"codeID"]; + [coder encodeObject:self.countryCode forKey:@"countryCode"]; + [coder encodeObject:self.internationalPrefix forKey:@"internationalPrefix"]; + [coder encodeObject:self.preferredInternationalPrefix forKey:@"preferredInternationalPrefix"]; + [coder encodeObject:self.nationalPrefix forKey:@"nationalPrefix"]; + [coder encodeObject:self.preferredExtnPrefix forKey:@"preferredExtnPrefix"]; + [coder encodeObject:self.nationalPrefixForParsing forKey:@"nationalPrefixForParsing"]; + [coder encodeObject:self.nationalPrefixTransformRule forKey:@"nationalPrefixTransformRule"]; + [coder encodeObject:[NSNumber numberWithBool:self.sameMobileAndFixedLinePattern] forKey:@"sameMobileAndFixedLinePattern"]; + [coder encodeObject:self.numberFormats forKey:@"numberFormats"]; + [coder encodeObject:self.intlNumberFormats forKey:@"intlNumberFormats"]; + [coder encodeObject:[NSNumber numberWithBool:self.mainCountryForCode] forKey:@"mainCountryForCode"]; + [coder encodeObject:self.leadingDigits forKey:@"leadingDigits"]; + [coder encodeObject:[NSNumber numberWithBool:self.leadingZeroPossible] forKey:@"leadingZeroPossible"]; +} + + +- (NSMutableArray*)numberFormatArrayFromData:(id)data +{ + NSMutableArray *resArray = [[NSMutableArray alloc] init]; + if (data != nil && [data isKindOfClass:[NSArray class]]) + { + for (id numFormat in data) + { + NBNumberFormat *newNumberFormat = [[NBNumberFormat alloc] initWithData:numFormat]; + [resArray addObject:newNumberFormat]; + } + } + + return resArray; +} + +/* +- (NSString*)getNormalizedNationalPrefixFormattingRule +{ + NSString *replacedFormattingRule = [self.nationalPrefixFormattingRule stringByReplacingOccurrencesOfString:@"$NP" withString:self.nationalPrefix]; + return replacedFormattingRule; +} + + +- (BOOL)sameMobileAndFixedLinePattern +{ + if ([self.mobile isEqual:self.fixedLine]) return YES; + return NO; +} + + +- (NBPhoneNumberDesc*)inheriteValues:(NBPhoneNumberDesc*)targetDesc +{ + if (targetDesc == nil) + { + targetDesc = [[NBPhoneNumberDesc alloc] init]; + } + + if (self.generalDesc != nil) + { + if (targetDesc.nationalNumberPattern == nil) + { + if (self.generalDesc.nationalNumberPattern != nil) + targetDesc.nationalNumberPattern = [self.generalDesc.nationalNumberPattern copy]; + } + + if (targetDesc.possibleNumberPattern == nil) + { + if (self.generalDesc.possibleNumberPattern != nil) + targetDesc.possibleNumberPattern = [self.generalDesc.possibleNumberPattern copy]; + } + + if (targetDesc.exampleNumber == nil) + { + if (self.generalDesc.exampleNumber != nil) + targetDesc.exampleNumber = [self.generalDesc.exampleNumber copy]; + } + } + + return targetDesc; +} + + +- (void)updateDescriptions +{ + self.fixedLine = [[self inheriteValues:self.fixedLine] copy]; + self.mobile = [[self inheriteValues:self.mobile] copy]; + self.tollFree = [[self inheriteValues:self.tollFree] copy]; + self.premiumRate = [[self inheriteValues:self.premiumRate] copy]; + self.sharedCost = [[self inheriteValues:self.sharedCost] copy]; + self.personalNumber = [[self inheriteValues:self.personalNumber] copy]; + self.voip = [[self inheriteValues:self.voip] copy]; + self.pager = [[self inheriteValues:self.pager] copy]; + self.uan = [[self inheriteValues:self.uan] copy]; + self.emergency = [[self inheriteValues:self.emergency] copy]; + self.voicemail = [[self inheriteValues:self.voicemail] copy]; + self.noInternationalDialling = [[self inheriteValues:self.noInternationalDialling] copy]; +} + + +- (void)setAttributes:(NSDictionary*)data +{ + NSString *attributeName = [data valueForKey:@"attributeName"]; + id attributeContent = [data valueForKey:@"nodeContent"]; + + if ([attributeContent isKindOfClass:[NSString class]] && [attributeContent length] > 0) + attributeContent = [NBPhoneNumberManager stringByTrimming:attributeContent]; + + if (attributeName && [attributeName isKindOfClass:[NSString class]] && [attributeName length] > 0 && [attributeName isEqualToString:@"id"] && + attributeContent && [attributeContent isKindOfClass:[NSString class]] && [attributeContent length] > 0) + { + [self setCodeID:attributeContent]; + } + else if (attributeName && [attributeName isKindOfClass:[NSString class]] && [attributeName length] > 0 && attributeContent && [attributeContent isKindOfClass:[NSString class]] && [attributeContent length] > 0) + { + @try { + if ([[attributeContent lowercaseString] isEqualToString:@"true"]) + { + [self setValue:[NSNumber numberWithBool:YES] forKey:attributeName]; + } + else if ([[attributeContent lowercaseString] isEqualToString:@"false"]) + { + [self setValue:[NSNumber numberWithBool:NO] forKey:attributeName]; + } + else + { + [self setValue:attributeContent forKey:attributeName]; + } + } + @catch (NSException *ex) { + NSLog(@"setAttributes setValue:%@ forKey:%@ error [%@]", attributeContent, attributeName, [attributeContent class]); + } + } +} + + +- (BOOL)setChilds:(id)data +{ + if (data && [data isKindOfClass:[NSDictionary class]]) + { + NSString *nodeName = [data valueForKey:@"nodeName"]; + id nodeContent = [data valueForKey:@"nodeContent"]; + + if ([nodeContent isKindOfClass:[NSString class]] && [nodeContent length] > 0) + nodeContent = [NBPhoneNumberManager stringByTrimming:nodeContent]; + + // [TYPE] PhoneNumberDesc + if ([nodeName isEqualToString:@"generalDesc"] || [nodeName isEqualToString:@"fixedLine"] || [nodeName isEqualToString:@"mobile"] || [nodeName isEqualToString:@"shortCode"] || [nodeName isEqualToString:@"emergency"] || [nodeName isEqualToString:@"voip"] || [nodeName isEqualToString:@"voicemail"] || [nodeName isEqualToString:@"uan"] || [nodeName isEqualToString:@"premiumRate"] || [nodeName isEqualToString:@"nationalNumberPattern"] || [nodeName isEqualToString:@"sharedCost"] || [nodeName isEqualToString:@"tollFree"] || [nodeName isEqualToString:@"noInternationalDialling"] || [nodeName isEqualToString:@"personalNumber"] || [nodeName isEqualToString:@"pager"] || [nodeName isEqualToString:@"areaCodeOptional"]) + { + [self setNumberDescData:data]; + return YES; + } + else if ([nodeName isEqualToString:@"availableFormats"]) + { + [self setNumberFormatsData:data]; + return YES; + } + else if ([nodeName isEqualToString:@"comment"] == NO && [nodeContent isKindOfClass:[NSString class]]) + { + [self setValue:nodeContent forKey:nodeName]; + return YES; + } + else if ([nodeName isEqualToString:@"comment"]) + { + return YES; + } + } + + return NO; +} + + +- (void)setNumberFormatsData:(id)data +{ + NSArray *nodeChildArray = [data valueForKey:@"nodeChildArray"]; + + for (id childNumberFormat in nodeChildArray) + { + NSArray *nodeChildAttributeNumberFormatArray = [childNumberFormat valueForKey:@"nodeAttributeArray"]; + NSArray *nodeChildNodeNumberFormatArray = [childNumberFormat valueForKey:@"nodeChildArray"]; + + NSString *nodeName = [childNumberFormat valueForKey:@"nodeName"]; + + if ([nodeName isEqualToString:@"numberFormat"]) + { + NBNumberFormat *newNumberFormat = [[NBNumberFormat alloc] init]; + + for (id childAttribute in nodeChildAttributeNumberFormatArray) + { + NSString *childNodeName = [childAttribute valueForKey:@"attributeName"]; + NSString *childNodeContent = nil; + + if ([childNodeName isEqualToString:@"comment"]) + { + continue; + } + + childNodeContent = [NBPhoneNumberManager stringByTrimming:[childAttribute valueForKey:@"nodeContent"]]; + + @try { + [newNumberFormat setValue:childNodeContent forKey:childNodeName]; + } + @catch (NSException *ex) { + NSLog(@"nodeChildAttributeArray setValue:%@ forKey:%@ error [%@] %@", childNodeContent, childNodeName, [childNodeContent class], childAttribute); + } + } + + for (id childNode in nodeChildNodeNumberFormatArray) + { + NSString *childNodeName = [childNode valueForKey:@"nodeName"]; + NSString *childNodeContent = nil; + + if ([childNodeName isEqualToString:@"comment"]) + { + continue; + } + + childNodeContent = [NBPhoneNumberManager stringByTrimming:[childNode valueForKey:@"nodeContent"]]; + + @try { + if ([childNodeName isEqualToString:@"leadingDigits"]) + { + [newNumberFormat.leadingDigitsPattern addObject:childNodeContent]; + } + else + { + if ([[childNodeContent lowercaseString] isEqualToString:@"true"]) + { + [newNumberFormat setValue:[NSNumber numberWithBool:YES] forKey:childNodeName]; + } + else if ([[childNodeContent lowercaseString] isEqualToString:@"false"]) + { + [newNumberFormat setValue:[NSNumber numberWithBool:NO] forKey:childNodeName]; + } + else + { + [newNumberFormat setValue:childNodeContent forKey:childNodeName]; + } + } + } + @catch (NSException *ex) { + NSLog(@"nodeChildArray setValue:%@ forKey:%@ error [%@] %@", childNodeContent, childNodeName, [childNodeContent class], childNode); + } + } + [self.numberFormats addObject:newNumberFormat]; + } + else if ([nodeName isEqualToString:@"comment"] == NO) + { + NSLog(@"process ========== %@", childNumberFormat); + } + } +} + + +- (void)setNumberDescData:(id)data +{ + NSString *nodeName = [data valueForKey:@"nodeName"]; + NSArray *nodeChildArray = [data valueForKey:@"nodeChildArray"]; + + NBPhoneNumberDesc *newNumberDesc = [[NBPhoneNumberDesc alloc] init]; + + for (id childNode in nodeChildArray) + { + NSString *childNodeName = [childNode valueForKey:@"nodeName"]; + NSString *childNodeContent = [NBPhoneNumberManager stringByTrimming:[childNode valueForKey:@"nodeContent"]]; + + if ([childNodeName isEqualToString:@"comment"]) + { + continue; + } + + @try { + if (childNodeContent && childNodeContent.length > 0) + { + [newNumberDesc setValue:childNodeContent forKey:childNodeName]; + } + } + @catch (NSException *ex) { + NSLog(@"setNumberDesc setValue:%@ forKey:%@ error [%@]", childNodeContent, childNodeName, [childNodeContent class]); + } + } + + nodeName = [nodeName lowercaseString]; + + if ([nodeName isEqualToString:[@"generalDesc" lowercaseString]]) + self.generalDesc = newNumberDesc; + + if ([nodeName isEqualToString:[@"fixedLine" lowercaseString]]) + self.fixedLine = newNumberDesc; + + if ([nodeName isEqualToString:[@"mobile" lowercaseString]]) + self.mobile = newNumberDesc; + + if ([nodeName isEqualToString:[@"tollFree" lowercaseString]]) { + [self setTollFree:newNumberDesc]; + } + + if ([nodeName isEqualToString:[@"premiumRate" lowercaseString]]) + self.premiumRate = newNumberDesc; + + if ([nodeName isEqualToString:[@"sharedCost" lowercaseString]]) { + self.sharedCost = newNumberDesc; + } + + if ([nodeName isEqualToString:[@"personalNumber" lowercaseString]]) + self.personalNumber = newNumberDesc; + + if ([nodeName isEqualToString:[@"voip" lowercaseString]]) + self.voip = newNumberDesc; + + if ([nodeName isEqualToString:[@"pager" lowercaseString]]) + self.pager = newNumberDesc; + + if ([nodeName isEqualToString:[@"uan" lowercaseString]]) + self.uan = newNumberDesc; + + if ([nodeName isEqualToString:[@"emergency" lowercaseString]]) + self.emergency = newNumberDesc; + + if ([nodeName isEqualToString:[@"voicemail" lowercaseString]]) + self.voicemail = newNumberDesc; + + if ([nodeName isEqualToString:[@"noInternationalDialling" lowercaseString]]) + self.noInternationalDialling = newNumberDesc; + + [self updateDescriptions]; +} +*/ + +@end \ No newline at end of file
diff --git a/libPhoneNumber/NBPhoneNumber.h b/libPhoneNumber/NBPhoneNumber.h new file mode 100644 index 0000000..dfa3b17 --- /dev/null +++ b/libPhoneNumber/NBPhoneNumber.h
@@ -0,0 +1,25 @@ +// +// NBPhoneNumber.h +// libPhoneNumber +// +// Created by NHN Corp. Last Edited by BAND dev team (band_dev@nhn.com) +// + +#import <Foundation/Foundation.h> +#import "NBPhoneNumberDefines.h" + +@interface NBPhoneNumber : NSObject <NSCopying> + +// from phonemetadata.pb.js +/* 1 */ @property (nonatomic, strong, readwrite) NSNumber *countryCode; +/* 2 */ @property (nonatomic, strong, readwrite) NSNumber *nationalNumber; +/* 3 */ @property (nonatomic, strong, readwrite) NSString *extension; +/* 4 */ @property (nonatomic, assign, readwrite) BOOL italianLeadingZero; +/* 5 */ @property (nonatomic, strong, readwrite) NSString *rawInput; +/* 6 */ @property (nonatomic, strong, readwrite) NSNumber *countryCodeSource; +/* 7 */ @property (nonatomic, strong, readwrite) NSString *preferredDomesticCarrierCode; + +- (void)clearCountryCodeSource; +- (NBECountryCodeSource)getCountryCodeSourceOrDefault; + +@end \ No newline at end of file
diff --git a/libPhoneNumber/NBPhoneNumber.m b/libPhoneNumber/NBPhoneNumber.m new file mode 100644 index 0000000..cd79761 --- /dev/null +++ b/libPhoneNumber/NBPhoneNumber.m
@@ -0,0 +1,121 @@ +// +// NBPhoneNumber.m +// libPhoneNumber +// +// Created by NHN Corp. Last Edited by BAND dev team (band_dev@nhn.com) +// + +#import "NBPhoneNumber.h" +#import "NBPhoneNumberDefines.h" + +@implementation NBPhoneNumber + +@synthesize countryCode, nationalNumber, extension, italianLeadingZero, rawInput, countryCodeSource, preferredDomesticCarrierCode; + +- (id)init +{ + self = [super init]; + + if (self) + { + self.countryCodeSource = nil; + self.italianLeadingZero = NO; + self.nationalNumber = @-1; + self.countryCode = @-1; + } + + return self; +} + + +- (void)clearCountryCodeSource +{ + [self setCountryCodeSource:nil]; +} + + +- (NBECountryCodeSource)getCountryCodeSourceOrDefault +{ + if (self.countryCodeSource == nil) + return NBECountryCodeSourceFROM_NUMBER_WITH_PLUS_SIGN; + + return [self.countryCodeSource intValue]; +} + + +- (BOOL)isEqualToObject:(NBPhoneNumber*)otherObj +{ + return [self isEqual:otherObj]; +} + + +- (NSUInteger)hash +{ + NSData *selfObject = [NSKeyedArchiver archivedDataWithRootObject:self]; + return [selfObject hash]; +} + + +- (BOOL)isEqual:(id)object +{ + if ([object isKindOfClass:[NBPhoneNumber class]] == NO) + return NO; + + NBPhoneNumber *other = object; + return ([self.countryCode isEqualToNumber: other.countryCode]) && ([self.nationalNumber isEqualToNumber: other.nationalNumber]) && + (self.italianLeadingZero == other.italianLeadingZero) && + ((self.extension == nil && other.extension == nil) || [self.extension isEqualToString:other.extension]); +} + + +- (id)copyWithZone:(NSZone *)zone +{ + NBPhoneNumber *phoneNumberCopy = [[NBPhoneNumber allocWithZone:zone] init]; + + phoneNumberCopy.countryCode = [self.countryCode copy]; + phoneNumberCopy.nationalNumber = [self.nationalNumber copy]; + phoneNumberCopy.extension = [self.extension copy]; + phoneNumberCopy.italianLeadingZero = self.italianLeadingZero; + phoneNumberCopy.rawInput = [self.rawInput copy]; + phoneNumberCopy.countryCodeSource = [self.countryCodeSource copy]; + phoneNumberCopy.preferredDomesticCarrierCode = [self.preferredDomesticCarrierCode copy]; + + return phoneNumberCopy; +} + + +- (id)initWithCoder:(NSCoder*)coder +{ + if (self = [super init]) + { + self.countryCode = [coder decodeObjectForKey:@"countryCode"]; + self.nationalNumber = [coder decodeObjectForKey:@"nationalNumber"]; + self.extension = [coder decodeObjectForKey:@"extension"]; + self.italianLeadingZero = [[coder decodeObjectForKey:@"italianLeadingZero"] boolValue]; + self.rawInput = [coder decodeObjectForKey:@"rawInput"]; + self.countryCodeSource = [coder decodeObjectForKey:@"countryCodeSource"]; + self.preferredDomesticCarrierCode = [coder decodeObjectForKey:@"preferredDomesticCarrierCode"]; + } + return self; +} + + +- (void)encodeWithCoder:(NSCoder*)coder +{ + [coder encodeObject:self.countryCode forKey:@"countryCode"]; + [coder encodeObject:self.nationalNumber forKey:@"nationalNumber"]; + [coder encodeObject:self.extension forKey:@"extension"]; + [coder encodeObject:[NSNumber numberWithBool:self.italianLeadingZero] forKey:@"italianLeadingZero"]; + [coder encodeObject:self.rawInput forKey:@"rawInput"]; + [coder encodeObject:self.countryCodeSource forKey:@"countryCodeSource"]; + [coder encodeObject:self.preferredDomesticCarrierCode forKey:@"preferredDomesticCarrierCode"]; +} + + + +- (NSString *)description +{ + return [NSString stringWithFormat:@" - countryCode[%@], nationalNumber[%@], extension[%@], italianLeadingZero[%@], rawInput[%@] countryCodeSource[%d] preferredDomesticCarrierCode[%@]", self.countryCode, self.nationalNumber, self.extension, self.italianLeadingZero?@"Y":@"N", self.rawInput, [self.countryCodeSource intValue], self.preferredDomesticCarrierCode]; +} + +@end
diff --git a/libPhoneNumber/NBPhoneNumberDefines.h b/libPhoneNumber/NBPhoneNumberDefines.h new file mode 100644 index 0000000..f7c9bf8 --- /dev/null +++ b/libPhoneNumber/NBPhoneNumberDefines.h
@@ -0,0 +1,107 @@ +// +// NBPhoneNumberDefines.h +// libPhoneNumber +// +// Created by NHN Corp. Last Edited by BAND dev team (band_dev@nhn.com) +// + +#ifndef libPhoneNumber_NBPhoneNumberDefines_h +#define libPhoneNumber_NBPhoneNumberDefines_h + +#define NB_YES [NSNumber numberWithBool:YES] +#define NB_NO [NSNumber numberWithBool:NO] + +#pragma mark - Enum - + +typedef enum { + NBEPhoneNumberFormatE164 = 0, + NBEPhoneNumberFormatINTERNATIONAL = 1, + NBEPhoneNumberFormatNATIONAL = 2, + NBEPhoneNumberFormatRFC3966 = 3 +} NBEPhoneNumberFormat; + + +typedef enum { + NBEPhoneNumberTypeFIXED_LINE = 0, + NBEPhoneNumberTypeMOBILE = 1, + // In some regions (e.g. the USA), it is impossible to distinguish between + // fixed-line and mobile numbers by looking at the phone number itself. + NBEPhoneNumberTypeFIXED_LINE_OR_MOBILE = 2, + // Freephone lines + NBEPhoneNumberTypeTOLL_FREE = 3, + NBEPhoneNumberTypePREMIUM_RATE = 4, + // The cost of this call is shared between the caller and the recipient, and + // is hence typically less than PREMIUM_RATE calls. See + // http://en.wikipedia.org/wiki/Shared_Cost_Service for more information. + NBEPhoneNumberTypeSHARED_COST = 5, + // Voice over IP numbers. This includes TSoIP (Telephony Service over IP). + NBEPhoneNumberTypeVOIP = 6, + // A personal number is associated with a particular person, and may be routed + // to either a MOBILE or FIXED_LINE number. Some more information can be found + // here = http://en.wikipedia.org/wiki/Personal_Numbers + NBEPhoneNumberTypePERSONAL_NUMBER = 7, + NBEPhoneNumberTypePAGER = 8, + // Used for 'Universal Access Numbers' or 'Company Numbers'. They may be + // further routed to specific offices, but allow one number to be used for a + // company. + NBEPhoneNumberTypeUAN = 9, + // Used for 'Voice Mail Access Numbers'. + NBEPhoneNumberTypeVOICEMAIL = 10, + // A phone number is of type UNKNOWN when it does not fit any of the known + // patterns for a specific region. + NBEPhoneNumberTypeUNKNOWN = -1 +} NBEPhoneNumberType; + + +typedef enum { + NBEMatchTypeNOT_A_NUMBER = 0, + NBEMatchTypeNO_MATCH = 1, + NBEMatchTypeSHORT_NSN_MATCH = 2, + NBEMatchTypeNSN_MATCH = 3, + NBEMatchTypeEXACT_MATCH = 4 +} NBEMatchType; + + +typedef enum { + NBEValidationResultIS_POSSIBLE = 0, + NBEValidationResultINVALID_COUNTRY_CODE = 1, + NBEValidationResultTOO_SHORT = 2, + NBEValidationResultTOO_LONG = 3 +} NBEValidationResult; + + +typedef enum { + NBECountryCodeSourceFROM_NUMBER_WITH_PLUS_SIGN = 1, + NBECountryCodeSourceFROM_NUMBER_WITH_IDD = 5, + NBECountryCodeSourceFROM_NUMBER_WITHOUT_PLUS_SIGN = 10, + NBECountryCodeSourceFROM_DEFAULT_COUNTRY = 20 +} NBECountryCodeSource; + + +@interface NSArray (NBAdditions) + +- (id)safeObjectAtIndex:(NSUInteger)index; + +@end + + +@implementation NSArray (NBAdditions) + +- (id)safeObjectAtIndex:(NSUInteger)index +{ + @synchronized(self) + { + if(index >= [self count]) return nil; + + id res = [self objectAtIndex:index]; + + if (res == nil || (NSNull*)res == [NSNull null]) + return nil; + + return res; + } +} + +@end + +#endif
diff --git a/libPhoneNumber/NBPhoneNumberDesc.h b/libPhoneNumber/NBPhoneNumberDesc.h new file mode 100644 index 0000000..288727c --- /dev/null +++ b/libPhoneNumber/NBPhoneNumberDesc.h
@@ -0,0 +1,19 @@ +// +// NBPhoneNumberDesc.h +// libPhoneNumber +// +// Created by NHN Corp. Last Edited by BAND dev team (band_dev@nhn.com) +// + +#import <Foundation/Foundation.h> + +@interface NBPhoneNumberDesc : NSObject + +// from phonemetadata.pb.js +/* 2 */ @property (nonatomic, strong, readwrite) NSString *nationalNumberPattern; +/* 3 */ @property (nonatomic, strong, readwrite) NSString *possibleNumberPattern; +/* 6 */ @property (nonatomic, strong, readwrite) NSString *exampleNumber; + +- (id)initWithData:(id)data; + +@end
diff --git a/libPhoneNumber/NBPhoneNumberDesc.m b/libPhoneNumber/NBPhoneNumberDesc.m new file mode 100644 index 0000000..f698103 --- /dev/null +++ b/libPhoneNumber/NBPhoneNumberDesc.m
@@ -0,0 +1,90 @@ +// +// NBPhoneNumberDesc.m +// libPhoneNumber +// +// Created by NHN Corp. Last Edited by BAND dev team (band_dev@nhn.com) +// + +#import "NBPhoneNumberDesc.h" +#import "NBPhoneNumberDefines.h" + +@implementation NBPhoneNumberDesc + +@synthesize nationalNumberPattern, possibleNumberPattern, exampleNumber; + + +- (id)initWithData:(id)data +{ + self = [self init]; + + if (self && data != nil && [data isKindOfClass:[NSArray class]]) + { + /* 2 */ self.nationalNumberPattern = [data safeObjectAtIndex:2]; + /* 3 */ self.possibleNumberPattern = [data safeObjectAtIndex:3]; + /* 6 */ self.exampleNumber = [data safeObjectAtIndex:6]; + } + + return self; +} + + +- (id)init +{ + self = [super init]; + + if (self) + { + } + + return self; +} + + +- (id)initWithCoder:(NSCoder*)coder +{ + if (self = [super init]) + { + self.nationalNumberPattern = [coder decodeObjectForKey:@"nationalNumberPattern"]; + self.possibleNumberPattern = [coder decodeObjectForKey:@"possibleNumberPattern"]; + self.exampleNumber = [coder decodeObjectForKey:@"exampleNumber"]; + } + return self; +} + + +- (void)encodeWithCoder:(NSCoder*)coder +{ + [coder encodeObject:self.nationalNumberPattern forKey:@"nationalNumberPattern"]; + [coder encodeObject:self.possibleNumberPattern forKey:@"possibleNumberPattern"]; + [coder encodeObject:self.exampleNumber forKey:@"exampleNumber"]; +} + + +- (NSString *)description +{ + return [NSString stringWithFormat:@"nationalNumberPattern[%@] possibleNumberPattern[%@] exampleNumber[%@]", self.nationalNumberPattern, self.possibleNumberPattern, self.exampleNumber]; +} + +- (id)copyWithZone:(NSZone *)zone +{ + NBPhoneNumberDesc *phoneDescCopy = [[NBPhoneNumberDesc allocWithZone:zone] init]; + + phoneDescCopy.nationalNumberPattern = [self.nationalNumberPattern copy]; + phoneDescCopy.possibleNumberPattern = [self.possibleNumberPattern copy]; + phoneDescCopy.exampleNumber = [self.exampleNumber copy]; + + return phoneDescCopy; +} + +- (BOOL)isEqual:(id)object +{ + if ([object isKindOfClass:[NBPhoneNumberDesc class]] == NO) + return NO; + + NBPhoneNumberDesc *other = object; + return [self.nationalNumberPattern isEqual:other.nationalNumberPattern] && + [self.possibleNumberPattern isEqual:other.possibleNumberPattern] && + [self.exampleNumber isEqual:other.exampleNumber]; +} + +@end
diff --git a/libPhoneNumber/NBPhoneNumberMetadata.plist b/libPhoneNumber/NBPhoneNumberMetadata.plist new file mode 100644 index 0000000..8cd2d03 --- /dev/null +++ b/libPhoneNumber/NBPhoneNumberMetadata.plist Binary files differ
diff --git a/libPhoneNumber/NBPhoneNumberUtil.h b/libPhoneNumber/NBPhoneNumberUtil.h new file mode 100644 index 0000000..e9e13ec --- /dev/null +++ b/libPhoneNumber/NBPhoneNumberUtil.h
@@ -0,0 +1,108 @@ +// +// NBPhoneNumberUtil.h +// Band +// +// Created by NHN Corp. Last Edited by BAND dev team (band_dev@nhn.com) +// + +#import <Foundation/Foundation.h> +#import "NBPhoneNumberDefines.h" + +@class NBPhoneMetaData, NBPhoneNumber; + +@interface NBPhoneNumberUtil : NSObject + ++ (NBPhoneNumberUtil*)sharedInstance; ++ (NBPhoneNumberUtil*)sharedInstanceForTest; + +// regular expressions +- (NSArray*)matchesByRegex:(NSString*)sourceString regex:(NSString*)pattern; +- (NSArray*)matchedStringByRegex:(NSString*)sourceString regex:(NSString*)pattern; +- (NSString*)replaceStringByRegex:(NSString*)sourceString regex:(NSString*)pattern withTemplate:(NSString*)templateString; +- (NSInteger)stringPositionByRegex:(NSString*)sourceString regex:(NSString*)pattern; + + ++ (NSString*)stringByTrimming:(NSString*)aString; + +//- (NSString*)numbersOnly:(NSString*)phoneNumber; +- (NSArray*)regionCodeFromCountryCode:(NSNumber *)countryCodeNumber; +- (NSString*)countryCodeFromRegionCode:(NSString*)regionCode; + + +// libPhoneNumber Util functions +- (NSString*)convertAlphaCharactersInNumber:(NSString*)number; + +- (NSString*)normalizePhoneNumber:(NSString*)phoneNumber; +- (NSString*)normalizeDigitsOnly:(NSString*)number; + +- (BOOL)isNumberGeographical:(NBPhoneNumber*)phoneNumber; + +- (NSString*)extractPossibleNumber:(NSString*)phoneNumber; +- (NSNumber*)extractCountryCode:(NSString*)fullNumber nationalNumber:(NSString**)nationalNumber; + +- (NSString*)getNddPrefixForRegion:(NSString*)regionCode stripNonDigits:(BOOL)stripNonDigits; +- (NSString*)getNationalSignificantNumber:(NBPhoneNumber*)phoneNumber; + +- (NBEPhoneNumberType)getNumberType:(NBPhoneNumber*)phoneNumber; + +- (NSNumber*)getCountryCodeForRegion:(NSString*)regionCode; + +- (NSString*)getRegionCodeForCountryCode:(NSNumber*)countryCallingCode; +- (NSArray*)getRegionCodesForCountryCode:(NSNumber*)countryCallingCode; +- (NSString*)getRegionCodeForNumber:(NBPhoneNumber*)phoneNumber; + +- (NBPhoneNumber*)getExampleNumber:(NSString*)regionCode error:(NSError**)error; +- (NBPhoneNumber*)getExampleNumberForType:(NSString*)regionCode type:(NBEPhoneNumberType)type error:(NSError**)error; +- (NBPhoneNumber*)getExampleNumberForNonGeoEntity:(NSNumber*)countryCallingCode error:(NSError**)error; + +- (NBPhoneMetaData*)getMetadataForNonGeographicalRegion:(NSNumber*)countryCallingCode; +- (NBPhoneMetaData*)getMetadataForRegion:(NSString*)regionCode; + +- (BOOL)canBeInternationallyDialled:(NBPhoneNumber*)number error:(NSError**)error; + +- (BOOL)truncateTooLongNumber:(NBPhoneNumber*)number error:(NSError**)error; + +- (BOOL)isValidNumber:(NBPhoneNumber*)number; +- (BOOL)isViablePhoneNumber:(NSString*)phoneNumber; +- (BOOL)isAlphaNumber:(NSString*)number; +- (BOOL)isValidNumberForRegion:(NBPhoneNumber*)number regionCode:(NSString*)regionCode; +- (BOOL)isNANPACountry:(NSString*)regionCode; +- (BOOL)isLeadingZeroPossible:(NSNumber*)countryCallingCode; + +- (NBEValidationResult)isPossibleNumberWithReason:(NBPhoneNumber*)number error:(NSError**)error; + +- (BOOL)isPossibleNumber:(NBPhoneNumber*)number error:(NSError**)error; +- (BOOL)isPossibleNumberString:(NSString*)number regionDialingFrom:(NSString*)regionDialingFrom error:(NSError**)error; + +- (NBEMatchType)isNumberMatch:(id)firstNumberIn second:(id)secondNumberIn error:(NSError**)error; + +- (NSUInteger)getLengthOfGeographicalAreaCode:(NBPhoneNumber*)phoneNumber error:(NSError**)error; +- (NSUInteger)getLengthOfNationalDestinationCode:(NBPhoneNumber*)phoneNumber error:(NSError**)error; + +- (BOOL)maybeStripNationalPrefixAndCarrierCode:(NSString**)numberStr metadata:(NBPhoneMetaData*)metadata carrierCode:(NSString**)carrierCode error:(NSError**)error; +- (NBECountryCodeSource)maybeStripInternationalPrefixAndNormalize:(NSString**)numberStr possibleIddPrefix:(NSString*)possibleIddPrefix error:(NSError**)error; +- (NSNumber*)maybeExtractCountryCode:(NSString*)number metadata:(NBPhoneMetaData*)defaultRegionMetadata + nationalNumber:(NSString**)nationalNumber keepRawInput:(BOOL)keepRawInput phoneNumber:(NBPhoneNumber**)phoneNumber error:(NSError**)error; + +- (NBPhoneNumber*)parse:(NSString*)numberToParse defaultRegion:(NSString*)defaultRegion error:(NSError**)error; +- (NBPhoneNumber*)parseAndKeepRawInput:(NSString*)numberToParse defaultRegion:(NSString*)defaultRegion error:(NSError**)error; +- (NBPhoneNumber*)parseWithPhoneCarrierRegion:(NSString*)numberToParse error:(NSError**)error; + +- (NSString*)format:(NBPhoneNumber*)phoneNumber numberFormat:(NBEPhoneNumberFormat)numberFormat error:(NSError**)error; +- (NSString*)formatByPattern:(NBPhoneNumber*)number numberFormat:(NBEPhoneNumberFormat)numberFormat userDefinedFormats:(NSArray*)userDefinedFormats error:(NSError**)error; +- (NSString*)formatNumberForMobileDialing:(NBPhoneNumber*)number regionCallingFrom:(NSString*)regionCallingFrom withFormatting:(BOOL)withFormatting error:(NSError**)error; +- (NSString*)formatOutOfCountryCallingNumber:(NBPhoneNumber*)number regionCallingFrom:(NSString*)regionCallingFrom error:(NSError**)error; +- (NSString*)formatOutOfCountryKeepingAlphaChars:(NBPhoneNumber*)number regionCallingFrom:(NSString*)regionCallingFrom error:(NSError**)error; +- (NSString*)formatNationalNumberWithCarrierCode:(NBPhoneNumber*)number carrierCode:(NSString*)carrierCode error:(NSError**)error; +- (NSString*)formatInOriginalFormat:(NBPhoneNumber*)number regionCallingFrom:(NSString*)regionCallingFrom error:(NSError**)error; +- (NSString*)formatNationalNumberWithPreferredCarrierCode:(NBPhoneNumber*)number fallbackCarrierCode:(NSString*)fallbackCarrierCode error:(NSError**)error; + +- (BOOL)formattingRuleHasFirstGroupOnly:(NSString*)nationalPrefixFormattingRule; + +@property (nonatomic, readonly) NSString *VALID_PUNCTUATION; +@property (nonatomic, readonly) NSString *VALID_DIGITS_STRING; +@property (nonatomic, readonly) NSString *PLUS_CHARS_; +@property (nonatomic, readonly) NSString *REGION_CODE_FOR_NON_GEO_ENTITY; +@property (nonatomic, readonly) NSDictionary *DIGIT_MAPPINGS; + +@end
diff --git a/libPhoneNumber/NBPhoneNumberUtil.m b/libPhoneNumber/NBPhoneNumberUtil.m new file mode 100644 index 0000000..bfb95c9 --- /dev/null +++ b/libPhoneNumber/NBPhoneNumberUtil.m
@@ -0,0 +1,4305 @@ +// +// NBPhoneNumberUtil.m +// Band +// +// Created by NHN Corp. Last Edited by BAND dev team (band_dev@nhn.com) +// + +#import "NBPhoneNumberUtil.h" +#import "NBPhoneNumber.h" +#import "NBNumberFormat.h" +#import "NBPhoneNumberDesc.h" +#import "NBPhoneMetaData.h" +#import "math.h" + +#import <CoreTelephony/CTTelephonyNetworkInfo.h> +#import <CoreTelephony/CTCarrier.h> + + +#pragma mark - Static Int variables - +const static NSUInteger NANPA_COUNTRY_CODE_ = 1; +const static NSUInteger MIN_LENGTH_FOR_NSN_ = 2; +const static NSUInteger MAX_LENGTH_FOR_NSN_ = 16; +const static NSUInteger MAX_LENGTH_COUNTRY_CODE_ = 3; +const static NSUInteger MAX_INPUT_STRING_LENGTH_ = 250; + + +#pragma mark - Static String variables - +NSString *INVALID_COUNTRY_CODE_STR = @"Invalid country calling code"; +NSString *NOT_A_NUMBER_STR = @"The string supplied did not seem to be a phone number"; +NSString *TOO_SHORT_AFTER_IDD_STR = @"Phone number too short after IDD"; +NSString *TOO_SHORT_NSN_STR = @"The string supplied is too short to be a phone number"; +NSString *TOO_LONG_STR = @"The string supplied is too long to be a phone number"; + +NSString *UNKNOWN_REGION_ = @"ZZ"; +NSString *COLOMBIA_MOBILE_TO_FIXED_LINE_PREFIX_ = @"3"; +NSString *PLUS_SIGN = @"+"; +NSString *STAR_SIGN_ = @"*"; +NSString *RFC3966_EXTN_PREFIX_ = @";ext="; +NSString *RFC3966_PREFIX_ = @"tel:"; +NSString *RFC3966_PHONE_CONTEXT_ = @";phone-context="; +NSString *RFC3966_ISDN_SUBADDRESS_ = @";isub="; +NSString *DEFAULT_EXTN_PREFIX_ = @" ext. "; +NSString *VALID_ALPHA_ = @"A-Za-z"; + +#pragma mark - Static regular expression strings - +NSString *NON_DIGITS_PATTERN_ = @"\\D+"; +NSString *CC_PATTERN_ = @"\\$CC"; +NSString *FIRST_GROUP_PATTERN_ = @"(\\$\\d)"; +NSString *FIRST_GROUP_ONLY_PREFIX_PATTERN_ = @"^\\(?\\$1\\)?"; +NSString *NP_PATTERN_ = @"\\$NP"; +NSString *FG_PATTERN_ = @"\\$FG"; +NSString *VALID_ALPHA_PHONE_PATTERN_STRING = @"(?:.*?[A-Za-z]){3}.*"; +NSString *UNIQUE_INTERNATIONAL_PREFIX_ = @"[\\d]+(?:[~\\u2053\\u223C\\uFF5E][\\d]+)?"; + + +#pragma mark - NBPhoneNumberUtil interface - +@interface NBPhoneNumberUtil () + +/* + Terminologies + - Country Number (CN) = Country code for i18n calling + - Country Code (CC) : ISO country codes (2 chars) + Ref. site (countrycode.org) + */ +@property (nonatomic, readonly) NSDictionary *coreMetaData; +@property (nonatomic, readonly) NSRegularExpression *PLUS_CHARS_PATTERN, *CAPTURING_DIGIT_PATTERN, *VALID_ALPHA_PHONE_PATTERN_; +@property (nonatomic, readonly) NSString *LEADING_PLUS_CHARS_PATTERN_, *EXTN_PATTERN_, *SEPARATOR_PATTERN_, *VALID_PHONE_NUMBER_PATTERN_, *VALID_START_CHAR_PATTERN_, *UNWANTED_END_CHAR_PATTERN_, *SECOND_NUMBER_START_PATTERN_; + +@property (nonatomic, readonly) NSDictionary *ALPHA_MAPPINGS_, *ALL_NORMALIZATION_MAPPINGS_, *DIALLABLE_CHAR_MAPPINGS_, *ALL_PLUS_NUMBER_GROUPING_SYMBOLS_; + +@property (nonatomic, strong, readwrite) NSMutableDictionary *mapCCode2CN; +@property (nonatomic, strong, readwrite) NSMutableDictionary *mapCN2CCode; + +@property (nonatomic, strong, readwrite) NSMutableDictionary *i18nNumberFormat; +@property (nonatomic, strong, readwrite) NSMutableDictionary *i18nPhoneNumberDesc; +@property (nonatomic, strong, readwrite) NSMutableDictionary *i18nPhoneMetadata; + +@end + + +@implementation NBPhoneNumberUtil + ++ (NBPhoneNumberUtil*)sharedInstance +{ + static NBPhoneNumberUtil *sharedOnceInstance = nil; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ sharedOnceInstance = [[self alloc] init]; }); + return sharedOnceInstance; +} + + ++ (NBPhoneNumberUtil*)sharedInstanceForTest +{ + static NBPhoneNumberUtil *sharedOnceInstanceForTest = nil; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ sharedOnceInstanceForTest = [[self alloc] initForTest]; }); + return sharedOnceInstanceForTest; +} + + ++ (NSString*)stringByTrimming:(NSString*)aString +{ + NSString *aRes = @""; + NSArray *newlines = [aString componentsSeparatedByCharactersInSet:[NSCharacterSet newlineCharacterSet]]; + for (NSString *line in newlines) + { + NSString *performedString = [line stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]]; + + if (performedString.length > 0) + aRes = [aRes stringByAppendingString:performedString]; + } + + if (newlines.count <= 0) + return aString; + + return aRes; +} + + +#pragma mark - Regular expression Utilities - +- (BOOL)hasValue:(NSString*)string +{ + if (string == nil || [string stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]].length <= 0) + { + return NO; + } + + return YES; +} + + +- (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; +} + + +- (NSInteger)stringPositionByRegex:(NSString*)sourceString regex:(NSString*)pattern +{ + if (sourceString == nil || sourceString.length <= 0 || pattern == nil || pattern.length <= 0) { + return -1; + } + + NSError *error = nil; + NSRegularExpression *currentPattern = [NSRegularExpression regularExpressionWithPattern:pattern options:0 error:&error]; + NSArray *matches = [currentPattern matchesInString:sourceString options:0 range:NSMakeRange(0, sourceString.length)]; + + NSInteger foundPosition = -1; + + if (matches.count > 0) + { + NSTextCheckingResult *match = [matches objectAtIndex:0]; + return match.range.location; + } + + return foundPosition; +} + + +- (NSInteger)indexOfStringByString:(NSString*)sourceString target:(NSString*)targetString +{ + NSRange finded = [sourceString rangeOfString:targetString]; + if (finded.location != NSNotFound) + { + return finded.location; + } + + return -1; +} + + +- (NSString*)replaceFirstStringByRegex:(NSString*)sourceString regex:(NSString*)pattern withTemplate:(NSString*)templateString +{ + NSString *replacementResult = [sourceString copy]; + NSError *error = nil; + + NSRegularExpression *currentPattern = [NSRegularExpression 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 = [NSRegularExpression 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 = [NSRegularExpression 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 = [NSRegularExpression 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 = [NSRegularExpression 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(NSUInteger 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)isNaN:(NSString*)sourceString +{ + NSCharacterSet *alphaNums = [NSCharacterSet decimalDigitCharacterSet]; + NSCharacterSet *inStringSet = [NSCharacterSet characterSetWithCharactersInString:sourceString]; + BOOL hasNumberOnly = [alphaNums isSupersetOfSet:inStringSet]; + + return !hasNumberOnly; +} + +/* + - (NSString*)numbersOnly:(NSString*)phoneNumber + { + NSMutableString *strippedString = [NSMutableString stringWithCapacity:phoneNumber.length]; + + NSScanner *scanner = [NSScanner scannerWithString:phoneNumber]; + NSCharacterSet *numbers = [NSCharacterSet characterSetWithCharactersInString:@"0123456789"]; + + while ([scanner isAtEnd] == NO) { + NSString *buffer; + if ([scanner scanCharactersFromSet:numbers intoString:&buffer]) { + [strippedString appendString:buffer]; + + } else { + [scanner setScanLocation:([scanner scanLocation] + 1)]; + } + } + + return strippedString; + } + */ + + +- (NSString*)getNationalSignificantNumber:(NBPhoneNumber*)phoneNumber +{ + if (phoneNumber.italianLeadingZero) + { + return [NSString stringWithFormat:@"0%@", phoneNumber.nationalNumber]; + } + + return [NSString stringWithFormat:@"%@", phoneNumber.nationalNumber]; +} + + +- (NSArray*)regionCodeFromCountryCode:(NSNumber *)countryCodeNumber +{ + if (self.mapCN2CCode == nil || [self.mapCN2CCode count] <= 0) + { + return nil; + } + + id res = [self.mapCN2CCode objectForKey:[NSString stringWithFormat:@"%@", countryCodeNumber]]; + + if (res && [res isKindOfClass:[NSArray class]] && [((NSArray*)res) count] > 0) + { + return res; + } + + return nil; +} + + +- (NSString*)countryCodeFromRegionCode:(NSString*)regionCode +{ + if (self.mapCCode2CN == nil || [self.mapCCode2CN count] <= 0) + { + return nil; + } + + id res = [self.mapCCode2CN objectForKey:regionCode]; + + if (res) + { + return res; + } + + return nil; +} + + +#pragma mark - Initializations - +- (id)init +{ + self = [super init]; + if (self) + { + [self initRegularExpressionSet]; + [self initNormalizationMappings]; + + NSDictionary *resData = [self loadMetadata:@"NBPhoneNumberMetadata"]; + _coreMetaData = [resData objectForKey:@"countryToMetadata"]; + _mapCN2CCode = [resData objectForKey:@"countryCodeToRegionCodeMap"]; + + [self initCC2CN]; + } + + return self; +} + + +- (id)initForTest +{ + self = [super init]; + if (self) + { + [self initRegularExpressionSet]; + [self initNormalizationMappings]; + + NSDictionary *resData = [self loadMetadata:@"NBPhoneNumberMetadataForTesting"]; + _coreMetaData = [resData objectForKey:@"countryToMetadata"]; + _mapCN2CCode = [resData objectForKey:@"countryCodeToRegionCodeMap"]; + + [self initCC2CN]; + } + + return self; +} + + +- (NSDictionary*)loadMetadata:(NSString*)name +{ + NSDictionary *unarchiveData = nil; + + @try { + NSString *filePath = [[NSBundle mainBundle] pathForResource:name ofType:@"plist"]; + NSData *fileData = [NSData dataWithContentsOfFile:filePath]; + unarchiveData = [NSKeyedUnarchiver unarchiveObjectWithData:fileData]; + } + @catch (NSException *exception) { + return unarchiveData; + } + + return unarchiveData; +} + +/* + - (NSDictionary*)generateMetadata:(id)metaClass + { + NSMutableDictionary *resMedata = [[NSMutableDictionary alloc] init]; + NSDictionary *srcMedata = nil; + + if ([metaClass isKindOfClass:[NBPhoneNumberMetadataForTesting class]]) + { + srcMedata = ((NBPhoneNumberMetadataForTesting*)metaClass).metadata; + } + else if ([metaClass isKindOfClass:[NBPhoneNumberMetadata class]]) + { + srcMedata = ((NBPhoneNumberMetadata*)metaClass).metadata; + } + else + { + return resMedata; + } + + NSDictionary *countryCodeToRegionCodeMap = [srcMedata objectForKey:@"countryCodeToRegionCodeMap"]; + NSDictionary *countryToMetadata = [srcMedata objectForKey:@"countryToMetadata"]; + NSLog(@"- countryCodeToRegionCodeMap count [%d]", [countryCodeToRegionCodeMap count]); + NSLog(@"- countryToMetadata count [%d]", [countryToMetadata count]); + + NSMutableDictionary *genetatedMetaData = [[NSMutableDictionary alloc] init]; + + for (id key in [countryToMetadata allKeys]) + { + id metaData = [countryToMetadata objectForKey:key]; + + NBPhoneMetaData *newMetaData = [[NBPhoneMetaData alloc] init]; + [newMetaData buildData:metaData]; + + [genetatedMetaData setObject:newMetaData forKey:key]; + } + + [resMedata setObject:countryCodeToRegionCodeMap forKey:@"countryCodeToRegionCodeMap"]; + [resMedata setObject:genetatedMetaData forKey:@"countryToMetadata"]; + + return resMedata; + } + */ + + +- (void)initRegularExpressionSet +{ + _VALID_PUNCTUATION = @"-x‐-―−ー--/ ()()[].\\[\\]/~⁓∼~"; + _VALID_DIGITS_STRING = @"0-90-9٠-٩۰-۹"; + _PLUS_CHARS_ = @"++"; + _REGION_CODE_FOR_NON_GEO_ENTITY = @"001"; + + NSString *EXTN_PATTERNS_FOR_PARSING_ = @"(?:;ext=([0-90-9٠-٩۰-۹]{1,7})|[ \\t,]*(?:e?xt(?:ensi(?:ó?|ó))?n?|e?xtn?|[,xxX##~~]|int|anexo|int)[:\\..]?[ \\t,-]*([0-90-9٠-٩۰-۹]{1,7})#?|[- ]+([0-90-9٠-٩۰-۹]{1,5})#)$"; + + NSError *error = nil; + + _PLUS_CHARS_PATTERN = + [NSRegularExpression regularExpressionWithPattern:[NSString stringWithFormat:@"[%@]+", _PLUS_CHARS_] options:0 error:&error]; + + _LEADING_PLUS_CHARS_PATTERN_ = [NSString stringWithFormat:@"^[%@]+", _PLUS_CHARS_]; + + _CAPTURING_DIGIT_PATTERN = + [NSRegularExpression regularExpressionWithPattern:[NSString stringWithFormat:@"([%@])", _VALID_DIGITS_STRING] options:0 error:&error]; + + _VALID_START_CHAR_PATTERN_ = [NSString stringWithFormat:@"[%@%@]", _PLUS_CHARS_, _VALID_DIGITS_STRING]; + + _SECOND_NUMBER_START_PATTERN_ = @"[\\\\\\/] *x"; + + _VALID_ALPHA_PHONE_PATTERN_ = [NSRegularExpression regularExpressionWithPattern:VALID_ALPHA_PHONE_PATTERN_STRING options:0 error:&error]; + + _UNWANTED_END_CHAR_PATTERN_ = [NSString stringWithFormat:@"[^%@%@#]+$", _VALID_DIGITS_STRING, VALID_ALPHA_]; + + _EXTN_PATTERN_ = [NSString stringWithFormat:@"(?:%@)$", EXTN_PATTERNS_FOR_PARSING_]; + + + _SEPARATOR_PATTERN_ = [NSString stringWithFormat:@"[%@]+", _VALID_PUNCTUATION]; + + _VALID_PHONE_NUMBER_PATTERN_ = @"^[0-90-9٠-٩۰-۹]{2}$|^[++]*(?:[-x‐-―−ー--/ ()()[].\\[\\]/~⁓∼~*]*[0-90-9٠-٩۰-۹]){3,}[-x‐-―−ー--/ ()()[].\\[\\]/~⁓∼~*A-Za-z0-90-9٠-٩۰-۹]*(?:;ext=([0-90-9٠-٩۰-۹]{1,7})|[ \\t,]*(?:e?xt(?:ensi(?:ó?|ó))?n?|e?xtn?|[,xx##~~]|int|anexo|int)[:\\..]?[ \\t,-]*([0-90-9٠-٩۰-۹]{1,7})#?|[- ]+([0-90-9٠-٩۰-۹]{1,5})#)?$"; +} + + +- (void)dealloc +{ + [self clearCC2CN]; + [self clearCN2CC]; +} + + +- (void)clearCC2CN +{ + if (self.mapCCode2CN != nil) + { + [self.mapCCode2CN removeAllObjects]; + self.mapCCode2CN = nil; + } +} + + +- (void)clearCN2CC +{ + if (self.mapCN2CCode != nil) + { + [self.mapCN2CCode removeAllObjects]; + self.mapCN2CCode = nil; + } +} + + +- (void)initNormalizationMappings +{ + _DIGIT_MAPPINGS = [NSMutableDictionary 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]; + + _DIALLABLE_CHAR_MAPPINGS_ = [NSMutableDictionary dictionaryWithObjectsAndKeys: + @"0", @"0", @"1", @"1", @"2", @"2", @"3", @"3", @"4", @"4", @"5", @"5", @"6", @"6", @"7", @"7", @"8", @"8", @"9", @"9", + @"+", @"+", @"*", @"*", nil]; + + _ALPHA_MAPPINGS_ = [NSMutableDictionary 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]; + + _ALL_NORMALIZATION_MAPPINGS_ = [NSMutableDictionary 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]; + + _ALL_PLUS_NUMBER_GROUPING_SYMBOLS_ = [NSMutableDictionary 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]; + +} + + +- (void)initCC2CN +{ + [self clearCC2CN]; + _mapCCode2CN = [NSMutableDictionary dictionaryWithObjectsAndKeys: + @"1", @"US", @"1", @"AG", @"1", @"AI", @"1", @"AS", @"1", @"BB", @"1", @"BM", @"1", @"BS", @"1", @"CA", @"1", @"DM", @"1", @"DO", + @"1", @"GD", @"1", @"GU", @"1", @"JM", @"1", @"KN", @"1", @"KY", @"1", @"LC", @"1", @"MP", @"1", @"MS", @"1", @"PR", @"1", @"SX", + @"1", @"TC", @"1", @"TT", @"1", @"VC", @"1", @"VG", @"1", @"VI", @"7", @"RU", @"7", @"KZ", + @"20", @"EG", @"27", @"ZA", + @"30", @"GR", @"31", @"NL", @"32", @"BE", @"33", @"FR", @"34", @"ES", @"36", @"HU", @"39", @"IT", + @"40", @"RO", @"41", @"CH", @"43", @"AT", @"44", @"GB", @"44", @"GG", @"44", @"IM", @"44", @"JE", @"45", @"DK", @"46", @"SE", @"47", @"NO", @"47", @"SJ", @"48", @"PL", @"49", @"DE", + @"51", @"PE", @"52", @"MX", @"53", @"CU", @"54", @"AR", @"55", @"BR", @"56", @"CL", @"57", @"CO", @"58", @"VE", + @"60", @"MY", @"61", @"AU", @"61", @"CC", @"61", @"CX", @"62", @"ID", @"63", @"PH", @"64", @"NZ", @"65", @"SG", @"66", @"TH", + @"81", @"JP", @"82", @"KR", @"84", @"VN", @"86", @"CN", + @"90", @"TR", @"91", @"IN", @"92", @"PK", @"93", @"AF", @"94", @"LK", @"95", @"MM", @"98", @"IR", + @"211", @"SS", @"212", @"MA", @"212", @"EH", @"213", @"DZ", @"216", @"TN", @"218", @"LY", + @"220", @"GM", @"221", @"SN", @"222", @"MR", @"223", @"ML", @"224", @"GN", @"225", @"CI", @"226", @"BF", @"227", @"NE", @"228", @"TG", @"229", @"BJ", + @"230", @"MU", @"231", @"LR", @"232", @"SL", @"233", @"GH", @"234", @"NG", @"235", @"TD", @"236", @"CF", @"237", @"CM", @"238", @"CV", @"239", @"ST", + @"240", @"GQ", @"241", @"GA", @"242", @"CG", @"243", @"CD", @"244", @"AO", @"245", @"GW", @"246", @"IO", @"247", @"AC", @"248", @"SC", @"249", @"SD", + @"250", @"RW", @"251", @"ET", @"252", @"SO", @"253", @"DJ", @"254", @"KE", @"255", @"TZ", @"256", @"UG", @"257", @"BI", @"258", @"MZ", + @"260", @"ZM", @"261", @"MG", @"262", @"RE", @"262", @"YT", @"263", @"ZW", @"264", @"NA", @"265", @"MW", @"266", @"LS", @"267", @"BW", @"268", @"SZ", @"269", @"KM", + @"290", @"SH", @"291", @"ER", @"297", @"AW", @"298", @"FO", @"299", @"GL", + @"350", @"GI", @"351", @"PT", @"352", @"LU", @"353", @"IE", @"354", @"IS", @"355", @"AL", @"356", @"MT", @"357", @"CY", @"358", @"FI",@"358", @"AX", @"359", @"BG", + @"370", @"LT", @"371", @"LV", @"372", @"EE", @"373", @"MD", @"374", @"AM", @"375", @"BY", @"376", @"AD", @"377", @"MC", @"378", @"SM", @"379", @"VA", + @"380", @"UA", @"381", @"RS", @"382", @"ME", @"385", @"HR", @"386", @"SI", @"387", @"BA", @"389", @"MK", + @"420", @"CZ", @"421", @"SK", @"423", @"LI", + @"500", @"FK", @"501", @"BZ", @"502", @"GT", @"503", @"SV", @"504", @"HN", @"505", @"NI", @"506", @"CR", @"507", @"PA", @"508", @"PM", @"509", @"HT", + @"590", @"GP", @"590", @"BL", @"590", @"MF", @"591", @"BO", @"592", @"GY", @"593", @"EC", @"594", @"GF", @"595", @"PY", @"596", @"MQ", @"597", @"SR", @"598", @"UY", @"599", @"CW", @"599", @"BQ", + @"670", @"TL", @"672", @"NF", @"673", @"BN", @"674", @"NR", @"675", @"PG", @"676", @"TO", @"677", @"SB", @"678", @"VU", @"679", @"FJ", + @"680", @"PW", @"681", @"WF", @"682", @"CK", @"683", @"NU", @"685", @"WS", @"686", @"KI", @"687", @"NC", @"688", @"TV", @"689", @"PF", + @"690", @"TK", @"691", @"FM", @"692", @"MH", + @"800", @"001", @"808", @"001", + @"850", @"KP", @"852", @"HK", @"853", @"MO", @"855", @"KH", @"856", @"LA", + @"870", @"001", @"878", @"001", + @"880", @"BD", @"881", @"001", @"882", @"001", @"883", @"001", @"886", @"TW", @"888", @"001", + @"960", @"MV", @"961", @"LB", @"962", @"JO", @"963", @"SY", @"964", @"IQ", @"965", @"KW", @"966", @"SA", @"967", @"YE", @"968", @"OM", + @"970", @"PS", @"971", @"AE", @"972", @"IL", @"973", @"BH", @"974", @"QA", @"975", @"BT", @"976", @"MN", @"977", @"NP", @"979", @"001", + @"992", @"TJ", @"993", @"TM", @"994", @"AZ", @"995", @"GE", @"996", @"KG", @"998", @"UZ", + nil]; +} + +/* + - (void)initCN2CC + { + [self clearCN2CC]; + _mapCN2CCode = [NSMutableDictionary dictionaryWithObjectsAndKeys: + @[@"US", @"AG", @"AI", @"AS", @"BB", @"BM", @"BS", @"CA", @"DM", @"DO", @"GD", @"GU", @"JM", @"KN", @"KY", @"LC", @"MP", @"MS", @"PR", @"SX", @"TC", @"TT", @"VC", @"VG", @"VI"], @"1", @[@"RU", @"KZ"], @"7", + @[@"EG"], @"20", @[@"ZA"], @"27", + @[@"GR"], @"30", @[@"NL"], @"31", @[@"BE"], @"32", @[@"FR"], @"33", @[@"ES"], @"34", @[@"HU"], @"36", @[@"IT"], @"39", + @[@"RO"], @"40", @[@"CH"], @"41", @[@"AT"], @"43", @[@"GB", @"GG", @"IM", @"JE"], @"44", @[@"DK"], @"45", @[@"SE"], @"46", @[@"NO", @"SJ"], @"47", @[@"PL"], @"48", @[@"DE"], @"49", + @[@"PE"], @"51", @[@"MX"], @"52", @[@"CU"], @"53", @[@"AR"], @"54", @[@"BR"], @"55", @[@"CL"], @"56", @[@"CO"], @"57", @[@"VE"], @"58", + @[@"MY"], @"60", @[@"AU", @"CC", @"CX"], @"61", @[@"ID"], @"62", @[@"PH"], @"63", @[@"NZ"], @"64", @[@"SG"], @"65", @[@"TH"], @"66", + @[@"JP"], @"81", @[@"KR"], @"82", @[@"VN"], @"84", @[@"CN"], @"86", + @[@"TR"], @"90", @[@"IN"], @"91", @[@"PK"], @"92", @[@"AF"], @"93", @[@"LK"], @"94", @[@"MM"], @"95", @[@"IR"], @"98", + @[@"SS"], @"211", @[@"MA", @"EH"], @"212", @[@"DZ"], @"213", @[@"TN"], @"216", @[@"LY"], @"218", + @[@"GM"], @"220", @[@"SN"], @"221", @[@"MR"], @"222", @[@"ML"], @"223", @[@"GN"], @"224", @[@"CI"], @"225", @[@"BF"], @"226", @[@"NE"], @"227", @[@"TG"], @"228", @[@"BJ"], @"229", + @[@"MU"], @"230", @[@"LR"], @"231", @[@"SL"], @"232", @[@"GH"], @"233", @[@"NG"], @"234", @[@"TD"], @"235", @[@"CF"], @"236", @[@"CM"], @"237", @[@"CV"], @"238", @[@"ST"], @"239", + @[@"GQ"], @"240", @[@"GA"], @"241", @[@"CG"], @"242", @[@"CD"], @"243", @[@"AO"], @"244", @[@"GW"], @"245", @[@"IO"], @"246", @[@"AC"], @"247", @[@"SC"], @"248", @[@"SD"], @"249", + @[@"RW"], @"250", @[@"ET"], @"251", @[@"SO"], @"252", @[@"DJ"], @"253", @[@"KE"], @"254", @[@"TZ"], @"255", @[@"UG"], @"256", @[@"BI"], @"257", @[@"MZ"], @"258", + @[@"ZM"], @"260", @[@"MG"], @"261", @[@"RE", @"YT"], @"262", @[@"ZW"], @"263", @[@"NA"], @"264", @[@"MW"], @"265", @[@"LS"], @"266", @[@"BW"], @"267", @[@"SZ"], @"268", @[@"KM"], @"269", + @[@"SH"], @"290", @[@"ER"], @"291", @[@"AW"], @"297", @[@"FO"], @"298", @[@"GL"], @"299", + @[@"GI"], @"350", @[@"PT"], @"351", @[@"LU"], @"352", @[@"IE"], @"353", @[@"IS"], @"354", @[@"AL"], @"355", @[@"MT"], @"356", @[@"CY"], @"357", @[@"FI", @"AX"], @"358", @[@"BG"], @"359", + @[@"LT"], @"370", @[@"LV"], @"371", @[@"EE"], @"372", @[@"MD"], @"373", @[@"AM"], @"374", @[@"BY"], @"375", @[@"AD"], @"376", @[@"MC"], @"377", @[@"SM"], @"378", @[@"VA"], @"379", + @[@"UA"], @"380", @[@"RS"], @"381", @[@"ME"], @"382", @[@"HR"], @"385", @[@"SI"], @"386", @[@"BA"], @"387", @[@"MK"], @"389", + @[@"CZ"], @"420", @[@"SK"], @"421", @[@"LI"], @"423", + @[@"FK"], @"500", @[@"BZ"], @"501", @[@"GT"], @"502", @[@"SV"], @"503", @[@"HN"], @"504", @[@"NI"], @"505", @[@"CR"], @"506", @[@"PA"], @"507", @[@"PM"], @"508", @[@"HT"], @"509", + @[@"GP", @"BL", @"MF"], @"590", @[@"BO"], @"591", @[@"GY"], @"592", @[@"EC"], @"593", @[@"GF"], @"594", @[@"PY"], @"595", @[@"MQ"], @"596", @[@"SR"], @"597", @[@"UY"], @"598", @[@"CW", @"BQ"], @"599", + @[@"TL"], @"670", @[@"NF"], @"672", @[@"BN"], @"673", @[@"NR"], @"674", @[@"PG"], @"675", @[@"TO"], @"676", @[@"SB"], @"677", @[@"VU"], @"678", @[@"FJ"], @"679", + @[@"PW"], @"680", @[@"WF"], @"681", @[@"CK"], @"682", @[@"NU"], @"683", @[@"WS"], @"685", @[@"KI"], @"686", @[@"NC"], @"687", @[@"TV"], @"688", @[@"PF"], @"689", + @[@"TK"], @"690", @[@"FM"], @"691", @[@"MH"], @"692", + @[@"001"], @"800", @[@"001"], @"808", + @[@"KP"], @"850", @[@"HK"], @"852", @[@"MO"], @"853", @[@"KH"], @"855", @[@"LA"], @"856", + @[@"001"], @"870", @[@"001"], @"878", + @[@"BD"], @"880", @[@"001"], @"881", @[@"001"], @"882", @[@"001"], @"883", @[@"TW"], @"886", @[@"001"], @"888", + @[@"MV"], @"960", @[@"LB"], @"961", @[@"JO"], @"962", @[@"SY"], @"963", @[@"IQ"], @"964", @[@"KW"], @"965", @[@"SA"], @"966", @[@"YE"], @"967", @[@"OM"], @"968", + @[@"PS"], @"970", @[@"AE"], @"971", @[@"IL"], @"972", @[@"BH"], @"973", @[@"QA"], @"974", @[@"BT"], @"975", @[@"MN"], @"976", @[@"NP"], @"977", @[@"001"], @"979", + @[@"TJ"], @"992", @[@"TM"], @"993", @[@"AZ"], @"994", @[@"GE"], @"995", @[@"KG"], @"996", @[@"UZ"], @"998", 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 +{ + NSString *possibleNumber = @""; + NSInteger start = [self stringPositionByRegex:number regex:self.VALID_START_CHAR_PATTERN_]; + + if (start >= 0) + { + possibleNumber = [number substringFromIndex:start]; + // Remove trailing non-alpha non-numerical characters. + possibleNumber = [self replaceStringByRegex:possibleNumber regex:self.UNWANTED_END_CHAR_PATTERN_ withTemplate:@""]; + + // Check for extra numbers at the end. + NSInteger secondNumberStart = [self stringPositionByRegex:number regex:self.SECOND_NUMBER_START_PATTERN_]; + if (secondNumberStart >= 0) + { + possibleNumber = [possibleNumber substringWithRange:NSMakeRange(0, secondNumberStart - 1)]; + } + } + else + { + possibleNumber = @""; + } + + return possibleNumber; + + /* + NSString *possibleNumber = @""; + NSRegularExpression *currentPattern = self.VALID_START_CHAR_PATTERN_; + int sourceLength = phoneNumber.length; + + NSArray *matches = [currentPattern matchesInString:phoneNumber options:0 range:NSMakeRange(0, sourceLength)]; + if (matches && [matches count] > 0) + { + NSRange rangeOfFirstMatch = ((NSTextCheckingResult*)[matches objectAtIndex:0]).range; + possibleNumber = [phoneNumber substringFromIndex:rangeOfFirstMatch.location]; + + // Remove trailing non-alpha non-numerical characters. + currentPattern = self.UNWANTED_END_CHAR_PATTERN_; + possibleNumber = [currentPattern stringByReplacingMatchesInString:possibleNumber options:0 + range:NSMakeRange(0, [possibleNumber length]) withTemplate:@""]; + // Check for extra numbers at the end. + currentPattern = self.SECOND_NUMBER_START_PATTERN_; + matches = [currentPattern matchesInString:possibleNumber options:0 + range:NSMakeRange(0, [possibleNumber length])]; + if (matches && [matches count] > 0) + { + NSRange rangeOfSecondMatch = ((NSTextCheckingResult*)[matches objectAtIndex:0]).range; + possibleNumber = [possibleNumber substringWithRange:NSMakeRange(0, rangeOfSecondMatch.location)]; + } + } + + 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 +{ + if (phoneNumber.length < MIN_LENGTH_FOR_NSN_) + { + return NO; + } + + return [self matchesEntirely:self.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 +{ + if ([self matchesEntirely:VALID_ALPHA_PHONE_PATTERN_STRING string:number]) + { + return [self normalizeHelper:number normalizationReplacements:self.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 +{ + 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 +{ + return [self stringByReplacingOccurrencesString:number + withMap:self.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. + */ +- (NSUInteger)getLengthOfGeographicalAreaCode:(NBPhoneNumber*)phoneNumber error:(NSError **)error +{ + NSUInteger 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; +} + + +- (NSUInteger)getLengthOfGeographicalAreaCode:(NBPhoneNumber*)phoneNumber +{ + NSString *regionCode = [self getRegionCodeForNumber:phoneNumber]; + NBPhoneMetaData *metadata = [self 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. + */ +- (NSUInteger)getLengthOfNationalDestinationCode:(NBPhoneNumber*)phoneNumber error:(NSError **)error +{ + NSUInteger 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; +} + + +- (NSUInteger)getLengthOfNationalDestinationCode:(NBPhoneNumber*)phoneNumber +{ + NBPhoneNumber *copiedProto = nil; + if ([self 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 = [self 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 ((NSString*)[numberGroups objectAtIndex:2]).length + 1; + } + + return ((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 = @""; + NSUInteger numberLength = sourceString.length; + + for (NSUInteger 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. + return numberType == NBEPhoneNumberTypeFIXED_LINE || numberType == NBEPhoneNumberTypeFIXED_LINE_OR_MOBILE; +} + + +/** + * 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. + return [self hasValue:regionCode] && [self isNaN:regionCode] && [self 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 +{ + id res = [self 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 +{ + if ([phoneNumber.nationalNumber isEqualToNumber:@0] && [self 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 ([self 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 = [self 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. + NSArray *regionCodes = [self 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 +{ + return [_REGION_CODE_FOR_NON_GEO_ENTITY isEqualToString:regionCode] ? + [self getMetadataForNonGeographicalRegion:countryCallingCode] : [self 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 [self 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 = [self 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 isEqualToNumber:@(NANPA_COUNTRY_CODE_)] || + [regionCode isEqualToString:_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:self.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; + NSString *nationalSignificantNumber = [self getNationalSignificantNumber:number]; + if ([self hasValidCountryCallingCode:countryCallingCode] == NO) + { + return nationalSignificantNumber; + } + + if ([countryCallingCode isEqualToNumber: @(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]]) + { + // 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. + NBPhoneMetaData *metadataForRegionCallingFrom = [self 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 ([self 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 +{ + if ([self 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 intValue]) + { + 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 = [self 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 +{ + NSString *normalizedNationalNumber = [self normalizeDigitsOnly:rawInput]; + if ([self isStartingStringByRegex:normalizedNationalNumber regex:nationalPrefix]) + { + @try { + // 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]; + return [self isValidNumber:[self parse:subString defaultRegion:regionCode]]; + } + @catch (NSException *ex) { + return NO; + } + } + return NO; +} + + +/** + * 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) + { + NSInteger firstNationalNumberDigit = [self indexOfStringByString:rawInput target:[nationalNumber substringWithRange:NSMakeRange(0, 3)]]; + if (firstNationalNumberDigit != -1) + { + rawInput = [rawInput substringFromIndex:firstNationalNumberDigit]; + } + } + + NBPhoneMetaData *metadataForRegionCallingFrom = [self getMetadataForRegion:regionCallingFrom]; + if ([countryCode isEqualToNumber: @(NANPA_COUNTRY_CODE_)]) + { + if ([self isNANPACountry:regionCallingFrom]) + { + return [NSString stringWithFormat:@"%@ %@", countryCode, rawInput]; + } + } + else if (metadataForRegionCallingFrom != nil && [countryCode isEqualToNumber: [self getCountryCodeForValidRegion:regionCallingFrom]]) + { + 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) + { + NSUInteger size = [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 && [self 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 && [self 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:@"^%@", self.SEPARATOR_PATTERN_] withTemplate:@""]; + + // Replace the rest with a dash between each number group. + formattedNationalNumber = [self replaceStringByRegex:formattedNationalNumber regex:self.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 = nil; + @try { + res = [self getExampleNumber:regionCode]; + } + @catch (NSException *exception) { + NSDictionary *userInfo = [NSDictionary dictionaryWithObject:exception.reason + forKey:NSLocalizedDescriptionKey]; + (*error) = [NSError errorWithDomain:exception.name code:0 userInfo:userInfo]; + } + return res; +} + + +- (NBPhoneNumber*)getExampleNumber:(NSString*)regionCode +{ + return [self getExampleNumberForType:regionCode type:NBEPhoneNumberTypeFIXED_LINE]; +} + + +/** + * 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; + @try { + res = [self getExampleNumberForType:regionCode type:type]; + } + @catch (NSException *exception) { + NSDictionary *userInfo = [NSDictionary dictionaryWithObject:exception.reason + forKey:NSLocalizedDescriptionKey]; + (*error) = [NSError errorWithDomain:exception.name code:0 userInfo:userInfo]; + } + return res; +} + + +- (NBPhoneNumber*)getExampleNumberForType:(NSString*)regionCode type:(NBEPhoneNumberType)type +{ + // Check the region code is valid. + if ([self isValidRegionCode:regionCode] == NO) + { + return nil; + } + + NBPhoneNumberDesc *desc = [self getNumberDescByType:[self getMetadataForRegion:regionCode] type:type]; + + @try { + if ([self hasValue:desc.exampleNumber ]) + { + return [self parse:desc.exampleNumber defaultRegion:regionCode]; + } + } + @catch (NSException *e) + { + } + + return nil; +} + + +/** + * 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; + @try { + res = [self getExampleNumberForNonGeoEntity:countryCallingCode]; + } + @catch (NSException *exception) { + NSDictionary *userInfo = [NSDictionary dictionaryWithObject:exception.reason + forKey:NSLocalizedDescriptionKey]; + (*error) = [NSError errorWithDomain:exception.name code:0 userInfo:userInfo]; + } + return res; +} + + +- (NBPhoneNumber*)getExampleNumberForNonGeoEntity:(NSNumber*)countryCallingCode +{ + NBPhoneMetaData *metadata = [self getMetadataForNonGeographicalRegion:countryCallingCode]; + + if (metadata != nil) + { + NBPhoneNumberDesc *desc = metadata.generalDesc; + @try { + if ([self hasValue:desc.exampleNumber]) + { + NSString *callCode = [NSString stringWithFormat:@"+%@%@", countryCallingCode, desc.exampleNumber]; + return [self parse:callCode defaultRegion:UNKNOWN_REGION_]; + } + } + @catch (NSException *e) { + } + } + return nil; +} + + +/** + * 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 ([self hasValue:number.extension] == NO) + { + return @""; + } + else + { + if (numberFormat == NBEPhoneNumberFormatRFC3966) + { + return [NSString stringWithFormat:@"%@%@", RFC3966_EXTN_PREFIX_, number.extension]; + } + else + { + if ([self 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 ([self 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; +} + + +/** + * Returns the metadata for the given region code or {@code nil} if the region + * code is invalid or unknown. + * + * @param {?string} regionCode + * @return {i18n.phonenumbers.PhoneMetadata} + */ +- (NBPhoneMetaData*)getMetadataForRegion:(NSString*)regionCode +{ + if ([self hasValue:regionCode] == NO) + { + return nil; + } + + regionCode = [regionCode uppercaseString]; + + NBPhoneMetaData *metadata = [self.coreMetaData objectForKey:regionCode]; + + return metadata; +} + + +/** + * @param {number} countryCallingCode + * @return {i18n.phonenumbers.PhoneMetadata} + */ +- (NBPhoneMetaData*)getMetadataForNonGeographicalRegion:(NSNumber*)countryCallingCode +{ + NSString *countryCallingCodeStr = [NSString stringWithFormat:@"%@", countryCallingCode]; + return [self getMetadataForRegion:countryCallingCodeStr]; +} + + +/** + * @param {string} nationalNumber + * @param {i18n.phonenumbers.PhoneNumberDesc} numberDesc + * @return {boolean} + * @private + */ +- (BOOL)isNumberMatchingDesc:(NSString*)nationalNumber numberDesc:(NBPhoneNumberDesc*)numberDesc +{ + if (numberDesc == nil) + return NO; + + if ([self hasValue:numberDesc.possibleNumberPattern] == NO || [numberDesc.possibleNumberPattern isEqual:@"NA"]) + return [self matchesEntirely:numberDesc.nationalNumberPattern string:nationalNumber]; + + if ([self 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; + + NBPhoneMetaData *metadata = [self getMetadataForRegionOrCallingCode:countryCode regionCode:regionCode]; + if (metadata == nil || + ([_REGION_CODE_FOR_NON_GEO_ENTITY isEqualToString:regionCode] == NO && + ![countryCode isEqualToNumber: [self getCountryCodeForValidRegion:regionCode]])) + { + // 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 ([self hasValue:generalNumDesc.nationalNumberPattern] == NO) + { + NSUInteger numberLength = 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; + } + + NSArray *regionCodes = [self 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]; + NSUInteger regionCodesCount = [regionCodes count]; + + for (NSUInteger i = 0; i<regionCodesCount; i++) + { + NSString *regionCode = [regionCodes objectAtIndex:i]; + NBPhoneMetaData *metadata = [self getMetadataForRegion:regionCode]; + + if ([self 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 +{ + NSArray *regionCodes = [self regionCodeFromCountryCode:countryCallingCode]; + return regionCodes == nil ? 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 +{ + NSArray *regionCodes = [self 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 +{ + NSNumber * res = nil; + @try { + res = [self getCountryCodeForValidRegion:regionCode]; + } + @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; +} + +- (NSNumber *)getCountryCodeForValidRegion:(NSString*)regionCode +{ + NBPhoneMetaData *metadata = [self getMetadataForRegion:regionCode]; + if (metadata == nil) + { + NSException* metaException = [NSException exceptionWithName:@"INVALID_REGION_CODE" + reason:[NSString stringWithFormat:@"Invalid region code:%@", regionCode] + userInfo:nil]; + @throw metaException; + } + 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 +{ + NBPhoneMetaData *metadata = [self 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; + + NSArray *res = [self regionCodeFromCountryCode:@(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; + } + + /** @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 = -1; + @try { + res = [self isPossibleNumberWithReason:number]; + } + @catch (NSException *exception) { + NSDictionary *userInfo = [NSDictionary dictionaryWithObject:exception.reason + forKey:NSLocalizedDescriptionKey]; + (*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 ([self hasValue:generalNumDesc.nationalNumberPattern] == NO) + { + NSUInteger numberLength = 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 +{ + BOOL res = NO; + @try { + res = [self isPossibleNumberString:number regionDialingFrom:regionDialingFrom]; + } + @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)isPossibleNumberString:(NSString*)number regionDialingFrom:(NSString*)regionDialingFrom +{ + @try { + return [self isPossibleNumber:[self parse:number defaultRegion:regionDialingFrom]]; + } + @catch (NSException *e) { + return NO; + } +} + + +/** + * 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 error:(NSError**)error +{ + BOOL res = NO; + @try { + res = [self truncateTooLongNumber: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)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 +{ + if ((fullNumber.length == 0) || ([[fullNumber substringToIndex:1] isEqualToString:@"0"])) + { + // Country codes do not begin with a '0'. + return @0; + } + + NSUInteger numberLength = fullNumber.length; + + for (NSUInteger i = 1; i <= MAX_LENGTH_COUNTRY_CODE_ && i <= numberLength; ++i) + { + NSString *subNumber = [fullNumber substringWithRange:NSMakeRange(0, i)]; + NSNumber *potentialCountryCode = [NSNumber numberWithInteger:[subNumber integerValue]]; + + NSArray *regionCodes = [self 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; +} + + +/** + * 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) + { + return @0; + } + + NSNumber *res = @0; + @try { + res = [self maybeExtractCountryCode:number metadata:defaultRegionMetadata + nationalNumber:nationalNumber keepRawInput:keepRawInput phoneNumber:phoneNumber]; + } + @catch (NSException *exception) { + NSDictionary *userInfo = [NSDictionary dictionaryWithObject:exception.reason + forKey:NSLocalizedDescriptionKey]; + (*error) = [NSError errorWithDomain:exception.name code:0 userInfo:userInfo]; + } + + return res; +} + +- (NSNumber*)maybeExtractCountryCode:(NSString*)number metadata:(NBPhoneMetaData*)defaultRegionMetadata + nationalNumber:(NSString**)nationalNumber keepRawInput:(BOOL)keepRawInput + phoneNumber:(NBPhoneNumber**)phoneNumber +{ + 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 numberWithInt:countryCodeSource]; + } + + if (countryCodeSource != NBECountryCodeSourceFROM_DEFAULT_COUNTRY) + { + if (fullNumber.length <= MIN_LENGTH_FOR_NSN_) + { + @throw [NSException exceptionWithName:@"TOO_SHORT_AFTER_IDD" + reason:[NSString stringWithFormat:@"TOO_SHORT_AFTER_IDD:%@", fullNumber] + userInfo:nil]; + } + + 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. + @throw [NSException exceptionWithName:@"INVALID_COUNTRY_CODE" + reason:[NSString stringWithFormat:@"INVALID_COUNTRY_CODE:%@", potentialCountryCode] + userInfo:nil]; + } + 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 numberWithInt: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]; + NSUInteger matchEnd = matchedString.length; + NSString *remainString = [numberStr substringFromIndex:matchEnd]; + + NSRegularExpression *currentPattern = self.CAPTURING_DIGIT_PATTERN; + NSArray *matchedGroups = [currentPattern 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 error:(NSError**)error +{ + if (numberStr == NULL) + { + return 0; + } + + NBECountryCodeSource res = 0; + @try { + res = [self maybeStripInternationalPrefixAndNormalize:numberStr possibleIddPrefix:possibleIddPrefix]; + } + @catch (NSException *exception) { + NSDictionary *userInfo = [NSDictionary dictionaryWithObject:exception.reason + forKey:NSLocalizedDescriptionKey]; + (*error) = [NSError errorWithDomain:exception.name code:0 userInfo:userInfo]; + } + return res; +} + +- (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:self.LEADING_PLUS_CHARS_PATTERN_]) + { + (*numberStr) = [self replaceStringByRegex:(*numberStr) regex:self.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 error:(NSError**)error +{ + if (number == NULL) + { + return NO; + } + + BOOL res = NO; + @try { + res = [self maybeStripNationalPrefixAndCarrierCode:number metadata:metadata carrierCode:carrierCode]; + } + @catch (NSException *exception) { + NSDictionary *userInfo = [NSDictionary dictionaryWithObject:exception.reason + forKey:NSLocalizedDescriptionKey]; + (*error) = [NSError errorWithDomain:exception.name code:0 userInfo:userInfo]; + } + return res; +} + + +- (BOOL)maybeStripNationalPrefixAndCarrierCode:(NSString**)number metadata:(NBPhoneMetaData*)metadata carrierCode:(NSString**)carrierCode +{ + if (number == NULL) + { + return NO; + } + + NSString *numberStr = [(*number) copy]; + NSUInteger numberLength = numberStr.length; + NSString *possibleNationalPrefix = metadata.nationalPrefixForParsing; + + if (numberLength == 0 || [self 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 = [NSRegularExpression 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. + NSUInteger numOfGroups = 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 || [self 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 ([self hasValue:nationalNumberRule ] && [self matchesEntirely:nationalNumberRule string:numberStr] && + [self matchesEntirely:nationalNumberRule string:transformedNumber] == NO) + { + return NO; + } + + if ((noTransform && numOfGroups > 0 && [self hasValue:firstMatchStringWithGroup]) || (!noTransform && numOfGroups > 1)) + { + if (carrierCode != NULL && (*carrierCode) != nil) + { + (*carrierCode) = [(*carrierCode) stringByAppendingString:firstMatchStringWithGroup]; + } + } + else if ((noTransform && numOfGroups > 0 && [self 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]; + NSInteger mStart = [self stringPositionByRegex:numberStr regex:self.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:self.EXTN_PATTERN_]; + NSUInteger matchedGroupsLength = [firstMatch numberOfRanges]; + for (NSUInteger 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:self.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 +{ + return [self parseHelper:numberToParse defaultRegion:defaultRegion keepRawInput:NO checkRegion:YES]; +} + + +- (NBPhoneNumber*)parse:(NSString*)numberToParse defaultRegion:(NSString*)defaultRegion error:(NSError**)error +{ + NBPhoneNumber *phoneNumber = nil; + + @try { + phoneNumber = [self parseHelper:numberToParse defaultRegion:defaultRegion keepRawInput:NO checkRegion:YES]; + } + @catch (NSException *exception) { + NSDictionary *userInfo = [NSDictionary dictionaryWithObject:exception.reason + forKey:NSLocalizedDescriptionKey]; + if (error != NULL) + (*error) = [NSError errorWithDomain:exception.name code:0 userInfo:userInfo]; + } + + 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 +{ + NSString *(^ISOCountryCodeByCarrier)() = ^() { + CTTelephonyNetworkInfo *networkInfo = [[CTTelephonyNetworkInfo alloc] init]; + CTCarrier *carrier = [networkInfo subscriberCellularProvider]; + return [carrier isoCountryCode]; + }; + NSString *isoCode = ISOCountryCodeByCarrier(); + + if (!isoCode) { + isoCode = @"ZZ"; + } + + return [self parse:numberToParse defaultRegion:isoCode]; +} + + +/** + * 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 +{ + NBPhoneNumber *res = nil; + @try { + res = [self parseAndKeepRawInput:numberToParse defaultRegion:defaultRegion]; + } + @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; +} + +- (NBPhoneNumber*)parseAndKeepRawInput:(NSString*)numberToParse defaultRegion:(NSString*)defaultRegion +{ + if ([self isValidRegionCode:defaultRegion] == NO) + { + if (numberToParse.length > 0 && [numberToParse hasPrefix:@"+"] == NO) + { + NSException* metaException = [NSException exceptionWithName:@"INVALID_COUNTRY_CODE" + reason:[NSString stringWithFormat:@"Invalid country code:%@", numberToParse] + userInfo:nil]; + @throw metaException; + } + } + return [self parseHelper:numberToParse defaultRegion:defaultRegion keepRawInput:YES checkRegion:YES]; +} + + +/** + * 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 +{ + NBPhoneNumber *res = nil; + @try { + res = [self parseHelper:numberToParse defaultRegion:defaultRegion keepRawInput:keepRawInput checkRegion:checkRegion]; + } + @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; +} + +- (NBPhoneNumber*)parseHelper:(NSString*)numberToParse defaultRegion:(NSString*)defaultRegion + keepRawInput:(BOOL)keepRawInput checkRegion:(BOOL)checkRegion +{ + if (numberToParse == nil) + { + @throw [NSException exceptionWithName:@"NOT_A_NUMBER" reason:[NSString stringWithFormat:@"NOT_A_NUMBER:%@", numberToParse] userInfo:nil]; + } + else if (numberToParse.length > MAX_INPUT_STRING_LENGTH_) + { + @throw [NSException exceptionWithName:@"TOO_LONG" reason:[NSString stringWithFormat:@"TOO_LONG:%@", numberToParse] userInfo:nil]; + } + + NSString *nationalNumber = @""; + [self buildNationalNumberForParsing:numberToParse nationalNumber:&nationalNumber]; + + if ([self isViablePhoneNumber:nationalNumber] == NO) + { + @throw [NSException exceptionWithName:@"NOT_A_NUMBER" reason:[NSString stringWithFormat:@"NOT_A_NUMBER:%@", nationalNumber] userInfo: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) + { + @throw [NSException exceptionWithName:@"INVALID_COUNTRY_CODE" reason:[NSString stringWithFormat:@"INVALID_COUNTRY_CODE:%@", defaultRegion] userInfo: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 = [self 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 = @0; + NSString *nationalNumberStr = [nationalNumber copy]; + @try { + countryCode = [self maybeExtractCountryCode:nationalNumberStr + metadata:regionMetadata + nationalNumber:&normalizedNationalNumber + keepRawInput:keepRawInput + phoneNumber:&phoneNumber]; + } + @catch (NSException *e) { + if ([e.name isEqualToString:@"INVALID_COUNTRY_CODE"] && [self stringPositionByRegex:nationalNumberStr + regex:self.LEADING_PLUS_CHARS_PATTERN_] >= 0) + { + // Strip the plus-char, and try again. + nationalNumberStr = [self replaceStringByRegex:nationalNumberStr regex:self.LEADING_PLUS_CHARS_PATTERN_ withTemplate:@""]; + countryCode = [self maybeExtractCountryCode:nationalNumberStr + metadata:regionMetadata + nationalNumber:&normalizedNationalNumber + keepRawInput:keepRawInput + phoneNumber:&phoneNumber]; + if ([countryCode isEqualToNumber:@0]) + { + @throw [NSException exceptionWithName:e.name + reason:[NSString stringWithFormat:@"%@:%@", e.name, nationalNumberStr] + userInfo:nil]; + } + } + else + { + @throw e; + } + } + + 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_) + { + @throw [NSException exceptionWithName:@"TOO_SHORT_NSN" + reason:[NSString stringWithFormat:@"TOO_SHORT_NSN:%@", normalizedNationalNumber] + userInfo:nil]; + } + + if (regionMetadata != nil) + { + NSString *carrierCode = @""; + [self maybeStripNationalPrefixAndCarrierCode:&normalizedNationalNumber metadata:regionMetadata carrierCode:&carrierCode]; + + if (keepRawInput) + { + phoneNumber.PreferredDomesticCarrierCode = [carrierCode copy]; + } + } + + NSString *normalizedNationalNumberStr = [normalizedNationalNumber copy]; + + NSUInteger lengthOfNationalNumber = normalizedNationalNumberStr.length; + if (lengthOfNationalNumber < MIN_LENGTH_FOR_NSN_) + { + @throw [NSException exceptionWithName:@"TOO_SHORT_NSN" + reason:[NSString stringWithFormat:@"TOO_SHORT_NSN:%@", normalizedNationalNumberStr] + userInfo:nil]; + } + + if (lengthOfNationalNumber > MAX_LENGTH_FOR_NSN_) + { + @throw [NSException exceptionWithName:@"TOO_LONG" + reason:[NSString stringWithFormat:@"TOO_LONG:%@", normalizedNationalNumberStr] + userInfo:nil]; + } + + if ([normalizedNationalNumberStr hasPrefix:@"0"]) + { + phoneNumber.italianLeadingZero = YES; + } + + phoneNumber.nationalNumber = [NSNumber numberWithLongLong:(UInt64)[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; + + NSInteger indexOfPhoneContext = [self indexOfStringByString:numberToParse target:RFC3966_PHONE_CONTEXT_]; + if (indexOfPhoneContext > 0) + { + NSUInteger phoneContextStart = indexOfPhoneContext + 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. + NSUInteger rfc3966Start = [self indexOfStringByString:numberToParse target:RFC3966_PREFIX_] + 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]; + NSInteger 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. + @try { + firstNumber = [self parse:firstNumberIn defaultRegion:UNKNOWN_REGION_]; + } + @catch (NSException *e) { + if ([e.name 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 != UNKNOWN_REGION_) + { + @try { + firstNumber = [self parse:firstNumberIn defaultRegion:secondNumberRegion]; + } + @catch (NSException *e2) { + 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. + @try { + firstNumber = [self parseHelper:firstNumberIn defaultRegion:nil keepRawInput:NO checkRegion:NO]; + } + @catch (NSException *e2) { + return NBEMatchTypeNOT_A_NUMBER; + } + } + } + else + { + firstNumber = [firstNumberIn copy]; + } + + if ([secondNumberIn isKindOfClass:[NSString class]]) + { + @try { + secondNumber = [self parse:secondNumberIn defaultRegion:UNKNOWN_REGION_]; + return [self isNumberMatch:firstNumberIn second:secondNumber]; + } + @catch (NSException *e2) { + if ([e2.name isEqualToString:@"INVALID_COUNTRY_CODE"] == NO) + { + return NBEMatchTypeNOT_A_NUMBER; + } + return [self isNumberMatch:secondNumberIn second:firstNumber]; + } + } + 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 ([self hasValue:firstNumber.extension] && [self 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 +{ + NBPhoneMetaData *metadata = [self 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 rangeOfString:@"^"].location == NSNotFound) + { + regex = [NSString stringWithFormat:@"^(?:%@)$", regex]; + } + + NSError *error = nil; + NSRegularExpression *currentPattern = [NSRegularExpression regularExpressionWithPattern:regex options:0 error:&error]; + NSTextCheckingResult *matchResult = [currentPattern firstMatchInString:str options:0 range:NSMakeRange(0, str.length)]; + + if (matchResult != nil) + { + NSString *founds = [str substringWithRange:matchResult.range]; + + if ([founds isEqualToString:str]) + { + return YES; + } + } + + return NO; +} + + +@end