NSArray Equivalent of Map

49,809

Solution 1

Update: If you're using Swift, see map.


BlocksKit is an option:

NSArray *new = [stringArray bk_map:^id(NSString *obj) { 
    return [obj stringByAppendingString:@".png"]; 
}];

Underscore is another option. There is a map function, here is an example from the website:

NSArray *tweets = Underscore.array(results)
    // Let's make sure that we only operate on NSDictionaries, you never
    // know with these APIs ;-)
    .filter(Underscore.isDictionary)
    // Remove all tweets that are in English
    .reject(^BOOL (NSDictionary *tweet) {
        return [tweet[@"iso_language_code"] isEqualToString:@"en"];
    })
    // Create a simple string representation for every tweet
    .map(^NSString *(NSDictionary *tweet) {
        NSString *name = tweet[@"from_user_name"];
        NSString *text = tweet[@"text"];

        return [NSString stringWithFormat:@"%@: %@", name, text];
    })
    .unwrap;

Solution 2

It only saves a couple lines, but I use a category on NSArray. You need to ensure your block never returns nil, but other than that it's a time saver for cases where -[NSArray valueForKey:] won't work.

@interface NSArray (Map)

- (NSArray *)mapObjectsUsingBlock:(id (^)(id obj, NSUInteger idx))block;

@end

@implementation NSArray (Map)

- (NSArray *)mapObjectsUsingBlock:(id (^)(id obj, NSUInteger idx))block {
    NSMutableArray *result = [NSMutableArray arrayWithCapacity:[self count]];
    [self enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) {
        [result addObject:block(obj, idx)];
    }];
    return result;
}

@end

Usage is much like -[NSArray enumerateObjectsWithBlock:]:

NSArray *people = @[
                     @{ @"name": @"Bob", @"city": @"Boston" },
                     @{ @"name": @"Rob", @"city": @"Cambridge" },
                     @{ @"name": @"Robert", @"city": @"Somerville" }
                  ];
// per the original question
NSArray *names = [people mapObjectsUsingBlock:^(id obj, NSUInteger idx) {
    return obj[@"name"];
}];
// (Bob, Rob, Robert)

// you can do just about anything in a block
NSArray *fancyNames = [people mapObjectsUsingBlock:^(id obj, NSUInteger idx) {
    return [NSString stringWithFormat:@"%@ of %@", obj[@"name"], obj[@"city"]];
}];
// (Bob of Boston, Rob of Cambridge, Robert of Somerville)

Solution 3

I've no idea what that bit of Ruby does but I think you are looking for NSArray's implementation of -valueForKey:. This sends -valueForKey: to every element of the array and returns an array of the results. If the elements in the receiving array are NSDictionaries, -valueForKey: is nearly the same as -objectForKey:. It will work as long as the key doesn't start with an @

Solution 4

To summarize all other answers:

Ruby (as in the question):

array.map{|o| o.name}

Obj-C (with valueForKey):

[array valueForKey:@"name"];

Obj-C (with valueForKeyPath, see KVC Collection Operators):

[array valueForKeyPath:@"[collect].name"];

Obj-C (with enumerateObjectsUsingBlock):

NSMutableArray *newArray = [NSMutableArray array];
[array enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) {
     [newArray addObject:[obj name]];
}];

Swift (with map, see closures)

array.map { $0.name }

And, there are a couple of libraries that allow you to handle arrays in a more functional way. CocoaPods is recommended to install other libraries.

Solution 5

I think valueForKeyPath is a good choice.

Sit below has very cool examples. Hopes it is helpful.

http://kickingbear.com/blog/archives/9

Some example:

NSArray *names = [allEmployees valueForKeyPath: @"[collect].{daysOff<10}.name"];
NSArray *albumCovers = [records valueForKeyPath:@"[collect].{artist like 'Bon Iver'}.<NSUnarchiveFromDataTransformerName>.albumCoverImageData"];
Share:
49,809
Stussa
Author by

Stussa

Updated on October 25, 2020

Comments

  • Stussa
    Stussa over 3 years

    Given an NSArray of NSDictionary objects (containing similar objects and keys) is it possible to write perform a map to an array of specified key? For example, in Ruby it can be done with:

    array.map(&:name)
    
  • Stussa
    Stussa almost 13 years
    It does exactly that! Thanks so much!
  • DHamrick
    DHamrick about 12 years
    Don't forget to check for nil on the block you are passing in to mapObjectsUsingBlock. It will currently crash if you pass in a nil block.
  • Manav
    Manav over 11 years
    mikeash.com/pyblog/friday-qa-2009-08-14-practical-blocks.htm‌​l contains a better (syntactic) variant of the above, IMHO.
  • James Moore
    James Moore over 11 years
    This isn't what map does. Ruby's map takes a block, not just a single method call. This particular case only works because the block is a single call to a single method. Answer from @JustinAnderson is much closer to what you can do in Ruby.
  • matt
    matt over 11 years
    Actually, this solution is far more flexible than you might suppose, because valueForKey: works by calling the corresponding getter. Thus, if you know in advance the various things you might want an object to do, you can inject the needed getters into it (e.g. with a category) and then use NSArray's valueForKey: as a way of passing a call to a particular getter through the array to each object and getting an array of the results.
  • abbood
    abbood about 11 years
    @Justin Anderson can you please include in your answer a sample usage of the above?
  • Mark Amery
    Mark Amery almost 11 years
    +1; this is the cleanest way to handle the particular (fairly common) use case that the question asker described, even if it's not a fully flexible map function as asked for in the question title. For people who genuinely need a map method, use the category from Justin Anderson's answer instead.
  • ArpitM
    ArpitM almost 11 years
    You're returning a MutableArray back. Is it better practice to do return [result copy]?
  • Justin Anderson
    Justin Anderson almost 11 years
    @MrRogers result copy would potentially reduce memory usage, but there's a time cost to copying the array. For large arrays that could be pretty expensive. The other benefit of making the response truly immutable is that you would get runtime exceptions if you tried to mutate it, but the return type of the method is immutable, so you'll get build warnings either way if you try to treat the response as mutable.
  • ArpitM
    ArpitM almost 11 years
    @JustinAnderson copy returns an immutable array. Isn't that what one would expect if that's the return type of the method? There is a speed hit though. Thanks!
  • Justin Anderson
    Justin Anderson almost 11 years
    @MrRogers There's no expectation or implication about the actual mutability of the returned object, just that you should treat it as though it is immutable. A return type of NSArray is just a promise that a response will be an NSArray or an instance of a subclass of it. It's like how you need to treat an id response as an NSObject until you've tested it with respondsToSelector and the like before you can expect to treat it as something more specific.
  • abbood
    abbood over 10 years
    follow up question here: what to do when I would like to skip an object in the internal enumerator
  • Justin Anderson
    Justin Anderson over 10 years
    @abbood If you're looking to keep a 1-1 mapping of the indexes from your original array to your mapped array, I'd suggest returning NSNull. That's the Obj-C object for representing nil in collection classes. NSNull is pretty uncommon and a hassle to use though, since Obj-C doesn't do unboxing, so NSNull != nil. But if you want to just filter some items out of the array, you could modify mapObjectsUsingBlock to check for nil responses from the block and skip them.
  • Rudolf Adamkovič
    Rudolf Adamkovič about 10 years
    IMHO, using dot syntax for Objective-C methods is not a good idea. This is not Ruby.
  • 11684
    11684 almost 10 years
    @RudolfAdamkovic While I do agree with you in principle, doing this with bracket notation would be less readable.
  • Gregarious
    Gregarious over 9 years
    Unless you're doing functional operations all over the place, it's a much better idea to use the NSArray's standard mapObjectsUsingBlock: or valueForKey: methods (suggested in the answers below). Avoiding a few more characters of "ugly" Objective-C syntax doesn't justify adding a Cocoapods dependency.
  • tothemario
    tothemario about 9 years
    You could use a simpler example, like [1,2,3].map {$0 * 2} //=> [2,4,6]. AND, the question is about Obj-C NSArray, not Swift ;)
  • tothemario
    tothemario about 9 years
    I keep coming back to this answer to remember that name. I'm so used to "map" in other programming languages that I never can remember "valueForKey"
  • mcfedr
    mcfedr about 8 years
    mapObjectsUsingBlock: isn't a standard function, but an extension suggested by another answer
  • J2N
    J2N about 8 years
    Agreed with @Gregarious, we should never go against coding standards just to make it "prettier"
  • Muhammad Aamir Ali
    Muhammad Aamir Ali almost 8 years
    Excellent! Thank you so much
  • Pang
    Pang almost 6 years
    Page linked from "Underscore" in answer seems not related to Objective-C any more.
  • uchuugaka
    uchuugaka almost 5 years
    Use __block on variables that declared outside the block but need to be accessed inside the block.
  • Justin Anderson
    Justin Anderson almost 5 years
    @uchuugaka __block doesn't apply here. There are no outside variables being captured and no state to persist beyond the block's return value.