Removing duplicates from array based on a property in Objective-C
Asked Answered
R

8

12

I have an array with custom objects. Each array item has a field named "name". Now I want to remove duplicate entries based on this name value.

How should I go about achieving this?

Reclusion answered 24/10, 2010 at 7:33 Comment(3)
Are the items in an Objective-C container like NSArray, or C/C++ (e.g., std::vector or a C array)?Yancy
sorry removed the tag, its objective c array with custom objects... "name" is one of its member variable, I want to filter based on this "name" value.Reclusion
Check my answer with code example: https://mcmap.net/q/909493/-removing-duplicates-from-nsmutablearrayGaussmeter
Y
10

You might have to actually write this filtering method yourself:

@interface NSArray (CustomFiltering)
@end

@implementation NSArray (CustomFiltering) 

- (NSArray *) filterObjectsByKey:(NSString *) key {
   NSMutableSet *tempValues = [[NSMutableSet alloc] init];
   NSMutableArray *ret = [NSMutableArray array];
   for(id obj in self) {
       if(! [tempValues containsObject:[obj valueForKey:key]]) {
            [tempValues addObject:[obj valueForKey:key]];
            [ret addObject:obj];
       }
   }
   [tempValues release];
   return ret;
}

@end
Yam answered 24/10, 2010 at 7:40 Comment(6)
its not a string array its a custom object array.... with a string name as the property... I want to filter based in this name propertyReclusion
@Jacob: Wrote my answer just as you edited yours. You really should use an NSMutableSet instead of a NSMutableArray for lookups, the performance boost from a hash lookup is quite allot better than a linear search.Nigrescent
A couple of comments. First, you should just use [NSMutableSet set] instead of alloc/init+release. Second, you should use -member: instead of -containsObject:. -containsObject: is documented as returning whether the given object is present in the set, without defining "present". It is reasonable to assume it uses pointer equality. -member: is documented as using -isEqual:, which is what you actually want to test with.Faxen
Thanks all of you guys... your answers & comments really helped me. Thumbs up for all of you...Reclusion
@Kevin, -containsObject: looks for membership the same way as -member: does. The only diff is returning BOOL versus the object itself.Peddle
@Kevin, I used alloc/init because I didn't want the set to be sitting around in the autorelease pool. The code only actually needs the set to be around while the loop is executing, so this way it can be released from memory at the earliest possible time.Yam
N
30

I do not know of any standard way to to do this provided by the frameworks. So you will have to do it in code. Something like this should be doable:

NSArray* originalArray = ... // However you fetch it
NSMutableSet* existingNames = [NSMutableSet set];
NSMutableArray* filteredArray = [NSMutableArray array];
for (id object in originalArray) {
   if (![existingNames containsObject:[object name]]) {
      [existingNames addObject:[object name]];
      [filteredArray addObject:object];
   }
}
Nigrescent answered 24/10, 2010 at 8:4 Comment(4)
Thanks PeyloW your answer was right on spot... thumbs up... I wish you best of luck in your endeavors....Reclusion
Shouldn't it be [object objectForKey:@"name"]; in side that loop?Sensitize
Really nice solutionSetiform
Got SIGABRT on this in 2017 O_OBulla
Y
10

You might have to actually write this filtering method yourself:

@interface NSArray (CustomFiltering)
@end

@implementation NSArray (CustomFiltering) 

- (NSArray *) filterObjectsByKey:(NSString *) key {
   NSMutableSet *tempValues = [[NSMutableSet alloc] init];
   NSMutableArray *ret = [NSMutableArray array];
   for(id obj in self) {
       if(! [tempValues containsObject:[obj valueForKey:key]]) {
            [tempValues addObject:[obj valueForKey:key]];
            [ret addObject:obj];
       }
   }
   [tempValues release];
   return ret;
}

@end
Yam answered 24/10, 2010 at 7:40 Comment(6)
its not a string array its a custom object array.... with a string name as the property... I want to filter based in this name propertyReclusion
@Jacob: Wrote my answer just as you edited yours. You really should use an NSMutableSet instead of a NSMutableArray for lookups, the performance boost from a hash lookup is quite allot better than a linear search.Nigrescent
A couple of comments. First, you should just use [NSMutableSet set] instead of alloc/init+release. Second, you should use -member: instead of -containsObject:. -containsObject: is documented as returning whether the given object is present in the set, without defining "present". It is reasonable to assume it uses pointer equality. -member: is documented as using -isEqual:, which is what you actually want to test with.Faxen
Thanks all of you guys... your answers & comments really helped me. Thumbs up for all of you...Reclusion
@Kevin, -containsObject: looks for membership the same way as -member: does. The only diff is returning BOOL versus the object itself.Peddle
@Kevin, I used alloc/init because I didn't want the set to be sitting around in the autorelease pool. The code only actually needs the set to be around while the loop is executing, so this way it can be released from memory at the earliest possible time.Yam
V
7

I know this is an old question but here is another possibility, depending on what you need.

Apple does provide a way to do this -- Key-Value Coding Collection Operators.

Object operators let you act on a collection. In this case, you want:

@distinctUnionOfObjects

The @distinctUnionOfObjects operator returns an array containing the distinct objects in the property specified by the key path to the right of the operator.

NSArray *distinctArray = [arrayWithDuplicates valueForKeyPath:@"@distinctUnionOfObjects.name"];

In your case, though, you want the whole object. So what you'd have to do is two-fold: 1) Use @distinctUnionOfArrays instead. E.g. If you had these custom objects coming from other collections, use @distinctUnionOfArray.myCollectionOfObjects 2) Implement isEqual: on those objects to return if their .name's are equal

Vertu answered 11/7, 2014 at 17:42 Comment(0)
S
2

I'm going to get flak for this...

You can convert your array into a dictionary. Not sure how efficient this is, depends on the implementation and comparison call, but it does use a hash map.

//Get unique entries
NSArray *myArray = @[@"Hello", @"World", @"Hello"];
NSDictionary *uniq = [NSDictionary dictionaryWithObjects:myArray forKeys:myArray];
NSLog(@"%@", uniq.allKeys);

*Note, this may change the order of your array.

Selves answered 28/4, 2014 at 15:55 Comment(2)
Hehehehe I LOVE this!Bulla
Edit: well I just spent the last 15 minutes fixing a bug in my code. Using this NSDictionary method will NOT keep the ORDER of your array. All my items were scrambled. That's what I get for using hacky wordk-arounds where OP prefaces "I'm going to get flak for this..." lolBulla
T
1

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

Theurich answered 2/5, 2013 at 13:34 Comment(0)
P
1

If you are worried about the order

NSArray * newArray =
        [[NSOrderedSet orderedSetWithArray:oldArray] array]; **// iOS 5.0 and later** 
Peephole answered 23/8, 2013 at 12:31 Comment(0)
A
1

It is quite simple in one line

NSArray *duplicateList = ... 

If you don't care about elements order then (unordered)

NSArray *withoutDUP1 = [[NSSet setWithArray:duplicateList] allObjects];

Keep the elements in order then (ordered)

NSArray *withoutDUP2 = [[NSOrderedSet orderedSetWithArray:duplicateList] array];
Agnail answered 18/8, 2016 at 16:48 Comment(1)
But how is your code filtering based on field "name" ?Arianaariane
R
0

Implement isEqual to make your objects comparable:

@interface SomeObject (Equality)
@end

@implementation SomeObject (Equality)

- (BOOL)isEqual:(SomeObject*)other
{
    return self.hash == other.hash;
}

- (NSUInteger)hash
{
    return self.name;///your case
}

@end

How to use:

- (NSArray*)distinctObjectsFromArray:(NSArray*)array
{
    return [array valueForKeyPath:@"@distinctUnionOfObjects.self"];
}
Roommate answered 16/2, 2016 at 10:5 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.