12

I have an array, with custom objects.

I Would like to pop the repeated objects, with the repeated properties:

let product = Product()
product.subCategory = "one"

let product2 = Product()
product2.subCategory = "two"

let product3 = Product()
product3.subCategory = "two"

let array = [product,product2,product3]

in this case, pop the product2 or product3

rmaddy
  • 314,917
  • 42
  • 532
  • 579
Vinícius Albino
  • 527
  • 1
  • 7
  • 23

6 Answers6

24

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
    }
}

using this you can so this

let unique = [product,product2,product3].unique{$0.subCategory}

this has the advantage of not requiring the Hashable and being able to return an unique list based on any field or combination

Ciprian Rarau
  • 3,040
  • 1
  • 30
  • 27
  • From the API design guidelines this method should probably be called `uniqueing`, and a mutating variant `unique` could me written. That seems a little odd to me so I'd push for `removingDuplicates` and `removeDuplicates` as the pair of method names. - https://swift.org/documentation/api-design-guidelines/#strive-for-fluent-usage – calql8edkos Jan 30 '19 at 19:47
10

You can use Swift Set:

let array = [product,product2,product3]

let set = Set(array)

You have to make Product conform to Hashable (and thus, Equatable) though:

class Product : Hashable {
    var subCategory = ""

    var hashValue: Int { return subCategory.hashValue }
}

func ==(lhs: Product, rhs: Product) -> Bool {
    return lhs.subCategory == rhs.subCategory
}

And, if Product was a NSObject subclass, you have to override isEqual:

override func isEqual(object: AnyObject?) -> Bool {
    if let product = object as? Product {
        return product == self
    } else {
        return false
    }
}

Clearly, modify those to reflect other properties you might have in your class. For example:

class Product : Hashable {
    var category = ""
    var subCategory = ""

    var hashValue: Int { return [category, subCategory].hashValue }
}

func ==(lhs: Product, rhs: Product) -> Bool {
    return lhs.category == rhs.category && lhs.subCategory == rhs.subCategory
}
Rob
  • 415,655
  • 72
  • 787
  • 1,044
  • 3
    You should mention that using a set will not preserve the array order. – Duncan C Nov 22 '15 at 23:03
  • @Rob im using RLMObject (with is an NSObject, so conforms to Equatable, and Hashable). Tried to override var hashValue: Int { return subCategoria.hashValue } ` but im only getting the error `Cannot invoke initializer for type 'Set<_>' with an argument list of type '([Product])' ` – Vinícius Albino Nov 22 '15 at 23:05
  • Hmm. That's the error that you get if `Product` didn't conform to `Hashable`. You might try explicitly declaring it (or extending it) as conforming to `Hashable`. – Rob Nov 22 '15 at 23:12
  • Using override on hashValue declaration should resolve the problem don't ? Cause if i try to explicit conforms to protocol Hashable i get `Redundant conformance of 'Product' to protocol 'Hashable'` – Vinícius Albino Nov 22 '15 at 23:15
  • 1
    @user1108474 Just tried it myself with just some super and subclass. `override hasValue` should work. Are you also implementing the `Equatable` functions? – R Menke Nov 22 '15 at 23:26
  • 2
    @user1108474 - Correct, if it already conformed to `Hashable`, then you shouldn't declare it to conform again. It was just that a failure to conform to `Hashable` was the only way I could reproduce the error you shared with us. Anyway, I tested this with `RLMObject` based `Product`, and it worked fine (though I had to override `isEqual`, too, as shown in revised answer). – Rob Nov 22 '15 at 23:45
2

If Product conforms to Equatable, where a product is equal based on it's subcategory (and you don't care about order), you can add the objects to a set, and take an array from that set:

let array = [product,product2,product3]
let set = NSSet(array: array)
let uniqueArray = set.allObjects

or

let array = [product,product2,product3]
let set = Set(array)
let uniqueArray = Array(set)
JAL
  • 41,701
  • 23
  • 172
  • 300
  • Was writing my answer and tried your sweet Set solution. It did not work in an extension of Array :/ – R Menke Nov 22 '15 at 22:48
  • Found it, for a Set based solution, the objects need to be `Hashable` – R Menke Nov 22 '15 at 22:50
  • @RMenke Ah that's right, I assumed the object conformed to `NSObject`, which conforms to `Equatable` and `Hashable`. – JAL Nov 22 '15 at 22:51
1
class Product {
    var subCategory: String = ""
}

let product = Product()
product.subCategory = "one"

let product2 = Product()
product2.subCategory = "two"

let product3 = Product()
product3.subCategory = "two"

let array = [product,product2,product3]

extension Product : Hashable {
    var hashValue: Int {
        return subCategory.hashValue
    }
}
func ==(lhs: Product, rhs: Product)->Bool {
    return lhs.subCategory == rhs.subCategory
}

let set = Set(array)
set.forEach { (p) -> () in
    print(p, p.subCategory)
}
/*
Product one
Product two
*/

if an item is part of set or not doesn't depends on hashValue, it depends on comparation. if your product conform to Hashable, it should conform to Equatable. if you need that the creation of the set depends solely on subCategory, the comparation should depends solely on subCategory. this can be a big trouble, if you need to compare your products some other way

user3441734
  • 16,722
  • 2
  • 40
  • 59
1

If your class conforms to protocol Hashable and you would like to keep the original array order you can create an extension as follow:

extension Array where Element: Hashable {
    var uniqueElements: [Element] {
        var elements: [Element] = []
        for element in self {
            if let _ = elements.indexOf(element) {
                print("item found")
            } else {
                print("item not found, add it")
                elements.append(element)
            }
        }
        return elements
    }
}
Leo Dabus
  • 229,809
  • 59
  • 489
  • 571
1

Here is a KeyPath based version of the Ciprian Rarau' solution

extension Array {
    func unique<T: Hashable>(by keyPath: KeyPath<Element, T>) -> [Element] {
        var set = Set<T>()
        return self.reduce(into: [Element]()) { result, value in
            guard !set.contains(value[keyPath: keyPath]) else {
                return
            }
            set.insert(value[keyPath: keyPath])
            result.append(value)
        }
    }
}

example usage:

let unique = [product, product2, product3].unique(by: \.subCategory)
Tomasz Pe
  • 696
  • 7
  • 19