Advent of Code 2020 - Day 2

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

My solution:

#!/usr/bin/env elixir

xor = &((&1 and not(&2)) or (not(&1) and &2))

ok_for_part1? = fn{lo, hi, char, password}->
  password
  |> String.graphemes()
  |> Enum.count(& &1 == char)
  |> Kernel.in(lo..hi)
end

ok_for_part2? = fn{i, j, char, password}->
  xor.(
    String.at(password, i - 1) == char,
    String.at(password, j - 1) == char
  )
end

"./day2.txt"
|> File.stream!([], :line)
|> Stream.map(&String.trim/1)
|> Stream.map(&String.split(&1, ~r/[-: ]/, trim: true))
|> Stream.map(fn[lo, hi, char, password]-> {String.to_integer(lo), String.to_integer(hi), char, password} end)
|> Enum.count(ok_for_part1?)
|> IO.puts()

For part 2, just modify this line: |> Enum.count(ok_for_part1?)

2 Likes

Bit cheeky to mark your own post as the solution :stuck_out_tongue:

My answer was pretty similar, but I used a Regex with captures to parse the input - I prefer your use of String.split though. I was wondering if it was worth importing Bitwise, but like you I just wrote it manually.

1 Like

I’d like to mark everyone’s working code as solution, mine included :grin:

The Bitwise.^^^/2 only works on integers, so we can’t use it.

Following should work:

(String.at(password, i - 1) == char) != (String.at(password, j - 1) == char)
5 Likes

Since the input is all ANSI characters, it could be faster just handling bytes.

String.at(string, n) is O(n) if string contains only ANSI characters, but :binary.at(binary, n) is O(1). (Confirmed using Benchee)

#!/usr/bin/env elixir

xor = &((&1 and not(&2)) or (not(&1) and &2))

ok_for_part1? = fn{lo, hi, char, password}->
  password
  |> :binary.bin_to_list()
  |> Enum.count(& &1 == char)
  |> Kernel.in(lo..hi)
end

ok_for_part2? = fn{i, j, char, password}->
  xor.(
    :binary.at(password, i - 1) == char,
    :binary.at(password, j - 1) == char
  )
end

"./day2.txt"
|> File.stream!([], :line)
|> Stream.map(&String.trim/1)
|> Stream.map(&String.split(&1, ~r/[-: ]/, trim: true))
|> Stream.map(fn[lo, hi, char, password]-> {String.to_integer(lo), String.to_integer(hi), :binary.first(char), password} end)
|> Enum.count(ok_for_part1?)
|> IO.inspect()
2 Likes

I’m the only one who didn’t think about using regex it seems, although I think I like the String.split method used by Aetherus.

Thanks for sharing the solutions, it definitely gives me some inspiration on how to improve mine:
https://git.sr.ht/~servert/aoc2020/tree/master/day2/lib/day2.ex (not sure how to link to the version in this specific commit on sourcehut so it’s immutable).

2 Likes

Thank you for sharing your solution. I like the pins :+1:

Enum.count! That could’ve helped me. Oh, well.

defmodule Day02 do
  def readinput() do
    File.stream!("2.input.txt")
    |> Enum.map(&split/1)
  end

  def part1(input \\ readinput()) do
    Enum.reduce(input, 0, fn row, acc -> if valid1(row), do: acc + 1, else: acc end)
  end

  def part2(input \\ readinput()) do
    Enum.reduce(input, 0, fn row, acc -> if valid2(row), do: acc + 1, else: acc end)
  end

  def split(line) do
    [[_, left, right, char, password]] = Regex.scan(~r/(\d+)-(\d+) (\w): (\w+)/, line)

    [
      [String.to_integer(left), String.to_integer(right)],
      char,
      password
    ]
  end

  def valid1([[left, right], char, password]) do
    pchar = String.to_charlist(char)
    pwdlist = String.to_charlist(password)
    range = Range.new(left, right)

    Enum.reduce(pwdlist, 0, fn char, acc -> if [char] == pchar, do: acc + 1, else: acc end) in range
  end

  def valid2([[index1, index2], char, password]) do
    at1 = String.at(password, index1 - 1) == char
    at2 = String.at(password, index2 - 1) == char

    case [at1, at2] do
      [true, false] -> true
      [false, true] -> true
      _ -> false
    end
  end
end

2 Likes

My solution.

defmodule AdventOfCode.Day02 do
  def part1(input) do
    input
    |> String.trim()
    |> String.split("\n")
    |> Enum.map(fn line ->
      [min, max, letter, password] = get_line_data(line)
      check_password_1(String.to_integer(min), String.to_integer(max), letter, password)
    end)
    |> Enum.count(& &1)
  end

  def part2(input) do
    input
    |> String.trim()
    |> String.split("\n")
    |> Enum.map(fn line ->
      [pos_1, pos_2, letter, password] = get_line_data(line)
      check_password_2(String.to_integer(pos_1), String.to_integer(pos_2), letter, password)
    end)
    |> Enum.count(& &1)
  end

  def get_line_data(line) do
    Regex.run(~r/^(\d+)-(\d+) ([a-z]): ([a-z]+)$/, line, capture: :all_but_first)
  end

  def check_password_1(min, max, letter, password) do
    letter_count =
      password
      |> String.codepoints()
      |> Enum.count(&(&1 == letter))

    letter_count >= min && letter_count <= max
  end

  def check_password_2(pos_1, pos_2, letter, password) do
    letters = String.codepoints(password)
    # positions are 1-indexed
    pos_1_valid = Enum.at(letters, pos_1 - 1) == letter
    pos_2_valid = Enum.at(letters, pos_2 - 1) == letter

    pos_1_valid != pos_2_valid
  end
end

Edit: I forgot about String.at, that’s probably a minor refactor that could be done.

2 Likes

My approach to make it not only fast, but also clean and readable:

defmodule Solution do
  def read(path) do
    path
    |> File.stream!()
    |> Enum.map(&String.trim/1)
    |> Enum.map(&parse/1)
  end

  defp parse(input) do
    [spec, pass] = String.split(input, ": ", parts: 2)
    [range, <<char>>] = String.split(spec, " ", parts: 2)
    [min, max] =
      range
      |> String.split("-", parts: 2)
      |> Enum.map(&String.to_integer/1)

    {min..max, char, pass}
  end

  def validate_1({range, char, pass}) do
    count = for <<^char <- pass>>, reduce: 0, do: (n -> n + 1)

    count in range
  end

  def validate_2({a..b, char, pass}) do
    <<char_1>> = binary_part(pass, a - 1, 1)
    <<char_2>> = binary_part(pass, b - 1, 1)

    char_1 != char_2 and char in [char_1, char_2]
  end
end

data = Solution.read("2/input.txt")

IO.inspect(Enum.count(data, &Solution.validate_1/1), label: "task 1")
IO.inspect(Enum.count(data, &Solution.validate_2/1), label: "task 2")
6 Likes
defmodule Aoc.Y2020.D2 do
  use Aoc.Boilerplate,
    transform: fn raw ->
      raw
      |> String.split("\n")
      |> Enum.map(fn line ->
        [rules, password] = line |> String.split(": ")
        [occurance, letter] = rules |> String.split(" ")
        [min, max] = occurance |> String.split("-")

        {password, String.to_integer(min), String.to_integer(max), letter}
      end)
    end

  def part1(input \\ processed()) do
    input
    |> Enum.reduce(0, fn {password, min, max, letter}, valid_passwords ->
      if valid_password?(password, min, max, letter) do
        valid_passwords + 1
      else
        valid_passwords
      end
    end)
  end

  def part2(input \\ processed()) do
    input
    |> Enum.reduce(0, fn {password, position1, position2, letter}, valid_passwords ->
      if valid_toboggan_password?(password, position1, position2, letter) do
        valid_passwords + 1
      else
        valid_passwords
      end
    end)
  end

  @doc """
    Validates the password based on rules.
    Uses a regex, for example `~r/^([^a]*a){1,3}[^a]*$/`
    ^     Start of string
    (     Start of group
    [^a]* Any character except `a`, zero or more times
    a     our character `a`
    ){1,3} End and repeat the group 1 to 3 times
    [^a]* Any character except `a`, zero or more times again
    $     End of string
    Examples:
      iex> Aoc.Y2020.D2.valid_password?("aabbcc", 1, 2, "a")
      true
      iex> Aoc.Y2020.D2.valid_password?("aaabbcc", 1, 2, "a")
      false
  """
  def valid_password?(password, min, max, letter) do
    Regex.match?(~r/^([^#{letter}]*#{letter}){#{min},#{max}}[^#{letter}]*$/, password)
  end

  @doc """
    Validates the password based on Toboggan rule.
    Examples:
      iex> Aoc.Y2020.D2.valid_toboggan_password?("aabbcc", 1, 3, "a")
      true
      iex> Aoc.Y2020.D2.valid_toboggan_password?("aaabbcc", 1, 2, "a")
      false
  """
  def valid_toboggan_password?(password, position1, position2, letter) do
    letter1 = String.at(password, position1 - 1)
    letter2 = String.at(password, position2 - 1)

    (letter1 == letter || letter2 == letter) && letter1 != letter2
  end
end

Short solution part 1:

File.stream!("input")
|> Stream.filter(fn str ->
  [_, min, max, char, pass] = Regex.run(~r/^(\d+)-(\d+) (.): (\S+)/, str)
  count = String.graphemes(pass) |> Enum.frequencies() |> Map.get(char, 0)
  count >= String.to_integer(min) && count <= String.to_integer(max)
end)
|> Enum.count()
|> IO.puts()

5 Likes

Short solution part 2:

true_one = &if String.at(&1, String.to_integer(&2) - 1) == &3, do: 1, else: 0

File.stream!("input")
|> Enum.count(fn str ->
  [_, p1, p2, char, pass] = Regex.run(~r/^(\d+)-(\d+) (.): (\S+)/, str)
  true_one.(pass, p1, char) + true_one.(pass, p2, char) == 1
end)
|> IO.puts()
1 Like

My solution:

defmodule AdventOfCode.DayTwo do
  def validate_1(path) do
    path
    |> read_file()
    |> String.split("\r\n", trim: true)
    |> Enum.filter(&process_input_1/1)
    |> Enum.count()
    |> IO.puts()
  end

  def validate_2(path) do
    path
    |> read_file()
    |> String.split("\r\n", trim: true)
    |> Enum.filter(&process_input_2/1)
    |> Enum.count()
    |> IO.puts()
  end

  def process_input_1(input) do
    [spec, password] = String.split(input, ": ")
    [range, key] = String.split(spec, " ")
    [min, max] = String.split(range, "-")
    count = password |> String.graphemes() |> Enum.frequencies() |> Map.get(key, 0)
    count >= String.to_integer(min) && count <= String.to_integer(max)
  end

  def process_input_2(input) do
    [spec, password] = String.split(input, ": ")
    [range, key] = String.split(spec, " ")
    [min, max] = String.split(range, "-")
    min = String.to_integer(min)
    max = String.to_integer(max)
    letter1 = String.at(password, min - 1)
    letter2 = String.at(password, max - 1)

    (letter1 == key || letter2 == key) && letter1 != letter2
  end

  def read_file(path) do
    case File.read(path) do
      {:ok, body} ->
          body
      {:error, reason} ->
          reason
    end
  end
end

AdventOfCode.DayTwo.validate_1("input.txt")
AdventOfCode.DayTwo.validate_2("input.txt")
1 Like

Seems no one stumbled upon :erang.xor :slight_smile:

defmodule Event2 do
  def run do
    IO.puts("Test part1: #{part1("input/event2/test.txt")}")
    IO.puts("Puzzle part1: #{part1("input/event2/puzzle.txt")}")
    IO.puts("Test part2: #{part2("input/event2/test.txt")}")
    IO.puts("Puzzle part2: #{part2("input/event2/puzzle.txt")}")
  end

  def part1(path), do: input_stream(path) |> Stream.filter(&filter_fun/1) |> Enum.count()
  def part2(path), do: input_stream(path) |> Stream.filter(&filter_fun2/1) |> Enum.count()

  def input_stream(path), do: path |> File.stream!() |> Stream.map(&parse_input/1)

  def parse_input(input) do
    [low, high, letter, pass] = String.trim(input) |> String.split(~r/[-: ]/, trim: true)
    {String.to_integer(low), String.to_integer(high), letter, pass}
  end

  def filter_fun({low, high, letter, pass}) do
    pass_letter_count = pass |> String.graphemes() |> Enum.count(&(&1 == letter))
    low <= pass_letter_count and pass_letter_count <= high
  end

  def filter_fun2({low, high, letter, pass}),
    do: :erlang.xor(String.at(pass, low - 1) == letter, String.at(pass, high - 1) == letter)
end
4 Likes

Not sure how kosher it is to use an import but this seemed like a good exercise for using a parser library. I really like how easy NimbleParsec is to use.

defmodule Day2 do
  import NimbleParsec

  rule =
    integer(min: 1, max: 3)
    |> ignore(string("-"))
    |> integer(min: 1, max: 3)
    |> ignore(string(" "))
    |> ascii_char([?a..?z])
    |> ignore(string(": "))

  defparsec(:pw_parse, rule)

  def pw_check(file, fun) do
    File.stream!(file)
    |> Enum.map(&pw_parse(&1))
    |> Enum.count(&fun.(&1))
  end
end

valid? = fn parsed ->
  case parsed do
    {:ok, [a, b, c], pw, _, _, _} ->
      a..b
      |> Enum.member?(String.to_charlist(pw) |> Enum.count(fn x -> x == c end))

    _ ->
      false
  end
end

valid2? = fn parsed ->
  case parsed do
    {:ok, [a, b, c], pw, _, _, _} ->
      [a - 1, b - 1]
      |> Enum.count(&(Enum.at(String.to_charlist(pw), &1) == c))
      |> Kernel.==(1)

    _ ->
      false
  end
end

IO.inspect(Day2.pw_check("lib/input.txt", valid?))

IO.inspect(Day2.pw_check("lib/input.txt", valid2?))

4 Likes

Seems like we all came up with the same solution, more or less :slight_smile:

defmodule Aoc2020.Day02 do
  def part1() do
    input_stream("lib/02/input.txt")
    |> Enum.count(fn {policy, password} -> valid_password?(password, policy) end)
    |> IO.inspect(label: "part1")
  end

  def part2() do
    input_stream("lib/02/input.txt")
    |> Enum.count(fn {policy, password} ->
      valid_password2?(password, policy)
    end)
    |> IO.inspect(label: "part2")
  end

  defp input_stream(path) do
    File.stream!(path)
    |> Stream.map(fn line ->
      [policy, password] = String.split(line, ":")
      {parse_policy(policy), String.trim(password)}
    end)
  end

  def parse_policy(policy) do
    [spec, char] = String.split(policy, " ", parts: 2)
    [min, max] = String.split(spec, "-", parts: 2)

    {String.to_integer(min), String.to_integer(max), char}
  end

  def valid_password?(password, {min, max, char}) do
    password
    |> String.graphemes()
    |> Enum.count(fn x -> x === char end)
    |> Kernel.in(min..max)
  end

  @spec valid_password2?(binary, {integer, integer, any}) :: boolean
  def valid_password2?(password, {a, b, char}) do
    xor(String.at(password, a - 1) === char, String.at(password, b - 1) === char)
  end

  defp xor(a, b) when is_boolean(a) and is_boolean(b), do: a != b
end
2 Likes

I really like your solutions. Would it be too much too ask if you could explain this syntax a little bit?

<<^char <- pass>> is a generator I presume?
for, reduce, do - I remember vaguely it was one of the newer syntax in 1.10 or about but can’t find the docs now.

1 Like

I’ll give it a shot.
<<^char <- pass>> filters the characters from the pass string to match the char which is pinned to avoid reassignment. I confess I’m not sure why the pin is necessary. You can see the docs here about the filtering step. This works because wrapping it in the << ... >> syntax exposes the bitstring representation of pass.

Generators can also be used to filter as it removes any value that doesn’t match the pattern on the left side of <-

The pin operator is discussed here.

Hopefully someone more knowledgeable will correct me or explain it better.

1 Like