Advent of Code 2020 - Day 4

I went ahead and made Passport a struct, but it didn’t turn out to be terribly important.

I thought about using Ecto.Schema for this, but the hand-rolled version is reasonably readable and has a strong “this could be written by a macro someday” smell.

Also check out the classic “sum type represented as a tagged tuple” pattern for a number with units.

GitHub

defmodule Aoc.Y2020.D4 do
  use Aoc.Boilerplate,
    transform: fn raw ->
      raw
      |> String.split("\n\n", trim: true)
      |> Enum.map(fn line ->
        line
        |> String.split()
        |> Enum.map(fn field ->
          [key, value] = String.split(field, ":")
          {key, value}
        end)
        |> Enum.into(%{})
      end)
    end

  @required_fields ~w(byr iyr eyr hgt hcl ecl pid)

  def part1(input \\ processed()) do
    input
    |> Enum.filter(&simple_valid_passport?/1)
    |> Enum.count()
  end

  def part2(input \\ processed()) do
    input
    |> Enum.filter(&(Enum.sort(@required_fields) == Enum.sort(Map.keys(&1) -- ["cid"])))
    |> Enum.filter(&strict_valid_passport?/1)
    |> Enum.count()
  end

  defp simple_valid_passport?(passport) do
    @required_fields
    |> Enum.all?(&Map.has_key?(passport, &1))
  end

  @doc """
  validates passport

      iex> Aoc.Y2020.D4.strict_valid_passport?(%{"byr" => "1980", "ecl" => "grn", "eyr" => "2030", "hcl" => "#623a2f", "hgt" => "74in", "iyr" => "2012", "pid" => "087499704"})
      true

      iex> Aoc.Y2020.D4.strict_valid_passport?(%{"byr" => "1989", "cid" => "129", "ecl" => "blu", "eyr" => "2029", "hcl" => "#a97842", "hgt" => "165cm", "iyr" => "2014", "pid" => "896056539"})
      true

      iex> Aoc.Y2020.D4.strict_valid_passport?(%{"byr" => "2001", "cid" => "88", "ecl" => "hzl", "eyr" => "2022", "hcl" => "#888785", "hgt" => "164cm", "iyr" => "2015", "pid" => "545766238"})
      true

      iex> Aoc.Y2020.D4.strict_valid_passport?(%{"byr" => "1944", "ecl" => "blu", "eyr" => "2021", "hcl" => "#b6652a", "hgt" => "158cm", "iyr" => "2010", "pid" => "093154719"})
      true

      iex> Aoc.Y2020.D4.strict_valid_passport?(%{"byr" => "1926", "cid" => "100", "ecl" => "amb", "eyr" => "1972", "hcl" => "#18171d", "hgt" => "170", "iyr" => "2018", "pid" => "186cm"})
      false

      iex> Aoc.Y2020.D4.strict_valid_passport?(%{"byr" => "1946", "ecl" => "grn", "eyr" => "1967", "hcl" => "#602927", "hgt" => "170cm", "iyr" => "2019", "pid" => "012533040"})
      false

      iex> Aoc.Y2020.D4.strict_valid_passport?(%{"byr" => "1992", "cid" => "277", "ecl" => "brn", "eyr" => "2020", "hcl" => "dab227", "hgt" => "182cm", "iyr" => "2012", "pid" => "021572410"})
      false

      iex> Aoc.Y2020.D4.strict_valid_passport?(%{"byr" => "2007", "ecl" => "zzz", "eyr" => "2038", "hcl" => "74454a", "hgt" => "59cm", "iyr" => "2023", "pid" => "3556412378"})
      false
  """
  def strict_valid_passport?(passport) do
    passport
    |> Enum.all?(&valid_field?/1)
  end

  defp valid_field?({"byr", byr}) do
    case Integer.parse(byr) do
      {byr_int, ""} -> byr_int in 1920..2002
      _ -> false
    end
  end

  defp valid_field?({"iyr", iyr}) do
    case Integer.parse(iyr) do
      {iyr_int, ""} -> iyr_int in 2010..2020
      _ -> false
    end
  end

  defp valid_field?({"eyr", eyr}) do
    case Integer.parse(eyr) do
      {eyr_int, ""} -> eyr_int in 2020..2030
      _ -> false
    end
  end

  defp valid_field?({"hgt", hgt}) do
    case Integer.parse(hgt) do
      {hgt_int, "cm"} -> hgt_int in 150..193
      {hgt_int, "in"} -> hgt_int in 59..76
      _ -> false
    end
  end

  defp valid_field?({"hcl", hcl}) do
    String.match?(hcl, ~r/^#[0-9a-f]{6}$/)
  end

  defp valid_field?({"ecl", ecl}) do
    ecl in ~w(amb blu brn gry grn hzl oth)
  end

  defp valid_field?({"pid", pid}) do
    String.match?(pid, ~r/^\d{9}$/)
  end

  defp valid_field?({"cid", _cid}), do: true
end

Chunking and regular expression? Fun stuff!

1 Like

part 2 - O(n)

defmodule Advent.Day4b do

  def start(file \\ "/tmp/input.txt"), do:
    File.stream!(file)
    |> Stream.chunk_by(fn o -> byte_size(o) == 1 end)
    |> Stream.filter(&(&1 !== ["\n"]))
    |> Stream.scan(0, fn o, _acc -> verify_passports(o) == 7 && 1 || 0 end)
    |> Enum.sum()

  defp verify_passports(o) do
    Stream.scan(o, 0, fn item, _acc ->
      item
      |> :binary.split([<<32>>, <<10>>], [:global, :trim])
      |> Enum.reduce(0, fn x, acc -> acc + verify_passport_item(x) end)
     end)
    |> Enum.sum()
  end

  def verify_passport_item(<<"byr:", data::binary>>), do: in_range(String.to_integer(data), 1920,2002)
  def verify_passport_item(<<"iyr:", data::binary>>), do: in_range(String.to_integer(data), 2010,2020)
  def verify_passport_item(<<"eyr:", data::binary>>), do: in_range(String.to_integer(data), 2020,2030)
  def verify_passport_item(<<"hgt:", data::24, "cm">>), do: <<data::24>> |> String.to_integer |> in_range(150,193)
  def verify_passport_item(<<"hgt:", data::16, "in">>), do: <<data::16>> |> String.to_integer |> in_range(59,76)
  def verify_passport_item(<<"hcl:#", data::48>>), do: <<data::48>> =~ ~r(^[a-z, 0-9]*$) && 1 || 0
  def verify_passport_item(<<"ecl:", data::binary>>) when data in ["amb", "blu", "brn", "gry", "grn", "hzl", "oth"], do: 1
  def verify_passport_item(<<"pid:", data::72>>), do: <<data::72>> =~ ~r(^[0-9]*$) && 1 || 0
  def verify_passport_item(_), do: 0

  defp in_range(v, min, max) when v >= min and v <= max, do: 1
  defp in_range(_v, _min, _max), do: 0

end

I hate my solution but here it is. The first time through part 1 I only cared about matching field names of the correct length. Decided that was too brittle so added a regex that matched the actual field names.

defmodule Day4 do
  import NimbleParsec

  @input File.read!("lib/input.txt") |> String.split("\n\n")

  defmodule ParsingHelper do
    byr =
      string("byr")
      |> ignore(string(":"))
      |> choice([
        string("1") |> string("9") |> ascii_char([?2..?9]) |> integer(1),
        string("2") |> string("0") |> string("0") |> ascii_char([?0..?2])
      ])

    # matches 5 entries

    iyr =
      string("iyr")
      |> ignore(string(":"))
      |> string("20")
      |> choice([
        string("1") |> ascii_char([?0..?9]),
        string("2") |> string("0")
      ])

    # matches 4 entries

    eyr =
      string("eyr")
      |> ignore(string(":"))
      |> string("20")
      |> choice([
        string("2") |> ascii_char([?0..?9]),
        string("3") |> string("0")
      ])

    # matches 4 entries

    hgt =
      string("hgt")
      |> ignore(string(":"))
      |> choice([integer(3) |> string("cm"), integer(2) |> string("in")])

    # matches 3 entries

    hcl =
      string("hcl")
      |> ignore(string(":"))
      |> string("#")
      |> ascii_string([?0..?9, ?a..?f], 6)

    # matches 3 entries

    ecl =
      string("ecl")
      |> ignore(string(":"))
      |> choice([
        string("amb"),
        string("blu"),
        string("brn"),
        string("gry"),
        string("grn"),
        string("hzl"),
        string("oth")
      ])

    # matches 2 entries

    pid = string("pid") |> ignore(string(":")) |> integer(9)
    # matches 2 entries

    cid = string("cid") |> ignore(string(":")) |> choice([integer(3), integer(2)])
    # matches 2 entries

    fields =
      repeat(
        choice([
          byr,
          iyr,
          eyr,
          hgt,
          hcl,
          ecl,
          pid,
          cid
        ])
        |> ignore(choice([string(" "), ascii_char([10..10]), empty()]))
      )

    defparsec(:fields, fields)
  end

  defp valid_fields?(fields, regex_match) do
    case length(fields) do
      8 -> true
      7 -> not Enum.member?(fields, regex_match)
      _ -> false
    end
  end

  def valid_number_of_fields() do
    @input
    |> Enum.map(&Regex.scan(~r/[a-z]{3}:/, &1))
    |> Enum.filter(&valid_fields?(&1, ["cid:"]))
    |> Enum.count()
  end

  def valid_number_of_fields_and_expected_names() do
    @input
    |> Enum.map(&Regex.scan(~r/(byr|iyr|eyr|hgt|hcl|ecl|pid|cid)(?=:)/, &1))
    |> Enum.filter(&valid_fields?(&1, ["cid", "cid"]))
    |> Enum.count()
  end

  def valid_fields_and_values() do
    @input
    |> Enum.map(&Day4.ParsingHelper.fields(&1))
    |> Enum.filter(fn {a, _, _, _, _, _} -> a == :ok end)
    |> Enum.filter(fn {_, b, _, _, _, _} ->
      length(b) == 25 or (length(b) == 23 and not Enum.member?(b, "cid"))
    end)
    |> Enum.count()
  end
end

IO.inspect(Day4.valid_number_of_fields())
IO.inspect(Day4.valid_number_of_fields_and_expected_names())
IO.inspect(Day4.valid_fields_and_values())

Very slowly catching up!

2 Likes