2

I am trying to lessen the number of lines in a function that I wrote. In order to achieve this I need to select a property of a struct based on some condition and pass that property as a parameter in order to compare that property among other elements in an array that are of the same struct type. Here is my gigantic code with repetition of same thing for different properties.

I have tried the following the code. Although it works as a charm but I feel the same work has been done a number of times.

func checkContentsForMatching(forIndex: Int) {
    var cards = [SetCard]()
    for index in game.indicesOfChosenCards.indices {
        cards.append(cardTitles[testCards[game.indicesofChosenCards[index]]]!)


    }

    if (cards[0].color == cards[1].color) && (cards[1].color == cards[2].color) && (cards[0].color == cards[2].color) {
        game.playingCards[game.indicesOfChosenCards[0]].color = true
        game.playingCards[game.indicesOfChosenCards[1]].color = true
        game.playingCards[game.indicesOfChosenCards[2]].color = true

    }
    else if cards[0].color == cards[1].color {
        game.playingCards[game.indicesOfChosenCards[0]].color = true
        game.playingCards[game.indicesOfChosenCards[1]].color = true

    }
    else if cards[1].color == cards[2].color {
        game.playingCards[game.indicesOfChosenCards[1]].color = true
        game.playingCards[game.indicesOfChosenCards[2]].color = true

    }
    else if cards[0].color == cards[2].color {
        game.playingCards[game.indicesOfChosenCards[0]].color = true
        game.playingCards[game.indicesOfChosenCards[2]].color = true
    }

    if (cards[0].shape == cards[1].shape) && (cards[1].shape == cards[2].shape) && (cards[0].shape == cards[2].shape) {
        game.playingCards[game.indicesOfChosenCards[0]].shape = true
        game.playingCards[game.indicesOfChosenCards[1]].shape = true
        game.playingCards[game.indicesOfChosenCards[2]].shape = true
    }
    else if cards[0].shape == cards[1].shape {
        game.playingCards[game.indicesOfChosenCards[0]].shape = true
        game.playingCards[game.indicesOfChosenCards[1]].shape = true
    }
    else if cards[1].shape == cards[2].shape {
        game.playingCards[game.indicesOfChosenCards[1]].shape = true
        game.playingCards[game.indicesOfChosenCards[2]].shape = true
    }
    else if cards[0].shape == cards[2].shape {
        game.playingCards[game.indicesOfChosenCards[0]].shape = true
        game.playingCards[game.indicesOfChosenCards[2]].shape = true
    }
3
  • Is the actual problem an implementation of Sets? If so, maybe you should ask about that and I'll show you how to implement it. As it stands, this is an x-y problem. Commented Apr 22, 2019 at 17:20
  • Yeah it is an implementation of Sets which was a programming assignment from CS193P. I have already implemented it. I wanted to compress my code into 300 lines if not lesser. Commented Apr 22, 2019 at 17:30
  • Yes, my implementation is quite compact (certainly a lot more compact than yours). There is a pretty elegant way to do this. Commented Apr 22, 2019 at 17:33

3 Answers 3

5

What you're looking for is Swift KeyPath.

In my Sets implementation (Zotz!), this is what I do. First, I use enums with Int raw values for the four card attributes. This allows me to represent any attribute as a common type (Int). I express those raw values as computed properties, and I vend a list of all four computed properties as an array of key paths (note, this is Swift 5):

public struct Card: Codable, CustomStringConvertible {
    
    let itsColor : Color
    let itsNumber : Number
    let itsShape : Shape
    let itsFill : Fill
    
    var itsColorRaw : Int { return itsColor.rawValue }
    var itsNumberRaw : Int { return itsNumber.rawValue }
    var itsShapeRaw : Int { return itsShape.rawValue }
    var itsFillRaw : Int { return itsFill.rawValue }
    public static let attributes : [KeyPath<Card, Int>] = [
        \itsColorRaw, \itsNumberRaw, \itsShapeRaw, \itsFillRaw
    ]
    // ...
}

OK, so now the rule for whether a given triple of cards constitutes a valid match is easy to express:

private func isCorrectCards(_ cards:[Card]) -> Bool {
    func evaluateOneAttribute(_ nn:[Int]) -> Bool {
        let allSame = (nn[0] == nn[1]) && (nn[1] == nn[2])
        let allDiff = (nn[0] != nn[1]) && (nn[1] != nn[2]) && (nn[2] != nn[0])
        return allSame || allDiff
    }
    return Card.attributes.allSatisfy { att in
        evaluateOneAttribute(cards.map{$0[keyPath:att]})
    }
}

Even more elegant version suggested by @vacawama:

private func isCorrect(cards:[Card]) -> Bool {
    return Card.attributes.allSatisfy { att in
        Set(cards.map{$0[keyPath:att]}).count != 2
    }
}
Sign up to request clarification or add additional context in comments.

6 Comments

Well, you inspired me to update my code to use KeyPath and allSatisfy. My revised code has ended up even shorter and clearer than it was before!
I am like three months into coding and this my first app build in three or four days. Thank you for suggesting keypath and allsatisfy. This will surely make my code more compact.
Checking for a set using Swift’s Set: Set(nn).count != 2
@vacawama Whoa, cool! Sure, because it's 1 if they are all the same and 3 if they are all different. I can throw away evaluateOneAttribute and do the whole thing as a one-liner inside the allSatisfy block.
Well now we're displaying both!
|
1

Not sure what you are trying to do but if you are trying to genericify the if-s and else-s I would do something like this:

var allColorsMatch = cards[0].color == cards[1].color) && (cards[1].color == cards[2].color) && (cards[0].color == cards[2].color

var zeroAndOneMatch = cards[0].color == cards[1].color
var oneAndTwoMatch = cards[1].color == cards[2].color
var zeroAndTwoMatch = cards[0].color == cards[2].color

game.playingCards[game.indicesOfChosenCards[0]].color = allColorsMatch || zeroAndOneMatch || zeroAndTwoMatch
game.playingCards[game.indicesOfChosenCards[1]].color = allColorsMatch || zeroAndOneMatch || oneAndTwoMatch
game.playingCards[game.indicesOfChosenCards[2]].color = allColorsMatch || zeroAndTwoMatch || oneAndTwoMatch

That's it, you don't need if-s or else-s now for color, you can replace the color if-else blocks with the code above.

Now, you might ask how would you do the same for shape. shape like color can also be true or false as far I can see. So, you might wanna wrap the above code in a function with three parameters and perform the same operations.

Comments

0

You can use Swift's higher order functions to filter your cards array instead of using a for in loop to start.

You should probably create separate model objects to avoid having to make nested subscript calls like cardTitles[testCards[game.indicesOfChosenCards[index]]]!.

Also, why is your color property in game.playingCards[game.indicesOfChosenCards[i]].color a boolean? Seems a bit confusing to have a property named color return a true of false value.

Try breaking up your function into several functions. You're trying to do too much in this one.

Comments

Your Answer

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

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.