Advent of Code 2020 - Day 4

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

Thanks to @egze, we have a private leaderboard:
https://adventofcode.com/2020/leaderboard/private/view/39276

The join code is:
39276-eeb74f9a

A straightforward solution for Part 2:

#!/usr/bin/env elixir

defmodule Validator do

  def valid?({"byr", value}) do
    value
    |> String.to_integer()
    |> Kernel.in(1920..2002)
  end

  def valid?({"iyr", value}) do
    value
    |> String.to_integer()
    |> Kernel.in(2010..2020)
  end

  def valid?({"eyr", value}) do
    value
    |> String.to_integer()
    |> Kernel.in(2020..2030)
  end

  def valid?({"hgt", value}) do
    case String.split(value, ~r/(?<=\d)(?=[a-z])/) do
      [num, unit] ->
        valid_height?(String.to_integer(num), unit)
      _ -> false
    end
  end

  def valid?({"hcl", value}) do
    value =~ ~r/^\#[0-9a-f]{6}$/
  end

  def valid?({"ecl", value}) do
    value in ~w[amb blu brn gry grn hzl oth]
  end

  def valid?({"pid", value}) do
    value =~ ~r/^\d{9}$/
  end

  def valid?({_key, _value}) do
    true
  end

  defp valid_height?(num, "cm"), do: num in (150..193)
  defp valid_height?(num, "in"), do: num in (59..76)
  defp valid_height?(_, _), do: false
end

required_fields = ~w[
  byr
  iyr
  eyr
  hgt
  hcl
  ecl
  pid
]

to_passport = fn(fields)->
  fields
  |> Stream.map(&String.split(&1, ":"))
  |> Stream.map(&List.to_tuple/1)
  |> Map.new()
end

valid? = fn(passport)->
  has_all_required_keys? = Enum.all?(required_fields, &Map.has_key?(passport, &1))
  all_values_are_valid? = Enum.all?(passport, &Validator.valid?/1)
  has_all_required_keys? and all_values_are_valid?
end

"day4.txt"
|> File.read!()
|> String.split("\n\n")
|> Stream.map(&String.split/1)
|> Enum.map(to_passport)
|> Enum.count(valid?)
|> IO.puts()
4 Likes

I see some pattern matching in there, but you could do a lot more of it. Also, Integer.parse is your friend on the height field.

1 Like

Observations:

  • Woof, data validation… kinda tedious. A lot like work, and I do AoC to take a break from work…
  • I anticipated having to look up things, so I parsed into a map, but that ended up being entirely unnecessary.
  • Thank goodness for pattern matching.

Excerpt:

  def all_fields_valid?(record) do
    Enum.all?(record, fn {field, value} -> valid?(field, value) end)
  end

  def valid?(field, year) when field in ["byr", "eyr", "iyr"] and is_binary(year),
    do: valid?(field, String.to_integer(year))

  def valid?("byr", year) when year in 1920..2002, do: true
  def valid?("iyr", year) when year in 2010..2020, do: true
  def valid?("eyr", year) when year in 2020..2030, do: true
  def valid?("hgt", {cm, "cm"}) when cm in 150..193, do: true
  def valid?("hgt", {inch, "in"}) when inch in 59..76, do: true
  def valid?("hgt", {_, _}), do: false
  def valid?("hgt", height), do: valid?("hgt", Integer.parse(height))

  def valid?("hcl", color), do: Regex.match?(~r/^#[0-9a-f]{6}$/, color)

  def valid?("ecl", color) when color in ["amb", "blu", "brn", "gry", "grn", "hzl", "oth"],
    do: true

  def valid?("pid", pid), do: Regex.match?(~r/^[0-9]{9}$/, pid)
  def valid?("cid", _), do: true
  def valid?(_, _), do: false
5 Likes

Yea, pattern matching provides great help in validation. That’s one of the reasons why FP languages rock.

As this is typical day to day work I just imported Ecto in my solution

2 Likes

As this is typical day to day work …

True. This year’s quizzes are not so interesting.

At least properly collecting passports via Stream.transform was fun learning experience. I’m using AoC to properly dig into that module.

1 Like

Seems like you are seeking fun in a quite boring quiz. I did so too, in Day 1. See my last post in Day 1’s thread.

Nothing much to add.

I missed a trick not overloading the validator function for each case, so that you can just validate the map with Enum.all - that’s pretty nice.

I forgot about the =~ operator, and I used to work with Perl!

1 Like

I’m not familiar with Perl, but I coded and am still coding a lot in Ruby. Is the =~ operator an Perl invention?

Not much, I dit not reach for Ecto intentionally… which I would have done if it was production code. :slight_smile:

The most interesting part was to use Stream.chunk_while to cleanup the input:

def input_stream(path) do
    File.stream!(path)
    |> Stream.chunk_while(
      [],
      fn
        "\n", parts ->
          {:cont, Enum.join(parts, " "), []}

        part, parts ->
          {:cont, [String.trim(part) | parts]}
      end,
      fn parts -> {:cont, Enum.join(parts, " "), []} end
    )
    |> Stream.map(&parse/1)
  end

  defp parse(line) do
    line
    |> String.trim()
    |> String.split(" ")
    |> Enum.into(%{}, fn kv ->
      [k, v] = String.split(kv, ":", parts: 2)
      {k, v}
    end)
  end
1 Like

In these 4 days, it seems we are digging deeper and deeper into the Stream module :grin:

So far we’ve seen

  • Stream.map/2
  • Stream.drop/2
  • Stream.transform/3
  • Stream.cycle/1
  • Stream.unfold/2
  • Stream.take_every/2
  • Stream.chunk_while/4

Let’s see what else functions will be used in future solutions.

Nice use of ~w[] @Aetherus I should really start using some more of these sigils. Here my solution before seeing this:

#!/usr/bin/env elixir
must_have = MapSet.new(["byr", "iyr", "eyr", "hgt", "hcl", "ecl", "pid"])
colors = ["amb", "blu", "brn", "gry", "grn", "hzl", "oth"]
check_int = fn (int, min, max) ->
  case Integer.parse(int) do
    {num, ""} -> num >= min and num <= max
    _ -> false
  end
end

result = File.read!("4.csv")
|> String.split("\n\n", trim: true)
|> Enum.map(fn record ->
  entries = String.replace(record, "\n", " ") |> String.split(" ")
    |> Enum.map(fn field ->
      field = String.split(field, ":")
      case field do
        ["byr", <<year :: binary-size(4)>>] -> check_int.(year, 1920, 2002)
        ["iyr", <<year :: binary-size(4)>>] -> check_int.(year, 2010, 2020)
        ["eyr", <<year :: binary-size(4)>>] -> check_int.(year, 2020, 2030)
        ["hgt", <<cm :: binary-size(3), "cm">>] -> check_int.(cm, 150, 193)
        ["hgt", <<inch :: binary-size(2), "in">>] -> check_int.(inch, 59, 76)
        ["hcl", <<"#", hex :: binary-size(6)>>] -> Regex.match?(~r/^[0-9a-f]+$/, hex)
        ["ecl", color] -> Enum.member?(colors, color)
        ["pid", <<num :: binary-size(9)>>] -> Regex.match?(~r/^[0-9]+$/, num)
        _other -> false
      end
      |> if do
        hd(field)
      end
    end)
    |> MapSet.new()

  MapSet.size(MapSet.difference(must_have, entries)) == 0
end)
|> Enum.count(fn valid -> valid end)

:io.format("~p~n", [result])
1 Like

I really like the not so harsh ramp-up I experienced last year, but I also hope for a bit more variety in the upcoming challenges.

1 Like

Brilliant! I’ve used Integer.parse some days ago, and I wonder why it didn’t come to me this time :roll_eyes:

Not sure who invented the operator, but Regex in most languages is inspired by Perl, AFAIK. Elixir/Erlang use “Perl-like regular expressions”.

Naive solution, with some pattern matching and some regex:

defmodule AdventOfCode.Day04 do
  def part1(input) do
    input
    |> String.split("\n\n")
    |> Enum.map(&valid_passport_1?/1)
    |> Enum.count(& &1)
  end

  def part2(input) do
    input
    |> String.split("\n\n")
    |> Enum.map(&(valid_passport_1?(&1) and valid_passport_2?(&1)))
    |> Enum.count(& &1)
  end

  def valid_passport_1?(passport) do
    ~w/byr iyr eyr hgt hcl ecl pid/
    |> Enum.map(&String.contains?(passport, &1 <> ":"))
    |> Enum.all?(& &1)
  end

  def valid_passport_2?(passport) do
    passport
    |> String.split()
    |> Enum.map(fn fragment ->
      [field, content] = String.split(fragment, ":")
      valid_field?(field, content)
    end)
    |> Enum.all?(& &1)
  end

  def valid_field?("byr", date), do: valid_range?(date, 1920..2002)
  def valid_field?("iyr", date), do: valid_range?(date, 2010..2020)
  def valid_field?("eyr", date), do: valid_range?(date, 2020..2030)

  def valid_field?("hgt", height) do
    case String.split_at(height, -2) do
      {h, "cm"} -> valid_range?(h, 150..193)
      {h, "in"} -> valid_range?(h, 59..76)
      _ -> false
    end
  end

  def valid_field?("hcl", "#" <> color) do
    Regex.match?(~r/^([0-9a-f]){6}$/, color)
  end

  def valid_field?("hcl", _), do: false

  def valid_field?("ecl", color) when color in ~w/amb blu brn gry grn hzl oth/, do: true
  def valid_field?("ecl", _), do: false

  def valid_field?("pid", id) do
    Regex.match?(~r/^([0-9]){9}$/, id)
  end

  def valid_field?("cid", _), do: true

  def valid_field?(_, _), do: false

  def valid_range?(num, range) do
    try do
      num |> String.to_integer() |> Kernel.in(range)
    catch
      _ -> false
    end
  end
end

Enum.all? could’ve saved me a bunch of code.

defmodule Day04 do
  @requiredkeys MapSet.new(~w{byr iyr eyr hgt hcl ecl pid})
  @eyecolors MapSet.new(~w{amb blu brn gry grn hzl oth})

  # returns a list of maps
  def readinput() do
    File.read!("4.input.txt")
    |> String.split("\n\n")
    |> Enum.map(&String.replace(&1, "\n", " "))
    |> Enum.map(fn entry ->
      Regex.scan(~r/(\w+):(\S+)/, entry, capture: :all_but_first)
      |> Enum.map(&List.to_tuple/1)
      |> Map.new()
    end)
  end

  def allkeys?(entry) do
    entry
    |> Enum.filter(fn k -> k != "cid" end)
    |> MapSet.new()
    |> MapSet.equal?(@requiredkeys)
  end

  def valid?({"byr", value}),
    do: String.length(value) == 4 && String.to_integer(value) in 1920..2002

  def valid?({"iyr", value}),
    do: String.length(value) == 4 && String.to_integer(value) in 2010..2020

  def valid?({"eyr", value}),
    do: String.length(value) == 4 && String.to_integer(value) in 2020..2030

  def valid?({"hgt", value}) do
    case hd(Regex.scan(~r/(\d+)(\w+)/, value, capture: :all_but_first)) do
      [cm, "cm"] -> String.to_integer(cm) in 150..193
      [inch, "in"] -> String.to_integer(inch) in 59..76
      _ -> false
    end
  end

  def valid?({"hcl", value}),
    do:
      String.first(value) == "#" && String.length(value) == 7 &&
        Regex.match?(~r/[0-9a-f]/, String.trim_leading(value, "#"))

  def valid?({"ecl", value}), do: MapSet.member?(@eyecolors, value)

  def valid?({"pid", value}), do: String.length(value) == 9 && Regex.match?(~r/[0-9]/, value)

  def valid?({"cid", _}), do: false

  def part1(input \\ readinput()) do
    input
    |> Enum.map(&Map.keys/1)
    |> Enum.map(&allkeys?/1)
    |> Enum.count(& &1)
  end

  def part2(input \\ readinput()) do
    input
    |> Enum.map(fn entry ->
      entry
      |> Enum.map(fn {k, v} -> if valid?({k, v}), do: k end)
      |> Enum.filter(& &1)
    end)
    |> Enum.map(&allkeys?/1)
    |> Enum.count(& &1)
  end
end

My code looks a lot like yours, pattern matching rocks :sunglasses: