5

I don't like my following implementation of surroundingPositions to get the x- and y-Coordinates surrounding a specific position, because in my opinion it is too long for the simple intent and it has a pyramid of doom structure.

struct Position: CustomStringConvertible {

    let x, y: Int

    var surroundingPositions: [Position] {
        var surroundingPositions: [Position] = []
        for x in (self.x - 1)...(self.x + 1) {
            for y in (self.y - 1)...(self.y + 1) {
                if !(x == self.x && y == self.y) {
                    surroundingPositions.append(Position(x: x, y: y))
                }
            }
        }
        return surroundingPositions
    }

    var description: String {
        return "(\(x),\(y))"
    }

}

Usage:

let testPosition = Position(x: 1, y: 1)
print(testPosition.surroundingPositions)
// Output: [(0,0), (0,1), (0,2), (1,0), (1,2), (2,0), (2,1), (2,2)]

What is the shortest way to implement this with the same (correct) result? I'm thinking of functions like map, filter, reduce, etc., but can't find the right combination so far ...

Benno Kress
  • 2,637
  • 27
  • 39

2 Answers2

4

Well, you could always hardcode it. Here, I hardcoded the deltas and create a new Position for each delta:

var surroundingPositionsHardcoded: [Position] {
    let deltas: [(Int, Int)] = [(-1, -1), (-1, 0), (-1, +1), (0, -1), (0, +1), (+1, -1), (+1, 0), (+1, +1)]
    return deltas.map { Position(x: x+$0.0, y: y+$0.1) }
}

Or, you could compute the deltas using map. This has the added benefit that you could increase the surrounding distance.

var surroundingPositionsComputed: [Position] {
    let deltas = (-1...1).map { dx in (-1...1).map { dy in (dx, dy) } }
        .joined()
        .filter { $0.0 != 0 || $0.1 != 0 }
    return deltas.map { Position(x: x+$0.0, y: y+$0.1) }
}
thm
  • 1,217
  • 10
  • 12
  • 1
    Especially the second solution was what I was looking for. Short and elegant, thanks! – Benno Kress May 25 '17 at 13:20
  • 1
    I should point out (for possible future readers) that the second approach should not be preferred if the neighbourhood "radius" is a constant (which it is in this example: `1`). Possibly the compiler will optimize the `deltas` computation to a single call, but then I would advice putting the `filter` call on the `deltas` instantiation rather than at the `return` call, as (given a constant "radius") we don't really want to perform the filtering of constants vs. constants for each call to the _computation_ of the surrounding positions given a position instance. For a varying radius, go for #2! – dfrib May 25 '17 at 13:36
  • 1
    I second what @dfri says. I actually had the filter on the deltas creation but put it on the second line to have shorter lines. I updated the post applying dfri`s suggestion and added a few linebreaks. If the distance is (and will be) a constant you could certainly go ahead and make this a static variable like it is in dfri's answer. – thm May 25 '17 at 13:53
1

Since you only look for the neighbouring positions and seemingly have no constraint on x or y, you could save an array of (x, y) translations that each maps a given Position to one of its 8 different neighbouring ones.

struct Position: CustomStringConvertible {
    let x, y: Int

    // order of resulting neighbouring positions, given a position P
    // (1)  (2)  (3)
    // (4)  (P)  (5)
    // (6)  (7)  (8)
    private static let surroundingPositionsTranslations: [(x: Int, y: Int)] = [
        (-1, -1), (0, -1), (1, -1),
        (-1,  0),          (1,  0),
        (-1, -1), (0, -1), (1, -1)]

    var surroundingPositions: [Position] {
        return Position.surroundingPositionsTranslations
            .map { Position(x: x + $0.x, y: y + $0.y) }
    }

    var description: String {
        return "(\(x),\(y))"
    }
}

// Note that I've changed the order w.r.t. OP:s original code
// (modify the transfotmation above to modify the order)
let testPosition = Position(x: 1, y: 1)
print(testPosition.surroundingPositions)
// Output: [(0,0), (1,0), (2,0), (0,1), (2,1), (0,0), (1,0), (2,0)]
dfrib
  • 70,367
  • 12
  • 127
  • 192
  • This is definitely a better solution than what I have so far. However I accepted another answer that has the same content (hardcoded) plus an even more flexible and elegant solution (computed). Thanks for your answer nevertheless! – Benno Kress May 25 '17 at 13:24
  • 1
    @BennoKress happy to help. Yes I noticed we posted somewhat the same content at the same time :) just make sure to prefer the "hardcoded" one in case you don't have a various radius for the neighbourhood (as in your example: static and set to `1`), as the "computed" approach, for a constant radius, will needlessly perform, upon each call, a nested `map`, an array initialization etc (possibly optimizable by the compiler) followed by a needless `filter` just to filter out the single `self` coordinate that we may already omit if using a set of static "hardcoded" transformations. – dfrib May 25 '17 at 13:32