### Advent of Code - Day 3

Mon, Jan 03 2022

There has been a gap as I went on a Xmas break. In Australia, we have our Summer holidays at Xmas time so tend to take longer holidays.

## Day Three - Part A

Part A is pretty straightforward. The biggest issue was working out how to handle data once I transformed it. I decided to use strings, but more on this later.

The data is a list of strings representing a list of bits.

``[ "011000001000", "111000110101", "101000011100", "100110010100","001111111111" ...]``

To find the solution, I needed to transform the list to determine the most common bit at each position, i.e. are there more `1` or `0` at each index for the complete list. For the sample above at index 0, there are more `1` than `0`, so the result will start with `1` at index 0 and so on until the last index where there are more `0` than `1`.

The first thing I wanted to do was convert the data into a list of lists. My two options are to create a `charlist` or a `list`. I decided to use a list because I had more options for manipulating a list in the core library (something I realised later is untrue).

``    lists = Enum.map(@data, &String.split(&1, "", trim: true))``

The next thing I wanted to do was create a list of values for each index.

``````  defp lists_by_position(lists, length_of_list) do
for i <- 0..length_of_list - 1 do
Enum.map(lists, &Enum.at(&1, i))
end
end

# result - list of all bits at each index
[
["0", "1", "0", "1", "1", "0", "0", "0", "1", "0", "0", "0", "0", "0", "1",
"1", "1", "0", "0", "0", "1", "1", "0", "0", "1", "1", "1", "1", "1", "0",
"1", "0", "0", "1", "1", "1", "1", "1", "0", "0", "0", "0", "0", "0", "1",
"1", "1", "0", "1", ...],
["0", "0", "0", "0", "1", "0", "1", "1", "1", "0", "1", "0", "0", "0", "0",
"0", "1", "1", "0", "1", "1", "0", "1", "0", "1", "1", "1", "0", "0", "0",
"1", "1", "1", "1", "0", "0", "0", "1", "1", "1", "0", "0", "1", "1", "1",
"0", "1", "1", ...]
...
]
``````

I was then able to count the most common bits for each position. I used an `Enum.reduce/3` function and started by counting the `1` for each index. If the number was greater than half of the total, I added `1` to the accumulator; if not, I added `0`. Finally, because I am adding to the `head` of the list, I reversed it before returning the value.

``````  defp calc_most_common_bit(lists_to_count, length_of_pos_list) do
Enum.reduce(lists_to_count, [], fn list, acc ->
ones = Enum.count(list, &(&1 == "1"))
bit = if ones > length_of_pos_list/2, do: "1", else: "0"
[bit | acc]
end)
|> Enum.reverse()
end

# result - the most common bits at each index for the list of data
["0", "0", "1", "1", "1", "0", "1", "0", "0", "1", "1", "1"]
``````

`calc_most_common_bit` gave me the value of `gamma` represented by the most common bits at each index. To calculate `epsilon` (The least common bits), I reversed the `gamma` values.

``````  defp bitwise_of_gamma(gamma_list) do
Enum.reduce(gamma_list, [], fn i, acc ->
[(if i == "1", do: "0", else: "1") | acc]
end)
|> Enum.reverse
end``````

Now I had the `gamma` and `epsilon` values and needed to convert them into `integer`. To do this, I needed to convert the list of strings to a `charlist`; I also realised that I could have been working with a `charlist` all along (Lesson learnt :)). I created a function to convert to `charlist` and an `integer`.

``````  defp to_integer(list) do
list
|> List.to_charlist()
|> List.to_integer(2)
end``````

Bringing this all together, I have a function that calculates the power consumption of the submarine.

``````  def day_three_a() do
lists =
Enum.map(@data, &String.split(&1, "", trim: true))

length_of_raw_list = Enum.count(List.first(lists))

lists_to_count = lists_by_position(lists, length_of_raw_list)

length_of_pos_list = Enum.count(List.first(lists_to_count))

gamma_list = calc_most_common_bit(lists_to_count, length_of_pos_list)
epsilon_list = bitwise_of_gamma(gamma_list)

gamma = to_integer(gamma_list)
epsilon = to_integer(epsilon_list)

gamma * epsilon
end``````

Everything worked, and I have my star!

## Day Two - Part B

The second part of this problem took me way longer than it should have, I blame being on holidays, but I made a fundamentally wrong assumption upfront. Below is the solution I ended up with; I will point out my flawed assumption below.

This time I had to filter the list based on the most common bit at each position, discarding values as they failed the test provided in the problem. For the `@data` I needed to look at each bit value at each index and for the `oxygen generator rating` keep the numbers that have the most common bits (Defaulting to `1` if it is even) and for the `CO2 scrubber rating` keep numbers that have the least common values at each index.

As I have done so far, I wanted to use recursion as I find it easier to read and debug when there are issues. I started with filtering for the most common bits for each index. I started with my exit, and I knew that my answer was as soon as I had one value.

``  defp filter_by_most_common(matches) when length(matches) == 1, do: matches``

Now I needed to add some logic; I started by getting the most common bit at the current position I was testing. I realised that I needed an index value to track where to test the value. So I started with all the data and filtered it down to a single value.

``````  defp filter_by_most_common(matches, _idx) when length(matches) == 1, do: matches
defp filter_by_most_common(matches, idx) do
bit =
matches
|> lists_by_position(Enum.count(List.first(matches)))
|> calc_most_common_bit(Enum.count(matches))
|> Enum.at(idx)
end``````

I could reuse the functions for Part A and grab the bit value based on the index. This is where I had initially made a mistake. Instead of calculating the most common bit for the filtered list, I started by passing in the result of `calc_most_common_bit/2`. This gave me an incorrect answer and proved challenging to debug. After rereading the question, I realised that the test was based on the most common bit in the remaining list. I wasted over an hour debugging my logic!

Now that I had the most common bit at `index = n`, I was able to get the matches and pass them back to the function.

``````  defp filter_by_most_common(matches, _idx) when length(matches) == 1, do: matches
defp filter_by_most_common(matches, idx) do
bit =
matches
|> lists_by_position(Enum.count(List.first(matches)))
|> calc_most_common_bit(Enum.count(matches))
|> Enum.at(idx)

matches = Enum.filter(matches, &(Enum.at(&1, idx) == bit))

filter_by_most_common(matches, idx + 1)
end``````

Unfortunately, this did not work. After rereading the question, if the most and least common bits were equal, I needed to default to a `1` or a `0`, depending on what I calculated. I added some more logic to test the equality of the counts and used a default value for the filter.

``````  defp filter_by_most_common(matches, _idx, _default) when length(matches) == 1, do: matches
defp filter_by_most_common(matches, idx, default) do
bit =
matches
|> lists_by_position(Enum.count(List.first(matches)))
|> calc_most_common_bit(Enum.count(matches))
|> Enum.at(idx)

match = Enum.filter(matches, &(Enum.at(&1, idx) == bit))
dont_match = Enum.filter(matches,
&(Enum.at(&1, idx) == (if bit == "1", do: "0", else: "1"))
)

matches =
cond do
Enum.count(match) == Enum.count(dont_match) ->
Enum.filter(matches, &(Enum.at(&1, idx) == default))
true -> match
end

filter_by_most_common(matches, idx + 1, default)
end``````

I spent a bit more time refactoring but, in the end, ended up using a bit of `copy/paste coding`. I will refactor the recursion and pass in a function to provide a default filter if I get time. In the meantime, I added the following code.

``````  defp filter_by_least_common(matches, _idx, _default) when length(matches) == 1, do: matches
defp filter_by_least_common(matches, idx, default) do
bit =
matches
|> lists_by_position(Enum.count(List.first(matches)))
|> calc_most_common_bit(Enum.count(matches))
|> bitwise_of_gamma()
|> Enum.at(idx)

match = Enum.filter(matches, &(Enum.at(&1, idx) == bit))
dont_match = Enum.filter(matches, &(Enum.at(&1, idx) == (if bit == "1", do: "0", else: "1")))

matches =
cond do
Enum.count(match) == Enum.count(dont_match) ->
Enum.filter(matches, &(Enum.at(&1, idx) == default))
true -> match
end

filter_by_least_common(matches, idx + 1, default)
end``````

Now that my filters were working, I created a header function and ran the code.

``````  def day_three_b() do
lists =
Enum.map(@data, &String.split(&1, "", trim: true))

[o2_list] = filter_by_most_common(lists, 0, "1")
[co2_list] = filter_by_least_common(lists, 0, "0")

o2 = to_integer(o2_list)
co2 = to_integer(co2_list)

o2 * co2
end``````

Running using my data, and I get the correct answer … woohoo!