4
\$\begingroup\$

I have some data…

let points = [(718, 620), (4596, 1280), (410, 333), (4597, 993), 
(410, 337), (4597, 996), (428, 337), (4597, 1000), (431, 335), (4599, 1044)]

… that follows the pattern

[(pointStartOne.x, pointStartOne.y), (pointEndOne.x, pointEndOne.y),
(pointStartTwo.x, pointStartTwo.x) // ... 

I need to structure it to

typealias PointPair = [(start: Point, end: Point)]
struct Point {
    let x: Int
    let y: Int
}

Here is my code:

var result: PointPair = []
while i < points.count{
    result.append((Point(x: points[i].0, y: points[i].1), Point(x: points[i+1].0, y: points[i+1].1)))
    i+=2
}

I think maybe it is nice situation to use functional programming reduce

Here is code ituitive:

let result = zip((points.enumerated().filter({ (offset: Int, element: (Int, Int)) -> Bool in
        return offset%2==0
    })), (points.enumerated().filter({ (offset: Int, element: (Int, Int)) -> Bool in
        return offset%2==1
    }))).reduce(PointPair(), { (result: PointPair, arg) -> PointPair in
        let (one, other) = arg
        let (offset, point) = one
        let (offsetTwo, otherPoint) = other
        return result + [(Point(x: point.0,y: point.1), Point(x: otherPoint.0,y: otherPoint.1))]
    })

It is messy. Trying to use functional programming conveniently and elegantly.

Here is the improved code:

result = points.enumerated().reduce(into: PointPair(), {(temp, arg) in
        let (offset, point) = arg
        if offset % 2 == 0{
            temp += [(Point(x: point.0, y: point.1), Point(x: 0, y: 0) )]
        }
        else if let last = temp.last {
           temp[temp.count - 1] = (last.0, Point(x: point.0, y: point.1))
        }
    })
\$\endgroup\$

2 Answers 2

6
\$\begingroup\$

Let's start with the first version: A loop

var i = 0
while i < points.count {
    // ...
    i += 2
}

can be written as a for-loop over a stride:

for i in stride(from: 0, to: points.count, by: 2) {
    // ...
}

which restricts the scope of i to the loop body, and makes it a constant. Now each loop iteration appends exactly one element to var result: PointPair, which means that we can write this as a map operation:

let result: PointPair = stride(from: 0, to: points.count, by: 2)
    .map { i in
        (start: Point(x: points[i].0, y: points[i].1),
         end: Point(x: points[i+1].0, y: points[i+1].1))
    }

This may already be the “functional” version that you are looking for, but let's also have a look at your other two variants.

The “intuitive code” creates two additional arrays by filtering the original one for even/odd indices. After filtering the arrays the offsets are not needed, giving the warnings

Immutable value 'offset' was never used; consider replacing with '_' or removing it
Immutable value 'offsetTwo' was never used; consider replacing with '_' or removing it

This can be fixed with

let (_, point) = one
let (_, otherPoint) = other

A better way is to use compactMap() (which can be thought of as a combination of filter() and map()) to create arrays of the points with even/odd indices, without the element offsets.

Using reduce() for creating the result array is inefficient, as it creates intermediate arrays for each iteration. reduce(into:_:) would be an improvement, but actually this is just a map():

let result: PointPair = zip(
    points.enumerated().compactMap { (offset, element) -> Point? in
        offset % 2 == 0 ? Point(x: element.0, y: element.1) : nil },
    points.enumerated().compactMap { (offset, element) -> Point? in
        offset % 2 == 1 ? Point(x: element.0, y: element.1) : nil }
).map { ($0, $1) }

This looks a lot “less messy” than your original version, but still has the disadvantage of creating intermediate arrays.

I have not much to say about your last version. It uses reduce(into:) which is good to avoid the creation of intermediate arrays. The only disadvantage is that temporary pairs are created and modified in every second step.

So the “stride + map” variant looks like the best to me: It is efficient and easy to read.

A final remark: The type alias

typealias PointPair = [(start: Point, end: Point)]

is misleading, because it does not define a pair of points but an array of pairs of points. I would probably define a dedicated type for a pair of points instead

struct Line {
    let from: Point
    let to: Point
}

and then use [Line] as the result type.

\$\endgroup\$
2
\$\begingroup\$

one other idea is to add chunked to Array (source: HackingWithSwift):

import UIKit

extension Array {
    func chunked(into size: Int) -> [[Element]] {
        return stride(from: 0, to: count, by: size).map {
            Array(self[$0 ..< Swift.min($0 + size, count)])
        }
    }
}

struct Point {
    let x: Int
    let y: Int
}


struct Line {
    let from: Point
    let to: Point
}

just for better prints in Playground -> it removes the __lldb_expr prefix:

extension Point: CustomStringConvertible {
    var description: String { return "Point( x:\(x), y:\(y) )" }
}
extension Line: CustomStringConvertible {
    var description: String { return "Line( \(from) -> \(to) )" }
}

test it:

let points = [(718, 620), (4596, 1280), (410, 333), (4597, 993),
(410, 337), (4597, 996), (428, 337), (4597, 1000), (431, 335), (4599, 1044)]

let lines = points.map{ point in Point(x: point.0, y: point.1) }
                  .chunked(into: 2)
                  .map{ pointpair in Line(from: pointpair[0],to:pointpair[1]) }

dump(lines)


output:

▿ 5 elements
  ▿ Line( Point( x:718, y:620 ) -> Point( x:4596, y:1280 ) )
    ▿ from: Point( x:718, y:620 )
      - x: 718
      - y: 620
    ▿ to: Point( x:4596, y:1280 )
      - x: 4596
      - y: 1280
  ▿ Line( Point( x:410, y:333 ) -> Point( x:4597, y:993 ) )
    ▿ from: Point( x:410, y:333 )
      - x: 410
      - y: 333
    ▿ to: Point( x:4597, y:993 )
      - x: 4597
      - y: 993
  ▿ Line( Point( x:410, y:337 ) -> Point( x:4597, y:996 ) )
    ▿ from: Point( x:410, y:337 )
      - x: 410
      - y: 337
    ▿ to: Point( x:4597, y:996 )
      - x: 4597
      - y: 996
  ▿ Line( Point( x:428, y:337 ) -> Point( x:4597, y:1000 ) )
    ▿ from: Point( x:428, y:337 )
      - x: 428
      - y: 337
    ▿ to: Point( x:4597, y:1000 )
      - x: 4597
      - y: 1000
  ▿ Line( Point( x:431, y:335 ) -> Point( x:4599, y:1044 ) )
    ▿ from: Point( x:431, y:335 )
      - x: 431
      - y: 335
    ▿ to: Point( x:4599, y:1044 )
      - x: 4599
      - y: 1044

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.