NSPredicate
NSPredicate is a Foundation class that specifies how data should be fetched or filtered. Its query language, which is like a cross between a SQL WHERE clause and a regular expression, provides an expressive, natural language interface to define logical conditions on which a collection is searched.
It’s easier to show NSPredicate in use, rather than talk about it in the abstract, so we’re going to revisit the example data set used in the NSSort article:
first |
last |
age |
|---|---|---|
| Alice | Smith | 24 |
| Bob | Jones | 27 |
| Charlie | Smith | 33 |
| Quentin | Alberts | 31 |
@objc Members class Person: NSObject {
let first Name: String
let last Name: String
let age: Int
init(first Name: String, last Name: String, age: Int) {
self.first Name = first Name
self.last Name = last Name
self.age = age
}
override var description: String {
return "\(first Name) \(last Name)"
}
}
let alice = Person(first Name: "Alice", last Name: "Smith", age: 24)
let bob = Person(first Name: "Bob", last Name: "Jones", age: 27)
let charlie = Person(first Name: "Charlie", last Name: "Smith", age: 33)
let quentin = Person(first Name: "Quentin", last Name: "Alberts", age: 31)
let people = [alice, bob, charlie, quentin] as NSArray
let bob Predicate = NSPredicate(format: "first Name = 'Bob'")
let smith Predicate = NSPredicate(format: "last Name = %@", "Smith")
let thirties Predicate = NSPredicate(format: "age >= 30")
people.filtered(using: bob Predicate)
// ["Bob Jones"]
people.filtered(using: smith Predicate)
// ["Alice Smith", "Charlie Smith"]
people.filtered(using: thirties Predicate)
// ["Charlie Smith", "Quentin Alberts"]
@interface Person : NSObject
@property NSString *first Name;
@property NSString *last Name;
@property NSNumber *age;
@end
@implementation Person
- (NSString *)description {
return [NSString string With Format:@"%@ %@", self.first Name, self.last Name];
}
@end
#pragma mark -
NSArray *first Names = @[ @"Alice", @"Bob", @"Charlie", @"Quentin" ];
NSArray *last Names = @[ @"Smith", @"Jones", @"Smith", @"Alberts" ];
NSArray *ages = @[ @24, @27, @33, @31 ];
NSMutable Array *people = [NSMutable Array array];
[first Names enumerate Objects Using Block:^(id obj, NSUInteger idx, BOOL *stop) {
Person *person = [[Person alloc] init];
person.first Name = first Names[idx];
person.last Name = last Names[idx];
person.age = ages[idx];
[people add Object:person];
}];
NSPredicate *bob Predicate = [NSPredicate predicate With Format:@"first Name = 'Bob'"];
NSPredicate *smith Predicate = [NSPredicate predicate With Format:@"last Name = %@", @"Smith"];
NSPredicate *thirties Predicate = [NSPredicate predicate With Format:@"age >= 30"];
// ["Bob Jones"]
NSLog(@"Bobs: %@", [people filtered Array Using Predicate:bob Predicate]);
// ["Alice Smith", "Charlie Smith"]
NSLog(@"Smiths: %@", [people filtered Array Using Predicate:smith Predicate]);
// ["Charlie Smith", "Quentin Alberts"]
NSLog(@"30's: %@", [people filtered Array Using Predicate:thirties Predicate]);
Using NSPredicate with Collections
Foundation provides methods to filter NSArray / NSMutable & NSSet / NSMutable with predicates.
Immutable collections, NSArray & NSSet, have the methods filtered and filtered which return an immutable collection by evaluating a predicate on the receiver.
Mutable collections, NSMutable & NSMutable have the method filter, which removes any objects that evaluate to FALSE when running the predicate on the receiver.
NSDictionary can use predicates by filtering its keys or values (both NSArray objects). NSOrdered can either create new ordered sets from a filtered NSArray or NSSet, or alternatively, NSMutable can simply remove, passing objects filtered with the negated predicate.
Using NSPredicate with Core Data
NSFetch has a predicate property, which specifies the logical conditions under which managed objects should be retrieved. The same rules apply, except that predicates are evaluated by the persistent store coordinator within a managed object context, rather than collections being filtered in-memory.
Predicate Syntax
Substitutions
%@is a var arg substitution for an object value—often a string, number, or date.%Kis a var arg substitution for a key path.
let age Is33Predicate = NSPredicate(format: "%K = %@", "age", "33")
people.filtered(using: age Is33Predicate)
// ["Charlie Smith"]
NSPredicate *age Is33Predicate = [NSPredicate predicate With Format:@"%K = %@", @"age", @33];
// ["Charlie Smith"]
NSLog(@"Age 33: %@", [people filtered Array Using Predicate:age Is33Predicate]);
$VARIABLE_NAMEis a value that can be substituted withNSPredicate -predicate.With Substitution Variables:
let names Beginning With Letter Predicate = NSPredicate(format: "(first Name BEGINSWITH[cd] $letter) OR (last Name BEGINSWITH[cd] $letter)")
people.filtered(using: names Beginning With Letter Predicate.with Substitution Variables(["letter": "A"]))
// ["Alice Smith", "Quentin Alberts"]
NSPredicate *names Beginning With Letter Predicate = [NSPredicate predicate With Format:@"(first Name BEGINSWITH[cd] $letter) OR (last Name BEGINSWITH[cd] $letter)"];
// ["Alice Smith", "Quentin Alberts"]
NSLog(@"'A' Names: %@", [people filtered Array Using Predicate:[names Beginning With Letter Predicate predicate With Substitution Variables:@{@"letter": @"A"}]]);
Basic Comparisons
=,==: The left-hand expression is equal to the right-hand expression.>=,=>: The left-hand expression is greater than or equal to the right-hand expression.<=,=<: The left-hand expression is less than or equal to the right-hand expression.>: The left-hand expression is greater than the right-hand expression.<: The left-hand expression is less than the right-hand expression.!=,<>: The left-hand expression is not equal to the right-hand expression.BETWEEN: The left-hand expression is between, or equal to either of, the values specified in the right-hand side. The right-hand side is a two value array (an array is required to specify order) giving upper and lower bounds. For example,1 BETWEEN { 0 , 33 }, or$INPUT BETWEEN { $LOWER, $UPPER }.
Basic Compound Predicates
AND,&&: LogicalAND.OR,||: LogicalOR.NOT,!: LogicalNOT.
String Comparisons
String comparisons are by default case and diacritic sensitive. You can modify an operator using the key characters c and d within square braces to specify case and diacritic insensitivity respectively, for example firstName BEGINSWITH[cd] $FIRST_NAME.
BEGINSWITH: The left-hand expression begins with the right-hand expression.CONTAINS: The left-hand expression contains the right-hand expression.ENDSWITH: The left-hand expression ends with the right-hand expression.LIKE: The left hand expression equals the right-hand expression:?and*are allowed as wildcard characters, where?matches 1 character and*matches 0 or more characters.MATCHES: The left hand expression equals the right hand expression using a regex-style comparison according to ICU v3 (for more details see the ICU User Guide for Regular Expressions).
Aggregate Operations
Relational Operations
ANY,SOME: Specifies any of the elements in the following expression. For example,ANY children.age < 18.ALL: Specifies all of the elements in the following expression. For example,ALL children.age < 18.NONE: Specifies none of the elements in the following expression. For example,NONE children.age < 18. This is logically equivalent toNOT (ANY ...).IN: Equivalent to an SQLINoperation, the left-hand side must appear in the collection specified by the right-hand side. For example,name IN { 'Ben', 'Melissa', 'Nick' }.
Array Operations
array[index]: Specifies the element at the specified index inarray.array[FIRST]: Specifies the first element inarray.array[LAST]: Specifies the last element inarray.array[SIZE]: Specifies the size ofarray.
Boolean Value Predicates
TRUEPREDICATE: A predicate that always evaluates toTRUE.FALSEPREDICATE: A predicate that always evaluates toFALSE.
NSCompound Predicate
We saw that AND & OR can be used in predicate format strings to create compound predicates. However, the same can be accomplished using an NSCompound.
For example, the following predicates are equivalent:
NSCompound Predicate(
type: .and,
subpredicates: [
NSPredicate(format: "age > 25"),
NSPredicate(format: "first Name = %@", "Quentin")
]
)
NSPredicate(format: "(age > 25) AND (first Name = %@)", "Quentin")
[NSCompound Predicate and Predicate With Subpredicates:@[[NSPredicate predicate With Format:@"age > 25"], [NSPredicate predicate With Format:@"first Name = %@", @"Quentin"]]];
[NSPredicate predicate With Format:@"(age > 25) AND (first Name = %@)", @"Quentin"];
While the syntax string literal is certainly easier to type, there are occasions where you may need to combine existing predicates. In these cases, NSCompound & -or is the way to go.
NSComparison Predicate
Similarly, if after reading last week’s article you now find yourself with more NSExpression objects than you know what to do with, NSComparison can help you out.
Like NSCompound, NSComparison constructs an NSPredicate from subcomponents—in this case, NSExpressions on the left and right hand sides.
Analyzing its class constructor provides a glimpse into the way NSPredicate format strings are parsed:
+ (NSPredicate *)predicate With Left Expression:(NSExpression *)lhs
right Expression:(NSExpression *)rhs
modifier:(NSComparison Predicate Modifier)modifier
type:(NSPredicate Operator Type)type
options:(NSUInteger)options
init(left Expression lhs: NSExpression,
right Expression rhs: NSExpression,
custom Selector selector: Selector)
Parameters
lhs: The left hand expression.rhs: The right hand expression.modifier: The modifier to apply. (ANYorALL)type: The predicate operator type.options: The options to apply. For no options, pass0.
NSComparison Predicate Types
enum NSComparison Predicate.Operator: UInt {
case less Than
case less Than Or Equal To
case greater Than
case greater Than Or Equal To
case equal To
case not Equal To
case matches
case like
case begins With
case ends With
case `in`
case custom Selector
case contains
case between
}
enum {
NSLess Than Predicate Operator Type = 0,
NSLess Than Or Equal To Predicate Operator Type,
NSGreater Than Predicate Operator Type,
NSGreater Than Or Equal To Predicate Operator Type,
NSEqual To Predicate Operator Type,
NSNot Equal To Predicate Operator Type,
NSMatches Predicate Operator Type,
NSLike Predicate Operator Type,
NSBegins With Predicate Operator Type,
NSEnds With Predicate Operator Type,
NSIn Predicate Operator Type,
NSCustom Selector Predicate Operator Type,
NSContains Predicate Operator Type,
NSBetween Predicate Operator Type
};
typedef NSUInteger NSPredicate Operator Type;
NSComparison Predicate Options
NSCase: A case-insensitive predicate. You represent this option in a predicate format string using a [c] following a string operation (for example,Insensitive Predicate Option "Ne).XT" like[c] "next" NSDiacritic: A diacritic-insensitive predicate. You represent this option in a predicate format string using a [d] following a string operation (for example,Insensitive Predicate Option "naïve" like[d] "naive").NSNormalized: Indicates that the strings to be compared have been preprocessed. This option supersedes NSCaseInsensitivePredicateOption and NSDiacriticInsensitivePredicateOption, and is intended as a performance optimization option. You represent this option in a predicate format string using a [n] following a string operation (for example,Predicate Option "WXYZlan" matches[n] ".lan").NSLocale: Indicates that strings to be compared usingSensitive Predicate Option <,<=,=,=>,>should be handled in a locale-aware fashion. You represent this option in a predicate format string using a[l]following one of the<,<=,=,=>,>operators (for example,"straße" >[l] "strasse").
Block Predicates
Finally, if you just can’t be bothered to learn the NSPredicate format syntax, you can go through the motions with NSPredicate +predicate.
let short Name Predicate = NSPredicate { (evaluated Object, _) in
return (evaluated Object as! Person).first Name.utf16.count <= 5
}
people.filtered(using: short Name Predicate)
// ["Alice Smith", "Bob Jones"]
NSPredicate *short Name Predicate = [NSPredicate predicate With Block:^BOOL(id evaluated Object, NSDictionary *bindings) {
return [[evaluated Object first Name] length] <= 5;
}];
// ["Alice Smith", "Bob Jones"]
NSLog(@"Short Names: %@", [people filtered Array Using Predicate:short Name Predicate]);
…Alright, that whole dig on predicate as being the lazy way out wasn’t entirely charitable.
Actually, since blocks can encapsulate any kind of calculation, there is a whole class of queries that can’t be expressed with the NSPredicate format string (such as evaluating against values dynamically calculated at run-time). And while its possible to accomplish the same using an NSExpression with a custom selector, blocks provide a convenient interface to get the job done.
One important note: NSPredicates created with predicate cannot be used for Core Data fetch requests backed by a SQLite store.
NSPredicate is, and I know this is said a lot, truly one of the jewels of Cocoa. Other languages would be lucky to have something with half of its capabilities in a third-party library—let alone the standard library. Having it as a standard-issue component affords us as application and framework developers an incredible amount of leverage in working with data.
Together with NSExpression, NSPredicate reminds us what a treat Foundation is: a framework that is not only incredibly useful, but meticulously architected and engineered, to be taken as inspiration for how we should write our own code.