83

C# has syntax that allows you to specify the argument index in a string format specifier, e.g.:

string message = string.Format("Hello, {0}. You are {1} years old. How does it feel to be {1}?", name, age);

You can use arguments more than once and also omit arguments that are provided from being used. Another question mentions the same formatting for C/C++ in the form of %[index]$[format], e.g. %1$i. I have not been able to get NSString to fully respect this syntax, because it does behave well when omitting arguments from the format. The following does not work as expected (EXC_BAD_ACCESS because it attempts to dereference the age parameter as a NSObject*):

int age = 23;
NSString * name = @"Joe";
NSString * message = [NSString stringWithFormat:@"Age: %2$i", name, age];

The positional arguments are respected only if there are no missing arguments from the format (which seems to be an odd requirement):

NSString * message = [NSString stringWithFormat:@"Age: %2$i; Name: %1$@", name, age];

All these calls work properly in OS X:

printf("Age: %2$i", [name UTF8String], age);
printf("Age: %2$i %1$s", [name UTF8String], age);

Is there a way of accomplishing this using NSString in Objective-C / Cocoa? It would be useful for localization purposes.

Community
  • 1
  • 1
Jason
  • 28,040
  • 10
  • 64
  • 64

3 Answers3

129

NSString and CFString support reorderable/positional arguments.

NSString *string = [NSString stringWithFormat: @"Second arg: %2$@, First arg %1$@", @"<1111>", @"<22222>"];
NSLog(@"String = %@", string);

Also, see the documentation at Apple: String Resources

Michal
  • 15,429
  • 10
  • 73
  • 104
Jim Correia
  • 7,064
  • 1
  • 33
  • 24
  • 5
    I've updated the question with some clarifications. It appears Cocoa does not respect omitted arguments from the format, which was a side effect of the access violation I was receiving. – Jason Jul 01 '09 at 22:54
  • 2
    Respecting omitted arguments is not possible because of the way varargs in C work. There is no standard way to know the number of arguments or their size. String parsing handles this by inferring the information from the format specifiers, which requires that the specifiers are actually there. – Jens Ayton Jul 01 '09 at 23:50
  • 1
    I understand how va_args works; however, this appears to work as expected: printf("Age: %2$i", [name UTF8String], age); I've tried other printf's with reordered/missing args and they all give the expected output, whereas NSString does not. – Jason Jul 03 '09 at 20:11
  • 7
    Just to reiterate my findings, then: `stringWithFormat:` supports positional arguments as long as all arguments are specified in the format string. – Jason Dec 05 '09 at 17:33
  • 1
    Se also [String Format Specifiers](https://developer.apple.com/library/mac/documentation/Cocoa/Conceptual/Strings/Articles/formatSpecifiers.html#//apple_ref/doc/uid/TP40004265-SW1) – zaph Nov 02 '14 at 01:33
1

The following code fixes the bug specified in this issue. It is a workaround and renumbers the placeholders to fill gaps.

+ (id)stringWithFormat:(NSString *)format arguments:(NSArray*) arguments 
{
    NSMutableArray *filteredArguments = [[NSMutableArray alloc] initWithCapacity:arguments.count];
    NSMutableString *correctedFormat = [[NSMutableString alloc ] initWithString:format];
    NSString *placeHolderFormat = @"%%%d$";

    int actualPlaceholderIndex = 1;

    for (int i = 1; i <= arguments.count; ++i) {
        NSString *placeHolder = [[NSString alloc] initWithFormat:placeHolderFormat, i];
        if ([format rangeOfString:placeHolder].location != NSNotFound) {
            [filteredArguments addObject:[arguments objectAtIndex:i - 1]];

            if (actualPlaceholderIndex != i) {
                NSString *replacementPlaceHolder = [[NSString alloc] initWithFormat:placeHolderFormat, actualPlaceholderIndex];
                [correctedFormat replaceAllOccurrencesOfString:placeHolder withString:replacementPlaceHolder];    
                [replacementPlaceHolder release];
            }
            actualPlaceholderIndex++;
        }
        [placeHolder release];
    }

    if (filteredArguments.count == 0) {
        //No numbered arguments found: just copy the original arguments. Mixing of unnumbered and numbered arguments is not supported.
        [filteredArguments setArray:arguments];
    }

    NSString* result;
    if (filteredArguments.count == 0) {
        //Still no arguments: don't use initWithFormat in this case because it will crash: just return the format string
        result = [NSString stringWithString:format];
    } else {
        char *argList = (char *)malloc(sizeof(NSString *) * [filteredArguments count]);
        [filteredArguments getObjects:(id *)argList];
        result = [[[NSString alloc] initWithFormat:correctedFormat arguments:argList] autorelease];
        free(argList);    
    }

    [filteredArguments release];
    [correctedFormat release];

    return result;
}
Werner Altewischer
  • 10,080
  • 4
  • 53
  • 60
0

After doing more research, it appears Cocoa respects positional syntax in printf. Therefore an alternate pattern would be:

char msg[512] = {0};
NSString * format = @"Age %2$i, Name: %1$s"; // loaded from resource in practice
sprintf(msg, [format UTF8String], [name UTF8String], age);
NSString * message = [NSString stringWithCString:msg encoding:NSUTF8StringEncoding];

However, it would be nice if there was an implementation of this on NSString.

Jason
  • 28,040
  • 10
  • 64
  • 64
  • 1
    `sprintf` is not part of Cocoa, it's part of the C standard library, and the implementation of that is `stringWithFormat:`/`initWithFormat:`. – Peter Hosey Dec 17 '09 at 04:37
  • Clarifying my previous comment: The Cocoa version is `stringWithFormat:`/`initWithFormat:`. It's a separate implementation (currently, `CFStringCreateWithFormat`) from that of `sprintf` and friends. – Peter Hosey Dec 18 '09 at 03:05
  • 4
    I suppose there's little use to comment the fact that initializing msg with exactly 512 bytes is about as safe as performing a random selector on a random object, but anyway. To anyone not aware: fixed-size buffers are some of the easiest ways to get fired. google: buffer overflow – George Penn. Apr 27 '12 at 10:44