string with format as argument for method (objective-c) - objective-c

The [NSString stringWithFormat:]; can take multiple arguments even though it's declared as NSString not NSArray and there's only one colon.
How can I have this for my own method, which is like a replacement for NSLog that writes to a text field so it's used often and I don't want to keep adding more square brackets.

Use an ellipsis after your argument name:
(NSNumber *) addValues:(int) count, ...;
http://developer.apple.com/library/mac/#documentation/Cocoa/Conceptual/ObjectiveC/Articles/ocDefiningClasses.html
You then need to use va_list and va_start to iterate through the arguments provided:
- (NSNumber *) addValues:(int) count, ...
{
va_list args;
va_start(args, count);
NSNumber *value;
double retval;
for( int i = 0; i < count; i++ )
{
value = va_arg(args, NSNumber *);
retval += [value doubleValue];
}
va_end(args);
return [NSNumber numberWithDouble:retval];
}
Example from: http://numbergrinder.com/node/35
Note that this is a built-in C functionality, not part of Objective-C specifically; there is a good explanation of the va_arg usage here:
http://publications.gbdirect.co.uk/c_book/chapter9/stdarg.html

Related

Sorting a NSMutableArray of strings: 'unrecognized selector sent to instance'

I have made a program that gets some strings in input from the user (it stops typing "exit"), puts them in a NSMutableArray and sort them:
#import <Foundation/Foundation.h>
#import <Cocoa/Cocoa.h>
NSInteger compare( id id1, id id2, void* context)
{
NSString* str1=[id1 stringValue], *str2=[id2 stringValue]; /* ****** exception here */
const char* buf1, *buf2;
buf1=[str1 UTF8String];
buf2=[str2 UTF8String];
if(strcmp(buf1,buf2)<0)
return NSOrderedAscending;
else
return NSOrderedDescending;
}
int main (int argc, const char * argv[])
{
NSAutoreleasePool* pool=[[NSAutoreleasePool alloc]init];
NSMutableArray* array=[[NSMutableArray alloc]init];
NSString* str=#"";
char buffer[100];
NSLog(#"Type a sequence of strings, ending with 'exit', the strings will be sorted in alphabethical order");
while(! [str isEqualToString:#"exit\n"])
{
fgets(buffer,100,stdin);
str=[NSString stringWithUTF8String: buffer];
[array addObject: str];
}
[array sortUsingFunction: compare context: NULL];
[array release];
[pool drain];
return 0;
}
But I get an exception in the line that I have marked with "**", it says:
2012-05-09 00:10:14.502 CLITest[1029:903] -[NSCFString stringValue]: unrecognized selector sent to instance 0x10010cbf0
It seems like it doesn't take the string value from the id.Why? How should I change the comparator method?
id1 and id2 are actually NSStrings, even if they are casted to id and passed to your function (this approach is useful because you can define your compare function in order to sort custom objects).
It doesn't make sense to call stringValue on a string (and, in fact, NSString do not have a member function named stringValue).
To work with strings just cast back to NSStrings.
NSString *s1 = (NSString*) id1;
NSString *s2 = (NSString*) id2;
This is not strictly necessary (you can even work directly with id1 and id2) but casting will avoid you to get compiler warnings (also it's a clearer code).

Add objects in a NSMutableArray declared in another function

Ideally, I would like to make a function add objects in a NSMutableArray, and then do whatever I want with this array in another function.
Here is what I've tried to do lately, of course it doesn't work but it gives you an idea of what I want to do:
- (void)someThing
{
(...)
NSMutableArray *arrayOfThings = [[NSMutableArray alloc] init];
while (theObject = [aNSEnumerator nextObject]) {
const char *theObject_fixed = [theObject UTF8String];
function_something(theObject_fixed);
}
// do something with arrayOfThings
}
void function_something(const char *file)
{
(...)
unsigned int *p = memmem(buffer, fileLen, bytes, 4);
NSMutableString *aString = [[NSMutableString alloc] initWithCapacity:48];
unsigned long off_to_string = 0x10 + 4 + ((void *)p) - ((void *)buffer);
for (unsigned long c = off_to_string; c<off_to_string+0x30; c++)
{
[aString appendFormat:#"%.2x", (int)buffer[c]];
}
NSLog(#"%s: %#", file, aString);
[arrayOfThings addObject:[aString copy]];
free(buffer);
There are two ways to go about this:
The first requires only a slight modification to your code will allow you to do what you want:
In the funciton someThing pass the mutable array as an additional parameter.
function_something(theObject_fixed, arrayOfThings);
Then change function_something to accept that parameter.
void function_something(const char *file, NSMutableArray *arrayOfThings) {
// Code remains the same
}
The other and in my opinion better solution would be for the function_something to return the fixed string as an NSString object and let someThing do the adding to the mutable array.
So we get something like this in someThing:
...
NSString *aString = function_something(theObject_fixed);
[arrayOfThings addObject:aString];
And then a redefined *function_something*:
NSString* function_something(const char *file) {
...
return [aString autorelease];
}
By the way, your code is leaking memory. Be careful with you retain/release/autorelease.

Objective-C Iterating through an NSString to get characters

I have this function:
void myFunc(NSString* data) {
NSMutableArray *instrs = [[NSMutableArray alloc] initWithCapacity:[data length]];
for (int i=0; i < [data length]; i++) {
unichar c = [data characterAtIndex:i];
[instrs addObject:c];
}
NSEnumerator *e = [instrs objectEnumerator];
id inst;
while (inst = [e nextObject]) {
NSLog("%i\n", inst);
}
}
I think it fails at [instrs addObject:c]. It's purpose is to iterate through the hexadecimal numbers of an NSString. What causes this code to fail?
A unichar is not an object; it's an integer type.
NSMutableArray can only hold objects.
If you really want to put it into an NSMutableArray, you could wrap the integer value in an NSNumber object: [instrs addObject:[NSNumber numberWithInt:c]];
But, what's the point of stuffing the values into an array in the first place? You know how to iterate through the string and get the characters, why put them into an array just to iterate through them again?
Also note that:
the "%i" NSLog format expects an integer; you can't pass it an object
for hexadecimal output, you want "%x", not "%i"
If the function is only meant to display the characters as hexadecimal values, you could use:
void myFunc(NSString* data)
{
NSUInteger len = [data length];
unichar *buffer = calloc(len, sizeof(unichar));
if (!buffer) return;
[data getCharacters:buffer range:NSMakeRange(0, len)];
for (NSUInteger i = 0; i < len; i++)
NSLog(#"%04x", (unsigned) buffer[i]);
free(buffer);
}
This is just a little bit more efficient than your approach (also, in your approach you never release the instrs array, so it will leak in a non-garbage-collected environment).
If the string contains hexadecimal numbers, then you will want to repeatedly use an NSScanner's scanHexInt: method until it returns NO.
void myFunc(NSString* data)
{
NSScanner *scanner = [[NSScanner alloc] initWithString:data];
unsigned number;
while ([scanner scanHexInt:&number])
NSLog(#"%u", number);
[scanner release];
}

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

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.