Advent of Code 2020 in Elixir - Day 4

Passport Processing

defmodule Aoc2020.Day4 do
  @moduledoc "Passport Processing"

  def parse_passport(str) do
    str
    |> String.replace("\n", " ")
    |> String.split(" ")
    |> Enum.map(&String.split(&1, ":"))
    |> Enum.map(fn [k, v] -> {String.to_atom(k), v} end)
  end

  def parse_input() do
    File.read!("priv/inputs/2020/day4.txt")
    |> String.trim()
    |> String.split("\n\n")
    |> Enum.map(&parse_passport/1)
  end

  def split_into_passports(lst) do
    chunk_fun = fn element, acc ->
      if element == "|" do
        {:cont, Enum.reverse(acc), []}
      else
        {:cont, [element | acc]}
      end
    end

    after_fun = fn
      [] -> {:cont, []}
      acc -> {:cont, Enum.reverse(acc), []}
    end

    Enum.chunk_while(lst, [], chunk_fun, after_fun)
  end

  def drop_cid(passport) do
    Enum.filter(passport, fn {k, _} -> k != :cid end)
  end

  # part 2

  def between(value, min, max) do
    String.to_integer(value) >= min && String.to_integer(value) <= max
  end

  def valid_birthyear(passport) do
    Keyword.has_key?(passport, :byr) && between(passport[:byr], 1920, 2002)
  end

  def valid_issue_year(passport) do
    Keyword.has_key?(passport, :iyr) && between(passport[:iyr], 2010, 2020)
  end

  def valid_expiration_year(passport) do
    Keyword.has_key?(passport, :eyr) && between(passport[:eyr], 2020, 2030)
  end

  def valid_height(passport) do
    if passport[:hgt] do
      with [_, num, unit] <- Regex.run(~r/(\d+)(cm|in)/, passport[:hgt]) do
        case unit do
          "cm" -> between(num, 150, 193)
          "in" -> between(num, 59, 76)
          _ -> false
        end
      else
        nil -> false
      end
    else
      false
    end
  end

  def valid_hair_colour(passport) do
    Keyword.has_key?(passport, :hcl) && String.match?(passport[:hcl], ~r/^#[0-9a-f]{6}$/)
  end

  def valid_eye_colour(passport) do
    Keyword.has_key?(passport, :ecl) &&
      Enum.member?(~w(amb blu brn gry grn hzl oth), passport[:ecl])
  end

  def valid_passport_id(passport) do
    Keyword.has_key?(passport, :pid) && String.match?(passport[:pid], ~r/^[0-9]{9}$/)
  end

  def part_1 do
    parse_input()
    |> Enum.map(&drop_cid/1)
    |> length
  end

  def part_2 do
    validators = [
      &valid_birthyear/1,
      &valid_issue_year/1,
      &valid_expiration_year/1,
      &valid_height/1,
      &valid_hair_colour/1,
      &valid_eye_colour/1,
      &valid_passport_id/1
    ]

    parse_input()
    |> Enum.map(&drop_cid/1)
    |> Enum.filter(fn passport -> Enum.all?(validators, &apply(&1, [passport])) end)
    |> length
  end

  def run do
    part_2()
  end
end