Corporation Unknown Logo

NSArray and stringWithFormat:

The standard Cocoa method for a string with multiple replacements is [NSString stringWithFormat:]. To build a standard analogy string from the SATs:

hand : palm :: foot : sole

you would break out each of the replaceable elements into separate strings and replace them in the format string with the placeholder '%@'. (There are many other format specifiers—look up printf()—but '%@' is very common in Cocoa since it specifies an Objective-C object, not just a simple number or C null-terminated string.)

NSString * format = @"%@ : %@ :: %@ : %@";

To create the desired string:

NSString * myString = 
   [NSString stringWithFormat:format, 
                              @"hand",
                              @"palm",
                              @"foot",
                              @"sole", 
                              nil];

If you want to create a different analogy string, just call stringWithFormat: again, using the same format string but different parameters following it.

The stringWithFormat: method takes a list of objects, and the nil parameter indicates the end of the list. If you want to manage the list of substitution parameters, though, you shouldn’t need that nil since NSArray knows how many elements it contains. Unfortunately, there isn’t a method like stringWithFormat: which takes an NSArray instead of a list of objects; there also doesn’t seem to be a simple way to convert the contents of an NSArray to a nil-terminated parameter list.

How then can you make this templating more dynamic at runtime by storing the list in an NSArray?

If you wanted to promote the list to an NSArray, your first attempt might look like:

NSArray * analogy =
   [NSArray arrayWithObjects:@"hand",
                             @"palm",
                             @"foot",
                             @"sole",
                             nil];
NSString * myString = 
   [NSString stringWithFormat:format, analogy];

As someone currently working in Perl, that is the idiom I would like to see, but trust me, this doesn’t work. If you’re lucky, you get the value of [analogy description] in the first placeholder and nothing (or garbage) in the rest—but usually you will just crash hard.

You could do a bunch of [string appendWithFormat:] calls, adding one element at a time, but that obscures the desired output in the code, and ties you to the position of elements in the array: With a format, you can flip the analogies with a format string of @"%3$@ : %4$@ :: %1$@ : %2$@" and that’s a very powerful piece of functionality which should not be readily tossed aside.

I searched the web for help; there are a number of instances of this question of “how can I use NSArray with stringWithFormat:?” but no solution. So I started dredging up almost-forgotten memories of ANSI C, and found something that works. For a single stringWithFormat-for-NSArray invocation:

// allocate a C array with the same number of elements as array
id args[ [analogy count] ];
NSUInteger index = 0;
// copy the object pointers from NSArray to C array
for ( id item in analogy ) {
   args[ index++ ] = item;
}

NSString * myString = 
   [[NSString alloc] initWithFormat:format
                          arguments:(va_list)args];

(As much as I dislike casts, I dislike compiler warnings even more: The cast to (va_list) appears to be safe in this case and eliminates the warning of “warning: passing argument 2 of ‘initWithFormat:arguments:’ from incompatible pointer type.”)

Note that this solution uses initWithFormat:arguments: instead of the more common stringWithFormat: or even initWithFormat:—this technique only works when passing the format and arguments separately. Be aware, then, that this is not an autoreleased object—you will be responsible for releasing the resultant string.

This technique does have a couple drawbacks:

  • You can’t use primitives as arguments. Then again, NSArrays only hold objects, not primitives, so that turns out to be a non-issue.

  • There seems to be no way to pre-flight the format string and ensure there are enough arguments. This isn’t any more problematic than mismatched argument lists built at compile time, but the flexibility of using an NSArray may increase the likelihood of mismatch instances.

  • Even though it would seem reasonable to assume so, I see no guarantees that the memory layout of an array will always match that of passed parameters. This has been tested on 32-bit PPC and Intel, but not 64-bit.

But it also has the advantage of being able to use a well-known powerful syntax, without having to create yet another templating language (yet).

Now you finally have the flexibility to apply a choice of format strings to multiple objects. If you have a list of analogy “objects” to print out:

NSArray * analogies = 
   [NSArray arrayWithObjects:
      [NSArray arrayWithObjects:@"hand",
                                @"palm",
                                @"foot",
                                @"sole",
                                nil],
      [NSArray arrayWithObjects:@"color",
                                @"spectrum",
                                @"tone",
                                @"scale",
                                nil],
      [NSArray arrayWithObjects:@"lawyer",
                                @"courtroom",
                                @"gladiator",
                                @"arena",
                                nil],
      [NSArray arrayWithObjects:@"frugal",
                                @"miserly",
                                @"confident",
                                @"arrogant",
                                nil],
   nil];
NSArray * formats = 
   [NSArray arrayWithObjects:
      @"analogy: %@ : %@ :: %@ : %@",
      @"reverse: %3$@ : %4$@ :: %1$@ : %2$@",
      @"as text: '%@' is to '%@' as '%@' is to '%@'",
      nil];

for ( NSArray * analogy in analogies ) {
   id args[ [analogy count] ];
   NSUInteger index = 0;
   // copy the object pointers from NSArray to C array
   for ( id item in analogy ) {
      args[ index++ ] = item;
   }

   // print this analogy with multiple formats
   for ( NSString * format in formats ) {
      NSString * myString = 
         [[NSString alloc] initWithFormat:format 
                                arguments:(va_list)args];
      NSLog( @"%@", myString );
      [myString release];
   }
}

There you go. Now feel free to throw it into your own method, or create a category on NSArray to allow you to write code like:

for ( NSArray * analogy in analogies ) {
   for ( NSString * format in formats ) {
      NSLog( @"%@", [analogy myStringWithFormat:format];
   }
}

Comments