19

Suppose I have a class Event, and it has 2 properties: action (NSString) and date (NSDate).

And suppose I have an array of Event objects. The problem is that "date" properties can match.

I need to remove the duplicates, meaning that 2 different objects with the same date IS a duplicate.

I can remove duplicates in any array of strings or nsdates, they are easy to compare. But how to do it with complex objects, where their properties are to be compared?

Don't ask me what I did so far, cos' the only thing coming in my mind is a bubble sort, but it's a newbie solution, and slow.

Quite any help is highly appreciated (links, tuts, code).

Thanks in advance.

EDIT

Thanks to dasblinkenlight, I have made a custom method:

- (NSArray *)removeDuplicatesInArray:(NSArray*)arrayToFilter{

    NSMutableSet *seenDates = [NSMutableSet set];
    NSPredicate *dupDatesPred = [NSPredicate predicateWithBlock: ^BOOL(id obj, NSDictionary *bind) {
        YourClass *e = (YourClass*)obj;
        BOOL seen = [seenDates containsObject:e.propertyName];
        if (!seen) {
            [seenDates addObject:e.when];
        }
        return !seen;
    }];
    return [arrayToFilter filteredArrayUsingPredicate:dupDatesPred];
} 

Here YourClass is the name of your class the object belongs to, and propertyName is the property of that object you are going to compare.

Suppose self.arrayWithObjects contains the objects of YourClass.

After populating it, use

self.arrayWithObjects = [self removeDuplicatesInArray:self.arrayWithObjects];

and you are done.

All credits to dasblinkenlight. Cheers!

3
  • Works if you take the aproach for prevent adding Event objects with date equal to another from the Events already in the array? Commented Nov 21, 2012 at 18:07
  • Naah, the array is already populated from a huge JSON. The process is too complex, I think it's easier to remove the duplicates after. Commented Nov 21, 2012 at 18:08
  • Sort the array by date (which is fairly easy using one of the several sortedArray... functions of NSArray), then step through the sorted array, copying to a new NSMutableArray, skipping any element that has the same date as the last one. Commented Nov 21, 2012 at 18:19

6 Answers 6

26

You can create an NSMutableSet of dates, iterate your event list, and add only events the date for which you have not encountered before.

NSMutableSet *seenDates = [NSMutableSet set];
NSPredicate *dupDatesPred = [NSPredicate predicateWithBlock: ^BOOL(id obj, NSDictionary *bind) {
    Event *e = (Event*)obj;
    BOOL seen = [seenDates containsObject:e.date];
    if (!seen) {
        [seenDates addObject:e.date];
    }
    return !seen;
}];
NSArray *events = ... // This is your array which needs to be filtered
NSArray *filtered = [events filteredArrayUsingPredicate:dupDatesPred];
Sign up to request clarification or add additional context in comments.

Comments

3

Wouldn't this work with kvc. I suppose the following solution could work in your case;

Event *event1 = [[Event alloc] init];
event1.name = @"Event1";
event1.date = [NSDate distantFuture];
Event *event2 = [[Event alloc] init];
event2.name = @"Event2";
event2.date = [NSDate distantPast];
Event *event3 = [[Event alloc] init];
event3.name = @"Event1";
event3.date = [NSDate distantPast];
NSArray *array = @[event1, event2, event3];

NSArray *filteredEvents =  [array valueForKeyPath:@"@distinctUnionOfObjects.name"];

1 Comment

@distinctUnionOfObjects.name will only return an array of uniques property called "name". Not the rest of the objects.
2
NSMutableArray *leftObjects = [duplicateArray mutableCopy];
NSMutableArray *nonDuplicates = [NSMutableArray new];
while (leftObjects.count > 0)
{
    YourClass *object = [leftObjects objectAtIndex:0];

    // find all objects matching your comaprison equality definition for YourClass
    NSArray *matches = [leftObjects filteredArrayUsingPredicate:
                        [NSPredicate predicateWithBlock:^BOOL(YourClass *evaluatedObject, NSDictionary *bindings)
                         {
                             return (evaluatedObject.name == object.name);
                         }] ];
    [leftObjects removeObjectsInArray:matches];

    // add first object (arbitrary, may decide which duplicate to pick)
    [nonDuplicates addObject:matches.firstObject];
}

Comments

0

I think the most effective way is to use NSDictionary to store the object as value and the property value as key, and before adding any object to the dictionary you check if it exist or not which is O(1) operation, i.e. the whole process will take O(n)

Here is the code

- (NSArray *)removeDuplicatesFromArray:(NSArray *)array onProperty:(NSString *)propertyName {
    NSMutableDictionary *dictionary = [[NSMutableDictionary alloc] init];

    for (int i=0; i<array.count; i++) {

        NSManagedObject *currentItem = array[i];
        NSString *propertyValue = [currentItem valueForKey:propertyName];

        if ([dictionary valueForKey:propertyValue] == nil) {
            [dictionary setValue:currentItem forKey:propertyValue];
        }
    }

    NSArray *uniqueItems = [dictionary allValues];

    return uniqueItems;
}

you can use it as the following

self.arrayWithObjects = [self removeDuplicatesFromArray:self.arrayWithObjects onProperty:@"when"]; 

Comments

0

Here is a Swift extension on the NSArray class that removes duplicates for the specified property:

extension NSArray {
/**
 - parameter property: the name of the property to check for duplicates

 - returns: an array of objects without objects that share an identical value of the specified property
*/
  func arrayWithoutObjectsOfDuplicateProperty(property : String) -> [AnyObject] {
    var seenInstances = NSMutableSet()

    let predicate = NSPredicate { (obj, bind) -> Bool in
      let seen = seenInstances.containsObject(obj.valueForKey(property)!)

      if !seen {
        seenInstances.addObject(obj.valueForKey(property)!)
      }
        return !seen
      }      
      return self.filteredArrayUsingPredicate(predicate)
  }
}

Comments

0

Here is working Swift code snipped which does remove duplicates while keeping the order of elements.

// Custom Struct. Can be also class. 
// Need to be `equitable` in order to use `contains` method below
struct CustomStruct : Equatable {
      let name: String
      let lastName : String
    }

// conform to Equatable protocol. feel free to change the logic of "equality"
func ==(lhs: CustomStruct, rhs: CustomStruct) -> Bool {
  return (lhs.name == rhs.name && lhs.lastName == rhs.lastName)
}

let categories = [CustomStruct(name: "name1", lastName: "lastName1"),
                  CustomStruct(name: "name2", lastName: "lastName1"),
                  CustomStruct(name: "name1", lastName: "lastName1")]
print(categories.count) // prints 3

// remove duplicates (and keep initial order of elements)
let uniq1 : [CustomStruct] = categories.reduce([]) { $0.contains($1) ? $0 : $0 + [$1] }
print(uniq1.count) // prints 2 - third element has removed

And just if you are wondering how this reduce magic works - here is exactly the same, but using more expanded reduce syntax

let uniq2 : [CustomStruct] = categories.reduce([]) { (result, category) in
  var newResult = result
  if (newResult.contains(category)) {}
  else {
    newResult.append(category)
  }
  return newResult
}
uniq2.count // prints 2 - third element has removed

You can simply copy-paste this code into a Swift Playground and play around.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.