Welcome to ElixirForum
I handn’t considered hardcoding all the overlaps
A couple of functions that might be useful to simplify:
Welcome to ElixirForum
I handn’t considered hardcoding all the overlaps
A couple of functions that might be useful to simplify:
I used a regex trick that I just learned yesterday.
My code is messy so won’t share it. But I’m gonna share my trick.
Regex.scan(~r/(?=(\d+))/, "123456789", capture: :all_but_first)
returns
[
["123456789"],
["23456789"],
["3456789"],
["456789"],
["56789"],
["6789"],
["789"],
["89"],
["9"]
]
The key is the (?=)
part and the ()
combined with the capture: :all_but_first
option, not the \d+
part in the regex.
I stand corrected (I was in fact thinking of Ruby)! Updated my comment and referenced yours, thanks for teaching me something today.
I’ve not done AoC before so this was a fun diversion for a Saturday morning.
For part 1 I had a simple for
comprehension that I think is quite easy to understand:
def extract_integer(text) do
[first, last] =
for <<char <- text>>, char in ?0..?9, reduce: [nil, nil] do
[nil, nil] -> [char - ?0, char - ?0]
[first, _] -> [first, char - ?0]
end
first * 10 + last
end
But it runs in about 500 μs on my MacBook Pro (M1 Max) and uses about 1.6 MB of memory whereas my final solution just uses recursive functions and it finishes in less than 200 μs using less than 128 KB of memory.
def extract_integer_1b(<<>>, [first, last]) do
first * 10 + last
end
def extract_integer_1b(<<char, rest::binary>>, [nil, nil]) when char in ?0..?9 do
extract_integer_1b(rest, [char - ?0, char - ?0])
end
def extract_integer_1b(<<char, rest::binary>>, [first, _]) when char in ?0..?9 do
extract_integer_1b(rest, [first, char - ?0])
end
def extract_integer_1b(<<_char, rest::binary>>, acc) do
extract_integer_1b(rest, acc)
end
The second part just builds on the first part. And, like others, it generates the function heads for the recursive functions from static data. This runs in about 300 μs.
@integer_map %{
"zero" => 0,
"one" => 1,
"two" => 2,
"three" => 3,
"four" => 4,
"five" => 5,
"six" => 6,
"seven" => 7,
"eight" => 8,
"nine" => 9
}
def extract_integer_2(<<>>, [first, last]) do
first * 10 + last
end
for {text, int} <- @integer_map do
last_char = String.last(text)
def extract_integer_2(<<unquote(text) , rest::binary>>, [nil, nil]) do
extract_integer_2(<<unquote(last_char), rest::binary>>, [unquote(int), unquote(int)])
end
def extract_integer_2(<<unquote(text) , rest::binary>>, [first, _]) do
extract_integer_2(<<unquote(last_char), rest::binary>>, [first, unquote(int)])
end
end
def extract_integer_2(<<char, rest::binary>>, [nil, nil]) when char in ?0..?9 do
extract_integer_2(rest, [char - ?0, char - ?0])
end
def extract_integer_2(<<char, rest::binary>>, [first, _]) when char in ?0..?9 do
extract_integer_2(rest, [first, char - ?0])
end
def extract_integer_2(<<_char, rest::binary>>, acc) do
extract_integer_2(rest, acc)
end
Wow some solutions here are cool.
I used a simple regular expressions based approach
My solution does have to parse all numbers instead of just the first and last, and does not support “oNe” @al2o3cr but it’s still fast.
defmodule AdventOfCode.Y23.Day1 do
alias AoC.Input, warn: false
def read_file(file, _part) do
Input.stream!(file, trim: true)
end
def parse_input(input, _part) do
input
end
def part_one(problem) do
problem
|> Enum.map(&String.to_charlist/1)
|> Enum.map(&solve_line_p1/1)
|> Enum.reduce(&Kernel.+/2)
end
defp solve_line_p1(chars) do
digits = Enum.filter(chars, &(&1 in ?0..?9))
{first, last} = first_last(digits)
List.to_integer([first, last])
end
defp first_last(items) do
[h | _] = items
[g | _] = :lists.reverse(items)
{h, g}
end
def part_two(problem) do
problem
|> Enum.map(&solve_line_p2/1)
|> Enum.reduce(&Kernel.+/2)
end
defp solve_line_p2(line) do
line
|> collect_digits([])
|> first_last()
|> then(fn {a, b} -> a * 10 + b end)
end
defp collect_digits(<<c, rest::binary>>, acc) when c in ?0..?9,
do: collect_digits(rest, [c - ?0 | acc])
~w(one two three four five six seven eight nine)
|> Enum.with_index(1)
|> Enum.each(fn {<<c, keep::binary>>, digit} ->
defp collect_digits(<<unquote(c), unquote(keep)::binary, rest::binary>>, acc) do
collect_digits(<<unquote(keep)::binary, rest::binary>>, [unquote(digit) | acc])
end
end)
defp collect_digits(<<_, rest::binary>>, acc), do: collect_digits(rest, acc)
defp collect_digits(<<>>, acc), do: :lists.reverse(acc)
end
You can also use a capture in an positive lookahead to write the whole thing as a one-liner using Perl:
perl -pe 's/^[^\d]*(?=(\d)).*(\d)[^\d]*$/\1\2\n/' input.txt | paste -s -d+ - | bc
The lookahead is critical because otherwise inputs like abc4def
won’t find the 4
twice.
Here the my solution that uses macro to generate functions
My beginner’s solution
defmodule Day01 do
def part2(str) do
str
|> String.split
|> Enum.map(&spelled_out_words_to_digits/1)
|> Enum.map(&recover_two_digit_number/1)
|> Enum.sum
end
def part1(str) do
str
|> String.split
|> Enum.map(&recover_two_digit_number/1)
|> Enum.sum
end
def recover_two_digit_number(str) do
digits = str
|> String.graphemes
|> Enum.filter(&is_digit?/1)
List.first(digits) <> List.last(digits)
|> String.to_integer
end
def is_digit?(s) do
s |> String.contains?(["0", "1", "2", "3",
"4", "5", "6", "7", "8", "9"])
end
def spelled_out_words_to_digits(str) do
str
|> String.replace("one", "one1one")
|> String.replace("two", "two2two")
|> String.replace("three", "three3three")
|> String.replace("four", "four4four")
|> String.replace("five", "five5five")
|> String.replace("six", "six6six")
|> String.replace("seven", "seven7seven")
|> String.replace("eight", "eight8eight")
|> String.replace("nine", "nine9nine")
end
end
I am using Livebook for my solution.
Kino.FS.file_path("01.txt")
|> File.stream!()
|> Stream.map(fn line ->
matches =
Regex.scan(~r/\d/, line)
|> List.flatten()
first = List.first(matches)
last = List.last(matches)
String.to_integer("#{first}#{last}")
end)
|> Enum.sum()
pattern = ~r/(?=(\d|one|two|three|four|five|six|seven|eight|nine))/
Kino.FS.file_path("01.txt")
|> File.stream!()
|> Stream.map(fn line ->
matches =
Regex.scan(pattern, line, capture: :all_but_first)
|> List.flatten()
|> Enum.map(fn
"one" -> "1"
"two" -> "2"
"three" -> "3"
"four" -> "4"
"five" -> "5"
"six" -> "6"
"seven" -> "7"
"eight" -> "8"
"nine" -> "9"
input -> input
end)
first = List.first(matches)
last = List.last(matches)
String.to_integer(first <> last)
end)
|> Enum.sum()
You can also find the code on GitHub.
Really was not prepared for the overlapping text integers. In fact, based on my example input I assumed that was explicitly NOT supposed to be valid. Does not bode well for my performance long term this year, LOL.
My focus was on using a single pass over the string, i.e., avoiding one pass forward and another pass over the reversed string. Wish I had known about the :global
flag for String.replace
as I think that would have been cleaner than the regex solution I’m using.
@digit_strings %{
"one" => "1",
"two" => "2",
"three" => "3",
"four" => "4",
"five" => "5",
"six" => "6",
"seven" => "7",
"eight" => "8",
"nine" => "9"
}
@regex ("(?=(" <> (@digit_strings |> Map.keys() |> Enum.join("|") |> Kernel.<>("))")))
|> Regex.compile()
|> elem(1)
def parse(input) do
input
|> Day1.input()
|> Input.lines()
|> Enum.map(fn line ->
Regex.replace(@regex, line, fn _, x -> Map.get(@digit_strings, x) end)
end)
end
def do_solve(data) do
data
|> Enum.map(fn line ->
first_and_last_ints(line)
|> String.to_integer()
end)
|> Enum.sum()
end
defp first_and_last_ints(line) when is_binary(line) do
line
|> String.graphemes()
|> Enum.reduce({"", nil}, fn g, {s, last} ->
with {i, ""} <- Integer.parse(g) do
if s == "" do
{s <> "#{i}", i}
else
{s, i}
end
else
_ ->
{s, last}
end
end)
|> append_last()
end
defp append_last({s, i}), do: s <> "#{i}"
For Part 2, I wrote a NimbleParsec parser
defmodule Replacer do
import NimbleParsec
replacements =
for {<<head::binary-size(1), rest::binary>>, numeral} <-
Enum.with_index(~w[one two three four five six seven eight nine], 1) do
numeral = to_string(numeral)
choice([
string(numeral),
string(head) |> lookahead(string(rest)) |> replace(numeral)
])
end
ignored = ignore(utf8_string([], 1))
defparsec(:replace, repeat(choice(replacements ++ [ignored])))
end
But it runs in about 500 μs on my MacBook Pro (M1 Max) and uses about 1.6 MB of memory whereas my final solution just uses recursive functions and it finishes in less than 200 μs using less than 128 KB of memory.
Sorry, I am relatievely new to Elixir. Can you share how you are gathering these metrics?
benchee is the go-to for benchmarking in Elixir. You can see my configuration in the bench
directory in my advent_of_code repo.
To use it you would do mix run ./bench/bench.exs
then sit back and relax
I also have a rudimentary version in my answer using :timer.tc
Also wrote a parser, but yours is way better: https://github.com/ream88/advent-of-code-2023/blob/main/01/part2.exs
it’s always interesting to see the different solutions and learn from each other
not sure using the String.replace or Regex.scan is efficient as you process the whole line, most of the processing is therefore useless
I opted to scan left to right and right to left with a pattern-matching function.
Although it’s more verbose, I think it’s more efficient as you stop processing a line asap.
def p2(file) do
file
|> File.read!()
|> String.split("\n")
|> Enum.map(fn line ->
ldigit(line) * 10 + rdigit(String.reverse(line))
end)
|> Enum.sum()
end
defp ldigit("one" <> _), do: 1
defp ldigit("two" <> _), do: 2
defp ldigit("three" <> _), do: 3
defp ldigit("four" <> _), do: 4
defp ldigit("five" <> _), do: 5
defp ldigit("six" <> _), do: 6
defp ldigit("seven" <> _), do: 7
defp ldigit("eight" <> _), do: 8
defp ldigit("nine" <> _), do: 9
defp ldigit("zero" <> _), do: 0
defp ldigit(<<c>> <> _) when c >= ?0 and c <= ?9, do: c - ?0
defp ldigit(<<_, s::binary>>), do: ldigit(s)
defp rdigit("eno" <> _), do: 1
defp rdigit("owt" <> _), do: 2
defp rdigit("eerht" <> _), do: 3
defp rdigit("ruof" <> _), do: 4
defp rdigit("evif" <> _), do: 5
defp rdigit("xis" <> _), do: 6
defp rdigit("neves" <> _), do: 7
defp rdigit("thgie" <> _), do: 8
defp rdigit("enin" <> _), do: 9
defp rdigit("orez" <> _), do: 0
defp rdigit(<<c>> <> _) when c >= ?0 and c <= ?9, do: c - ?0
defp rdigit(<<_, s::binary>>), do: rdigit(s)
What I did is slightly different than what you did.
Instead of reversing each line, I reversed the whole input character-by-character
Here’s my code:
I noticed that the 2-digit number in each line is just first_digit * 10 + last_digit
, and what we need to do is to sum up those 2-digit numbers, and number addition is both commutative and associative, so I can just do this:
Enum.sum(first_digits) * 10 + Enum.sum(last_digits)
I came up with a relatively clean solution for part 2:
puzzle_input = File.read!("/path/to/aoc2023/day01.txt")
digit_map = %{
"one" => 1,
"two" => 2,
"three" => 3,
...,
"0" => 0,
"1" => 1,
"2" => 2,
...
}
# ~r/^.*?(0|1|2|...|one|two|...)/m
# Note the reluctant quantifier `*?`
first_digits_regex =
digit_map
|> Map.keys()
|> Enum.join("|")
|> then(&(~r/^.*?(#{&1})/m))
# ~r/^.*(0|1|2|...|one|two|...)/m
# Note the greedy quantifier `*`
last_digits_regex =
digit_map
|> Map.keys()
|> Enum.join("|")
|> then(&(~r/^.*?(#{&1})/m))
first_digits_sum =
first_digits_regex
|> Regex.scan(puzzle_input, capture: :all_but_first)
|> List.flatten()
|> Stream.map(&digit_map[&1])
|> Enum.sum()
last_digits_sum =
last_digits_regex
|> Regex.scan(puzzle_input, capture: :all_but_first)
|> List.flatten()
|> Stream.map(&digit_map[&1])
|> Enum.sum()
first_digits_sum * 10 + last_digits_sum
Here’s the livebook file: