Friday, January 22, 2010

Locale-aware phone number formatting on the iPhone

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.

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

20 comments:

habermann24 said...

Very cool stuff!!

I'm using this to implement a PhoneNumber formatter/validator for ruby...

You said you acquired the predefined localized phone formats from the UIPhoneFormats.plist... that's what i did now, also...but what's your most recent version of that file?? Because I only have one that was in Firmware 1.0.x ...

Other than that...i THINK i solved the automatic sorting (but in ruby code).

What i do is:

# formats is the array, e.g. ['(##) ###', '+1 ###', etc...]

formats.sort do |x,y|

# Use ljust so that '###' is before '#####'
x = strip_invalid_characters(x).ljust(256, '*')
y = strip_invalid_characters(y).ljust(256, '*')

y <=> x
end

strip_invalid_characters is the same you do..it strips all non-keypad-characters from a string.

ljust appends '*' to the format, so that internally...i compare:

"######*************"
"+1###**************"

so this can get lexicographically sorted. The ones with less '#' characters end up further up, and the ones with [0-9+] are even further up then ones starting with '#' ...

e.g. with the us format:

'###-####' has to be BEFORE '(###) ###-####' because otherwise your FSM-matching would match the first working formatting expressing (being '(###) ###-####' ..

Hope this isn't confusing and maybe helps someone ;)

Ahmed Abdelkader said...

@habermann24: Thank you!

Honestly, I don't remember which UIPhoneFormats.plist I used. I was mainly interested in US formats and I was comparing the results to the native iPhone apps since that's all that mattered to me.

As for the sorting problem, I updated the post to clarify what I meant by sorting.

I came across your Ruby library on github (http://github.com/habermann24/phone_number). Now that's what I was talking about :D Go for it man! Just remember to mention your old buddy down here :)

Keep in touch.

habermann24 said...
This comment has been removed by the author.
habermann24 said...

Just wanted to let you know that i found a more recent and more complete way of parsing rules.

In fact, the rules are stored in a different way...I suppose that's how the iPhone does it in recent firmwares.

http://github.com/habermann24/phone_number

The parser.rb is where the magic happens.
:)

mfunaki said...

I suppose the third object of jpPhoneFormats should be "010 $", instead of "001 $". 001 may work as it is tied with a specific int'l call operator (KDDI), but 010 is a generic prefix which applies a caller defined (or default) operator.

Tony said...

Works like a charm! I did have to change the country codes in the NSArray to uppercase for it to work properly. My iPhone 4 and iPod Touch 2G return the 2-digit country code in uppercase.

Many thanks!!

Tony said...

Sorry, not NSArray, but NSDictionary.

Anonymous said...

Hi, I was wondering if it would be ok for me to use this class in a project of mine for a class?

I tried it out and it works perfect.

Thanks

Ahmed Abdelkader said...

@Dec14: Sure, provided the CC and your class policies allow and you have a good understanding of how it works. Maybe you can help make it better or come-up with your own if you just give it a try!

Anonymous said...

UK telephone number formats are seemingly complex, but are fairly easy to understand once you know the rules:

== 7 digit NSNs ==

* 0800 1111
* 0845 46 4x

== 9 digit NSNs ==

* (016977) 2xxx
* (016977) 3xxx
* (01xxx) xxxxx
* 0500 xxxxxx
* 0800 xxxxxx

== 10 digit NSNs ==

* (013873) xxxxx
* (015242) xxxxx
* (015394) xxxxx
* (015395) xxxxx
* (015396) xxxxx
* (016973) xxxxx
* (016974) xxxxx
* (016977) xxxxx
* (017683) xxxxx
* (017684) xxxxx
* (017687) xxxxx
* (019467) xxxxx
* (011x) xxx xxxx
* (01x1) xxx xxxx
* (01xxx) xxxxxx
* (02x) xxxx xxxx
* 03xx xxx xxxx
* 055 xxxx xxxx
* 056 xxxx xxxx
* 070 xxxx xxxx
* 076 xxxx xxxx
* 07xxx xxxxxx
* 08xx xxx xxxx
* 09xx xxx xxxx

From abroad, replace the leading 0 with +44.

The extra complications come with Mixed and with ELNS areas, but those are only a problem if you are trying to match a number to a location.

Omar said...

Hey there. First of all, thankyou very much for this great code. It took me about 2 hours to go through all those loops and figure out what in the world you're doing, but it was worth it. Very elegantly done.

Secondly, what I did with your code is that I included into my code but instead of making a new class, I added it as a Category to NSString. I took all the code in the init function, and I put it at the head of the format function. I also took out the parsed phone number variable and just used self as the string to be evaluated, since it's in a category now. Another thing I did, is put NSLocale as the argument instead of (NSString *)locale. I figured it'd be more in tune with what apple would do. I hope these suggestions are good for anyone else who wants to use the code.

Zsolt said...

I am trying to figure out what the cc3 means exactl. If I were to use your code, what is that I need to do Ahmed?

Thanks!

Attribution — You must attribute the work in the manner specified by the author or licensor (but not in any way that suggests that they endorse you or your use of the work).

Ahmed Abdelkader said...

You can include it in your project but you need to keep the file header with the name and license. Good luck!

Zsolt said...

Thanks Ahmed. I really appreciate it.

Anonymous said...

There's a bug in the iPhone telephone number formatting for one area of the UK (in all software versions from 2007 to present day).

They only have eleven of the twelve 5-digit area codes implemented.

A number such as 01946755555 should be formatted as (019467) 55555 but the iPhone treats it as if it is (01946) 755555 which is incorrect.

There's a list of UK area code lengths published over at: http://www.aa-asterisk.org.uk/index.php/Number_Format and http://www.aa-asterisk.org.uk/index.php/01_numbers

SpoofApp said...

I was able to take the LibPhoneNumber javascript, load it into a hidden UIWebView and run stringByEvaluatingJavaScriptFromString to format numbers. The javascript clocked in at just under 250kb and it was lightening fast to perform lookups.

Ahmed Abdelkader said...

@SpoofApp: nice one ;)

Anonymous said...

@"0### - ### ####",

@"0## - #### ####",

@"0#### - ######


The above are the 3+7, 2+8 and 4+6 fomats used in the UK.

You're missing UK telephone numbers that use 4+5, 5+5 and 5+4 format as well as NDO 0+10 format and so on.

Anonymous said...

is there a C# version?

Ahmed Abdelkader said...

https://bitbucket.org/pmezard/libphonenumber-csharp. If you still want to port the code above, it should be very easy.