I have implemented a UISearchDisplayController that allows users to search a table. Currently the predicate I am using to search is as follows,
NSPredicate *resultPredicate = [NSPredicate predicateWithFormat:#"Name contains[cd] %#", searchText];
Now lets say a users searches for "beans, cooked" the corresponding matches are found in the table. But if the user enters the search text as "beans cooked" without the comma, there will be no matches found.
How can I re-write my predicate to "ignore" the commas when searching? In other words how can I re-write it so that it views "beans, cooked" being equal to "beans cooked" (NO COMMMA)?
First a disclaimer:
I think that what you are trying to do is to add some "fuzzyness" to your search algorithm, seen that you want to make your match insensitive to certain differences in user input.
Predicates (which are logic constructs) are by their very nature not fuzzy, so there is an underlying impedance mismatch between the problem and the tool chosen.
Anyway, one way to go about it could be to add a method to your model object class.
In this method, you can clean your name string so it only contains the most basic characters, say numbers, ascii letters and a space.
Being totally deterministic, such a method is effectively a read-only string property on your object, and as such it can be used to match in predicates.
Here is an implementation that removes punctuation, accents and diacritics:
- (NSString *)simplifiedName
{
// First convert the name string to a pure ASCII string
NSData *asciiData = [self.name dataUsingEncoding:NSASCIIStringEncoding allowLossyConversion:YES];
NSString *asciiString = [[[NSString alloc] initWithData:asciiData encoding:NSASCIIStringEncoding] lowercaseString];
// Define the characters that we will allow in our simplified name
NSString *searchCharacters = #"0123456789 abcdefghijklmnopqrstuvwxyz";
// Remove anything else
NSString *regExPattern = [NSString stringWithFormat:#"[^%#]", searchCharacters];
NSString *simplifiedName = [asciiString stringByReplacingOccurrencesOfString:regExPattern withString:#"" options:NSRegularExpressionSearch range:NSMakeRange(0, asciiString.length)];
return simplifiedName;
}
Now, a predicate could be made to search in the simplified name:
NSPredicate *pred = [NSPredicate predicateWithFormat:#"self.simplifiedName = %#", searchString];
You would of course want to clean the search string using the same algorithm used to clean the name, so it would probably be a good idea to factor it out into a general method to be used in both places.
Last, the simplifiedName method can also be added by implementing a category to the model object class so you don't have to modify its code, which is handy in case your object class is defined in an auto-generated file by Core Data.
This may be a bit hacky, but you could just remove the comma from the search term.
Example:
searchText = [searchText stringByReplacingOccurrencesOfString:#"," withString:#""];
NSPredicate *resultPredicate = [NSPredicate predicateWithFormat:#"Name contains[cd] %#", searchText];
The best solution I found for this type of problem is to actually add an entry in each items dictionary that has the same name but will all punctuations, commas, dashes, etc. removed like in this answer
Related
I have a NSManagedObject with the following attributes:
status
kind
priority
Now I want to be able to filter my entity with these attributes respectively. So I would expect that I have to have a predicate along those lines:
status CONTAINS[c] ‘open’
I get really weird results, as soon as I have two variables in my predicate and I have to reverse the order of kind and value in my case so that I get the desired results:
NSString *kind = #"status"; // DEBUGGING
NSString *value = #"open"; // DEBUGGING
// This works although it defies all logic
NSString *predicate = [NSString stringWithFormat:#"('%#' CONTAINS[c] %#)", value, kind];
self.myFilterPredicate = [NSPredicate predicateWithFormat:predicate];
This however, does not work for some reason:
NSString *predicate = [NSString stringWithFormat:#"(%# CONTAINS[c] ‘%#‘)”, kind, value];
I cannot reproduce the exact problem, but generally you should not use stringWithFormat to create predicate. It causes problems as soon as the substituted key or value contain
any special characters like spaces or quotation marks.
A better way is
self.myFilterPredicate = [NSPredicate predicateWithFormat:#"%K == %#", kind, value];
%K is a placeholder to be replaced by a key path such as "status".
I'm setting the string of my predicate like this:
[NSString stringWithFormat:#"(name like '%#')",name]
but if name contains ' characters, for example if name is "family's" it crashes.
How can I fix this?
You don't need the ' in NSPredicate. They are being inserted automatically.
Just try
[NSPredicate predicateWithFormat:#"(name like %#)",name];
The reason this crashed was because the predicate value was interrupted.
If you create a string with format, like in your example, your going to end up with (name like 'family's'), which obviously can't work.
If you use predicateWithFormat: on the other hand, you can let it handle this itself. It will escape your special characters.
If you don't want/aren't able to use predicateWithFormat, then you can simply replace any apostrophes in your string.
I do this in my app, when checking my UISearchBar control, to see if the user's trying to look for particular CoreData records:
NSString* searchString = self.searchBar.text;
if (searchString.length != 0)
{
searchString = [searchString stringByReplacingOccurrencesOfString:#"'" withString:#"\\'"];
NSString* filter = [NSString stringWithFormat:#"companyName CONTAINS[cd] '%#'", searchString];
NSPredicate* predicate1 = [NSPredicate predicateWithFormat:filter];
// ...etc...
}
(This is a simplified version of a generic function I use, which actually searches CoreData using a variable number of filter strings, which is why I don't use predicateWithFormat directly.)
I have a (to me) curious case with NSPredicate's predicateWithFormat: method.
Using the following I log the description of two NSPredicate instances to the console:
NSNumber *myNumber = [NSNumber numberWithInt:1];
NSString *predicateFormatByHand = [NSString stringWithFormat:#"self MATCHES 'chp%#_img[0-9]+\\.png'", myNumber];
NSPredicate *firstPredicate = [NSPredicate predicateWithFormat:predicateFormatByHand];
NSLog(#"firstPredicate description: %#", firstPredicate);
NSPredicate *secondPredicate = [NSPredicate predicateWithFormat:#"self MATCHES 'chp%#_img[0-9]+\\.png'", myNumber];
NSLog(#"secondPredicate description: %#", secondPredicate);
This outputs:
firstPredicate description: SELF MATCHES "chp1_img[0-9]+.png"
secondPredicate description: SELF MATCHES "chp%#_img[0-9]+.png"
I would expect these descriptions to be the same.
Can someone explain why they are not?
(Following this question I've played with various escape sequences for the embedded single-quotes but when doing so keep having NSPredicate complain that it cannot then parse the format string. I'd be grateful to know what's going on.)
UPDATE: one answer suggested it's an issue with using NSNumber rather than an int, so:
NSPredicate *thirdPredicate = [NSPredicate predicateWithFormat:#"self MATCHES 'chp%d_img[0-9]+\\.png'", [myNumber intValue]];
NSLog(#"thirdPredicate description: %#", thirdPredicate);
I began with this originally, but alas the output is the same:
thirdPredicate description: SELF MATCHES "chp%d_img[0-9]+.png"
(Something means the format specifier is not evaluated.)
The answer is simple: the parser used by NSPredicate assumes that anything inside the quote marks is a string literal, and does not attempt to do any substitutions on its contents. It you need to have a dynamic string value, you will have to build the string before substituting it into the predicate format string, as in your first example.
...because the predicate is not such thing than string.
for any of the predicates you should use two format specifier 100% safety only:
one for the key (%K); and
one for the value (%#);
you cannot format neither the key nor the value when you add them to the predicate. this is why your second (and third) predicates are not formatted inside the value.
you can format the value before you add it to the predicate like:
NSNumber *myNumber = [NSNumber numberWithInt:1];
NSString *string = [NSString stringWithFormat:#"chp%#_img[0-9]+\\.png", myNumber];
NSPredicate *predicate = [NSPredicate predicateWithFormat:#"SELF MATCHES %#", string];
NSLog(#"%#", predicate);
the result is:
SELF MATCHES "chp1_img[0-9]+\\.png"
...and never forget my first sentence: the predicates and the strings are not the same thing.
My interpretation of
%# is a var arg substitution for an object value
in the "Predicate Programming Guide" is that %# can only be used for substituting a value that a Core Data object can be compared against. For example
NSNumber *myNumber = [NSNumber numberWithInt:1];
NSPredicate *predicate = [NSPredicate predicateWithFormat:#"count = %#", myNumber];
is perfectly valid if "count" is a Number attribute of the entity. It is similar to binding values to SQLite prepared statements.
If %# could be used for general string formatting in predicates, then there would be no need to have two different format specifiers %K and %# for key paths and values.
All,
I am trying to use predicates to bring back a search return, giving precedence to strings that start with the search string VS. simply contained within it.
For example if the search string was "Objective-C", I want to get the filtered results back like this:
Objective-C a Primer
Objective-C Patterns
Objective-C Programming
All About Objective-C
How to program in Objective-C
Here is what I tried but since it's an OR, it clearly does not give precedence to the first condition. Is there a way to do a type of "chaining" with predicates? Thanks
NSPredicate *filter = [NSPredicate predicateWithFormat:#"subject BEGINSWITH [cd] %# OR subject CONTAINS [cd]", searchText,searchText];
NSArray *filtered = [myArray filteredArrayUsingPredicate: filter];
I don't think there's a way to do with NSPredicate by itself. What you seem to be asking for is sorted results. You do that by sorting the results after you get them back from the predicate. In this case, since the sort order isn't simply alphabetical, you should use NSArray's -sortedArrayUsingComparator: method. Something like this (not tested, typed off the top of my head).
NSArray *sortedStrings = [filtered sortedArrayUsingComparator:^NSComparisonResult(id obj1, id obj2) {
NSString *string1 = (NSString *)obj1;
NSString *string2 = (NSString *)obj2;
NSUInteger searchStringLocation1 = [string1 rangeOfString:searchString].location;
NSUInteger searchStringLocation2 = [string2 rangeOfString:searchString].location;
if (searchStringLocation1 < searchStringLocation2) return NSOrderedDescending;
if (searchStringLocation1 > searchStringLocation2) return NSOrderedAscending;
return NSOrderedSame;
}];
It looks like you're trying to sort your array using a predicate, or else trying to filter AND sort using just a predicate. Predicates return boolean values: either a string begins with or contains the search text, or it doesn't. Predicates don't tell you anything about relative order. Filtering an array removes those objects for which the predicate you supply returns NO, so only objects beginning with or containing the search text will be present in the filtered array. Indeed, since any string that begins with the search text also contains it, you could simplify your predicate to just the 'contains' part.
If you want to change the order of your filtered results, sort the filtered array with an appropriate comparator.
I currently have the following piece of code
NSPredicate *pred = [NSPredicate predicateWithFormat:#"SELF contains '-'"];
[resultsArray filterUsingPredicate:pred];
This returns an array with the elements which contain '-'. I want to do the inverse of this so all elements not containing '-' are returned.
Is this possible?
I've tried using the NOT keyword in various locations but to no avail. (I didn't think it would work anyway, based on the Apple documentation).
To further this, is it possible to provide the predicate with an array of chars that I don't want to be in the elements of the array? (The array is a load of strings).
I'm not an Objective-C expert, but the documentation seems to suggest this is possible. Have you tried:
predicateWithFormat:"not SELF contains '-'"
You can build a custom predicate to negate the predicate that you already have. In effect, you're taking an existing predicate and wrapping it in another predicate that works like the NOT operator:
NSPredicate *pred = [NSPredicate predicateWithFormat:#"SELF contains '-'"];
NSPredicate *notPred = [NSCompoundPredicate notPredicateWithSubpredicate:pred];
[resultsArray filterUsingPredicate:pred];
The NSCompoundPredicate class supports AND, OR, and NOT predicate types, so you could go through and build a large compound predicate with all the characters you don't want in your array, then filter on it. Try something like:
// Set up the arrays of bad characters and strings to be filtered
NSArray *badChars = [NSArray arrayWithObjects:#"-", #"*", #"&", nil];
NSMutableArray *strings = [[[NSArray arrayWithObjects:#"test-string", #"teststring",
#"test*string", nil] mutableCopy] autorelease];
// Build an array of predicates to filter with, then combine into one AND predicate
NSMutableArray *predArray = [[[NSMutableArray alloc]
initWithCapacity:[badChars count]] autorelease];
for(NSString *badCharString in badChars) {
NSPredicate *charPred = [NSPredicate
predicateWithFormat:#"SELF contains '%#'", badCharString];
NSPredicate *notPred = [NSCompoundPredicate notPredicateWithSubpredicate:pred];
[predArray addObject:notPred];
}
NSPredicate *pred = [NSCompoundPredicate andPredicateWithSubpredicates:predArray];
// Do the filter
[strings filterUsingPredicate:pred];
I make no guarantees as to its efficiency, though, and it's probably a good idea to put the characters which are likely to eliminate the most strings from the final array first so that the filter can short-circuit as many comparisons as possible.
I'd recommend NSNotPredicateType as described in the Apple documentation.