Functionality of sprintf for NSString - objective-c

How can I get the functionality of sprintf in Objective-C? The function is of course part of stdio in C, so I could certainly invoke it, but since I'm using Foundation, I need it to work with NSStrings as well.
EDIT
I apologize for my inarticulateness, but I'm actually hoping for something more like the PHP sprintf function that returns a string. (This was perhaps slightly evident before Josh Caswell's very efficient edit of my question.) It would be like NSLog but instead of writing to console would give a string (or pointer) as a return value.

The closest function that does the same thing in Cocoa is NSString's stringWithFormat.
Using sprintf:
char buf[100];
sprintf(buf, "Line %d of %d", currentLine, totalLines);
Using stringWithFormat:
NSString res = [NSString stringWithFormat:#"Line %d of %d", currentLine, totalLines];
Note that stringWithFormat: supports printing of Cocoa objects with %# format specifier.

One option:
NSString *OCSprintf(NSString *format, ...) NS_FORMAT_FUNCTION(1,2);
NSString *OCSprintf(NSString *format, ...)
{
va_list args;
va_start(args, format);
NSString *result = [[[NSString alloc] initWithFormat:format arguments:args] autorelease];
va_end(args);
return result;
}
This just wraps stringWithFormat: (more precisely, one of its cousins) as a function as you requested. The NS_FORMAT_FUNCTION(1,2) ensures you still get format string checking.

NSString * myString = [NSString stringWithFormat:#"...", params…];
const char * myStrPtr = [myString UTF8String];

Related

Printing an NSString

What's the correct way to print an NSString in Objective-C? A lot of examples use NSLog(), but according to the documentation:
NSLog is a FoundationKit function for printing debug statements to the console.
...
NSLog works basically like:
fprintf(stderr, format_string, args ...);
Which to me is a bit like the _TRACE macro in Win32/C++. I don't want to print to stderr, I want to print to stdout. There are people who suggest using printf() as follows:
printf("%s", [str cStringUsingEncoding:NSUTF8StringEncoding]);
But this seems like an extra level on indirection to get the NSString printed, and it doesn't "feel" like the solution.
Spewing stuff to stdout is actually a pretty rare thing to do in Cocoa, given that almost all projects are GUI in nature. There are relatively few projects that are built as command line tools or otherwise need to deal with stdout.
However, the Foundation does provide the means to write to stdout. Specifically, NSFileHandle has fileHandleWithStandardOutput which gives you a file handle that can write to stdout.
From there, it is a matter of converting the NSString to an NSData and writing it.
Quite a few steps, but easily wrapped up in a reusable function:
void MyLog(NSString *format, ...) {
va_list args;
va_start(args, format);
NSString *formattedString = [[NSString alloc] initWithFormat: format
arguments: args];
va_end(args);
[[NSFileHandle fileHandleWithStandardOutput]
writeData: [formattedString dataUsingEncoding: NSNEXTSTEPStringEncoding]];
[formattedString release];
}
Well this is the solution.
Since printf is a pure C function, it won't recognize the Objective-C objects. (NSLog's formatter is distinct from printf's one.) Therefore, you have to convert it into a C string before formatting.
BTW you can use [str UTF8String] instead of [str cStringUsingEncoding:NSUTF8StringEncoding].
C string (UTF8String) is a pointer to a structure inside the string object.
NSString *str = #"Hello, World.";
printf("%s\n", [str UTF8String]);
I think you'll find these adequate for your needs:
// print to stdout
static void NSPrint(NSString *format, ...) {
va_list args;
va_start(args, format);
NSString *string = [[NSString alloc] initWithFormat:format arguments:args];
va_end(args);
fprintf(stdout, "%s\n", [string UTF8String]);
#if !__has_feature(objc_arc)
[string release];
#endif
}
// print to stderr
static void NSPrintErr(NSString *format, ...) {
va_list args;
va_start(args, format);
NSString *string = [[NSString alloc] initWithFormat:format arguments:args];
va_end(args);
fprintf(stderr, "%s\n", [string UTF8String]);
#if !__has_feature(objc_arc)
[string release];
#endif
}
You should use the custom file handler or write a macro yourself.
Tip: when NSLog prints out an object it uses object's debugDescription method. You could override this method for your custom NSObject subclasses to print custom debugInfo to stdout.
I just do:
define NSPrintf(...) printf( "%s", [[NSString stringWithFormat: __VA_ARGS__] UTF8String] )
Then i can use it as:
NSPrintf( #"Sorry %#, I can't do that\n", name );

strchr in objective C?

I'm trying to write the equivalent of strchr, but with NSStrings... I've currently got this:
Boolean nsstrchr(NSString* s, char c)
{
NSString *tmps = [NSString stringWithFormat: #"%c", c];
NSCharacterSet *cSet = [NSCharacterSet characterSetWithCharactersInString: tmps];
NSRange rg = [s rangeOfCharacterFromSet: cSet];
return rg.location != NSNotFound;
}
This seems needlessly complex... Is there a way to do this (preferably, one that doesn't involve turning the NSString into a cstring which doubles the run time, or writing it myself using characterAtIndex:... Am I missing some obvious method in the NSString description?
KennyTM has already noted (in a comment) that you can use -[NSString rangeOfString:] instead of -[NSString rangeOfCharacterFromSet]. I don't mean to steal his helpful answer, but I wanted to point out that you can wrap this up in a category on NSString to make it easier to use:
#interface NSString (AdditionalStringOps)
- (BOOL) containsCharacter:(char) c;
#end
#implementation NSString (AdditionalStringOps)
- (BOOL) containsCharacter:(char) c
{
NSString *tmp = [NSString stringWithFormat:#"%c", c];
return [self rangeOfString:tmp].location != NSNotFound;
}
#end
And then you can use it like so:
if ([myString containsCharacter:'A']) {
// Do stuff...
}
As #KennyTM mentioned, use rangeOfString:. If you really have to deal with stuff on the char level, you can pull out the char* via -UTF8String (or -cStringUsingEncoding:) and then search for your character in there like you would on any other char* (probably by using strchr).
Boolean nsstrchr(NSString* s, char c)
{
NSString *tmps = [NSString stringWithFormat: #"%c", c];
NSRange rg = [s rangeOfString: tmps];
return rg.location != NSNotFound;
}
Several simplifications.
Use [s rangeOfString:tmps] to eliminate the character set.
The character set can be created with [NSCharacterSet characterSetWithRange:NSMakeRange(c, 1)], so you don't need to create tmps
strchr([s UTF8String], c) — assuming c is an ASCII character, and the returned pointer won't escape the function (there's a risk the pointer becomes dangling).

What's the proper way to wrap [NSString stringWithFormat:]?

Assume I have a method with the signature:
+ (NSString *) myFormattedString:(NSString *)format, ...;
And I want it to prepend a string of my choice (e.g. #"Foo: "). I guess the best way is to use [myString initWithFormat:arguments:], but how would you implement this method?
I tried doing the following, but I get the warning as specified in the comment:
+ (NSString *) myFormattedString:(NSString *)format, ... {
char *buffer;
[format getCString:buffer maxLength:[format length] encoding:NSASCIIStringEncoding];
va_list args;
va_start(args, buffer); // WARNING: second parameter of 'va_start' not last named argument
NSString *str = [[NSString alloc] initWithFormat:format arguments:args];
[str autorelease];
return [NSString stringWithFormat:#"Foo: %#.", str];
}
The reason I'm assuming va_start() can take in a (char *) is because of the example I saw on the manual page of STDARG(3). Feel free to completely rewrite the method if I'm doing it totally wrong.
I think what you want is something like:
+ (NSString *) myFormattedString:(NSString *)format, ... {
va_list args;
va_start(args, format);
NSString *str = [[[NSString alloc] initWithFormat:format arguments:args] autorelease];
va_end(args);
return [NSString stringWithFormat:#"Foo: %#.", str];
}
The stdarg.h va_* macros are used when a function (or, in this case, method) takes a variable number of arguments, as specified by "...". va_start() is used to find the start of where the variable number of arguments begins. As such, it needs to know a functions/methods last argument (the one just before the "...") in order to determine where the variable number of arguments starts. This is a somewhat simplified explanation since what actually happens under the hood is very ABI/Compiler specific. The most important point is that the second argument to va_start() is always the name of the variable 'just before the "..."'.
va_end() should be "called" (it's really a macro, not a function) for maximum portability. Again, this whole variable arguments thing is deep, deep black magic. Depending on the specifics of the compiler and ABI, va_end() may not do anything at all. On the other hand, failure to use va_end() may cause your program to crash when the return statement is reached because the stack frame (if there even is one) is no longer properly set up to actually perform a return.
You've almost got it; just a couple of tweaks:
+ (NSString *) myFormattedString:(NSString *)format, ... {
va_list args;
va_start(args, format);
NSString *str = [[NSString alloc] initWithFormat:format arguments:args];
[str autorelease];
va_end(args);
return [NSString stringWithFormat:#"Foo: %#.", str];
}
That should do what you're looking for.

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

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...
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] ];
}
Based on this answer using Automatic Reference Counting (ARC): https://stackoverflow.com/a/8217755/881197
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;
}
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.
#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.
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
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.
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
}
}
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.
- (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
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 .
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.
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.
No, you won't be able to. Variable argument calls are solved at compile time, and your NSArray has contents only at runtime.

Simple string parsing in Cocoa / Objective-C: parsing a command line into command and arguments

Here's a piece of code to take a string (either NSString or NSAttributedString) input that represents a command line and parse it into two strings, the command cmd and the arguments args:
NSString* cmd = [[input mutableCopy] autorelease];
NSString* args = [[input mutableCopy] autorelease];
NSScanner* scanner = [NSScanner scannerWithString:[input string]];
[scanner scanUpToCharactersFromSet:[NSCharacterSet
whitespaceAndNewlineCharacterSet]
intoString:&cmd];
if (![scanner scanUpToString:#"magicstring666" intoString:&args]) args = #"";
That seems to work but the magic string is a pretty absurd hack. Also, I'm not at all sure I'm doing things right with the autoreleases.
ADDED: The solution should also be robust to initial whitespace. Also, I originally had the input string called both input and inStr. Sorry for that confusion.
ADDED: I believe one thing the above code gets right that the answers so far don't is that args should not have any initial whitespace.
NSString *cmd;
NSScanner *scanner = [NSScanner scannerWithString:[inStr string]];
[scanner scanUpToCharactersFromSet:[NSCharacterSet
whitespaceAndNewlineCharacterSet]
intoString:&cmd];
NSString *args = [[scanner string] substringFromIndex:[scanner scanLocation]];
Your autoreleases were OK, but allocating strings in the first place was unnecessary since NSScanner returns a new string by reference. Since NSScanner's charactersToBeSkipped include whitespace by default, it shouldn't get tripped up by initial whitespace.
Something like this?
int index = [input rangeOfString:#" "].location;
NSString *cmd = [input substringToIndex:index]);
NSString *args = [input substringFromIndex:index + 1]);
The autoreleases you mentioned don't actually make any sense, all you're doing is creating a mutable copy (NSMutableString *) that's properly autoreleased, but since you're casting it to an NSString * there's no practical difference then just saying cmd = input;. Even that's not needed for args though, since NSScanner will overwrite what's there anyway.
The rangeOfString: would work, if you want to go this route you can trim leading whitespaces using NSString's stringByTrimmingCharactersInSet method (I would also test to be sure both arguments and the command exist, or you'll get an error). What I would do though, is use the NSString componentsSeparatedByCharactersInSet: method. This will give you an NSArray object containing the command and each argument in a separate index.
If you want to expand the string into a full array of arguments like the input to 'main', you can use wordexp.
#import <wordexp.h>
+ (NSMutableArray*) splitArgumentString:(NSString*)strArgs
{
wordexp_t expandedArgs;
NSMutableArray *argArray = [NSMutableArray array];
if(strArgs != nil && 0 == wordexp([strArgs UTF8String], &expandedArgs, 0))
{
for(size_t i = 0; i < expandedArgs.we_wordc; ++i)
{
NSString arg = [NSString stringWithCString:expandedArgs.we_wordv[i] encoding:NSUTF8StringEncoding];
[argArray addObject:arg];
}
wordfree(&expandedArgs);
}
return argArray;
}