sort NSArray with objects - ios

Supposing NSArray has several objects which belong to two classes,
#interface FWNewsObj:NSObject
{
NSString *newsTitle;
NSDate *newsTime;
}
#end
#interface FWPhotoObj:NSObject
{
NSString *photoTitle;
NSDate *photoTime;
}
#end
I'd like to sort the NSArray with object's title (or time). However the title variable in each class has different names.
Then How can I do the sort?
Thanks.

What is instantly coming to my mind is(If I am understanding the Q correctly) :
1st create a dictionary, with titleNames as the keys, and value as the objects
Sort the keys array simply
Then create a sorted array of objects pulling them out of the dictionary, on basis of this sorted keys array

You have to write a custom compare method that both your classes implement. It must take one object as a parameter and return an NSComparisonResult (NSOrderedAscending, NSOrderedDescending or NSOrderedSame)
You can then use sortedArrayUsingSelector: with your own comparison method.
Example:
In FWNewsObj:
- (NSComparisonResult)compareTitle:(id)obj
{
NSAssert([obj isKindOfClass:[FWNewsObj class]] || [obj isKindOfClass:[FWPhotoObj class]], #"Don't know how to compare %# to %#", self, obj);
if ([obj isKindOfClass:[FWPhotoObj class]]) {
return [newsTitle compare:[(FWPhotoObj *)obj photoTitle]];
} else {
return [newsTitle compare:[(FWNewsObj *)obj newsTitle]];
}
}
In FWPhotoObj:
- (NSComparisonResult)compareTitle:(id)obj
{
NSAssert([obj isKindOfClass:[FWNewsObj class]] || [obj isKindOfClass:[FWPhotoObj class]], #"Don't know how to compare %# to %#", self, obj);
if ([obj isKindOfClass:[FWPhotoObj class]]) {
return [photoTitle compare:[(FWPhotoObj *)obj photoTitle]];
} else {
return [photoTitle compare:[(FWNewsObj *)obj newsTitleTitle]];
}
}
It would actually be easier to just define a title method in both classes that wraps either the photoTitle or the newsTitle. Then you can just use NSSortDescriptor with title as the key.

Related

Filter NSArray based on attribute prefix using indexOfObject:inSortedRange:options:usingComparator: binary search

I'm trying to implement a simple binary search on an array using indexOfObject:inSortedRange:options:usingComparator: and the behavior of this method is not quite what I expect and I have no idea what is missed.
Let's dive into details:
The array I'm trying to find items with is a custom object named City with has a readableName property of type NSString on it. A sample value of readableName is like Alabama, US.
The array is already sorted alphabetically based on the readableName property.
What I'm trying to achieve using this binary search is to be able to filter the array based on a prefix that is searched, for example, if Ams is searched, I can filter the cities that start with "Ams" like Amsterdam, Amstelveen, etc.
Since the array is sorted I'm trying to find the first item that has this prefix and the last item that has this prefix and create a subarray from the items in between.
The followings are the methods using which I have implemented the search for the head and the tail of the range that contains the cities with a given prefix:
+ (FilterRange *)findRangeHeadAndTailForPrefix:(NSString *)prefix inCityArray:(NSArray *)array {
FilterRange *result = [[FilterRange alloc] init];
result.startIndex = [self findRangeBordersForPrefix:prefix inArray:array lookingForHead:YES];
result.endIndex = [self findRangeBordersForPrefix:prefix inArray:array lookingForHead:NO];
return result;
}
+ (long)findRangeBordersForPrefix:(NSString *)prefix inArray:(NSArray *)array lookingForHead:(BOOL)shouldLookForHead {
NSRange searchRange = NSMakeRange(0, [array count]);
long foundIndex = [array indexOfObject:prefix
inSortedRange:searchRange
options:(shouldLookForHead ? NSBinarySearchingFirstEqual : NSBinarySearchingLastEqual)
usingComparator:^(id obj1, id obj2)
{
City *castedCity = (City *)([obj1 isKindOfClass:[City class]] ? obj1 : obj2);
NSString *castedPrefix = (NSString *)([obj1 isKindOfClass:[City class]] ? obj2 : obj1);
NSComparisonResult comparisonResult = ([[[castedCity readableName] lowercaseString] hasPrefix:[castedPrefix lowercaseString]] ? NSOrderedSame :
[[[castedCity readableName] lowercaseString] compare:[castedPrefix lowercaseString]]);
return comparisonResult;
}];
return foundIndex;
}
The problem is the behavior of indexOfObject:inSortedRange:options:usingComparator: method and here's how it behaves (Saw this using breakpoints and step-by-step execution of the comparator):
For the startIndex, the comparator is called with the last object in the array and the prefix and the result is NSOrderedDescending
Then the comparator is called with the first object in the array and the prefix and the result is NSOrderedAscending
Then it immediately stops searching and comparing other array items and returns the maximum long number value.
The same happens for the endIndex
So the search is never properly performed.
Please kindly note that I don't want to using filterUsingPredicate because of its time complexity. The array is already sorted, so a much better efficiency level can be achieved via binary search.
Does anyone have any idea that what I might have been missed. I guess there's something really obvious and I'm not paying attention to it.
Any help or idea is really appreciated :)
Normalization
First problem I see is that you're using lowercase string which doesn't work well for accented characters, ... As a first thing, let's write some helper to normalize a string.
#interface NSString(Normalize)
- (NSString *)normalized;
#end
#implementation NSString(Normalize)
- (NSString *)normalized {
NSMutableString *result = [NSMutableString stringWithString:self];
CFStringTransform((__bridge CFMutableStringRef)result, NULL, kCFStringTransformStripCombiningMarks, NO);
return [result lowercaseString];
}
#end
This method returns lowercased string with stripped combining marks. Not a very performant version, but you have an idea what needs to be done here.
Caching
Normalization can be expensive, let's cache it.
#interface City: NSObject
#property(nonatomic, strong) NSString *readableName;
#property(nonatomic, strong, readonly) NSString *normalizedReadableName;
#end
#implementation City {
NSString *_normalizedReadableName;
}
- (instancetype)initWithName:(NSString *)name {
if ((self = [super init]) == nil) { return nil; }
_readableName = name;
_normalizedReadableName = nil;
return self;
}
- (NSString *)normalizedReadableName {
if (_normalizedReadableName == nil) {
_normalizedReadableName = [_readableName normalized];
}
return _normalizedReadableName;
}
- (void)setReadableName:(NSString *)readableName {
_readableName = readableName;
_normalizedReadableName = nil;
}
+(instancetype)cityWithName:(NSString *)name {
return [[self alloc] initWithName:name];
}
#end
Again, it's up to you how you'd like to proceed here. Take it as an example.
Search
indexOfObject:inSortedRange:options:usingComparator: says:
The elements in the array must have already been sorted using the comparator cmp (it's the usingComparator argument). If the array is not sorted, the result is undefined.
You wrote:
The array is already sorted alphabetically based on the readableName property.
But in your comparator, you're using lowercaseString. It's unclear if it's sorted by lowercased string or not, can be another issue. Otherwise the result is undefined. We have to operate on the same string (sort, compare, hasPrefix, ...) - this is the reason for the normalization dance.
Let's create a sample array, shuffle it and sort it.
NSArray *shuffledCities = [#[
[City cityWithName:#"Čáslav"],
[City cityWithName:#"Čelákovice"],
[City cityWithName:#"Černošice"],
[City cityWithName:#"Černošín"],
[City cityWithName:#"Černovice"],
[City cityWithName:#"Červená Řečice"],
[City cityWithName:#"Červený Kostelec"],
[City cityWithName:#"Česká Kamenice"],
[City cityWithName:#"Česká Lípa"],
[City cityWithName:#"Česká Skalice"],
[City cityWithName:#"Česká Třebová"],
[City cityWithName:#"České Budějovice"],
[City cityWithName:#"České Velenice"],
[City cityWithName:#"Český Brod"],
[City cityWithName:#"Český Dub"],
[City cityWithName:#"Český Krumlov"],
[City cityWithName:#"Český Těšín"],
[City cityWithName:#"Chodová Planá"]
] shuffledArray]; // It's from the GameplayKit.framework
NSArray *sortedCities = [shuffledCities sortedArrayUsingComparator:^NSComparisonResult(City *_Nonnull city1, City *_Nonnull city2) {
return [city1.normalizedReadableName compare:city2.normalizedReadableName];
}];
Important point here is that we're sorting by normalizedReadableName property.
Let's pretend that the prefix is an argument from your function - we have to normalize it as well ...
NSString *prefix = #"čEsKÝ dub";
NSString *normalizedPrefix = [prefix normalized];
... otherwise our comparator won't work:
NSComparisonResult (^comparator)(id _Nonnull, id _Nonnull) = ^(id _Nonnull obj1, id _Nonnull obj2) {
// One has to be City and another one NSString
assert([obj1 isKindOfClass:[NSString class]] || [obj2 isKindOfClass:[NSString class]]);
assert([obj1 isKindOfClass:[City class]] || [obj2 isKindOfClass:[City class]]);
if ([obj1 isKindOfClass:[City class]]) {
return [[obj1 normalizedReadableName] hasPrefix:obj2] ? NSOrderedSame : [[obj1 normalizedReadableName] compare:obj2];
} else {
return [[obj2 normalizedReadableName] hasPrefix:obj1] ? NSOrderedSame : [obj1 compare:[obj2 normalizedReadableName]];
}
};
Another issue I see is that your comparator is wrong if obj2 is City. Comparator expects comparison of [obj1 compare:obj2], but in this case your comparator is returning [obj2 compare:obj1] (obj2 is City, obj1 is NSString).
We have fixed the comparator, let's search for the first city:
NSUInteger first = [sortedCities indexOfObject:normalizedPrefix
inSortedRange:NSMakeRange(0, sortedCities.count)
options:NSBinarySearchingFirstEqual
usingComparator:comparator];
if (first == NSNotFound) {
NSLog(#"Prefix \"%#\" not found", prefix);
return;
}
If found, search for the 2nd one:
NSUInteger last = [sortedCities indexOfObject:normalizedPrefix
inSortedRange:NSMakeRange(first, sortedCities.count - first)
options:NSBinarySearchingLastEqual
usingComparator:comparator];
// Shouldn't happen as our search range includes the first one
assert(last != NSNotFound);
NSLog(#"Prefix \"%#\" found", prefix);
NSLog(#" - First %lu: \"%#\"", (unsigned long)first, [sortedCities[first] readableName]);
NSLog(#" - Last %lu: \"%#\"", (unsigned long)last, [sortedCities[last] readableName]);
Sample outputs
All of them are correct.
Prefix "čEsKÝ dub" found
- First 14: "Český Dub"
- Last 14: "Český Dub"
Prefix "Praha" not found
Prefix "ceskÝ" found
- First 13: "Český Brod"
- Last 16: "Český Těšín"
Prefix "cernos" found
- First 2: "Černošice"
- Last 3: "Černošín"

Obj C - NSMutableArray containsObject return true

NSMutableArray containsObject returns true even the address and data is different.
I've seen this post NSMutableArray containsObject returns true, but it shouldnt
already but still I'm not finding my solution:
Below is my scenario:
NSMutableArray *destClasses = [NSMutableArray array];
id sourceClasses = [dict objectForKey:#"Classes"];
if ([sourceClasses isKindOfClass:[NSArray class]]) {
for (NSDictionary *class in sourceClasses) {
MyClass *a = [[MyClass alloc] init];
[a arrangeClassWithDictionary:classDict]; //this methods assigns value to a from classDict
if (![destClasses containsObject:a]) {
[destClasses addObject:a];
}
}
}
In the first iteration destClasses adds an MyClass object and on the second iteration [destClasses containsObject:a] returns true even though the a has different address and different values assigned.
What I'm doing wrong here. Please help.
I got the answer.
containsObject: which sends the isEqual: message to every object it
contains with your object as the argument. It does not use == unless
the implementation of isEqual: relies on ==.
I've to override the isEqual: method to provide equality checking for my object fields like below,
- (BOOL)isEqual:(id)object
{
BOOL result = NO;
if ([class isKindOfClass:[self class]]) {
MyClass *otherObject = object;
result = [self.name isEqualToString:[otherObject name]];
}
return result;
}

How to use one array as predicate for another array?

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

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).

Need to check NSArray holding multiple data types if the objects are null in iOS

I have an NSArray that I'm using in my iOS application which is holding data of three types:
NSDate, NSString, and NSNumber
What I would like to do is iterate this NSArray in a for loop to check to see if the objects are null, however, I'm unsure how to do this because the array contains objects of different types instead of one single type. This is what I am thinking of doing:
for (id widget in myArray)
{
if ([widget isKindOfClass:[NSDate class])
{
if (widget == nil) {
widget = #"";
}
}
else if ([widget isKindOfClass:[NSString class])
{
if (widget == nil) {
widget = #"";
}
}
else if ([widget isKindOfClass:[NSNumber class])
{
if (widget == nil) {
widget = #"";
}
}
}
However, I am getting the compilation error: "Fast enumeration variables can't be modified by ARC by default; declare the variable __strong to allow this." I am not sure ahead of time what type the object is going before the iteration, so how do I get around this?
NSArray can't hold nil values. Just check for NSNull
for (id widget in myArray)
{
if ([widget isKindOfClass:[NSNull class]])
//do what you need
}

Resources