prad
prad

Reputation: 1156

Is there any easy way to merge two arrays in swift along with removing duplicates?

Basically I need a version of appendContentsOf: which does not append duplicate elements.

Example

var a = [1, 2, 3]
let b = [3, 4, 5]

a.mergeElements(b)
//gives a = [1, 2, 3, 4, 5] //order does not matter

Upvotes: 18

Views: 30547

Answers (7)

Alexey Chekanov
Alexey Chekanov

Reputation: 1117

Swift 5 Updated

In case you need to combine multiple arrays.

func combine<T>(_ arrays: Array<T>?...) -> Set<T> {
    return arrays.compactMap{$0}.compactMap{Set($0)}.reduce(Set<T>()){$0.union($1)}
}

Usage examples:

1.

    let stringArray1 = ["blue", "red", "green"]
    let stringArray2 = ["white", "blue", "black"]
    
    let combinedStringSet = combine(stringArray1, stringArray2)

    // Result: {"green", "blue", "red", "black", "white"}
    let numericArray1 = [1, 3, 5, 7]
    let numericArray2 = [2, 4, 6, 7, 8]
    let numericArray3 = [2, 9, 6, 10, 8]
    let numericArray4: Array<Int>? = nil
    
    let combinedNumericArray = Array(combine(numericArray1, numericArray2, numericArray3, numericArray4)).sorted()

    // Result: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]

Upvotes: 13

J. Doe
J. Doe

Reputation: 13053

I combined my extension of Sequence and Array with this answer to provide easy syntax when merging arrays with custom objects by a single property:

extension Dictionary {
    init<S>(_ values: S, uniquelyKeyedBy keyPath: KeyPath<S.Element, Key>) where S : Sequence, S.Element == Value {
        let keys = values.map { $0[keyPath: keyPath] }

        self.init(uniqueKeysWithValues: zip(keys, values))
    }
}

// Unordered example
extension Sequence {
    func merge<T: Sequence, U: Hashable>(mergeWith: T, uniquelyKeyedBy: KeyPath<T.Element, U>) -> [Element] where T.Element == Element {
        let dictOld = Dictionary(self, uniquelyKeyedBy: uniquelyKeyedBy)
        let dictNew = Dictionary(mergeWith, uniquelyKeyedBy: uniquelyKeyedBy)

        return dictNew.merging(dictOld, uniquingKeysWith: { old, new in old }).map { $0.value }
    }
}

// Ordered example
extension Array {
    mutating func mergeWithOrdering<U: Hashable>(mergeWith: Array, uniquelyKeyedBy: KeyPath<Array.Element, U>) {
        let dictNew = Dictionary(mergeWith, uniquelyKeyedBy: uniquelyKeyedBy)

        for (key, value) in dictNew {
            guard let index = firstIndex(where: { $0[keyPath: uniquelyKeyedBy] == key }) else {
                append(value)
                continue
            }

            self[index] = value
        }
    }
}

Test:

@testable import // Your project name
import XCTest

struct SomeStruct: Hashable {
    let id: Int
    let name: String
}

class MergeTest: XCTestCase {
    let someStruct1 = SomeStruct(id: 1, name: "1")
    let someStruct2 = SomeStruct(id: 2, name: "2")
    let someStruct3 = SomeStruct(id: 2, name: "3")
    let someStruct4 = SomeStruct(id: 4, name: "4")

    var arrayA: [SomeStruct]!
    var arrayB: [SomeStruct]!

    override func setUp() {
        arrayA = [someStruct1, someStruct2]
        arrayB = [someStruct3, someStruct4]
    }

    func testMerging() {
        arrayA = arrayA.merge(mergeWith: arrayB, uniquelyKeyedBy: \.id)

        XCTAssert(arrayA.count == 3)
        XCTAssert(arrayA.contains(someStruct1))
        XCTAssert(arrayA.contains(someStruct3))
        XCTAssert(arrayA.contains(someStruct4))
    }

    func testMergingWithOrdering() {
        arrayA.mergeWithOrdering(mergeWith: arrayB, uniquelyKeyedBy: \.id)

        XCTAssert(arrayA.count == 3)
        XCTAssert(arrayA[0] == someStruct1)
        XCTAssert(arrayA[1] == someStruct3)
        XCTAssert(arrayA[2] == someStruct4)
    }
}

Upvotes: 0

Kyle Goslan
Kyle Goslan

Reputation: 10930

Swift 4.0 Version

extension Array where Element : Equatable {

  public mutating func mergeElements<C : Collection>(newElements: C) where C.Iterator.Element == Element{
    let filteredList = newElements.filter({!self.contains($0)})
    self.append(contentsOf: filteredList)
  }

}

As mentioned: The array passed to the function is the array of object that will be omitted from the final array

Upvotes: 5

Bryan
Bryan

Reputation: 15155

This is commonly called a union, which is possible in Swift using a Set:

let a = [1, 2, 3]
let b = [3, 4, 5]

let set = Set(a)
let union = set.union(b)

Then you can just convert the set into an array:

let result = Array(union)

Upvotes: 11

Kyle Goslan
Kyle Goslan

Reputation: 10930

Swift 3.0 version of the accepted answer.

extension Array where Element : Equatable{

  public mutating func mergeElements<C : Collection>(newElements: C) where C.Generator.Element == Element{
    let filteredList = newElements.filter({!self.contains($0)})
    self.append(contentsOf: filteredList)
  }

}

Note: Worth saying here that the array passed to the function is the array of object that will be omitted from the final array. Important if your merging an array of objects where the Equatable property may be the same but others may differ.

Upvotes: 3

Oleg Gordiichuk
Oleg Gordiichuk

Reputation: 15512

Simply :

let unique = Array(Set(a + b))

Upvotes: 39

prad
prad

Reputation: 1156

An Array extension can be created to do this.

extension Array where Element : Equatable{

    public mutating func mergeElements<C : CollectionType where C.Generator.Element == Element>(newElements: C){
       let filteredList = newElements.filter({!self.contains($0)})
       self.appendContentsOf(filteredList)
   }
}

Of course, this is useful for only Equatable elements.

Upvotes: 1

Related Questions