How to query an NSDictionary using an expression written in NSString? - ios

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?

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

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.

Related

How to remove conflicting return type?

I am dividing string and and storing it in splitArray and want to return it.
But I am getting conflicting array on the first line
- (NSArray *)subdividingString:(NSString *)string {
NSArray *splitArray = [string componentsSeparatedByString:#" "];
return splitArray;
}
First: there is nothing wrong with the code, but you are most likely having another issue (e.g. where you call subdividingString:).
Second: You shouldn't introduce a method that is exactly doing what another one is doing already. Just use
NSString *mystring = #"some string";
NSArray *chunks = [mystring componentsSeparatedByString:#" "];

How do I get values from a tiered .plist?

I've already looked at Parse Plist (NSString) into NSDictionary and deemed it to be not a duplicate, as that question and its answer do not address my concerns.
I have a .plist file in the file system structured like this:
The source code of this .plist file looks like this:
{
"My App" = {
"Side Panel" = {
Items = {
Price = "#123ABC";
};
};
};
}
I know how to get an item in the Root like this:
[[NSBundle mainBundle] pathForResource:#"filename" ofType:#"plist"];
NSDictionary *dict = [NSDictionary dictionaryWithContentsOfFile:path];
NSString value = [dict objectForKey:#"key"]);
But what if the structure is like mine, with tiered dictionaries? How do I get the value of Price?
I would like to do this all in one method, ideally like this:
Calling
NSString *hexString = [self getColorForKey:#"My App.Side Panel.Items.Price"];
Definition
- (NSString *) getColorForKey: (NSString *)key
{
NSArray *path = [key componentsSeparatedByString:#"."];
NSDictionary *colors = [NSDictionary dictionaryWithContentsOfFile:[[NSBundle mainBundle] pathForResource:#"Colors" ofType:#"plist"]];
NSString *color = #"#FFFFFF"; // white is our backup
// What do I put here to get the color?
return color;
}
Here's the solution that worked for me:
+ (NSString*) getHexColorForKey:(NSString*)key
{
NSArray *path = [key componentsSeparatedByString:#"."];
NSDictionary *colors = [NSDictionary dictionaryWithContentsOfFile:[[NSBundle mainBundle] pathForResource:#"Colors" ofType:#"plist"]];
NSString *color = #"#FFFFFF";
for (NSString *location in path) {
NSObject *subdict = colors[location];
if ([subdict isKindOfClass:[NSString class]])
{
color = (NSString*)subdict;
break;
}
else if ([subdict isKindOfClass:[NSDictionary class]])
{
colors = (NSDictionary*)subdict; // if it's a dictinoary, our color may be inside it
}
else
{
[SilverLog level:SilverLogLevelError message:#"Unexpected type of dictinoary entry: %#", [subdict class]];
return color;
}
}
return color;
}
where key is an NSString that matches /^[^.]+(\.[^.]+)*$/, meaning it looks like my targeted #"My App.Side Panel.Items.Price".
Yes I understand what you're looking to accomplish; thank you for the clarification. I will however add that the resources and advice I have written do provide the necessary information resolve your problem.
That said, the following gets your dictionary:
NSURL *plistURL = [[NSBundle mainBundle] URLForResource:#"Info" withExtension:#"plist"];
NSData *plistData = [NSData dataWithContentsOfURL:plistURL];
NSDictionary *tieredPlistData = [NSPropertyListSerialization propertyListWithData:plistData
options:kCFPropertyListImmutable
format:NULL
error:nil];
Then, if we're interested in the information contained in Items
NSDictionary *allItemsDictionary = tieredPlistData[#"My App"][#"Side Panel"][#"Items"];
Assuming that Items will contain a number of objects, you could use
NSArray *keys = [allItems allKeys];
for(NSString *key in keys){
NSString *colorValue = allItemsDictionary[key];
// do something with said color value and key
}
Or, if there is a single value you need, then just reference that key
NSString *colorForPriceText = allItemsDictionary[#"Price"];
But a few tips:
It's generally considered a better idea to keep frequently accessed values in code instead of a plist/file that is loaded at runtime.
That said, you wouldn't put your call to load from NSBundle in the same method you would use to query a specific value. In your example, every time you need a color, you end up re-accessing NSBundle and pile on unneeded memory allocations. One method would load the plist into an iVar NSDictionary and then that NSDictionary would be used separately by another method.

How do you find characters in an NSArray and return YES in Objective-C?

I am trying to find "Worf" inside the array of strings and then returning the BOOL of "YES" if I do find it, but if not, then return a BOOL of "NO".
So far this is what I have but it isn't working.
NSArray *myArray = #[#"Worf", #"son of Mogh", #"slayer of Gowron"];
NSString *myWorfString = #"Worf";
BOOL yesOrNo = NO;
for (NSString *task in myArray) {
if ([task isEqualToString: myWorfString]) {
yesOrNo = YES;
return yesOrNo;
} else {
return yesOrNo;
}
How would I input "return [myArray containsObject:myWolfString];" into my equation?
First of all you shouldn't compare objects with == unless you know what you are doing, since it compares memory addresses (where they are allocated) and not the contents. So two NSString* which points to two different instances which contain the same value are not equal according to ==.
You should use isEqualToString: or isEqualTo:.
In addition NSArray already contains this functionality, you don't need to reinvent the wheel:
NSArray *myArray = #[#"Worf", #"son of Mogh", #"slayer of Gowron"];
BOOL yesOrNo = [myArray containsObject:#"Worf"];
You have a logic error - you return immediately if the first item in the array is not #"Worf". While others have proposed solutions that will work, this one is most like your original code:
NSArray *myArray = #[#"Worf", #"son of Mogh", #"slayer of Gowron"];
NSString *myWorfString = #"Worf";
for (NSString *task in myArray)
if ([task isEqualToString: myWolfString])
return YES;
return NO;
Rather than go through the items one at a time, you can search the array concurrently, with lots of searches happening in parallel.
NSArray *myArray = #[#"Worf", #"son of Mogh", #"slayer of Gowron"];
NSString *myWorfString = #"Worf";
__block BOOL searchStringFound = NO;
[myArray enumerateObjectsWithOptions:NSEnumerationConcurrent usingBlock:^(NSString *string, NSUInteger idx, BOOL *stop) {
if ([string isEqualToString:myWorfString]) {
searchStringFound = YES;
*stop = YES;
}
}];
What's the advantage of this? It's faster, rather than search the array serially, you are doing it concurrently. Okay, so in this instance with such a small array it's unlikely to make much difference, but for larger arrays it could.
It's also a useful technique to use when looking for objects in collections. Because once you have found the object, you just set the stop parameter to YES to signal that no more enumerations need to be performed.
If you are looking for a substring:
- (BOOL)array:(NSArray *)array containsSubstring:(NSString *)substringToSearchFor
{
for (NSString *string in array) {
if (! [string isKindOfClass:[NSString class]])
continue; // skip objects that are not an NSString
if ([string rangeOfString:substringToSearchFor].location != NSNotFound)
return YES;
}
return NO;
}
Never compare strings with == use isEqualToString: instead.
You can just write:
return [myArray containsObject:myWolfString];
If you need to also check substring, you can use NSPredicate :
NSArray *myArray = #[#"Worf", #"son of Mogh", #"slayer of Gowron"];
NSString *myWolfString = #"Worf";
NSPredicate *pred = [NSPredicate predicateWithFormat:[NSString stringWithFormat:#"SELF CONTAINS [c] '%#'",myWolfString]];
NSArray *filteredArray = [myArray filteredArrayUsingPredicate:pred];
if ([filteredArray count])
{
NSLog(#"Index Of Object in main Array : %lu",(unsigned long)[myArray indexOfObject:filteredArray[0]]);
}
There are various ways you can find characters in NSArray and return YES in objective-c:-
1) By using NSPredicate
NSPredicate *pd=[NSPredicate predicateWithFormat:#"self MATCHES[CD] %#",myWorfString];
BOOL yesOrNo=[[NSNumber numberWithUnsignedLong:[[myArray filteredArrayUsingPredicate:pd]count]]boolValue];
2) Second using containsObjectapi
BOOL yesOrNo = [myArray containsObject:myWorfString];
3) Refer #EricS answer
NSArray *myArray = #[#"Worf", #"son of Mogh", #"slayer of Gowron"];
NSString *myWorfString = #"Worf";
BOOL yesOrNo = NO;
for (NSString *task in myArray)
{
if (([task isEqualToString: myWorfString]) || ([task rangeOfString:myWorfString].location != NSNotFound)) {
yesOrNo = YES;
break;
}
}
return yesOrNo;
Above code will return either array will contain as string or substring in array.

Replace String Between Two Strings

I have a serious problem about indexing in array. I've been working on this for 2 days and couldn't find answer yet.
I want to do that, search specific character in array then replace it with other string. I'm using replaceObjectAtIndex method but my code is doesn't work.
Here is my code;
NSString *commentText = commentTextView.text;
NSUInteger textLength = [commentText length];
NSString *atSign = #"#";
NSMutableArray *commentArray = [[NSMutableArray alloc] init];
[commentArray addObject:commentText];
for (int arrayCounter=1; arrayCounter<=textLength; arrayCounter++)
{
NSRange isRange = [commentText rangeOfString:atSign options:NSCaseInsensitiveSearch];
if(isRange.location != NSNotFound)
{
commentText = [commentText stringByReplacingOccurrencesOfString:commentText withString:atSign];
[_mentionsearch filtrele:_mentionText];
id<textSearchProtocol> delegate;
[delegate closeList:[[self.searchResult valueForKey:#"user_name"] objectAtIndex:indexPath.row]];
}
}
Ok, now i can find "#" sign in the text and i can match it. But this is the source of problem that, i can not replace any string with "#" sign. Here is the last part of code;
-(void)closeList
{
NSArray *arrayWithSign = [commentTextView.text componentsSeparatedByString:#" "];
NSMutableArray *arrayCopy = [arrayWithSign mutableCopy];
[arrayCopy replaceObjectAtIndex:isRange.location withObject:[NSString stringWithFormat:#"#%#",username]];
}
When im logging isRange.location value, it returns correct. But when im try to run, my application is crashing. So, i can not replacing [NSString stringWithFormat:#"#%#",username] parameter. How can i solve this problem?
If I understand correctly you want to change a substring in a string with a new string. In this case, why don't you use directly the stringByReplacingOccurrencesOfString method of NSString:
NSString *stringToBeChanged = #"...";
NSString *stringToBeChangedWith = #"...";
NSString *commentTextNew = [commentText stringByReplacingOccurrencesOfString:stringToBeChanged withString:stringToBeChangedWith];

All elements of nsarray are present in nsstring or not

I want to scan NSString (case insensitive) to check whether all elements of an array contain in that String or not ?
Eg.
NSString *string1 = #"Java is pass by value : lets see how?";
NSString *string2 = #"Java is OOP Language";
NSArray *array = [[NSArray alloc] initWithObjects:#"Java",#"Pass",#"value", nil];
In this case string1 pass the test as it contain all keyword from array (i.e. Java,Pass,Value).
So, how can i achieve this functionality ?
I don't test it for speed, and it will fail on case-sensitive strings, but here's another solution (just in case)
NSArray *components = [string1 componentsSeparatedByString:#" "];
NSSet *textSet = [NSSet setWithArray:components];
NSSet *keywordsSet = [NSSet setWithArray:array];
BOOL result = [keywordsSet isSubsetOfSet:textSet];
Keep in mind that componentsSeparatedByString will tokenize very silly, like "how?" instead of "how" you need.
BOOL containsAll = YES;
for (NSString *test in array) {
if ([string1 rangeOfString:test].location == NSNotFound) {
containsAll = NO;
break;
}
}

Resources