My recent blog post, Zip, Map and Generics, looked at Swift's new zip() function for zipping together two arrays. zip returns a SequenceType with as many items as its shortest input sequence. What if we wanted to create a custom zip function that returned an array with the same length as the longest input and padded the shortest with nil?
The function would work something like:
let arrayOne = [1, 2, 3, 4]
let arrayTwo: [String?] = ["AAA", "BBB"]
let result = longZip(arrayOne, arrayTwo) // expect [(1, "AAA"), (2, "BBB"), (3, nil), (4, nil)]
If, for example, the input arrays were both non-optional strings, [String], our zipped result would need to return optionals, [String?], to allow for that padding. Therefore, using generics again, the signature to longZip, would look like this:
func longZip(arrayOne:[T], arrayTwo: [U]) -> [(T?, U?)]
This time, let's take a test driven development approach. Before writing any code, we'll write a test. I've created LongZipTests.swift which contains two test arrays:
let arrayOne = [1, 2, 55, 90]
let arrayTwo = ["AAA", "BBB"]
My tests will ensure the count of the output is 4, then loop over the output ensuring the output items match the input items or are nil where there is no input:
func testLongZip_v1()
{
let resultOne = longZip_v1(arrayOne, arrayTwo)
commonAssertions(resultOne)
}
func commonAssertions(results: [(Int?, String?)])
{
XCTAssert(results.count == 4, "Count")
for (idx: Int, result:(Int?, String?)) in enumerate(results)
{
if idx < arrayOne.count
{
XCTAssertEqual(result.0!, arrayOne[idx], "Array One Value")
}
else
{
XCTAssertNil(result.0, "Array One nil")
}
if idx < arrayTwo.count
{
XCTAssertEqual(result.1!, arrayTwo[idx], "Array Two Value")
}
else
{
XCTAssertNil(result.1, "Array Two nil")
}
}
}
The first implementation of longZip() is pretty simple, use max() to find the longest count, iterate over both arrays with a for loop and populate a return object with the items from those arrays or with nil if we've exceeded the count:
func longZip_v1(arrayOne:[T], arrayTwo: [U]) -> [(T?, U?)]
{
let n = max(arrayOne.count, arrayTwo.count)
var returnObjects = [(T?, U?)]()
forvar i = 0; i < n; i++
{
let returnObject: (T?, U?) = (i < arrayOne.count ? arrayOne[i] : nil, i < arrayTwo.count ? arrayTwo[i] : nil)
returnObjects.append(returnObject)
}
return returnObjects
}
Pressing command-u executes the tests which tend to work better if your fingers are crossed:
Test Case '-[FlexMonkeyExamplesTests.FlexMonkeyExamplesTests testLongZip_v1]' started.
Test Case '-[FlexMonkeyExamplesTests.FlexMonkeyExamplesTests testLongZip_v1]' passed (0.000 seconds).
You can use debugDescription() to be doubly certain:
[(Optional(1), Optional("AAA")), (Optional(2), Optional("BBB")), (Optional(55), nil), (Optional(90), nil)]
Now we have a working version, it's time to pick apart the code. Version two uses map() to convert from non-optional to optional and extend() to add the nils where required:
func longZip_v2(arrayOne:[T], arrayTwo: [U]) -> [(T?, U?)]
{
var arrayOneExtended = arrayOne.map({$0 asT?})
var arrayTwoExtended = arrayTwo.map({$0 asU?})
arrayOneExtended.extend([T?](count: max(0, arrayTwo.count - arrayOne.count), repeatedValue: nil))
arrayTwoExtended.extend([U?](count: max(0, arrayOne.count - arrayTwo.count), repeatedValue: nil))
returnArray(zip(arrayOneExtended, arrayTwoExtended))
}
...a new test case is added:
func testLongZip_v2()
{
let resultOne = longZip_v2(arrayOne, arrayTwo)
commonAssertions(resultOne)
}
Fingers crossed on both hands...
func testLongZip_v2()
{
let resultOne = longZip_v2(arrayOne, arrayTwo)
commonAssertions(resultOne)
}
Phew!
Test Case '-[FlexMonkeyExamplesTests.FlexMonkeyExamplesTests testLongZip_v2]' started.
Test Case '-[FlexMonkeyExamplesTests.FlexMonkeyExamplesTests testLongZip_v2]' passed (0.000 seconds).
Version two was good, but there's duplicated code and variables where I always favour constants. Sadly, the return of map() is immutable, so despite my best efforts, I've been unable to chain map and extend together. However, by moving that common code into a separate function, extendWithNil(), I've gone someway to mitigating that.
extendWithNil() accepts an array and an integer for the desired new length and returns an array of optionals of the input array's type:
func extendWithNil(array: [T], newCount: Int) -> [T?]
Again, before writing the code, let's write a test that checks the count and the that the newly added item is nil:
func testExtendWithNil()
{
let array = ["AAA", "BBB"]
let result = extendWithNil(array, 3)
XCTAssert(resultOne.count == 3, "Count")
XCTAssertNil(result[2], "Nil Added")
}
The guts of extendWithNil() are taken from version two of longZip():
func extendWithNil(array: [T], newCount: Int) -> [T?]
{
var returnArray = array.map({$0 asT?})
returnArray.extend([T?](count: max(0, newCount - array.count), repeatedValue: nil))
return returnArray
}
and finally, version three of longZip() uses extendWithNil() for a pretty tidy implementation:
func longZip_v3(arrayOne:[T], arrayTwo: [U]) -> [(T?, U?)]
{
let newCount = max(arrayOne.count, arrayTwo.count)
let arrayOneExtended = extendWithNil(arrayOne, newCount)
let arrayTwoExtended = extendWithNil(arrayTwo, newCount)
returnArray(zip(arrayOneExtended, arrayTwoExtended))
}
Before celebrating with a box of Honeycomb Fingers, a final test:
Test Case '-[FlexMonkeyExamplesTests.FlexMonkeyExamplesTests testExtendWithNil]' started.
[Optional("AAA"), Optional("BBB"), nil]
Test Case '-[FlexMonkeyExamplesTests.FlexMonkeyExamplesTests testExtendWithNil]' passed (0.001 seconds).
Test Case '-[FlexMonkeyExamplesTests.FlexMonkeyExamplesTests testLongZip_v3]' started.
Test Case '-[FlexMonkeyExamplesTests.FlexMonkeyExamplesTests testLongZip_v3]' passed (0.001 seconds).
I've created a new repository for these little technical examples and you can find all the source code for this post here.