// // NBAsYouTypeFormatter.m // libPhoneNumber // // Created by ishtar on 13. 2. 25.. // #import "NBAsYouTypeFormatter.h" #import "NBPhoneNumberDefines.h" #import "NBMetadataHelper.h" #import "NBNumberFormat.h" #import "NBPhoneMetaData.h" #import "NBPhoneNumberUtil.h" #import "NSArray+NBAdditions.h" /** * The digits that have not been entered yet will be represented by a \u2008, * the punctuation space. */ static NSString *const NBDigitPlaceHolder = @"\u2008"; /** * Character used when appropriate to separate a prefix, such as a long NDD or a * country calling code, from the national number. */ static NSString *const NBSeparatorBeforeNationalNumber = @" "; /** * 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. */ static const NSUInteger NBMinLeadingDigitsLength = 3; @interface NBAsYouTypeFormatter () @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, strong, readwrite) NSMutableArray *possibleFormats_; @property(nonatomic, strong, readwrite) NBPhoneMetaData *currentMetaData_, *defaultMetaData_; @end @implementation NBAsYouTypeFormatter - (instancetype)init { self = [super init]; if (self) { _isSuccessfulFormatting = NO; /** * @type {string} * @private */ self.currentOutput_ = @""; /** * @type {!goog.string.StringBuffer} * @private */ self.formattingTemplate_ = [[NSMutableString alloc] init]; NSError *anError = nil; /** * @type {RegExp} * @private */ self.DIGIT_PATTERN_ = [NSRegularExpression regularExpressionWithPattern:NBDigitPlaceHolder options:0 error:&anError]; /** * 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:&anError]; /** * 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:&anError]; /** * 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:&anError]; /** * 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:&anError]; /** * The pattern from numberFormat that is currently used to create * formattingTemplate. * @type {string} * @private */ self.currentFormattingPattern_ = @""; /** * @type {!goog.string.StringBuffer} * @private */ self.accruedInput_ = [[NSMutableString alloc] init]; /** * @type {!goog.string.StringBuffer} * @private */ self.accruedInputWithoutFormatting_ = [[NSMutableString alloc] init]; /** * 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 alloc] init]; /** * @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 alloc] init]; /** * @type {Array.} * @private */ self.possibleFormats_ = [[NSMutableArray alloc] init]; } return self; } /** * Constructs an AsYouTypeFormatter for the specific region. * * - param regionCode regionCode the ISO 3166-1 two-letter region code that denotes * the region where the phone number is being entered. * @constructor */ - (instancetype)initWithRegionCode:(NSString *)regionCode { return [self initWithRegionCode:regionCode bundle:[NSBundle mainBundle]]; } - (instancetype)initWithRegionCode:(NSString *)regionCode bundle:(NSBundle *)bundle { self = [self init]; if (self) { /** * @private * @type {i18n.phonenumbers.PhoneNumberUtil} */ self.phoneUtil_ = [NBPhoneNumberUtil sharedInstance]; self.defaultCountry_ = regionCode; self.currentMetaData_ = [self getMetadataForRegion_:self.defaultCountry_]; /** * @type {i18n.phonenumbers.PhoneMetadata} * @private */ self.defaultMetaData_ = self.currentMetaData_; } 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 regionCode regionCode an ISO 3166-1 two-letter region code. * @return {i18n.phonenumbers.PhoneMetadata} main metadata for this region. * @private */ - (NBPhoneMetaData *)getMetadataForRegion_:(NSString *)regionCode { NBMetadataHelper *helper = [[NBMetadataHelper alloc] init]; /** @type {number} */ NSNumber *countryCallingCode = [self.phoneUtil_ getCountryCodeForRegion:regionCode]; /** @type {string} */ NSString *mainCountry = [self.phoneUtil_ getRegionCodeForCountryCode:countryCallingCode]; /** @type {i18n.phonenumbers.PhoneMetadata} */ NBPhoneMetaData *metadata = [helper 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 [[NBPhoneMetaData alloc] init]; } /** * @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_ nb_safeObjectAtIndex:i class:[NBNumberFormat class]]; /** @type {string} */ NSString *pattern = numberFormat.pattern; if (!pattern.length || [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 leadingDigits leadingThreeDigits first three digits of entered number. * @private */ - (void)getAvailableFormats_:(NSString *)leadingDigits { /** @type {Array.} */ BOOL isIntlNumberFormats = (self.isCompleteNumber_ && self.currentMetaData_.intlNumberFormats.count > 0); NSArray *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 nb_safeObjectAtIndex:i class:[NBNumberFormat class]]; /** @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_:leadingDigits]; } /** * - param format format * @return {BOOL} * @private */ - (BOOL)isFormatEligible_:(NSString *)format { if (!format.length) { return NO; } NSTextCheckingResult *matchResult = [self.ELIGIBLE_FORMAT_PATTERN_ firstMatchInString:format options:0 range:NSMakeRange(0, [format length])]; return (matchResult != nil); } /** * - param leadingDigits leadingDigits * @private */ - (void)narrowDownPossibleFormats_:(NSString *)leadingDigits { /** @type {Array.} */ NSMutableArray *possibleFormats = [[NSMutableArray alloc] init]; /** @type {number} */ NSUInteger indexOfLeadingDigitsPattern = leadingDigits.length - NBMinLeadingDigitsLength; /** @type {number} */ NSUInteger possibleFormatsLength = self.possibleFormats_.count; for (NSUInteger i = 0; i < possibleFormatsLength; ++i) { /** @type {i18n.phonenumbers.NumberFormat} */ NBNumberFormat *format = [self.possibleFormats_ nb_safeObjectAtIndex:i class:[NBNumberFormat class]]; if (format.leadingDigitsPatterns.count == 0) { // Keep everything that isn't restricted by leading digits. [possibleFormats addObject:format]; continue; } /** @type {number} */ NSInteger lastLeadingDigitsPattern = MIN(indexOfLeadingDigitsPattern, format.leadingDigitsPatterns.count - 1); /** @type {string} */ NSString *leadingDigitsPattern = [format.leadingDigitsPatterns nb_safeStringAtIndex:lastLeadingDigitsPattern]; if ([self.phoneUtil_ stringPositionByRegex:leadingDigits regex:leadingDigitsPattern] == 0) { [possibleFormats addObject:format]; } } 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_ setString:@""]; /** @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.} */ NSArray *m = [self.phoneUtil_ matchedStringByRegex:longestPhoneNumber regex:numberPattern]; // this match will always succeed /** @type {string} */ NSString *aPhoneNumber = [m nb_safeStringAtIndex: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:NBDigitPlaceHolder]; return template; } /** * Clears the internal state of the formatter, so it can be reused. */ - (void)clear { self.currentOutput_ = @""; [self.accruedInput_ setString:@""]; [self.accruedInputWithoutFormatting_ setString:@""]; [self.formattingTemplate_ setString:@""]; self.lastMatchPosition_ = 0; self.currentFormattingPattern_ = @""; [self.prefixBeforeNationalNumber_ setString:@""]; self.nationalPrefixExtracted_ = @""; [self.nationalNumber_ setString:@""]; 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_]; } } - (NSString *)removeLastDigitAndRememberPosition { NSString *accruedInputWithoutFormatting = [self.accruedInput_ copy]; [self clear]; NSString *result = @""; NSUInteger length = accruedInputWithoutFormatting.length; if (length == 0) { return result; } for (NSUInteger i = 0; i < length - 1; i++) { NSString *ch = [accruedInputWithoutFormatting substringWithRange:NSMakeRange(i, 1)]; result = [self inputDigitAndRememberPosition:ch]; } return result; } - (NSString *)removeLastDigit { NSString *accruedInputWithoutFormatting = [self.accruedInput_ copy]; [self clear]; NSString *result = @""; NSUInteger length = accruedInputWithoutFormatting.length; if (length == 0) { return result; } for (NSUInteger i = 0; i < length - 1; i++) { NSString *ch = [accruedInputWithoutFormatting substringWithRange:NSMakeRange(i, 1)]; result = [self inputDigit:ch]; } return result; } - (NSString *)inputStringAndRememberPosition:(NSString *)string { [self clear]; NSString *result = @""; NSUInteger length = string.length; for (NSUInteger i = 0; i < length; i++) { NSString *ch = [string substringWithRange:NSMakeRange(i, 1)]; result = [self inputDigitAndRememberPosition:ch]; } return result; } - (NSString *)inputString:(NSString *)string { [self clear]; NSString *result = @""; NSUInteger length = string.length; for (NSUInteger i = 0; i < length; i++) { NSString *ch = [string substringWithRange:NSMakeRange(i, 1)]; result = [self inputDigit:ch]; } return result; } /** * 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 { if (!nextChar || nextChar.length <= 0) { return self.currentOutput_; } 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 { if (!nextChar || nextChar.length <= 0) { return self.currentOutput_; } 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 { if (!nextChar || nextChar.length <= 0) { _isSuccessfulFormatting = NO; return self.currentOutput_; } [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_) { _isSuccessfulFormatting = YES; return [NSString stringWithString:self.accruedInput_]; } else if ([self attemptToExtractIdd_]) { if ([self attemptToExtractCountryCallingCode_]) { _isSuccessfulFormatting = YES; 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:NBSeparatorBeforeNationalNumber]; _isSuccessfulFormatting = YES; return [self attemptToChoosePatternWithPrefixExtracted_]; } _isSuccessfulFormatting = NO; 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: _isSuccessfulFormatting = YES; 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_]; _isSuccessfulFormatting = YES; return [self attemptToChooseFormattingPattern_]; } default: if (self.isExpectingCountryCallingCode_) { if ([self attemptToExtractCountryCallingCode_]) { self.isExpectingCountryCallingCode_ = NO; } _isSuccessfulFormatting = YES; return [NSString stringWithFormat:@"%@%@", self.prefixBeforeNationalNumber_, self.nationalNumber_]; } if (self.possibleFormats_.count > 0) { // The formatting patterns are 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) { _isSuccessfulFormatting = YES; return formattedNumber; } [self narrowDownPossibleFormats_:self.nationalNumber_]; if ([self maybeCreateNewTemplate_]) { _isSuccessfulFormatting = YES; return [self inputAccruedNationalNumber_]; } if (self.ableToFormat_) { _isSuccessfulFormatting = YES; return [self appendNationalNumber_:tempNationalNumber]; } else { _isSuccessfulFormatting = NO; return self.accruedInput_; } } else { _isSuccessfulFormatting = NO; return [self attemptToChooseFormattingPattern_]; } } _isSuccessfulFormatting = NO; } /** * @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_ = [self.nationalPrefixExtracted_ mutableCopy]; [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 = [self.prefixBeforeNationalNumber_ copy]; NSRange lastRange = [prefixBeforeNationalNumberStr rangeOfString:self.nationalPrefixExtracted_ options:NSBackwardsSearch]; /** @type {number} */ NSUInteger indexOfPreviousNdd = lastRange.location; self.prefixBeforeNationalNumber_ = [[prefixBeforeNationalNumberStr substringWithRange:NSMakeRange(0, indexOfPreviousNdd)] mutableCopy]; } return self.nationalPrefixExtracted_ != [self removeNationalPrefixFromNationalNumber_]; } /** * - param {string} nextChar * @return {BOOL} * @private */ - (BOOL)isDigitOrLeadingPlusSign_:(NSString *)nextChar { NSString *digitPattern = [NSString stringWithFormat:@"([%@])", NB_VALID_DIGITS_STRING]; NSString *plusPattern = [NSString stringWithFormat:@"[%@]+", NB_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 = [NBSeparatorBeforeNationalNumber 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_, NBSeparatorBeforeNationalNumber, 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} */ NSInteger accruedInputIndex = 0; /** @type {number} */ NSInteger 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 >= NBMinLeadingDigitsLength) { [self getAvailableFormats_:nationalNumber]; // See if the accrued digits can be formatted properly already. NSString *formattedNumber = [self attemptToFormatAccruedDigits_]; if (formattedNumber.length > 0) { return formattedNumber; } 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 isEqual:@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_ appendFormat:@"1%@", NBSeparatorBeforeNationalNumber]; 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.} */ NSArray *m = [self.phoneUtil_ matchedStringByRegex:nationalNumber regex:nationalPrefixForParsing]; NSString *firstString = [m nb_safeStringAtIndex: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_ = [[nationalNumber substringFromIndex:startOfNationalNumber] mutableCopy]; 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.} */ NSArray *m = [self.phoneUtil_ matchedStringByRegex:accruedInputWithoutFormatting regex:internationalPrefix]; NSString *firstString = [m nb_safeStringAtIndex:0]; if (m != nil && firstString != nil && firstString.length > 0) { self.isCompleteNumber_ = YES; /** @type {number} */ NSUInteger startOfCountryCallingCode = firstString.length; self.nationalNumber_ = [[accruedInputWithoutFormatting substringFromIndex:startOfCountryCallingCode] mutableCopy]; self.prefixBeforeNationalNumber_ = [[accruedInputWithoutFormatting substringWithRange:NSMakeRange(0, startOfCountryCallingCode)] mutableCopy]; if ([accruedInputWithoutFormatting characterAtIndex:0] != '+') { [self.prefixBeforeNationalNumber_ appendString:NBSeparatorBeforeNationalNumber]; } 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_ = [numberWithoutCountryCallingCode mutableCopy]; /** @type {string} */ NSString *newRegionCode = [self.phoneUtil_ getRegionCodeForCountryCode:countryCode]; if ([NB_REGION_CODE_FOR_NON_GEO_ENTITY isEqualToString:newRegionCode]) { NBMetadataHelper *helper = [[NBMetadataHelper alloc] init]; self.currentMetaData_ = [helper getMetadataForNonGeographicalRegion:countryCode]; } else if (newRegionCode != self.defaultCountry_) { self.currentMetaData_ = [self getMetadataForRegion_:newRegionCode]; } /** @type {string} */ [self.prefixBeforeNationalNumber_ appendFormat:@"%@%@", countryCode, NBSeparatorBeforeNationalNumber]; 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]; if (!normalizedChar) return @""; [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:NBDigitPlaceHolder] >= 0) { /** @type {number} */ int digitPatternStart = [self.phoneUtil_ stringPositionByRegex:formattingTemplate regex:NBDigitPlaceHolder]; /** @type {string} */ NSRange tempRange = [formattingTemplate rangeOfString:NBDigitPlaceHolder]; NSString *tempTemplate = [formattingTemplate stringByReplacingOccurrencesOfString:NBDigitPlaceHolder withString:nextChar options:NSLiteralSearch range:tempRange]; self.formattingTemplate_ = [tempTemplate mutableCopy]; 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_; } } /** * Returns the formatted number. * * @return {string} */ - (NSString *)description { return self.currentOutput_; } @end