Advent of Code 2020 - Day 2

Mostly what @stevensonmt said. The pin operator is needed to filter characters, as generator will allow only matches, in other words it is the same as:

for <<c <- pass>>, match?(^char, c), …

Which in the end behaves exactly the same as:

for <<c <- pass>>, char == c, …

But is shorter and a little bit more confusing definition of such behaviour.

The rest is new, as you have spotted, syntax for defining reduction. In short:

for item <- generator, reduce: x, do: (n -> …)

Is the same as:

Enum.reduce(generator, x, fn item n -> … end)

So in the end it just count characters that match given byte.

5 Likes

Is this run through the list even needed? (if so you could always just make this part of parse/1, just curious)

I can’t believe I have been doing this for this long without knowing about binary_part/3 - thanks!

The <<^char <- pass>> part is cool! Thank you for showing me another thing in Elixir that I don’t know :heavy_heart_exclamation:

1 Like

O(mn) solution.

solution 1

defmodule Advent.Day2 do
  import String, only: [split: 2, to_integer: 1, at: 2]

  def start(part, file \\ "/tmp/data.txt") do
    File.read!(file)
    |> String.split("\n")
    |> Enum.reduce(0, fn line, acc -> acc + is_valid(line, part) end)
  end

  defp is_valid(line, _part) when line in ["", nil], do: 0
  defp is_valid(line, :part1) do
    [min, max, letter, _, passwd] = split(line, ["-", " ", ":"])
    <<word::utf8>> = letter
    count = Enum.count(to_charlist(passwd), fn x -> x == word end)
    ((to_integer(min) <= count) and (count <= to_integer(max))) && 1 || 0
  end
  defp is_valid(line, :part2) do
    [p1, p2, letter, _, passwd] = split(line, ["-", " ", ":"])
    case {at(passwd, to_integer(p1) - 1) == letter, at(passwd, to_integer(p2) - 1) == letter} do
       {true, false} -> 1
       {false, true} -> 1
       _ -> 0
    end
  end

end

antoher solution for part1

defmodule Advent.Day2b do
  import String, only: [split: 2, to_integer: 1, replace: 3, at: 2]

  def start(part, file \\ "/tmp/data.txt") do
    File.read!(file)
    |> split("\n")
    |> Enum.reduce(0, fn line, acc -> acc + is_valid(line, part) end)
  end

  defp is_valid(line, _) when line in ["", nil], do: 0
  defp is_valid(line, :part1) do
    [min, max, letter, _, passwd] = split(line, ["-", " ", ":"])
    count = byte_size(passwd) - byte_size(replace(passwd, letter, ""))
    ((to_integer(min) <= count) and (count <= to_integer(max))) && 1 || 0
  end
  defp is_valid(line, :part2) do
    [p1, p2, letter, _, passwd] = split(line, ["-", " ", ":"])
    case {at(passwd, to_integer(p1) - 1) == letter, at(passwd, to_integer(p2) - 1) == letter} do
       {true, false} -> 1
       {false, true} -> 1
       _ -> 0
    end
  end

end
1 Like

What is the benefit of duing

over something like

pass
|> String.codepoints()
|> Enum.count(& &1 == char)
1 Like

Codepoints is slower as it needs to understand UTF-8 encoding. Mine is just passing over all bytes in the binary. Also mine does everything in one pass instead of 2 passes (one pass over the binary to construct list and another over the list to count occurrences). Not that this matter in this case, but I didn’t wanted to do 2 passes.

1 Like

I was trying this today, but for take the total in the final I was unable.
someone could help me with this?
trying this

defmodule Day2 do
  @type claim :: String.t
  @type parsed_claim :: list
  @spec parse_claim(binary) :: :ok

  @moduledoc """
  Advent of Code `Day2`.
  """

  def values do
      values =
        File.read!("lib/input.txt")
        |> String.trim()
        |> String.split("\n")
        |> Enum.map(&parse_claim/1)
  end

  @spec parse_claim(claim) :: parsed_claim
  def parse_claim(string) when is_binary(string) do
    string
    |> String.split(["-", " ", ":", " "], trim: true)
    |> claimed_values()
  end


  def claimed_values(parsed_claims) do

    [min, max, char_str, str] = parsed_claims

      r_min = String.to_integer(min)
      r_max = String.to_integer(max)

      #validate password

      if str |> String.contains?(char_str) do

        char_key = take_char(char_str)
        list_char = take_char(str)

        calculate_password([r_min, r_max, char_key, list_char])
      end
  end

  defp take_char(char) do
    char
    |> String.graphemes
    |> Enum.map(&String.to_charlist/1)
  end

  defp calculate_password([min, max, char_key, list_char]) do
    count = Enum.count(list_char, &(&1 == char_key))

    if ((count >= min) && (count <= max)), do: total + 1, else: total
  end

end

Sorry for being late (I was sleeping).

I’m trying your solution, and the first error I found is in this function:

defp calculate_password([min, max, char_key, list_char]) do                                                    
  count = Enum.count(list_char, &(&1 == char_key))                                                             
                                                                                                                 
  if ((count >= min) && (count <= max)), do: total + 1, else: total
  #                                          ^^^^^            ^^^^^
end

The variable total is not defined. If you want a global variable, well, there’s no variable variables of any kind in Elixir. So, you have to use some kind of reduce.

UPDATE 1

The take_char/1 function doesn’t make sense to me. It returns a list of lists with each sub-list contains only 1 integer (the codepoint of the character). I think the |> Enum.map(&String.to_charlist/1) can be dropped. By the way, you can directly compare to strings/binaries without converting them to charlists.

Thank you for your reply Aetherus:
I realized doing this:

defmodule Day2 do
    @moduledoc """
  Advent of Code `Day2`.
  """

  def run do
    File.read!("lib/input.txt")
    |> String.split("\n", trim: true)
    |> Enum.map(&parse_claim/1)
  end
  @doc """
  Reads the file and execute the logic and returns the result

  ## Examples

    iex> Day2.values
    636
  """
  def part1 do
    run()
    |> Enum.map(fn [min, max, key, password] ->
      check_part1?(String.to_integer(min), String.to_integer(max), key, password)
    end)
    |> Enum.count(&(&1))
  end

  def part2 do
    run()
    |> Enum.map(fn [min, max, key, password] ->
      check_part2?(String.to_integer(min), String.to_integer(max), key, password)
    end)
    |> Enum.count(&(&1))
  end

  @doc """
  Parses a claim.

  ## Examples

    iex> Day2.parse_claim("1-3 a: abcde")
    ["1", "3", "a", "abcde"]

  """
  def parse_claim(list_string) when is_binary(list_string) do
    list_string
    |> String.split(["-", " ", ":", " "], trim: true) #  ["1", "3", "p", "pppp"], [...]
  end

  defp check_part1?(r_min, r_max, key, password) do
    if  String.contains?(password, key) do
      count =
        password
        |> String.graphemes
        |> Enum.filter(fn x -> x == key end)
        |> Enum.count

      #count >= r_min && count <= r_max
      count in r_min..r_max
    end
  end

  defp check_part2?(r_min, r_max, key, password) do
    if  String.contains?(password, key) do
        list =
          password
          |> String.graphemes

        x_min = list |> Enum.at(r_min - 1) == key
        x_max = list |> Enum.at(r_max - 1) == key

        x_min  != x_max
    end
  end
end

1 Like

My approach, as I started learning Erlang… :wink:

-module(day2).
-export([run/0]).

run()->
    ParsedLines = load_file("day2input.txt"),
    {part1(ParsedLines), part2(ParsedLines)}.

part1(ParsedLines)->
    ValidLines = [ Line || Line <- ParsedLines, is_valid1(Line)],
    length(ValidLines).

part2(ParsedLines)->
    ValidLines = [ Line || Line <- ParsedLines, is_valid2(Line)],
    length(ValidLines).

is_valid1({From, To, Char, Pw}) ->
    N = length([[X] || X <- Pw, [X] =:= Char]),
    (N >= From) and (N =< To).

is_valid2({Idx1, Idx2, Char, Pw}) ->
    (string:slice(Pw, Idx1-1, 1) =:= Char) xor (string:slice(Pw, Idx2-1, 1) =:= Char).

load_file(Filename)->
    {ok, Binary} = file:read_file(Filename),
    StringContent = unicode:characters_to_list(Binary),
    [ parse(Line) || Line <- string:tokens(StringContent, "\n")]. 

parse(Line)->
    {ok, MP} = re:compile("([0-9]+)-([0-9]+) ([a-z]): ([a-z]+)"),
    {_,[_,From, To, Char, Pw]} = re:run(Line, MP),
    {extract_num(Line, From), extract_num(Line, To), extract_str(Line, Char), extract_str(Line, Pw)}.

extract_str(String, {Start, End})->
    string:slice(String, Start, End).

extract_num(String, {Start, End})->
    Str = string:slice(String, Start, End),
    {Int, _} = string:to_integer(Str),
    Int.

For part 1 I did a list comprehension on the string to get the number of occurrences.

3 Likes

For the first part I did the following knowing that splitting a string with non-existing character leave the string itself and if it’s present n times it gives n+1 chunks:

defp valid?({range, letter, password}) do
  chunks =
    password
    |> String.split(letter)
    |> Enum.count()
  (chunks - 1) in range
end
1 Like

I used a helper function for Enum.count that used a regex with String.split that returned true or false depending on the occurrence of the substring. Probably not optimal, but I’m still learning:

  def day2 do
    input = File.read!(Path.absname("lib/day_two_input.txt"))
            |> String.split("\n", trim: true)
            |> Enum.count(&(day2_helper(&1) == true)) 
  end
  
  defp day2_helper(s) do
    split = String.split(s, ~r/-|:|\s/, trim: :ok)
    letter = Enum.at(split, 2)
    min = Enum.at(split, 0)
          |> String.to_integer
    max = Enum.at(split, 1)
          |> String.to_integer
    count = Enum.at(split, 3)
            |> String.graphemes
            |> Enum.count(&(&1 == letter))
    if count >= min && count <= max do
      true
    else
      false
    end
  end

A bit late to the party, finally my take!

This seemed to be all about parsing the input properly. I chose to use a list of 4-tuples, and from there, both parts were trivial to solve.

defp parse_line(line) do
  [reqs, pass] = String.split(line, ": ")
  [qty_range, letter] = String.split(reqs, " ")
  [a, b] = String.split(qty_range, "-") |> Enum.map(&String.to_integer/1)

  {pass, a, b, letter}
end

Full solution @ Github

1 Like