NSArray Equivalent of Map

IphoneObjective CCocoa TouchCocoaMacos

Iphone Problem Overview


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)

Iphone Solutions


Solution 1 - Iphone

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 2 - Iphone

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 3 - Iphone

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 4 - Iphone

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 5 - Iphone

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

Solution 6 - Iphone

I'm no Ruby expert so I'm not 100% confident I'm answering correctly, but based on the interpretation that 'map' does something to everything in the array and produces a new array with the results, I think what you probably want is something like:

NSMutableArray *replacementArray = [NSMutableArray array];

[existingArray enumerateObjectsUsingBlock:
    ^(NSDictionary *dictionary, NSUInteger idx, BOOL *stop)
    {
         NewObjectType *newObject = [something created from 'dictionary' somehow];
         [replacementArray addObject:newObject];
    }
];

So you're using the new support for 'blocks' (which are closures in more general parlance) in OS X 10.6/iOS 4.0 to perform the stuff in the block on everything in the array. You're choosing to do some operation and then add the result to a separate array.

If you're looking to support 10.5 or iOS 3.x, you probably want to look into putting the relevant code into the object and using makeObjectsPerformSelector: or, at worst, doing a manual iteration of the array using for(NSDictionary *dictionary in existingArray).

Solution 7 - Iphone

@implementation NSArray (BlockRockinBeats)

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

@end


##A slight improvement upon a couple of the answers posted.

  1. Checks for nil—you can use nil to remove objects as you're mapping
  2. Method name better reflects that the method doesn't modify the array it's called on
  3. This is more a style thing but I've IMO improved the argument names of the block
  4. Dot syntax for count

Solution 8 - Iphone

For Objective-C, I would add the ObjectiveSugar library to this list of answers: https://github.com/supermarin/ObjectiveSugar

Plus, its tagline is "ObjectiveC additions for humans. Ruby style." which should suit OP well ;-)

My most common use-case is mapping an dictionary returned by a server call to an array of simpler objects e.g. getting an NSArray of NSString IDs from your NSDictionary posts:

NSArray *postIds = [results map:^NSString*(NSDictionary* post) {
                       return [post objectForKey:@"post_id"];
                   }];

Solution 9 - Iphone

For Objective-C, I would add the Higher-Order-Functions to this list of answers: https://github.com/fanpyi/Higher-Order-Functions;

There is a JSON array studentJSONList like this:

[
    {"number":"100366","name":"Alice","age":14,"score":80,"gender":"female"},
    {"number":"100368","name":"Scarlett","age":15,"score":90,"gender":"female"},
    {"number":"100370","name":"Morgan","age":16,"score":69.5,"gender":"male"},
    {"number":"100359","name":"Taylor","age":14,"score":86,"gender":"female"},
    {"number":"100381","name":"John","age":17,"score":72,"gender":"male"}
]
//studentJSONList map to NSArray<Student *>
NSArray *students = [studentJSONList map:^id(id obj) {
return [[Student alloc]initWithDictionary:obj];
}];

// use reduce to get average score
NSNumber *sum = [students reduce:@0 combine:^id(id accumulator, id item) {
Student *std = (Student *)item;
return @([accumulator floatValue] + std.score);
}];
float averageScore = sum.floatValue/students.count;

// use filter to find all student of score greater than 70
NSArray *greaterthan = [students filter:^BOOL(id obj) {
Student *std = (Student *)obj;
return std.score > 70;
}];

//use contains check students whether contain the student named 'Alice'
BOOL contains = [students contains:^BOOL(id obj) {
Student *std = (Student *)obj;
return [std.name isEqual:@"Alice"];
}];

Solution 10 - Iphone

There is a special key-path operator for this: @unionOfObjects. Probably it replaced [collect] from previous versions.

Imagine a Transaction class with payee property:

NSArray *payees = [self.transactions valueForKeyPath:@"@unionOfObjects.payee"];

Apple docs on Array Operators in Key-Value coding.

Solution 11 - Iphone

Swift introduces a new map function.

Here is an example from the documentation:

let digitNames = [
    0: "Zero", 1: "One", 2: "Two",   3: "Three", 4: "Four",
    5: "Five", 6: "Six", 7: "Seven", 8: "Eight", 9: "Nine"
]
let numbers = [16, 58, 510]

let strings = numbers.map {
    (var number) -> String in
    var output = ""
    while number > 0 {
        output = digitNames[number % 10]! + output
        number /= 10
    }
    return output
}
// strings is inferred to be of type String[]
// its value is ["OneSix", "FiveEight", "FiveOneZero"]

The map function takes a closure which returns a value of any type and maps the existing values in the array to instances of this new type.

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestionStussaView Question on Stackoverflow
Solution 1 - IphoneJustin AndersonView Answer on Stackoverflow
Solution 2 - IphoneJeremyPView Answer on Stackoverflow
Solution 3 - IphonetothemarioView Answer on Stackoverflow
Solution 4 - IphoneSensefulView Answer on Stackoverflow
Solution 5 - IphoneSteven JiangView Answer on Stackoverflow
Solution 6 - IphoneTommyView Answer on Stackoverflow
Solution 7 - Iphonejames_womackView Answer on Stackoverflow
Solution 8 - IphoneLordParsleyView Answer on Stackoverflow
Solution 9 - Iphone范陆离View Answer on Stackoverflow
Solution 10 - IphonekelinView Answer on Stackoverflow
Solution 11 - IphoneSensefulView Answer on Stackoverflow