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

i0S Swift Issue

Question or problem in the Swift programming language:

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

How to solve the problem:

Solution 1:

Simply :

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

Solution 2:

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)

Solution 3:

Swift 4.0 Version

extension Array where Element : Equatable {

  public mutating func mergeElements(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

Solution 4:

Swift 3.0 version of the accepted answer.

extension Array where Element : Equatable{

  public mutating func mergeElements(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.

Solution 5:

Swift 5
Updated

In case you need to combine multiple arrays.

func combine(_ arrays: Array?...) -> Set {
    return arrays.compactMap{$0}.compactMap{Set($0)}.reduce(Set()){$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? = nil
    
    let combinedNumericArray = Array(combine(numericArray1, numericArray2, numericArray3, numericArray4)).sorted()

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

Solution 6:

An Array extension can be created to do this.

extension Array where Element : Equatable{

    public mutating func mergeElements(newElements: C){
       let filteredList = newElements.filter({!self.contains($0)})
       self.appendContentsOf(filteredList)
   }
}

Of course, this is useful for only Equatable elements.

Solution 7:

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(_ values: S, uniquelyKeyedBy keyPath: KeyPath) 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(mergeWith: T, uniquelyKeyedBy: KeyPath) -> [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(mergeWith: Array, uniquelyKeyedBy: KeyPath) {
        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)
    }
}

Hope this helps!