iPhone developers often find themselves trying to stick to the standards set by Apple. Most of the time, doing so is facilitated by the SDK and the results are better. Sometimes though, it's not easy at all, mainly because some APIs are not open.
One example is phone number formatting. The Contacts application utilizes an auto-formatting UITextField for phone number entry. Many business applications would make use of such functionality but it's not provided by the current SDK.
I've been through that and was not happy about the task. After procrastinating for a while, I thought of a solution and later on, I actually implemented it, and it worked well for me. I wished I could make it into the sought after NSPhoneNumberFormatter, but I was done with the task. I thought I should share it with fellow iPhone developers. I hope many will be able to use it right away as-is, and it would be great if someone could finish the job and make the formatter.
I acquired the predefined localized phone formats from the UIPhoneFormats.plist. I was mainly interested in the us locale, but I kept the implementation generic. The main idea is to build some sort of a finite state machine (FSM) for the phone format and use it to process the input string. The FSM will both validate and add formatting characters to the string as needed. If the whole input could be processed successfully, then it is a valid format and the output is returned.
The problem that I didn't solve yet, is when the string can be matched to more than one format. I had to manually sort the formats from the most restrictive to the least restrictive, so the one that comes first is always selected. This hack was okay for US formats, but there was nothing I could do with the last 3 UK formats since, to me, they are essentially the same. I guess this can be worked around somehow.
I'll post the code here, and will try to add more comments later. Please feel free to add your comments or ask for clarifications.
Update 02/14/2010: I'm so glad to have habermann24 join in and create his phoney ruby lib. If your Ruby/Rails application deals with phone numbers, you got to check it out!
Update 09/08/2010: Check out libphonenumber: Google's common Java, C++ and Javascript library for parsing, formatting, storing and validating international phone numbers. The Java version is optimized for running on smartphones. You'll need to look for the AsYouTypeFormatter.java. Update++, check out the comment below by +SpoofApp.
Update 08/15/2011: Updates on libphonenumber: New development of the library will be presented in the 35th Internationalization and Unicode Conference in a session titled: libphonenumber - The Swiss Army Knife of International Telephone Number Handling. See session description for details.
Update 02/06/2015: libphonenumber on Github by Google Internationalization (i18n).
PhoneNumberFormatter.h
// Created by Ahmed Abdelkader on 1/22/10.
// This work is licensed under a Creative Commons Attribution 3.0 License.
#import <Foundation/Foundation.h>
@interface PhoneNumberFormatter : NSObject {
//stores predefiend formats for each locale
NSDictionary *predefinedFormats;
}
/*
Loads predefined formats for each locale.
The formats should be sorted so as more restrictive formats should come first.
This is necessary as the formatting code processes the formats in order and
selects the first one that matches the whole input string.
*/
- (id)init;
/*
Attemps to format the phone number to the specified locale.
*/
- (NSString *)format:(NSString *)phoneNumber withLocale:(NSString *)locale;
/*
Strips the input string from characters added by the formatter.
Namely, it removes any character that couldn't have been entered by the user.
*/
- (NSString *)strip:(NSString *)phoneNumber;
/*
Returns true if the character comes from a phone pad.
*/
- (BOOL)canBeInputByPhonePad:(char)c;
@end
PhoneNumberFormatter.m
// Created by Ahmed Abdelkader on 1/22/10.
// This work is licensed under a Creative Commons Attribution 3.0 License.
#import "PhoneNumberFormatter.h"
@implementation PhoneNumberFormatter
- (id)init {
NSArray *usPhoneFormats = [NSArray arrayWithObjects:
@"+1 (###) ###-####",
@"1 (###) ###-####",
@"011 $",
@"###-####",
@"(###) ###-####", nil];
NSArray *ukPhoneFormats = [NSArray arrayWithObjects:
@"+44 ##########",
@"00 $",
@"0### - ### ####",
@"0## - #### ####",
@"0#### - ######", nil];
NSArray *jpPhoneFormats = [NSArray arrayWithObjects:
@"+81 ############",
@"001 $",
@"(0#) #######",
@"(0#) #### ####", nil];
predefinedFormats = [[NSDictionary alloc] initWithObjectsAndKeys:
usPhoneFormats, @"us",
ukPhoneFormats, @"uk",
jpPhoneFormats, @"jp",
nil];
return self;
}
- (NSString *)format:(NSString *)phoneNumber withLocale:(NSString *)locale {
NSArray *localeFormats = [predefinedFormats objectForKey:locale];
if(localeFormats == nil) return phoneNumber;
NSString *input = [self strip:phoneNumber];
for(NSString *phoneFormat in localeFormats) {
int i = 0;
NSMutableString *temp = [[[NSMutableString alloc] init] autorelease];
for(int p = 0; temp != nil && i < [input length] && p < [phoneFormat length]; p++) {
char c = [phoneFormat characterAtIndex:p];
BOOL required = [self canBeInputByPhonePad:c];
char next = [input characterAtIndex:i];
switch(c) {
case '$':
p--;
[temp appendFormat:@"%c", next]; i++;
break;
case '#':
if(next < '0' || next > '9') {
temp = nil;
break;
}
[temp appendFormat:@"%c", next]; i++;
break;
default:
if(required) {
if(next != c) {
temp = nil;
break;
}
[temp appendFormat:@"%c", next]; i++;
} else {
[temp appendFormat:@"%c", c];
if(next == c) i++;
}
break;
}
}
if(i == [input length]) {
return temp;
}
}
return input;
}
- (NSString *)strip:(NSString *)phoneNumber {
NSMutableString *res = [[[NSMutableString alloc] init] autorelease];
for(int i = 0; i < [phoneNumber length]; i++) {
char next = [phoneNumber characterAtIndex:i];
if([self canBeInputByPhonePad:next])
[res appendFormat:@"%c", next];
}
return res;
}
- (BOOL)canBeInputByPhonePad:(char)c {
if(c == '+' || c == '*' || c == '#') return YES;
if(c >= '0' && c <= '9') return YES;
return NO;
}
- (void)dealloc {
[predefinedFormats release];
[super dealloc];
}
@end