Filtering a large NSArray with NSPredicate - ios

I have an array containing 170k strings (words in a dictionary), and a string, looking something like "glapplega". I'm trying to extract the word "apple" from the string (with "apple" being a word in the array). I also need to make sure that the extracted word is at least 3 characters. The code I have right now is the following:
NSPredicate *wordPredicate = [NSPredicate predicateWithFormat:#"'%#' contains[cd] SELF", string];
NSPredicate *lengthPredicate = [NSPredicate predicateWithFormat:#"SELF.length > 2"];
NSPredicate *predicate = [NSCompoundPredicate andPredicateWithSubpredicates:#[wordPredicate, lengthPredicate]];
return [_words filteredArrayUsingPredicate:lengthPredicate];
The length predicate works on it's own, but the word predicate does not (it returns an empty array, despite "apple" being a word in the array).
I suspect that there might be a problem with using SELF as the right expression in the predicate, as all the examples I found have it as the left expression, although I have no way of confirming this.
Edit: I'm aware that this can likely be accomplished with regexs (as described here), but was hoping there would be a way around this, as regexs can be slow with such a large dataset.

Solving this problem is easy if you iterate the array yourself using a block predicate. At some point a formatted NSPredicate would have to boil down to this, so there shouldn't be much of a performance hit. -[NSString rangeOfString:] can be used to test for inclusion of the string.
return [_words filteredArrayUsingPredicate:[NSPredicate predicateWithBlock:^BOOL (id evaluatedString, NSDictionary *bindings) {
return string.length > 2 && [string rangeOfString:evaluatedString].location != NSNotFound;
}]];

You know what your above assumption and predicate is perfectly valid. The only thing that you have been doing wrong is quotations. Reformat your predicate and make it like this,
NSArray * array = #[#"Apple", #"lega", #"foo", #"bar"];
NSString *string = #"glapplega";
NSPredicate *predicate = [NSPredicate predicateWithFormat:#"%# contains[cd] SELF and SELF.length > 2", string];
NSLog(#"%#",[array filteredArrayUsingPredicate:predicate]);
(
Apple,
lega
)
When you specify the format and supply the string to the format, the predicate places the quotes by itself. So, you have been mistaking over here.

#define rchar (rand() % ('z'-'a') + 'a')
- (void)applicationDidFinishLaunching:(NSNotification *)aNotification
{
NSMutableArray * mar = [NSMutableArray new];
for (int i = 0; i<170000; i++)
{
NSString * str = [NSString stringWithFormat:#"%c%c%c%c",rchar, rchar, rchar, rchar];
[mar addObject:str];
}
NSString * bigStr = #"asdfghjkl;loiuytrdcvcdrtgvfrtghvcftyghvfghcfdtyjghvncdfjtygmvcnfhjghjkgfhdgsxgrecrvtbkunhlmnhubkujvytchrtxgrecdjvbyhnkbjgcfhvyjhbghnkbjchgdfvbghnukbytvjycterwxrzewxcevfbjnkmjohgytreytwexkutckhtdtcfhvjgkjmhgcjhewwzsserdp9dlkuydssqwsxdchvggjhmgbj";
NSDate *start = [NSDate date];
NSArray * marFiltered = [mar filteredArrayUsingPredicate:[NSPredicate predicateWithBlock:^BOOL(id evaluatedObject, NSDictionary *bindings) {
return [bigStr rangeOfString:evaluatedObject].length>2;
}]];
NSLog(#"found %lu items in %f seconds", (unsigned long)[marFiltered count], -[start timeIntervalSinceNow]);
}
output:
2014-05-11 09:09:53.048 170k[89396:303] found 85 items in 0.542431 seconds

You can try two options for defining the predicate. A format string and a block. Here is a bit of code that demonstrates both. I've played a bit with both and can share that the performance is the same. I've only had the patience to run it with a max value of INT32_MAX/2 (a lot of items).
Here goes. Hope this clarifies and helps:
NSString* searchString = #"AB0";
NSUInteger capacity = 1000000;
NSMutableArray* array = [NSMutableArray array];
NSLog(#"Fillling array with %lu UUIDS. Be patient.", (unsigned long)capacity);
NSUInteger batch = 0;
for ( NSUInteger i = 0; i < capacity; i++ ) {
[array setObject:[[NSUUID UUID] UUIDString] atIndexedSubscript:i];
if (i != 0 && i % (capacity / 10) == 0 ) {
NSLog(#"Completed %lu%%", (unsigned long)++batch * 10);
}
}
NSLog(#"Done.");
NSPredicate* formatPredicate = [NSPredicate predicateWithFormat:#"SELF contains[cd] %# AND SELF.length > 3", searchString];
NSLog(#"Filtering with predicate: %#", formatPredicate);
NSArray* formatArray = [array filteredArrayUsingPredicate:formatPredicate];
NSLog(#"Got %lu results.", formatArray.count);
NSPredicate* blockPredicate = [NSPredicate predicateWithBlock:^BOOL(id evaluatedObject, NSDictionary *bindings) {
NSString* theString = evaluatedObject;
return theString.length > 3 && [theString rangeOfString:searchString].location != NSNotFound;
}];
NSLog(#"Filtering with predicate: %#", blockPredicate);
NSArray* blockArray = [array filteredArrayUsingPredicate:blockPredicate];
NSLog(#"Got %lu results.", blockArray.count);
PS: I wouldn't run this on a phone if you are using big numbers line INT32_MAX :)

Related

Obj-C - Filter NSMutableArray with multiple objects?

I'm currently filtering an NSMutableArray (neighbourUIDs) with a value, but I now want to filter the same array with a second string as well, e.g.
[predicateString appendFormat:#"SELF.neighbourhood ==[c] '%1$#' OR %#", neighbourUIDs[i], hq];
That being said, when I attempt to write the above, XCode throws me the following error:
Cannot mix positional and non-positional arguments in format string
Any idea how I can accomplish this? See current code below:
ViewController.m
NSMutableArray *neighbourUIDs = [self.currentUser valueForKey:#"neighbourhood"];
NSMutableString *predicateString = [NSMutableString string];
for (NSInteger i = 0; i < [neighbourUIDs count]; i++) {
if (i != 0) {
[predicateString appendString:#" OR "];
}
NSString *hq = #"Headquarters";
[predicateString appendFormat:#"SELF.neighbourhood ==[c] '%1$#'", neighbourUIDs[i]];
}
NSPredicate *predicate = [NSPredicate predicateWithFormat:predicateString];
self.closeByNeighbours = [[self.neighbourData filteredArrayUsingPredicate:predicate] mutableCopy];

Obj-c - Filter array to return dictionaries in which a key contains values listed in NSMutableArray?

I have an array (allFriends) which returns a number of dictionaries (data for about 50 people). Each dictionary contains the key 'uid'. I want to filter allFriends so that all dictionaries in which uid contains the numbers 1,2,3, or 4 are returned.
How might I accomplish this? The numbers I want to filter with are returned in an array, and are grabbed like so:
NSMutableArray *friendUIDs = [self.friendData valueForKey:#"uid"];
This returns the data as: "1, 2, 3, 4"
No matter what I try however (thought NSPredicate would be the way to go), my code doesn't seem to want to let me filter with multiple values separated by a comma?
NSArray *filteredData = [self.allFriends filteredArrayUsingPredicate:[NSPredicate predicateWithFormat:#"(uid contains[c] %#)", friendUIDs]];
Hope I worded this correctly.
Your final predicate should look something like this:
NSPredicate *predicate = [NSPredicate predicateWithFormat:#"SELF.uid contains[c] %# OR SELF.uid contains[c] %#", #"1",#"2"];
If I understand the question correctly, this should work:
NSMutableArray *friendUIDs = [self.friendData valueForKey:#"uid"];
NSMutableString *predicateString = [NSMutableString string];
for (NSInteger i = 0; i < [friendUIDs count]; i++) {
if (i != 0) {
[predicateString appendString:#" OR "];
}
[predicateString appendFormat:#"SELF.uid contains[c] '%1$#'", friendUIDs[i]];
}
NSPredicate *predicate = [NSPredicate predicateWithFormat:predicateString];
NSArray *filteredData = [allFriends filteredArrayUsingPredicate:predicate];

Searching an Array of Strings in Array of Dictionaries

I have an array of combinations which needs to be searched in another array of dictionaries
Array Of Dictionaries is as follows:
self.listOfAllContacts
({
name = "William";
recordId = 541;
},
{
name = "Soan";
recordId = 541;
},
{
name = "kamal";
recordId = 541;
},
{
name = "Elisia";
recordId = 541;
},
{
name = "Ben";
recordId = 541;
},
{
name = "Loki";
recordId = 541;
},
{
name = "Fraser";
recordId = 541;
});
Array Of Combinations are as follows : array named as
self.arrayOfSearchCombinationsFormed
<__NSArrayM 0x1702518b0>(
ABCD,
JK,
AND,
MIKE,
ELI,
STEV,
FRASE,
WIILIA
)
Present Code in work:
self.filteredContacts = [[NSMutableArray alloc] init];
NSArray *arrayToTraversed = [[NSArray alloc] initWithArray:self.arrayOfSearchCombinationsFormed];
for(NSString *combination in arrayToTraversed){
NSPredicate *predicateInsideLoop = [NSPredicate predicateWithFormat:#"name CONTAINS[cd] %#", combination];
NSArray *filteredContactByName = [self.listOfAllContacts filteredArrayUsingPredicate:predicateInsideLoop];
if([filteredContactByName count]>0){
[self.filteredContacts addObjectsFromArray:filteredContactByName];
}
else{
[self.arrayOfSearchCombinationsFormed removeObject:combination];
}
}
Presently this solution is inefficient and consuming a lot of memory.
Any help would be appreciated.
Also note that any combination not found in the dictionary needs to be removed from the combinations array.
So my question is that i want the most efficient way of searching the names in terms of memory allocation. So that it uses minimum memory.
It might be helpful to use (NSPredicate*)predicateWithBlock: method to speed up searching.
Suppose you have a keys array and a source array, you want to filter the source array with the keys array.
NSArray *keysArray = #[#"1",#"2",#"3"];
NSArray *sourceArray = #[#"12",#"2",#"3",#"1",#"2"];
For the first object #"12" in sourceArray, looking at the keysArray, since #"12" contains #"1", you can stop filtering and keep the first object of both arrays. But original code uses #"1" to filter the sourceArray, result is #"12" and #"1", each element needs to be checked.
You can refer to the below code:
- (void)searchWithBlock:(NSArray*)keysArray
{
NSDate *beginDate = [NSDate date];
NSMutableSet *keySet = [NSMutableSet set];
NSPredicate *intersectPredicate = [NSPredicate predicateWithBlock:^BOOL(id evaluatedObject, NSDictionary *bindings) {
for (NSString *str in keysArray) {
NSString *name = evaluatedObject[#"name"];
NSRange r = [name rangeOfString:str options:NSCaseInsensitiveSearch];
if (r.location != NSNotFound) {
[keySet addObject:str];
return true;
}
}
return false;
}];
NSArray *intersect = [self.listOfAllContacts filteredArrayUsingPredicate:intersectPredicate];
self.filteredContacts = [[NSMutableArray alloc] initWithArray:intersect];
self.arrayOfSearchCombinationsFormed = [NSMutableArray arrayWithArray:[keySet allObjects]];
NSDate *endDate = [NSDate date];
NSTimeInterval interval = [endDate timeIntervalSinceDate:beginDate];
NSLog(#"interval is %f",interval);
NSLog(#"intersect %#\n, filtered key array is %#\n", intersect,keySet);
}
It needs about 1/3 of the original time for filtering, memory allocation is a little bit less. I suggest you split the larger data source to smaller chunks to use less memory.
This should do the trick:
NSString *sourceRegexp =
[NSString stringWithFormat:#".*%#.*",
[combinations componentsJoinedByString:#".*|.*"]];
NSPredicate *sourcePredicate =
[NSPredicate predicateWithFormat:#"name MATCHES[c] %#", sourceRegexp];
NSArray *filteredSource =
[source filteredArrayUsingPredicate:sourcePredicate];
NSPredicate *combinationsPredicate =
[NSPredicate predicateWithFormat:
#"SUBQUERY(%#, $s, $s.name CONTAINS[c] SELF).#count > 0",
filteredSource];
NSArray *filteredCombinations =
[combinations filteredArrayUsingPredicate:combinationsPredicate];
I may have misunderstood the question, but wouldn't using an NSPredicate with a set work?
NSSet *contactsToSearchFor = [NSSet setWithArray:self.arrayOfSearchCombinationsFormed];
NSPredicate *prediate = [NSPredicate predicateWithFormat:#"name IN[cd] %#", contactsToSearchFor];
NSArray *results = [self.listOfAllContacts filteredArrayUsingPredicate:predicate];
I haven't tested this in XCode, but it should work.
Why not implement a binary search algorithm to search array.
The link provided below gives you full details on how to implement binary search.
See: http://oleb.net/blog/2013/07/nsarray-binary-search/
I would recommend you to use swift for this purposes: it is much faster and allocates much less memory. Here is a solution in Swift:
func filterContacts(contacts: [Dictionary<String, String>], searchCombinations: [String]) -> [Dictionary<String, String>]{
return contacts.filter { dict in
let name = dict["name"]!
for string in searchCombinations{
if name.rangeOfString(string) != nil { return true }
}
return false
}
}
Another much more complex solution would involve using Suffix Tree for storing your contacts data if duration of searching is important.

How to perform search on array if it has items matching then copy those items in another array in ipad app

I have a app in which i want searching. I have a array resultArray which contain all the things which display like
Book*bookObj=[resultArray objectAtIndex.indexPath.row];
NSString*test=bookObj.title;
I want to perform search on title item in resultArray if search text enter in textfield matches with title with any of the arrays then copy those all array values in testArray.
Use this as :
NSMutableArray *searchDataMA = [NSMutableArray new];
for (int i = 0; i < resultArray.count; i++) {
Book *bookObj=[resultArray objectAtIndex:i];
NSString*test=bookObj.title;
NSRange rangeValue1 = [test rangeOfString:searchText options:NSCaseInsensitiveSearch];
if (rangeValue1.length != 0) {
if (![resultArray containsObject:test]) {
[searchDataMA addObject:test];
}
}
}
You have to take another array for this. this will add your object
for (Book * bookObj in resultArray) {
NSString *strName=[[bookObj.title]lowercaseString];
if ([strName rangeOfString:searchText].location !=NSNotFound) {
[arrTemp addObject:bookObj];
}
}
- (NSArray *)filteredArrayUsingPredicate:(NSPredicate *)predicate is exactly the function that you want. It will return a new array with only the elements that pass the test in the NSPredicate object.
For example:
NSArray *newArray = [oldArray filteredArrayUsingPredicate:[NSPredicate predicateWithBlock:^BOOL(Book *evaluatedObject, NSDictionary *bindings) {
//Do whatever logic you want to in here
return [evaluatedObject.title isEqualToString:theTitle];
}];
It works for me try this:
NSArray *fruits = [NSArray arrayWithObjects:#"Apple", #"Crabapple", #"Watermelon", #"Lemon", #"Raspberry", #"Rockmelon", #"Orange", #"Lime", #"Grape", #"Kiwifruit", #"Bitter Orange", #"Manderin", nil];
NSPredicate *findMelons = [NSPredicate predicateWithFormat:#"SELF contains[cd] 'melon'"];
NSArray *melons = [fruits filteredArrayUsingPredicate:findMelons];
NSPredicate *findApple = [NSPredicate predicateWithFormat:#"SELF beginswith 'Apple'"];
NSArray *apples = [fruits filteredArrayUsingPredicate:findApple];
NSPredicate *findRNotMelons = [NSPredicate predicateWithFormat:#"SELF beginswith 'R' AND NOT SELF contains[cd] 'melon'"];
NSArray *rNotMelons = [fruits filteredArrayUsingPredicate:findRNotMelons];
NSLog(#"Fruits: %#", fruits);
NSLog(#"Melons: %#", melons);
NSLog(#"Apples: %#", apples);
NSLog(#"RNotMelons: %#", rNotMelons);
Predicates also have more condition functions, some of which I have only touched on here:
beginswith : matches anything that begins with the supplied condition
contains : matches anything that contains the supplied condition
endswith : the opposite of begins with
like : the wildcard condition, similar to its SQL counterpart. Matches anything that fits the wildcard condition
matches : a regular expression matching condition. Beware: quite intense to run
The syntax also contains the following other function, predicates and operations:
AND (&&), OR (||), NOT (!)
ANY, ALL, NONE, IN
FALSE, TRUE, NULL, SELF
Still if don't understand take a look at this link;
Useful link

How to properly filter a NSArray using a NSPredicate

I am trying to find the tags inside a NSDictionary inside myAr that matches the criteria of str and I want the result that has only those exact arrays no more nor less. In this example I want only the 2nd NSDictionary of myAr.
I though of trying to achieve this by using a predicate but that always returns empty when i use arrays.
I am trying to filter using an array but this is not working. I was wondering if anyone could tell me what i am doing wrong and how could i achieve my objective. thanks in advance
NSArray * myAr = #[ #{ #"tags": #[#"one",#"two",#"three"],
#"number": #"4"
},
#{ #"tags": #[#"one",#"two"],
#"number":#"4"
},
#{ #"tags": #[#"one",#"two",#"four"],
#"number":#"4"
},
#{ #"tags": #[#"chacho",#"chocho"],
#"number":#"4"
},
#{ #"tags": #[#"one"],
#"number":#"4"
} ];
NSArray* str = #[#"one",#"two"];
NSPredicate* pre = [NSPredicate predicateWithFormat:#"tags CONTAINS %# ",str];
myAr = [myAr filteredArrayUsingPredicate:pre];
NSLog(#"%#",myAr);
If I understand your question correctly, you just have to replace "CONTAINS" by "="
in the predicate:
[NSPredicate predicateWithFormat:#"tags = %# ",str]
This gives an array with all dictionaries where the "tags" value is equal to the
given array str. In your example, it returns an array with the second dictionary
only.
UPDATE: To find all dictionaries where the "tags" value is an array with the
given elements, but independent of the order, the following slightly more
complicated predicate should work:
[NSPredicate predicateWithFormat:#"tags.#count = %d AND SUBQUERY(tags, $t, $t in %#).#count = %d",
[str count], str, [str count]];
UPDATE 2: That was too complicated, the following predicate seems to work as well:
[NSPredicate predicateWithFormat:#"tags.#count = %d AND ALL tags in %#",
[str count], str]
(I have assumed that str contains only different elements.)
For an answer that uses neither a for loop nor predicate format strings, try using a block and make use of NSSet to determine if the set of tags you want to match is equal to a set of the array element's tags. For example:
NSSet* desiredTags = [NSSet setWithObjects:#"one", #"two", nil];
NSPredicate *tagFilterPredicate = [NSPredicate
predicateWithBlock:^BOOL (id data, NSDictionary *bindings) {
NSSet *tags = [NSSet setWithArray:[data objectForKey:#"tags"]];
return [desiredTags isEqual:tags];
}];
NSArray *resultArray = [myArr filteredArrayUsingPredicate:tagFilterPredicate];
Bear in mind that this does allocate a set per iteration. So, if you're looking to avoid allocations, this is not adequate. Otherwise, it at least avoids a format string.
A brute-force way to do this would be to remove your predicate and just enumerate:
NSArray *required = #[#"one", #"two"];
NSMutableArray *matches = [#[] mutableCopy];
[myAr enumerateObjectsUsingBlock:^(NSDictionary *dict, NSUInteger idx, BOOL *stop) {
[dict enumerateKeysAndObjectsUsingBlock:^(id key, id obj, BOOL *stop) {
if ([obj isKindOfClass:[NSArray class]]) {
BOOL match = YES;
for (NSString *item in required) {
if (![obj containsObject:item]) {
match = NO;
break;
}
}
if (match && [(NSArray *)obj count] == required.count) {
[matches addObject:obj];
}
}
}];
}];

Resources