Question

To ensure that a formatted string returned by NSString initWithFormat:arguments: is as expected, I need to determine if there are the same number of format specifiers as arguments. Below is a (slightly contrived and highly edited) example:

- (void)thingsForStuff:(CustomStuff)stuff, ...
{
    NSString *format;
    switch (stuff)
    {
        case CustomStuffTwo:
            format = @"Two things: %@ and %@";
        break;

        case CustomStuffThree:
            format = @"Three things: %@, %@, and %@";
        break;

        default:
            format = @"Just one thing: %@";
        break;
    }

    va_list args;
    va_start(args, method);
    // Want to check if format has the same number of %@s as there are args, but not sure how
    NSString *formattedStuff = [[NSString alloc] initWithFormat:format arguments:args];
    va_end(args);

    NSLog(@"Things: %@", formattedStuff);
}

Using this method, [self thingsForStuff:CustomStuffTwo, @"Hello", @"World"] would log

"Two things: Hello and World"

...but [self thingsForStuff:CustomStuffTwo, @"Hello"] would log

"Two things: Hello and "

...something that would be preferred to be caught before it happens.

Is there a way to count the format specifiers in a string, preferably something lightweight/inexpensive?

Was it helpful?

Solution

Is there a way to count the format specifiers in a string, preferably something lightweight/inexpensive?

Nope -- really isn't. At least, not if you want it to work across all possible format strings. You would have to duplicate the parser that is used by stringWithFormat:. I.e. don't try to validate everything.

You could count the number of %, but that would not catch things like %% or other special cases. That may be good enough for your purposes.

OTHER TIPS

Well, I created my own regex, I have no idea if it's going to catch all of them, and may end finding some false positives, but seems to be working for me:

static NSString *const kStringFormatSpecifiers =
@"%(?:\\d+\\$)?[+-]?(?:[lh]{0,2})(?:[qLztj])?(?:[ 0]|'.{1})?\\d*(?:\\.\\d+)?[@dDiuUxXoOfeEgGcCsSpaAFn]";

You can count the number of arguments using:

NSRegularExpression *regEx = [NSRegularExpression regularExpressionWithPattern: kStringFormatSpecifiers options:0 error:nil];
NSInteger numSpecifiers = [regEx numberOfMatchesInString: yourString options:0 range:NSMakeRange(0, yourString.length)];

Because of the way C and Objective-C handle variadic functions/methods like yours, you cannot in general tell how many arguments the user has provided.

Here are two ways to handle your situation.

First, look for another way to do this. The number of arguments you pass to the method is determined at compile-time. So maybe instead of using a variadic method, you should just have three methods:

- (void)doStuff:(CustomStuff)stuff withThing:(Thing *)thing;
- (void)doStuff:(CustomStuff)stuff withThing:(Thing *)thing1 thing:(Thing *)thing2;
- (void)doStuff:(CustomStuff)stuff withThing:(Thing *)thing1 thing:(Thing *)thing2 hatWearer:(Cat *)cat;

And you select the right method to call at compile-time based on how many arguments you want to pass, eliminating the switch statement entirely.

Second, I see that your predefined format strings only use the %@ format. Does this mean that you expect the user to only pass objects to your method (aside from the (CustomStuff)stuff argument)?

If the user will only pass objects to your method, and you require those arguments to be non-nil, then you can get the compiler to help you out. Change your method to require the user to pass nil at the end of the argument list. You can tell the compiler that the argument list has to be nil-terminated by declaring the method (in your @interface) like this:

@interface MyObject : NSObject

- (void)thingsForStuff:(CustomStuff)stuff, ... NS_REQUIRES_NIL_TERMINATION

@end

Now the compiler will warn the user “Missing sentinel in method dispatch” if he calls your method without putting a literal nil at the end of the argument list.

So, having changed your API to require some non-nil arguments followed by a nil argument, you can change your method to count up the non-nil arguments like this:

- (void)thingsForStuff:(CustomStuff)stuff, ... {
    int argCount = 0;
    va_list args;
    va_start(args, stuff);
    while (va_arg(args, id)) {
        ++argCount;
    }
    va_end(args)

    int expectedArgCount;
    NSString *format;
    switch (stuff) {
        case CustomStuffTwo:
            expectedArgCount = 2;
            format = @"Two things: %@ and %@";
            break;

        case CustomStuffThree:
            expectedArgCount = 3;
            format = @"Three things: %@, %@, and %@";
            break;

        // etc.
    }

    NSAssert(argCount == expectedArgCount, @"%@ %s called with %d non-nil arguments, but I expected %d", self, (char*)_cmd, argCount, expectedArgCount);

    va_start(args, stuff);
    NSString *formattedStuff = [[NSString alloc] initWithFormat:format arguments:args];
    va_end(args);

    NSLog(@"Things: %@", formattedString);
}

long specifierCount = [myFormatString componentsSeparatedByString:@"%"].count;

This will get you close. Its just a simple split. You would have to account for escaped % values.

You could count the number of format specifiers, but IIRC you will never be able to count the number of arguments passed into a variable-argument method. This is because of the way C pushes arguments on the stack without specifying how many it has pushed.

Most functions overcome this by requiring that the last argument be nil or some kind of terminator (see [NSArray arrayWithObjects:]). There's even a macro that allows the compiler to check this and emit a warning at compile time.

You can use NS_FORMAT_FUNCTION at the end of your function prototype, as in stringWithFormat method of NSString.

So your method's prototype should be like this:

- (void)thingsForStuff:(CustomStuff)stuff, ... NS_FORMAT_FUNCTION(1,2);
Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top