NSArray: Remove objects with duplicate properties - objective-c

I have an NSMutableArray that contains a few custom objects. Two of the objects have the same properties such as title and author. I want to remove the duplicate object and leave the other.
Asset *asset;
NSMutableArray *items = [[[NSMutableArray alloc] init] autorelease];
// First
asset = [[Asset alloc] init];
asset.title = #"Developer";
asset.author = #"John Smith";
[items addObject:asset];
[asset release];
// Second
asset = [[Asset alloc] init];
asset.title = #"Writer";
asset.author = #"Steve Johnson";
[items addObject:asset];
[asset release];
// Third
asset = [[Asset alloc] init];
asset.title = #"Developer";
asset.author = #"John Smith";
[items addObject:asset];
[asset release];
Since they are NOT the same object, but only having duplicate properties, how can I remove the duplicate?

You could create a HashSet and as you loop, you could add "title+author" concatenated set to the HashSet (NSMutableSet). As you arrive at each item, if the HashSet contains your key, either remove it or don't copy (either deleting or creating a copy without duplicates).
That makes it order n (1 loop)
Here's the NSMutableSet class:
http://developer.apple.com/library/mac/#documentation/Cocoa/Reference/Foundation/Classes/NSMutableSet_Class/Reference/NSMutableSet.html#//apple_ref/occ/cl/NSMutableSet
EDIT with code:
The meat of the code is the one loop.
void print(NSMutableArray *assets)
{
for (Asset *asset in assets)
{
NSLog(#"%#/%#", [asset title], [asset author]);
}
}
int main (int argc, const char * argv[])
{
NSAutoreleasePool * pool = [[NSAutoreleasePool alloc] init];
//
// Create the initial data set
//
Asset *asset;
NSMutableArray *items = [[[NSMutableArray alloc] init] autorelease];
// First
asset = [[Asset alloc] init];
asset.title = #"Developer";
asset.author = #"John Smith";
[items addObject:asset];
[asset release];
// Second
asset = [[Asset alloc] init];
asset.title = #"Writer";
asset.author = #"Steve Johnson";
[items addObject:asset];
[asset release];
// Third
asset = [[Asset alloc] init];
asset.title = #"Developer";
asset.author = #"John Smith";
[items addObject:asset];
[asset release];
NSLog(#"****Original****");
print(items);
//
// filter the data set in one pass
//
NSMutableSet *lookup = [[NSMutableSet alloc] init];
for (int index = 0; index < [items count]; index++)
{
Asset *curr = [items objectAtIndex:index];
NSString *identifier = [NSString stringWithFormat:#"%#/%#", [curr title], [curr author]];
// this is very fast constant time lookup in a hash table
if ([lookup containsObject:identifier])
{
NSLog(#"item already exists. removing: %# at index %d", identifier, index);
[items removeObjectAtIndex:index];
}
else
{
NSLog(#"distinct item. keeping %# at index %d", identifier, index);
[lookup addObject:identifier];
}
}
NSLog(#"****Filtered****");
print(items);
[pool drain];
return 0;
}
Here's the output:
Craplet[11991:707] ****Original****
Craplet[11991:707] Developer/John Smith
Craplet[11991:707] Writer/Steve Johnson
Craplet[11991:707] Developer/John Smith
Craplet[11991:707] distinct item. keeping Developer/John Smith at index 0
Craplet[11991:707] distinct item. keeping Writer/Steve Johnson at index 1
Craplet[11991:707] item already exists. removing: Developer/John Smith at index 2
Craplet[11991:707] ****Filtered****
Craplet[11991:707] Developer/John Smith
Craplet[11991:707] Writer/Steve Johnson

You can use the uniqueness of an NSSet to get distinct items from your original array. If you have the source code for Assest you will need to override the hash and isEqual: method on the Asset class.
#interface Asset : NSObject
#property(copy) NSString *title, *author;
#end
#implementation Asset
#synthesize title, author;
-(NSUInteger)hash
{
NSUInteger prime = 31;
NSUInteger result = 1;
result = prime * result + [self.title hash];
result = prime * result + [self.author hash];
return result;
}
-(BOOL)isEqual:(id)object
{
return [self.title isEqualToString:[object title]] &&
[self.author isEqualToString:[object author]];
}
- (void)dealloc {
[title release];
[author release];
[super dealloc];
}
#end
Then to implement:
Asset *asset;
NSMutableArray *items = [[[NSMutableArray alloc] init] autorelease];
// First
asset = [[Asset alloc] init];
asset.title = #"Developer";
asset.author = #"John Smith";
[items addObject:asset];
[asset release];
// Second
asset = [[Asset alloc] init];
asset.title = #"Writer";
asset.author = #"Steve Johnson";
[items addObject:asset];
[asset release];
// Third
asset = [[Asset alloc] init];
asset.title = #"Developer";
asset.author = #"John Smith";
[items addObject:asset];
[asset release];
NSLog(#"Items: %#", items);
NSSet *distinctItems = [NSSet setWithArray:items];
NSLog(#"Distinct: %#", distinctItems);
And if you need an array at the end you can just call [distinctItems allObjects]

First, I'd override the isEqual: method for Asset like this:
-(BOOL)isEqual:(Asset *)otherAsset {
return [self.title isEqual:otherAsset.title] && [self.author isEqual:otherAsset.author];
}
Then, if you want to avoid placing duplicates in the array in the first place:
NSUInteger idx = [items indexOfObject:asset]; // tests objects for equality using isEqual:
if (idx == NSNotFound) [items addObject:asset];
If the array already contains duplicates, then any algorithm that finds them has a run time already worse than linear, but I think creating a new array and only adding unique elements like above is the best algorithm. Something like this:
NSMutableArray *itemsWithUniqueElements = [NSMutableArray arrayWithCapacity:[items count]];
for (Asset *anAsset in items) {
if ([items indexOfObject:anAsset] == NSNotFound)
[itemsWithUniqueElements addObject:anAsset];
}
[items release];
items = [itemsWithUniqueElements retain];
In the worst case scenario (all elements are already unique) the number of iterations is:
1 + 2 + 3 + ... + n = n * (n+1) / 2
Which is still O(n^2) but is slightly better than #Justin Meiners' algorithm. No offense! :)

This is one way you could do it
:
NSMutableArray* toRemove = [NSMutableArray array];
for (Asset* asset1 in items)
{
for (Asset* asset2 in items)
{
if (asset1 != asset2)
{
if ([asset1.title isEqualToString:asset2.title] && [asset1.author isEqualToString:asset2.author])
{
[toRemove addObject:asset2];
}
}
}
}
for (Asset* deleted in toRemove)
{
[items removeObject:toRemove];
}

If you'd like your custom NSObject subclasses to be considered equal when their names are equal you may implement isEqual: and hash. This will allow you to add of the objects to an NSSet/NSMutableSet (a set of distinct objects).
You may then easily create a sorted NSArray by using NSSet's sortedArrayUsingDescriptors:method.
MikeAsh wrote a pretty solid piece about implementing custom equality: Friday Q&A 2010-06-18: Implementing Equality and Hashing

Related

Memory leak while handling Foundation Object from NSJSONSerialization

I'm struggling to fix a memory leak in a helper function I have made. The helper function takes the result of
+ (id)JSONObjectWithData:(NSData *)data options:(NSJSONReadingOptions)opt error:(NSError * _Nullable *)error
and converts all the leaf elements into NSStrings if they are NSNumbers.
Here is the method:
-(NSArray *) stringisizeObjects:(NSArray *)inputArray{
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
NSMutableArray *mutable = [[NSMutableArray alloc] initWithCapacity:[inputArray count]];
for (int i = 0; i < [inputArray count]; i++) {
NSArray *keys = [inputArray[i] allKeys];
NSMutableDictionary *addDictionary = [[NSMutableDictionary alloc] initWithCapacity:[keys count]];
for (int j = 0; j < [keys count]; j++) {
id theObject = [[inputArray[i] objectForKey:keys[j]]autorelease];
if ([theObject isKindOfClass:[NSNumber class]]) {
[addDictionary setObject:[theObject stringValue] forKey:keys[j]];
[theObject release];
}else if ([theObject isKindOfClass:[NSString class]]){
[addDictionary setObject:[inputArray[i] objectForKey:keys[j]] forKey:keys[j]];
}
}
[mutable addObject:addDictionary];
}
NSArray *returnArray = [mutable copy];
[mutable removeAllObjects];
[mutable release];
[pool drain];
return returnArray;
}
Here is how I get the input array.
id parsedThingy = [NSJSONSerialization JSONObjectWithData:resultJSONData options:1 error:&jsonDecodeError];
Before I can pass the result to my stringisize method I must ensure that I have an NSArray of NSDictionaries with matching keys.
NSArray *resultArray = [self stringisizeObjects:parsedThingy];
The X-Code memory leaks tool has pointed me to this method as the cause of my problem.
Instruments showing leaks
As you can see I have tried wrapping things in autorelease pools, autoreleasing and releasing. I just don't see any way forward here.
This is a non ARC project that runs 24/7.
Edit: I took the advice from Droppy and tried to re-write the method using mutableCopy. The leak is still there. At this point my only work around maybe to change the source of the JSON to send only strings. :(
-(NSArray *) stringisizeObjects2:(NSArray *)inputArray{
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
NSMutableArray *mutableArray = [inputArray mutableCopy];
for (int i = 0; i < [mutableArray count]; i++) {
NSMutableDictionary *mutableDict = [mutableArray[i] mutableCopy];
NSArray *keys = [mutableDict allKeys];
for (int j = 0; j < [keys count]; j++) {
if ([[mutableDict objectForKey:keys[j]] isKindOfClass:[NSNumber class]]) {
NSString *stringValue = [[mutableDict objectForKey:keys[j]] stringValue];
[mutableDict removeObjectForKey:keys[j]];
[mutableDict setObject:stringValue forKey:keys[j]];
}
}
mutableArray[i] = [mutableDict copy];
[mutableDict release];
}
NSArray *returnArray = [mutableArray copy];
[mutableArray release];
[pool drain];
return returnArray;
}
problem:
addDictionary called alloc but not call release or autorelease
returnArray = [mutable copy]; // did increase retainCount +1, need autorelease here
id theObject = [inputArray[i] objectForKey:keys[j]]; // not need autorelease or release for object that You not own
add NSAutoreleasePool to top an bottom here just do nothing
solution:
-(NSArray *) stringisizeObjects:(NSArray *)inputArray{
NSMutableArray *mutable = [[NSMutableArray alloc] initWithCapacity:[inputArray count]];
for (int i = 0; i < [inputArray count]; i++) {
NSArray *keys = [inputArray[i] allKeys];
NSMutableDictionary *addDictionary = [[NSMutableDictionary alloc] initWithCapacity:[keys count]];
for (int j = 0; j < [keys count]; j++) {
id theObject = [inputArray[i] objectForKey:keys[j]]; // not need autorelease
if ([theObject isKindOfClass:[NSNumber class]]) {
[addDictionary setObject:[theObject stringValue] forKey:keys[j]];
//[theObject release]; // not need release value here
}else if ([theObject isKindOfClass:[NSString class]]){
[addDictionary setObject:[inputArray[i] objectForKey:keys[j]] forKey:keys[j]];
}
}
[mutable addObject:addDictionary];
[addDictionary release]; // release after not use
}
NSArray *returnArray = [[[NSArray alloc] initWithArray:mutable] autorelease]; // auto release for return value
[mutable removeAllObjects];
[mutable release];
return returnArray;
}

Problem realeasing allocated objects

Crash occurs at [searchDict release]. If I switch order of the two latest lines it still crash on the latest line (now [searchArray release]). I'm quite new to Objective C and I guess I haven't got the alloc/release right... Help? :)
NSMutableDictionary *searchDict = [[NSMutableDictionary alloc] init];
NSMutableArray *searchArray = [[NSMutableArray alloc] init];
for(int i = 0; i < 2; i++) { // Run two times ("Favorites" and "All")
searchDict = [listOfItems objectAtIndex:i];
searchArray = [searchDict objectForKey:#"Entries"];
for (NSString *sTemp in searchArray) {
NSRange titleResultsRange = [sTemp rangeOfString:searchText options:NSCaseInsensitiveSearch];
if (titleResultsRange.length > 0)
[listOfItemsDynamic addObject:sTemp];
}
}
[searchArray release];
[searchDict release];
You allocate space and assign it to the variables:
NSMutableDictionary *searchDict = [[NSMutableDictionary alloc] init];
NSMutableArray *searchArray = [[NSMutableArray alloc] init];
But then you assign non locally-allocated data to them:
searchDict = [listOfItems objectAtIndex:i];
searchArray = [searchDict objectForKey:#"Entries"];
So basically, you don't need to allocate and release. Instead do something like this:
NSMutableDictionary *searchDict; // Just declartion, no allocation / init
NSMutableArray *searchArray; // Just declartion, no allocation / init
for(int i = 0; i < 2; i++) { // Run two times ("Favorites" and "All")
searchDict = [listOfItems objectAtIndex:i];
searchArray = [searchDict objectForKey:#"Entries"];
for (NSString *sTemp in searchArray) {
NSRange titleResultsRange = [sTemp rangeOfString:searchText options:NSCaseInsensitiveSearch];
if (titleResultsRange.length > 0)
[listOfItemsDynamic addObject:sTemp];
}
}
// No release needed here
If you are familiar with C, it is similar to:
char *pChar;
pChar = malloc(15); // allocate memory and assign to pChar
pChar = "Hello"; // assign new address to pChar
free(pChar); // Error here ;)

releasing a NSMutableArray is causing EXC_BAD_ACCESS error

so when i try to run the following code, i end up with a EXC_BAD_ACCESS error. it happens when i try to release a NSMutableArray retrievedAnalysisDataList. the array is a list of retrievedAnalysisData objects. if i try to either release the data list or if i set up the init with an autorelease, i get the same result. i'm kinda guessing it has something to do with the sorting section of the code since i don't have this issue with the retrievedAnalysisIDarray.
any ideas?
if (tempDict != NULL)
{
NSMutableArray *retrievedAnalysisDataList = [[NSMutableArray alloc] init];
NSMutableArray *retrievedAnalysisIDarray = [[NSMutableArray alloc] init];
for (id key in tempDict)
{
retrievedAnalysisData = [[RetrievedAnalysisData alloc] init];
retrievedAnalysisData.createDate = [[tempDict objectForKey:key] objectForKey:#"createdate"];
retrievedAnalysisData.ID = [[tempDict objectForKey:key] objectForKey:#"id"];
retrievedAnalysisData.mode = [[tempDict objectForKey:key] objectForKey:#"mode"];
retrievedAnalysisData.name = [[tempDict objectForKey:key] objectForKey:#"name"];
retrievedAnalysisData.numZones = [[tempDict objectForKey:key] objectForKey:#"numzones"];
retrievedAnalysisData.srcImg = [[tempDict objectForKey:key] objectForKey:#"srcimg"];
retrievedAnalysisData.type = [[tempDict objectForKey:key] objectForKey:#"type"];
//NSLog(#"\n createDate: %# \n id: %# \n mode: %# \n name: %# \n numzone: %# \n srcimg: %# \n type: %#", retrievedAnalysisData.createDate, retrievedAnalysisData.ID, retrievedAnalysisData.mode, retrievedAnalysisData.name, retrievedAnalysisData.numZones, retrievedAnalysisData.srcImg, retrievedAnalysisData.type);
[retrievedAnalysisDataList addObject:retrievedAnalysisData];
[retrievedAnalysisData release];
}
NSSortDescriptor *sortDescriptor;
sortDescriptor = [[[NSSortDescriptor alloc] initWithKey:#"createDate" ascending:YES] autorelease];
NSArray *sortDescriptors = [NSArray arrayWithObject:sortDescriptor];
NSArray *sortedRetrievedAnalysisDataList;
sortedRetrievedAnalysisDataList = [retrievedAnalysisDataList sortedArrayUsingDescriptors:sortDescriptors];
int count = [sortedRetrievedAnalysisDataList count];
for (int i = 0; i < count; i++) {
retrievedAnalysisData = [[RetrievedAnalysisData alloc] init];
retrievedAnalysisData = [sortedRetrievedAnalysisDataList objectAtIndex:i];
[retrievedAnalysisIDarray addObject:retrievedAnalysisData.ID];
[retrievedAnalysisData release];
}
dataCenter.sortedRetrievedAnalysisDataList = sortedRetrievedAnalysisDataList;
dataCenter.retrievedAnalysisIDarray = retrievedAnalysisIDarray;
[retrievedAnalysisIDarray release];
[retrievedAnalysisDataList release];
dataCenter.isRetrieve = [NSNumber numberWithInt:1];
[activityIndicator stopAnimating];
[picker reloadAllComponents];
picker.hidden = FALSE;
pickerToolBar.hidden = FALSE;
toolBar.hidden = TRUE;
innerCircle.hidden = TRUE;
outerCircle.hidden = TRUE;
trackLabel.hidden = TRUE;
displayGPSLabel.hidden = TRUE;
}
Your problem lies in this section of code:
retrievedAnalysisData = [[RetrievedAnalysisData alloc] init];
retrievedAnalysisData = [sortedRetrievedAnalysisDataList objectAtIndex:i];
[retrievedAnalysisIDarray addObject:retrievedAnalysisData.ID];
[retrievedAnalysisData release];
The first line allocates a new RetrievedAnalysisData, but then the second throws that away (leaking it) and places an object fetched from the array in the retrievedAnalysisData variable instead. You don't own this object fetched from the array, and you don't take ownership by calling retain. So the release on the fourth line is incorrect, releasing an object that you do not own.
Then when you release your NSMutableArray, it tries to release the object again and you get a crash because the object is already released.
To fix it, get rid of the useless first line, and also get rid of the incorrect release.
Without knowing the memory management of all your properties, it’s hard to see exactly what’s going on. But take a look here:
for (int i = 0; i < count; i++) {
retrievedAnalysisData = [[RetrievedAnalysisData alloc] init];
retrievedAnalysisData = [sortedRetrievedAnalysisDataList objectAtIndex:i];
[retrievedAnalysisIDarray addObject:retrievedAnalysisData.ID];
[retrievedAnalysisData release];
}
You call -release on an autoreleased object (retrievedAnalysisData). Try this instead:
for (int i = 0; i < count; i++) {
retrievedAnalysisData = [sortedRetrievedAnalysisDataList objectAtIndex:i];
[retrievedAnalysisIDarray addObject:retrievedAnalysisData.ID];
}

Different Keys Point to Same Object(s) in NSMutableDictionary

I have a custom object called Person that among other things contains an NSString field called descriptor, which stores what sort of person that Person object is (angry, sad, wild, happy, morose, etc). All of my Person objects are in an NSMutableArray, but I would like to store them in an NSMutableDictionary in such a manner:
Key: A, Object: An NSMutableArray where all Person objects have descriptor starting with 'A'
Key: B, Object: An NSMutableArray where all Person objects have descriptor starting with 'B'
Key: C, Object: An NSMutableArray where all Person objects have descriptor starting with 'C'
etc...
I've tried to do this in my code below, and at the comment //POINT 1, the keys and arrays seem to match up, but at //POINT 2, when I print out the complete dictionary, all the keys come up with the same values!
So I wanted to know why the NSMutableArray I seem to have is not being stored as I want it in the NSMutableDictionary?
- (void)buildDictionaryForIndexList {
NSMutableDictionary *tempDict = [[[NSMutableDictionary alloc] init] autorelease];
NSMutableArray *personsStartingWithLetter = [[NSMutableArray alloc] init];
NSMutableArray *indexList = [[[NSMutableArray alloc] init] autorelease];
NSInteger loopCounter = 1;
NSString *firstLetter = [[[NSString alloc] init] autorelease];
for (Person *v in persons) {
firstLetter = [[v descriptor] substringWithRange:NSMakeRange(0, 1)];
if ([indexList containsObject:firstLetter]) {
[personsStartingWithLetter addObject:v];
if (loopCounter == [persons count]) {
[tempDict setObject:personsStartingWithLetter forKey:firstLetter];
}
} else {
if (loopCounter > 1) {
//POINT 1
NSLog(#"%#",[indexList objectAtIndex:[indexList count]-1]);
for (Person *q in personsStartingWithLetter) {
NSLog(#"%#",[q descriptor]);
}
[tempDict setObject:personsStartingWithLetter forKey:[indexList objectAtIndex:([indexList count] - 1)]];
[personsStartingWithLetter removeAllObjects];
}
[indexList addObject:firstLetter];
[personsStartingWithLetter addObject:v];
} // else
loopCounter++;
} // for
//POINT 2
NSEnumerator *enumerator = [tempDict keyEnumerator];
for (NSString *str in enumerator) {
NSLog(#"%#",str);
for (Person *c in [tempDict objectForKey:str]) {
NSLog(#"%#",[c descriptor]);
}
}
self.dictionary = tempDict;
} // buildDictionaryForIndexList
So, for example, at POINT 1 my output is:
A
Angry
Amiable
B
Belligerent
C
Cool
...
W
Wild
but at POINT 2 my output is
T
Wild
J
Wild
A
Wild
...
W
Wild
Change [tempDict setObject:personsStartingWithLetter forKey:[indexList objectAtIndex:([indexList count] - 1)]]; (just after point 1) to [tempDict setObject:[[personsStartingWithLetter copy] autorelease] forKey:[indexList objectAtIndex:([indexList count] - 1)]];. The problem is that NSDictionary copies the key, but retains the value. Therefore, if you add a mutable array to the dictionary and then change it, the array in the dictionary also changes. You need to create a non-mutable copy of the array to put in the dictionary.
The whole method is a bit overcomplicated.
- (void)buildDictionaryForIndexList
{
NSMutableDictionary *tempDict = [[[NSMutableDictionary alloc] init] autorelease];
for (Person *v in persons)
{
NSString* firstLetter = [[v descriptor] substringWithRange:NSMakeRange(0, 1)];
NSMutableArray* personsStartingWithLetter = tempDict [firstLetter];
if (personsStartingWithLetter == nil)
{
personsStartingWithLetter = [NSMutableArray array];
tempDict [firstLetter] = personsStartingWithLetter;
}
[personsStartingWithLetter addObject:v];
} // for
self.dictionary = tempDict;
}
You start with an empty dictionary that will contain arrays. For every person, you check whether there is a suitable array or not, and if there isn't one, you create it. So now there is an array for the person, so you add it to the array. That's all.

Memory leak in NSMutableArray allocation

I am getting memory leak in NSMutableArray allocation.. in
NSMutableArray *contactsArray =[[NSMutableArray alloc] init];
CODE:
+(NSMutableArray*)getContacts
{
addressBook = ABAddressBookCreate();
NSArray* peopleArray = (NSArray*) ABAddressBookCopyArrayOfAllPeople(addressBook);
int noOfPeople = [peopleArray count];
NSMutableArray *contactsArray =[[NSMutableArray alloc] init];
for ( int i = 0; i < noOfPeople; i++)
{
ABRecordRef person = [peopleArray objectAtIndex:i];
ABRecordID personId = ABRecordGetRecordID(person);
NSString* personIdStr = [NSString stringWithFormat:#"%d", personId];
ContactDTO* contactDTO = [AddressBookUtil getContactDTOForId:personIdStr];
[contactsArray addObject:contactDTO];
}
[peopleArray release];
return contactsArray;
}
It is standard procedure that objects returned from methods (in your case, contactsArray) are autoreleased before returning.
You could either return [contactsArray autorelease]; or create it already autoreleased with [NSMutableArray arrayWithCapacity:noOfPeople]
You need to release contactsArray manually somewhere, because it does not define autorelease.