0

I want to be able to run the following hypothetical function called evaluateExpression:on: and get "John" as answer.

NSDictionary *dict = @{"result": @[@{@"name": @"John"}, @{@"name": @"Mary"}]};
NSString *expression = @"response['result'][0]['name']";
NSString *answer = [self evaluateExpression: expression on: dict];

Is this possible?

Vlad
  • 8,038
  • 14
  • 60
  • 92

2 Answers2

2

There's an NSObject category that extends valueForKeyPath to give valueForKeyPathWithIndexes. It lets you write this:

NSDictionary *dict = @{@"result": @[@{@"name": @"John"}, @{@"name": @"Mary"}]};
NSString *path = @"result[0].name";
NSString *answer = [dict valueForKeyPathWithIndexes:path];
XCTAssertEqualStrings(answer, @"John");

The category is by psy, here: Getting array elements with valueForKeyPath

@interface NSObject (ValueForKeyPathWithIndexes)
   -(id)valueForKeyPathWithIndexes:(NSString*)fullPath;
@end


#import "NSObject+ValueForKeyPathWithIndexes.h"    
@implementation NSObject (ValueForKeyPathWithIndexes)

-(id)valueForKeyPathWithIndexes:(NSString*)fullPath
{
    NSRange testrange = [fullPath rangeOfString:@"["];
    if (testrange.location == NSNotFound)
        return [self valueForKeyPath:fullPath];

    NSArray* parts = [fullPath componentsSeparatedByString:@"."];
    id currentObj = self;
    for (NSString* part in parts)
    {
        NSRange range1 = [part rangeOfString:@"["];
        if (range1.location == NSNotFound)          
        {
            currentObj = [currentObj valueForKey:part];
        }
        else
        {
            NSString* arrayKey = [part substringToIndex:range1.location];
            int index = [[[part substringToIndex:part.length-1] substringFromIndex:range1.location+1] intValue];
            currentObj = [[currentObj valueForKey:arrayKey] objectAtIndex:index];
        }
    }
    return currentObj;
}
@end

Plain old valueForKeyPath will get you close, but not exactly what you asked for. It may be useful in this form though:

NSDictionary *dict = @{@"result": @[@{@"name": @"John"}, @{@"name": @"Mary"}]};
NSString *path = @"result.name";
NSString *answer = [dict valueForKeyPath:path];
XCTAssertEqualObjects(answer, (@[@"John", @"Mary"]));
Community
  • 1
  • 1
Ewan Mellor
  • 6,747
  • 1
  • 24
  • 39
  • Thank you so much! As I guessed, there's no simple way around it but this solution looks like it would work! – Vlad Jan 05 '16 at 20:46
0

After spending quite a while trying to tackle this problem in the most efficient way, here's another take I came up with: Use javascript.

The above approach posted by Ewan definitely takes care of the problem, but I had some additional custom features I wanted to add, and objective-c approach became too complex very quickly. I ended up writing eval code in javascript and integrating it into objective-c using JavascriptCore. With that, it becomes as simple as one line of eval() call.

Vlad
  • 8,038
  • 14
  • 60
  • 92