Day 2: Dive!
by @mlachkar
Puzzle description
https://adventofcode.com/2021/day/2
Solution of Part 1
Parsing the file
The first step is to model the problem and to parse the input file.
The command can be either Forward
, Down
or Up
. I use an Enumeration to model it.
An enumeration is used to define a type consisting of a set of named values. Read the official documentation for more details.
Scala 3 enums are more concise and easier to read that the Scala 2 ADTs.
// in Scala 3:
enum Command:
case Forward(x: Int)
case Down(x: Int)
case Up(x: Int)
// in Scala 2:
sealed trait Command
object Command {
case class Forward(x: Int) extends Command
case class Down(x: Int) extends Command
case class Up(x: Int) extends Command
}
Now let's parse the input to a Command:
object Command:
def from(s: String): Command =
s match
case s"forward $x" if x.toIntOption.isDefined => Forward(x.toInt)
case s"up $x" if x.toIntOption.isDefined => Up(x.toInt)
case s"down $x" if x.toIntOption.isDefined => Down(x.toInt)
case _ => throw new Exception(s"value $s is not valid command")
Here I have chosen to fail during the parsing method Command.from
to keep the types as simple as possible.
If an input file is not valid, we throw
an exception.
It's possible to delay the parsing error to the main method, and return an Option[Command]
or Try[Command]
Command.from
Moving the sonar
Now we need to create a method to compute the new position of a sonar given
the initial position and a command.
For that we create a case class Position(horizontal: Int, depth: Int)
, that will represent a position,
and then add a method move
that will translate the puzzle's rules to a position.
case class Position(horizontal: Int, depth: Int):
def move(p: Command): Position =
p match
case Command.Forward(x) => Position(horizontal + x, depth)
case Command.Down(x) => Position(horizontal, depth + x)
case Command.Up(x) => Position(horizontal, depth - x)
To apply all the commands from the input file, we use foldLeft
val firstPosition = Position(0, 0)
val lastPosition = entries.foldLeft(firstPosition)((position, command) => position.move(command))
foldLeft
is a method from the standard library on iterable collections: Seq
, List
, Iterator
...
It's a super convenient method that allows to iterate from left to right on a list.
The signature of foldLeft
is:
def foldLeft[B](initialElement: B)(op: (B, A) => B): B
Let's see an example:
// Implementing a sum on a List
List(1, 3, 2, 4).foldLeft(0)((accumulator, current) => accumulator + current) // 10
It is the same as:
(((0 + 1) + 3) + 2) + 4
Final code for part 1
def part1(input: String): Int =
val entries = input.linesIterator.map(Command.from)
val firstPosition = Position(0, 0)
// we iterate on each entry and move it following the received command
val lastPosition = entries.foldLeft(firstPosition)((position, command) => position.move(command))
lastPosition.result
case class Position(horizontal: Int, depth: Int):
def move(p: Command): Position =
p match
case Command.Forward(x) => Position(horizontal + x, depth)
case Command.Down(x) => Position(horizontal, depth + x)
case Command.Up(x) => Position(horizontal, depth - x)
def result = horizontal * depth
enum Command:
case Forward(x: Int)
case Down(x: Int)
case Up(x: Int)
object Command:
def from(s: String): Command =
s match
case s"forward $x" if x.toIntOption.isDefined => Forward(x.toInt)
case s"up $x" if x.toIntOption.isDefined => Up(x.toInt)
case s"down $x" if x.toIntOption.isDefined => Down(x.toInt)
case _ => throw new Exception(s"value $s is not valid command")
Solution of Part 2
The part 2 introduces new rules to move the sonar.
So we need a new position that takes into account the aim
and a new method move with the new rules.
The remaining code remains the same.
Moving the sonar part 2
case class PositionWithAim(horizontal: Int, depth: Int, aim: Int):
def move(p: Command): PositionWithAim =
p match
case Command.Forward(x) => PositionWithAim(horizontal + x, depth + x * aim, aim)
case Command.Down(x) => PositionWithAim(horizontal, depth, aim + x)
case Command.Up(x) => PositionWithAim(horizontal, depth, aim - x)
Final code for part 2
case class PositionWithAim(horizontal: Int, depth: Int, aim: Int):
def move(p: Command): PositionWithAim =
p match
case Command.Forward(x) => PositionWithAim(horizontal + x, depth + x * aim, aim)
case Command.Down(x) => PositionWithAim(horizontal, depth, aim + x)
case Command.Up(x) => PositionWithAim(horizontal, depth, aim - x)
def result = horizontal * depth
enum Command:
case Forward(x: Int)
case Down(x: Int)
case Up(x: Int)
object Command:
def from(s: String): Command =
s match
case s"forward $x" if x.toIntOption.isDefined => Forward(x.toInt)
case s"up $x" if x.toIntOption.isDefined => Up(x.toInt)
case s"down $x" if x.toIntOption.isDefined => Down(x.toInt)
case _ => throw new Exception(s"value $s is not valid command")
Run it locally
You can get this solution locally by cloning the scalacenter/scala-advent-of-code repository.
$ git clone https://github.com/scalacenter/scala-advent-of-code
$ cd scala-advent-of-code
The you can run it with scala-cli:
$ scala-cli 2021 -M day2.part1
The answer is 2070300
$ scala-cli 2021 -M day2.part2
The answer is 2078985210
You can replace the content of the input/day2
file with your own input from adventofcode.com to get your own solution.
Solutions from the community
- Solution of @tgodzik.
- Solution of @otobrglez.
- Solution of @s5bug using cats-effect and fs2
- Solution of @tOverney.
- Solution by @philip_schwarz
- Solution of @FlorianCassayre.
- Solution of Jan Boerman.
- Solution by Rui Alves
Share your solution to the Scala community by editing this page.