object c 数字转字符串,特别是大数字
时间:2010-08-25 来源:Likwo
http://forgecode.net/2010/02/simplifying-nsnumberformatter-use-on-the-iphone-with-objective-c-categories/
Simplifying NSNumberFormatter use on the iPhone with Objective-C categories
Tuesday 2nd February 2010Something that comes up regularly in iPhone development is the need to convert NSNumberobjects to NSString objects and vice versa. For converting an NSNumber to an NSString, there is the -stringValue method, which is useful if you want just the plain number without any extra formatting. This isn’t particularly useful for presenting numbers in a user interface however, since -stringValue doesn’t give you any control over how the number is formatted.
Enter NSNumberFormatter
The NSNumberFormatter class is Cocoa’s way of converting between NSNumber and NSStringobjects. It provides both printing (number-to-string) and parsing (string-to-number).NSNumberFormatter offers a number of built-in “formatter styles”; these are pre-built styles that allow you to print or parse numbers in a number of common styles. The different styles are declared in an enum as the type: NSNumberFormatterStyle. Here are the different formatter styles, and the result when converting [NSNumber numberWithDouble:123.4] into an NSString.
- NSNumberFormatterNoStyle (123)
- NSNumberFormatterDecimalStyle (123.4)
- NSNumberFormatterCurrencyStyle ($123.40)
- NSNumberFormatterPercentStyle (12,340%)
- NSNumberFormatterScientificStyle (1.234E2)
- NSNumberFormatterSpellOutStyle (one hundred and twenty-three point four)
Note that my iPhone and Mac have the country set to Australia, and the language set to Australian English. These built-in styles are localised, so the results may be different depending on the country and language of your iPhone (or Mac, if you’re running in the iPhone Simulator).
NSNumberFormatter In Practice
To actually convert an NSNumber to an NSString using NSNumberFormatter using one of the pre-defined NSNumberFormatterStyle options (in this case, NSNumberFormatterCurrencyStyle), you would use the following code:
formatter.numberStyle = NSNumberFormatterCurrencyStyle;
NSString *string = [formatter stringFromNumber:number];
[formatter release];
Simple enough, but every time I write code that does this, a little voice in the back of my head says:
You repeat those 4 lines of code all the time. Write a category on NSNumber, stick those 4 lines in a category method, beautify your code, and save the world!
Right. The following methods could be written in a category on NSNumber, and the world would be a much better place:
+ (NSNumber *)numberWithString:(NSString *)string
numberStyle:(NSNumberFormatterStyle)style;
Then we could do the conversion mentioned earlier as easily as this:
We would have turned 4 lines of code into 1 easy to read line of code, which is clearly a lot nicer than the “normal” way of using an NSNumberFormatter.
Then another voice in my head says:
But that means we’ll be creating, configuring and destroying an NSNumberFormatterevery time we use these methods! That’ll be inefficient, right?
Instead of seeking medication for the voices I seem to be hearing in my head, I decided to write up the categories and do some testing to see just how inefficient it would be.
I ended up with 4 different versions of my category methods. I wrote a test app that timed how long it took to perform the same conversions with each version, then collected the data to compare the efficiency of each version.
The Benchmark
To calculate the “efficiency” of each of my different approaches, I needed a “best practice” benchmark. I used code resembling the following to do a large number of conversions in the most efficient way possible:
formatter.numberStyle = NSNumberFormatterPercentStyle;
for (int i = 0; i < TEST_ITERATIONS; i ++) {
NSNumber *num = [NSNumber numberWithInt:i];
NSString *str = [formatter stringFromNumber:num];
}
[formatter release];
Testing this code with TEST_ITERATIONS equal to 10,000 revealed that on average, each number-to-string conversion took approximately 113.7µs on my iPhone 3GS. This was my benchmark – now for the alternative methods.
Version 1 – Dumb
Version 1 is the most simple (and obvious) way to solve this problem:
{
NSNumberFormatter *formatter = [[NSNumberFormatter alloc] init];
formatter.numberStyle = style;
NSString *string = [formatter stringFromNumber:self];
[formatter release];
return string;
}
To do the same conversions as the benchmark, I used code like this:
NSNumber *num = [NSNumber numberWithInt:i];
NSString *str = [num stringWithNumberStyle:NSFormatterPercentStyle];
}
The obvious inefficiency here is that every time we call the method, we create, configure, then destroy an NSNumberFormatter object. If we’re using this method a lot, this might start to add up.
Testing showed that this method took 2741.1µs per conversion, which is around 24 times slower than the benchmark. If you need to do more than 100 of these conversions at a time, this adds up to a quarter of a second, which is definitely a noticeable delay (this compares to only 10ms for the benchmark). I decided once I saw these figures that it was worth optimising the code to see how fast we could get it to go.
Version 2 – Smart?
Version 2 caches a single NSNumberFormatter object in a global variable (effectively a singleton), so we don’t have to create and destroy an object every time we use our category method.
NSString *kSharedNumberFormatterLock = @"kSharedNumberFormatterLock";
...
- (NSString *)stringWithNumberStyle2:(NSNumberFormatterStyle)style
{
NSString *string = nil;
@synchronized(kSharedNumberFormatterLock) {
if (sharedNumberFormatter == nil) {
sharedNumberFormatter = [[NSNumberFormatter alloc] init];
}
sharedNumberFormatter.numberStyle = style;
string = [sharedNumberFormatter stringFromNumber:self];
}
return string;
}
This time, the only difference between our category method and the benchmark is that we have to run through a mutex lock every time, and we are re-configuring theNSNumberFormatter every time we use it. How much does this slow us down? It turns out that this code averages 1466.2µs per conversion, which works out to be nearly twice as fast as version 1, but still 13 times slower than the benchmark.
Version 3 – Smarter
It turns out that re-configuring the NSNumberFormatter is slow. By maintaining a separateNSNumberFormatter for each of the NSNumberFormatterStyle styles, our efficiency starts to approach the benchmark.
NSNumberFormatter *sharedNumberFormatterCurrencyStyle = nil;
...
static NSString *kSharedNumberFormatterDecimalStyleLock = @"kSharedNumberFormatterDecimalStyleLock";
static NSString *kSharedNumberFormatterCurrencyStyleLock = @"kSharedNumberFormatterCurrencyStyleLock";
...
- (NSString *)stringWithNumberStyle3:(NSNumberFormatterStyle)style
{
NSNumberFormatter *formatter = nil;
switch (style) {
case NSNumberFormatterDecimalStyle:
@synchronized(kSharedNumberFormatterDecimalStyleLock) {
if (sharedNumberFormatterDecimalStyle == nil) {
sharedNumberFormatterDecimalStyle = [[NSNumberFormatter alloc] init];
sharedNumberFormatterDecimalStyle.numberStyle = NSNumberFormatterDecimalStyle;
}
}
formatter = sharedNumberFormatterDecimalStyle;
break;
case NSNumberFormatterCurrencyStyle:
@synchronized(kSharedNumberFormatterCurrencyStyleLock) {
if (sharedNumberFormatterCurrencyStyle == nil) {
sharedNumberFormatterCurrencyStyle = [[NSNumberFormatter alloc] init];
sharedNumberFormatterCurrencyStyle.numberStyle = NSNumberFormatterCurrencyStyle;
}
}
formatter = sharedNumberFormatterCurrencyStyle;
break;
...
default:
break;
}
return [formatter stringFromNumber:self];
}
Note that I have removed the other styles from this code to aid in readability; where you see ‘…’, there is in fact duplicate code for the remaining formatter styles.
Version 3 clocks in at 127.8µs per conversion, which is only 12.4% longer than the benchmark. This is what we would expect – the only things that should be slowing us down (explaining the 12% speed penalty) are the mutex locks.
Version 4 – Smartest!
In version 4, I added an if statement that skips the mutex lock if the appropriate sharedNumberFormatter has already been initialised. An if statement is a whole lot faster than a @synchronized statement, so this is another win.
{
NSNumberFormatter *formatter = nil;
switch (style) {
case NSNumberFormatterDecimalStyle:
if (sharedNumberFormatterDecimalStyle) {
formatter = sharedNumberFormatterDecimalStyle;
break;
}
@synchronized(kSharedNumberFormatterDecimalStyleLock) {
if (sharedNumberFormatterDecimalStyle == nil) {
sharedNumberFormatterDecimalStyle = [[NSNumberFormatter alloc] init];
sharedNumberFormatterDecimalStyle.numberStyle = NSNumberFormatterDecimalStyle;
}
}
formatter = sharedNumberFormatterDecimalStyle;
break;
...
default:
break;
}
return [formatter stringFromNumber:self];
}
Again, I’ve removed the code that handles the other formatter styles for readability (I only included the switch/case for the Decimal Style). This version clocks in at 116.3µs, which is only 1.02 times as long as the benchmark (2.3% to be precise). There’s clearly little point in trying to optimise this any further – this is about as fast is we’re going to get.
Number-to-String results
Here are the results for Number-to-Strong conversions. I generated these figures by running the tests multiple times, discarding any obvious outliers, then taking averages. If anyone is interested in seeing the code used to generate these numbers, let me know and I’ll post the whole Xcode project.
Average (µs) | Penalty (%) | |
Benchmark | 113.7 | - |
Version 1 | 2741.1 | 2311% |
Version 2 | 1466.2 | 1190% |
Version 3 | 127.8 | 12.4% |
Version 4 | 116.3 | 2.3% |
String-to-Number results
Writing string-to-number methods was relatively straightforward after writing the number-to-string methods. The actual structure of each method was exactly the same, so the hard work was already done.
I also generated results for the string-to-number conversions:
Average (µs) | Penalty (%) | |
Benchmark | 348.2 | - |
Version 1 | 2909.2 | 735.5% |
Version 2 | 1664.7 | 378.1% |
Version 3 | 369.0 | 5.9% |
Version 4 | 359.7 | 3.3% |
Conclusion
If you want to use any of the pre-built NSNumberFormatterStyle options, the NSNumber category methods that I have described here give you a much nicer syntax, with a negligible performance hit. If you need to use a non-standard formatter style, then clearly these methods won’t help, and you’ll need to use an NSNumberFormatter directly. Creating, configuring and destroying each NSNumberFormatter instance is expensive compared to the time it takes to actually do the conversion; if you ever have to use an NSNumberFormatterwith the same settings more than a couple of times, hold on to it (e.g. in an ivar).
Download
I’ve uploaded the source code for all 4 versions for both -stringWithNumberStyle: and+numberWithString:numberStyle:. I will publish a polished version of the “Version 4″ methods on GitHub once I’ve given the code a proper test-drive.