How to create a NSString from a format string like @"xxx=%@, yyy=%@" and a NSArray of objects?
Asked Answered
B

13

32

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...

Brightness answered 29/6, 2009 at 14:46 Comment(1)
Retagged since this question really pertains to Cocoa, not iPhone.Dolan
N
46

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:@[@"XXX", @"YYY"]];
NSLog( @"%@", s );

Unfortunately, for 64-bit, the va_list format has changed, so the above code no longer works. And probably should not be used anyway given it depends on the format that is clearly subject to change. Given there is no really robust way to create a va_list, a better solution is to simply limit the number of arguments to a reasonable maximum (say 10) and then call stringWithFormat with the first 10 arguments, something like this:

+ (id)stringWithFormat:(NSString *)format array:(NSArray*) arguments
{
    if ( arguments.count > 10 ) {
        @throw [NSException exceptionWithName:NSRangeException reason:@"Maximum of 10 arguments allowed" userInfo:@{@"collection": arguments}];
    }
    NSArray* a = [arguments arrayByAddingObjectsFromArray:@[@"X",@"X",@"X",@"X",@"X",@"X",@"X",@"X",@"X",@"X"]];
    return [NSString stringWithFormat:format, a[0], a[1], a[2], a[3], a[4], a[5], a[6], a[7], a[8], a[9] ];
}
Nomology answered 30/6, 2009 at 4:36 Comment(8)
+! 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.Dolan
There is an autorelease - I almost always use the idiom [[[alloc] init] autorelease] to minimize the chances of a memory release.Nomology
Wonder how to do this in iOS 5.0 which doesn't allow [arguments getObjects:(id *)argList]; and spits out Pointer to non-const type 'id' with no explicit ownershipHalberd
Added an answer for those wondering how to do this using ARC: https://mcmap.net/q/445539/-how-to-create-a-nsstring-from-a-format-string-like-quot-xxx-yyy-quot-and-a-nsarray-of-objectsVicar
I have nothing better to suggest, but I'm pretty sure this isn't standard.Tormentil
Sadly this fails in the 64-bit simulator.Tendril
For those of you using 64-bit please see my answer below.Safeguard
@Safeguard your answer does not work for arrays with more than one element in them.Nomology
V
37

Based on this answer using Automatic Reference Counting (ARC): https://mcmap.net/q/453868/-fake-va_list-in-arc

Add a category to NSString with the following method:

+ (id)stringWithFormat:(NSString *)format array:(NSArray *)arguments
{
    NSRange range = NSMakeRange(0, [arguments count]);
    NSMutableData *data = [NSMutableData dataWithLength:sizeof(id) * [arguments count]];
    [arguments getObjects:(__unsafe_unretained id *)data.mutableBytes range:range];
    NSString *result = [[NSString alloc] initWithFormat:format arguments:data.mutableBytes];
    return result;
}
Vicar answered 21/11, 2012 at 18:28 Comment(5)
A wild semicolon appears at the end of the first line. Be carefull people!Dorsett
@Dorsett That semicolon doesn't make any difference, it's actually allowed there. Removed it anyway to prevent any confusion since putting a semicolon there is not conventional.Vicar
I am almost sure my xCode gave an error. I have it on super strict.Dorsett
After adding arm64, i got EXC_BAD_ACCESS from this sometimesAndrien
Same here getting EXC_BAD_ACCESSScamper
B
16

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.

Brightness answered 29/6, 2009 at 22:28 Comment(4)
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...Dolan
Actually I'd vote this as a best solution. Quite practical and guaranteed to be portable (although the contents of va_list is unlikely to change, so it's a pretty safe bet too). There's nothing wrong with typing 20 arguments by hand (you can automate it with a script or Vim/Emacs/whatever macro if you really wanted to).Textbook
This is the only method that actually worked for me on all architectures when compiling for iOS 7 and iOS 8.Literator
This should become the excepted answer. I had sporadic crashes when using the accepted answer in the simulator.Amah
D
4

@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.

Dolan answered 29/6, 2009 at 16:37 Comment(5)
Of course, this only handles making strings with that particular format. It's not a general solution to format strings with NSArray.Zettazeugma
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.Dolan
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.Brightness
@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.Unsuspecting
Unfortunately there is currently not any built-in regex support in Cocoa. (See 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. regexkit.sourceforge.netDolan
S
4

This answer is buggy. As noted, there is no solution to this problem that is guaranteed to work when new platforms are introduced other than using the "10 element array" method.


The answer by solidsun was working well, until I went to compile with 64-bit architecture. This caused an error:

EXC_BAD_ADDRESS type EXC_I386_GPFLT

The solution was to use a slightly different approach for passing the argument list to the method:

+ (id)stringWithFormat:(NSString *)format array:(NSArray*) arguments;
{
     __unsafe_unretained id  * argList = (__unsafe_unretained id  *) calloc(1UL, sizeof(id) * arguments.count);
    for (NSInteger i = 0; i < arguments.count; i++) {
        argList[i] = arguments[i];
    }

    NSString* result = [[NSString alloc] initWithFormat:format, *argList] ;//  arguments:(void *) argList];
    free (argList);
    return result;
}

This only works for arrays with a single element

Safeguard answered 28/1, 2014 at 23:59 Comment(6)
Please don't post the exact same answer to multiple questions: it's either not a good fit for all or the questions are duplicates which should be flagged/closed as such.Lophobranch
Sorry, I didn't realize that I could flag the other question as a duplicate. It's been flagged now.Safeguard
Thanks @BerttThePark, had exact same issue, solved with your answer!Chyou
I had to make an #ifdef LP64 branch to make it work on both 32 bit and 64 bitLeibowitz
This code fails if the arguments array has more than one element in it. You are basically passing a single argument (the first object) to the initWithFormat method - the rest of the arguments will be got randomly off the stack, so you will be lucky not to crash.Nomology
You are correct Peter. Not sure how I never an across this bug in the last year.Safeguard
Z
3

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];

EDIT: The accepted answer claims there is a way to do this, but regardless of how fragile this answer might seem, that approach is far more fragile. It relies on implementation-defined behavior (specifically, the structure of a va_list) that is not guaranteed to remain the same. I maintain that my answer is correct and my proposed solution is less fragile since it only relies on defined features of the language and frameworks.

Zettazeugma answered 29/6, 2009 at 15:50 Comment(4)
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:].Dolan
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.Zettazeugma
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.Dolan
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.Dolan
A
3

For those who need a Swift solution, here is an extension to do this in Swift

extension String {

    static func stringWithFormat(format: String, argumentsArray: Array<AnyObject>) -> String {
        let arguments = argumentsArray.map { $0 as! CVarArgType }
        let result = String(format:format, arguments:arguments)
        return result
    }

}
Aspersorium answered 30/8, 2015 at 8:11 Comment(0)
B
2

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.

Bindery answered 30/6, 2009 at 3:35 Comment(0)
F
1
- (NSString *)stringWithFormat:(NSString *)format andArguments:(NSArray *)arguments {
    NSMutableString *result = [NSMutableString new];
    NSArray *components = format ? [format componentsSeparatedByString:@"%@"] : @[@""];
    NSUInteger argumentsCount = [arguments count];
    NSUInteger componentsCount = [components count] - 1;
    NSUInteger iterationCount = argumentsCount < componentsCount ? argumentsCount : componentsCount;
    for (NSUInteger i = 0; i < iterationCount; i++) {
        [result appendFormat:@"%@%@", components[i], arguments[i]];
    }
    [result appendString:[components lastObject]];
    return iterationCount == 0 ? [result stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceCharacterSet]] : result;
}

Tested with format and arguments:

NSString *format = @"xxx=%@, yyy=%@ last component";
NSArray *arguments = @[@"XXX", @"YYY", @"ZZZ"];

Result: xxx=XXX, yyy=YYY last component

NSString *format = @"xxx=%@, yyy=%@ last component";
NSArray *arguments = @[@"XXX", @"YYY"];

Result: xxx=XXX, yyy=YYY last component

NSString *format = @"xxx=%@, yyy=%@ last component";
NSArray *arguments = @[@"XXX"];

Result: xxx=XXX last component

NSString *format = @"xxx=%@, yyy=%@ last component";
NSArray *arguments = @[];

Result: last component

NSString *format = @"some text";
NSArray *arguments = @[@"XXX", @"YYY", @"ZZZ"];

Result: some text

Functionary answered 16/11, 2016 at 17:26 Comment(0)
U
0

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 .

Unsuspecting answered 29/6, 2009 at 15:31 Comment(2)
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.Dolan
I completely agree Quinn. You're suggestion for using arrays seems the right way to go.Unsuspecting
N
0

One can create a category for NSString and make a function which receives a format, an array and returns the string with replaced objects.

@interface NSString (NSArrayFormat)

+ (NSString *)stringWithFormat:(NSString *)format arrayArguments:(NSArray *)arrayArguments;

@end

@implementation NSString (NSArrayFormat)

+ (NSString *)stringWithFormat:(NSString *)format arrayArguments:(NSArray *)arrayArguments {
    static NSString *objectSpecifier = @"%@"; // static is redundant because compiler will optimize this string to have same address
    NSMutableString *string = [[NSMutableString alloc] init]; // here we'll create the string
    NSRange searchRange = NSMakeRange(0, [format length]);
    NSRange rangeOfPlaceholder = NSMakeRange(NSNotFound, 0); // variables are declared here because they're needed for NSAsserts
    NSUInteger index;
    for (index = 0; index < [arrayArguments count]; ++index) {
        rangeOfPlaceholder = [format rangeOfString:objectSpecifier options:0 range:searchRange]; // find next object specifier
        if (rangeOfPlaceholder.location != NSNotFound) { // if we found one
            NSRange substringRange = NSMakeRange(searchRange.location, rangeOfPlaceholder.location - searchRange.location);
            NSString *formatSubstring = [format substringWithRange:substringRange];
            [string appendString:formatSubstring]; // copy the format from previous specifier up to this one
            NSObject *object = [arrayArguments objectAtIndex:index];
            NSString *objectDescription = [object description]; // convert object into string
            [string appendString:objectDescription];
            searchRange.location = rangeOfPlaceholder.location + [objectSpecifier length]; // update the search range in order to minimize search
            searchRange.length = [format length] - searchRange.location;
        } else {
            break;
        }
    }
    if (rangeOfPlaceholder.location != NSNotFound) { // we need to check if format still specifiers
        rangeOfPlaceholder = [format rangeOfString:@"%@" options:0 range:searchRange];
    }
    NSAssert(rangeOfPlaceholder.location == NSNotFound, @"arrayArguments doesn't have enough objects to fill specified format");
    NSAssert(index == [arrayArguments count], @"Objects starting with index %lu from arrayArguments have been ignored because there aren't enough object specifiers!", index);
    return string;
}

@end

Because NSArray is created at runtime we cannot provide compile-time warnings, but we can use NSAssert to tell us if number of specifiers is equal with number of objects within array.

Created a project on Github where this category can be found. Also added Chuck's version by using 'stringByReplacingCharactersInRange:' plus some tests.

Using one million objects into array, version with 'stringByReplacingCharactersInRange:' doesn't scale very well (waited about 2 minutes then closed the app). Using the version with NSMutableString, function made the string in about 4 seconds. The tests were made using simulator. Before usage, tests should be done on a real device (use a device with lowest specs).

Edit: On iPhone 5s the version with NSMutableString takes 10.471655s (one million objects); on iPhone 5 takes 21.304876s.

Neolatin answered 20/6, 2015 at 10:18 Comment(0)
A
-2

Here's the answer without explicitly creating an array:

   NSString *formattedString = [NSString stringWithFormat:@"%@ World, Nice %@", @"Hello", @"Day"];

First String is the target string to be formatted, the next string are the string to be inserted in the target.

Arbour answered 26/6, 2013 at 4:53 Comment(0)
S
-3

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

Sgraffito answered 29/6, 2009 at 15:39 Comment(1)
This is so very wrong. (Yes, variable argument calls are resolved at compile time, but all sane libraries have matching functions taking a va_list.)Textbook

© 2022 - 2024 — McMap. All rights reserved.