Oscar Apeland
Oscar Apeland

Reputation: 6662

Remove objects with duplicate properties from Swift array

The question here involves removing duplicate objects from an array:

Removing duplicate elements from an array in Swift

I instead need to remove objects that are not themselves duplicates, but have specific duplicate properties such as id.


I have an array containing my Post objects. Every Post has an id property.

Is there a more effective way to find duplicate Post ID's in my array than

for post1 in posts {
    for post2 in posts {
        if post1.id == post2.id {
            posts.removeObject(post2)
        }
    }
}

Upvotes: 45

Views: 61332

Answers (16)

marouan azizi
marouan azizi

Reputation: 417

You can create an empty array "uniquePosts", and loop through your array "Posts" to append elements to "uniquePosts" and every time you append you have to check if you already append the element or you didn't. The method "contains" can help you.

func removeDuplicateElements(posts: [Post]) -> [Post] {
    var uniquePosts = [Post]()
    for post in posts {
        if !uniquePosts.contains(where: {$0.postId == post.postId }) {
            uniquePosts.append(post)
        }
    }
    return uniquePosts
}

Upvotes: 33

Victor Socaciu
Victor Socaciu

Reputation: 71

based on Danielvgftv's answer, we can rewrite it by leveraging KeyPaths, as follows:

extension Sequence {
    func removingDuplicates<T: Hashable>(withSame keyPath: KeyPath<Element, T>) -> [Element] {
        var seen = Set<T>()
        return filter { element in
            guard seen.insert(element[keyPath: keyPath]).inserted else { return false }
            return true
        }
    }
}

Usage:

struct Car {
    let id: UUID = UUID()
    let manufacturer: String
    // ... other vars
}

let cars: [Car] = [
    Car(manufacturer: "Toyota"),
    Car(manufacturer: "Tesla"),
    Car(manufacturer: "Toyota"),
]

print(cars.removingDuplicates(withSame: \.manufacturer)) // [Car(manufacturer: "Toyota"), Car(manufacturer: "Tesla")]

Upvotes: 7

Mike Neilens
Mike Neilens

Reputation: 121

A generic solution which preserves the original order is:

extension Array {
    func unique(selector:(Element,Element)->Bool) -> Array<Element> {
        return reduce(Array<Element>()){
            if let last = $0.last {
                return selector(last,$1) ? $0 : $0 + [$1]
            } else {
                return [$1]
            }
        }
    }
}

let uniquePosts = posts.unique{$0.id == $1.id }

Upvotes: 10

user652038
user652038

Reputation:

This is a less-specialized question than the more popular variant found here: https://stackoverflow.com/a/33553374/652038

Use that answer of mine, and you can do this:

posts.firstUniqueElements(\.id)

Upvotes: 1

Luca Angeletti
Luca Angeletti

Reputation: 59496

I am going to suggest 2 solutions.

Both approaches will need Post to be Hashable and Equatable

Conforming Post to Hashable and Equatable

Here I am assuming your Post struct (or class) has an id property of type String.

struct Post: Hashable, Equatable {
    let id: String
    var hashValue: Int { get { return id.hashValue } }
}

func ==(left:Post, right:Post) -> Bool {
    return left.id == right.id
}

Solution 1 (losing the original order)

To remove duplicated you can use a Set

let uniquePosts = Array(Set(posts))

Solution 2 (preserving the order)

var alreadyThere = Set<Post>()
let uniquePosts = posts.flatMap { (post) -> Post? in
    guard !alreadyThere.contains(post) else { return nil }
    alreadyThere.insert(post)
    return post
}

Upvotes: 64

Max Niagolov
Max Niagolov

Reputation: 714

There is a good example from this post

Here is an Array extension to return the unique list of objects based on a given key:

extension Array {
    func unique<T:Hashable>(map: ((Element) -> (T)))  -> [Element] {
        var set = Set<T>() //the unique list kept in a Set for fast retrieval
        var arrayOrdered = [Element]() //keeping the unique list of elements but ordered
        for value in self {
            if !set.contains(map(value)) {
                set.insert(map(value))
                arrayOrdered.append(value)
            }
        }

        return arrayOrdered
    }
}

for your example do:

let uniquePosts = posts.unique{$0.id ?? ""}

Upvotes: 4

Danielvgftv
Danielvgftv

Reputation: 635

My solution on Swift 5:

Add Extension:

extension Array where Element: Hashable {

    func removingDuplicates<T: Hashable>(byKey key: (Element) -> T)  -> [Element] {
         var result = [Element]()
         var seen = Set<T>()
         for value in self {
             if seen.insert(key(value)).inserted {
                 result.append(value)
             }
         }
         return result
     }

}

Class Client, important have the class like Hashable :

struct Client:Hashable {

   let uid :String
   let notifications:Bool

   init(uid:String,dictionary:[String:Any]) {
       self.uid = uid
       self.notifications = dictionary["notificationsStatus"] as? Bool ?? false
   }

   static func == (lhs: Client, rhs: Client) -> Bool {
    return lhs.uid == rhs.uid
   }

}

Use:

arrayClients.removingDuplicates(byKey: { $0.uid })

Have a good day swift lovers ♥️

Upvotes: 4

vitas168
vitas168

Reputation: 31

This works for multidimensional arrays as well:

for (index, element) in arr.enumerated().reversed() {
    if arr.filter({ $0 == element}).count > 1 {
        arr.remove(at: index)
    }
}

Upvotes: 3

GSerjo
GSerjo

Reputation: 4778

struct Post {
    var id: Int
}

extension Post: Hashable {
    var hashValue: Int {
        return id
    }

    static func == (lhs: Post, rhs: Post) -> Bool {
        return lhs.id == rhs.id
    }
}

and additional extension

public extension Sequence {
    func distinct<E: Hashable>() -> [E] where E == Iterator.Element {
        return Array(Set(self))
    }
}

Upvotes: 0

dfrib
dfrib

Reputation: 73166

(Updated for Swift 3)

As I mentioned in my comment to the question, you can make use of a modified Daniel Kroms solution in the thread we previously marked this post to be duplicate of. Just make your Post object hashable (implicitly equatable via id property) and implement a modified (using Set rather than Dictionary; the dict value in the linked method is not used anyway) version of Daniel Kroms uniq function as follows:

func uniq<S: Sequence, E: Hashable>(_ source: S) -> [E] where E == S.Iterator.Element {
    var seen = Set<E>()
    return source.filter { seen.update(with: $0) == nil }
}

struct Post : Hashable {
    var id : Int
    var hashValue : Int { return self.id }
}

func == (lhs: Post, rhs: Post) -> Bool {
    return lhs.id == rhs.id
}

var posts : [Post] = [Post(id: 1), Post(id: 7), Post(id: 2), Post(id: 1), Post(id: 3), Post(id: 5), Post(id: 7), Post(id: 9)]
print(Posts)
/* [Post(id: 1), Post(id: 7), Post(id: 2), Post(id: 1), Post(id: 3), Post(id: 5), Post(id: 7), Post(id: 9)] */


var myUniquePosts = uniq(posts)
print(myUniquePosts)
/* [Post(id: 1), Post(id: 7), Post(id: 2), Post(id: 3), Post(id: 5), Post(id: 9)] */

This will remove duplicates while maintaining the order of the original array.


Helper function uniq as a Sequence extension

Alternatively to using a free function, we could implement uniq as a constrained Sequence extension:

extension Sequence where Iterator.Element: Hashable {
    func uniq() -> [Iterator.Element] {
        var seen = Set<Iterator.Element>()
        return filter { seen.update(with: $0) == nil }
    }
}

struct Post : Hashable {
    var id : Int
    var hashValue : Int { return self.id }
}

func == (lhs: Post, rhs: Post) -> Bool {
    return lhs.id == rhs.id
}

var posts : [Post] = [Post(id: 1), Post(id: 7), Post(id: 2), Post(id: 1), Post(id: 3), Post(id: 5), Post(id: 7), Post(id: 9)]
print(posts)
/* [Post(id: 1), Post(id: 7), Post(id: 2), Post(id: 1), Post(id: 3), Post(id: 5), Post(id: 7), Post(id: 9)] */


var myUniquePosts = posts.uniq()
print(myUniquePosts)
/* [Post(id: 1), Post(id: 7), Post(id: 2), Post(id: 3), Post(id: 5), Post(id: 9)] */

Upvotes: 3

maslovsa
maslovsa

Reputation: 1639

Swift 3.1 Most Elegant Solution (Thanx dfri)

Apple Swift version 3.1 (swiftlang-802.0.51 clang-802.0.41)

func uniq<S: Sequence, E: Hashable>(source: S) -> [E] where E==S.Iterator.Element {
    var seen: [E:Bool] = [:]
    return source.filter({ (v) -> Bool in
        return seen.updateValue(true, forKey: v) == nil
    })
}

struct Post : Hashable {
    var id : Int
    var hashValue : Int { return self.id }
}

func == (lhs: Post, rhs: Post) -> Bool {
    return lhs.id == rhs.id
}

var Posts : [Post] = [Post(id: 1), Post(id: 7), Post(id: 2), Post(id: 1), Post(id: 3), Post(id: 5), Post(id: 7), Post(id: 9)]
print(Posts)
/* [Post(id: 1), Post(id: 7), Post(id: 2), Post(id: 1), Post(id: 3), Post(id: 5), Post(id: 7), Post(id: 9)] */


var myUniquePosts = uniq(source: Posts)

print(myUniquePosts)
/*[Post(id: 1), Post(id: 7), Post(id: 2), Post(id: 3), Post(id: 5), Post(id: 9)]*/

Upvotes: 0

Kiran K
Kiran K

Reputation: 949

In swift 3 refer below code:

let filterSet = NSSet(array: orignalArray as NSArray as! [NSObject])
let filterArray = filterSet.allObjects as NSArray  //NSArray
 print("Filter Array:\(filterArray)")

Upvotes: 2

mbdavis
mbdavis

Reputation: 4010

Preserving order, without adding extra state:

func removeDuplicates<T: Equatable>(accumulator: [T], element: T) -> [T] {
    return accumulator.contains(element) ?
        accumulator :
        accumulator + [element]
}

posts.reduce([], removeDuplicates)

Upvotes: 3

Gerard Taylor
Gerard Taylor

Reputation: 1

Instead of using a hashable object, you could just use a set. Take an attribute value that you want to remove duplicates for and use that as your test value. In my example, I am checking for duplicate ISBN values.

do {
    try fetchRequestController.performFetch()
    print(fetchRequestController.fetchedObjects?.count)
    var set = Set<String>()
    for entry in fetchRequestController.fetchedObjects! {
        if set.contains(entry.isbn!){
            fetchRequestController.managedObjectContext.delete(entry)
        }else {
            set.insert(entry.isbn!)
        }
    }
    try fetchRequestController.performFetch()
    print(fetchRequestController.fetchedObjects?.count) 
    } catch {
    fatalError()
}

Upvotes: 0

vikingosegundo
vikingosegundo

Reputation: 52227

use a Set

To use it, make your Post hashable and implement the == operator

import Foundation

class Post: Hashable, Equatable {
    let id:UInt
    let title:String
    let date:NSDate
    var hashValue: Int { get{
            return Int(self.id)
        }
    }

    init(id:UInt, title:String, date:NSDate){
        self.id = id
        self.title = title
        self.date = date

    }

}
func ==(lhs: Post, rhs: Post) -> Bool {
    return lhs.id == rhs.id
}



let posts = [Post(id: 11, title: "sadf", date: NSCalendar.currentCalendar().dateFromComponents({let c = NSDateComponents(); c.day = 1; c.month = 1; c.year = 2016; return c}())!),
             Post(id: 33, title: "sdfr", date: NSCalendar.currentCalendar().dateFromComponents({let c = NSDateComponents(); c.day = 3; c.month = 1; c.year = 2016; return c}())!),
             Post(id: 22, title: "sdfr", date: NSCalendar.currentCalendar().dateFromComponents({let c = NSDateComponents(); c.day = 1; c.month = 12; c.year = 2015; return c}())!),
             Post(id: 22, title: "sdfr", date: NSCalendar.currentCalendar().dateFromComponents({let c = NSDateComponents(); c.day = 1; c.month = 12; c.year = 2015; return c}())!)]

Create set from array with duplicates

let postsSet = Set(posts)

This is unordered, create a new array, apply order.

let uniquePosts = Array(postsSet).sort { (p1, p2) -> Bool in
    return p1.date.timeIntervalSince1970 < p2.date.timeIntervalSince1970
}

Instead of making your Post model hashable, you could also use a wrapper class. This wrapper class would use the post objects property to calculate the hash and equality.
this wrapper could be configurable through closure:

class HashableWrapper<T>: Hashable {
    let object: T
    let equal: (obj1: T,obj2: T) -> Bool
    let hash: (obj: T) -> Int

    var hashValue:Int {
        get {
            return self.hash(obj: self.object)
        }
    }
    init(obj: T, equal:(obj1: T, obj2: T) -> Bool, hash: (obj: T) -> Int) {
        self.object = obj
        self.equal = equal
        self.hash = hash
    }

}

func ==<T>(lhs:HashableWrapper<T>, rhs:HashableWrapper<T>) -> Bool
{
    return lhs.equal(obj1: lhs.object,obj2: rhs.object)
}

The Post could be simply

class Post {
    let id:UInt
    let title:String
    let date:NSDate

    init(id:UInt, title:String, date:NSDate){
        self.id = id
        self.title = title
        self.date = date
    }
}

Let's create some post as before

let posts = [
    Post(id: 3, title: "sadf", date: NSCalendar.currentCalendar().dateFromComponents({let c = NSDateComponents(); c.day = 1; c.month = 1; c.year = 2016; return c}())!),
    Post(id: 1, title: "sdfr", date: NSCalendar.currentCalendar().dateFromComponents({let c = NSDateComponents(); c.day = 3; c.month = 1; c.year = 2016; return c}())!),
    Post(id: 2, title: "sdfr", date: NSCalendar.currentCalendar().dateFromComponents({let c = NSDateComponents(); c.day = 1; c.month = 12; c.year = 2015; return c}())!),
    Post(id: 2, title: "sdfr", date: NSCalendar.currentCalendar().dateFromComponents({let c = NSDateComponents(); c.day = 1; c.month = 12; c.year = 2015; return c}())!),
    Post(id: 1, title: "sdfr", date: NSCalendar.currentCalendar().dateFromComponents({let c = NSDateComponents(); c.day = 3; c.month = 1; c.year = 2016; return c}())!)
]

Now we create wrapper objects for every post with closure to determine equality and the hash. And we create the set.

let wrappers = posts.map { (p) -> HashableWrapper<Post> in
    return HashableWrapper<Post>(obj: p, equal: { (obj1, obj2) -> Bool in
            return obj1.id == obj2.id
        }, hash: { (obj) -> Int in
            return Int(obj.id)
    })
}

let s = Set(wrappers)

Now we extract the wrapped objects and sort it by date.

let objects = s.map { (w) -> Post in
    return w.object
}.sort { (p1, p2) -> Bool in
    return p1.date.timeIntervalSince1970 > p2.date.timeIntervalSince1970
}

and

print(objects.map{$0.id})

prints

[1, 3, 2]

Upvotes: 1

user3441734
user3441734

Reputation: 17534

my 'pure' Swift solutions without Post conformance to Hashable (required by Set )

struct Post {
    var id: Int
}

let posts = [Post(id: 1),Post(id: 2),Post(id: 1),Post(id: 3),Post(id: 4),Post(id: 2)]

// (1)
var res:[Post] = []
posts.forEach { (p) -> () in
    if !res.contains ({ $0.id == p.id }) {
        res.append(p)
    }
}
print(res) // [Post(id: 1), Post(id: 2), Post(id: 3), Post(id: 4)]

// (2)
let res2 = posts.reduce([]) { (var r, p) -> [Post] in
    if !r.contains ({ $0.id == p.id }) {
        r.append(p)
    }
    return r
}

print(res2) // [Post(id: 1), Post(id: 2), Post(id: 3), Post(id: 4)]

I prefer (1) encapsulated into function (aka func unique(posts:[Post])->[Post] ), maybe an extension Array ....

Upvotes: 3

Related Questions