views:

3870

answers:

8

Is there any way to create a new NSString from a format string like @"xxx=%@, yyy=%@" and a NSArray of objects?

In the NSSTring class there are many methods like:

- (id)initWithFormat:(NSString *)format arguments:(va_list)argList
- (id)initWithFormat:(NSString *)format locale:(id)locale arguments:(va_list)argList
+ (id)stringWithFormat:(NSString *)format, ...

but non of them takes a NSArray as an argument, and I cannot find a way to create a va_list from a NSArray...

A: 

A va_list isn't really an argument so much as an indicator that it actually takes a variable number of arguments. So if you have 5 parameters in your string, just pass them as 5 separate arguments.

Eric Petroelje
I have a variable number of arguments that is defined dynamically at runtime. So I don't know the exact number when I am writing the code.
Panagiotis Korros
A: 

I found some code on the web that claims that this is possible however I haven't managed to do it myself, however if you don't know the number of arguments in advance you also need to build the format string dynamically so I just don't see the point.

You better off just building the string by iterating the array.

You might find the stringByAppendingString: or stringByAppendingFormat: instance method handy .

Ron Srebro
I also fail to see the point of using a single massive format string. :-) Those 2 methods are decent suggestions, but creates progressively longer autoreleased (immutable) strings for each array element. Creating short strings and joining them at the end allows Cocoa to do what it does best. Another alternative is to use an NSMutableString and -appendFormat, but then you have to append the comma separator between entries anyway.
Quinn Taylor
I completely agree Quinn. You're suggestion for using arrays seems the right way to go.
Ron Srebro
A: 

No, you won't be able to. Variable argument calls are solved at compile time, and your NSArray has contents only at runtime.

Marco Mustapic
A: 

There is no general way to pass an array to a function or method that uses varargs. In this particular case, however, you could fake it by using something like:

for (NSString *currentReplacement in array)
    [string stringByReplacingCharactersInRange:[string rangeOfString:@"%@"] 
            withString:currentReplacement];
Chuck
There are a few problems with this approach. Searching for the range of %@ isn't a good idea — that's what methods like +stringWithFormat: do, and probably in a smarter way than off-the-cuff code would. (Assume -rangeOfString: performs a linear search from the start of the string each time.) Also, as @Ron mentions, there is still the problem of dynamically creating a giant format string to accommodate N elements in the array. Lastly, the asker said it's an array of objects, not necessarily strings. A much better solution is -[NSArray componentsJoinedByString:].
Quinn Taylor
You could easily get around the linear search from the start problem by using rangeOfString:options:range:. I was simply showing a general technique that gets around the problem.
Chuck
Agreed, you could specify a starting range. However, even that kind of modification is still fragile, especially since you're replacing characters in the middle of a string, which (aside from being inefficient by itself) can change the length of the string (and hence the starting point for the search), and it creates a new (progressively longer) autoreleased string each time. It may not be possible to avoid autoreleased strings altogether, but minimizing the size can reduce the memory footprint dramatically.
Quinn Taylor
Then use an NSMutableString. Good grief, man.
Chuck
My apologies, I wasn't trying to be pedantic. :-) I have a curse of tending to think about these kind of memory and performance issues. The NSMutableString is definitely an equally good idea.
Quinn Taylor
+3  A: 

@Chuck is correct about the fact that you can't convert an NSArray into varargs. However, I don't recommend searching for the pattern %@ in the string and replacing it each time. (Replacing characters in the middle of a string is generally quite inefficient, and not a good idea if you can accomplish the same thing in a different way.) Here is a more efficient way to create a string with the format you're describing:

NSArray *array = ...
NSAutoreleasePool *pool = [NSAutoreleasePool new];
NSMutableArray *newArray = [NSMutableArray arrayWithCapacity:[array count]];
for (id object in array) {
    [newArray addObject:[NSString stringWithFormat:@"x=%@", [object description]]];
}
NSString *composedString = [[newArray componentsJoinedByString:@", "] retain];
[pool drain];

I included the autorelease pool for good housekeeping, since an autoreleased string will be created for each array entry, and the mutable array is autoreleased as well. You could easily make this into a method/function and return composedString without retaining it, and handle the autorelease elsewhere in the code if desired.

Quinn Taylor
Of course, this only handles making strings with that particular format. It's not a general solution to format strings with NSArray.
Chuck
True, but I couldn't infer from his format string @"xxx=%@, yyy=%@" what exactly he was going for. If he wants a particular format string for each element, it would be easy to add a parallel array that contains the format string for the respective element in the original object array.
Quinn Taylor
I really don;t have much control over the supplied format string. The format string I gave was an example. Sorry if I was not being clear enough.
Panagiotis Korros
@Panagitios you can still use Quinn's suggestion and just take format specifiers from an array.@Quinn - is there a good regular expression support in the SDK? Might also prove useful in this case.
Ron Srebro
Unfortunately there is currently not any built-in regex support in Cocoa. (See http://stackoverflow.com/questions/1019280/#1019823 for my plug.) With any luck, it will happen in 10.7, but for now, I recommend checking out RegexKit, an open-source codebase that adds regex support. http://regexkit.sourceforge.net/
Quinn Taylor
A: 

One solution that came to my mind is that I could create a method that works with a fixed large number of arguments like:

+ (NSString *) stringWithFormat: (NSString *) format arguments: (NSArray *) arguments {
    return [NSString stringWithFormat: format ,
          (arguments.count>0) ? [arguments objectAtIndex: 0]: nil,
          (arguments.count>1) ? [arguments objectAtIndex: 1]: nil,
          (arguments.count>2) ? [arguments objectAtIndex: 2]: nil,
          ...
          (arguments.count>20) ? [arguments objectAtIndex: 20]: nil];
}

I could also add a check to see if the format string has more than 21 '%' characters and throw an exception in that case.

Panagiotis Korros
That's generally a bad idea. Way too much unnecessary code. (Also, there could be %% combos to get a % sign, etc.) Just accept an array of objects and generate your own NSMutableString. If you really don't have control over the format string, it's best to stick to the number of format specifiers it has, rather than the length of the array. Refactoring the problem would be my first choice, but it sounds like that may not be an option. From your description of what you're stuck with, this sound like an ugly, unfortunate situation to be in...
Quinn Taylor
+1  A: 

Yes, it is possible. In GCC targeting Mac OS X, at least, va_list is simply a C array, so you'll make one of ids, then tell the NSArray to fill it:

NSArray *argsArray = [[NSProcessInfo processInfo] arguments];
va_list args = malloc(sizeof(id) * [argsArray count]);
NSAssert1(args != nil, @"Couldn't allocate array for %u arguments", [argsArray count]);

[argsArray getObjects:(id *)args];

//Example: NSLogv is the version of NSLog that takes a va_list instead of separate arguments.
NSString *formatSpecifier = @"\n%@";
NSString *format = [@"Arguments:" stringByAppendingString:[formatSpecifier stringByPaddingToLength:[argsArray count] * 3U withString:formatSpecifier startingAtIndex:0U]];
NSLogv(format, args);

free(args);

You shouldn't rely on this nature in code that should be portable. iPhone developers, this is one thing you should definitely test on the device.

Peter Hosey
+5  A: 

It is actually not hard to create a va_list from an NSArray. See Matt Gallagher's excellent article on the subject.

Here is an NSString category to do what you want:

@interface NSString (NSArrayFormatExtension)

+ (id)stringWithFormat:(NSString *)format array:(NSArray*) arguments;

@end

@implementation NSString (NSArrayFormatExtension)

+ (id)stringWithFormat:(NSString *)format array:(NSArray*) arguments;
{
    char *argList = (char *)malloc(sizeof(NSString *) * [arguments count]);
    [arguments getObjects:(id *)argList];
    NSString* result = [[[NSString alloc] initWithFormat:format arguments:argList] autorelease];
    free(argList);
    return result;
}

@end

Then:

NSString* s = [NSString stringWithFormat:@"xxx=%@, yyy=%@" array:[NSArray arrayWithObjects:@"XXX", @"YYY", nil]];
NSLog( @"%@", s );
Peter N Lewis
Thank you, great solution! and works as expected!
Panagiotis Korros
+! Great find! Much better than parsing %@ oneself, and much easier in the situation when you don't control the format string. The one downside I see is that NSArray only stores objects (not primitives like int, float, etc.) so you're limited there to some degree. (This is a problem inherent with using an NSArray.) You could try NSPointerArray, but it doesn't have -getObjects, only -(void*)pointerAtIndex: so you' have to loop one at a time. Incidentally, since +stringWithFormat:array: is a convenience constructor, don't forget to autorelease the return value, or you'll have a memory leak.
Quinn Taylor