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
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
Here is my solution:
I used a similar approach
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
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()
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 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!
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()
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?
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
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
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.
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
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.
Here are my solutions. I don’t like Part 1 and i know that it’s not DRY.
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
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)