Есть ли способ указать позицию / индекс аргумента в NSString stringWithFormat?

83

В C # есть синтаксис, который позволяет указать индекс аргумента в спецификаторе строкового формата, например:

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

Вы можете использовать аргументы несколько раз, а также не использовать предоставленные аргументы. В другом вопросе упоминается такое же форматирование для C / C ++ в форме %[index]$[format], например %1$i. Мне не удалось заставить NSString полностью соблюдать этот синтаксис, потому что он хорошо себя ведет при исключении аргументов из формата. Следующее не работает должным образом (EXC_BAD_ACCESS, потому что он пытается разыменовать ageпараметр как NSObject *):

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

Позиционные аргументы соблюдаются только в том случае, если в формате нет пропущенных аргументов (что кажется странным требованием):

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

Все эти вызовы правильно работают в OS X:

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

Есть ли способ сделать это с помощью NSString в Objective-C / Cocoa? Было бы полезно для целей локализации.

Джейсон
источник
Отправьте отчет об ошибке (и сообщите нам номер ошибки).
Дирк Тайзен

Ответы:

126

NSString и CFString поддерживают переупорядочиваемые / позиционные аргументы.

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

Также см. Документацию на Apple: String Resources

Джим Коррейя
источник
5
Я обновил вопрос, добавив некоторые пояснения. Похоже, что Какао не принимает во внимание пропущенные аргументы из формата, что было побочным эффектом нарушения прав доступа, которое я получал.
Джейсон
2
Учет пропущенных аргументов невозможно из-за того, как работают varargs в C. Не существует стандартного способа узнать количество аргументов или их размер. Синтаксический анализ строки обрабатывает это путем вывода информации из спецификаторов формата, что требует, чтобы спецификаторы действительно присутствовали.
Дженс Эйтон,
1
Я понимаю, как работает va_args; однако, похоже, это работает, как ожидалось: printf ("Возраст:% 2 $ i", [name UTF8String], age); Я пробовал другие printf с переупорядоченными / отсутствующими аргументами, и все они дают ожидаемый результат, тогда как NSString - нет.
Джейсон
7
Просто чтобы повторить мои выводы, тогда: stringWithFormat:поддерживает позиционные аргументы, пока все аргументы указаны в строке формата.
Джейсон
1

Следующий код исправляет ошибку, указанную в этой проблеме. Это обходной путь и перенумеровывает заполнители, чтобы заполнить пробелы.

+ (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;
}
Вернер Альтевишер
источник
0

После дополнительных исследований выяснилось, что Какао уважает позиционный синтаксис в printf. Поэтому альтернативный шаблон будет:

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

Однако было бы неплохо, если бы это было реализовано на NSString.

Джейсон
источник
1
sprintfне является частью Какао, это часть стандартной библиотеки C, и реализацией этого является stringWithFormat:/ initWithFormat:.
Питер Хози
Уточняю мой предыдущий комментарий: версия Какао - stringWithFormat:/ initWithFormat:. Это отдельная реализация (в настоящее время CFStringCreateWithFormat) от sprintfи друзей.
Питер Хози
4
Я полагаю, что мало смысла комментировать тот факт, что инициализация msg ровно 512 байтами примерно так же безопасна, как выполнение случайного селектора для случайного объекта, но в любом случае. Для тех, кто не в курсе: буферы фиксированного размера - один из самых простых способов уволиться. google: переполнение буфера
Джордж Пенн.