Today’s puzzle is called Scratchcards.

Part 1

I am going to start by creating a type called card to store the information in.

type Card = Card of no: int * winners: int [] * mine: int []

I need to be able to parse the card text, This could be done more elegant, but I’m going to split the string up, and assume there is no problem in the input data.

module Card =
    let parse (text: string) =
        let cleanSplit (text: string) (chr: char) =
            text.Split(chr) |> Array.filter (fun x -> x.Length > 0)
        let toNumArray (text: string) =
            cleanSplit text ' '
            |> Array.map (fun x -> x.Trim())
            |> Array.filter (fun x -> x.Length > 0)
            |> Array.map int

        let cardNoSplit = text.Split(':')
        let no = (cleanSplit cardNoSplit[0] ' ').[1].Trim() |> int
        let numbersSplit = cardNoSplit[1].Split('|')
        let winners = toNumArray numbersSplit[0]
        let mine = toNumArray numbersSplit[1]
        Card (no, winners, mine)

I can test this on the first row.

Card.parse "Card 1: 41 48 83 86 17 | 83 86  6 31 17  9 48 53"

Which returns:

Card.parse "Card 1: 41 48 83 86 17 | 83 86  6 31 17  9 48 53";;
val it: Card =
  Card (1, [|41; 48; 83; 86; 17|], [|83; 86; 6; 31; 17; 9; 48; 53|])

I now need to score a card, let me first get the number of winning numbers in my numbers.

module Card =
    let winningNumbers (Card (_, winners, mine)) =
        [| for no in mine do
           match (winners |> Seq.tryFind ((=) no)) with
           | Some x -> yield x
           | _ -> () |]

I will test this.

"Card 2: 13 32 20 16 61 | 61 30 68 82 17 32 24 19"
|> Card.parse
|> Card.winningNumbers

Which returns:

| 61 | 32 |
module Card =
    let score card =
        let rec double acc numbers =
            match numbers with
            | [||] -> acc
            | _ -> double (if acc = 0 then 1 else acc * 2) numbers[1..]
        double 0 (Card.winningNumbers card)

I will test this.

"Card 3:  1 21 53 59 44 | 69 82 63 72 16 21 14  1"
|> Card.parse
|> Card.score

Which returns:

2

I am happy this far, lets pipe it all together and see what we get.

let sampleText =
    "Card 1: 41 48 83 86 17 | 83 86  6 31 17  9 48 53
Card 2: 13 32 20 16 61 | 61 30 68 82 17 32 24 19
Card 3:  1 21 53 59 44 | 69 82 63 72 16 21 14  1
Card 4: 41 92 73 84 69 | 59 84 76 51 58  5 54 83
Card 5: 87 83 26 28 32 | 88 30 70 12 93 22 82 36
Card 6: 31 18 13 56 72 | 74 77 10 23 35 67 36 11"

sampleText.Split('\n')
|> Seq.map Card.parse
|> Seq.map Card.score
|> Seq.sum

13

13 is the correct answer for the sample data. My final answer is also correct, so I earn another gold star.

Part 2

In part two we need to keep track of how many times a card is copies.

I am going to create a very basic record type that will keep the number of matching numbers on a card, and how many copies of this card we have.

type CardMatch = { matches: int; copies: int }

I am also creating some functions that will help me track the number of copies we have of each card. In F# items in an array can be changed 1, so I am using that to update the count as we go down all the cards.

module CardMatch =
    let create matches = { matches = matches; copies = 1 }
    let checkMatches cards = cards |> Seq.map (Card.winningNumbers >> Seq.length >> create)
    let copies m = m.copies

    let copyCards cardsWithMatches =
        let copiesOfCards = Seq.toArray cardsWithMatches
        for i = 0 to copiesOfCards.Length - 1 do
            let m = copiesOfCards[i]
            for j = i + 1 to i + m.matches do
                let c = copiesOfCards[j]
                copiesOfCards[j] <- { c with copies = c.copies + m.copies }
        copiesOfCards

I now pipe this together and see what we get.

sampleText.Split('\n')
|> Seq.map Card.parse
|> CardMatch.checkMatches
|> CardMatch.copyCards
|> Seq.sumBy CardMatch.copies

30

30 is the value I am expecting with the sample data. My answer with the complete data is also correct, and I get another star.


  1. Changing things is not normally the functional way to do something, but it is the way that makes sense to me in this situation. ↩︎