My goal is to have an array that contains all filenames of a specific extension, but without the extension.
There's an elegant solution to get all filenames of a specific extension using a predicate filter and instructions on how to split a path into filename and extension, but to combine them I would have to write a loop (not terrible, but not elegant either).
Is there a way with Objective-C (may be similar to the predicate mechanism) to apply some function to every element of an array and put the results in a second array, like the transform
algorithm of the C++ STL does?
What I'd like to write:
// let's pretend 'anArray' was filled by querying the filesystem and not hardcoded
NSArray* anArray = [[NSArray alloc] initWithObjects:@"one.ext", @"two.ext", nil];
// that's what I liked to write (pseudo code)
NSArray* transformed = [anArray transform: stringByDeletingPathExtension];
// Yuji's answer below proposes this (which may be as close as you can get
// to my wish with Objective C)
NSArray* transformed = [anArray my_arrayByApplyingBlock:^(id x){
return [x stringByDeletingPathExtension];
}];
Actually, there is a very simple way. It's been around since 2003 and it is poorly named.
NSArray *array = [NSArray arrayWithObjects:@"one.ext", @"two.ext", nil];
// short solution
NSArray *transformed = [array valueForKey:@"stringByDeletingPathExtension"];
// long solution (more robust against refactoring)
NSString *key = NSStringFromSelector(@selector(stringByDeletingPathExtension));
NSArray *transformed = [array valueForKey:key];
Both produce the output:
(
one,
two
)
That's a topic called Higher Order Messaging in Cocoa, and developed by many people on the web. Start from here and try googling more. They add a category method to NSArray
so that you can do
NSArray*transformed=[[anArray map] stringByDeletingPathExtension];
The idea is as follows:
[anArray map]
creates a temporary object (say hom
) hom
receives the message stringByDeletingPathExtension
hom
re-sends the message to all the elements of anArray
hom
collects the results and returns the resulting array.If you just want a quick transform, I would define a category method:
@interface NSArray (myTransformingAddition)
-(NSArray*)my_arrayByApplyingBlock:(id(^)(id))block;
@end
@implementation NSArray (myTransformingAddition)
-(NSArray*)my_arrayByApplyingBlock:(id(^)(id))block{
NSMutableArray*result=[NSMutableArray array];
for(id x in self){
[result addObject:block(x)];
}
return result;
}
@end
Then you can do
NSArray* transformed=[anArray my_arrayByApplyingBlock:^id(id x){return [x stringByDeletingPathExtension];}];
Note the construct ^ return-type (arguments) { ...}
which creates a block. The return-type can be omitted, and clang
is quite smart on guessing it, but gcc
is quite strict about it and needs to be specified sometime. (In this case, it's guessed from the return
statement which has [x stringBy...]
which returns an NSString*
. So GCC guesses the return type of the block to be NSString*
instead of id
, which GCC thinks is incompatible, thus comes the error. )
On OS X Leopard or iOS 3, you can use PLBlocks to support blocks. My personal subjective opinion is that people who care about new software typically upgrade to the newest OS, so supporting the latest OS should be just fine; supporting an older OS won't increase your customer by a factor of two...
THAT SAID, there's already a nice open-source framework which does all I said above. See the discussion here, and especially the FunctionalKit linked there.
More addition: it's in fact easy to realize your pseudocode [array transform:stringByDeletingPathExtension]
.
@interface NSArray (myTransformingAddition)
-(NSArray*)my_transformUsingSelector:(SEL)sel;
@end
@implementation NSArray (myTransformingAddition)
-(NSArray*)my_transformUsingSelector:(SEL)sel;{
NSMutableArray*result=[NSMutableArray array];
for(id x in self){
[result addObject:[x performSelector:sel withObject:nil]];
}
return result;
}
@end
Then you can use it as follows:
NSArray*transformed=[array my_transformUsingSelector:@selector(stringByDeletingPathExtension)];
However I don't like it so much; you need to have a method already defined on the object in the array to use this method. For example, if NSString
doesn't have the operation what you want to do as a method, what would you do in this case? You need to first add it to NSString
via a category:
@interface NSString (myHack)
-(NSString*)my_NiceTransformation;
@end
@implementation NSString (myHack)
-(NSString*)my_NiceTransformation{
... computes the return value from self ...
return something;
}
@end
Then you can use
NSArray*transformed=[array my_transformUsingSelector:@selector(my_NiceTransformation)];
But it tends to be very verbose, because you need to define the method in other places first. I prefer providing what I want to operate directly at the call site, as in
NSArray*transformed=[array my_arrayByApplyingBlock:^id(id x){
... computes the return value from x ...
return something;
}];
Finally, never add category methods which do not start with a prefix like my_
or whatever. For example, in the future Apple might provide a nice method called transform
which does exactly what you want. But if you have a method called transform
in the category already, that will lead to an undefined behavior. In fact, it can happen that there is a private method by Apple already in the class.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With