Filtering arrays with NSPredicate

I think my first experience of using the NSPredicate class on the iPhone was with core data queries but it is has many other useful applications. I had the need the other day to process a list of filenames and match against a simple regular expression. The code to create the array of filenames that I needed to process looked something like this:

NSFileManager *fileManager = [NSFileManager defaultManager];
NSString *defaultPath = [[NSBundle mainBundle] resourcePath];
NSError *error;
  
NSArray *directoryContents = [fileManager contentsOfDirectoryAtPath:defaultPath
                              error:&error];

The first few lines just setup a search of the resource directory in the main application bundle so that the array directoryContents ends up containing the filenames of all the files in the directory. I then need to process that array and pull out the filenames that match my pre-defined regular expression.

NSPredicate Format Strings

The NSPredicate class provides a general way to specify a query that can then be applied to filter an array. The actual query to use is specified when creating the predicate as an argument to the predicateWithFormat: method. Some examples will hopefully make it clear:

Simple match

The simplest example is when you just need an exact match against a single value. This is a fairly meaningless example in this case but illustrates the basic technique:

NSString *match = @"imagexyz-999.png";
NSPredicate *predicate = [NSPredicate predicateWithFormat:@"SELF == %@", match];
NSArray *results = [directoryContents filteredArrayUsingPredicate:predicate];

The query string is simply SELF == %@ where SELF refers to the each element in the array. To use the predicate we apply it to the array of file names using filterArrayUsingPredicate:. In this trivial (and pointless) example the results array would contain a single string imagexyz-999.png assuming that file existed in the directory.

Wildcard match

To match using a wildcard string the query can use like instead of the comparison operator and include the * (match zero or more characters) or ? (match exactly 1 character) as wildcards as follows:

NSString *match = @"imagexyz*.png";
NSPredicate *predicate = [NSPredicate predicateWithFormat:@"SELF like %@", match];
NSArray *results = [directoryContents filteredArrayUsingPredicate:predicate];

In this case the results array would contain the filenames that match imagexyz*.png (e.g. imagexyz-001.png, imagexyz-002.png, etc.).

Case Insensitive match

To make the previous example use a case insensitive match the like operator is modified to like[c]. There is also an additional option to match in a diacritic-insenstive way to ignore accented characters by using like[d]. You can combine these two options as follows:

NSString *match = @"imagexyz*.png";
NSPredicate *predicate = [NSPredicate predicateWithFormat:@"SELF like[cd] %@", match];
NSArray *results = [directoryContents filteredArrayUsingPredicate:predicate];

Other string comparisons

In addition to like you can also use beginswith, endswith, contains and matches. These are mostly self-explanatory with the exception perhaps of matches. With matches you can specify a full regular expression which allows more complex matches than is possible with just the * and ? wildcard characters. For example to match filenames of the form imagexyz-ddd.png where ddd are all digits:

NSString *match = @"imagexyz-\\d{3}\\.png";
NSPredicate *predicate = [NSPredicate predicateWithFormat:@"SELF matches %@", match];
NSArray *results = [directoryContents filteredArrayUsingPredicate:predicate];

I will not try to explain regular expression syntax here but note that the backslash (\) characters are escaped by using a double-backslash. Also note that strict ICU regular expression formats are used.