17

If I have two arrays e.g

let one = [1,3,5]
let two = [2,4,6]

I would like to merge/interleave the arrays in the following pattern [one[0], two[0], one[1], two[1] etc....]

//prints [1,2,3,4,5,6]
let comibned = mergeFunction(one, two)
print(combined)

What would be a good way to implement the combining function?

func mergeFunction(one: [T], _ two: [T]) -> [T] {
    var mergedArray = [T]()
    //What goes here
    return mergedArray
}
Dan Beaulieu
  • 19,406
  • 19
  • 101
  • 135
Neil Horton
  • 707
  • 6
  • 13

5 Answers5

34

If both arrays have the same length then this is a possible solution:

let one = [1,3,5]
let two = [2,4,6]

let merged = zip(one, two).flatMap { [$0, $1] }

print(merged) // [1, 2, 3, 4, 5, 6]

Here zip() enumerates the arrays in parallel and returns a sequence of pairs (2-element tuples) with one element from each array. flatMap() creates a 2-element array from each pair and concatenates the result.

If the arrays can have different length then you append the extra elements of the longer array to the result:

func mergeFunction<T>(one: [T], _ two: [T]) -> [T] {
    let commonLength = min(one.count, two.count)
    return zip(one, two).flatMap { [$0, $1] } 
           + one.suffixFrom(commonLength)
           + two.suffixFrom(commonLength)
}

Update for Swift 3:

func mergeFunction<T>(_ one: [T], _ two: [T]) -> [T] {
    let commonLength = min(one.count, two.count)
    return zip(one, two).flatMap { [$0, $1] } 
           + one.suffix(from: commonLength)
           + two.suffix(from: commonLength)
}
Martin R
  • 529,903
  • 94
  • 1,240
  • 1,382
  • `flatMap` does not "create a tuple". `zip` creates the tuples; in effect, it yields an array of tuples. `flatMap` flattens! Simple `map` would give `[[1, 2], [3, 4], [5, 6]]`; `flatMap` removes the extra level of array. – matt Jan 22 '16 at 18:31
  • 1
    You are still not doing yourself justice. `flatMap` "concatenates the result" and at the same time removes the very array that it created, leaving just the two elements. This is the ingenuity of your solution. Of course I thought of `zip` immediately, and hoped you would use it, but what you are doing here that is brilliant is making a little array only to destroy it again. You are passing through the little array as a way of removing the tuple wrapper, leaving just the two elements. – matt Jan 22 '16 at 19:34
  • :( Pfft, suddenly Swift needs the args to be called without destructuring the tuple. Eg: let merged = zip(one, two).flatMap { [$0.0, $0.1] } – original_username Jul 15 '17 at 12:27
  • 1
    @Charlesism: Yes, but that may change again. Is was discussed on the Swift evolution mailing list (e.g. here https://lists.swift.org/pipermail/swift-evolution-announce/2017-June/000386.html), I don't know what the current status is. – Martin R Jul 15 '17 at 18:35
6

If you're just looking to interleave two arrays, you could just do something like:

let maxIndex = max(one.count, two.count)
var mergedArray = Array<T>()
for index in 0..<maxIndex {
    if index < one.count { mergedArray.append(one[index]) }
    if index < two.count { mergedArray.append(two[index]) }
}

return mergedArray
Charles A.
  • 10,685
  • 1
  • 42
  • 39
  • Ill update the question to be more specific, I was using ints as an easy example, this is more about merging two arrays based on the index of the array element (one after another where possible) – Neil Horton Jan 22 '16 at 16:50
  • You could still use the approach above, just write a custom sort function instead of using – Charles A. Jan 22 '16 at 16:51
  • Im looking to insert based on position in the array not by the value of the element i.e [one[0], two[0], one[1], two[1], etc....] – Neil Horton Jan 22 '16 at 16:53
  • So you just want to interleave two arrays? I'll update my answer. – Charles A. Jan 22 '16 at 16:54
  • I thought so I was praying for a clever functional method :( I'll mark yours as correct – Neil Horton Jan 22 '16 at 16:58
  • 2
    Well, flatmap wouldn't interleave the items. You could potentially use map across a tuple of your two arrays, but I don't think that would be any clearer. – Charles A. Jan 22 '16 at 17:00
  • I don't agree. I think Martin R's answer is _much_ clearer. Swift likes functional programming better than imperative programming. – matt Jan 22 '16 at 18:35
  • @matt I like his answer better as well, but it only works with arrays of the same length. The OPs trivial example would work with that answer, but it's not a general answer. – Charles A. Jan 22 '16 at 20:03
  • True. That's a pity. I can see how to work around that with some preparation but it's not worth it for this example. – matt Jan 22 '16 at 20:10
  • @matt Seems like you'd be more or less stuck implementing a version of zip that worked across different length arrays, which would probably look a lot like my answer. That or a subarray of the longer, zip them, flatMap them, and append the remainder of the longer. – Charles A. Jan 22 '16 at 20:14
  • Actually my trick was to pad the shorter array with nils and call `flatMap` _twice_. As I say, not worth it. – matt Jan 22 '16 at 20:15
2

With Swift 5, you can use one of the following Playground sample codes in order to solve your problem.


#1. Using zip(_:_:) function and Collection's flatMap(_:) method

let one = [1, 3, 5, 7]
let two = [2, 4, 6]

let array = zip(one, two).flatMap({ [$0, $1] })
print(array) // print: [1, 2, 3, 4, 5, 6]

Apple states:

If the two sequences passed to zip(_:_:) are different lengths, the resulting sequence is the same length as the shorter sequence.


#2. Using an object that conforms to Sequence and IteratorProtocol protocols

struct InterleavedSequence<T>: Sequence, IteratorProtocol {

    private let firstArray: [T]
    private let secondArray: [T]
    private let thresholdIndex: Int
    private var index = 0
    private var toggle = false

    init(firstArray: [T], secondArray: [T]) {
        self.firstArray = firstArray
        self.secondArray = secondArray
        self.thresholdIndex = Swift.min(firstArray.endIndex, secondArray.endIndex)
    }

    mutating func next() -> T? {
        guard index < thresholdIndex else { return nil }
        defer {
            if toggle {
                index += 1
            }
            toggle.toggle()
        }
        return !toggle ? firstArray[index] : secondArray[index]
    }

}

let one = [1, 3, 5, 7]
let two = [2, 4, 6]

let sequence = InterleavedSequence(firstArray: one, secondArray: two)
let array = Array(sequence)
print(array) // print: [1, 2, 3, 4, 5, 6]
Imanou Petit
  • 89,880
  • 29
  • 256
  • 218
  • 1
    If `one` is longer than `two`, by any amount, your #2 and #3 will take one more element from `one`, than from two. Check my answer for an option of how to deal with that. –  Apr 11 '20 at 15:21
  • 1
    @Jessy Thanks for your comment. I've updated the examples to reflect this. – Imanou Petit Apr 11 '20 at 15:54
0
  /// Alternates between the elements of two sequences.
  /// - Parameter keepSuffix:
  /// When `true`, and the sequences have different lengths,
  /// the suffix of `interleaved`  will be the suffix of the longer sequence.
  func interleaved<Sequence: Swift.Sequence>(
    with sequence: Sequence,
    keepingLongerSuffix keepSuffix: Bool = false
  ) -> AnySequence<Element>
  where Sequence.Element == Element {
    keepSuffix
    ? .init { () -> AnyIterator<Element> in
      var iterators = (
        AnyIterator( self.makeIterator() ),
        AnyIterator( sequence.makeIterator() )
      )
      return .init {
        defer { iterators = (iterators.1, iterators.0) }
        return iterators.0.next() ?? iterators.1.next()
      }
    }
    : .init(
      zip(self, sequence).lazy.flatMap { [$0, $1] }
    )
  }
let oddsTo7 = stride(from: 1, to: 7, by: 2)
let evensThrough10 = stride(from: 2, through: 10, by: 2)
let oneThrough6 = Array(1...6)

XCTAssertEqual(
  Array( oddsTo7.interleaved(with: evensThrough10) ),
  oneThrough6
)

XCTAssertEqual(
  Array(
    oddsTo7.interleaved(with: evensThrough10, keepingLongerSuffix: true)
  ),
  oneThrough6 + [8, 10]
)
  • Very similar to existing answer https://stackoverflow.com/a/53842830/341994. – matt Apr 11 '20 at 13:59
  • No, those stop at the same time `zip` does. (Or after one more, which is probably never a desired result.) –  Apr 11 '20 at 14:43
  • 2
    I see, that's an important distinction. You might want to supplement your code with some words of explanation! Code alone is almost never all that useful. – matt Apr 11 '20 at 15:01
0

This can be done also using sequence(state:next:) (https://developer.apple.com/documentation/swift/sequence(state:next:)

For example:

let seq1 = [1,2,3,4]
let seq2 = [10,20,30,40,50, 60]

// Interleave two sequences that yield the same element type
let result = sequence(state: (false, seq1.makeIterator(), seq2.makeIterator()), next: { iters in
  iters.0 = !iters.0
  return iters.0 ? iters.1.next() : iters.2.next()
})

print(Array(result)) // Prints: [1, 10, 2, 20, 3, 30, 4, 40]
joan
  • 2,407
  • 4
  • 29
  • 35