Initial release
This commit is contained in:
@@ -0,0 +1,4 @@
|
|||||||
|
# Used by "mix format"
|
||||||
|
[
|
||||||
|
inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"]
|
||||||
|
]
|
||||||
+36
@@ -0,0 +1,36 @@
|
|||||||
|
# The directory Mix will write compiled artifacts to.
|
||||||
|
/_build/
|
||||||
|
|
||||||
|
# If you run "mix test --cover", coverage assets end up here.
|
||||||
|
/cover/
|
||||||
|
|
||||||
|
# The directory Mix downloads your dependencies sources to.
|
||||||
|
/deps/
|
||||||
|
|
||||||
|
# Where third-party dependencies like ExDoc output generated docs.
|
||||||
|
/doc/
|
||||||
|
|
||||||
|
# Ignore .fetch files in case you like to edit your project deps locally.
|
||||||
|
/.fetch
|
||||||
|
|
||||||
|
# If the VM crashes, it generates a dump, let's ignore it too.
|
||||||
|
erl_crash.dump
|
||||||
|
|
||||||
|
# Also ignore archive artifacts (built via "mix archive.build").
|
||||||
|
*.ez
|
||||||
|
|
||||||
|
# Ignore package tarball (built via "mix hex.build").
|
||||||
|
aoc2024-*.tar
|
||||||
|
|
||||||
|
# Temporary files, for example, from tests.
|
||||||
|
/tmp/
|
||||||
|
|
||||||
|
# MacOS
|
||||||
|
.DS_Store
|
||||||
|
|
||||||
|
# VSCode
|
||||||
|
*.code-workspace
|
||||||
|
|
||||||
|
# AoC Session File
|
||||||
|
.session
|
||||||
|
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
# Advent of Code 2024 in Elixir
|
||||||
|
|
||||||
|
If you've never heard of Advent of Code, you're missing out. Check it out now at [adventofcode.com](https://adventofcode.com/about)!
|
||||||
|
|
||||||
|
I mostly do AoC puzzles for fun, generally coding in Python or Rust, but I've also found that it's a great way to get some experience with other programming languages, such as Elixir, Haskell, or Julia.
|
||||||
|
|
||||||
|
This repository contains all my solutions to the [AoC 2024](https://adventofcode.com/2024) puzzles using Elixir.
|
||||||
|
|
||||||
|
As I'm still learning Elixir, the code may not always be as simple or idiomatic as it could be. If you have suggestions for things I could improve, please don't hesitate to file an issue or submit a PR. Thanks!
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
Make sure you have Elixir and Git installed, then open a terminal and run:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
git clone https://github.com/ageron/aoc2024-elixir
|
||||||
|
cd aoc2024-elixir
|
||||||
|
mix aoc2024.run
|
||||||
|
```
|
||||||
|
|
||||||
|
You can specify days to run if you want, for example this will run days 2, 4, and 6:
|
||||||
|
|
||||||
|
```
|
||||||
|
mix aoc2024.run 2 4 6
|
||||||
|
```
|
||||||
|
|
||||||
|
## Getting the data
|
||||||
|
|
||||||
|
I've also included a little `get_data.py` utility to automatically download the data of the day, at the right time (you'll get a countdown if you're early). I was too lazy to code it in Elixir, but perhaps I'll port it one day. To use it, just type the following command in a terminal, replacing `{day}` with the day you want:
|
||||||
|
|
||||||
|
```
|
||||||
|
cd /path/to/this/repository
|
||||||
|
python get_data.py 2024 {day}
|
||||||
|
```
|
||||||
|
|
||||||
|
The script requires the `requests` and `pytz` libraries, which you can install like this:
|
||||||
|
|
||||||
|
```
|
||||||
|
python -m pip install --user requests pytz
|
||||||
|
```
|
||||||
|
|
||||||
|
The first time you run `get_data.py`, you will be asked to login to AoC in your browser, [find your session cookie](https://github.com/wimglenn/advent-of-code-wim/issues/1), and save it into a `.session` file in the current directory.
|
||||||
|
|
||||||
|
Have fun!
|
||||||
+1000
File diff suppressed because it is too large
Load Diff
Executable
+72
@@ -0,0 +1,72 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
import sys
|
||||||
|
import requests
|
||||||
|
from datetime import datetime
|
||||||
|
from pytz import timezone
|
||||||
|
from time import sleep
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
# Eric Wastl, the author of Advent-of-Code, asked that if you're automatically
|
||||||
|
# querying adventofcode.com (as this script does), then you should include
|
||||||
|
# contact details in the User-Agent field. So please replace my name with yours:
|
||||||
|
NAME = "Aurélien Geron"
|
||||||
|
|
||||||
|
def usage():
|
||||||
|
print("Usage:")
|
||||||
|
print(f"{sys.argv[0]} {{year}} {{day}}")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
def session_error():
|
||||||
|
print("""Please open your browser, login to adventofcode.com, lookup
|
||||||
|
the session cookie, and save its value to the .session file.
|
||||||
|
Here's how to find this cookie in Chrome: right-click > Inspect, select the
|
||||||
|
Application tab in the inspector, then in the left menu select
|
||||||
|
Storage > Cookies > https://adventofcode.com, and click on session in the list.
|
||||||
|
Copy the cookie value: it's a long hexadecimal .""")
|
||||||
|
sys.exit(2)
|
||||||
|
|
||||||
|
if len(sys.argv) != 3:
|
||||||
|
usage()
|
||||||
|
|
||||||
|
try:
|
||||||
|
year, day = map(int, sys.argv[1:3])
|
||||||
|
except ValueError:
|
||||||
|
usage()
|
||||||
|
|
||||||
|
session_path = Path(".session")
|
||||||
|
if not session_path.is_file():
|
||||||
|
session_error()
|
||||||
|
|
||||||
|
eastern = timezone('US/Eastern')
|
||||||
|
requested_day = eastern.localize(datetime(year, 12, day))
|
||||||
|
|
||||||
|
while True:
|
||||||
|
now = datetime.now(eastern)
|
||||||
|
seconds_left = (requested_day - now).total_seconds()
|
||||||
|
if seconds_left > 0:
|
||||||
|
wait_time = seconds_left % 1
|
||||||
|
if wait_time < 0.5:
|
||||||
|
wait_time += 1
|
||||||
|
sleep(wait_time)
|
||||||
|
print(f"⏰ {int(seconds_left)} ", end="\r");
|
||||||
|
else:
|
||||||
|
break
|
||||||
|
|
||||||
|
sleep(1) # just to be safe
|
||||||
|
|
||||||
|
url = f"https://adventofcode.com/{year}/day/{day}/input"
|
||||||
|
cookies = {"session": open(".session").read().strip()}
|
||||||
|
headers = {"User-Agent": NAME}
|
||||||
|
request = requests.get(url, cookies=cookies, headers=headers)
|
||||||
|
if request.status_code != 200:
|
||||||
|
print(f"HTTP Error {request.status_code}")
|
||||||
|
print(request.text)
|
||||||
|
session_error()
|
||||||
|
|
||||||
|
text = request.text
|
||||||
|
|
||||||
|
if year == 2023:
|
||||||
|
with open(f"data/day{day:02}.txt", "w") as f:
|
||||||
|
f.write(text)
|
||||||
|
else:
|
||||||
|
print(text)
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
defmodule Aoc2024.Day01 do
|
||||||
|
def parse(input) do
|
||||||
|
numbers =
|
||||||
|
input
|
||||||
|
|> String.split("\n", trim: true)
|
||||||
|
|> Enum.map(fn line ->
|
||||||
|
line
|
||||||
|
|> String.split(~r/\s+/)
|
||||||
|
|> Enum.map(fn int_str ->
|
||||||
|
case int_str |> Integer.parse() do
|
||||||
|
{int, ""} -> int
|
||||||
|
_ -> raise "Invalid integer #{int_str}"
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
end)
|
||||||
|
|
||||||
|
numbers
|
||||||
|
|> Enum.map(&List.to_tuple/1)
|
||||||
|
|> Enum.unzip()
|
||||||
|
end
|
||||||
|
|
||||||
|
def part1({col0, col1}) do
|
||||||
|
sorted_col0 = col0 |> Enum.sort()
|
||||||
|
sorted_col1 = col1 |> Enum.sort()
|
||||||
|
|
||||||
|
Enum.zip(sorted_col0, sorted_col1)
|
||||||
|
|> Enum.map(fn {val0, val1} ->
|
||||||
|
abs(val0 - val1)
|
||||||
|
end)
|
||||||
|
|> Enum.sum()
|
||||||
|
end
|
||||||
|
|
||||||
|
def part2({col0, col1}) do
|
||||||
|
col0
|
||||||
|
|> Enum.map(fn val0 ->
|
||||||
|
count = Enum.count(col1, fn val1 -> val0 == val1 end)
|
||||||
|
count * val0
|
||||||
|
end)
|
||||||
|
|> Enum.sum()
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -0,0 +1,89 @@
|
|||||||
|
defmodule Mix.Tasks.Aoc2024.Run do
|
||||||
|
use Mix.Task
|
||||||
|
|
||||||
|
@shortdoc "Run all the solutions, or just the ones for the given days"
|
||||||
|
def run(args) do
|
||||||
|
days =
|
||||||
|
if args |> Enum.empty?() do
|
||||||
|
1..25 |> Enum.map(&Integer.to_string/1)
|
||||||
|
else
|
||||||
|
args
|
||||||
|
end
|
||||||
|
|
||||||
|
days |> Enum.each(&run_day/1)
|
||||||
|
end
|
||||||
|
|
||||||
|
defp check_and_pad_day(day_str) do
|
||||||
|
case day_str |> Integer.parse() do
|
||||||
|
{day, ""} when day >= 1 and day <= 25 ->
|
||||||
|
IO.puts("---- Day #{day}")
|
||||||
|
{:ok, day |> Integer.to_string() |> String.pad_leading(2, "0")}
|
||||||
|
|
||||||
|
_ ->
|
||||||
|
{:error, "Invalid day `#{day_str}`"}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp load_module(padded_day_str) do
|
||||||
|
module = Module.concat([Aoc2024, "Day#{padded_day_str}"])
|
||||||
|
|
||||||
|
if match?({:module, _}, Code.ensure_loaded(module)) do
|
||||||
|
{:ok, module}
|
||||||
|
else
|
||||||
|
{:error, "Module `#{inspect(module)}` is not defined."}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp load_input(padded_day_str) do
|
||||||
|
file_path = "data/day_#{padded_day_str}.txt"
|
||||||
|
|
||||||
|
case File.read(file_path) do
|
||||||
|
{:ok, content} ->
|
||||||
|
{:ok, content}
|
||||||
|
|
||||||
|
{:error, reason} ->
|
||||||
|
{:error, "Failed to read #{file_path}: #{reason}"}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp parse_input(module, input) do
|
||||||
|
if function_exported?(module, :parse, 1) do
|
||||||
|
{:ok, module.parse(input)}
|
||||||
|
else
|
||||||
|
{:ok, input}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp run_part1(module, parsed) do
|
||||||
|
if function_exported?(module, :part1, 1) do
|
||||||
|
result1 = module.part1(parsed)
|
||||||
|
IO.inspect(result1)
|
||||||
|
{:ok, true}
|
||||||
|
else
|
||||||
|
{:error, "Function `#{inspect(module)}.part1/1` is not defined."}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp run_part2(module, parsed) do
|
||||||
|
if function_exported?(module, :part2, 1) do
|
||||||
|
result2 = module.part2(parsed)
|
||||||
|
IO.inspect(result2)
|
||||||
|
{:ok, true}
|
||||||
|
else
|
||||||
|
{:error, "Function `#{inspect(module)}.part2/1` is not defined."}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp run_day(day_str) do
|
||||||
|
with {:ok, padded_day_str} <- check_and_pad_day(day_str),
|
||||||
|
{:ok, module} <- load_module(padded_day_str),
|
||||||
|
{:ok, input} <- load_input(padded_day_str),
|
||||||
|
{:ok, parsed_input} <- parse_input(module, input),
|
||||||
|
{:ok, _success} <- run_part1(module, parsed_input),
|
||||||
|
{:ok, _success} <- run_part2(module, parsed_input) do
|
||||||
|
else
|
||||||
|
{:error, msg} ->
|
||||||
|
IO.puts("Error: #{msg}")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
defmodule Aoc2024.MixProject do
|
||||||
|
use Mix.Project
|
||||||
|
|
||||||
|
def project do
|
||||||
|
[
|
||||||
|
app: :aoc2024,
|
||||||
|
version: "0.1.0",
|
||||||
|
elixir: "~> 1.17",
|
||||||
|
start_permanent: Mix.env() == :prod,
|
||||||
|
deps: deps()
|
||||||
|
]
|
||||||
|
end
|
||||||
|
|
||||||
|
# Run "mix help compile.app" to learn about applications.
|
||||||
|
def application do
|
||||||
|
[
|
||||||
|
extra_applications: [:logger]
|
||||||
|
]
|
||||||
|
end
|
||||||
|
|
||||||
|
# Run "mix help deps" to learn about dependencies.
|
||||||
|
defp deps do
|
||||||
|
[
|
||||||
|
# {:dep_from_hexpm, "~> 0.3.0"},
|
||||||
|
# {:dep_from_git, git: "https://github.com/elixir-lang/my_dep.git", tag: "0.1.0"}
|
||||||
|
]
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
defmodule Aoc2024Test do
|
||||||
|
use ExUnit.Case
|
||||||
|
doctest Aoc2024
|
||||||
|
|
||||||
|
test "greets the world" do
|
||||||
|
assert Aoc2024.hello() == :world
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
ExUnit.start()
|
||||||
Reference in New Issue
Block a user