Powered by AppSignal & Oban Pro
Would you like to see your link here? Contact us

Exercism Elixir Easy Part 1

easy-part-1.livemd

Exercism Elixir Easy Part 1

Mix.install([
  {:benchee_dsl, "~> 0.5"},
  {:benchee_markdown, "~> 0.3"}
])

require Integer
import ExUnit.Assertions

Resistor Color

https://exercism.org/tracks/elixir/exercises/resistor-color

defmodule ResistorColor do
  @type color ::
          :black | :brown | :red | :orange | :yellow | :green | :blue | :violet | :grey | :white

  @doc """
  Return the value of a color band.

  ## Examples

      iex> ResistorColor.code(:blue)
      6

  """
  @spec code(color()) :: integer()
  def code(:black), do: 0
  def code(:brown), do: 1
  def code(:red), do: 2
  def code(:orange), do: 3
  def code(:yellow), do: 4
  def code(:green), do: 5
  def code(:blue), do: 6
  def code(:violet), do: 7
  def code(:grey), do: 8
  def code(:white), do: 9
end

Resistor Color: Tests

https://github.com/exercism/elixir/blob/main/exercises/practice/resistor-color/test/resistor_color_test.exs

assert ResistorColor.code(:black) == 0
assert ResistorColor.code(:brown) == 1
assert ResistorColor.code(:red) == 2
assert ResistorColor.code(:orange) == 3
assert ResistorColor.code(:yellow) == 4
assert ResistorColor.code(:green) == 5
assert ResistorColor.code(:blue) == 6
assert ResistorColor.code(:violet) == 7
assert ResistorColor.code(:grey) == 8
assert ResistorColor.code(:white) == 9

:passed

Two Fer

https://exercism.org/tracks/elixir/exercises/two-fer

defmodule TwoFer do
  @doc """
  Two-fer or 2-fer is short for two for one. One for you and one for me.

  ## Examples

      iex> TwoFer.two_fer()
      "One for you, one for me."

      iex> TwoFer.two_fer("John")
      "One for John, one for me."

  """
  @spec two_fer(String.t()) :: String.t()
  def two_fer(name \\ "you") when is_binary(name),
    do: "One for #{name}, one for me."
end

Two Fer: Tests

https://github.com/exercism/elixir/blob/main/exercises/practice/two-fer/test/two_fer_test.exs

assert TwoFer.two_fer() == "One for you, one for me."
assert TwoFer.two_fer("Alice") == "One for Alice, one for me."
assert TwoFer.two_fer("Bob") == "One for Bob, one for me."

assert_raise FunctionClauseError, fn ->
  TwoFer.two_fer(10)
end

assert_raise FunctionClauseError, fn ->
  TwoFer.two_fer(:bob)
end

assert_raise FunctionClauseError, fn ->
  refute TwoFer.two_fer('Jon Snow')
end

:passed

Accumulate

https://exercism.org/tracks/elixir/exercises/accumulate

defmodule Accumulate do
  @doc """
  Given a list and a function, apply the function to each list item and
  replace it with the function's return value.

  Returns a list.

  ## Examples

      iex> Accumulate.accumulate([], fn(x) -> x * 2 end)
      []

      iex> Accumulate.accumulate([1, 2, 3], fn(x) -> x * 2 end)
      [2, 4, 6]

  """
  @spec accumulate(list(), (any() -> any())) :: list()
  def accumulate([], _fun), do: []
  def accumulate([item | tail], fun), do: [fun.(item) | accumulate(tail, fun)]
end

Accumulate: Tests

https://github.com/exercism/elixir/blob/main/exercises/practice/accumulate/test/accumulate_test.exs

assert Accumulate.accumulate([], fn n -> n * n end) == []
assert Accumulate.accumulate([1, 2, 3], fn n -> n * n end) == [1, 4, 9]

fun = fn w -> String.upcase(w) end
assert Accumulate.accumulate(["hello", "world"], fun) == ["HELLO", "WORLD"]

fun = fn w -> String.reverse(w) end
words = ~w(the quick brown fox etc)
expected = ["eht", "kciuq", "nworb", "xof", "cte"]
assert Accumulate.accumulate(words, fun) == expected

chars = ~w(a b c)
nums = ~w(1 2 3)
fun = fn c -> Accumulate.accumulate(nums, &amp;(c <> &amp;1)) end
expected = [["a1", "a2", "a3"], ["b1", "b2", "b3"], ["c1", "c2", "c3"]]
assert Accumulate.accumulate(chars, fun) == expected

:passed

Acronym

https://exercism.org/tracks/elixir/exercises/acronym

defmodule Acronym do
  @doc """
  Generate an acronym from a string.

  ## Examples

    iex> Acronym.abbreviate("This is a string")
    "TIAS"

  """
  @spec abbreviate(String.t()) :: String.t()
  def abbreviate(string) do
    String.split(string, ~r/\p{Z}|-|_/, trim: true)
    |> Enum.map_join("", &amp;String.at(&amp;1, 0))
    |> String.upcase()
  end
end

Acronym: Tests

https://github.com/exercism/elixir/blob/main/exercises/practice/acronym/test/acronym_test.exs

assert Acronym.abbreviate("Portable Networks Graphic") === "PNG"
assert Acronym.abbreviate("Ruby on Rails") === "ROR"
assert Acronym.abbreviate("First in, First out") === "FIFO"
assert Acronym.abbreviate("GNU Image Manipulation Program") === "GIMP"
assert Acronym.abbreviate("Complementary Metal-Oxide semiconductor") === "CMOS"

assert Acronym.abbreviate(
         "Rolling On The Floor Laughing So Hard That My Dogs Came Over And Licked Me"
       ) === "ROTFLSHTMDCOALM"

assert Acronym.abbreviate("Something - I made up from thin air") === "SIMUFTA"
assert Acronym.abbreviate("Halley's Comet") === "HC"
assert Acronym.abbreviate("The Road _Not_ Taken") === "TRNT"

:passed

All Your Base (TCO)

https://exercism.org/tracks/elixir/exercises/all-your-base

defmodule AllYourBase.TCO do
  @typep digits :: [non_neg_integer()]

  @doc """
  Given a number in input base, represented as a sequence of digits, converts it to output base,
  or returns an error tuple if either of the bases are less than 2.

  ## Examples

      iex> AllYourBase.TCO.convert([1, 0, 1], 2, 10)
      {:ok, [5]}

      iex> AllYourBase.TCO.convert([15, 15], 16, 2)
      {:ok, [1, 1, 1, 1, 1, 1, 1, 1]}

  """
  @spec convert(digits(), input_base :: pos_integer(), output_base :: pos_integer()) ::
          {:ok, digits()} | {:error, String.t()}
  def convert(digits, input_base, output_base)

  def convert(_, _, output_base) when output_base < 2,
    do: {:error, "output base must be >= 2"}

  def convert(_, input_base, _) when input_base < 2,
    do: {:error, "input base must be >= 2"}

  def convert([], _, _), do: {:ok, [0]}

  def convert(digits, input_base, output_base) do
    case do_convert_from(digits, input_base, 0) do
      {:ok, 0} -> {:ok, [0]}
      {:ok, number} -> do_convert_to(number, output_base, [])
      {:error, _reason} = err -> err
    end
  end

  defp do_convert_from(digits, base, acc)
  defp do_convert_from([], _, acc), do: {:ok, acc}

  defp do_convert_from([digit | _rest], base, _) when digit < 0 or digit >= base,
    do: {:error, "all digits must be >= 0 and < input base"}

  defp do_convert_from([digit | rest], base, acc),
    do: do_convert_from(rest, base, acc * base + digit)

  defp do_convert_to(number, base, acc)
  defp do_convert_to(0, _, acc), do: {:ok, acc}

  defp do_convert_to(number, base, acc),
    do: do_convert_to(div(number, base), base, [rem(number, base) | acc])
end

All Your Base (Enum.all?/2, Enum.zip/2)

defmodule AllYourBase.EnumFuncs do
  @typep digits :: [non_neg_integer()]

  @doc """
  Given a number in input base, represented as a sequence of digits, converts it to output base,
  or returns an error tuple if either of the bases are less than 2.

  ## Examples

      iex> AllYourBase.EnumFuncs.convert([1, 0, 1], 2, 10)
      {:ok, [5]}

      iex> AllYourBase.EnumFuncs.convert([15, 15], 16, 2)
      {:ok, [1, 1, 1, 1, 1, 1, 1, 1]}

  """
  @spec convert(digits(), input_base :: pos_integer(), output_base :: pos_integer()) ::
          {:ok, digits()} | {:error, String.t()}
  def convert(digits, input_base, output_base)

  def convert(_digits, _input_base, output_base) when output_base < 2,
    do: {:error, "output base must be >= 2"}

  def convert(_digits, input_base, _output_base) when input_base < 2,
    do: {:error, "input base must be >= 2"}

  def convert([], _input_base, _output_base), do: {:ok, [0]}

  def convert(digits, input_base, output_base) when input_base >= 2 and output_base >= 2 do
    cond do
      all_zeros?(digits) ->
        {:ok, [0]}

      not all_valid?(digits, input_base) ->
        {:error, "all digits must be >= 0 and < input base"}

      true ->
        number =
          digits
          |> convert_decimal_from_base(input_base)
          |> do_convert_to_base(output_base, [])

        {:ok, number}
    end
  end

  defp all_zeros?(digits), do: Enum.all?(digits, &amp;(&amp;1 == 0))

  defp all_valid?(digits, input_base), do: Enum.all?(digits, &amp;(&amp;1 >= 0 and &amp;1 < input_base))

  defp convert_decimal_from_base(digits, base) do
    exponent = Enum.count(digits)

    for {digit, position} <- Enum.zip(digits, (exponent - 1)..0), reduce: 0 do
      acc -> acc + digit * Integer.pow(base, position)
    end
  end

  defp do_convert_to_base(number, _base, acc) when number <= 0, do: acc

  defp do_convert_to_base(number, base, acc) do
    acc = [rem(number, base) | acc]
    do_convert_to_base(div(number, base), base, acc)
  end
end

All Your Base: Tests

https://github.com/exercism/elixir/blob/main/exercises/practice/all-your-base/test/all_your_base_test.exs

for module <- [AllYourBase.TCO, AllYourBase.EnumFuncs] do
  assert module.convert([1], 2, 10) == {:ok, [1]}
  assert module.convert([1, 0, 1], 2, 10) == {:ok, [5]}
  assert module.convert([5], 10, 2) == {:ok, [1, 0, 1]}
  assert module.convert([1, 0, 1, 0, 1, 0], 2, 10) == {:ok, [4, 2]}
  assert module.convert([4, 2], 10, 2) == {:ok, [1, 0, 1, 0, 1, 0]}
  assert module.convert([1, 1, 2, 0], 3, 16) == {:ok, [2, 10]}
  assert module.convert([2, 10], 16, 3) == {:ok, [1, 1, 2, 0]}
  assert module.convert([3, 46, 60], 97, 73) == {:ok, [6, 10, 45]}
  assert module.convert([], 2, 10) == {:ok, [0]}
  assert module.convert([0], 10, 2) == {:ok, [0]}
  assert module.convert([0, 0, 0], 10, 2) == {:ok, [0]}
  assert module.convert([0, 6, 0], 7, 10) == {:ok, [4, 2]}
  assert module.convert([0], 1, 10) == {:error, "input base must be >= 2"}
  assert module.convert([], 0, 10) == {:error, "input base must be >= 2"}
  assert module.convert([1], -2, 10) == {:error, "input base must be >= 2"}

  assert module.convert([1, -1, 1, 0, 1, 0], 2, 10) ==
           {:error, "all digits must be >= 0 and < input base"}

  assert module.convert([1, 2, 1, 0, 1, 0], 2, 10) ==
           {:error, "all digits must be >= 0 and < input base"}

  assert module.convert([1, 0, 1, 0, 1, 0], 2, 1) == {:error, "output base must be >= 2"}
  assert module.convert([7], 10, 0) == {:error, "output base must be >= 2"}
  assert module.convert([1], 2, -7) == {:error, "output base must be >= 2"}
  assert module.convert([1], -2, -7) == {:error, "output base must be >= 2"}
end

:passed

All Your Base: Benchmark

{:module, name, _binary, _bindings} =
  defmodule AllYourBase.Benchmark do
    use BencheeDsl.Benchmark

    config(
      warmup: 1,
      time: 3,
      memory_time: 1,
      reduction_time: 1,
      pre_check: true,
      print: [configuration: false]
    )

    inputs(%{"Small" => [15, 15], "Bigger" => [15, 15, 15, 15, 15, 15, 15, 15]})

    job(tco(input)) do
      AllYourBase.TCO.convert(input, 16, 2)
    end

    job(enum_funcs(input)) do
      AllYourBase.EnumFuncs.convert(input, 16, 2)
    end
  end

BencheeDsl.Livebook.benchee_config() |> name.run() |> BencheeDsl.Livebook.render()

Anagram

https://exercism.org/tracks/elixir/exercises/anagram

defmodule Anagram do
  @doc """
  Returns all candidates that are anagrams of, but not equal to, 'base'.

  ## Examples

      iex> Anagram.match("solemn", ~w(lemons cherry melons))
      ~w(lemons melons)

  """
  @spec match(String.t(), [String.t()]) :: [String.t()]
  def match(base, candidates) do
    for candidate <- candidates,
        anagram?(String.downcase(base), String.downcase(candidate)),
        into: [],
        do: candidate
  end

  defp anagram?(word, word), do: false
  defp anagram?(word_a, word_b), do: sorted(word_a) == sorted(word_b)

  defp sorted(word), do: to_charlist(word) |> Enum.sort()
end

Anagram: Tests

https://github.com/exercism/elixir/blob/main/exercises/practice/anagram/test/anagram_test.exs

assert Anagram.match("diaper", ~w(hello world zombies pants)) == []

assert Anagram.match(
         "solemn",
         ~w(lemons cherry melons)
       ) == ~w(lemons melons)

assert Anagram.match("good", ~w(dog goody)) == []

assert Anagram.match(
         "listen",
         ~w(enlists google inlets banana)
       ) == ~w(inlets)

assert Anagram.match(
         "allergy",
         ~w(gallery ballerina regally clergy largely leading)
       ) ==
         ~w(gallery regally largely)

assert Anagram.match("nose", ~w(Eons ONES)) == ~w(Eons ONES)
assert Anagram.match("mass", ~w(last)) == []

assert Anagram.match(
         "orchestra",
         ~w(cashregister Carthorse radishes)
       ) ==
         ~w(Carthorse)

assert Anagram.match(
         "Orchestra",
         ~w(cashregister carthorse radishes)
       ) == ~w(carthorse)

assert Anagram.match("orchestra", ~w(cashregister Carthorse radishes)) ==
         ~w(Carthorse)

assert Anagram.match("go", ~w(go Go GO)) == []
assert Anagram.match("tapper", ~w(patter)) == []
assert Anagram.match("BANANA", ~w(BANANA)) == []
assert Anagram.match("BANANA", ~w(Banana)) == []
assert Anagram.match("BANANA", ~w(banana)) == []
assert Anagram.match("LISTEN", ~w(Silent LISTEN)) == ~w(Silent)

:passed

Armstrong Numbers (List Comprehension)

https://exercism.org/tracks/elixir/exercises/armstrong-numbers

defmodule ArmstrongNumber.ListComprehension do
  @doc """
  Checks if a given number is an Armstrong number.

  ## Examples

      iex> ArmstrongNumber.ListComprehension.valid?(9474)
      true

      iex> ArmstrongNumber.ListComprehension.valid?(9475)
      false

  """
  @spec valid?(integer()) :: boolean()
  def valid?(0), do: true

  def valid?(number) do
    digits = Integer.digits(number)

    exponent = Enum.count(digits)

    number ==
      for digit <- digits, reduce: 0 do
        acc -> acc + Integer.pow(digit, exponent)
      end
  end
end

Armstrong Numbers (Optimized integer exponentiation)

defmodule ArmstrongNumber.ListComprehensionOIE do
  import Bitwise

  @doc """
  Checks if a given number is an Armstrong number.

  ## Examples

      iex> ArmstrongNumber.ListComprehensionOIE.valid?(9474)
      true

      iex> ArmstrongNumber.ListComprehensionOIE.valid?(9475)
      false

  """
  @spec valid?(integer()) :: boolean()
  def valid?(0), do: true

  def valid?(number) do
    digits = Integer.digits(number)
    exponent = Enum.count(digits)

    number ==
      for digit <- digits, reduce: 0 do
        acc -> acc + pow(digit, exponent)
      end
  end

  # Optimized integer exponentiation
  # https://en.wikipedia.org/wiki/Exponentiation_by_squaring
  defp pow(base, exponent), do: pow(base, exponent, 1)
  defp pow(_base, 0, result), do: result
  defp pow(base, 1, result), do: result * base
  defp pow(2, exponent, result), do: result <<< exponent

  defp pow(base, exponent, result) when rem(exponent, 2) == 0,
    do: pow(base * base, exponent >>> 1, result)

  defp pow(base, exponent, result),
    do: pow(base * base, exponent >>> 1, result * base)
end

Armstrong Numbers (Enum.reduce/3)

defmodule ArmstrongNumber.EnumReduce do
  @doc """
  Checks if a given number is an Armstrong number.

  ## Examples

      iex> ArmstrongNumber.EnumReduce.valid?(9474)
      true

      iex> ArmstrongNumber.EnumReduce.valid?(9475)
      false

  """
  @spec valid?(integer()) :: boolean()
  def valid?(0), do: true

  def valid?(number) do
    digits = Integer.digits(number)
    exponent = Enum.count(digits)

    number ==
      Enum.reduce(digits, 0, fn digit, acc ->
        acc + Integer.pow(digit, exponent)
      end)
  end
end

Armstrong Numbers (Enum.reduce/3, Optimized integer exponentiation)

defmodule ArmstrongNumber.EnumReduceOIE do
  import Bitwise

  @doc """
  Checks if a given number is an Armstrong number.

  ## Examples

      iex> ArmstrongNumber.EnumReduceOIE.valid?(9474)
      true

      iex> ArmstrongNumber.EnumReduceOIE.valid?(9475)
      false

  """
  @spec valid?(integer()) :: boolean()
  def valid?(0), do: true

  def valid?(number) do
    digits = Integer.digits(number)
    exponent = Enum.count(digits)

    number ==
      Enum.reduce(digits, 0, fn digit, acc ->
        acc + pow(digit, exponent)
      end)
  end

  # Optimized integer exponentiation
  # https://en.wikipedia.org/wiki/Exponentiation_by_squaring
  defp pow(base, exponent), do: pow(base, exponent, 1)
  defp pow(_base, 0, result), do: result
  defp pow(base, 1, result), do: result * base
  defp pow(2, exponent, result), do: result <<< exponent

  defp pow(base, exponent, result) when rem(exponent, 2) == 0,
    do: pow(base * base, exponent >>> 1, result)

  defp pow(base, exponent, result),
    do: pow(base * base, exponent >>> 1, result * base)
end

Armstrong Numbers (Naive, Enum.reduce/3)

defmodule ArmstrongNumber.NaiveEnumReduce do
  @doc """
  Checks if a given number is an Armstrong number.

  ## Examples

      iex> ArmstrongNumber.NaiveEnumReduce.valid?(9474)
      true

      iex> ArmstrongNumber.NaiveEnumReduce.valid?(9475)
      false

  """
  @spec valid?(integer()) :: boolean()
  def valid?(0), do: true

  def valid?(number) do
    digits =
      to_string(number)
      |> String.split("", trim: true)
      |> Enum.map(&amp;String.to_integer/1)

    number ==
      Enum.reduce(digits, 0, fn digit, acc ->
        acc + Integer.pow(digit, length(digits))
      end)
  end
end

Armstrong Numbers (Naive, Enum.reduce/3, Optimized integer exponentiation)

defmodule ArmstrongNumber.NaiveEnumReduceOIE do
  import Bitwise

  @doc """
  Checks if a given number is an Armstrong number.

  ## Examples

      iex> ArmstrongNumber.NaiveEnumReduceOIE.valid?(9474)
      true

      iex> ArmstrongNumber.NaiveEnumReduceOIE.valid?(9475)
      false

  """
  @spec valid?(integer()) :: boolean()
  def valid?(0), do: true

  def valid?(number) do
    digits =
      to_string(number)
      |> String.split("", trim: true)
      |> Enum.map(&amp;String.to_integer/1)

    number ==
      Enum.reduce(digits, 0, fn digit, acc ->
        acc + pow(digit, length(digits))
      end)
  end

  # Optimized integer exponentiation
  # https://en.wikipedia.org/wiki/Exponentiation_by_squaring
  defp pow(base, exponent), do: pow(base, exponent, 1)
  defp pow(_base, 0, result), do: result
  defp pow(base, 1, result), do: result * base
  defp pow(2, exponent, result), do: result <<< exponent

  defp pow(base, exponent, result) when rem(exponent, 2) == 0,
    do: pow(base * base, exponent >>> 1, result)

  defp pow(base, exponent, result),
    do: pow(base * base, exponent >>> 1, result * base)
end

Armstrong Numbers: Tests

https://github.com/exercism/elixir/blob/main/exercises/practice/armstrong-numbers/test/armstrong_number_test.exs

modules = [
  ArmstrongNumber.ListComprehension,
  ArmstrongNumber.ListComprehensionOIE,
  ArmstrongNumber.EnumReduce,
  ArmstrongNumber.EnumReduceOIE,
  ArmstrongNumber.NaiveEnumReduce,
  ArmstrongNumber.NaiveEnumReduceOIE
]

for module <- modules do
  assert module.valid?(0)
  assert module.valid?(5)
  refute module.valid?(10)
  assert module.valid?(153)
  refute module.valid?(100)
  assert module.valid?(9474)
  refute module.valid?(9475)
  assert module.valid?(9_926_315)
  refute module.valid?(9_926_134)
end

:passed

Armstrong Numbers: Benchmark

{:module, name, _binary, _bindings} =
  defmodule ArmstrongNumber.Benchmark do
    use BencheeDsl.Benchmark

    config(
      warmup: 1,
      time: 3,
      memory_time: 1,
      reduction_time: 1,
      pre_check: true,
      print: [configuration: false]
    )

    inputs(%{"Small" => 9474, "Bigger" => 947_494_749_474})

    job(list_comphrehension(input)) do
      ArmstrongNumber.ListComprehension.valid?(input)
    end

    job(list_comphrehension_oie(input)) do
      ArmstrongNumber.ListComprehensionOIE.valid?(input)
    end

    job(enum_reduce(input)) do
      ArmstrongNumber.EnumReduce.valid?(input)
    end

    job(enum_reduce_oie(input)) do
      ArmstrongNumber.EnumReduceOIE.valid?(input)
    end

    job(naive_enum_reduce(input)) do
      ArmstrongNumber.NaiveEnumReduce.valid?(input)
    end

    job(naive_enum_reduce_oie(input)) do
      ArmstrongNumber.NaiveEnumReduceOIE.valid?(input)
    end
  end

BencheeDsl.Livebook.benchee_config() |> name.run() |> BencheeDsl.Livebook.render()

Collatz Conjecture (Stream.iterate/2, Stream.find_index/2)

https://exercism.org/tracks/elixir/exercises/collatz-conjecture

defmodule CollatzConjecture.StreamIterateFindIndex do
  import Integer, only: [is_even: 1]

  @doc """
  calc/1 takes an integer and returns the number of steps required to get the
  number to 1 when following the rules:
  - if number is odd, multiply with 3 and add 1
  - if number is even, divide by 2

  ## Examples

      iex> CollatzConjecture.StreamIterateFindIndex.calc(16)
      4

      iex> CollatzConjecture.StreamIterateFindIndex.calc(21)
      7

  """
  @spec calc(input :: pos_integer()) :: non_neg_integer()
  def calc(input) when is_integer(input) and input > 0 do
    input
    |> Stream.iterate(fn
      i when is_even(i) -> div(i, 2)
      i -> i * 3 + 1
    end)
    |> Enum.find_index(&amp;(&amp;1 == 1))
  end
end

Collatz Conjecture (Stream.unfold/2)

defmodule CollatzConjecture.StreamUnfold do
  import Integer, only: [is_even: 1]

  @doc """
  calc/1 takes an integer and returns the number of steps required to get the
  number to 1 when following the rules:
  - if number is odd, multiply with 3 and add 1
  - if number is even, divide by 2

  ## Examples

      iex> CollatzConjecture.StreamUnfold.calc(16)
      4

      iex> CollatzConjecture.StreamUnfold.calc(21)
      7

  """
  @spec calc(input :: pos_integer()) :: non_neg_integer()
  def calc(input) when is_integer(input) and input > 0 do
    input
    |> Stream.unfold(fn
      1 -> nil
      i when is_even(i) -> {i, div(i, 2)}
      i -> {i, i * 3 + 1}
    end)
    |> Enum.count()
  end
end

Collatz Conjecture (Stream.iterate/2, Stream.take_while/2)

defmodule CollatzConjecture.StreamIterateTakeWhile do
  import Integer, only: [is_even: 1]

  @doc """
  calc/1 takes an integer and returns the number of steps required to get the
  number to 1 when following the rules:
  - if number is odd, multiply with 3 and add 1
  - if number is even, divide by 2

  ## Examples

      iex> CollatzConjecture.StreamIterateTakeWhile.calc(16)
      4

      iex> CollatzConjecture.StreamIterateTakeWhile.calc(21)
      7

  """
  @spec calc(input :: pos_integer()) :: non_neg_integer()
  def calc(input) when is_integer(input) and input > 0 do
    input
    |> Stream.iterate(fn
      i when is_even(i) -> div(i, 2)
      i -> i * 3 + 1
    end)
    |> Stream.take_while(&amp;(&amp;1 != 1))
    |> Enum.count()
  end
end

Collatz Conjecture (Recursion)

defmodule CollatzConjecture.Recursion do
  import Integer, only: [is_even: 1, is_odd: 1]

  @doc """
  calc/1 takes an integer and returns the number of steps required to get the
  number to 1 when following the rules:
  - if number is odd, multiply with 3 and add 1
  - if number is even, divide by 2

  ## Examples

      iex> CollatzConjecture.Recursion.calc(16)
      4

      iex> CollatzConjecture.Recursion.calc(21)
      7

  """
  @spec calc(input :: pos_integer()) :: non_neg_integer()
  def calc(1), do: 0
  def calc(input) when input > 1 and is_even(input), do: calc(div(input, 2)) + 1
  def calc(input) when input > 1 and is_odd(input), do: calc(input * 3 + 1) + 1
end

Collatz Conjecture (TCO)

defmodule CollatzConjecture.TCO do
  import Integer, only: [is_even: 1]

  @doc """
  calc/1 takes an integer and returns the number of steps required to get the
  number to 1 when following the rules:
  - if number is odd, multiply with 3 and add 1
  - if number is even, divide by 2

  ## Examples

      iex> CollatzConjecture.TCO.calc(16)
      4

      iex> CollatzConjecture.TCO.calc(21)
      7

  """
  @spec calc(input :: pos_integer()) :: non_neg_integer()
  def calc(input) when is_integer(input) and input > 0, do: do_calc(input, 0)
  defp do_calc(1, count), do: count
  defp do_calc(number, count) when is_even(number), do: do_calc(div(number, 2), count + 1)
  defp do_calc(number, count), do: do_calc(number * 3 + 1, count + 1)
end

Collatz Conjecture (TCO, Case)

defmodule CollatzConjecture.TCOCase do
  @doc """
  calc/1 takes an integer and returns the number of steps required to get the
  number to 1 when following the rules:
  - if number is odd, multiply with 3 and add 1
  - if number is even, divide by 2

  ## Examples

      iex> CollatzConjecture.TCOCase.calc(16)
      4

      iex> CollatzConjecture.TCOCase.calc(21)
      7

  """
  @spec calc(input :: pos_integer()) :: non_neg_integer()
  def calc(input) when is_integer(input) and input > 0, do: do_calc(input, 0)
  defp do_calc(1, count), do: count

  defp do_calc(number, count) do
    number =
      case rem(number, 2) do
        0 -> div(number, 2)
        _ -> number * 3 + 1
      end

    do_calc(number, count + 1)
  end
end

Collatz Conjecture: Tests

https://github.com/exercism/elixir/blob/main/exercises/practice/collatz-conjecture/test/collatz_conjecture_test.exs

modules = [
  CollatzConjecture.StreamIterateFindIndex,
  CollatzConjecture.StreamUnfold,
  CollatzConjecture.StreamIterateTakeWhile,
  CollatzConjecture.Recursion,
  CollatzConjecture.TCO,
  CollatzConjecture.TCOCase
]

for module <- modules do
  assert module.calc(1) == 0
  assert module.calc(16) == 4
  assert module.calc(12) == 9
  assert module.calc(1_000_000) == 152
  assert module.calc(21) == 7
  assert module.calc(7) == 16

  assert_raise FunctionClauseError,
               fn -> module.calc(0) end

  assert_raise FunctionClauseError,
               fn -> module.calc(-15) end

  assert_raise FunctionClauseError,
               fn -> module.calc("fubar") end
end

:passed

Collatz Conjecture: Benchmark

{:module, name, _binary, _bindings} =
  defmodule CollatzConjecture.Benchmark do
    use BencheeDsl.Benchmark

    config(
      warmup: 1,
      time: 3,
      memory_time: 1,
      reduction_time: 1,
      pre_check: true,
      print: [configuration: false]
    )

    inputs(%{"Small" => 16, "Bigger" => 16_161_616_161_616})

    job(stream_iterate_find_index(input)) do
      CollatzConjecture.StreamIterateFindIndex.calc(input)
    end

    job(stream_unfold(input)) do
      CollatzConjecture.StreamUnfold.calc(input)
    end

    job(stream_iterate_take_while(input)) do
      CollatzConjecture.StreamIterateTakeWhile.calc(input)
    end

    job(recursion(input)) do
      CollatzConjecture.Recursion.calc(input)
    end

    job(tco(input)) do
      CollatzConjecture.TCO.calc(input)
    end

    job(tco_case(input)) do
      CollatzConjecture.TCOCase.calc(input)
    end
  end

BencheeDsl.Livebook.benchee_config() |> name.run() |> BencheeDsl.Livebook.render()

Darts

https://exercism.org/tracks/elixir/exercises/darts

ETL

https://exercism.org/tracks/elixir/exercises/etl

defmodule ETL do
  @moduledoc """
  This module defines `Transform` step of Extract-Transform-Load which allows to
  extract some Scrabble scores from legacy system and transform them to format
  which is understood by new Scrabble system.
  """

  @doc """
  Transforms an old Scrabble score system to a new one.

  ## Examples

    iex> ETL.transform(%{1 => ["A", "E"], 2 => ["D", "G"]})
    %{"a" => 1, "d" => 2, "e" => 1, "g" => 2}

  """
  @spec transform(map()) :: map()
  def transform(input) do
    for {points, letters} <- input, letter <- letters, into: %{} do
      {String.downcase(letter), points}
    end
  end
end

ETL: Tests

https://github.com/exercism/elixir/blob/main/exercises/practice/etl/test/etl_test.exs

old = %{1 => ["A"]}
expected = %{"a" => 1}

assert ETL.transform(old) == expected

old = %{1 => ~W(A E I O U)}
expected = %{"a" => 1, "e" => 1, "i" => 1, "o" => 1, "u" => 1}

assert ETL.transform(old) == expected

old = %{1 => ["A", "E"], 2 => ["D", "G"]}

expected = %{
  "a" => 1,
  "d" => 2,
  "e" => 1,
  "g" => 2
}

assert ETL.transform(old) == expected

old = %{
  1 => ~W(A E I O U L N R S T),
  2 => ~W(D G),
  3 => ~W(B C M P),
  4 => ~W(F H V W Y),
  5 => ~W(K),
  8 => ~W(J X),
  10 => ~W(Q Z)
}

expected = %{
  "a" => 1,
  "b" => 3,
  "c" => 3,
  "d" => 2,
  "e" => 1,
  "f" => 4,
  "g" => 2,
  "h" => 4,
  "i" => 1,
  "j" => 8,
  "k" => 5,
  "l" => 1,
  "m" => 3,
  "n" => 1,
  "o" => 1,
  "p" => 3,
  "q" => 10,
  "r" => 1,
  "s" => 1,
  "t" => 1,
  "u" => 1,
  "v" => 4,
  "w" => 4,
  "x" => 8,
  "y" => 4,
  "z" => 10
}

assert ETL.transform(old) == expected

:passed

Hamming (TCO)

https://exercism.org/tracks/elixir/exercises/hamming

defmodule Hamming.TCO do
  @doc """
  Returns number of differences between two strands of DNA, known as the Hamming Distance.

  ## Examples

    iex> Hamming.TCO.hamming_distance('AAGTCATA', 'TAGCGATC')
    {:ok, 4}

  """
  @spec hamming_distance([char()], [char()]) :: {:ok, non_neg_integer()} | {:error, String.t()}
  def hamming_distance(strand1, strand2) when length(strand1) == length(strand2),
    do: {:ok, do_hamming_distance(strand1, strand2, 0)}

  def hamming_distance(_starnd1, _strand2), do: {:error, "strands must be of equal length"}

  defp do_hamming_distance([], [], distance), do: distance

  defp do_hamming_distance([gene1 | tail1], [gene2 | tail2], distance) do
    distance = if gene1 == gene2, do: distance, else: distance + 1

    do_hamming_distance(tail1, tail2, distance)
  end
end

Hamming (Stream.zip/2)

defmodule Hamming.StreamZip do
  @doc """
  Returns number of differences between two strands of DNA, known as the Hamming Distance.

  ## Examples

    iex> Hamming.StreamZip.hamming_distance('AAGTCATA', 'TAGCGATC')
    {:ok, 4}

  """
  @spec hamming_distance([char()], [char()]) :: {:ok, non_neg_integer()} | {:error, String.t()}
  def hamming_distance(strand1, strand2) when length(strand1) == length(strand2) do
    {:ok, Stream.zip(strand1, strand2) |> Enum.count(fn {gene1, gene2} -> gene1 != gene2 end)}
  end

  def hamming_distance(_starnd1, _strand2), do: {:error, "strands must be of equal length"}
end

Hamming: Tests

https://github.com/exercism/elixir/blob/main/exercises/practice/hamming/test/hamming_test.exs

for module <- [Hamming.TCO, Hamming.StreamZip] do
  assert module.hamming_distance('', '') == {:ok, 0}
  assert module.hamming_distance('A', 'A') == {:ok, 0}
  assert module.hamming_distance('G', 'T') == {:ok, 1}
  assert module.hamming_distance('GGACTGAAATCTG', 'GGACTGAAATCTG') == {:ok, 0}
  assert module.hamming_distance('GGACGGATTCTG', 'AGGACGGATTCT') == {:ok, 9}
  assert {:error, "strands must be of equal length"} = module.hamming_distance('AATG', 'AAA')
  assert {:error, "strands must be of equal length"} = module.hamming_distance('ATA', 'AGTG')
  assert {:error, "strands must be of equal length"} = module.hamming_distance('', 'G')
  assert {:error, "strands must be of equal length"} = module.hamming_distance('G', '')
end

:passed

Hamming: Benchmark

{:module, name, _binary, _bindings} =
  defmodule Benchmark do
    use BencheeDsl.Benchmark

    config(
      warmup: 1,
      time: 3,
      memory_time: 1,
      reduction_time: 1,
      pre_check: true,
      print: [configuration: false]
    )

    inputs(%{
      "Small" => {'AAGTCATA', 'TAGCGATC'},
      "Bigger" =>
        {'AAGTCATAAAGTCATAAAGTCATAAAGTCATAAAGTCATAAAGTCATA',
         'TAGCGATCTAGCGATCTAGCGATCTAGCGATCTAGCGATCTAGCGATC'}
    })

    job(tco({a, b})) do
      Hamming.TCO.hamming_distance(a, b)
    end

    job(stream_zip({a, b})) do
      Hamming.StreamZip.hamming_distance(a, b)
    end
  end

BencheeDsl.Livebook.benchee_config() |> name.run() |> BencheeDsl.Livebook.render()

Nth Prime

https://exercism.org/tracks/elixir/exercises/nth-prime

defmodule Prime do
  @doc """
  Generates the nth prime number.

  ## Examples

      iex> Prime.nth(6)
      13

  """
  @spec nth(non_neg_integer) :: non_neg_integer
  def nth(count) when is_integer(count) and count > 0, do: do_nth(count, 2, [])

  defp do_nth(0, _, primes), do: hd(primes)

  defp do_nth(count, value, primes) do
    case prime?(value, primes) do
      true -> do_nth(count - 1, value + 1, [value | primes])
      false -> do_nth(count, value + 1, primes)
    end
  end

  defp prime?(number, primes), do: Enum.all?(primes, &amp;(rem(number, &amp;1) != 0))
end

Nth Prime: Tests

https://github.com/exercism/elixir/blob/main/exercises/practice/nth-prime/test/prime_test.exs

assert Prime.nth(1) == 2
assert Prime.nth(2) == 3
assert Prime.nth(6) == 13
assert Prime.nth(100) == 541
assert Prime.nth(10001) == 104_743
catch_error(Prime.nth(0))

:passed

Nucleotide Count

https://exercism.org/tracks/elixir/exercises/nucleotide-count

defmodule NucleotideCount do
  @doc """
  Counts individual nucleotides in a DNA strand.

  ## Examples

    iex> NucleotideCount.count('AATAA', ?A)
    4

    iex> NucleotideCount.count('AATAA', ?T)
    1

  """
  @spec count(charlist(), char()) :: non_neg_integer()
  def count(strand, nucleotide),
    do: Enum.count(strand, &amp;(&amp;1 == nucleotide))

  @histogram %{?A => 0, ?T => 0, ?C => 0, ?G => 0}

  @doc """
  Returns a summary of counts by nucleotide.

  ## Examples

    iex> NucleotideCount.histogram('AATAA')
    %{?A => 4, ?T => 1, ?C => 0, ?G => 0}

  """
  @spec histogram(charlist()) :: map()
  def histogram(strand) do
    for nucleotide <- strand, reduce: @histogram do
      acc -> Map.update(acc, nucleotide, 1, &amp;(&amp;1 + 1))
    end
  end
end

Nucleotide Count: Tests

https://github.com/exercism/elixir/blob/main/exercises/practice/nucleotide-count/test/nucleotide_count_test.exs

assert NucleotideCount.count('', ?A) == 0
assert NucleotideCount.count('G', ?G) == 1
assert NucleotideCount.count('CCCCC', ?C) == 5
assert NucleotideCount.count('GGGGGTAACCCGG', ?T) == 1

expected = %{?A => 0, ?T => 0, ?C => 0, ?G => 0}
assert NucleotideCount.histogram('') == expected

expected = %{?A => 0, ?T => 0, ?C => 0, ?G => 1}
assert NucleotideCount.histogram('G') == expected

expected = %{?A => 0, ?T => 0, ?C => 0, ?G => 8}
assert NucleotideCount.histogram('GGGGGGGG') == expected

s = 'AGCTTTTCATTCTGACTGCAACGGGCAATATGTCTCTGTGTGGATTAAAAAAAGAGTGTCTGATAGCAGC'
expected = %{?A => 20, ?T => 21, ?C => 12, ?G => 17}
assert NucleotideCount.histogram(s) == expected

:passed

Pangram (List Comprehension Uniq)

https://exercism.org/tracks/elixir/exercises/pangram

defmodule Pangram.ListComprehensionUniq do
  @ascii_alphabet Enum.to_list(?a..?z)
  @ascii_alphabet_size length(@ascii_alphabet)

  @doc """
  Determines if a word or sentence is a pangram.
  A pangram is a sentence using every letter of the alphabet at least once.

  Returns a boolean.

  ## Examples

    iex> Pangram.ListComprehensionUniq.pangram?("the quick brown fox jumps over the lazy dog")
    true

    iex> Pangram.ListComprehensionUniq.pangram?("7h3 qu1ck brown fox jumps ov3r 7h3 lazy dog")
    false

  """
  @spec pangram?(String.t()) :: boolean()
  def pangram?(sentence) do
    chars =
      sentence
      |> String.downcase()
      |> to_charlist()

    chars = for char <- chars, char in @ascii_alphabet, uniq: true, into: [], do: char

    length(chars) == @ascii_alphabet_size
  end
end

Pangram (TCO)

defmodule Pangram.TCO do
  @ascii_alphabet Enum.to_list(?a..?z)

  @doc """
  Determines if a word or sentence is a pangram.
  A pangram is a sentence using every letter of the alphabet at least once.

  Returns a boolean.

  ## Examples

    iex> Pangram.TCO.pangram?("the quick brown fox jumps over the lazy dog")
    true

    iex> Pangram.TCO.pangram?("7h3 qu1ck brown fox jumps ov3r 7h3 lazy dog")
    false

  """
  @spec pangram?(String.t()) :: boolean()
  def pangram?(sentence) do
    chars =
      sentence
      |> String.downcase()
      |> to_charlist()

    do_pangram(chars, @ascii_alphabet)
  end

  defp do_pangram(_chars, []), do: true
  defp do_pangram([], _letters), do: false
  defp do_pangram([char | tail], letters), do: do_pangram(tail, List.delete(letters, char))
end

Pangram (Enum.all?/2)

defmodule Pangram.EnumAll do
  @ascii_alphabet ?a..?z

  @doc """
  Determines if a word or sentence is a pangram.
  A pangram is a sentence using every letter of the alphabet at least once.

  Returns a boolean.

  ## Examples

    iex> Pangram.EnumAll.pangram?("the quick brown fox jumps over the lazy dog")
    true

    iex> Pangram.EnumAll.pangram?("7h3 qu1ck brown fox jumps ov3r 7h3 lazy dog")
    false

  """
  @spec pangram?(String.t()) :: boolean()
  def pangram?(sentence) do
    chars =
      sentence
      |> String.downcase()
      |> to_charlist()

    Enum.all?(@ascii_alphabet, &amp;(&amp;1 in chars))
  end
end

Pangram (List Substraction)

defmodule Pangram.ListSubstraction do
  @ascii_alphabet ?a..?z

  @doc """
  Determines if a word or sentence is a pangram.
  A pangram is a sentence using every letter of the alphabet at least once.

  Returns a boolean.

  ## Examples

    iex> Pangram.ListSubstraction.pangram?("the quick brown fox jumps over the lazy dog")
    true

    iex> Pangram.ListSubstraction.pangram?("7h3 qu1ck brown fox jumps ov3r 7h3 lazy dog")
    false

  """
  @spec pangram?(String.t()) :: boolean()
  def pangram?(sentence) do
    Enum.empty?(Enum.to_list(@ascii_alphabet) -- to_charlist(String.downcase(sentence)))
  end
end

Pangram (List Comprehension Reduce)

defmodule Pangram.ListComprehensionReduce do
  @doc """
  Determines if a word or sentence is a pangram.
  A pangram is a sentence using every letter of the alphabet at least once.

  Returns a boolean.

  ## Examples

    iex> Pangram.ListComprehensionReduce.pangram?("the quick brown fox jumps over the lazy dog")
    true

    iex> Pangram.ListComprehensionReduce.pangram?("7h3 qu1ck brown fox jumps ov3r 7h3 lazy dog")
    false

  """
  @spec pangram?(String.t()) :: boolean()
  def pangram?(sentence) do
    sentence = String.downcase(sentence) |> to_charlist()

    for char <- sentence, char in ?a..?z, reduce: %{} do
      acc -> Map.put(acc, char, true)
    end
    |> Map.keys()
    |> Enum.count() == 26
  end
end

Pangram: Tests

https://github.com/exercism/elixir/blob/main/exercises/practice/pangram/test/pangram_test.exs

modules = [
  Pangram.ListComprehensionUniq,
  Pangram.TCO,
  Pangram.EnumAll,
  Pangram.ListSubstraction,
  Pangram.ListComprehensionReduce
]

for module <- modules do
  refute module.pangram?("")
  assert module.pangram?("abcdefghijklmnopqrstuvwxyz")
  assert module.pangram?("the quick brown fox jumps over the lazy dog")
  refute module.pangram?("a quick movement of the enemy will jeopardize five gunboats")
  refute module.pangram?("the quick brown fish jumps over the lazy dog")
  refute module.pangram?("five boxing wizards jump quickly at it")
  assert module.pangram?("the_quick_brown_fox_jumps_over_the_lazy_dog")
  assert module.pangram?("the 1 quick brown fox jumps over the 2 lazy dogs")
  refute module.pangram?("7h3 qu1ck brown fox jumps ov3r 7h3 lazy dog")
  assert module.pangram?("\"Five quacking Zephyrs jolt my wax bed.\"")
  assert module.pangram?("the quick brown fox jumps over the lazy DOG")
  refute module.pangram?("abcdefghijklm ABCDEFGHIJKLM")
  assert module.pangram?("Victor jagt zwölf Boxkämpfer quer über den großen Sylter Deich.")

  refute module.pangram?(
           "Широкая электрификация южных губерний даст мощный толчок подъёму сельского хозяйства."
         )
end

:passed

Pangram: Benchmark

{:module, name, _binary, _bindings} =
  defmodule Pangram.Benchmark do
    use BencheeDsl.Benchmark

    config(
      warmup: 1,
      time: 3,
      memory_time: 1,
      reduction_time: 1,
      pre_check: true,
      print: [configuration: false]
    )

    inputs(%{
      "Small" => "the quick brown fox jumps over the lazy dog",
      "Bigger" =>
        "the quick brown fox jumps over the lazy dog the quick brown fox jumps over the lazy dog the quick brown fox jumps over the lazy dog the quick brown fox jumps over the lazy dog the quick brown fox jumps over the lazy dog"
    })

    job(list_comprehension_uniq(input)) do
      Pangram.ListComprehensionUniq.pangram?(input)
    end

    job(tco(input)) do
      Pangram.TCO.pangram?(input)
    end

    job(enum_all(input)) do
      Pangram.EnumAll.pangram?(input)
    end

    job(list_substraction(input)) do
      Pangram.ListSubstraction.pangram?(input)
    end

    job(list_comprehension_reduce(input)) do
      Pangram.ListComprehensionReduce.pangram?(input)
    end
  end

BencheeDsl.Livebook.benchee_config() |> name.run() |> BencheeDsl.Livebook.render()

Pig Latin (Binary Patter Matching)

https://exercism.org/tracks/elixir/exercises/pig-latin

defmodule PigLatin.Binary do
  @vowels [?a, ?e, ?i, ?o, ?u]

  defguardp is_vowel(c) when c in @vowels
  defguardp is_consonant(c) when not is_vowel(c)

  @doc """
  Given a `phrase`, translate it a word at a time to Pig Latin.

  ## Examples

    iex> PigLatin.translate("fox")
    "oxfay"

    iex> PigLatin.translate("throat")
    "oatthray"

  """
  @spec translate(phrase :: String.t()) :: String.t()
  def translate(phrase) do
    phrase
    |> String.split()
    |> Enum.map_join(" ", &amp;word/1)
  end

  defp word(<> = word) when is_vowel(c),
    do: <>

  defp word(<<"qu", rest::binary>>),
    do: <>

  defp word(<>) when is_consonant(c),
    do: <>

  defp word(<> = word) when xy in [?x, ?y] and is_consonant(c),
    do: <>

  defp word(<c::utf8,rest::binary>) when is_vowel(c),
    do: <>

  defp word(word),
    do: cluster(word, [[] | ""])

  defp cluster(<>, [_ | c2] = acc) when is_consonant(c) and c2 != ~c"y",
    do: cluster(rest, [acc | [c]])

  defp cluster(word, [acc | [?y]]),
    do: <word::binary,io.iodata_to_binary(acc)::binary,"ay">

  defp cluster(word, acc),
    do: <>
end

Pig Latin (Regex)

defmodule PigLatin.Regex do
  @regex ~r/^(?:y([aeiou][a-z]+))|((?:xr)?(?:xb)?[aeiouy][a-z]+)|(?:([^aeioyqu]*qu|[^aeiouy]+)([a-z]+))/

  @doc """
  Given a `phrase`, translate it a word at a time to Pig Latin.

  ## Examples

    iex> PigLatin.translate("fox")
    "oxfay"

    iex> PigLatin.translate("throat")
    "oatthray"

  """
  @spec translate(phrase :: String.t()) :: String.t()
  def translate(phrase) do
    phrase
    |> String.split()
    |> Enum.map_join(" ", &amp;do_translate/1)
  end

  defp do_translate(word) do
    case Regex.run(@regex, word) do
      [_, m1] -> m1 <> "yay"
      [_, _, m2] -> m2 <> "ay"
      [_, _, _, m3, m4] -> IO.iodata_to_binary([m4, m3, "ay"])
    end
  end
end

Pig Latin: Tests

https://github.com/exercism/elixir/blob/main/exercises/practice/pig-latin/test/pig_latin_test.exs

for module <- [PigLatin.Binary, PigLatin.Regex] do
  assert module.translate("apple") == "appleay"
  assert module.translate("ear") == "earay"
  assert module.translate("igloo") == "iglooay"
  assert module.translate("object") == "objectay"
  assert module.translate("under") == "underay"
  assert module.translate("equal") == "equalay"
  assert module.translate("pig") == "igpay"
  assert module.translate("koala") == "oalakay"
  assert module.translate("xenon") == "enonxay"
  assert module.translate("qat") == "atqay"
  assert module.translate("pleasure") == "easureplay"
  assert module.translate("stringify") == "ingifystray"
  assert module.translate("zkrrkrkrkrzzzkewk") == "ewkzkrrkrkrkrzzzkay"
  assert module.translate("chair") == "airchay"
  assert module.translate("queen") == "eenquay"
  assert module.translate("square") == "aresquay"
  assert module.translate("therapy") == "erapythay"
  assert module.translate("thrush") == "ushthray"
  assert module.translate("school") == "oolschay"
  assert module.translate("yttria") == "yttriaay"
  assert module.translate("yddria") == "yddriaay"
  assert module.translate("xray") == "xrayay"
  assert module.translate("xbot") == "xbotay"
  assert module.translate("yellow") == "ellowyay"
  assert module.translate("rhythm") == "ythmrhay"
  assert module.translate("my") == "ymay"
  assert module.translate("quick fast run") == "ickquay astfay unray"
end

:passed

Pig Latin: Benchmark

{:module, name, _binary, _bindings} =
  defmodule PigLatin.Benchmark do
    use BencheeDsl.Benchmark

    config(
      warmup: 1,
      time: 3,
      memory_time: 1,
      reduction_time: 1,
      pre_check: true,
      print: [configuration: false]
    )

    inputs(%{
      "Small" => "the quick brown fox jumps over the lazy dog",
      "Bigger" =>
        "the quick brown fox jumps over the lazy dog the quick brown fox jumps over the lazy dog the quick brown fox jumps over the lazy dog the quick brown fox jumps over the lazy dog the quick brown fox jumps over the lazy dog"
    })

    job(binary_patter_matching(input)) do
      PigLatin.Binary.translate(input)
    end

    job(regex(input)) do
      PigLatin.Regex.translate(input)
    end
  end

BencheeDsl.Livebook.benchee_config() |> name.run() |> BencheeDsl.Livebook.render()

Protein Translation (TCO)

https://exercism.org/tracks/elixir/exercises/protein-translation

defmodule ProteinTranslation.TCO do
  @doc """
  Given an RNA string, return a list of proteins specified by codons, in order.

  ## Examples

      iex> ProteinTranslation.TCO.of_rna("UUUUUU")
      {:ok, ~w(Phenylalanine Phenylalanine)}

  """
  @spec of_rna(String.t()) :: {:ok, list(String.t())} | {:error, String.t()}
  def of_rna(rna), do: do_of_rna(rna, [])

  defp do_of_rna(<>, proteins) do
    case of_codon(codon) do
      {:ok, "STOP"} -> do_of_rna(<<>>, proteins)
      {:ok, protein} -> do_of_rna(rest, proteins ++ [protein])
      {:error, _reason} -> {:error, "invalid RNA"}
    end
  end

  defp do_of_rna(<<>>, proteins), do: {:ok, proteins}
  defp do_of_rna(_rna, _proteins), do: {:error, "invalid RNA"}

  @codon_to_protein %{
    "UGU" => "Cysteine",
    "UGC" => "Cysteine",
    "UUA" => "Leucine",
    "UUG" => "Leucine",
    "AUG" => "Methionine",
    "UUU" => "Phenylalanine",
    "UUC" => "Phenylalanine",
    "UCU" => "Serine",
    "UCC" => "Serine",
    "UCA" => "Serine",
    "UCG" => "Serine",
    "UGG" => "Tryptophan",
    "UAU" => "Tyrosine",
    "UAC" => "Tyrosine",
    "UAA" => "STOP",
    "UAG" => "STOP",
    "UGA" => "STOP"
  }

  @doc """
  Given a codon, return the corresponding protein

  UGU -> Cysteine
  UGC -> Cysteine
  UUA -> Leucine
  UUG -> Leucine
  AUG -> Methionine
  UUU -> Phenylalanine
  UUC -> Phenylalanine
  UCU -> Serine
  UCC -> Serine
  UCA -> Serine
  UCG -> Serine
  UGG -> Tryptophan
  UAU -> Tyrosine
  UAC -> Tyrosine
  UAA -> STOP
  UAG -> STOP
  UGA -> STOP
  """
  @spec of_codon(String.t()) :: {:ok, String.t()} | {:error, String.t()}
  for {codon, protein} <- @codon_to_protein do
    def of_codon(unquote(codon)), do: {:ok, unquote(protein)}
  end

  def of_codon(_codon), do: {:error, "invalid codon"}
end

Protein Translation (TCO, Case)

defmodule ProteinTranslation.TCOCase do
  @doc """
  Given an RNA string, return a list of proteins specified by codons, in order.

  ## Examples

      iex> ProteinTranslation.TCOCase.of_rna("UUUUUU")
      {:ok, ~w(Phenylalanine Phenylalanine)}

  """
  @spec of_rna(String.t()) :: {:ok, list(String.t())} | {:error, String.t()}
  def of_rna(rna), do: do_of_rna(rna, [])

  defp do_of_rna(<>, proteins) do
    case of_codon(codon) do
      {:ok, "STOP"} -> do_of_rna(<<>>, proteins)
      {:ok, protein} -> do_of_rna(rest, proteins ++ [protein])
      {:error, _reason} -> {:error, "invalid RNA"}
    end
  end

  defp do_of_rna(<<>>, proteins), do: {:ok, proteins}
  defp do_of_rna(_rna, _proteins), do: {:error, "invalid RNA"}

  @doc """
  Given a codon, return the corresponding protein

  UGU -> Cysteine
  UGC -> Cysteine
  UUA -> Leucine
  UUG -> Leucine
  AUG -> Methionine
  UUU -> Phenylalanine
  UUC -> Phenylalanine
  UCU -> Serine
  UCC -> Serine
  UCA -> Serine
  UCG -> Serine
  UGG -> Tryptophan
  UAU -> Tyrosine
  UAC -> Tyrosine
  UAA -> STOP
  UAG -> STOP
  UGA -> STOP
  """
  @spec of_codon(String.t()) :: {:ok, String.t()} | {:error, String.t()}
  def of_codon(codon) do
    case codon do
      "UGU" -> {:ok, "Cysteine"}
      "UGC" -> {:ok, "Cysteine"}
      "UUA" -> {:ok, "Leucine"}
      "UUG" -> {:ok, "Leucine"}
      "AUG" -> {:ok, "Methionine"}
      "UUU" -> {:ok, "Phenylalanine"}
      "UUC" -> {:ok, "Phenylalanine"}
      "UCU" -> {:ok, "Serine"}
      "UCC" -> {:ok, "Serine"}
      "UCA" -> {:ok, "Serine"}
      "UCG" -> {:ok, "Serine"}
      "UGG" -> {:ok, "Tryptophan"}
      "UAU" -> {:ok, "Tyrosine"}
      "UAC" -> {:ok, "Tyrosine"}
      "UAA" -> {:ok, "STOP"}
      "UAG" -> {:ok, "STOP"}
      "UGA" -> {:ok, "STOP"}
      _ -> {:error, "invalid codon"}
    end
  end
end

Protein Translation: Tests

https://github.com/exercism/elixir/blob/main/exercises/practice/protein-translation/test/protein_translation_test.exs

for module <- [ProteinTranslation.TCO, ProteinTranslation.TCOCase] do
  assert module.of_codon("AUG") == {:ok, "Methionine"}
  assert module.of_codon("UUU") == {:ok, "Phenylalanine"}
  assert module.of_codon("UUC") == {:ok, "Phenylalanine"}
  assert module.of_codon("UUA") == {:ok, "Leucine"}
  assert module.of_codon("UUG") == {:ok, "Leucine"}
  assert module.of_codon("UCU") == {:ok, "Serine"}
  assert module.of_codon("UCC") == {:ok, "Serine"}
  assert module.of_codon("UCA") == {:ok, "Serine"}
  assert module.of_codon("UCG") == {:ok, "Serine"}
  assert module.of_codon("UAU") == {:ok, "Tyrosine"}
  assert module.of_codon("UAC") == {:ok, "Tyrosine"}
  assert module.of_codon("UGU") == {:ok, "Cysteine"}
  assert module.of_codon("UGC") == {:ok, "Cysteine"}
  assert module.of_codon("UGG") == {:ok, "Tryptophan"}
  assert module.of_codon("UAA") == {:ok, "STOP"}
  assert module.of_codon("UAG") == {:ok, "STOP"}
  assert module.of_codon("UGA") == {:ok, "STOP"}
  assert module.of_codon("UG") == {:error, "invalid codon"}
  assert module.of_codon("UGGG") == {:error, "invalid codon"}
  assert module.of_codon("AAA") == {:error, "invalid codon"}
  assert module.of_codon("XYZ") == {:error, "invalid codon"}
  assert module.of_rna("") == {:ok, []}

  assert module.of_rna("AUGUUUUGG") ==
           {:ok, ~w(Methionine Phenylalanine Tryptophan)}

  assert module.of_rna("UUUUUU") ==
           {:ok, ~w(Phenylalanine Phenylalanine)}

  assert module.of_rna("UUAUUG") == {:ok, ~w(Leucine Leucine)}
  assert module.of_rna("UAGUGG") == {:ok, ~w()}
  assert module.of_rna("UGGUAG") == {:ok, ~w(Tryptophan)}
  assert module.of_rna("AUGUUUUAA") == {:ok, ~w(Methionine Phenylalanine)}
  assert module.of_rna("UGGUAGUGG") == {:ok, ~w(Tryptophan)}

  assert module.of_rna("UGGUGUUAUUAAUGGUUU") ==
           {:ok, ~w(Tryptophan Cysteine Tyrosine)}

  assert module.of_rna("UG") == {:error, "invalid RNA"}
  assert module.of_rna("AAA") == {:error, "invalid RNA"}
  assert module.of_rna("XYZ") == {:error, "invalid RNA"}
  assert module.of_rna("UUUROT") == {:error, "invalid RNA"}
  assert module.of_rna("AUGU") == {:error, "invalid RNA"}

  assert module.of_rna("UUCUUCUAAUGGU") ==
           {:ok, ~w(Phenylalanine Phenylalanine)}
end

:passed

Protein Translation: Benchmark

{:module, name, _binary, _bindings} =
  defmodule ProteinTranslation.Benchmark do
    use BencheeDsl.Benchmark

    config(
      warmup: 1,
      time: 3,
      memory_time: 1,
      reduction_time: 1,
      pre_check: true,
      print: [configuration: false]
    )

    inputs(%{
      "Small" => "UUUUUU",
      "Bigger" => "UUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUU"
    })

    job(tco(input)) do
      ProteinTranslation.TCO.of_rna(input)
    end

    job(tco_case(input)) do
      ProteinTranslation.TCOCase.of_rna(input)
    end
  end

BencheeDsl.Livebook.benchee_config() |> name.run() |> BencheeDsl.Livebook.render()

Raindrops

https://exercism.org/tracks/elixir/exercises/raindrops

defmodule Raindrops do
  @drops Pling: 3, Plang: 5, Plong: 7

  @doc """
  Returns a string based on raindrop factors.

  - If the number contains 3 as a prime factor, output 'Pling'.
  - If the number contains 5 as a prime factor, output 'Plang'.
  - If the number contains 7 as a prime factor, output 'Plong'.
  - If the number does not contain 3, 5, or 7 as a prime factor,
    just pass the number's digits straight through.

  ## Examples

      iex> Raindrops.convert(105)
      "PlingPlangPlong"

  """
  @spec convert(pos_integer()) :: String.t()
  def convert(number) do
    @drops
    |> Enum.filter(&amp;factor?(number, elem(&amp;1, 1)))
    |> Enum.map(&amp;elem(&amp;1, 0))
    |> join(number)
  end

  defp factor?(number, factor) do
    rem(number, factor) == 0
  end

  defp join([], number), do: to_string(number)
  defp join(list, _number), do: Enum.join(list, "")
end

Raindrops: Tests

https://github.com/exercism/elixir/blob/main/exercises/practice/raindrops/test/raindrops_test.exs

assert Raindrops.convert(1) == "1"
assert Raindrops.convert(3) == "Pling"
assert Raindrops.convert(5) == "Plang"
assert Raindrops.convert(7) == "Plong"
assert Raindrops.convert(6) == "Pling"
assert Raindrops.convert(8) == "8"
assert Raindrops.convert(9) == "Pling"
assert Raindrops.convert(10) == "Plang"
assert Raindrops.convert(14) == "Plong"
assert Raindrops.convert(15) == "PlingPlang"
assert Raindrops.convert(21) == "PlingPlong"
assert Raindrops.convert(25) == "Plang"
assert Raindrops.convert(35) == "PlangPlong"
assert Raindrops.convert(49) == "Plong"
assert Raindrops.convert(52) == "52"
assert Raindrops.convert(105) == "PlingPlangPlong"
assert Raindrops.convert(12121) == "12121"

:passed

Resistor Color Duo

https://exercism.org/tracks/elixir/exercises/resistor-color-duo

defmodule ResistorColorDuo do
  @type color ::
          :black | :brown | :red | :orange | :yellow | :green | :blue | :violet | :grey | :white

  @spec code(color :: color()) :: non_neg_integer()
  defp code(:black), do: 0
  defp code(:brown), do: 1
  defp code(:red), do: 2
  defp code(:orange), do: 3
  defp code(:yellow), do: 4
  defp code(:green), do: 5
  defp code(:blue), do: 6
  defp code(:violet), do: 7
  defp code(:grey), do: 8
  defp code(:white), do: 9

  @doc """
  Calculate a resistance value from two colors.

  ## Examples

      iex> ResistorColorDuo.value([:yellow, :red])
      42

  """
  @spec value(colors :: [color()]) :: non_neg_integer()
  def value(colors) when length(colors) >= 2 do
    [color1, color2 | _tail] = colors

    code(color1) * 10 + code(color2)
  end
end

Resistor Color Duo: Tests

https://github.com/exercism/elixir/blob/main/exercises/practice/resistor-color-duo/test/resistor_color_duo_test.exs

colors = [:brown, :black]
output = ResistorColorDuo.value(colors)
expected = 10
assert output == expected

colors = [:blue, :grey]
output = ResistorColorDuo.value(colors)
expected = 68
assert output == expected

colors = [:yellow, :violet]
output = ResistorColorDuo.value(colors)
expected = 47
assert output == expected

colors = [:white, :red]
output = ResistorColorDuo.value(colors)
expected = 92
assert output == expected

colors = [:orange, :orange]
output = ResistorColorDuo.value(colors)
expected = 33
assert output == expected

colors = [:green, :brown, :orange]
output = ResistorColorDuo.value(colors)
expected = 51
assert output == expected

colors = [:black, :brown]
output = ResistorColorDuo.value(colors)
expected = 1
assert output == expected

:passed

RNA Transcription (TCO)

https://exercism.org/tracks/elixir/exercises/rna-transcription

defmodule RnaTranscription.TCO do
  @doc """
  Transcribes a character list representing DNA nucleotides to RNA.

  ## Examples

    iex> RnaTranscription.TCO.to_rna('ACTG')
    'UGAC'

  """
  @spec to_rna([char()]) :: [char()]
  def to_rna(dna), do: do_to_rna(dna, [])

  defp do_to_rna([], acc), do: Enum.reverse(acc)
  defp do_to_rna([?G | dna], acc), do: do_to_rna(dna, [?C | acc])
  defp do_to_rna([?C | dna], acc), do: do_to_rna(dna, [?G | acc])
  defp do_to_rna([?T | dna], acc), do: do_to_rna(dna, [?A | acc])
  defp do_to_rna([?A | dna], acc), do: do_to_rna(dna, [?U | acc])
end

RNA Transcription (Recursion)

defmodule RnaTranscription.Recursion do
  @doc """
  Transcribes a character list representing DNA nucleotides to RNA.

  ## Examples

    iex> RnaTranscription.Recursion.to_rna('ACTG')
    'UGAC'

  """
  @spec to_rna([char()]) :: [char()]
  def to_rna([]), do: []
  def to_rna([?G | dna]), do: [?C | to_rna(dna)]
  def to_rna([?C | dna]), do: [?G | to_rna(dna)]
  def to_rna([?T | dna]), do: [?A | to_rna(dna)]
  def to_rna([?A | dna]), do: [?U | to_rna(dna)]
end

RNA Transcription (Enum.map/2, Multi Clause)

defmodule RnaTranscription.EnumMapMultiClause do
  @doc """
  Transcribes a character list representing DNA nucleotides to RNA.

  ## Examples

    iex> RnaTranscription.EnumMapMultiClause.to_rna('ACTG')
    'UGAC'

  """
  @spec to_rna([char()]) :: [char()]
  def to_rna(dna) do
    Enum.map(dna, fn
      ?G -> ?C
      ?C -> ?G
      ?T -> ?A
      ?A -> ?U
    end)
  end
end

RNA Transcription (Enum.map/2, Case)

defmodule RnaTranscription.EnumMapCase do
  @doc """
  Transcribes a character list representing DNA nucleotides to RNA.

  ## Examples

    iex> RnaTranscription.EnumMapCase.to_rna('ACTG')
    'UGAC'

  """
  @spec to_rna([char()]) :: [char()]
  def to_rna(dna) do
    Enum.map(dna, fn nucleotide ->
      case nucleotide do
        ?G -> ?C
        ?C -> ?G
        ?T -> ?A
        ?A -> ?U
      end
    end)
  end
end

RNA Transcription (List Comprehension)

defmodule RnaTranscription.ListComprehension do
  @doc """
  Transcribes a character list representing DNA nucleotides to RNA.

  ## Examples

    iex> RnaTranscription.ListComprehension.to_rna('ACTG')
    'UGAC'

  """
  @spec to_rna([char()]) :: [char()]
  def to_rna(dna) do
    for nucleotide <- dna, into: [] do
      transcribe(nucleotide)
    end
  end

  defp transcribe(?G), do: ?C
  defp transcribe(?C), do: ?G
  defp transcribe(?T), do: ?A
  defp transcribe(?A), do: ?U
end

RNA Transcription (Enum.map/2)

defmodule RnaTranscription.EnumMap do
  @doc """
  Transcribes a character list representing DNA nucleotides to RNA.

  ## Examples

    iex> RnaTranscription.EnumMap.to_rna('ACTG')
    'UGAC'

  """
  @spec to_rna([char()]) :: [char()]
  def to_rna(dna) do
    Enum.map(dna, &amp;transcribe/1)
  end

  defp transcribe(?G), do: ?C
  defp transcribe(?C), do: ?G
  defp transcribe(?T), do: ?A
  defp transcribe(?A), do: ?U
end

RNA Transcription (Enum.map/2, Lookup List)

defmodule RnaTranscription.EnumMapLookupDict do
  @rna_complements %{
    ?G => ?C,
    ?C => ?G,
    ?T => ?A,
    ?A => ?U
  }

  @doc """
  Transcribes a character list representing DNA nucleotides to RNA.

  ## Examples

    iex> RnaTranscription.EnumMapLookupDict.to_rna('ACTG')
    'UGAC'

  """
  @spec to_rna([char()]) :: [char()]
  def to_rna(dna) do
    Enum.map(dna, &amp;@rna_complements[&amp;1])
  end
end

RNA Transcription (List Comprehension, Lookup Dict)

defmodule RnaTranscription.ListComprehensionLookupDict do
  @rna_complements %{
    ?G => ?C,
    ?C => ?G,
    ?T => ?A,
    ?A => ?U
  }

  @doc """
  Transcribes a character list representing DNA nucleotides to RNA.

  ## Examples

    iex> RnaTranscription.ListComprehensionLookupDict.to_rna('ACTG')
    'UGAC'

  """
  @spec to_rna([char()]) :: [char()]
  def to_rna(dna) do
    for nucleotide <- dna, into: [] do
      @rna_complements[nucleotide]
    end
  end
end

RNA Transcription (List Comprehension, Case)

defmodule RnaTranscription.ListComprehensionCase do
  @doc """
  Transcribes a character list representing DNA nucleotides to RNA.

  ## Examples

    iex> RnaTranscription.ListComprehensionCase.to_rna('ACTG')
    'UGAC'

  """
  @spec to_rna([char()]) :: [char()]
  def to_rna(dna) do
    for nucleotide <- dna, into: [] do
      case nucleotide do
        ?G -> ?C
        ?C -> ?G
        ?T -> ?A
        ?A -> ?U
      end
    end
  end
end

RNA Transcription: Tests

https://github.com/exercism/elixir/blob/main/exercises/practice/rna-transcription/test/rna_transcription_test.exs

modules = [
  RnaTranscription.TCO,
  RnaTranscription.Recursion,
  RnaTranscription.EnumMapMultiClause,
  RnaTranscription.EnumMapCase,
  RnaTranscription.ListComprehension,
  RnaTranscription.EnumMap,
  RnaTranscription.EnumMapLookupDict,
  RnaTranscription.ListComprehensionLookupDict,
  RnaTranscription.ListComprehensionCase
]

for module <- modules do
  assert module.to_rna('') == ''
  assert module.to_rna('G') == 'C'
  assert module.to_rna('C') == 'G'
  assert module.to_rna('T') == 'A'
  assert module.to_rna('A') == 'U'
  assert module.to_rna('ACGTGGTCTTAA') == 'UGCACCAGAAUU'
end

:passed

RNA Transcription: Benchmark

{:module, name, _binary, _bindings} =
  defmodule RnaTranscription.Benchmark do
    use BencheeDsl.Benchmark

    config(
      warmup: 1,
      time: 3,
      memory_time: 1,
      reduction_time: 1,
      pre_check: true,
      print: [configuration: false]
    )

    inputs(%{
      "Small" => 'ACTG',
      "Bigger" => 'ACTGACTGACTGACTGACTGACTGACTGACTGACTGACTGACTGACTGACTGACTGACTGACTGACTGACTGACTG'
    })

    job(tco(input)) do
      RnaTranscription.TCO.to_rna(input)
    end

    job(recursion(input)) do
      RnaTranscription.Recursion.to_rna(input)
    end

    job(enum_map_multi_clause(input)) do
      RnaTranscription.EnumMapMultiClause.to_rna(input)
    end

    job(enum_map_case(input)) do
      RnaTranscription.EnumMapCase.to_rna(input)
    end

    job(list_comprehension(input)) do
      RnaTranscription.ListComprehension.to_rna(input)
    end

    job(enum_map(input)) do
      RnaTranscription.EnumMap.to_rna(input)
    end

    job(enum_map_lookup_dict(input)) do
      RnaTranscription.EnumMapLookupDict.to_rna(input)
    end

    job(list_comprehension_lookup_dict(input)) do
      RnaTranscription.ListComprehensionLookupDict.to_rna(input)
    end

    job(list_comprehension_case(input)) do
      RnaTranscription.ListComprehensionCase.to_rna(input)
    end
  end

BencheeDsl.Livebook.benchee_config() |> name.run() |> BencheeDsl.Livebook.render()

Roman Numerals

https://exercism.org/tracks/elixir/exercises/roman-numerals

defmodule RomanNumerals do
  @roman_numerals [
    {1000, "M"},
    {900, "CM"},
    {500, "D"},
    {400, "CD"},
    {100, "C"},
    {90, "XC"},
    {50, "L"},
    {40, "XL"},
    {10, "X"},
    {9, "IX"},
    {5, "V"},
    {4, "IV"},
    {1, "I"}
  ]

  @doc """
  Convert the number to a roman number.

  ## Examples

      iex> RomanNumerals.numeral(420)
      "CDXX"

  """
  @spec numeral(pos_integer) :: String.t()
  def numeral(number) when number > 0 do
    {part, letter} = hd(Enum.filter(@roman_numerals, fn {p, _} -> p <= number end))
    letter <> numeral(number - part)
  end

  def numeral(_), do: ""
end

Roman Numerals: Tests

https://github.com/exercism/elixir/blob/main/exercises/practice/roman-numerals/test/roman_numerals_test.exs

assert RomanNumerals.numeral(1) == "I"
assert RomanNumerals.numeral(2) == "II"
assert RomanNumerals.numeral(3) == "III"
assert RomanNumerals.numeral(4) == "IV"
assert RomanNumerals.numeral(5) == "V"
assert RomanNumerals.numeral(6) == "VI"
assert RomanNumerals.numeral(9) == "IX"
assert RomanNumerals.numeral(27) == "XXVII"
assert RomanNumerals.numeral(48) == "XLVIII"
assert RomanNumerals.numeral(59) == "LIX"
assert RomanNumerals.numeral(93) == "XCIII"
assert RomanNumerals.numeral(141) == "CXLI"
assert RomanNumerals.numeral(163) == "CLXIII"
assert RomanNumerals.numeral(402) == "CDII"
assert RomanNumerals.numeral(575) == "DLXXV"
assert RomanNumerals.numeral(911) == "CMXI"
assert RomanNumerals.numeral(1024) == "MXXIV"
assert RomanNumerals.numeral(3000) == "MMM"
assert RomanNumerals.numeral(16) == "XVI"
assert RomanNumerals.numeral(66) == "LXVI"
assert RomanNumerals.numeral(166) == "CLXVI"
assert RomanNumerals.numeral(666) == "DCLXVI"
assert RomanNumerals.numeral(1666) == "MDCLXVI"
assert RomanNumerals.numeral(3001) == "MMMI"
assert RomanNumerals.numeral(3999) == "MMMCMXCIX"

:passed

Rotational Cipher

https://exercism.org/tracks/elixir/exercises/rotational-cipher

defmodule RotationalCipher.EnumMap do
  @doc """
  Given a plaintext and amount to shift by, return a rotated string.

  ## Examples

      iex> RotationalCipher.EnumMap.rotate("Attack at dawn", 13)
      "Nggnpx ng qnja"

  """
  @spec rotate(text :: String.t(), shift :: integer) :: String.t()
  def rotate(text, shift) do
    text
    |> to_charlist()
    |> Enum.map(&amp;translate(&amp;1, shift))
    |> to_string()
  end

  defp translate(letter, shift) when letter in ?a..?z,
    do: rem(letter + shift - ?a, 26) + ?a

  defp translate(letter, shift) when letter in ?A..?Z,
    do: rem(letter + shift - ?A, 26) + ?A

  defp translate(other_character, _), do: other_character
end

Rotational Cipher (Macro)

defmodule RotationalCipher.Macro do
  @alphabet "abcdefghijklmnopqrstuvwxyz"
  @alphabet_size String.length(@alphabet)

  for shift <- 0..25 do
    plain = String.split(@alphabet, "", trim: true)

    cipher =
      @alphabet
      |> Kernel.<>(@alphabet)
      |> String.split("", trim: true)
      |> Enum.drop(shift)
      |> Enum.take(@alphabet_size)

    for {plain_letter, cipher_letter} <- Enum.zip(plain, cipher) do
      defp translate(unquote(plain_letter), unquote(shift)),
        do: unquote(cipher_letter)

      defp translate(unquote(plain_letter |> String.upcase()), unquote(shift)),
        do: unquote(cipher_letter |> String.upcase())
    end
  end

  defp translate(other_character, _), do: other_character

  @doc """
  Given a plaintext and amount to shift by, return a rotated string.

  ## Examples

      iex> RotationalCipher.Macro.rotate("Attack at dawn", 13)
      "Nggnpx ng qnja"

  """
  @spec rotate(text :: String.t(), shift :: integer) :: String.t()
  def rotate(text, shift) do
    text
    |> String.split("", trim: true)
    |> Enum.map_join(&amp;translate(&amp;1, shift))
  end
end

Rotational Cipher (List Comprehension)

defmodule RotationalCipher.ListComprehension do
  @doc """
  Given a plaintext and amount to shift by, return a rotated string.

  ## Examples

      iex> RotationalCipher.ListComprehension.rotate("Attack at dawn", 13)
      "Nggnpx ng qnja"

  """
  @spec rotate(text :: String.t(), shift :: integer) :: String.t()

  def rotate(text, shift) do
    for char <- to_charlist(text), into: [] do
      cond do
        char >= ?a and char <= ?z ->
          char = char + shift

          if char > ?z do
            ?a + (char - ?z) - 1
          else
            char
          end

        char >= ?A and char <= ?Z ->
          char = char + shift

          if char > ?Z do
            ?A + (char - ?Z) - 1
          else
            char
          end

        true ->
          char
      end
    end
    |> to_string()
  end
end

Rotational Cipher: Tests

https://github.com/exercism/elixir/blob/main/exercises/practice/rotational-cipher/test/rotational_cipher_test.exs

modules = [
  RotationalCipher.EnumMap,
  RotationalCipher.Macro,
  RotationalCipher.ListComprehension
]

for module <- modules do
  plaintext = "a"
  shift = 1
  assert module.rotate(plaintext, shift) == "b"

  plaintext = "a"
  shift = 26
  assert module.rotate(plaintext, shift) == "a"

  plaintext = "a"
  shift = 0
  assert module.rotate(plaintext, shift) == "a"

  plaintext = "m"
  shift = 13
  assert module.rotate(plaintext, shift) == "z"

  plaintext = "n"
  shift = 13
  assert module.rotate(plaintext, shift) == "a"

  plaintext = "OMG"
  shift = 5
  assert module.rotate(plaintext, shift) == "TRL"

  plaintext = "O M G"
  shift = 5
  assert module.rotate(plaintext, shift) == "T R L"

  plaintext = "Testing 1 2 3 testing"
  shift = 4
  assert module.rotate(plaintext, shift) == "Xiwxmrk 1 2 3 xiwxmrk"

  plaintext = "Let's eat, Grandma!"
  shift = 21
  assert module.rotate(plaintext, shift) == "Gzo'n zvo, Bmviyhv!"

  plaintext = "The quick brown fox jumps over the lazy dog."
  shift = 13

  assert module.rotate(plaintext, shift) ==
           "Gur dhvpx oebja sbk whzcf bire gur ynml qbt."
end

:passed

Rotational Cipher: Benchmark

{:module, name, _binary, _bindings} =
  defmodule RotationalCipher.Benchmark do
    use BencheeDsl.Benchmark

    config(
      warmup: 1,
      time: 3,
      memory_time: 1,
      reduction_time: 1,
      pre_check: true,
      print: [configuration: false]
    )

    inputs(%{
      "Small" => "Attack at dawn",
      "Bigger" =>
        "Attack at dawn Attack at dawn Attack at dawn Attack at dawn Attack at dawn Attack at dawn"
    })

    job(enum_map(input)) do
      RotationalCipher.EnumMap.rotate(input, 13)
    end

    job(macro(input)) do
      RotationalCipher.Macro.rotate(input, 13)
    end

    job(list_comprehension(input)) do
      RotationalCipher.ListComprehension.rotate(input, 13)
    end
  end

BencheeDsl.Livebook.benchee_config() |> name.run() |> BencheeDsl.Livebook.render()