How to use one array as predicate for another array? - ios

NSArray *arrClient = [[NSArray alloc] initWithObjects:#"record 1", #"record 2", nil];
NSArray *arrServer = [[NSArray alloc] initWithObjects:#"record 1", #"record 3", nil];
On arrServer I would like to apply predicates to filter only those entries that DON'T already exist in arrClient. e.g. in this case record 1 exist in both arrays and shall be ignored, hence only an array with one entry with the "record 3" string shall be returned.
Is this possible?
UPDATE
The answers below are great. I believe I need to give a real example to verify if what I am doing makes sense after all. (I am still giving a compact version below)
Now the clientItems will be of type FTRecord (Core Data)
#interface FTRecord : NSManagedObject
...
#property (nonatomic) NSTimeInterval recordDate;
#end
#implementation FTRecord
...
#dynamic recordDate;
#end
This class below is a holder for parsing json from a REST service. Hence the serverItems we mentioned earlier will be of this type.
#interface FTjsonRecord : NSObject <JSONSerializable>
{
}
#property (nonatomic) NSDate *recordDate;
#implementation FTjsonRecord
- (NSUInteger)hash
{
return [[self recordDate] hash];
}
- (BOOL)isEqual:(id)object
{
if ([object isKindOfClass:[FTjsonRecord self]]) {
FTjsonRecord *other = object;
return [[self recordDate] isEqualToDate:[other recordDate]];
}
else if ([object isKindOfClass:[FTRecord self]]) {
FTRecord *other = object;
return [[self recordDate] isEqualToDate:[NSDate dateWithTimeIntervalSinceReferenceDate:[other recordDate]]];
}
else {
return NO;
}
}
Going with Wain's example, this seems to work fine. Now is this feasible?
Keep in mind that serverItems are just temporary and only used for syncing with server, and will be thrown away. clientItems is the one that remains in place.
UPDATE 2:
This time I am trying Manu's solution:
I have created this method on my Client DBStore, which is called by the predicate.
The reason I can't use containsObject is because the class types in serverItems and clientItems are not the same type.
-(BOOL)recordExistsForDate:(NSDate *)date
{
NSPredicate *predicate = [NSPredicate predicateWithFormat:#"recordDate == %#", date];
NSArray *arr = [allRecords filteredArrayUsingPredicate:predicate];
if (arr && [arr count] > 0) {
return YES;
} else {
return NO;
}
}
NSPredicate *predicate = [NSPredicate predicateWithBlock:^BOOL(FTjsonRecord *evaluatedObject, NSDictionary *bindings) {
return ![store recordExistsForDate:[evaluatedObject recordDate]];
}];
NSSet *set = [[serverRecords items] filteredSetUsingPredicate:predicate];
What worries me about this solution though, is the linear read from my clientItems (allRecords). I am not sure how efficient it is using the predicate on the array, wonder if there is a better way to achieve this.

You can use NSSet to get the union, intersection and difference (minus) with other sets. This more accurately matches what you're trying to do.
NSMutableSet *serverItems = [[NSMutableSet alloc] init];
[arrServerItems addObjectsFromArray:arrServer];
NSSet *clientItems = [[NSSet alloc] init];
[clientItems addObjectsFromArray:arrClient];
[arrServerItems minus:clientItems];
This does remove the ordering information though.
For predicates you can use:
NSPredicate *filterPredicate = [NSPredicate predicateWithFormat:#"NOT (SELF IN %#)", arrClient];

depend to the predicate that you want to use:
you can use an array of arguments using this
[NSPredicate predicateWithFormat:<#(NSString *)#> argumentArray:<#(NSArray *)#>];
and build your predicate using the objects in the array
or use a predicate with block
[NSPredicate predicateWithBlock:^BOOL(id evaluatedObject, NSDictionary *bindings) {
<#code#>
}]
and in the block evluate the object comparing it with the object in your array
a possible check you can do is:
return ![arrClient containsObject:evaluatedObject];
will exclude objects contained in arrClient
containsObject: use 'isEqual:' to compare the objects

Related

Use NSPredicate to filter by object particular attribute

I am having an array with collection custom objects.
#interface DataClass : NSObject
#property (strong,nonatomic)NSString *customerName;
#property (strong,nonatomic)NSString *departmentName;
DataClass *dataClass = [[DataClass alloc]init];
dataClass.customerName = #"John";
dataClass.departmentName = #"Electricals";
[inputArray addObject: dataClass];
DataClass *dataClass = [[DataClass alloc]init];
dataClass.customerName = #"Ezhil";
dataClass.departmentName = #"Electronics";
[inputArray addObject: dataClass];
Now I want to filter the department name alone in a new array. After filtering I want "Electrical" and "Electronics" in a array. How can I achieve this using NSPredicate?
Its not possible to use NSPredicate in the way you want
because dataClass.customerName = #"John";
dataClass.departmentName = #"Electricals";
is one single object
we can use NSPredicate if you want to "Fetch all objects having department name = Electricals"
Hope it helps you :)
NSArray *filtered = [inputArray filteredArrayUsingPredicate:[NSPredicate predicateWithFormat:#"(key == %#)", currentKeyName]];
currentKeyName is the String with key Name.
Likewise you can filter the array with Desired Predicate.

NSPredicate with block - how does it work

I have a search mechanism in my application and thus far an object of NSPredicate is used to filter objects.
I'm pretty confused because of the magic happening in the runtime of my program. In my program there is an array with names and another mutable array with filtered names declared as properties.
#property (strong, nonatomic) NSArray *names;
#property (strong, nonatomic) NSMutableArray *filteredNames;
I initialize the array with names when the view is loaded.
self.names = #[
#"Aeron",
#"Brandon",
#"Chris",
#"David",
#"Elvis",
#"Francisco",
#"George",
#"Oliver",
#"Lary",
#"Neythan",
#"Marcus",
#"Phil"
];
Then I setup a table view and its content, install search mechanism (with iOS 8's one, i.e. UISearchController). And here we go, I implement the UISearchResultsUpdating protocol and my updateSearchResultsForSearchController: is presented below.
- (void)updateSearchResultsForSearchController:(UISearchController *)searchController {
NSString *input = searchController.searchBar.text;
if (input.length > 0) {
[self.filteredNames removeAllObjects];
NSPredicate *predicate = [NSPredicate predicateWithBlock:^BOOL(id evaluatedObject, NSDictionary *bindings) {
NSLog(#"Got [%#] string.", evaluatedObject);
NSRange range = [evaluatedObject rangeOfString:input options:NSCaseInsensitiveSearch];
return range.location != NSNotFound;
}];
NSArray *matches = [self.names filteredArrayUsingPredicate:predicate];
[self.filteredNames addObjectsFromArray:matches];
[((UITableViewController *)self.searchController.searchResultsController).tableView reloadData];
}
}
So it works perfectly. But I wonder, why do I get an iteration of my array with names inside of the block? I know that blocks are good due to their specification of saving their frame from a stack with all variables and certain values for the moment in past, therefore they can be executed lately. But why does the block iterate this particular array? I tried to declare a new one and initialize it (since I thought it took all collections and their values), but it didn't work.
Thank you in advance!
To filter the array, you have to iterate through every element and test it against the predicate block. I believe the behaviour you're experiencing is intended.

Warning at method definition

Following method is showing me a warning, but the app is executing as expected. Please could you check the code and tell me what is wrong there? Only if this important to the app, if the warning is not dangering the app, then tell me if I could let this as it is...thank you
The warning is : Incompatible pointer types assigning to 'NSMutableArray *' from 'NSArray *' at the method definition.
-(void)filterContentForSearchText:(NSString*)searchText scope:(NSString*)scope {
self.searchResults = [[self.fetchedResultsController fetchedObjects] filteredArrayUsingPredicate:[NSPredicate predicateWithBlock:^BOOL(id evaluatedObject, NSDictionary *bindings)
{
ToDoItem * item = evaluatedObject;
NSString* name = item.todoName;
//searchText having length < 3 should not be considered
if (!!searchText && [searchText length] < 3) {
return YES;
}
if ([scope isEqualToString:#"All"] || [name isEqualToString:scope]) {
return ([name rangeOfString:searchText].location != NSNotFound);
}
return NO; //if nothing matches
}]];
}
self.searchResults = [[[self.fetchedResultsController fetchedObjects]
filteredArrayUsingPredicate:[NSPredicate predicateWithBlock:
^BOOL(id evaluatedObject, NSDictionary *bindings) mutableCopy];
mutableCopy is a method on many objects for which a mutable and immutable version exists. In the case of things like NSArray, NSString, NSData, etc, calling mutableCopy on one of these instances will return a mutable version containing the same contents as the original object you called the method on.
For example,
NSArray *immutableArray = [NSArray arrayWithObjects:#"foo",#"bar"];
NSMutableArray *mutableArray = [immutableArray mutableCopy];
However, if you don't intend for searchResults to be an NSMutableArray, you should change it's declaration:
#property (nonatomic,strong) NSArray *searchResults
If you don't intend it to be mutable, it should be declared as immutable.
Given your claim that the warning is not effecting the performance of your app, my best guess is that the proper solution would be changing searchResults from NSMutableArray to NSArray.
filteredArrayUsingPredicate returns an immutable NSArray,
and you seem to have declared searchResults as NSMutableArray.
So either
change the declaration of searchResults to NSArray, or
make a mutableCopy before assigning it.
The proper solution depends on whether you need to modify the searchResults later or not.

How to add unique object to nsmutablearray?

I have an NSObject with NSStrings inside. How do I add objects with unique obj.name only to an NSMutableArray? I tried NSOrderedSet but it only works if you add NSString to an array and not objects with NSString inside.
Example.
##interface MyObject : NSObject
#property (strong, nonatomic) NSString *name;
#end
NSMutableArray *array = {MyObject.name,MyObject.name,MyObject.name};
How do I make sure that no two MyObjects have the same name?
Use NSPredicate for seraching object in NSMutableArray if not present then add it to NSMutableArray.
Try this.
NSArray * filtered = [array filteredArrayUsingPredicate:[NSPredicate predicateWithFormat:#"name = %#", #"MyObject.name"]];
if(![array count])
[array addObject:MyObject ];
All NSSet classes use isEqual: in combination with hash: to compare equality.
Because you have not redefined these simply storing two objects with the same name in a set will be possible as the NSObject implementation of isEqual: and hash: will be used.
The documentation of NSObject Protocol talks about overriding isEqual and hash.
This previous answer on Stackoverflow details how to implement hash and isEqual correctly.
In your own implementation of hash you can use NSString's hash method.
Example
- (NSUInteger) hash {
NSUInteger prime = 31;
NSUInteger result = 1;
result = prime * result + [super hash];
result = prime * result + self.name == nil ? 0 : [self.name hash];
return result;
}
- (bool) isEqual:(id)other {
if (other == self) {
return YES;
}
if (!other || ![other isKindOfClass:[self class]]) {
return NO;
}
return [self.name isEqualToString:other.name];
}
Personally, I would use a NSMutableDictionary with MyObject.name as the key. That way all you would have to do is this:
if( myDictionary[MyObject.name] == nil )
{
myDictionary[MyObject.name] = MyObject;
}
Much more efficient than using a regular NSMutableArray based on the number of additions you are doing. Plus if you want to get access to an array of all the values, all you have to do is:
NSArray *array = [myDictionary allValues];
The Big-O Runtime for NSPredicate is O(n) where the dictionary method is O(1).

Filtering NSMutableArray with NSPredicates

I've checked StackOverflow for some of the topics on NSPredicates, and although they all point in the right direction, I must be missing something essential about it all.
I've got an NSMutableArray that contains a list of products.
Each product has several properties like Brand, Category and Type.
Now I want my users to be able to filter that NSMutableArray using NSPredicates, insomuch that if any of the selected filters are blank, it shouldn't use that filter.
But, in turn, if for example all filters are on: Filter with Brand A with Category B and Type C, it should only show Brand A with Cat B and Type C.
Should I then deselect Cat B, it would filter on Brand A with Type C.
I've written some code, but it mainly returns an empty NSMutableArray, so I guess my NSPredicates are off.
I also found out that I need to default to the 'all products' NSMutableArray before running the predicate, or it will filter the already filtered array when a new filter option is selected. Should I use multiple Arrays with some BOOLean magick, or is this an issue that can be solved using NSPredicates?
Here's my code:
-(void)filterTable
{
NSPredicate *brandPredicate;
NSPredicate *categoryPredicate;
NSMutableArray *compoundPredicateArray;
if( ![self.selectedBrand isEqual: #"Show All Brands"] || !(self.currentBrand == NULL))
{
brandPredicate = [NSPredicate predicateWithFormat:#"brand CONTAINS[cd] %#",self.currentBrand];
compoundPredicateArray = [ NSMutableArray arrayWithObject: brandPredicate ];
}
if( ![self.currentCategory isEqual: #"Show All Categories"] || !(self.currentCategory == NULL))
{
categoryPredicate = [NSPredicate predicateWithFormat:#"category CONTAINS[cd] %#",self.currentCategory];
[ compoundPredicateArray addObject: categoryPredicate];
}
NSPredicate *predicate = [NSCompoundPredicate andPredicateWithSubpredicates:
compoundPredicateArray ];
[self.tableData filterUsingPredicate:predicate];
[self.popNTwinBee dismissPopoverAnimated:YES]; // PopoverController
[self.tableView reloadData];
}
You have a couple of conceptual errors in your code.
First, you should init your NSMutableArray of predicates as you declare it:
NSMutableArray *compoundPredicateArray = [NSMutableArray array];
Right now you only instantiate it inside your first if(), so that if the brand filter is not set the mutable array doesn't even get instantiated so adding objects to it later (for example in the second filtering if()) is uneffective and the compound predicate created empty.
Inside your first if() you will then have:
[compoundPredicateArray addObject:brandPredicate];
Your second issue is that, as you correctly imagined, you are filtering what you have already filtered previously, when you use filterUsingPredicate.
What you should be doing is to always keep the unfiltered data in a NSArray and use the filteredArrayUsingPredicate method on it to retrieve a new filtered NSArray you will use to display the data from.
Well I took a good look at the code and came up with this:
Got this handy little block of code from this site.
NSArray+filter.h
#import <Foundation/Foundation.h>
#interface NSArray (Filter)
- (NSArray*)filter:(BOOL(^)(id elt))filterBlock;
#end
NSArray+filter.m
#import "NSArray+filter.h"
#implementation NSArray(Filter)
- (NSArray*)filter:(BOOL(^)(id elt))filterBlock
{ // Create a new array
id filteredArray = [NSMutableArray array]; // Collect elements matching the block condition
for (id elt in self)
if (filterBlock(elt))
[filteredArray addObject:elt];
return filteredArray;
}
#end
And edited my method accordingly.
In TableViewController.m
- (void)filterTable {
WebServiceStore *wss = [WebServiceStore sharedWebServiceStore];
self.allProducts = [wss.allProductArray mutableCopy];
NSArray *filteredOnBrand;
NSArray *filteredOnCategory;
NSArray *filteredOnCategoryAndBrand;
if (![self.currentBrand isEqualToString:#"All Brands"] && !(self.currentBrand == nil))
{
filteredOnBrand = [self.allProducts filter:^(id elt)
{
return [[elt brand] isEqualToString:self.currentBrand];
}];
[self.tableData removeAllObjects];
[self.tableData addObjectsFromArray:filteredOnBrand];
}
if ([self.currentBrand isEqualToString:#"All Brands"] || self.currentBrand == nil)
{
filteredOnBrand = [self.allProducts mutableCopy];
[self.tableData removeAllObjects];
[self.tableData addObjectsFromArray:filteredOnBrand];
}
if (![self.currentCategory isEqualToString:#"All Categories"] && !(self.currentCategory == nil))
{
filteredOnCategory = [self.allProducts filter:^(id elt)
{
return [[elt category] isEqualToString:self.currentCategory];
}];
[self.tableData removeAllObjects];
[self.tableData addObjectsFromArray:filteredOnCategory];
}
if (![self.currentCategory isEqualToString:#"All Categories"] && !(self.currentCategory == nil) && ![self.currentBrand isEqualToString:#"All Brands"] && !(self.currentBrand == nil)) {
filteredOnBrand = [self.allProducts filter:^(id elt) {
return [[elt brand] isEqualToString:self.currentBrand];
}];
filteredOnCategoryAndBrand = [filteredOnBrand filter:^(id elt) {
return [[elt category] isEqualToString:self.currentCategory];
}];
[self.tableData removeAllObjects];
[self.tableData addObjectsFromArray:filteredOnCategoryAndBrand];
}
}
You should also reload the table data afterwards of course, but I used a custom method for that, which I left out.

Resources