Programming

Advent of Code 2019 Day 4 in Scala

Advent of Code 2019

I decided to do Advent of Code 2019 Day 4! I’m gonna solve the puzzles in Scala! I’m starting late, because I found out about it late. So we’re going to start at Day 4, and catch up in another post, or posts.

I have also decided to implement the solutions in Scala. My Scala skills have gotten rusty, so this is a good time to exercise them again. I have forgotten much of my Scala, since I don’t use it. Reading another post, in Kotlin, showed me just how much I’ve forgotten of the standard library functions. My first solution follows. I may add another solution using the Scala Standard Library functions to do even less actual code.

The full code is on my gitlab.

Part 1

To start with, I have a simple executable that runs the valid password count method, and prints it out to the command line. I just hard-coded the range, rather than reading in the input from a file.

object Puzzle4Main extends App {
  val p4 = new Puzzle4

  val range = 134792 to 675810

  println("Valid passwords Part 1: " + p4.validPasswordCount1(range))
  println("Valid passwords Part 2: " + p4.validPasswordCount2(range))
}

I didn’t start with both parts, but that’s the final product. Next I took the criteria from the puzzle and built a scalatest FunSpec for part one.

class Puzzle4Spec extends AnyFunSpec {
  val p4 = new Puzzle4

  describe("Tests for puzzle 4: part 1") {
    it("111111 meets the password criteria") {
      assert(p4.passwordValidPart1("111111"))
    }
    it("223450 does not meet the criteria") {
      assert(!p4.passwordValidPart1("223450"))
    }
    it("123789 does not meet criteria") {
      assert(!p4.passwordValidPart1("123789"))
    }
    it("123455 meets criteria") {
      assert(p4.passwordValidPart1("123455"))
    }
  }
}

I added another criteria when I wasn’t sure if my indexing logic was actually doing the right thing. These tests all passed my implementation, eventually.

  def passwordValidPart1(input: String): Boolean = {
    if (input.length != 6) {
      false
    } else {
      val numbers = input.toCharArray.map(_.asDigit).toList
      atLeastOneDouble(numbers) && numbersNeverDecrease(numbers)
    }
  }

  def atLeastOneDouble(numbers: List[Int]): Boolean = {
    (0 until (numbers.length - 1)).exists(index => numbers(index) == numbers(index + 1))
  }

  def numbersNeverDecrease(numbers: List[Int]): Boolean = {
    (0 until (numbers.length - 1)).forall(index => numbers(index) <= numbers(index + 1))
  }

Since I’ve forgotten lots of Scala, especially the methods available to me on the standard library, I probably did a lot manually here that I didn’t need to. It passes the tests, however, and produces an accurate result!

Part 2

This revealed part two of the puzzle. Tests first!

  describe("Puzzle 4: part 2") {
    it("112233 meets criteria") {
      assert(p4.passwordValidPart2("112233"))
    }
    it("123444 does not meet") {
      assert(!p4.passwordValidPart2("123444"))
    }
    it("111122 meets criteria") {
      assert(p4.passwordValidPart2("111122"))
    }
  }

My implementation here is arguably more complex than the first one, and looking at the kotlin blog, I should’ve remembered a lot more of the standard library.

  def validPasswordCount2(inclusive: Range.Inclusive): Int = {
    inclusive.count(number => passwordValidPart2(number.toString))
  }

  def passwordValidPart2(input: String): Boolean = {
    val numbers = input.toCharArray.map(_.asDigit).toList

    val grouped = numberGroups(numbers)
    numbersNeverDecrease(numbers) && numberGroupsOfOnlyTwo(grouped)
  }


  def numbersNeverDecrease(numbers: List[Int]): Boolean = {
    (0 until (numbers.length - 1)).forall(index => numbers(index) <= numbers(index + 1))
  }

  // find the groups of numbers
  def numberGroups(numbers: List[Int]): List[List[Int]] = {
    @tailrec def findGroups(working: List[Int], acc: List[List[Int]], previousNumber: Int): List[List[Int]] = {
      if (working.isEmpty) {
        acc
      } else {
        val current = working.head
        if (current == previousNumber) {
          val sequence = acc.head :+ current
          findGroups(working.tail, sequence :: acc.tail, current)
        } else {
          val sequence = List(current)
          findGroups(working.tail, sequence :: acc, current)
        }
      }
    }

    findGroups(numbers, List.empty[List[Int]], -1).reverse
  }

  def numberGroupsOfOnlyTwo(groups: List[List[Int]]): Boolean = {
    groups.exists(_.length == 2)
  }

I reused the numbersNeverDecrease method, and then built a couple of methods to group the numbers into a list of lists, and then make sure I had at least one group of numbers that was only a size of 2. I think I can use a Scala standard library method to do the same thing, but it is fun to write tail recursive methods 🙂

Further reading

Related posts

Advent of Code 2019 Day 5 in Scala

David Kowis

Advent of Code 2019 in Scala Day 7

David Kowis

Advent of Code 2019 Day 3 in Scala

David Kowis

Leave a Reply

avatar
  Subscribe  
Notify of
Adventures in Software Development, Home Labbing, and DevOps