Advent of Code 2021 - Day 2

This topic is about Day 2 of the Advent of Code 2021.

We have a private leaderboard (shared with users of Erlang Forums):

https://adventofcode.com/2021/leaderboard/private/view/370884

The entry code is:
370884-a6a71927

3 Likes

Here is my solution:

2 Likes

I used a similar approach

1 Like

This one wasn’t as fun as the day 1 one for me.

defmodule AdventOfCode.Y2021.Day02 do
  @moduledoc """
  --- Day 2: Dive! ---
  Problem Link: https://adventofcode.com/2021/day/2
  """
  use AdventOfCode.Helpers.InputReader, year: 2021, day: 2

  def run_1, do: input!() |> parse() |> track_positions() |> then(& &1.depth * &1.horizontal)
  def run_2, do: input!() |> parse() |> track_aims() |> then(& &1.depth * &1.horizontal)

  def parse(data) do
    data
    |> String.split("\n")
    |> Enum.map(fn line ->
      [direction, value] = String.split(line, " ")
      {String.to_existing_atom(direction), String.to_integer(value)}
    end)
  end

  defp track_positions(directions) do
    directions
    |> Enum.reduce(%{horizontal: 0, depth: 0}, fn
      {:forward, v}, %{horizontal: horizontal} = acc -> %{acc | horizontal: horizontal + v}
      {:backward, v}, %{horizontal: horizontal} = acc -> %{acc | horizontal: horizontal - v}
      {:up, v}, %{depth: depth} = acc -> %{acc | depth: depth - v}
      {:down, v}, %{depth: depth} = acc -> %{acc | depth: depth + v}
    end)
  end

  defp track_aims(directions) do
    directions
    |> Enum.reduce(%{horizontal: 0, depth: 0, aim: 0}, fn
      {:forward, v}, %{horizontal: horizontal, depth: depth, aim: aim} = acc ->
        %{acc | horizontal: horizontal + v, depth: depth + aim * v}

      {:backward, v}, %{horizontal: horizontal} = acc ->
        %{acc | horizontal: horizontal - v}

      {:up, v}, %{aim: aim} = acc ->
        %{acc | aim: aim - v}

      {:down, v}, %{aim: aim} = acc ->
        %{acc | aim: aim + v}
    end)
  end
end

2 Likes

As usual, my solution is extremely scripting style:

Part 1

#!/usr/bin/env elixir

File.stream!("input.txt")
|> Stream.map(&String.split(&1, ~r/\s+/, trim: true))
|> Stream.map(fn
  ["forward", amount] -> {String.to_integer(amount), 0}
  ["down", amount] -> {0, String.to_integer(amount)}
  ["up", amount] -> {0, -String.to_integer(amount)}
end)
|> Enum.reduce({0, 0}, fn {dx, dy}, {x, y} ->
  {x + dx, y + dy}
end)
|> then(fn {x, y} -> x * y end)
|> IO.inspect()

Part 2

#!/usr/bin/env elixir

File.stream!("input.txt")
|> Stream.map(&String.split(&1, ~r/\s+/, trim: true))
|> Stream.map(fn [command, steps] ->
  {command, String.to_integer(steps)}
end)
|> Enum.reduce({0, 0, 0}, fn
  {"down", amount}, {x, y, aim} -> {x, y, aim + amount}
  {"up", amount}, {x, y, aim} -> {x, y, aim - amount}
  {"forward", amount}, {x, y, aim} -> {x + amount, y + aim * amount, aim}
end)
|> then(fn {x, y, _} -> x * y end)
|> IO.inspect()
1 Like

In Part 1, you could’ve used Tuple.product and get rid of that then ? I was contemplating on representing my data structure as tuple only so that I could use Tuple.product - one of my favourite functions :smiley: but since I was in a noisy room, I made the representation more verbose so I don’t mentally keep track of the which position means what.

Clever way to represent the positions though!

4 Likes

Agree not as fun as Day 1 for some reason. My solution:

Exactly. But the first few days should be a warmup, right?

To make things a little bit more fun, I introduced some sort of sigil.

defmodule Point2D do
  defstruct x: 0, y: 0

  def new(x, y) when is_integer(x) and is_integer(y), do:
    %__MODULE__{x: x, y: y}

  def sigil_p(string, _) do
    ~r/-?\d+/
    |> Regex.scan(string)
    |> List.flatten
    |> Enum.map(&String.to_integer/1)
    |> then(&apply(__MODULE__, :new, &1))
  end

  def add(%__MODULE__{x: x1, y: y1}, %__MODULE__{x: x2, y: y2}) do
    new(x1 + x2, y1 + y2)
  end
end

and then rewrote Part 1:

#!/usr/bin/env elixir

import Point2D, only: [sigil_p: 2]

File.stream!("input.txt")
|> Stream.map(&String.split(&1, ~r/\s+/, trim: true))
|> Stream.map(fn
  ["forward", amount] -> ~p(#{amount} 0)
  ["down", amount] -> ~p(0 #{amount})
  ["up", amount] -> ~p(0 -#{amount})
end)
|> Enum.reduce(~p(0 0), &Point2D.add/2)
|> then(fn %{x: x, y: y} -> x * y end)
|> IO.inspect()
1 Like

niiice. I will freeze my Elixir refactor attempts and get an F# solution out tomorrow. There is an opportunity of using a syntax gimmick there which I’d very much like to try out.

You planning on running Benchee for your two solutions?

1 Like

Here’s mine:

defmodule Aoc.Y2021.Day02 do
  @moduledoc false
  import Aoc.Helper.IO

  def run_part1(), do: get_input() |> solve_part1()
  def run_part2(), do: get_input() |> solve_part2()

  def solve_part1(data), do: data |> plan_course(0, 0)
  def plan_course([], width, depth), do: width * depth
  def plan_course([["forward", value] | rest], width, depth), do: plan_course(rest, width + value, depth)
  def plan_course([["down", value] | rest], width, depth), do: plan_course(rest, width, depth + value)
  def plan_course([["up", value] | rest], width, depth), do: plan_course(rest, width, depth - value)

  def solve_part2(data), do: data |> process_aim(0, 0, 0)

  def process_aim([], width, depth, _aim), do: width * depth

  def process_aim([["forward", value] | rest], width, depth, aim),
    do: process_aim(rest, width + value, depth + aim * value, aim)

  def process_aim([["down", value] | rest], width, depth, aim), do: process_aim(rest, width, depth, aim + value)
  def process_aim([["up", value] | rest], width, depth, aim), do: process_aim(rest, width, depth, aim - value)

  defp get_input(),
    do:
      get_string_input("2021", "02")
      |> String.split("\n")
      |> Enum.map(&String.split(&1, " "))
      |> Enum.map(fn [ins, value] -> [ins, String.to_integer(value)] end)
end
1 Like

No, but I guess the first solution should be faster since it doesn’t use regular expressions. However, I can make that sigil_p a macro (the interpolation part can be tricky to implement), and put some of the parsing jobs to the compile-time, but I feel it does not worth the effort because it’s an Elixir script anyway, not a never-should-die service.

I wonder why nearly all the solutions do String.split/2 when we can directly pattern-match strings

  defp d2_destination_reducer("forward " <> value, acc),
    do: %{acc | h: acc.h + String.to_integer(value)}

  defp d2_destination_reducer("down " <> value, acc),
    do: %{acc | v: acc.v + String.to_integer(value)}

  defp d2_destination_reducer("up " <> value, acc),
    do: %{acc | v: acc.v - String.to_integer(value)}

  defp d2_destination_reducer(_, acc), do: acc
7 Likes

Yeah, I went for that as well which was really nice but ended up duplicating the move functions for step 2 to change the logic.

 def process2(input) do
    input
    |> parse_input()
    |> Enum.reduce(%{h: 0, d: 0, aim: 0}, fn val, acc -> move2(acc, val) end)
    |> calc_position
  end

  def parse_input(input) do
    input
    |> String.split("\n", trim: true)
  end

  def calc_position(%{:h => h, :d => d}), do: h * d

  def move2(%{:h => h, :d => d, :aim => aim} = p, "forward " <> steps) do
    steps = String.to_integer(steps)
    %{p | :h => h + steps, :d => d + aim * steps}
  end

  def move2(%{:aim => aim} = p, "up " <> steps) do
    steps = String.to_integer(steps)
    %{p | :aim => aim - steps}
  end

  def move2(%{:aim => aim} = p, "down " <> steps) do
    steps = String.to_integer(steps)
    %{p | aim: aim + steps}
  end

And thought putting the accumulator in the first argument would make a nice API but made using it in Enum.reduce more cumbersome.

1 Like

Simple Enum.reduce

y2021/d2.ex

Here’s mine.

defmodule Day2 do
  defmodule Part1 do
    def run(input) do
      {depth, distance} =
        Enum.reduce(input, {_depth = 0, _distance = 0}, fn
          {:forward, amount}, {depth, distance} ->
            {depth, distance + amount}

          {:up, amount}, {depth, distance} ->
            {depth - amount, distance}

          {:down, amount}, {depth, distance} ->
            {depth + amount, distance}
        end)

      depth * distance
    end
  end

  defmodule Part2 do
    def run(input) do
      {depth, distance, _aim} =
        Enum.reduce(input, {_depth = 0, _distance = 0, _aim = 0}, fn
          {:forward, amount}, {depth, distance, aim} ->
            depth = depth + aim * amount

            {depth, distance + amount, aim}

          {:up, amount}, {depth, distance, aim} ->
            {depth, distance, aim - amount}

          {:down, amount}, {depth, distance, aim} ->
            {depth, distance, aim + amount}
        end)

      depth * distance
    end
  end

  def input do
    "inputs/2.txt"
    |> File.stream!()
    |> Stream.map(fn line ->
      {instruction_atom, amount_string} =
        case line do
          "up " <> amount -> {:up, amount}
          "down " <> amount -> {:down, amount}
          "forward " <> amount -> {:forward, amount}
        end

      {
        instruction_atom,
        amount_string
        |> String.trim()
        |> String.to_integer()
      }
    end)
  end

  def example_input do
    [
      {:forward, 5},
      {:down, 5},
      {:forward, 8},
      {:up, 3},
      {:down, 8},
      {:forward, 2}
    ]
  end
end

Here’s my take on day2:

defmodule Day02 do
  def parse(path) do
    {:ok, cont}=File.read(path)
    cont |> String.split(["\n","\r"], trim: true) |> trim()
  end

  def trim([],c), do: c
  def trim([h|t],c) do
    [str,i]=String.split(h," ")
    trim(t,c++[[str,String.to_integer(i)]])
  end
  def trim(l), do: trim(l,[])

  def reg([],n,m), do: n*m
  def reg([["forward",i]|t],n,m), do: reg(t,i+n,m)
  def reg([["down",i]|t],n,m), do: reg(t,n,i+m)
  def reg([["up",i]|t],n,m), do: reg(t,n,m-i)

  def reg2([],n,m,_), do: n*m
  def reg2([["forward",i]|t],n,m,a), do: reg2(t,n+i,m+(a*i),a)
  def reg2([["down",i]|t],n,m,a), do: reg2(t,n,m,i+a)
  def reg2([["up",i]|t],n,m,a), do: reg2(t,n,m,a-i)

  def part1(input), do: reg(parse(input),0,0)

  def part2(input), do: reg2(parse(input),0,0,0)
end
1 Like

Hi, @hadar_sm, a small suggestion, avoid using ++.

  def trim([],c), do: c
  def trim([h|t],c) do
    [str,i]=String.split(h," ")
    trim(t,c++[[str,String.to_integer(i)]])  # <-- here
  end
  def trim(l), do: trim(l,[])

can be optimized as

  def trim([],c), do: Enum.reverse(c)  # <-- do a reverse when everything is done
  def trim([h|t],c) do
    [str,i]=String.split(h," ")
    trim(t, [[str,String.to_integer(i)] | c])  # <-- prepending instead of appending
  end
  def trim(l), do: trim(l,[])

This is a very common trick.

2 Likes

Here are my solutions. I don’t like Part 1 and i know that it’s not DRY.

Part 1

instructions =
  File.stream!("data/day_2.txt")
  |> Stream.map(&String.trim/1)
  |> Stream.map(fn str -> String.split(str, " ") end)
  |> Enum.group_by(fn x -> hd(x) end, fn x -> tl(x) end)

down =
  instructions
  |> Map.get("down")
  |> List.flatten()
  |> Enum.map(&String.to_integer/1)
  |> Enum.sum()

up =
  instructions 
  |> Map.get("up") 
  |> List.flatten() 
  |> Enum.map(&String.to_integer/1) 
  |> Enum.sum()

forward =
  instructions
  |> Map.get("forward")
  |> List.flatten()
  |> Enum.map(&String.to_integer/1)
  |> Enum.sum()

position = (down - up) * forward

Part 2

instructions =
  File.stream!("data/day_2.txt")
  |> Stream.map(&String.trim/1)
  |> Stream.map(fn str -> String.split(str, " ") end)
  |> Stream.map(fn [cmd, x] -> [cmd, String.to_integer(x)] end)
  |> Enum.reduce({0, 0, 0}, fn instruction, {depth, aim, horizontal_pos} ->
    case instruction do
      ["forward", x] ->
        {aim * x + depth, aim, horizontal_pos + x}

      ["up", x] ->
        {depth, aim - x, horizontal_pos}

      ["down", x] ->
        {depth, aim + x, horizontal_pos}
    end
  end)
  |> then(fn {depth, _aim, hor_pos} -> depth * hor_pos end)

I made it with some internal states.

defmodule Day2 do
  defmodule State do
    defstruct [x: 0, depth: 0]

    def apply_command({:forward, x}, %__MODULE__{} = state),
      do: %{state | x: state.x + x}
    def apply_command({:up, depth}, %__MODULE__{} = state),
      do: %{state | depth: state.depth - depth}
    def apply_command({:down, depth}, %__MODULE__{} = state),
      do: %{state | depth: state.depth + depth}
  end

  defmodule State2 do
    defstruct [x: 0, depth: 0, aim: 0]

    def apply_command({:forward, x}, %__MODULE__{} = state),
      do: %{state | x: state.x + x, depth: state.depth + state.aim * x}
    def apply_command({:up, aim}, %__MODULE__{} = state),
      do: %{state | aim: state.aim - aim}
    def apply_command({:down, aim}, %__MODULE__{} = state),
      do: %{state | aim: state.aim + aim}
  end

  def part1 do
    state = load_commands()
    |> Enum.reduce(%State{}, &State.apply_command/2)
    |> IO.inspect(label: "FINAL STATE")

    state.x * state.depth
  end

  def part2 do
    state = load_commands()
    |> Enum.reduce(%State2{}, &State2.apply_command/2)
    |> IO.inspect(label: "FINAL STATE")

    state.x * state.depth
  end

  defp load_commands do
    "input.txt"
    |> File.read!()
    |> String.split("\n", trim: true)
    |> Enum.map(& to_command(&1))
  end

  defp to_command(binary) when is_binary(binary),
    do: to_command(String.split(binary))

  defp to_command([key, value]) when key in ~w(forward up down),
    do: {String.to_atom(key), String.to_integer(value)}
end

Here’s mine:

defmodule Submarine do
  def input_stream(file) do
    file
    |> File.stream!()
    |> Stream.map(&String.trim/1)
    |> Stream.map(&String.split/1)
    |> Stream.map(fn [direction, count] -> [direction, String.to_integer(count)] end)
  end

  # Rules for Part1
  def update_position(["forward", count], {horizontal, depth}), do: {horizontal+count, depth}
  def update_position(["up", count], {horizontal, depth}), do: {horizontal, depth-count}
  def update_position(["down", count], {horizontal, depth}), do: {horizontal, depth+count}

  # Rules for Part2
  def update_position(["forward", count], {horizontal, depth, aim}), do: {horizontal+count, depth+count*aim, aim}
  def update_position(["up", count], {horizontal, depth, aim}), do: {horizontal, depth, aim-count}
  def update_position(["down", count], {horizontal, depth, aim}), do: {horizontal, depth, aim+count}
end

[{0, 0}, {0, 0, 0}]
|> Enum.map(fn start_pos ->
  System.argv()
  |> hd()
  |> Submarine.input_stream()
  |> Enum.reduce(start_pos, &Submarine.update_position/2)
end)
|> Enum.with_index(1)
|> Enum.map(fn {tuple, part_number} ->
  result = elem(tuple, 0) * elem(tuple, 1)
  "Part #{part_number}: #{result}"
end)
|> Enum.map(&IO.puts/1)