Take the 2-minute tour ×
Code Review Stack Exchange is a question and answer site for peer programmer code reviews. It's 100% free, no registration required.

So this was originally inspired by @nhgrif's Int extension for translating integer to plain English, however the code is mainly a port of @rolfl's Converting a number to the text representation; taking into consideration the reviews on both answers as well.


NumberToText.h:

#import <Foundation/Foundation.h>

@interface NSString (NumberToText)

- (instancetype)initWithInteger:(NSInteger)number;
- (instancetype)initWithInteger:(NSInteger)number negativeSign:(NSString *)negativeSign;
+ (instancetype)stringWithInteger:(NSInteger)number;
+ (instancetype)stringWithInteger:(NSInteger)number negativeSign:(NSString *)negativeSign;
+ (NSString *)tripleAsText:(NSInteger)value;

@end

NumberToText.m:

#import "NumberToText.h"
#define DIGIT_LENGTH 19
#define TENS_LENGTH 9
#define MODIFIER_LENGTH 7

NSString * digitName(NSInteger number) {
    switch (number) {
        case 1: return @"one";
        case 2: return @"two";
        case 3: return @"three";
        case 4: return @"four";
        case 5: return @"five";
        case 6: return @"six";
        case 7: return @"seven";
        case 8: return @"eight";
        case 9: return @"nine";
        case 10: return @"ten";
        case 11: return @"eleven";
        case 12: return @"twelve";
        case 13: return @"thirteen";
        case 14: return @"fourteen";
        case 15: return @"fifteen";
        case 16: return @"sixteen";
        case 17: return @"seventeen";
        case 18: return @"eighteen";
        case DIGIT_LENGTH: return @"nineteen";
        default: return @"";
    }
}

NSString * tensName(NSInteger number) {
    switch (number) {
        case 0: return @"";
        case 1: return @"ten";
        case 2: return @"twenty";
        case 3: return @"thirty";
        case 4: return @"fourty";
        case 5: return @"fifty";
        case 6: return @"sixty";
        case 7: return @"seventy";
        case 8: return @"eighty";
        case TENS_LENGTH: return @"ninety";
        default: return @"";
    }
}

NSString * modifierName(NSInteger number) {
    switch (number) {
        case 1: return @"thousand";
        case 2: return @"million";
        case 3: return @"billion";
        case 4: return @"trillion";
        case 5: return @"quadrillion";
        case 6: return @"quintillion";
        case MODIFIER_LENGTH: return @"sextillion";
        default: return @"";
    }
}

@implementation NSString (NumberToText)

- (instancetype)initWithInteger:(NSInteger)number {
    return [self initWithInteger:number negativeSign:NULL];
}

+ (instancetype)stringWithInteger:(NSInteger)number {
    return [[self alloc] initWithInteger:number];
}

+ (instancetype)stringWithInteger:(NSInteger)number negativeSign:(NSString *)negativeSign {
    return [[self alloc] initWithInteger:number negativeSign:negativeSign];
}

/**
 * Convert any value from 0 to 999 inclusive, to a string.
 * @param value The value to convert.
 * @param and whether to use the word 'and' in the output.
 * @return a String representation of the value.
 */
+ (NSString *)tripleAsText:(NSInteger) value {
    int subhun = value % 100;
    long hun = value / 100;
    NSMutableString *string = [[NSMutableString alloc] init];
    if (hun > 0) {
        [string appendString:digitName(hun)];
        [string appendString:@" hundred"];
        if (subhun > 0) {
            [string appendString:@" and "];
        }
    }
    if (subhun < DIGIT_LENGTH) {
        [string appendString:digitName(subhun)];
    } else {
        int tens = subhun / 10;
        int units = subhun % 10;
        [string appendString:tensName(tens)];
        if (units>0) {
            [string appendString:@" "];
        }
        [string appendString:digitName(units)];
    }

    return string;
}

/**
 * Convert any long input value to a text representation
 * @param value The value to convert
 * @param useand true if you want to use the word 'and' in the text (eleven thousand and thirteen)
 * @param negname
 * @return Number as a text representation
 */
- (instancetype)initWithInteger:(NSInteger)number negativeSign:(NSString *)negativeSign {
    NSMutableString *string = [[NSMutableString alloc] init];
    if (number < 0) {
        if (!negativeSign) {
            negativeSign = @"negative";
        }
        number = ABS(number);
    }
    else {
        negativeSign = nil;
    }

    if (negativeSign) {
        [string appendString:negativeSign];
        [string appendString:@" "];
    }

    if (0 == number) {
        return digitName(0);
    }

    // break the value down in to sets of three digits (thousands).
    NSMutableArray *thous = [NSMutableArray array];
    for (int i = 0; i < MODIFIER_LENGTH; ++i) {
        [thous addObject: modifierName(i)];
    }
    // do not make negative numbers positive, to handle Long.MIN_VALUE
    int scale = 0;
    while (number != 0) {
        // use abs to convert thousand-groups to positive, if needed.
        thous[scale] = @(ABS(number % 1000));
        number /= 1000;
        ++scale;
    }

    Boolean first = true;
    while (--scale > 0) {
        if (!first) {
            [string appendString:@", "];
        }
        first = false;
        if (thous[scale] > 0) {
            [string appendString:[[self class] tripleAsText:[thous[scale] integerValue]]];
            [string appendString:@" "];
            [string appendString:modifierName(scale)];
        }
    }

    if (!first && thous[0] != 0) {
        [string appendString:@" and "];
    }
    [string appendString:[[self class] tripleAsText:[thous[0] integerValue]]];
    return string;
}

@end

main.m:

#import "NumberToText.h"

int main(void)
{
    @autoreleasepool
    {
        NSLog(@"%ld: %@", LONG_MIN, [NSString stringWithInteger:LONG_MIN]);
        NSLog(@"%ld: %@", LONG_MAX, [NSString stringWithInteger:LONG_MAX]);
    }
}

Please feel free to review anything you see could be better in the code.

share|improve this question
1  
Your self-answer is definitely going to be the suggested method. With that said, there's plenty of general Objective-C and OOP things worthy of a review in this question. –  nhgrif Aug 9 at 0:09
    
Is this officially rags-to-riches or not?? –  Vogel612 Aug 9 at 19:47

2 Answers 2

up vote 6 down vote accepted
+ (NSString *)tripleAsText:(NSInteger)value;

We've declared this method in our .h file. Our .h file is for publicly available methods. We've implemented this method to provide some convenience to what we're actually trying to do with the other methods. Do we really want/need to make this particular method public? If not, simply remove its declaration from the .h file.


#define DIGIT_LENGTH 19
#define TENS_LENGTH 9
#define MODIFIER_LENGTH 7

There is very little reason to use #define directives in Objective-C (and most languages, as far as I know). There might be some scenarios where a #define is necessary and good, but for simple constant values, we shouldn't do this.

Instead, we should use constants:

const NSInteger kDigitLength = 19;
const NSInteger kTensLength = 9;
const NSInteger kModifierLength = 7;

case MODIFIER_LENGTH: return @"sextillion";

The largest possible NSInteger value is just over 9 quintillion. Even if we modify the method to take an NSUInteger, the largest value is just over 18 quintillion. We will never have a sextillion value.


- (instancetype)initWithInteger:(NSInteger)number {
    return [self initWithInteger:number negativeSign:NULL];
}

When we want to pass a null value for an argument expecting an Objective-C object, we should use nil rather than NULL. I don't think there's an actual difference, just is just Objective-C convention. However, I'd argue that we should actually choose a default string to signify negative values such as @"negative". This method is most likely to be used by people who want to just use the default string, not by people who don't want to differentiate between negative and non-negative numbers.


int subhun = value % 100;
long hun = value / 100;

subhun and hun? What are these? Longer methods names that actually tell Mr. Maintainer what the variable represent doesn't make your program slower or make it use more run time memory.

However, dividing an NSInteger and saving the result as a long WILL make your program use more memory on any 32-bit system.

NSInteger is a typedef which serves as a 32-bit integer on 32-bit systems and a 64-bit integer on 64-bit systems. So on a 32-bit system, NSInteger is an int, and on 64-bit systems, it's a long. Saving the result of value % 100 to an int is fine--we don't lose precision because we know this value is always less than 100. Saving the result of value / 100 to a long means on a 32-bit system, we're now using 8 bytes to represent a number that is definitely smaller than 4 bytes. Instead, we should just:

NSInteger hun = value / 100;

Except, we still need to have a better variable name.


[string appendString:digitName(hun)];
[string appendString:@" hundred"];
if (subhun > 0) {
    [string appendString:@" and "];
}

We can make this slightly better.

First, the first two lines should definitely be combined into:

[string appendFormat:@"%@ hundred", digitName(hun)];

But with a ternary operator, we can do it all in one operation:

[string appendFormat:@"%@ hundred%@", digitName(hun), (subhun > 0) ? @" and " : @""];

if (units>0) {
    [string appendString:@" "];
}
[string appendString:digitName(units)];

Since we know that the result of digitName(units) when units>0 evaluates to false is an empty string, we can just do this:

if (units > 0) {
    [string appendFormat:@" %@", digitName(units)];
}

Not only does this eliminate a call to appendFormat:/appendString:, it also eliminates useless calls to digitName().


if (number < 0) {
    if (!negativeSign) {
        negativeSign = @"negative";
    }
    number = ABS(number);
}
else {
    negativeSign = nil;
}

if (negativeSign) {
    [string appendString:negativeSign];
    [string appendString:@" "];
}

Remember my comment about sending @"negative" rather than nil as the default value for the negative sign? It eliminates this sort of stuff.

We start by checking whether or not the number is negative, then use this information to set the value of negativeSign, then check whether or not negativeSign is set... which we already knew from the previous if/else block...

So, keeping in mind that the user is sending us a value for negativeSign, why don't we refactor this a bit:

if (number < 0) {
    [string appendFormat:@"%@ ", negativeSign];
}

I've also eliminated the call to the ABS() macro, because, well, it's not needed. The reason I know it's not needed is because your code works for LONG_MIN and for NSIntegerMin... however the result of ABS(NSIntegerMin) is: -9223372036854775808. The actual absolute value of this number can't be represented as an NSInteger... so apparently ABS() just leaves it as is. And despite this, despite number being negative all the way through your function, it still produces the correct results... so just get rid of this line.


if (0 == number) {
    return digitName(0);
}

First... number == 0 please. Second... digitName(number) please. Third... this would actually return @"" and not @"zero".

Since this is the only case in which we actually need the string @"zero" and is definitely a special case, I'd just write this as:

if (number == 0) {
    return @"zero";
}

Boolean first = true;

first = false;

In Objective-C, we should write this as:

BOOL first = YES;

first = NO;

if (thous[scale] > 0) and thous[0] != 0 are both problematic. Both of these will always evaluate to true because in any instance they'd evaluate to false, the app would have crashed before getting to this point.

thous (which should be called thousands) is an NSArray of object pointers. A pointer's value can never be negative, and 0 is nil essentially. A pointer could be nil (in which case this would evaluate false), however you can't put nil pointers inside an NSArray. (You have to use NSNull to represent nil in an Objective-C collection).

Specifically, thous is an array of NSNumber objects, and we must use different methods to compare it. The simplest method is to convert the NSNumber back into an integer:

if ([thous[scale] integerValue] > 0)

And

[[thous firstObject] integerValue] != 0

As a final note, I'd probably make tripleAsText: into a regular C-style function. It's not really necessary for it to be publicly available, and using [self class] to call it just looks clunky.

share|improve this answer

So it turns out we are doing a lot of extra work here. Apple has implemented their own way of accomplishing this.

NSNumberFormatter *formatter = [[NSNumberFormatter alloc] init];
[formatter setNumberStyle: NSNumberFormatterSpellOutStyle];
NSString* numberString = [formatter stringFromNumber:[NSNumber numberWithInt: 100]];

That's a lot simpler IMO. Also take into consideration the naming for the methods that Apple used.

share|improve this answer
2  
It's also worth noting that NSNumberFormatter can take an NSLocale. Much better than rolling a manual 'spell out' that takes into account i18n and accessibility. :-) –  Matthew Iselin Aug 9 at 0:02

Your Answer

 
discard

By posting your answer, you agree to the privacy policy and terms of service.

Not the answer you're looking for? Browse other questions tagged or ask your own question.