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

Excercism Learning Exercises

learning-exercises.livemd

Excercism Learning Exercises

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

require Integer
import ExUnit.Assertions
import ExUnit.CaptureIO, only: [capture_io: 1, capture_io: 2]

Application.load(:ex_unit)

Hello World

https://exercism.org/tracks/elixir/exercises/hello-world

defmodule HelloWorld do
  @doc """
  Simply returns "Hello, World!"

  ## Examples

      iex> HelloWorld.hello()
      "Hello, World!"

  """
  @spec hello :: String.t()
  def hello do
    "Hello, World!"
  end
end

Lasanga

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

defmodule Lasagna do
  @moduledoc """
  Modules contains some functions to help you cook a brilliant lasagna
  from you favorite cooking book.

  ## Examples

      iex> Lasagna.expected_minutes_in_oven()
      40

      iex> Lasagna.remaining_minutes_in_oven(10)
      30

      iex> Lasagna.preparation_time_in_minutes(2)
      4

      iex> Lasagna.total_time_in_minutes(2, 10)
      14

      iex> Lasagna.alarm()
      "Ding!"

  """

  @type minutes :: non_neg_integer()
  @type layers :: non_neg_integer()
  @total_minutes_in_oven 40
  @minutes_for_layer 2

  @doc "Returns how many minutes the lasagna should be in the oven."
  @spec expected_minutes_in_oven() :: minutes()
  def expected_minutes_in_oven(), do: @total_minutes_in_oven

  @doc """
  Takes the actual minutes the lasagna has been in the oven and returns
  how many minutes the lasagna still has to remain in the oven.
  """
  @spec remaining_minutes_in_oven(minutes()) :: minutes()
  def remaining_minutes_in_oven(actual_minutes) when actual_minutes >= 0 do
    expected_minutes_in_oven() - actual_minutes
  end

  @doc """
  Takes the number of layers you added to the lasagna as a argument and returns
  how many minutes you spent preparing the lasagna, assuming each layer takes
  you 2 minutes to prepare.
  """
  @spec preparation_time_in_minutes(minutes()) :: minutes()
  def preparation_time_in_minutes(layers) when layers > 0 do
    @minutes_for_layer * layers
  end

  @doc """
  Returns how many minutes in total you've worked on cooking the lasagna,
  which is sum of the preparation time, and the time in minutes the lasagna
  has spent in the oven at the moment.
  """
  @spec total_time_in_minutes(layers(), minutes()) :: minutes()
  def total_time_in_minutes(layers, actual_minutes)
      when layers > 0 and actual_minutes >= 0,
      do: preparation_time_in_minutes(layers) + actual_minutes

  @doc "Returns a message indicating that the lasagna is ready to eat."
  @spec alarm() :: String.t()
  def alarm(), do: "Ding!"
end

Lasagna: Tests

https://github.com/exercism/elixir/blob/main/exercises/concept/lasagna/test/lasagna_test.exs

assert Lasagna.expected_minutes_in_oven() === 40
assert Lasagna.remaining_minutes_in_oven(25) === 15
assert Lasagna.preparation_time_in_minutes(1) === 2
assert Lasagna.preparation_time_in_minutes(4) === 8
assert Lasagna.total_time_in_minutes(1, 30) === 32
assert Lasagna.total_time_in_minutes(4, 8) === 16
assert Lasagna.alarm() === "Ding!"

:passed

Pacman Rules

https://exercism.org/tracks/elixir/exercises/pacman-rules

defmodule Rules do
  @moduledoc """

  ## Examples

      iex> Rules.eat_ghost?(false, true)
      false

      iex> Rules.score?(true, false)
      true

      iex> Rules.lose?(false, true)
      true

      iex> Rules.win?(true, false, false)
      true

  """

  @doc "Returns true if Pac-Man is able to eat the ghost."
  @spec eat_ghost?(boolean(), boolean()) :: boolean()
  def eat_ghost?(power_pellet_active, touching_ghost),
    do: power_pellet_active and touching_ghost

  @doc "Returns true if Pac-Man is touching a power pellet or a dot."
  @spec score?(boolean(), boolean()) :: boolean()
  def score?(touching_power_pellet, touching_dot),
    do: touching_power_pellet or touching_dot

  @doc """
  Returns true if Pac-Man is touching ghost and does not have
  a power pellet active.
  """
  @spec lose?(boolean(), boolean()) :: boolean()
  def lose?(power_pellet_active, touching_ghost),
    do: not power_pellet_active and touching_ghost

  @doc "Returns true if Pac-Man has eaten all of the dots and has not lost."
  @spec win?(boolean(), boolean(), boolean()) :: boolean()
  def win?(has_eaten_all_dots, power_pellet_active, touching_ghost),
    do: not lose?(power_pellet_active, touching_ghost) and has_eaten_all_dots
end

Pacman Rules: Tests

https://github.com/exercism/elixir/blob/main/exercises/concept/pacman-rules/test/rules_test.exs

assert Rules.eat_ghost?(true, true)
refute Rules.eat_ghost?(false, true)
refute Rules.eat_ghost?(true, false)
refute Rules.eat_ghost?(false, false)

assert Rules.score?(false, true)
assert Rules.score?(true, false)
refute Rules.score?(false, false)

assert Rules.lose?(false, true)
refute Rules.lose?(true, true)
refute Rules.lose?(true, false)

assert Rules.win?(true, false, false)
refute Rules.win?(true, false, true)
assert Rules.win?(true, true, true)

:passed

Freelancer Rates

https://exercism.org/tracks/elixir/exercises/freelancer-rates

defmodule FreelancerRates do
  @moduledoc """

  ## Examples

      iex> FreelancerRates.daily_rate(100)
      800.0

      iex> FreelancerRates.apply_discount(100, 10)
      90.0

      iex> FreelancerRates.monthly_rate(100, 10)
      15840

      iex> FreelancerRates.days_in_budget(15840, 100, 10)
      22.0
  """

  @type rate :: number()
  @type discount :: number()
  @type amount :: number()

  @daily_rate 8.0
  @monthly_billable_days 22

  defguardp is_positive(number) when number > 0
  defguardp is_non_neg(number) when number >= 0

  @doc "Returns the daily rate which is 8 times the hourly rate."
  @spec daily_rate(rate()) :: amount()
  def daily_rate(hourly_rate) when is_positive(hourly_rate) do
    @daily_rate * hourly_rate
  end

  @doc "Calculates the price after a discount."
  @spec apply_discount(amount(), discount()) :: amount()
  def apply_discount(before_discount, discount)
      when is_positive(before_discount) and is_non_neg(discount) do
    before_discount - discount / 100 * before_discount
  end

  @doc "Calculates the monthly rate and applies a discount."
  @spec monthly_rate(rate(), amount()) :: amount()
  def monthly_rate(hourly_rate, discount)
      when is_positive(hourly_rate) and is_non_neg(discount) do
    (@monthly_billable_days * daily_discounted_rate(hourly_rate, discount))
    |> ceil()
  end

  @doc """
  Calculates how many days of work covers based on a budget,
  a hourly rate, and a discount.
  """
  @spec days_in_budget(amount(), rate(), discount()) :: amount()
  def days_in_budget(budget, hourly_rate, discount)
      when is_positive(budget) and is_positive(hourly_rate) and is_non_neg(discount) do
    total = daily_discounted_rate(hourly_rate, discount)
    Float.floor(budget / total, 1)
  end

  defp daily_discounted_rate(hourly_rate, discount) do
    hourly_rate
    |> daily_rate()
    |> apply_discount(discount)
  end
end

Freelancer Rates: Tests

https://github.com/exercism/elixir/blob/main/exercises/concept/freelancer-rates/test/freelancer_rates_test.exs

assert FreelancerRates.daily_rate(50) == 400.0
assert FreelancerRates.daily_rate(60) === 480.0
assert FreelancerRates.daily_rate(55.1) == 440.8

assert FreelancerRates.apply_discount(140.0, 10) == 126.0
assert FreelancerRates.apply_discount(100, 10) == 90.0
assert_in_delta FreelancerRates.apply_discount(111.11, 13.5), 96.11015, 0.000001

assert FreelancerRates.monthly_rate(62, 0.0) == 10_912
assert FreelancerRates.monthly_rate(70, 0.0) === 12_320
assert FreelancerRates.monthly_rate(62.8, 0.0) == 11_053
assert FreelancerRates.monthly_rate(65.2, 0.0) == 11_476
assert FreelancerRates.monthly_rate(67, 12.0) == 10_377

assert FreelancerRates.days_in_budget(1_600, 50, 0.0) == 4
assert FreelancerRates.days_in_budget(520, 65, 0.0) === 1.0
assert FreelancerRates.days_in_budget(4_410, 55, 0.0) == 10.0
assert FreelancerRates.days_in_budget(4_480, 55, 0.0) == 10.1
assert FreelancerRates.days_in_budget(480, 60, 20) == 1.2

:passed

Secrets

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

defmodule Secrets do
  @moduledoc """

  ## Example

      iex> Secrets.secret_add(1).(1)
      2

      iex> Secrets.secret_subtract(1).(1)
      0

      iex> Secrets.secret_multiply(1).(1)
      1

      iex> Secrets.secret_divide(10).(10)
      1

      iex> Secrets.secret_and(1).(10)
      0

      iex> Secrets.secret_xor(1).(10)
      11

      iex> Secrets.secret_combine(Secrets.secret_add(1), Secrets.secret_subtract(1)).(1)
      1

  """

  import Bitwise

  @doc """
  Returns a function which takes one argument and adds to it
  the argument passed in to `secret_add`.
  """
  @spec secret_add(integer()) :: (integer() -> integer())
  def secret_add(secret) when is_integer(secret) do
    &(trunc(&1) + secret)
  end

  @doc """
  Returns a function which takes one argument and subtracts from it
  the secret passed in to `secret_subtract`.
  """
  @spec secret_subtract(integer()) :: (integer() -> integer())
  def secret_subtract(secret) when is_integer(secret) do
    &(trunc(&1) - secret)
  end

  @doc """
  Returns a function which takes one argument and multiplies it
  by the secret passed in to `secret_multiply`.
  """
  @spec secret_multiply(integer()) :: (integer() -> integer())
  def secret_multiply(secret) when is_integer(secret) do
    &(trunc(&1) * secret)
  end

  @doc """
  Returns a function which takes one argument and divides it
  by the secrect passed in to `secret_divide`.
  """
  @spec secret_divide(integer()) :: (integer() -> integer())
  def secret_divide(secret) when is_integer(secret) and secret != 0 do
    &div(&1, secret)
  end

  @doc """
  Returns a function which takes one argument and performs
  a bitwise AND operation on it and the secret passed in to `secret_and`.
  """
  @spec secret_and(integer()) :: (integer() -> integer())
  def secret_and(secret) when is_integer(secret) do
    &band(&1, secret)
  end

  @doc """
  Returns a function which takes one argument and performs
  a bitwise XOR operation on it and the secret passed in to `secret_xor`.
  """
  @spec secret_xor(integer()) :: (integer() -> integer())
  def secret_xor(secret) when is_integer(secret) do
    &bxor(&1, secret)
  end

  @doc """
  Returns a function which takes one argument and applies to it
  the two functions passed in to `secret_combine` in order.
  """
  @spec secret_combine((integer() -> integer()), (integer() -> integer())) ::
          (integer() -> integer())
  def secret_combine(secret_function1, secret_function2) do
    &(&1 |> secret_function1.() |> secret_function2.())
  end
end

Secrets: Tests

https://github.com/exercism/elixir/blob/main/exercises/concept/secrets/test/secrets_test.exs

add = Secrets.secret_add(3)
assert add.(3) === 6

add = Secrets.secret_add(6)
assert add.(9) === 15

subtract = Secrets.secret_subtract(3)
assert subtract.(6) === 3

subtract = Secrets.secret_subtract(6)
assert subtract.(3) === -3

multiply = Secrets.secret_multiply(3)
assert multiply.(6) === 18

multiply = Secrets.secret_multiply(6)
assert multiply.(7) === 42

divide = Secrets.secret_divide(3)
assert divide.(6) === 2

divide = Secrets.secret_divide(6)
assert divide.(7) === 1

ander = Secrets.secret_and(1)
assert ander.(2) === 0

ander = Secrets.secret_and(7)
assert ander.(7) === 7

xorer = Secrets.secret_xor(1)
assert xorer.(2) === 3

xorer = Secrets.secret_xor(7)
assert xorer.(7) === 0

f = Secrets.secret_add(10)
g = Secrets.secret_subtract(5)
h = Secrets.secret_combine(f, g)

assert h.(5) === 10

f = Secrets.secret_multiply(2)
g = Secrets.secret_subtract(20)
h = Secrets.secret_combine(f, g)

assert h.(100) === 180

f = Secrets.secret_divide(10)
g = Secrets.secret_add(10)
h = Secrets.secret_combine(f, g)

assert h.(100) === 20

f = Secrets.secret_divide(3)
g = Secrets.secret_add(5)
h = Secrets.secret_combine(f, g)

assert h.(32) === 15

f = Secrets.secret_and(3)
g = Secrets.secret_and(5)
h = Secrets.secret_combine(f, g)

assert h.(7) === 1

f = Secrets.secret_and(7)
g = Secrets.secret_and(7)
h = Secrets.secret_combine(f, g)

assert h.(7) === 7

f = Secrets.secret_xor(1)
g = Secrets.secret_xor(2)
h = Secrets.secret_combine(f, g)

assert h.(4) === 7

f = Secrets.secret_xor(7)
g = Secrets.secret_xor(7)
h = Secrets.secret_combine(f, g)

assert h.(7) === 7

f = Secrets.secret_add(3)
g = Secrets.secret_xor(7)
h = Secrets.secret_combine(f, g)

assert h.(4) === 0

f = Secrets.secret_divide(9)
g = Secrets.secret_and(7)
h = Secrets.secret_combine(f, g)

assert h.(81) === 1

:passed

Log Level

https://exercism.org/tracks/elixir/exercises/log-level

defmodule LogLevel do
  @moduledoc """

  ## Examples

      iex> LogLevel.to_label(3, false)
      :warning

      iex> LogLevel.alert_recipient(3, false)
      false

      iex> LogLevel.alert_recipient(4, false)
      :ops

  """

  @type log_level :: :trace | :debug | :info | :warning | :error | :fatal | :unknown

  @doc "Returns the logging code label."
  @spec to_label(non_neg_integer(), boolean()) :: log_level()
  def to_label(level, legacy?) do
    cond do
      level === 0 and not legacy? -> :trace
      level === 1 -> :debug
      level === 2 -> :info
      level === 3 -> :warning
      level === 4 -> :error
      level === 5 and not legacy? -> :fatal
      true -> :unknown
    end
  end

  @doc "Determines to whom a alert need to be sent."
  @spec alert_recipient(non_neg_integer(), boolean()) :: boolean() | atom()
  def alert_recipient(level, legacy?) do
    label = to_label(level, legacy?)

    cond do
      label == :error or label == :fatal -> :ops
      label == :unknown and not legacy? -> :dev2
      label == :unknown -> :dev1
      true -> false
    end
  end
end

Log Level: Tests

https://github.com/exercism/elixir/blob/main/exercises/concept/log-level/test/log_level_test.exs

assert LogLevel.to_label(0, false) == :trace
assert LogLevel.to_label(0, true) == :unknown
assert LogLevel.to_label(1, false) == :debug
assert LogLevel.to_label(1, true) == :debug
assert LogLevel.to_label(2, false) == :info
assert LogLevel.to_label(2, true) == :info
assert LogLevel.to_label(3, false) == :warning
assert LogLevel.to_label(3, true) == :warning
assert LogLevel.to_label(4, false) == :error
assert LogLevel.to_label(4, true) == :error
assert LogLevel.to_label(5, false) == :fatal
assert LogLevel.to_label(5, true) == :unknown
assert LogLevel.to_label(6, false) == :unknown
assert LogLevel.to_label(6, true) == :unknown
assert LogLevel.to_label(-1, false) == :unknown
assert LogLevel.to_label(-1, true) == :unknown

assert LogLevel.alert_recipient(5, false) == :ops
assert LogLevel.alert_recipient(4, false) == :ops
assert LogLevel.alert_recipient(4, true) == :ops
assert LogLevel.alert_recipient(6, true) == :dev1
assert LogLevel.alert_recipient(0, true) == :dev1
assert LogLevel.alert_recipient(5, true) == :dev1
assert LogLevel.alert_recipient(6, false) == :dev2
assert LogLevel.alert_recipient(0, false) == false
assert LogLevel.alert_recipient(1, false) == false
assert LogLevel.alert_recipient(1, true) == false
assert LogLevel.alert_recipient(2, false) == false
assert LogLevel.alert_recipient(2, true) == false
assert LogLevel.alert_recipient(3, false) == false
assert LogLevel.alert_recipient(3, true) == false

:passed

Language List

https://exercism.org/tracks/elixir/exercises/language-list

defmodule LanguageList do
  @moduledoc """

  ## Examples

      iex> languages = LanguageList.new()
      ...> |> LanguageList.add("Elixir")
      ...> |> LanguageList.add("C")
      ["C", "Elixir"]
      iex> LanguageList.remove(languages)
      ["Elixir"]
      iex> LanguageList.first(languages)
      "C"
      iex> LanguageList.count(languages)
      2
      iex> LanguageList.functional_list?(languages)
      true

  """

  @type language_list :: list(String.t())

  @doc "Returns an empty list."
  @spec new() :: language_list()
  def new() do
    []
  end

  @doc "Prepends `list` with `language`."
  @spec add(language_list(), String.t()) :: language_list()
  def add(list, language) do
    [language | list]
  end

  @doc "Removes head of `list`."
  @spec remove(language_list()) :: language_list()
  def remove(list) do
    tl(list)
  end

  @doc "Returns head of `list`."
  @spec first(language_list()) :: language_list()
  def first(list) do
    hd(list)
  end

  @doc "Returns how many languages are in the list."
  @spec count(language_list()) :: non_neg_integer()
  def count(list) do
    length(list)
  end

  @doc "Checks the list for being an exciting."
  @spec functional_list?(language_list()) :: boolean()
  def functional_list?(list) do
    "Elixir" in list
  end
end

Language List: Tests

https://github.com/exercism/elixir/blob/main/exercises/concept/language-list/test/language_list_test.exs

assert LanguageList.new() == []

language = "Elixir"
list = [language]

assert LanguageList.new() |> LanguageList.add(language) == list

list =
  LanguageList.new()
  |> LanguageList.add("Clojure")
  |> LanguageList.add("Haskell")
  |> LanguageList.add("Erlang")
  |> LanguageList.add("F#")
  |> LanguageList.add("Elixir")

assert list == ["Elixir", "F#", "Erlang", "Haskell", "Clojure"]

list =
  LanguageList.new()
  |> LanguageList.add("Elixir")
  |> LanguageList.remove()

assert list == []

list =
  LanguageList.new()
  |> LanguageList.add("F#")
  |> LanguageList.add("Elixir")
  |> LanguageList.remove()

assert list == ["F#"]

assert LanguageList.new() |> LanguageList.add("Elixir") |> LanguageList.first() == "Elixir"

first =
  LanguageList.new()
  |> LanguageList.add("Elixir")
  |> LanguageList.add("Prolog")
  |> LanguageList.add("F#")
  |> LanguageList.first()

assert first == "F#"

assert LanguageList.new() |> LanguageList.count() == 0

count =
  LanguageList.new()
  |> LanguageList.add("Elixir")
  |> LanguageList.count()

assert count == 1

count =
  LanguageList.new()
  |> LanguageList.add("Elixir")
  |> LanguageList.add("Prolog")
  |> LanguageList.add("F#")
  |> LanguageList.count()

assert count == 3

assert LanguageList.functional_list?(["Clojure", "Haskell", "Erlang", "F#", "Elixir"])

refute LanguageList.functional_list?(["Java", "C", "JavaScript"])

:passed

Guessing Game

https://exercism.org/tracks/elixir/exercises/guessing-game

defmodule GuessingGame do
  @moduledoc """

  ## Examples

      iex> GuessingGame.compare(10, 10)
      "Correct"

      iex> GuessingGame.compare(10, 9)
      "So close"

  """

  @type guess :: number() | :no_guess

  @doc """
  Provides different responses depending on how the guess relates to the secret number
  """
  @spec compare(number(), guess()) :: String.t()
  def compare(secret_number, guess \\ :no_guess)

  def compare(_secret_number, :no_guess), do: "Make a guess"
  def compare(secret_number, guess) when secret_number == guess, do: "Correct"
  def compare(secret_number, guess) when abs(secret_number - guess) == 1, do: "So close"
  def compare(secret_number, guess) when secret_number < guess, do: "Too high"
  def compare(secret_number, guess) when secret_number > guess, do: "Too low"
end

Guessing Game: Tests

https://github.com/exercism/elixir/blob/main/exercises/concept/guessing-game/test/guessing_game_test.exs

assert GuessingGame.compare(7, 7) == "Correct"
assert GuessingGame.compare(9, 18) == "Too high"
assert GuessingGame.compare(42, 30) == "Too low"
assert GuessingGame.compare(64, 63) == "So close"
assert GuessingGame.compare(52, 53) == "So close"
assert GuessingGame.compare(15) == "Make a guess"
assert GuessingGame.compare(16, :no_guess) == "Make a guess"

:passed

Kitchen Calculator

https://exercism.org/tracks/elixir/exercises/kitchen-calculator

defmodule KitchenCalculator do
  @moduledoc """

  ## Examples

      iex> KitchenCalculator.get_volume({:cup, 2})
      2

      iex> KitchenCalculator.to_milliliter({:teaspoon, 10})
      {:milliliter, 50}

      iex> KitchenCalculator.from_milliliter({:milliliter, 50}, :teaspoon)
      {:teaspoon, 10.0}

      iex> KitchenCalculator.convert({:cup, 2}, :teaspoon)
      {:teaspoon, 96.0}

  """

  @type unit :: :cup | :fluid_ounce | :teaspoon | :tablespoon | :milliliter
  @type volume_pair :: {unit(), volume :: number()}
  @type volume :: non_neg_integer()

  @cup_ratio 240
  @fluid_once_ratio 30
  @teaspoon_ratio 5
  @tablespoon_ratio 15

  defguardp is_positive(volume) when volume > 0

  @doc """
  Returns the numeric component of a volume-pair tuple.
  """
  @spec get_volume(volume_pair()) :: volume()
  def get_volume({_, volume}), do: volume

  @doc """
  Converts volume of a given volume-pair tuple to the volume in milliliters.
  """
  @spec to_milliliter(volume_pair()) :: volume_pair()
  def to_milliliter({_unit, volume}) when volume <= 0,
    do: {:milliliter, 0}

  def to_milliliter({:milliliter, volume}) when is_positive(volume),
    do: {:milliliter, volume}

  def to_milliliter({:cup, volume}) when is_positive(volume),
    do: {:milliliter, volume * @cup_ratio}

  def to_milliliter({:fluid_ounce, volume}) when is_positive(volume),
    do: {:milliliter, volume * @fluid_once_ratio}

  def to_milliliter({:teaspoon, volume}) when is_positive(volume),
    do: {:milliliter, volume * @teaspoon_ratio}

  def to_milliliter({:tablespoon, volume}) when is_positive(volume),
    do: {:milliliter, volume * @tablespoon_ratio}

  @doc """
  Converts volume of a given volume-pair tuple to the volume in the desired unit.
  """
  @spec from_milliliter(volume_pair(), unit()) :: volume_pair()
  def from_milliliter(volume_pair, :milliliter), do: volume_pair
  def from_milliliter({_unit, volume}, to_unit) when volume <= 0, do: {to_unit, 0}

  def from_milliliter({:milliliter, volume}, :cup) when is_positive(volume),
    do: {:cup, volume / @cup_ratio}

  def from_milliliter({:milliliter, volume}, :fluid_ounce) when is_positive(volume),
    do: {:fluid_ounce, volume / @fluid_once_ratio}

  def from_milliliter({:milliliter, volume}, :teaspoon) when is_positive(volume),
    do: {:teaspoon, volume / @teaspoon_ratio}

  def from_milliliter({:milliliter, volume}, :tablespoon) when is_positive(volume),
    do: {:tablespoon, volume / @tablespoon_ratio}

  @doc """
  Converts given a volume-pair tuple to the desired unit.
  """
  @spec convert(volume_pair(), unit()) :: volume_pair()
  def convert(volume_pair, to_unit),
    do: volume_pair |> to_milliliter() |> from_milliliter(to_unit)
end

Kitchen Calculator: Tests

https://github.com/exercism/elixir/blob/main/exercises/concept/kitchen-calculator/test/kitchen_calculator_test.exs

assert KitchenCalculator.get_volume({:cup, 1}) == 1
assert KitchenCalculator.get_volume({:fluid_ounce, 2}) == 2
assert KitchenCalculator.get_volume({:teaspoon, 3}) == 3
assert KitchenCalculator.get_volume({:tablespoon, 4}) == 4
assert KitchenCalculator.get_volume({:milliliter, 5}) == 5
assert KitchenCalculator.to_milliliter({:milliliter, 3}) == {:milliliter, 3}
assert KitchenCalculator.to_milliliter({:cup, 3}) == {:milliliter, 720}
assert KitchenCalculator.to_milliliter({:fluid_ounce, 100}) == {:milliliter, 3000}
assert KitchenCalculator.to_milliliter({:teaspoon, 3}) == {:milliliter, 15}
assert KitchenCalculator.to_milliliter({:tablespoon, 3}) == {:milliliter, 45}
assert KitchenCalculator.from_milliliter({:milliliter, 4}, :milliliter) == {:milliliter, 4}
assert KitchenCalculator.from_milliliter({:milliliter, 840}, :cup) == {:cup, 3.5}

assert KitchenCalculator.from_milliliter({:milliliter, 4522.5}, :fluid_ounce) ==
         {:fluid_ounce, 150.75}

assert KitchenCalculator.from_milliliter({:milliliter, 61.25}, :teaspoon) ==
         {:teaspoon, 12.25}

assert KitchenCalculator.from_milliliter({:milliliter, 71.25}, :tablespoon) ==
         {:tablespoon, 4.75}

assert KitchenCalculator.convert({:teaspoon, 15}, :tablespoon) == {:tablespoon, 5}
assert KitchenCalculator.convert({:cup, 4}, :fluid_ounce) == {:fluid_ounce, 32}
assert KitchenCalculator.convert({:fluid_ounce, 4}, :teaspoon) == {:teaspoon, 24}
assert KitchenCalculator.convert({:tablespoon, 320}, :cup) == {:cup, 20}

:passed

High School Sweetheart

https://exercism.org/tracks/elixir/exercises/high-school-sweetheart

defmodule HighSchoolSweetheart do
  @moduledoc ~S"""

  ## Examples

      iex> HighSchoolSweetheart.pair("Avery Bryant", "Charlie Dixon")
      \"""
           ******       ******
         **      **   **      **
       **         ** **         **
      **            *            **
      **                         **
      **     A. B.  +  C. D.     **
       **                       **
         **                   **
           **               **
             **           **
               **       **
                 **   **
                   ***
                    *
      \"""
  """

  @doc """
  Extracts the name's first letter.
  """
  @spec first_letter(String.t()) :: String.t()
  def first_letter(name) do
    name
    |> String.trim_leading()
    |> String.first()
  end

  @doc """
  Formats the first letter as an initial.
  """
  @spec initial(String.t()) :: String.t()
  def initial(name), do: String.upcase("#{first_letter(name)}.")

  @doc """
  Splits the full name into the first name initial and the last name initial.
  """
  @spec initials(String.t()) :: String.t()
  def initials(full_name) do
    [first_name, last_name] = String.split(full_name, " ")
    "#{initial(first_name)} #{initial(last_name)}"
  end

  @doc """
  Puts the initials inside of the heart.
  """
  @spec pair(String.t(), String.t()) :: String.t()
  def pair(full_name1, full_name2) do
    i1 = initials(full_name1)
    i2 = initials(full_name2)

    """
         ******       ******
       **      **   **      **
     **         ** **         **
    **            *            **
    **                         **
    **     #{i1}  +  #{i2}     **
     **                       **
       **                   **
         **               **
           **           **
             **       **
               **   **
                 ***
                  *
    """
  end
end

High School Sweetheart: Tests

https://github.com/exercism/elixir/blob/main/exercises/concept/high-school-sweetheart/test/high_school_sweetheart_test.exs

assert HighSchoolSweetheart.first_letter("Mary") == "M"
assert HighSchoolSweetheart.first_letter("john") == "j"
assert HighSchoolSweetheart.first_letter("\n\t   Sarah   ") == "S"
assert HighSchoolSweetheart.initial("Betty") == "B."
assert HighSchoolSweetheart.initial("james") == "J."
assert HighSchoolSweetheart.initials("Linda Miller") == "L. M."

assert HighSchoolSweetheart.pair("Avery Bryant", "Charlie Dixon") ==
         """
              ******       ******
            **      **   **      **
          **         ** **         **
         **            *            **
         **                         **
         **     A. B.  +  C. D.     **
          **                       **
            **                   **
              **               **
                **           **
                  **       **
                    **   **
                      ***
                       *
         """

:passed

Bird Count

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

defmodule BirdCount do
  @moduledoc """
  Module provides help to any avid bird watcher that keeps track of
  how many birds have visited their garden on any given day.

  ## Examples

      iex> observations = [2, 4, 11, 10, 6, 8]
      [2, 4, 11, 10, 6, 8]
      iex> BirdCount.today(observations)
      2
      iex> BirdCount.has_day_without_birds?(observations)
      false
      iex> BirdCount.total(observations)
      41
      iex> BirdCount.busy_days(observations)
      4

  """

  @type observations :: list(non_neg_integer())

  @busy_day_count 5

  @doc """
  Returns count of how many birds have visited one's garden today.
  """
  @spec today(observations()) :: non_neg_integer()
  def today([]), do: nil
  def today([today_count | _]), do: today_count

  @doc """
  Increments today's bird watch count.
  """
  @spec increment_day_count(observations()) :: observations()
  def increment_day_count([]), do: [1]
  def increment_day_count([today_count | tail]), do: [today_count + 1 | tail]

  @doc """
  Checks if there was a day with no visiting birds.
  """
  @spec has_day_without_birds?(observations()) :: boolean()
  def has_day_without_birds?([]), do: false
  def has_day_without_birds?([0 | _tail]), do: true
  def has_day_without_birds?([_ | tail]), do: has_day_without_birds?(tail)

  @doc """
  Calculates the total number of visiting birds.
  """
  @spec total(observations()) :: non_neg_integer()
  def total(list), do: do_total(list, 0)

  defp do_total([], acc), do: acc
  defp do_total([count | tail], acc), do: do_total(tail, acc + count)

  @doc """
  Calculates the number of busy days.
  """
  @spec busy_days(observations()) :: non_neg_integer()
  def busy_days(list), do: do_busy_days(list, 0)

  defp do_busy_days([], acc), do: acc

  defp do_busy_days([count | tail], acc) when count >= @busy_day_count,
    do: do_busy_days(tail, acc + 1)

  defp do_busy_days([_count | tail], acc), do: do_busy_days(tail, acc)
end

Bird Count: Tests

https://github.com/exercism/elixir/blob/main/exercises/concept/bird-count/test/bird_count_test.exs

assert BirdCount.today([]) == nil
assert BirdCount.today([7]) == 7
assert BirdCount.today([2, 4, 11, 10, 6, 8]) == 2
assert BirdCount.increment_day_count([]) == [1]
assert BirdCount.increment_day_count([7]) == [8]
assert BirdCount.increment_day_count([4, 2, 1, 0, 10]) == [5, 2, 1, 0, 10]
assert BirdCount.has_day_without_birds?([]) == false
assert BirdCount.has_day_without_birds?([1]) == false
assert BirdCount.has_day_without_birds?([6, 7, 10, 2, 5]) == false
assert BirdCount.has_day_without_birds?([0]) == true
assert BirdCount.has_day_without_birds?([4, 4, 0, 1]) == true
assert BirdCount.has_day_without_birds?([0, 0, 3, 0, 5, 6, 0]) == true
assert BirdCount.total([]) == 0
assert BirdCount.total([4]) == 4
assert BirdCount.total([3, 0, 0, 4, 4, 0, 0, 10]) == 21
assert BirdCount.busy_days([]) == 0
assert BirdCount.busy_days([1]) == 0
assert BirdCount.busy_days([0, 5]) == 1
assert BirdCount.busy_days([0, 6, 10, 4, 4, 5, 0]) == 3

:passed

High Score

https://exercism.org/tracks/elixir/exercises/high-score

defmodule HighScore do
  @moduledoc """

  ## Examples

      iex> scores = HighScore.new()
      iex> scores = HighScore.add_player(scores, "José Valim")
      iex> scores = HighScore.add_player(scores, "Chris McCord")
      %{"Chris McCord" => 0, "José Valim" => 0}
      iex> scores = HighScore.add_player(scores, "Dave Thomas", 2_374)
      %{"Chris McCord" => 0, "José Valim" => 0, "Dave Thomas" => 2_374}
      iex> HighScore.remove_player(scores, "José Valim")
      %{"Chris McCord" => 0, "Dave Thomas" => 2_374}

  """

  @type player_name :: String.t()
  @type score :: non_neg_integer()
  @type score_map :: %{player_name() => score()}

  @initial_score 0

  @doc """
  Returns a new high score map.
  """
  @spec new() :: score_map()
  def new(), do: %{}

  @doc """
  Adds a players to the high score map.
  """
  @spec add_player(score_map(), player_name(), score()) :: score_map()
  def add_player(scores, name, score \\ @initial_score)

  def add_player(scores, name, score) when is_integer(score) and score >= 0,
    do: Map.put_new(scores, name, score)

  @doc """
  Removes a player from the high score map.
  """
  @spec remove_player(score_map(), player_name()) :: score_map()
  def remove_player(scores, name), do: Map.delete(scores, name)

  @doc """
  Resets a player's score to #{@initial_score} in the high score map.
  """
  @spec reset_score(score_map(), player_name()) :: score_map()
  def reset_score(scores, name), do: Map.put(scores, name, @initial_score)

  @doc """
  Updates a player's score.
  """
  @spec update_score(score_map(), player_name(), score()) :: score_map()
  def update_score(scores, name, score) when is_integer(score) and score >= 0,
    do: Map.update(scores, name, score, &amp;(score + &amp;1))

  @doc """
  Returns a list of players.
  """
  @spec get_players(score_map()) :: list(player_name())
  def get_players(scores), do: Map.keys(scores)
end

High Score: Tests

https://github.com/exercism/elixir/blob/main/exercises/concept/high-score/test/high_score_test.exs

assert HighScore.new() == %{}

scores = HighScore.new()
assert HighScore.add_player(scores, "José Valim") == %{"José Valim" => 0}

scores =
  HighScore.new()
  |> HighScore.add_player("José Valim")
  |> HighScore.add_player("Chris McCord")

assert scores == %{"Chris McCord" => 0, "José Valim" => 0}

scores =
  HighScore.new()
  |> HighScore.add_player("José Valim", 486_373)

assert scores == %{"José Valim" => 486_373}

scores =
  HighScore.new()
  |> HighScore.add_player("José Valim", 486_373)
  |> HighScore.add_player("Dave Thomas", 2_374)

assert scores == %{"José Valim" => 486_373, "Dave Thomas" => 2_374}

scores =
  HighScore.new()
  |> HighScore.remove_player("José Valim")

assert scores == %{}

map =
  HighScore.new()
  |> HighScore.add_player("José Valim")
  |> HighScore.remove_player("José Valim")

assert map == %{}

scores =
  HighScore.new()
  |> HighScore.add_player("José Valim")
  |> HighScore.add_player("Chris McCord")
  |> HighScore.remove_player("José Valim")

assert scores == %{"Chris McCord" => 0}

scores =
  HighScore.new()
  |> HighScore.add_player("José Valim")
  |> HighScore.add_player("Chris McCord")
  |> HighScore.remove_player("Chris McCord")

assert scores == %{"José Valim" => 0}

scores =
  HighScore.new()
  |> HighScore.reset_score("José Valim")

assert scores == %{"José Valim" => 0}

scores =
  HighScore.new()
  |> HighScore.add_player("José Valim", 486_373)
  |> HighScore.reset_score("José Valim")

assert scores == %{"José Valim" => 0}

scores =
  HighScore.new()
  |> HighScore.update_score("José Valim", 486_373)

assert scores == %{"José Valim" => 486_373}

scores =
  HighScore.new()
  |> HighScore.add_player("José Valim")
  |> HighScore.update_score("José Valim", 486_373)

assert scores == %{"José Valim" => 486_373}

scores =
  HighScore.new()
  |> HighScore.add_player("José Valim")
  |> HighScore.update_score("José Valim", 1)
  |> HighScore.update_score("José Valim", 486_373)

assert scores == %{"José Valim" => 486_374}

scores_by_player =
  HighScore.new()
  |> HighScore.get_players()

assert scores_by_player == []

players =
  HighScore.new()
  |> HighScore.add_player("José Valim")
  |> HighScore.update_score("José Valim", 486_373)
  |> HighScore.get_players()

assert players == ["José Valim"]

players =
  HighScore.new()
  |> HighScore.add_player("José Valim", 486_373)
  |> HighScore.add_player("Dave Thomas", 2_374)
  |> HighScore.add_player("Chris McCord", 0)
  |> HighScore.add_player("Saša Jurić", 762)
  |> HighScore.get_players()
  |> Enum.sort()

assert players == [
         "Chris McCord",
         "Dave Thomas",
         "José Valim",
         "Saša Jurić"
       ]

:passed

City Office

https://exercism.org/tracks/elixir/exercises/city-office

defmodule Form do
  @moduledoc """
  A collection of loosely related functions helpful for filling out
  various forms at the city office.

  ## Examples

      iex> Form.blanks(10)
      "XXXXXXXXXX"

      iex> Form.letters("word")
      ["W", "O", "R", "D"]

      iex> Form.check_length("word", 4)
      :ok

  """

  @type address_map :: %{street: String.t(), postal_code: String.t(), city: String.t()}
  @type address_tuple :: {street :: String.t(), postal_code :: String.t(), city :: String.t()}
  @type address :: address_map() | address_tuple()

  @doc """
  Generates a string of a given length.

  This string can be used to fill out a form field that is supposed to have no value.
  Such fields cannot be left empty because a malicious third party could fill them
  out with false data.
  """
  @spec blanks(n :: non_neg_integer()) :: String.t()
  def blanks(n) do
    String.duplicate("X", n)
  end

  @doc """
  Splits the string into a list of uppercase letters.

  This is needed for form fields that don't offer a single input for the whole
  string, but instead require splitting the string into a predefined number of
  single-letter inputs.
  """
  @spec letters(word :: String.t()) :: list(String.t())
  def letters(word) do
    word
    |> String.upcase()
    |> String.split("", trim: true)
  end

  @doc """
  Checks if the value has no more than the maximum allowed number of letters.

  This is needed to check that the values of fields do not exceed the maximum
  allowed length. It also tells you by how much the value exceeds the maximum.
  """
  @spec check_length(word :: String.t(), length :: non_neg_integer()) ::
          :ok | {:error, pos_integer()}
  def check_length(word, length) do
    diff = String.length(word) - length

    if diff <= 0 do
      :ok
    else
      {:error, diff}
    end
  end

  @doc """
  Formats the address as an uppercase multiline string.
  """
  @spec format_address(address()) :: String.t()
  def format_address(%{street: street, postal_code: postal_code, city: city}) do
    format_address({street, postal_code, city})
  end

  def format_address({street, postal_code, city}) do
    """
    #{String.upcase(street)}
    #{String.upcase(postal_code)} #{String.upcase(city)}
    """
  end
end

German Sysadmin

https://exercism.org/tracks/elixir/exercises/german-sysadmin

defmodule Username do
  @moduledoc """

  ## Examples

      iex> Username.sanitize('krüger')
      'krueger'

  """

  defguardp is_lowercase_latin(letter) when letter >= ?a and letter <= ?z

  @doc """
  Sanitises usernames by removing everything but lowercase letters, underscores,
  and German characters which are being replaced by latin sustitutions.
  """
  @spec sanitize(charlist()) :: charlist()
  def sanitize(username), do: do_sanitize(username)

  defp do_sanitize([letter | tail]) do
    letter =
      case letter do
         -> 'ae'
         -> 'oe'
         -> 'ue'
         -> 'ss'
        ?_ -> '_'
        letter when is_lowercase_latin(letter) -> [letter]
        _ -> ''
      end

    letter ++ do_sanitize(tail)
  end

  defp do_sanitize([]), do: ''
end

German Sysadmin: Tests

https://github.com/exercism/elixir/blob/main/exercises/concept/german-sysadmin/test/username_test.exs

assert Username.sanitize('') == ''
assert Username.sanitize('anne') == 'anne'
lowercase_latin_letters = 'abcdefghijklmnopqrstuvwxyz'
assert Username.sanitize(lowercase_latin_letters) == lowercase_latin_letters
assert Username.sanitize('schmidt1985') == 'schmidt'
assert Username.sanitize('*fritz*!$%') == 'fritz'
assert Username.sanitize(' olaf ') == 'olaf'
allowed_characters = 'abcdefghijklmnopqrstuvwxyz_ßäöü'
input = Enum.to_list(0..0x10FFFF) -- allowed_characters
assert Username.sanitize(input) == ''
assert Username.sanitize('marcel_huber') == 'marcel_huber'
assert Username.sanitize('krüger') == 'krueger'
assert Username.sanitize('köhler') == 'koehler'
assert Username.sanitize('jäger') == 'jaeger'
assert Username.sanitize('groß') == 'gross'

:passed

Date Parser

https://exercism.org/tracks/elixir/exercises/date-parser

defmodule DateParser do
  @moduledoc """
  Module provides a set of functions to parse various date formats.

  ## Examples

      iex> DateParser.match_numeric_date()
      ...> |> Regex.named_captures("01/02/1970")
      %{"year" => "1970", "month" => "02", "day" => "01"}

      iex> DateParser.match_month_name_date()
      ...> |> Regex.named_captures("January 1, 1970")
      %{"year" => "1970", "month_name" => "January", "day" => "1"}

      iex> DateParser.match_day_month_name_date()
      ...> |> Regex.named_captures("Thursday, January 1, 1970")
      %{"year" => "1970", "month_name" => "January", "day" => "1", "day_name" => "Thursday"}

  """

  @day_names ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"]
  @month_names [
    "January",
    "February",
    "March",
    "April",
    "May",
    "June",
    "July",
    "August",
    "September",
    "October",
    "November",
    "December"
  ]

  @doc """
  Returns a regex string which matches a day number.
  """
  @spec day() :: String.t()
  def day(), do: "\\d{1,2}"

  @doc """
  Returns a regex string which matches a month number.
  """
  @spec month() :: String.t()
  def month(), do: "\\d{1,2}"

  @doc """
  Returns a regex string which matches a year number.
  """
  @spec year() :: String.t()
  def year(), do: "\\d{4}"

  @doc """
  Returns a regex string which matches a day name.
  """
  @spec day_names() :: String.t()
  def day_names(), do: "(" <> Enum.join(@day_names, "|") <> ")"

  @doc """
  Returns a regex string which matches a month name.
  """
  @spec month_names() :: String.t()
  def month_names(), do: "(" <> Enum.join(@month_names, "|") <> ")"

  @doc """
  Returns a string pattern which captures the day number.
  """
  @spec capture_day() :: String.t()
  def capture_day(), do: "(?P#{day()})"

  @doc """
  Returns a string pattern which captures the month number.
  """
  @spec capture_month() :: String.t()
  def capture_month(), do: "(?P#{month()})"

  @doc """
  Returns a string pattern which captures the year number.
  """
  @spec capture_year() :: String.t()
  def capture_year(), do: "(?P#{year()})"

  @doc """
  Returns a string pattern which captures the day name.
  """
  @spec capture_day_name() :: String.t()
  def capture_day_name(), do: "(?P#{day_names()})"

  @doc """
  Returns a string pattern which captures the month name.
  """
  @spec capture_month_name() :: String.t()
  def capture_month_name(), do: "(?P#{month_names()})"

  @doc """
  Returns a string pattern which captures numeric date format.
  """
  @spec capture_numeric_date() :: String.t()
  def capture_numeric_date(),
    do: "#{capture_day()}/#{capture_month()}/#{capture_year()}"

  @doc """
  Returns a string pattern which captures month name date format.
  """
  @spec capture_month_name_date() :: String.t()
  def capture_month_name_date(),
    do: "#{capture_month_name()} #{capture_day()}, #{capture_year()}"

  @doc """
  Returns a string pattern which captures day month name date format.
  """
  @spec capture_day_month_name_date() :: String.t()
  def capture_day_month_name_date(),
    do: "#{capture_day_name()}, #{capture_month_name()} #{capture_day()}, #{capture_year()}"

  @doc """
  Returns a compiled regular expression that only matches the numeric date format,
  and which can also capture the date's components.
  """
  @spec match_numeric_date() :: Regex.t()
  def match_numeric_date(), do: ~r/^#{capture_numeric_date()}$/

  @doc """
  Returns a compiled regular expression that only matches the month name
  date format, and which can also capture the date's components.
  """
  @spec match_month_name_date() :: Regex.t()
  def match_month_name_date(), do: ~r/^#{capture_month_name_date()}$/

  @doc """
  Returns a compiled regular expression that only matches the day month name
  date format, and which can also capture the date's components.
  """
  @spec match_day_month_name_date() :: Regex.t()
  def match_day_month_name_date(), do: ~r/^#{capture_day_month_name_date()}$/
end

Date Parser: Tests

https://github.com/exercism/elixir/blob/main/exercises/concept/date-parser/test/date_parser_test.exs

assert match?(%Regex{}, DateParser.match_numeric_date())

assert DateParser.match_numeric_date() |> Regex.match?("01/02/1970")

assert %{"year" => "1970", "month" => "02", "day" => "01"} =
         DateParser.match_numeric_date()
         |> Regex.named_captures("01/02/1970")

refute DateParser.match_numeric_date() |> Regex.match?("The day was 01/02/1970")

refute DateParser.match_numeric_date() |> Regex.match?("01/02/1970 was the day")

assert match?(%Regex{}, DateParser.match_month_name_date())

assert DateParser.match_month_name_date() |> Regex.match?("January 1, 1970")

assert %{"year" => "1970", "month_name" => "January", "day" => "1"} =
         DateParser.match_month_name_date()
         |> Regex.named_captures("January 1, 1970")

refute DateParser.match_month_name_date() |> Regex.match?("The day was January 1, 1970")

refute DateParser.match_month_name_date() |> Regex.match?("January 1, 1970 was the day")

assert match?(%Regex{}, DateParser.match_day_month_name_date())

assert DateParser.match_day_month_name_date() |> Regex.match?("Thursday, January 1, 1970")

assert %{
         "year" => "1970",
         "month_name" => "January",
         "day" => "1",
         "day_name" => "Thursday"
       } =
         DateParser.match_day_month_name_date()
         |> Regex.named_captures("Thursday, January 1, 1970")

refute DateParser.match_day_month_name_date()
       |> Regex.match?("The day way Thursday, January 1, 1970")

refute DateParser.match_day_month_name_date()
       |> Regex.match?("Thursday, January 1, 1970 was the day")

:passed

RPG Character Set

https://exercism.org/tracks/elixir/exercises/rpg-character-sheet

defmodule RPG.CharacterSheet do
  @moduledoc """

  ## Examples

      iex> RPG.CharacterSheet.welcome()
      :ok

  """

  @spec welcome() :: :ok
  def welcome() do
    IO.puts("Welcome! Let's fill out your character sheet together.")
  end

  @spec ask_name() :: String.t()
  def ask_name() do
    name = IO.gets("What is your character's name?\n")
    String.trim(name)
  end

  @spec ask_class() :: String.t()
  def ask_class() do
    class = IO.gets("What is your character's class?\n")
    String.trim(class)
  end

  @spec ask_level() :: integer()
  def ask_level() do
    level = IO.gets("What is your character's level?\n")
    {level, _} = Integer.parse(level)
    level
  end

  @spec run() :: map()
  def run() do
    welcome()

    name = ask_name()
    class = ask_class()
    level = ask_level()

    %{name: name, class: class, level: level}
    |> IO.inspect(label: "Your character")
  end
end

RPG Character Set: Tests

https://github.com/exercism/elixir/blob/main/exercises/concept/rpg-character-sheet/test/rpg/character_sheet_test.exs

io =
  capture_io(fn ->
    assert RPG.CharacterSheet.welcome() == :ok
  end)

assert io == "Welcome! Let's fill out your character sheet together.\n"

io =
  capture_io("\n", fn ->
    RPG.CharacterSheet.ask_name()
  end)

assert io == "What is your character's name?\n"

capture_io("Maxwell The Great\n", fn ->
  assert RPG.CharacterSheet.ask_name() == "Maxwell The Great"
end)

io =
  capture_io("\n", fn ->
    RPG.CharacterSheet.ask_class()
  end)

assert io == "What is your character's class?\n"

capture_io("rogue\n", fn ->
  assert RPG.CharacterSheet.ask_class() == "rogue"
end)

io =
  capture_io("1\n", fn ->
    RPG.CharacterSheet.ask_level()
  end)

assert io == "What is your character's level?\n"

capture_io("3\n", fn ->
  assert RPG.CharacterSheet.ask_level() == 3
end)

io =
  capture_io("Susan The Fearless\nfighter\n6\n", fn ->
    RPG.CharacterSheet.run()
  end)

assert io =~ """
       Welcome! Let's fill out your character sheet together.
       What is your character's name?
       What is your character's class?
       What is your character's level?
       """

capture_io("The Stranger\nrogue\n2\n", fn ->
  assert RPG.CharacterSheet.run() == %{
           name: "The Stranger",
           class: "rogue",
           level: 2
         }
end)

io =
  capture_io("Anne\nhealer\n4\n", fn ->
    RPG.CharacterSheet.run()
  end)

assert io =~
         "\nYour character: " <>
           inspect(%{
             name: "Anne",
             class: "healer",
             level: 4
           })

:passed

Name Badge

https://exercism.org/tracks/elixir/exercises/name-badge

defmodule NameBadge do
  @doc """
  Prints a badge for an employee.

  ## Examples

      iex> NameBadge.print(3, "Marie", "Sales")
      "[3] - Marie - SALES"

  """
  @spec print(id :: pos_integer() | nil, name :: String.t(), department :: String.t() | nil) ::
          String.t()
  def print(id, name, department) do
    department = if department == nil, do: "OWNER", else: department
    prefix = if id == nil, do: "", else: "[#{id}] - "

    prefix <> "#{name} - #{String.upcase(department)}"
  end
end

Name Badge: Tests

https://github.com/exercism/elixir/blob/main/exercises/concept/name-badge/test/name_badge_test.exs

assert NameBadge.print(455, "Mary M. Brown", "MARKETING") ==
         "[455] - Mary M. Brown - MARKETING"

assert NameBadge.print(89, "Jack McGregor", "Procurement") ==
         "[89] - Jack McGregor - PROCUREMENT"

assert NameBadge.print(nil, "Barbara White", "Security") == "Barbara White - SECURITY"
assert NameBadge.print(1, "Anna Johnson", nil) == "[1] - Anna Johnson - OWNER"
assert NameBadge.print(nil, "Stephen Dann", nil) == "Stephen Dann - OWNER"

:passed

Take-A-Number

https://exercism.org/tracks/elixir/exercises/take-a-number

defmodule TakeANumber do
  @moduledoc """

  ## Examples

      iex> pid = TakeANumber.start()
      iex> send(pid, {:take_a_number, self()})
      iex> send(pid, {:report_state, self()})

  """

  @initial_state 0

  @doc """
  Starts a process with a Take-A-Number machine running in it.
  """
  @spec start() :: pid()
  def start() do
    spawn(fn -> loop(@initial_state) end)
  end

  defp loop(state) do
    receive do
      {:report_state, sender} ->
        send(sender, state)
        loop(state)

      {:take_a_number, sender} ->
        state = state + 1
        send(sender, state)
        loop(state)

      :stop ->
        nil

      _ ->
        loop(state)
    end
  end
end

Take-A-Number: Tests

https://github.com/exercism/elixir/blob/main/exercises/concept/take-a-number/test/take_a_number_test.exs

pid = TakeANumber.start()
assert is_pid(pid)
assert pid != self()
assert pid != TakeANumber.start()

pid = TakeANumber.start()
send(pid, {:report_state, self()})
assert_receive 0

pid = TakeANumber.start()
send(pid, {:report_state, self()})
assert_receive 0

send(pid, {:report_state, self()})
assert_receive 0

pid = TakeANumber.start()
send(pid, {:take_a_number, self()})
assert_receive 1

pid = TakeANumber.start()
send(pid, {:take_a_number, self()})
assert_receive 1

send(pid, {:take_a_number, self()})
assert_receive 2

send(pid, {:take_a_number, self()})
assert_receive 3

send(pid, {:report_state, self()})
assert_receive 3

send(pid, {:take_a_number, self()})
assert_receive 4

send(pid, {:take_a_number, self()})
assert_receive 5

send(pid, {:report_state, self()})
assert_receive 5

pid = TakeANumber.start()
assert Process.alive?(pid)
send(pid, {:report_state, self()})
assert_receive 0

send(pid, :stop)
send(pid, {:report_state, self()})
refute_receive 0
refute Process.alive?(pid)

pid = TakeANumber.start()

send(pid, :hello?)
send(pid, "I want to speak with the manager")

send(pid, {:take_a_number, self()})
assert_receive 1

send(pid, {:report_state, self()})
assert_receive 1

# This is necessary because `Process.info/1` is not guaranteed to return up-to-date info immediately.
dirty_hacky_delay_to_ensure_up_to_date_process_info = 200
:timer.sleep(dirty_hacky_delay_to_ensure_up_to_date_process_info)

# Do not use `Process.info/1` in your own code.
# It's meant for debugging purposes only.
# We use it here for didactic purposes because there is no alternative that would achieve the same result.
assert Keyword.get(Process.info(pid), :message_queue_len) == 0

:passed

Wine Cellar

https://exercism.org/tracks/elixir/exercises/wine-cellar

defmodule WineCellar do
  @moduledoc """
  This module contains a collection of functions which simplifies wine selection
  process allowing customers filter wines by thier preferences.

  ## Examples

      iex> cellar = [
      ...>     white: {"Chardonnay", 2015, "Italy"},
      ...>     white: {"Chardonnay", 2014, "France"},
      ...>     rose: {"Dornfelder", 2018, "Germany"},
      ...>     red: {"Merlot", 2015, "France"},
      ...>     white: {"Riesling ", 2017, "Germany"},
      ...>     white: {"Pinot grigio", 2015, "Germany"},
      ...>     red: {"Pinot noir", 2016, "France"},
      ...>     red: {"Pinot noir", 2013, "Italy"}
      ...> ]
      iex> WineCellar.filter(cellar, :rose)
      [{"Dornfelder", 2018, "Germany"}]
      iex> WineCellar.filter(cellar, :white, year: 2015, country: "Germany")
      [{"Pinot grigio", 2015, "Germany"}]

  """

  @type wine :: {name :: String.t(), year :: pos_integer(), country :: String.t()}
  @type cellar :: keyword(wine)

  @doc """
  Returns a keyword list with wine colors as keys and explanations as values.
  """
  @spec explain_colors() :: Keyword.t()
  def explain_colors do
    [
      white: "Fermented without skin contact.",
      red: "Fermented with skin contact using dark-colored grapes.",
      rose: "Fermented with some skin contact, but not enough to qualify as a red wine."
    ]
  end

  @doc """
  Takes a keyword list of wines, a color atom and a keyword list of options.
  Returns a list of wines of a given color, year, and country (if specified).
  """
  @spec filter(cellar :: cellar(), color :: atom(), keyword()) :: cellar()
  def filter(cellar, color, opts \\ [])
  def filter(cellar, color, []), do: Keyword.get_values(cellar, color)

  def filter(cellar, color, opts) do
    year = Keyword.get(opts, :year)
    country = Keyword.get(opts, :country)

    filter(cellar, color)
    |> then(&amp;if year, do: filter_by_year(&amp;1, year), else: &amp;1)
    |> then(&amp;if country, do: filter_by_country(&amp;1, country), else: &amp;1)
  end

  # The functions below do not need to be modified.

  defp filter_by_year(wines, year)
  defp filter_by_year([], _year), do: []

  defp filter_by_year([{_name, year, _country} = wine | tail], year),
    do: [wine | filter_by_year(tail, year)]

  defp filter_by_year([{_name, _year, _country} | tail], year),
    do: filter_by_year(tail, year)

  defp filter_by_country(wines, country)
  defp filter_by_country([], _country), do: []

  defp filter_by_country([{_name, _year, country} = wine | tail], country),
    do: [wine | filter_by_country(tail, country)]

  defp filter_by_country([{_name, _year, _country} | tail], country),
    do: filter_by_country(tail, country)
end

Wine Cellar: Tests

https://github.com/exercism/elixir/blob/main/exercises/concept/wine-cellar/test/wine_cellar_test.exs

import ExUnit.Assertions

assert WineCellar.explain_colors() == [
         white: "Fermented without skin contact.",
         red: "Fermented with skin contact using dark-colored grapes.",
         rose: "Fermented with some skin contact, but not enough to qualify as a red wine."
       ]

assert WineCellar.filter([], :rose) == []

cellar = [
  white: {"Chardonnay", 2015, "Italy"},
  white: {"Chardonnay", 2014, "France"},
  rose: {"Dornfelder", 2018, "Germany"},
  red: {"Merlot", 2015, "France"},
  white: {"Riesling ", 2017, "Germany"},
  white: {"Pinot grigio", 2015, "Germany"},
  red: {"Pinot noir", 2016, "France"},
  red: {"Pinot noir", 2013, "Italy"}
]

assert WineCellar.filter(cellar, :white) == [
         {"Chardonnay", 2015, "Italy"},
         {"Chardonnay", 2014, "France"},
         {"Riesling ", 2017, "Germany"},
         {"Pinot grigio", 2015, "Germany"}
       ]

assert WineCellar.filter(cellar, :rose) == [{"Dornfelder", 2018, "Germany"}]

cellar = [
  white: {"Chardonnay", 2015, "Italy"},
  white: {"Chardonnay", 2014, "France"},
  rose: {"Dornfelder", 2018, "Germany"},
  red: {"Merlot", 2015, "France"},
  white: {"Riesling ", 2017, "Germany"},
  white: {"Pinot grigio", 2015, "Germany"},
  red: {"Pinot noir", 2016, "France"},
  red: {"Pinot noir", 2013, "Italy"}
]

assert WineCellar.filter(cellar, :white, year: 2015) == [
         {"Chardonnay", 2015, "Italy"},
         {"Pinot grigio", 2015, "Germany"}
       ]

cellar = [
  white: {"Chardonnay", 2015, "Italy"},
  white: {"Chardonnay", 2014, "France"},
  rose: {"Dornfelder", 2018, "Germany"},
  red: {"Merlot", 2015, "France"},
  white: {"Riesling ", 2017, "Germany"},
  white: {"Pinot grigio", 2015, "Germany"},
  red: {"Pinot noir", 2016, "France"},
  red: {"Pinot noir", 2013, "Italy"}
]

assert WineCellar.filter(cellar, :red, country: "France") == [
         {"Merlot", 2015, "France"},
         {"Pinot noir", 2016, "France"}
       ]

cellar = [
  white: {"Chardonnay", 2015, "Italy"},
  white: {"Chardonnay", 2014, "France"},
  rose: {"Dornfelder", 2018, "Germany"},
  red: {"Merlot", 2015, "France"},
  white: {"Riesling ", 2017, "Germany"},
  white: {"Pinot grigio", 2015, "Germany"},
  red: {"Pinot noir", 2016, "France"},
  red: {"Pinot noir", 2013, "Italy"}
]

assert WineCellar.filter(cellar, :white, year: 2015, country: "Germany") == [
         {"Pinot grigio", 2015, "Germany"}
       ]

cellar = [
  white: {"Chardonnay", 2015, "Italy"},
  white: {"Chardonnay", 2014, "France"},
  rose: {"Dornfelder", 2018, "Germany"},
  red: {"Merlot", 2015, "France"},
  white: {"Riesling ", 2017, "Germany"},
  white: {"Pinot grigio", 2015, "Germany"},
  red: {"Pinot noir", 2016, "France"},
  red: {"Pinot noir", 2013, "Italy"}
]

assert WineCellar.filter(cellar, :red, country: "France", year: 2015) == [
         {"Merlot", 2015, "France"}
       ]

:passed

Paint By Number

https://exercism.org/tracks/elixir/exercises/paint-by-number

defmodule PaintByNumber do
  @moduledoc """

  ## Examples

      iex> PaintByNumber.palette_bit_size(50)
      6

      iex> PaintByNumber.get_first_pixel(<<1::2, 0::2, 0::2, 2::2>>, 3)
      1

  """

  @spec palette_bit_size(pos_integer()) :: pos_integer()
  def palette_bit_size(color_count) do
    do_palette_bit_size(1, color_count)
  end

  defp do_palette_bit_size(n, color_count) do
    case Integer.pow(2, n) < color_count do
      true -> do_palette_bit_size(n + 1, color_count)
      false -> n
    end
  end

  @spec empty_picture() :: bitstring()
  def empty_picture() do
    <<>>
  end

  @spec test_picture() :: bitstring()
  def test_picture() do
    <<0b0::2, 0b1::2, 0b10::2, 0b11::2>>
  end

  @spec prepend_pixel(bitstring(), pos_integer(), pos_integer()) :: bitstring()
  def prepend_pixel(picture, color_count, pixel_color_index) do
    palette = palette_bit_size(color_count)
    <>
  end

  @spec get_first_pixel(bitstring(), pos_integer()) :: pos_integer()
  def get_first_pixel(<<>>, _color_count), do: nil

  def get_first_pixel(picture, color_count) do
    palette = palette_bit_size(color_count)
    <> = picture
    first_pixel
  end

  @spec drop_first_pixel(bitstring(), pos_integer()) :: bitstring()
  def drop_first_pixel(<<>>, _color_count), do: <<>>

  def drop_first_pixel(picture, color_count) do
    palette = palette_bit_size(color_count)
    <<_first_pixel::size(palette), rest::bitstring>> = picture
    rest
  end

  @spec concat_pictures(bitstring(), bitstring()) :: bitstring()
  def concat_pictures(picture1, picture2) do
    <>
  end
end

Paint By Number: Tests

https://github.com/exercism/elixir/blob/main/exercises/concept/paint-by-number/test/paint_by_number_test.exs

assert PaintByNumber.palette_bit_size(2) == 1
assert PaintByNumber.palette_bit_size(3) == 2
assert PaintByNumber.palette_bit_size(4) == 2
assert PaintByNumber.palette_bit_size(7) == 3
assert PaintByNumber.palette_bit_size(8) == 3
assert PaintByNumber.palette_bit_size(9) == 4
assert PaintByNumber.palette_bit_size(14) == 4
assert PaintByNumber.palette_bit_size(50) == 6
assert PaintByNumber.palette_bit_size(1_000_000) == 20

assert PaintByNumber.empty_picture() == <<>>

assert PaintByNumber.test_picture() == <<0::2, 1::2, 2::2, 3::2>>

picture = <<>>
color_count = 16
pixel_color_index = 1
assert PaintByNumber.prepend_pixel(picture, color_count, pixel_color_index) == <<1::4>>

picture = <<3::3, 2::3, 2::3>>
color_count = 7
pixel_color_index = 0

assert PaintByNumber.prepend_pixel(picture, color_count, pixel_color_index) ==
         <<0::3, 3::3, 2::3, 2::3>>

picture = <<3::6>>
color_count = 64
pixel_color_index = 64

assert PaintByNumber.prepend_pixel(picture, color_count, pixel_color_index) ==
         <<0::6, 3::6>>

picture = <<>>
color_count = 16
assert PaintByNumber.get_first_pixel(picture, color_count) == nil

picture = <<1::2, 0::2, 0::2, 2::2>>
color_count = 3
assert PaintByNumber.get_first_pixel(picture, color_count) == 1

picture = <<0b01::2, 0b10::2, 0b00::2, 0b10::2>>
# Color count of 8 means 3 bits.
color_count = 8
# We take bits from segments until we have 3 bits.
# First, we take `01` from the first segment. Then, `1` from the second segment.
# This gives us the binary number `011`, which is equal to the decimal number 5.
assert PaintByNumber.get_first_pixel(picture, color_count) == 0b011

picture = <<>>
color_count = 5
assert PaintByNumber.drop_first_pixel(picture, color_count) == <<>>

picture = <<23::5, 21::5, 15::5, 3::5>>
color_count = 32
assert PaintByNumber.drop_first_pixel(picture, color_count) == <<21::5, 15::5, 3::5>>

picture = <<0b011011::6, 0b110001::6>>
# Color count of 4 means 2 bits.
color_count = 4
# We remove the first 2 bits from the first segment.
assert PaintByNumber.drop_first_pixel(picture, color_count) == <<0b1011::4, 0b110001::6>>

picture1 = <<>>
picture2 = <<>>
assert PaintByNumber.concat_pictures(picture1, picture2) == <<>>

picture1 = <<5::3, 2::3, 2::3, 4::3>>
picture2 = <<>>
assert PaintByNumber.concat_pictures(picture1, picture2) == picture1

picture1 = <<>>
picture2 = <<13::4, 11::4, 0::4>>
assert PaintByNumber.concat_pictures(picture1, picture2) == picture2

picture1 = <<2::4, 2::4, 1::4, 14::4>>
picture2 = <<15::4, 14::4>>

assert PaintByNumber.concat_pictures(picture1, picture2) ==
         <<2::4, 2::4, 1::4, 14::4, 15::4, 14::4>>

picture1 = <<0b00::2, 0b01::2, 0b11::2, 0b01::2>>
picture2 = <<0b10101::5, 0b10011::5>>

assert PaintByNumber.concat_pictures(picture1, picture2) ==
         <<0b00011101::8, 0b10101100::8, 0b11::2>>

:passed

DNA Encoding

https://exercism.org/tracks/elixir/exercises/dna-encoding

defmodule DNA do
  @moduledoc """

  ## Examples

      iex> DNA.encode('A')
      <<0b0001::4>>

      iex> DNA.encode('TGCA ')
      <<0b1000::4, 0b0100::4, 0b0010::4, 0b0001::4, 0b0000::4>>

      iex> DNA.decode(<<0b1000::4, 0b0100::4, 0b0010::4, 0b0001::4, 0b0000::4>>)
      'TGCA '

  """

  @type code_point :: ?A | ?C | ?G | ?T
  @type encoded_code :: non_neg_integer()

  @doc """
  Encoded nucleic acid code point to binary value.
  """
  @spec encode_nucleotide(code_point :: code_point()) :: encoded_code()
  def encode_nucleotide(?\s), do: 0b0000
  def encode_nucleotide(?A), do: 0b0001
  def encode_nucleotide(?C), do: 0b0010
  def encode_nucleotide(?G), do: 0b0100
  def encode_nucleotide(?T), do: 0b1000

  @doc """
  Decodes binary encoded nucleic acid to a code point.
  """
  @spec decode_nucleotide(encoded_code :: encoded_code()) :: code_point()
  def decode_nucleotide(0b0000), do: ?\s
  def decode_nucleotide(0b0001), do: ?A
  def decode_nucleotide(0b0010), do: ?C
  def decode_nucleotide(0b0100), do: ?G
  def decode_nucleotide(0b1000), do: ?T

  @doc """
  Encode a DNA charlist.
  """
  @spec encode(dna :: charlist()) :: bitstring()
  def encode(dna), do: do_encode(dna, <<>>)

  defp do_encode([], acc), do: acc

  defp do_encode([code_point | tail], acc),
    do: do_encode(tail, <>)

  @doc """
  Decode a DNA bitstring.
  """
  @spec decode(dna :: bitstring()) :: charlist()
  def decode(dna), do: do_decode(dna, [])

  defp do_decode(<<>>, acc), do: acc

  defp do_decode(<>, acc),
    do: do_decode(tail, acc ++ [decode_nucleotide(code)])
end

DNA Encoding (TCO)

defmodule DNA.TCO do
  @moduledoc """

  ## Examples

      iex> DNA.TCO.encode('A')
      <<0b0001::4>>

      iex> DNA.TCO.encode('TGCA ')
      <<0b1000::4, 0b0100::4, 0b0010::4, 0b0001::4, 0b0000::4>>

      iex> DNA.TCO.decode(<<0b1000::4, 0b0100::4, 0b0010::4, 0b0001::4, 0b0000::4>>)
      'TGCA '

  """

  @type code_point :: ?A | ?C | ?G | ?T
  @type encoded_code :: non_neg_integer()

  @doc """
  Encoded nucleic acid code point to binary value.
  """

  @spec encode_nucleotide(code_point :: code_point()) :: encoded_code()
  def encode_nucleotide(?\s), do: 0b0000
  def encode_nucleotide(?A), do: 0b0001
  def encode_nucleotide(?C), do: 0b0010
  def encode_nucleotide(?G), do: 0b0100
  def encode_nucleotide(?T), do: 0b1000

  @doc """
  Decodes binary encoded nucleic acid to a code point.
  """
  @spec decode_nucleotide(encoded_code :: encoded_code()) :: code_point()
  def decode_nucleotide(0b0000), do: ?\s
  def decode_nucleotide(0b0001), do: ?A
  def decode_nucleotide(0b0010), do: ?C
  def decode_nucleotide(0b0100), do: ?G
  def decode_nucleotide(0b1000), do: ?T

  @doc """
  Encode a DNA charlist.
  """
  @spec encode(dna :: charlist()) :: bitstring()
  def encode(dna), do: do_encode(dna, <<>>)

  defp do_encode([code_point | tail], acc),
    do: do_encode(tail, <>)

  defp do_encode([], acc), do: acc

  @doc """
  Decode a DNA bitstring.
  """
  @spec decode(dna :: bitstring()) :: charlist()
  def decode(dna), do: do_decode(dna, []) |> Enum.reverse()

  defp do_decode(<>, acc),
    do: do_decode(tail, [decode_nucleotide(code) | acc])

  defp do_decode(<<>>, acc), do: acc
end

DNA Encoding: Tests

https://github.com/exercism/elixir/blob/main/exercises/concept/dna-encoding/test/dna_test.exs

for module <- [DNA, DNA.TCO] do
  assert module.encode_nucleotide(?\s) == 0b0000
  assert module.encode_nucleotide(?A) == 0b0001
  assert module.encode_nucleotide(?C) == 0b0010
  assert module.encode_nucleotide(?G) == 0b0100
  assert module.encode_nucleotide(?T) == 0b1000
  assert module.decode_nucleotide(0b0000) == ?\s
  assert module.decode_nucleotide(0b0001) == ?A
  assert module.decode_nucleotide(0b0010) == ?C
  assert module.decode_nucleotide(0b0100) == ?G
  assert module.decode_nucleotide(0b1000) == ?T
  assert module.encode(' ') == <<0b0000::4>>
  assert module.encode('A') == <<0b0001::4>>
  assert module.encode('C') == <<0b0010::4>>
  assert module.encode('G') == <<0b0100::4>>
  assert module.encode('T') == <<0b1000::4>>
  assert module.encode(' ACGT') == <<0b0000::4, 0b0001::4, 0b0010::4, 0b0100::4, 0b1000::4>>
  assert module.encode('TGCA ') == <<0b1000::4, 0b0100::4, 0b0010::4, 0b0001::4, 0b0000::4>>
  assert module.decode(<<0b0000::4>>) == ' '
  assert module.decode(<<0b0001::4>>) == 'A'
  assert module.decode(<<0b0010::4>>) == 'C'
  assert module.decode(<<0b0100::4>>) == 'G'
  assert module.decode(<<0b1000::4>>) == 'T'
  assert module.decode(<<0b0000::4, 0b0001::4, 0b0010::4, 0b0100::4, 0b1000::4>>) == ' ACGT'
  assert module.decode(<<0b1000::4, 0b0100::4, 0b0010::4, 0b0001::4, 0b0000::4>>) == 'TGCA '
end

:passed

Library Fees

https://exercism.org/tracks/elixir/exercises/library-fees

defmodule LibraryFees do
  @moduledoc """

  ## Examples

      iex> LibraryFees.calculate_late_fee("2018-11-01T09:00:00Z", "2018-11-30T14:12:00Z", 320)
      320

      iex> LibraryFees.calculate_late_fee("2019-05-01T16:12:00Z", "2019-05-30T14:32:45Z", 313)
      0

  """

  @doc """
  Parses the stored datetime strings.
  """
  @spec datetime_from_string(string :: String.t()) :: NaiveDateTime.t()
  def datetime_from_string(string), do: NaiveDateTime.from_iso8601!(string)

  @doc """
  Determines if a book was checked out before noon.
  """
  @spec before_noon?(datetime :: NaiveDateTime.t()) :: boolean()
  def before_noon?(datetime), do: datetime.hour < 12

  @doc """
  Calculates the return date.
  """
  @spec return_date(checkout_datetime :: NaiveDateTime.t()) :: Date.t()
  def return_date(checkout_datetime) do
    days =
      case before_noon?(checkout_datetime) do
        true -> 28
        false -> 29
      end

    checkout_datetime
    |> NaiveDateTime.to_date()
    |> Date.add(days)
  end

  @doc """
  Determine how late the return of the book was.
  """
  @spec days_late(planned_return_date :: Date.t(), actual_return_datetime :: NaiveDateTime.t()) ::
          non_neg_integer()
  def days_late(planned_return_date, actual_return_datetime),
    do: Date.diff(actual_return_datetime, planned_return_date) |> max(0)

  @doc """
  Determines if the book was returned on a Monday.
  """
  @spec monday?(datetime :: NaiveDateTime.t()) :: boolean()
  def monday?(datetime), do: datetime |> NaiveDateTime.to_date() |> Date.day_of_week() == 1

  @doc """
  Calculates the late fee.
  """
  @spec calculate_late_fee(
          checkout :: String.t(),
          return :: String.t(),
          rate :: non_neg_integer()
        ) :: non_neg_integer()
  def calculate_late_fee(checkout, return, rate) do
    checkout = datetime_from_string(checkout)
    actual_return = datetime_from_string(return)
    planned_return = return_date(checkout)
    fee = rate * days_late(planned_return, actual_return)

    case monday?(actual_return) do
      true -> floor(fee * 0.5)
      false -> fee
    end
  end
end

Library Fees: Tests

https://github.com/exercism/elixir/blob/main/exercises/concept/library-fees/test/library_fees_test.exs

result = LibraryFees.datetime_from_string("2021-01-01T12:00:00Z")
assert result.__struct__ == NaiveDateTime

result = LibraryFees.datetime_from_string("2019-12-24T13:15:45Z")
assert result == ~N[2019-12-24 13:15:45Z]

assert LibraryFees.before_noon?(~N[2020-06-06 11:59:59Z]) == true
assert LibraryFees.before_noon?(~N[2021-01-03 12:01:01Z]) == false
assert LibraryFees.before_noon?(~N[2018-11-17 12:00:00Z]) == false

result = LibraryFees.return_date(~N[2020-02-14 11:59:59Z])
assert result == ~D[2020-03-13]

result = LibraryFees.return_date(~N[2021-01-03 12:01:01Z])
assert result == ~D[2021-02-01]

result = LibraryFees.return_date(~N[2018-12-01 12:00:00Z])
assert result == ~D[2018-12-30]

result = LibraryFees.days_late(~D[2021-02-01], ~N[2021-02-01 12:00:00Z])
assert result == 0

result = LibraryFees.days_late(~D[2019-03-11], ~N[2019-03-11 12:00:00Z])
assert result == 0

result = LibraryFees.days_late(~D[2020-12-03], ~N[2020-11-29 16:00:00Z])
assert result == 0

result = LibraryFees.days_late(~D[2020-06-12], ~N[2020-06-21 16:00:00Z])
assert result == 9

result = LibraryFees.days_late(~D[2020-06-12], ~N[2020-06-12 23:59:59Z])
assert result == 0

result = LibraryFees.days_late(~D[2020-06-12], ~N[2020-06-13 00:00:00Z])
assert result == 1

assert LibraryFees.monday?(~N[2021-02-01 14:01:00Z]) == true
assert LibraryFees.monday?(~N[2020-03-16 09:23:52Z]) == true
assert LibraryFees.monday?(~N[2019-04-22 15:44:03Z]) == true
assert LibraryFees.monday?(~N[2021-02-02 15:07:00Z]) == false
assert LibraryFees.monday?(~N[2020-03-14 08:54:51Z]) == false
assert LibraryFees.monday?(~N[2019-04-28 11:37:12Z]) == false

result = LibraryFees.calculate_late_fee("2018-11-01T09:00:00Z", "2018-11-13T14:12:00Z", 123)
assert result == 0

result = LibraryFees.calculate_late_fee("2018-11-01T09:00:00Z", "2018-11-29T14:12:00Z", 123)
assert result == 0

result = LibraryFees.calculate_late_fee("2018-11-01T09:00:00Z", "2018-11-30T14:12:00Z", 320)
assert result == 320

result = LibraryFees.calculate_late_fee("2019-05-01T16:12:00Z", "2019-05-17T14:32:45Z", 400)
assert result == 0

result = LibraryFees.calculate_late_fee("2019-05-01T16:12:00Z", "2019-05-30T14:32:45Z", 313)
assert result == 0

result = LibraryFees.calculate_late_fee("2019-05-01T16:12:00Z", "2019-05-31T14:32:45Z", 234)
assert result == 234

result = LibraryFees.calculate_late_fee("2021-01-01T08:00:00Z", "2021-02-13T08:00:00Z", 111)
assert result == 111 * 15

result = LibraryFees.calculate_late_fee("2021-01-01T08:00:00Z", "2021-02-15T08:00:00Z", 111)
assert result == trunc(111 * 17 * 0.5)

:passed

Basketball Website

https://exercism.org/tracks/elixir/exercises/basketball-website

defmodule BasketballWebsite do
  @moduledoc """

  ## Examples

      iex> team_data = %{
      ...>     "coach" => %{},
      ...>     "team_name" => "Hoop Masters",
      ...>     "players" => %{
      ...>         "99" => %{
      ...>             "first_name" => "Amalee",
      ...>             "last_name" => "Tynemouth",
      ...>             "email" => "atynemouth0@yellowpages.com",
      ...>             "statistics" => %{}
      ...>         },
      ...>         "98" => %{
      ...>             "first_name" => "Tiffie",
      ...>             "last_name" => "Derle",
      ...>             "email" => "tderle1@vimeo.com",
      ...>             "statistics" => %{}
      ...>         }
      ...>     }
      ...> }
      iex> BasketballWebsite.extract_from_path(team_data, "players.99.first_name")
      "Amalee"
      iex> BasketballWebsite.extract_from_path(team_data, "players.98.last_name")
      "Derle"

  """

  @doc """
  Uses the Access module to traverse the structures according to the given path.
  """
  @spec extract_from_path(Access.t(), String.t()) :: term()
  def extract_from_path(data, path), do: do_extract_from_path(keys(path), data)

  defp do_extract_from_path(_path, nil), do: nil
  defp do_extract_from_path([], data), do: data

  defp do_extract_from_path([key | tail], data),
    do: do_extract_from_path(tail, data[key])

  @doc """
  Uses the Access module to traverse the structures according to the given path.
  """
  @spec get_in_path(Access.t(), String.t()) :: term()
  def get_in_path(data, path), do: get_in(data, keys(path))

  defp keys(path), do: String.split(path, ".")
end

Basketball Website: Tests

https://github.com/exercism/elixir/blob/main/exercises/concept/basketball-website/test/basketball_website_test.exs

team_data = %{
  "coach" => %{},
  "team_name" => "Hoop Masters",
  "players" => %{}
}

assert BasketballWebsite.extract_from_path(team_data, "team_name") == "Hoop Masters"

team_data = %{
  "coach" => %{
    "first_name" => "Jane",
    "last_name" => "Brown"
  },
  "team_name" => "Hoop Masters",
  "players" => %{}
}

assert BasketballWebsite.extract_from_path(team_data, "coach.first_name") == "Jane"

team_data = %{
  "coach" => %{},
  "team_name" => "Hoop Masters",
  "players" => %{
    "99" => %{
      "first_name" => "Amalee",
      "last_name" => "Tynemouth",
      "email" => "atynemouth0@yellowpages.com",
      "statistics" => %{}
    },
    "98" => %{
      "first_name" => "Tiffie",
      "last_name" => "Derle",
      "email" => "tderle1@vimeo.com",
      "statistics" => %{}
    }
  }
}

assert BasketballWebsite.extract_from_path(team_data, "players.99.first_name") == "Amalee"

team_data = %{
  "coach" => %{},
  "team_name" => "Hoop Masters",
  "players" => %{
    "42" => %{
      "first_name" => "Conchita",
      "last_name" => "Elham",
      "email" => "celham4@wikia.com",
      "statistics" => %{
        "average_points_per_game" => 4.6,
        "free_throws_made" => 7,
        "free_throws_attempted" => 10
      }
    },
    "61" => %{
      "first_name" => "Noel",
      "last_name" => "Fawlkes",
      "email" => "nfawlkes5@yahoo.co.jp",
      "statistics" => %{
        "average_points_per_game" => 5.0,
        "free_throws_made" => 5,
        "free_throws_attempted" => 5
      }
    }
  }
}

assert BasketballWebsite.extract_from_path(
         team_data,
         "players.61.statistics.average_points_per_game"
       ) === 5.0

team_data = %{
  "coach" => %{},
  "team_name" => "Hoop Masters",
  "players" => %{}
}

assert BasketballWebsite.extract_from_path(team_data, "team_song") == nil

team_data = %{
  "coach" => %{
    "first_name" => "Jane",
    "last_name" => "Brown"
  },
  "team_name" => "Hoop Masters",
  "players" => %{}
}

assert BasketballWebsite.extract_from_path(team_data, "coach.age") == nil

team_data = %{
  "coach" => %{},
  "team_name" => "Hoop Masters",
  "players" => %{
    "32" => %{
      "first_name" => "Deni",
      "last_name" => "Lidster",
      "email" => nil,
      "statistics" => %{}
    }
  }
}

assert BasketballWebsite.extract_from_path(team_data, "players.32.height") == nil

team_data = %{
  "coach" => %{},
  "team_name" => "Hoop Masters",
  "players" => %{
    "12" => %{
      "first_name" => "Andy",
      "last_name" => "Napoli",
      "email" => "anapoli7@goodreads.com",
      "statistics" => %{
        "average_points_per_game" => 7
      }
    }
  }
}

assert BasketballWebsite.extract_from_path(
         team_data,
         "players.12.statistics.personal_fouls"
       ) == nil

team_data = %{
  "coach" => %{},
  "team_name" => "Hoop Masters",
  "players" => %{}
}

assert BasketballWebsite.extract_from_path(
         team_data,
         "support_personnel.physiotherapy.first_name"
       ) == nil

team_data = %{
  "coach" => %{},
  "team_name" => "Hoop Masters",
  "players" => %{}
}

assert BasketballWebsite.get_in_path(team_data, "team_name") == "Hoop Masters"

team_data = %{
  "coach" => %{
    "first_name" => "Jane",
    "last_name" => "Brown"
  },
  "team_name" => "Hoop Masters",
  "players" => %{}
}

assert BasketballWebsite.get_in_path(team_data, "coach.first_name") == "Jane"

team_data = %{
  "coach" => %{},
  "team_name" => "Hoop Masters",
  "players" => %{
    "99" => %{
      "first_name" => "Amalee",
      "last_name" => "Tynemouth",
      "email" => "atynemouth0@yellowpages.com",
      "statistics" => %{}
    },
    "98" => %{
      "first_name" => "Tiffie",
      "last_name" => "Derle",
      "email" => "tderle1@vimeo.com",
      "statistics" => %{}
    }
  }
}

assert BasketballWebsite.get_in_path(team_data, "players.99.first_name") == "Amalee"

team_data = %{
  "coach" => %{},
  "team_name" => "Hoop Masters",
  "players" => %{
    "42" => %{
      "first_name" => "Conchita",
      "last_name" => "Elham",
      "email" => "celham4@wikia.com",
      "statistics" => %{
        "average_points_per_game" => 4.6,
        "free_throws_made" => 7,
        "free_throws_attempted" => 10
      }
    },
    "61" => %{
      "first_name" => "Noel",
      "last_name" => "Fawlkes",
      "email" => "nfawlkes5@yahoo.co.jp",
      "statistics" => %{
        "average_points_per_game" => 5.0,
        "free_throws_made" => 5,
        "free_throws_attempted" => 5
      }
    }
  }
}

assert BasketballWebsite.get_in_path(
         team_data,
         "players.61.statistics.average_points_per_game"
       ) === 5.0

team_data = %{
  "coach" => %{},
  "team_name" => "Hoop Masters",
  "players" => %{}
}

assert BasketballWebsite.get_in_path(team_data, "team_song") == nil

team_data = %{
  "coach" => %{
    "first_name" => "Jane",
    "last_name" => "Brown"
  },
  "team_name" => "Hoop Masters",
  "players" => %{}
}

assert BasketballWebsite.get_in_path(team_data, "coach.age") == nil

team_data = %{
  "coach" => %{},
  "team_name" => "Hoop Masters",
  "players" => %{
    "32" => %{
      "first_name" => "Deni",
      "last_name" => "Lidster",
      "email" => nil,
      "statistics" => %{}
    }
  }
}

assert BasketballWebsite.get_in_path(team_data, "players.32.height") == nil

team_data = %{
  "coach" => %{},
  "team_name" => "Hoop Masters",
  "players" => %{
    "12" => %{
      "first_name" => "Andy",
      "last_name" => "Napoli",
      "email" => "anapoli7@goodreads.com",
      "statistics" => %{
        "average_points_per_game" => 7
      }
    }
  }
}

assert BasketballWebsite.get_in_path(team_data, "players.12.statistics.personal_fouls") ==
         nil

team_data = %{
  "coach" => %{},
  "team_name" => "Hoop Masters",
  "players" => %{}
}

assert BasketballWebsite.get_in_path(team_data, "support_personnel.physiotherapy.first_name") ==
         nil

:passed

Boutique Inventory

https://exercism.org/tracks/elixir/exercises/boutique-inventory

defmodule BoutiqueInventory do
  @moduledoc """

  ## Examples

      iex> BoutiqueInventory.sort_by_price([
      ...>   %{price: 65, name: "Maxi Yellow Summer Dress", quantity_by_size: %{}},
      ...>   %{price: 60, name: "Cream Linen Pants", quantity_by_size: %{}},
      ...>   %{price: 33, name: "Straw Hat", quantity_by_size: %{}},
      ...>   %{price: 60, name: "Brown Linen Pants", quantity_by_size: %{}}
      ...> ])
      [
        %{price: 33, name: "Straw Hat", quantity_by_size: %{}},
        %{price: 60, name: "Cream Linen Pants", quantity_by_size: %{}},
        %{price: 60, name: "Brown Linen Pants", quantity_by_size: %{}},
        %{price: 65, name: "Maxi Yellow Summer Dress", quantity_by_size: %{}}
      ]

      iex> BoutiqueInventory.with_missing_price([
      ...>   %{name: "Red Flowery Top", price: 50, quantity_by_size: %{}},
      ...>   %{name: "Purple Flowery Top", price: nil, quantity_by_size: %{}},
      ...>   %{name: "Bamboo Socks Avocado", price: 10, quantity_by_size: %{}},
      ...>   %{name: "Bamboo Socks Palm Trees", price: 10, quantity_by_size: %{}},
      ...>   %{name: "Bamboo Socks Kittens", price: nil, quantity_by_size: %{}}
      ...> ])
      [
        %{name: "Purple Flowery Top", price: nil, quantity_by_size: %{}},
        %{name: "Bamboo Socks Kittens", price: nil, quantity_by_size: %{}}
      ]

  """

  @type item :: %{
          price: non_neg_integer() | nil,
          name: String.t(),
          quantity_by_size: map()
        }

  @type inventory :: list(item())

  @doc """
  Sorts items in the inventory by price.
  """
  @spec sort_by_price(inventory :: inventory()) :: inventory()
  def sort_by_price(inventory), do: Enum.sort_by(inventory, &amp; &amp;1.price, :asc)

  @doc """
  Findsall items in the inventory with missing prices.
  """
  @spec with_missing_price(inventory :: inventory()) :: inventory()
  def with_missing_price(inventory), do: Enum.filter(inventory, &amp;is_nil(&amp;1.price))

  @doc """
  Replace the old word with a new one across all invetory names.
  """
  @spec update_names(inventory :: inventory(), String.t(), String.t()) :: inventory()
  def update_names(inventory, old_name, new_name) do
    Enum.map(inventory, fn item ->
      cond do
        String.contains?(item.name, old_name) ->
          %{item | name: String.replace(item.name, old_name, new_name)}

        true ->
          item
      end
    end)
  end

  @doc """
  Increments the item's quantity of each size by the count.
  """
  @spec increase_quantity(item(), pos_integer()) :: item()
  def increase_quantity(item, count) do
    quantity_by_size =
      item.quantity_by_size
      |> Enum.into(%{}, fn {size, quantity} -> {size, quantity + count} end)

    %{item | quantity_by_size: quantity_by_size}
  end

  @doc """
  Calculates the item's total quantity of all sizes.
  """
  @spec total_quantity(item()) :: non_neg_integer()
  def total_quantity(item),
    do: Enum.reduce(item.quantity_by_size, 0, fn {_size, quantity}, total -> total + quantity end)
end

Boutique Inventory: Tests

https://github.com/exercism/elixir/blob/main/exercises/concept/boutique-inventory/test/boutique_inventory_test.exs

assert BoutiqueInventory.sort_by_price([]) == []

assert BoutiqueInventory.sort_by_price([
         %{price: 65, name: "Maxi Yellow Summer Dress", quantity_by_size: %{}},
         %{price: 60, name: "Cream Linen Pants", quantity_by_size: %{}},
         %{price: 33, name: "Straw Hat", quantity_by_size: %{}}
       ]) == [
         %{price: 33, name: "Straw Hat", quantity_by_size: %{}},
         %{price: 60, name: "Cream Linen Pants", quantity_by_size: %{}},
         %{price: 65, name: "Maxi Yellow Summer Dress", quantity_by_size: %{}}
       ]

assert BoutiqueInventory.sort_by_price([
         %{price: 65, name: "Maxi Yellow Summer Dress", quantity_by_size: %{}},
         %{price: 60, name: "Cream Linen Pants", quantity_by_size: %{}},
         %{price: 33, name: "Straw Hat", quantity_by_size: %{}},
         %{price: 60, name: "Brown Linen Pants", quantity_by_size: %{}}
       ]) == [
         %{price: 33, name: "Straw Hat", quantity_by_size: %{}},
         %{price: 60, name: "Cream Linen Pants", quantity_by_size: %{}},
         %{price: 60, name: "Brown Linen Pants", quantity_by_size: %{}},
         %{price: 65, name: "Maxi Yellow Summer Dress", quantity_by_size: %{}}
       ]

assert BoutiqueInventory.with_missing_price([]) == []

assert BoutiqueInventory.with_missing_price([
         %{name: "Red Flowery Top", price: 50, quantity_by_size: %{}},
         %{name: "Purple Flowery Top", price: nil, quantity_by_size: %{}},
         %{name: "Bamboo Socks Avocado", price: 10, quantity_by_size: %{}},
         %{name: "Bamboo Socks Palm Trees", price: 10, quantity_by_size: %{}},
         %{name: "Bamboo Socks Kittens", price: nil, quantity_by_size: %{}}
       ]) == [
         %{name: "Purple Flowery Top", price: nil, quantity_by_size: %{}},
         %{name: "Bamboo Socks Kittens", price: nil, quantity_by_size: %{}}
       ]

assert BoutiqueInventory.update_names([], "T-Shirt", "Tee") == []

assert BoutiqueInventory.update_names(
         [
           %{name: "Bambo Socks Avocado", price: 10, quantity_by_size: %{}},
           %{name: "3x Bambo Socks Palm Trees", price: 26, quantity_by_size: %{}},
           %{name: "Red Sequin Top", price: 87, quantity_by_size: %{}}
         ],
         "Bambo",
         "Bamboo"
       ) == [
         %{name: "Bamboo Socks Avocado", price: 10, quantity_by_size: %{}},
         %{name: "3x Bamboo Socks Palm Trees", price: 26, quantity_by_size: %{}},
         %{name: "Red Sequin Top", price: 87, quantity_by_size: %{}}
       ]

assert BoutiqueInventory.update_names(
         [
           %{name: "GO! GO! GO! Tee", price: 8, quantity_by_size: %{}}
         ],
         "GO!",
         "Go!"
       ) == [
         %{name: "Go! Go! Go! Tee", price: 8, quantity_by_size: %{}}
       ]

assert BoutiqueInventory.increase_quantity(
         %{
           name: "Long Black Evening Dress",
           price: 105,
           quantity_by_size: %{}
         },
         1
       ) == %{
         name: "Long Black Evening Dress",
         price: 105,
         quantity_by_size: %{}
       }

assert BoutiqueInventory.increase_quantity(
         %{
           name: "Green Swimming Shorts",
           price: 46,
           quantity_by_size: %{s: 1, m: 2, l: 4, xl: 1}
         },
         3
       ) == %{
         name: "Green Swimming Shorts",
         price: 46,
         quantity_by_size: %{s: 4, m: 5, l: 7, xl: 4}
       }

assert BoutiqueInventory.total_quantity(%{
         name: "Red Denim Pants",
         price: 77,
         quantity_by_size: %{}
       }) == 0

assert BoutiqueInventory.total_quantity(%{
         name: "Black Denim Skirt",
         price: 50,
         quantity_by_size: %{s: 4, m: 11, l: 6, xl: 8}
       }) == 29

:passed

File Sniffer

https://exercism.org/tracks/elixir/exercises/file-sniffer

defmodule FileSniffer do
  @moduledoc """

  ## Examples

      iex> FileSniffer.type_from_extension("png")
      "image/png"

      iex> FileSniffer.type_from_binary(<<0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A, 0xFF>>)
      "image/png"

      iex> FileSniffer.verify(<<0x42, 0x4D>>, "bmp")
      {:ok, "image/bmp"}

  """

  @elf_signature <<0x7F, 0x45, 0x4C, 0x46>>
  @bmp_signature <<0x42, 0x4D>>
  @png_signature <<0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A>>
  @jpg_signature <<0xFF, 0xD8, 0xFF>>
  @gif_signature <<0x47, 0x49, 0x46>>

  @mime_application_octet_stream "application/octet-stream"
  @mime_image_bmp "image/bmp"
  @mime_image_png "image/png"
  @mime_image_jpg "image/jpg"
  @mime_image_gif "image/gif"

  @doc """
  Takes a file extension (string) and returns the media type.
  """
  @spec type_from_extension(extension :: String.t()) :: String.t()
  def type_from_extension("exe"), do: @mime_application_octet_stream
  def type_from_extension("bmp"), do: @mime_image_bmp
  def type_from_extension("png"), do: @mime_image_png
  def type_from_extension("jpg"), do: @mime_image_jpg
  def type_from_extension("gif"), do: @mime_image_gif
  def type_from_extension(_), do: nil

  @doc """
  Takes a file (binary) and returns the media type.
  """
  @spec type_from_binary(file_binary :: binary()) :: String.t()
  def type_from_binary(<<@elf_signature, _tail::binary>>), do: @mime_application_octet_stream
  def type_from_binary(<<@bmp_signature, _tail::binary>>), do: @mime_image_bmp
  def type_from_binary(<<@png_signature, _tail::binary>>), do: @mime_image_png
  def type_from_binary(<<@jpg_signature, _tail::binary>>), do: @mime_image_jpg
  def type_from_binary(<<@gif_signature, _tail::binary>>), do: @mime_image_gif
  def type_from_binary(_), do: nil

  @doc """
  Takes a file (binary) and extension (string) and return an `:ok` if media types are the same,
  otherwise returns `:error` tuple.
  """
  @spec verify(file_binary :: binary(), extension :: String.t()) ::
          {:ok, String.t()} | {:error, String.t()}
  def verify(file_binary, extension) do
    type_ext = type_from_extension(extension)
    type_bin = type_from_binary(file_binary)

    if type_ext == type_bin and (not is_nil(type_ext) or not is_nil(type_bin)) do
      {:ok, type_ext}
    else
      {:error, "Warning, file format and file extension do not match."}
    end
  end
end

File Sniffer: Tests

https://github.com/exercism/elixir/blob/main/exercises/concept/file-sniffer/test/file_sniffer_test.exs

exe_file = <<0x7F, 0x45, 0x4C, 0x46, 0xFF, 0xFF, 0xFF>>
bmp_file = <<0x42, 0x4D, 0xFF, 0xFF, 0xFF>>
png_file = <<0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A, 0xFF, 0xFF, 0xFF>>
jpg_file = <<0xFF, 0xD8, 0xFF, 0xFF, 0xFF, 0xFF>>
gif_file = <<0x47, 0x49, 0x46, 0xFF, 0xFF, 0xFF>>

assert FileSniffer.type_from_extension("bmp") == "image/bmp"
assert FileSniffer.type_from_extension("gif") == "image/gif"
assert FileSniffer.type_from_extension("jpg") == "image/jpg"
assert FileSniffer.type_from_extension("png") == "image/png"
assert FileSniffer.type_from_extension("exe") == "application/octet-stream"
assert FileSniffer.type_from_extension("txt") == nil
assert FileSniffer.type_from_extension("md") == nil
assert FileSniffer.type_from_extension("svg") == nil

assert FileSniffer.type_from_binary(bmp_file) == "image/bmp"
assert FileSniffer.type_from_binary(gif_file) == "image/gif"
assert FileSniffer.type_from_binary(jpg_file) == "image/jpg"
assert FileSniffer.type_from_binary(png_file) == "image/png"
assert FileSniffer.type_from_binary(exe_file) == "application/octet-stream"

assert FileSniffer.type_from_binary(String.slice(bmp_file, 0..0)) == nil
assert FileSniffer.type_from_binary(String.slice(gif_file, 0..1)) == nil
assert FileSniffer.type_from_binary(String.slice(jpg_file, 0..1)) == nil
assert FileSniffer.type_from_binary(String.slice(png_file, 0..5)) == nil
assert FileSniffer.type_from_binary(String.slice(exe_file, 0..2)) == nil

assert FileSniffer.verify(bmp_file, "bmp") == {:ok, "image/bmp"}
assert FileSniffer.verify(gif_file, "gif") == {:ok, "image/gif"}
assert FileSniffer.verify(jpg_file, "jpg") == {:ok, "image/jpg"}
assert FileSniffer.verify(png_file, "png") == {:ok, "image/png"}
assert FileSniffer.verify(exe_file, "exe") == {:ok, "application/octet-stream"}

assert FileSniffer.verify(exe_file, "bmp") ==
         {:error, "Warning, file format and file extension do not match."}

assert FileSniffer.verify(exe_file, "gif") ==
         {:error, "Warning, file format and file extension do not match."}

assert FileSniffer.verify(exe_file, "jpg") ==
         {:error, "Warning, file format and file extension do not match."}

assert FileSniffer.verify(exe_file, "png") ==
         {:error, "Warning, file format and file extension do not match."}

assert FileSniffer.verify(png_file, "exe") ==
         {:error, "Warning, file format and file extension do not match."}

:passed

Newsletter

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

defmodule Newsletter do
  @doc """
  Reads email addresses from a file.
  """
  @spec read_emails(path :: Path.t()) :: list(String.t())
  def read_emails(path) do
    path
    |> File.read!()
    |> String.split("\n")
    |> Enum.map(&amp;String.trim/1)
    |> Enum.filter(&amp;(&amp;1 != ""))
  end

  @doc """
  Opens a log file for writing.
  """
  @spec open_log(pah :: Path.t()) :: File.io_device()
  def open_log(path), do: File.open!(path, [:write])

  @doc """
  Logs a sent email.
  """
  @spec log_sent_email(pid :: File.io_device(), email :: String.t()) :: :ok
  def log_sent_email(pid, email) do
    IO.puts(pid, email)
  end

  @doc """
  Closes the log file.
  """
  @spec close_log(pid :: File.io_device()) :: :ok
  def close_log(pid), do: File.close(pid)

  @doc """
  Sends the newsletter and writes sent email address to the log file.
  """
  @spec send_newsletter(emails_path :: Path.t(), log_path :: Path.t(), (String.t() -> :ok)) :: :ok
  def send_newsletter(emails_path, log_path, send_fun) do
    log_pid = open_log(log_path)

    emails_path
    |> read_emails()
    |> Enum.each(fn email ->
      case send_fun.(email) do
        :ok -> log_sent_email(log_pid, email)
        _ -> nil
      end
    end)

    close_log(log_pid)
  end
end

Newsletter: Tests

https://github.com/exercism/elixir/blob/main/exercises/concept/newsletter/test/newsletter_test.exs

temp_file_path = "temp.txt"
emails_file_path = "emails.txt"
empty_file_path = "empty.txt"

File.write!(temp_file_path, "")

File.write!(
  emails_file_path,
  "alice@example.com\nbob@example.com\ncharlie@example.com\ndave@example.com"
)

File.write!(empty_file_path, "")

assert Newsletter.read_emails(emails_file_path) == [
         "alice@example.com",
         "bob@example.com",
         "charlie@example.com",
         "dave@example.com"
       ]

assert Newsletter.read_emails(empty_file_path) == []

file = Newsletter.open_log(temp_file_path)
assert is_pid(file)
File.close(file)

file = Newsletter.open_log(temp_file_path)
assert IO.write(file, "hello") == :ok
assert File.read!(temp_file_path) == "hello"
File.close(file)

file = File.open!(temp_file_path, [:write])
assert Newsletter.log_sent_email(file, "janice@example.com") == :ok
File.close(file)

file = File.open!(temp_file_path, [:write])
Newsletter.log_sent_email(file, "joe@example.com")
assert File.read!(temp_file_path) == "joe@example.com\n"
File.close(file)

file = File.open!(temp_file_path, [:write])
Newsletter.log_sent_email(file, "joe@example.com")
Newsletter.log_sent_email(file, "kathrine@example.com")
Newsletter.log_sent_email(file, "lina@example.com")

assert File.read!(temp_file_path) ==
         "joe@example.com\nkathrine@example.com\nlina@example.com\n"

File.close(file)

file = File.open!(temp_file_path, [:write])
assert Newsletter.close_log(file) == :ok

file = File.open!(temp_file_path, [:read])
assert Newsletter.close_log(file) == :ok
assert IO.read(file, :all) == {:error, :terminated}

send_fun = fn _ -> :ok end

assert Newsletter.send_newsletter(
         "emails.txt",
         temp_file_path,
         send_fun
       ) == :ok

send_fun = fn email -> send(self(), {:send, email}) &amp;&amp; :ok end

Newsletter.send_newsletter("emails.txt", temp_file_path, send_fun)

assert_received {:send, "alice@example.com"}
assert_received {:send, "bob@example.com"}
assert_received {:send, "charlie@example.com"}
assert_received {:send, "dave@example.com"}

send_fun = fn _ -> :ok end

Newsletter.send_newsletter("emails.txt", temp_file_path, send_fun)

assert File.read!(temp_file_path) ==
         """
         alice@example.com
         bob@example.com
         charlie@example.com
         dave@example.com
         """

send_fun = fn
  "bob@example.com" -> :error
  "charlie@example.com" -> :error
  _ -> :ok
end

Newsletter.send_newsletter("emails.txt", temp_file_path, send_fun)

assert File.read!(temp_file_path) == """
       alice@example.com
       dave@example.com
       """

send_fun = fn _ -> :ok end
Newsletter.send_newsletter("emails.txt", temp_file_path, send_fun)
Newsletter.send_newsletter("emails.txt", temp_file_path, send_fun)

assert File.read!(temp_file_path) ==
         """
         alice@example.com
         bob@example.com
         charlie@example.com
         dave@example.com
         """

send_fun = fn email ->
  case email do
    "alice@example.com" ->
      :ok

    "bob@example.com" ->
      assert File.read!(temp_file_path) == """
             alice@example.com
             """

      :ok

    "charlie@example.com" ->
      assert File.read!(temp_file_path) == """
             alice@example.com
             bob@example.com
             """

      :error

    "dave@example.com" ->
      assert File.read!(temp_file_path) == """
             alice@example.com
             bob@example.com
             """

      :ok
  end
end

Newsletter.send_newsletter("emails.txt", temp_file_path, send_fun)

assert File.read!(temp_file_path) ==
         """
         alice@example.com
         bob@example.com
         dave@example.com
         """

:passed

Chessboard

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

defmodule Chessboard do
  @moduledoc """

  ## Examples

      iex> Chessboard.rank_range()
      1..8

      iex> Chessboard.file_range()
      ?A..?H

      iex> Chessboard.ranks()
      [1, 2, 3, 4, 5, 6, 7, 8]

      iex> Chessboard.files()
      ["A", "B", "C", "D", "E", "F", "G", "H"]

  """

  @doc """
  Simply returns a range of integers, from 1 to 8.
  """
  @spec rank_range() :: Range.t()
  def rank_range(), do: 1..8

  @doc """
  Returns a range of integers, from the code point of the uppercase letter A,
  to the code point of the uppercase letter H.
  """
  @spec file_range() :: Range.t()
  def file_range(), do: ?A..?H

  @doc """
  Returns a list of integers, from 1 to 8.
  """
  @spec ranks() :: list(pos_integer())
  def ranks(), do: Enum.to_list(rank_range())

  @doc """
  Returns a list of letters (strings) from "A" to "H".
  """
  @spec files() :: list(String.t())
  def files(), do: Enum.map(file_range(), &amp;<<&amp;1>>)
end

Chessboard: Tests

https://github.com/exercism/elixir/blob/main/exercises/concept/chessboard/test/chessboard_test.exs

assert Chessboard.rank_range() == 1..8
assert Chessboard.file_range() == ?A..?H
assert Chessboard.ranks() == [1, 2, 3, 4, 5, 6, 7, 8]
assert Chessboard.files() == ["A", "B", "C", "D", "E", "F", "G", "H"]

:passed

Remote Controll Car

https://exercism.org/tracks/elixir/exercises/remote-control-car

defmodule RemoteControlCar do
  @moduledoc """

  ## Examples

      iex> RemoteControlCar.new("Red")
      %RemoteControlCar{nickname: "Red", battery_percentage: 100, distance_driven_in_meters: 0}

  """

  @type t :: %__MODULE__{
          battery_percentage: non_neg_integer(),
          distance_driven_in_meters: non_neg_integer(),
          nickname: String.t()
        }

  @enforce_keys [:battery_percentage, :distance_driven_in_meters, :nickname]
  defstruct @enforce_keys

  @doc """
  Creates a brand-new remote controlled car with a nickname.
  """
  @spec new(String.t()) :: t()
  def new(nickname \\ "none"),
    do: %__MODULE__{
      battery_percentage: 100,
      distance_driven_in_meters: 0,
      nickname: nickname
    }

  @doc """
  Returns the distance as displayed on the LED display.
  """
  @spec display_distance(t()) :: non_neg_integer()
  def display_distance(%__MODULE__{distance_driven_in_meters: distance}),
    do: "#{distance} meters"

  @doc """
  Returns the battery percentage as displayed on the LED display.
  """
  @spec display_battery(t()) :: non_neg_integer()
  def display_battery(%__MODULE__{battery_percentage: 0}),
    do: "Battery empty"

  def display_battery(%__MODULE__{battery_percentage: battery}),
    do: "Battery at #{battery}%"

  @doc """
  Updates the number of meters driven by 20 and drains 1% of the battery.
  """
  @spec drive(t()) :: t()
  def drive(%__MODULE__{battery_percentage: 0} = remote_car), do: remote_car

  def drive(%__MODULE__{} = remote_car),
    do: %{
      remote_car
      | battery_percentage: remote_car.battery_percentage - 1,
        distance_driven_in_meters: remote_car.distance_driven_in_meters + 20
    }
end
defmodule FakeRemoteControlCar do
  defstruct battery_percentage: 100,
            distance_driven_in_meters: 0,
            nickname: nil
end
# https://github.com/exercism/elixir/blob/main/exercises/concept/remote-control-car/test/remote_control_car_test.exs

assert_raise ArgumentError, fn ->
  quote do
    %RemoteControlCar{}
  end
  |> Code.eval_quoted()
end

car = RemoteControlCar.new()

assert car.__struct__ == RemoteControlCar
assert car.battery_percentage == 100
assert car.distance_driven_in_meters == 0
assert car.nickname == "none"

nickname = "Red"
car = RemoteControlCar.new(nickname)

assert car.__struct__ == RemoteControlCar
assert car.battery_percentage == 100
assert car.distance_driven_in_meters == 0
assert car.nickname == nickname

fake_car = %{
  battery_percentage: 100,
  distance_driven_in_meters: 0,
  nickname: "Fake"
}

assert_raise(FunctionClauseError, fn ->
  RemoteControlCar.display_distance(fake_car)
end)

fake_car = %FakeRemoteControlCar{
  battery_percentage: 100,
  distance_driven_in_meters: 0,
  nickname: "Fake"
}

assert_raise(FunctionClauseError, fn ->
  RemoteControlCar.display_distance(fake_car)
end)

car = RemoteControlCar.new()

assert RemoteControlCar.display_distance(car) == "0 meters"

car = RemoteControlCar.new()
car = %{car | distance_driven_in_meters: 20}

assert RemoteControlCar.display_distance(car) == "20 meters"

fake_car = %{
  battery_percentage: 100,
  distance_driven_in_meters: 0,
  nickname: "Fake"
}

assert_raise(FunctionClauseError, fn ->
  RemoteControlCar.display_battery(fake_car)
end)

fake_car = %FakeRemoteControlCar{
  battery_percentage: 100,
  distance_driven_in_meters: 0,
  nickname: "Fake"
}

assert_raise(FunctionClauseError, fn ->
  RemoteControlCar.display_battery(fake_car)
end)

car = RemoteControlCar.new()

assert RemoteControlCar.display_battery(car) == "Battery at 100%"

car = RemoteControlCar.new()
car = %{car | battery_percentage: 0}

assert RemoteControlCar.display_battery(car) == "Battery empty"

fake_car = %{
  battery_percentage: 100,
  distance_driven_in_meters: 0,
  nickname: "Fake"
}

assert_raise(FunctionClauseError, fn ->
  RemoteControlCar.drive(fake_car)
end)

fake_car = %FakeRemoteControlCar{
  battery_percentage: 100,
  distance_driven_in_meters: 0,
  nickname: "Fake"
}

assert_raise(FunctionClauseError, fn ->
  RemoteControlCar.drive(fake_car)
end)

car = RemoteControlCar.new() |> RemoteControlCar.drive()

assert car.__struct__ == RemoteControlCar
assert car.battery_percentage == 99
assert car.distance_driven_in_meters == 20

car =
  RemoteControlCar.new()
  |> Map.put(:battery_percentage, 0)
  |> RemoteControlCar.drive()

assert car.__struct__ == RemoteControlCar
assert car.battery_percentage == 0
assert car.distance_driven_in_meters == 0

:passed

Boutique Suggestions

https://exercism.org/tracks/elixir/exercises/boutique-suggestions

defmodule BoutiqueSuggestions do
  @moduledoc """

  ## Examples

      iex> top = %{
      ...>  item_name: "Long Sleeve T-shirt",
      ...>  price: 19.95,
      ...>  color: "Deep Red",
      ...>  base_color: "red"
      ...> }
      iex> bottom = %{
      ...>  item_name: "Wonderwall Pants",
      ...>  price: 48.97,
      ...>  color: "French Navy",
      ...>  base_color: "blue"
      ...> }
      iex> BoutiqueSuggestions.get_combinations([top], [bottom])
      [{top, bottom}]

  """

  @type item :: %{
          item_name: String.t(),
          base_color: String.t(),
          price: pos_integer()
        }

  @doc """
  Takes a list of tops, a list of bottoms, and keyword list of options then
  produces cartesian product and filter out clashing outfits based on matching
  color. Filters outs a combination if its price exceed the maximum price.
  """
  @spec get_combinations(list(item()), list(item()), keyword()) :: list(item())
  def get_combinations(tops, bottoms, options \\ []) do
    maximum_price = Keyword.get(options, :maximum_price, 100.0)

    for top <- tops,
        bottom <- bottoms,
        top.base_color != bottom.base_color,
        top.price + bottom.price <= maximum_price do
      {top, bottom}
    end
  end
end

Boutique Suggestions: Tests

https://github.com/exercism/elixir/blob/main/exercises/concept/boutique-suggestions/test/boutique_suggestions_test.exs

assert BoutiqueSuggestions.get_combinations([], [])

top = %{
  item_name: "Long Sleeve T-shirt",
  price: 19.95,
  color: "Deep Red",
  base_color: "red"
}

bottom = %{
  item_name: "Wonderwall Pants",
  price: 48.97,
  color: "French Navy",
  base_color: "blue"
}

assert BoutiqueSuggestions.get_combinations([top], [bottom]) == [{top, bottom}]

top1 = %{
  item_name: "Long Sleeve T-shirt",
  price: 19.95,
  color: "Deep Red",
  base_color: "red"
}

top2 = %{
  item_name: "Brushwood Shirt",
  price: 19.10,
  color: "Camel-Sandstone Woodland Plaid",
  base_color: "brown"
}

bottom1 = %{
  item_name: "Wonderwall Pants",
  price: 48.97,
  color: "French Navy",
  base_color: "blue"
}

bottom2 = %{
  item_name: "Terrena Stretch Pants",
  price: 79.95,
  color: "Cast Iron",
  base_color: "grey"
}

tops = [top1, top2]
bottoms = [bottom1, bottom2]
expected = [{top1, bottom1}, {top1, bottom2}, {top2, bottom1}, {top2, bottom2}]
assert BoutiqueSuggestions.get_combinations(tops, bottoms) == expected

top = %{
  item_name: "Long Sleeve T-shirt",
  price: 19.95,
  color: "Deep Red",
  base_color: "red"
}

bottom = %{
  item_name: "Happy Hike Studio Pants",
  price: 99.00,
  color: "Ochre Red",
  base_color: "red"
}

assert BoutiqueSuggestions.get_combinations([top], [bottom]) == []

assert BoutiqueSuggestions.get_combinations([], [], maximum_price: 200.00)

top = %{
  item_name: "Sano Long Sleeve Shirt",
  price: 45.47,
  color: "Linen Chambray",
  base_color: "yellow"
}

bottom = %{
  item_name: "Happy Hike Studio Pants",
  price: 99.00,
  color: "Ochre Red",
  base_color: "red"
}

assert BoutiqueSuggestions.get_combinations([top], [bottom], maximum_price: 100.00) == []

top = %{
  item_name: "Sano Long Sleeve Shirt",
  price: 45.47,
  color: "Linen Chambray",
  base_color: "yellow"
}

bottom = %{
  item_name: "Happy Hike Studio Pants",
  price: 99.00,
  color: "Ochre Red",
  base_color: "red"
}

assert BoutiqueSuggestions.get_combinations([top], [bottom], maximum_price: 200.00) == [
         {top, bottom}
       ]

top = %{
  item_name: "Sano Long Sleeve Shirt",
  price: 45.47,
  color: "Linen Chambray",
  base_color: "yellow"
}

bottom = %{
  item_name: "Happy Hike Studio Pants",
  price: 99.00,
  color: "Ochre Red",
  base_color: "red"
}

assert BoutiqueSuggestions.get_combinations([top], [bottom], other_option: "test") == []

top1 = %{
  item_name: "Long Sleeve T-shirt",
  price: 19.95,
  color: "Deep Red",
  base_color: "red"
}

top2 = %{
  item_name: "Brushwood Shirt",
  price: 19.10,
  color: "Camel-Sandstone Woodland Plaid",
  base_color: "brown"
}

top3 = %{
  item_name: "Sano Long Sleeve Shirt",
  price: 45.47,
  color: "Linen Chambray",
  base_color: "yellow"
}

bottom1 = %{
  item_name: "Wonderwall Pants",
  price: 48.97,
  color: "French Navy",
  base_color: "blue"
}

bottom2 = %{
  item_name: "Terrena Stretch Pants",
  price: 79.95,
  color: "Cast Iron",
  base_color: "grey"
}

bottom3 = %{
  item_name: "Happy Hike Studio Pants",
  price: 99.00,
  color: "Ochre Red",
  base_color: "red"
}

tops = [top1, top2, top3]
bottoms = [bottom1, bottom2, bottom3]

expected = [
  {top1, bottom1},
  {top1, bottom2},
  {top2, bottom1},
  {top2, bottom2},
  {top3, bottom1}
]

assert BoutiqueSuggestions.get_combinations(tops, bottoms) == expected

:passed

Community Graden

https://exercism.org/tracks/elixir/exercises/community-garden

defmodule Plot do
  @type t :: %__MODULE__{
          plot_id: non_neg_integer(),
          registered_to: String.t()
        }

  @enforce_keys [:plot_id, :registered_to]
  defstruct [:plot_id, :registered_to]
end

defmodule CommunityGarden do
  @moduledoc """

  ## Examples

      iex> {:ok, pid} = CommunityGarden.start()
      iex> plot = CommunityGarden.register(pid, "Johnny Appleseed")
      %Plot{plot_id: 1, registered_to: "Johnny Appleseed"}
      iex> CommunityGarden.list_registrations(pid)
      [plot]

  """

  @doc """
  Starts an agents process which holds the garden registry state.
  """
  @spec start(opts :: GenServer.options()) :: Agent.on_start()
  def start(opts \\ []), do: Agent.start_link(fn -> {0, opts} end)

  @doc """
  Lists the registrations in the local garden.
  """
  @spec list_registrations(pid :: pid()) :: [Plot.t()]
  def list_registrations(pid), do: Agent.get(pid, fn {_last_id, plots} -> plots end)

  @doc """
  Registers plots to a person.
  """
  @spec register(pid :: pid(), register_to :: String.t()) :: Plot.t()
  def register(pid, register_to) do
    Agent.get_and_update(pid, fn {last_id, plots} ->
      plot = %Plot{plot_id: last_id + 1, registered_to: register_to}
      {plot, {last_id + 1, [plot | plots]}}
    end)
  end

  @doc """
  Release plots from the community garden.
  """
  @spec release(pid :: pid(), plot_id :: non_neg_integer()) :: :ok
  def release(pid, plot_id) do
    Agent.update(pid, fn {last_id, plots} ->
      {last_id, Enum.filter(plots, fn plot -> plot.plot_id != plot_id end)}
    end)
  end

  @doc """
  Gets a registered plot from the community garden registry.
  """
  @spec get_registration(pid :: pid(), plot_id :: non_neg_integer()) ::
          Plot.t() | {:not_found, String.t()}
  def get_registration(pid, plot_id) do
    Agent.get(pid, fn {_last_id, plots} ->
      Enum.find(plots, {:not_found, "plot is unregistered"}, &amp;(&amp;1.plot_id == plot_id))
    end)
  end
end

Community Graden: Tests

https://github.com/exercism/elixir/blob/main/exercises/concept/community-garden/test/community_garden_test.exs

assert {:ok, pid} = CommunityGarden.start()
assert Process.alive?(pid)

assert {:ok, pid} = CommunityGarden.start()
assert [] == CommunityGarden.list_registrations(pid)

assert {:ok, pid} = CommunityGarden.start()
assert %Plot{} = CommunityGarden.register(pid, "Johnny Appleseed")

assert {:ok, pid} = CommunityGarden.start()
assert %Plot{} = plot = CommunityGarden.register(pid, "Johnny Appleseed")
assert [plot] == CommunityGarden.list_registrations(pid)

assert {:ok, pid} = CommunityGarden.start()
plot = CommunityGarden.register(pid, "Johnny Appleseed")
assert plot.plot_id == 1

assert {:ok, pid} = CommunityGarden.start()
plot_1 = CommunityGarden.register(pid, "Johnny Appleseed")
plot_2 = CommunityGarden.register(pid, "Frederick Law Olmsted")
plot_3 = CommunityGarden.register(pid, "Lancelot (Capability) Brown")

assert plot_1.plot_id == 1
assert plot_2.plot_id == 2
assert plot_3.plot_id == 3

assert {:ok, pid} = CommunityGarden.start()
assert %Plot{} = plot = CommunityGarden.register(pid, "Johnny Appleseed")
assert :ok = CommunityGarden.release(pid, plot.plot_id)
assert [] == CommunityGarden.list_registrations(pid)

assert {:ok, pid} = CommunityGarden.start()

plot_1 = CommunityGarden.register(pid, "Keanu Reeves")
plot_2 = CommunityGarden.register(pid, "Thomas A. Anderson")

assert plot_1.plot_id == 1
assert plot_2.plot_id == 2

CommunityGarden.release(pid, plot_1.plot_id)
CommunityGarden.release(pid, plot_2.plot_id)

plot_3 = CommunityGarden.register(pid, "John Doe")
plot_4 = CommunityGarden.register(pid, "Jane Doe")

assert plot_3.plot_id == 3
assert plot_4.plot_id == 4

assert {:ok, pid} = CommunityGarden.start()
assert %Plot{} = plot = CommunityGarden.register(pid, "Johnny Appleseed")
assert %Plot{} = registered_plot = CommunityGarden.get_registration(pid, plot.plot_id)
assert registered_plot.plot_id == plot.plot_id
assert registered_plot.registered_to == "Johnny Appleseed"

assert {:ok, pid} = CommunityGarden.start()
assert {:not_found, "plot is unregistered"} = CommunityGarden.get_registration(pid, 1)

:passed

Bread And Potions

https://exercism.org/tracks/elixir/exercises/bread-and-potions

defmodule RPG do
  defmodule Character do
    defstruct health: 100, mana: 0
  end

  defmodule LoafOfBread do
    defstruct []
  end

  defmodule ManaPotion do
    defstruct strength: 10
  end

  defmodule Poison do
    defstruct []
  end

  defmodule EmptyBottle do
    defstruct []
  end

  # Add code to define the protocol and its implementations below here...

  defprotocol Edible do
    def eat(item, character)
  end

  defimpl Edible, for: LoafOfBread do
    def eat(%RPG.LoafOfBread{}, %RPG.Character{health: health} = character) do
      {nil, %{character | health: health + 5}}
    end
  end

  defimpl Edible, for: ManaPotion do
    def eat(%RPG.ManaPotion{strength: strength}, %RPG.Character{mana: mana} = character) do
      {%EmptyBottle{}, %{character | mana: mana + strength}}
    end
  end

  defimpl Edible, for: Poison do
    def eat(%RPG.Poison{}, %RPG.Character{} = character) do
      {%EmptyBottle{}, %{character | health: 0}}
    end
  end
end
defmodule NewItem do
  defstruct []
end

Bread And Potions: Tests

https://github.com/exercism/elixir/blob/main/exercises/concept/bread-and-potions/test/rpg_test.exs

alias RPG.{Edible, Character, LoafOfBread, ManaPotion, Poison, EmptyBottle}

assert Edible.__protocol__(:functions) == [eat: 2]

assert_raise Protocol.UndefinedError, fn ->
  Edible.eat(%NewItem{}, %Character{})
end

character = %Character{health: 50}
{_byproduct, %Character{} = character} = Edible.eat(%LoafOfBread{}, character)
assert character.health == 55

character = %Character{}
{byproduct, %Character{}} = Edible.eat(%LoafOfBread{}, character)
assert byproduct == nil

character = %Character{mana: 77}
{_byproduct, %Character{} = character} = Edible.eat(%LoafOfBread{}, character)
assert character.mana == 77

character = %Character{mana: 10}
{_byproduct, %Character{} = character} = Edible.eat(%ManaPotion{strength: 6}, character)
assert character.mana == 16
{_byproduct, %Character{} = character} = Edible.eat(%ManaPotion{strength: 9}, character)
assert character.mana == 25

character = %Character{}
{byproduct, %Character{}} = Edible.eat(%ManaPotion{}, character)
assert byproduct == %EmptyBottle{}

character = %Character{health: 4}
{_byproduct, %Character{} = character} = Edible.eat(%ManaPotion{strength: 6}, character)
assert character.health == 4

character = %Character{health: 120}
{_byproduct, %Character{} = character} = Edible.eat(%Poison{}, character)
assert character.health == 0

character = %Character{}
{byproduct, %Character{}} = Edible.eat(%Poison{}, character)
assert byproduct == %EmptyBottle{}

character = %Character{mana: 99}
{_byproduct, %Character{} = character} = Edible.eat(%Poison{}, character)
assert character.mana == 99

items = [
  %LoafOfBread{},
  %ManaPotion{strength: 10},
  %ManaPotion{strength: 2},
  %LoafOfBread{}
]

character = %Character{health: 100, mana: 100}

character =
  Enum.reduce(items, character, fn item, character ->
    {_, character} = Edible.eat(item, character)
    character
  end)

assert character.health == 110
assert character.mana == 112

:passed

Captain’s Log

https://exercism.org/tracks/elixir/exercises/captains-log

defmodule CaptainsLog do
  @moduledoc """

  ## Examples

      iex> CaptainsLog.format_stardate(4100.0)
      "4100.0"

  """

  @planetary_classes ["D", "H", "J", "K", "L", "M", "N", "R", "T", "Y"]

  @spec random_planet_class() :: String.t()
  def random_planet_class(), do: Enum.random(@planetary_classes)

  @spec random_ship_registry_number() :: String.t()
  def random_ship_registry_number(), do: "NCC-#{Enum.random(1000..9999)}"

  @spec random_stardate() :: float()
  def random_stardate(), do: :rand.uniform() * 1000 + 41000

  @spec format_stardate(stardate :: float()) :: String.t()
  def format_stardate(stardate), do: "~.1f" |> :io_lib.format([stardate]) |> to_string()
end

Captain’s Log: Tests

https://github.com/exercism/elixir/blob/main/exercises/concept/captains-log/test/captains_log_test.exs

planetary_classes = ["D", "H", "J", "K", "L", "M", "N", "R", "T", "Y"]

Enum.each(0..100, fn _ ->
  assert CaptainsLog.random_planet_class() in planetary_classes
end)

never_returned_planetary_classes =
  Enum.reduce_while(0..1000, planetary_classes, fn _, remaining_planetary_classes ->
    if remaining_planetary_classes == [] do
      {:halt, remaining_planetary_classes}
    else
      {:cont, remaining_planetary_classes -- [CaptainsLog.random_planet_class()]}
    end
  end)

assert never_returned_planetary_classes == []

assert String.starts_with?(CaptainsLog.random_ship_registry_number(), "NCC-")

Enum.each(0..100, fn _ ->
  random_ship_registry_number = CaptainsLog.random_ship_registry_number()
  just_the_number = String.replace(random_ship_registry_number, "NCC-", "")

  case Integer.parse(just_the_number) do
    {integer, ""} ->
      assert integer >= 1000
      assert integer <= 9999

    _ ->
      flunk("Expected #{just_the_number} to be an integer")
  end
end)

assert is_float(CaptainsLog.random_stardate())

Enum.each(0..100, fn _ ->
  assert CaptainsLog.random_stardate() >= 41_000.0
end)

Enum.each(0..100, fn _ ->
  assert CaptainsLog.random_stardate() < 42_000.0
end)

decimal_parts =
  Enum.map(0..10, fn _ ->
    random_stardate = CaptainsLog.random_stardate()
    Float.ceil(random_stardate) - random_stardate
  end)

assert Enum.count(Enum.uniq(decimal_parts)) > 3

decimal_parts =
  Enum.map(0..10, fn _ ->
    random_stardate = CaptainsLog.random_stardate()
    Float.ceil(random_stardate * 10) - random_stardate * 10
  end)

assert Enum.count(Enum.uniq(decimal_parts)) > 3

assert is_bitstring(CaptainsLog.format_stardate(41010.7))

assert CaptainsLog.format_stardate(41543.3) == "41543.3"

assert CaptainsLog.format_stardate(41032.4512) == "41032.5"

assert_raise ArgumentError, fn -> CaptainsLog.format_stardate(41411) end

:passed

Need For Speed

https://exercism.org/tracks/elixir/exercises/need-for-speed

defmodule NeedForSpeed.Race do
  defstruct [:title, :cars]

  def display_status(_), do: nil
  def display_battery(_), do: nil
  def display_distance(_), do: nil
end

defmodule NeedForSpeed.RemoteControlCar do
  defstruct [:nickname, :color]

  def display_battery(_), do: nil
  def display_distance(_), do: nil
end

defmodule NeedForSpeed do
  alias NeedForSpeed.Race
  alias NeedForSpeed.RemoteControlCar, as: Car

  import IO, only: [puts: 1]
  import IO.ANSI, except: [color: 1]

  # Do not edit the code below.

  def print_race(%Race{} = race) do
    puts("""
    🏁 #{race.title} 🏁
    Status: #{Race.display_status(race)}
    Distance: #{Race.display_distance(race)}

    Contestants:
    """)

    race.cars
    |> Enum.sort_by(&amp;(-1 * &amp;1.distance_driven_in_meters))
    |> Enum.with_index()
    |> Enum.each(fn {car, index} -> print_car(car, index + 1) end)
  end

  defp print_car(%Car{} = car, index) do
    color = color(car)

    puts("""
      #{index}. #{color}#{car.nickname}#{default_color()}
      Distance: #{Car.display_distance(car)}
      Battery: #{Car.display_battery(car)}
    """)
  end

  defp color(%Car{} = car) do
    case car.color do
      :red -> red()
      :blue -> cyan()
      :green -> green()
    end
  end
end

RPN Calculator

https://exercism.org/tracks/elixir/exercises/rpn-calculator

defmodule RPNCalculator do
  @moduledoc """

  ## Examples

      iex> RPNCalculator.calculate_verbose([], fn _ -> :success end)
      {:ok, :success}

  """

  def calculate!(stack, operation), do: operation.(stack)

  def calculate(stack, operation) do
    try do
      {:ok, calculate!(stack, operation)}
    rescue
      _ -> :error
    end
  end

  def calculate_verbose(stack, operation) do
    try do
      {:ok, calculate!(stack, operation)}
    rescue
      e in ArgumentError -> {:error, e.message}
    end
  end
end

RPN Calculator: Tests

https://github.com/exercism/elixir/blob/main/exercises/concept/rpn-calculator/test/rpn_calculator_test.exs

assert RPNCalculator.calculate!([], fn _ -> :ok end) == :ok

assert RPNCalculator.calculate!([], fn _ -> "ok" end) == "ok"

assert_raise(RuntimeError, fn ->
  RPNCalculator.calculate!([], fn _ -> raise "test error" end)
end)

assert RPNCalculator.calculate([], fn _ -> "operation completed" end) ==
         {:ok, "operation completed"}

assert RPNCalculator.calculate([], fn _ -> :success end) ==
         {:ok, :success}

assert RPNCalculator.calculate([], fn _ -> raise "test error" end) == :error

assert RPNCalculator.calculate_verbose([], fn _ -> "operation completed" end) ==
         {:ok, "operation completed"}

assert RPNCalculator.calculate_verbose([], fn _ -> :success end) ==
         {:ok, :success}

assert RPNCalculator.calculate_verbose([], fn _ -> raise ArgumentError, "test error" end) ==
         {:error, "test error"}

:passed

Stack Underflow

https://exercism.org/tracks/elixir/exercises/stack-underflow

defmodule RPNCalculator.Exception do
  defmodule DivisionByZeroError do
    @moduledoc "Dividing a number by zero produces an undefined result."
    defexception message: "division by zero occurred"
  end

  defmodule StackUnderflowError do
    @moduledoc "When there are not enough numbers on the stack, this exception is raised."
    defexception message: "stack underflow occurred"

    @impl true
    def exception(value) do
      case value do
        [] ->
          %__MODULE__{}

        _ ->
          %__MODULE__{message: "stack underflow occurred, context: " <> value}
      end
    end
  end

  @doc """
  Divides two numbers from the stack.
  - Raises `StackUnderflowError` when the stack does not contain enough numbers.
  - Raises `DivisionByZeroError` when the divisior is 0.
  """
  @spec divide(list()) :: no_return() | number()
  def divide(stack) when length(stack) <= 1, do: raise(StackUnderflowError, "when dividing")
  def divide([0 | _tail]), do: raise(DivisionByZeroError)
  def divide([divisor, y | _tail]), do: y / divisor
end

RPN Calculator Inspection

https://exercism.org/tracks/elixir/exercises/rpn-calculator-inspection

defmodule RPNCalculatorInspection do
  @type calculator :: (any() -> any())
  @type calculator_task :: %{input: String.t(), pid: pid()}

  @doc """
  Starts a reliability check for a single input. Takes two arguments, a calculator function,
  and an input for the calculator. Returns a map that contains the input and the PID of the
  spawned process.
  """
  @spec start_reliability_check(calculator :: calculator(), input :: String.t()) ::
          calculator_task()
  def start_reliability_check(calculator, input) do
    pid = spawn_link(fn -> calculator.(input) end)
    %{input: input, pid: pid}
  end

  @doc """
  Interperts the results of a reliability check.
  """
  @spec await_reliability_check_result(task :: calculator_task(), map()) :: map()
  def await_reliability_check_result(%{pid: pid, input: input}, results) do
    receive do
      {:EXIT, ^pid, :normal} -> Map.put(results, input, :ok)
      {:EXIT, ^pid, _reason} -> Map.put(results, input, :error)
    after
      100 -> Map.put(results, input, :timeout)
    end
  end

  @doc """
  Runs a concurrent reliability check for many inputs.
  """
  @spec reliability_check(calculator :: calculator(), list(String.t())) :: map()
  def reliability_check(calculator, inputs) do
    previous_trap = Process.flag(:trap_exit, true)

    tasks =
      for input <- inputs, into: [] do
        start_reliability_check(calculator, input)
      end

    results =
      for task <- tasks, reduce: %{} do
        results -> await_reliability_check_result(task, results)
      end

    Process.flag(:trap_exit, previous_trap)

    results
  end

  @doc """
  Run a concurrent correctness check for many inputs.
  """
  @spec correctness_check(calculator :: calculator(), list(String.t())) :: list()
  def correctness_check(calculator, inputs) do
    inputs
    |> Enum.map(&amp;Task.async(fn -> calculator.(&amp;1) end))
    |> Enum.map(&amp;Task.await(&amp;1, 100))
  end
end

Lucas Numbers

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

defmodule LucasNumbers do
  @moduledoc """
  Lucas numbers are an infinite sequence of numbers which build progressively
  which hold a strong correlation to the golden ratio (φ or ϕ)

  E.g.: 2, 1, 3, 4, 7, 11, 18, 29, ...

  ## Examples

      iex> LucasNumbers.generate(3)
      [2, 1, 3]

  """
  @spec generate(count :: pos_integer()) :: [pos_integer()]
  def generate(1), do: [2]
  def generate(2), do: [2, 1]

  def generate(count) when is_integer(count) and count > 2 do
    seq = generate(count - 1)
    last_index = length(seq) - 1

    seq ++ [Enum.at(seq, last_index - 1) + Enum.at(seq, last_index)]
  end

  def generate(_),
    do: raise(ArgumentError, "count must be specified as an integer >= 1")
end

Lucas Numbers (Stream.unfold/2)

defmodule LucasNumbers.StreamUnfold do
  @moduledoc """
  Lucas numbers are an infinite sequence of numbers which build progressively
  which hold a strong correlation to the golden ratio (φ or ϕ)

  E.g.: 2, 1, 3, 4, 7, 11, 18, 29, ...

  ## Examples

      iex> LucasNumbers.StreamUnfold.generate(3)
      [2, 1, 3]

  """
  @spec generate(count :: pos_integer()) :: [pos_integer()]
  def generate(count) when is_integer(count) and count >= 1 do
    1
    |> Stream.unfold(fn
      1 -> {2, 2}
      2 -> {1, {2, 1}}
      {a, b} -> {a + b, {b, a + b}}
    end)
    |> Enum.take(count)
  end

  def generate(_),
    do: raise(ArgumentError, "count must be specified as an integer >= 1")
end

Lucas Numbers (Stream.iterate/2)

defmodule LucasNumbers.StreamIterate do
  @moduledoc """
  Lucas numbers are an infinite sequence of numbers which build progressively
  which hold a strong correlation to the golden ratio (φ or ϕ)

  E.g.: 2, 1, 3, 4, 7, 11, 18, 29, ...

  ## Examples

      iex> LucasNumbers.StreamIterate.generate(3)
      [2, 1, 3]

  """

  @spec generate(count :: pos_integer()) :: [pos_integer()]
  def generate(count)
  def generate(1), do: [2]
  def generate(2), do: [2, 1]

  def generate(count) when is_integer(count) and count > 2 do
    sequence =
      {2, 1}
      |> Stream.iterate(fn {a, b} -> {b, a + b} end)
      |> Stream.map(fn {_a, b} -> b end)
      |> Enum.take(count - 1)

    [2 | sequence]
  end

  def generate(_),
    do: raise(ArgumentError, "count must be specified as an integer >= 1")
end

Lucas Numbers: Tests

https://github.com/exercism/elixir/blob/main/exercises/concept/lucas-numbers/test/lucas_numbers_test.exs

for module <- [LucasNumbers, LucasNumbers.StreamUnfold, LucasNumbers.StreamIterate] do
  assert module.generate(1) == [2]
  assert module.generate(2) == [2, 1]
  assert module.generate(3) == [2, 1, 3]
  assert module.generate(4) == [2, 1, 3, 4]
  assert module.generate(5) == [2, 1, 3, 4, 7]
  assert module.generate(6) == [2, 1, 3, 4, 7, 11]
  assert module.generate(7) == [2, 1, 3, 4, 7, 11, 18]
  assert module.generate(8) == [2, 1, 3, 4, 7, 11, 18, 29]
  assert module.generate(9) == [2, 1, 3, 4, 7, 11, 18, 29, 47]
  assert module.generate(10) == [2, 1, 3, 4, 7, 11, 18, 29, 47, 76]

  assert module.generate(25) == [
           2,
           1,
           3,
           4,
           7,
           11,
           18,
           29,
           47,
           76,
           123,
           199,
           322,
           521,
           843,
           1364,
           2207,
           3571,
           5778,
           9349,
           15127,
           24476,
           39603,
           64079,
           103_682
         ]

  assert_raise ArgumentError, "count must be specified as an integer >= 1", fn ->
    module.generate("Hello world!")
  end

  assert_raise ArgumentError, "count must be specified as an integer >= 1", fn ->
    module.generate(-1)
  end
end

:passed

New Passport

https://exercism.org/tracks/elixir/exercises/new-passport

defmodule NewPassport do
  @spec get_new_passport(now :: NaiveDateTime.t(), birthday :: Date.t(), form :: atom()) ::
          {:ok, number :: String.t()}
          | {:error, reason :: String.t()}
          | {:retry, at :: NaiveDateTime.t()}
  def get_new_passport(now, birthday, form) do
    with {:ok, timestamp} <- enter_building(now),
         {:ok, manual} <- find_counter_information(now),
         counter <- manual.(birthday),
         {:ok, checksum} <- stamp_form(timestamp, counter, form),
         number <- get_new_passport_number(timestamp, counter, checksum) do
      {:ok, number}
    else
      {:error, _reason} = error -> error
      {:coffee_break, _reason} -> {:retry, NaiveDateTime.add(now, 15 * 60, :second)}
    end
  end

  # Do not modify the functions below

  defp enter_building(%NaiveDateTime{} = datetime) do
    day = Date.day_of_week(datetime)
    time = NaiveDateTime.to_time(datetime)

    cond do
      day <= 4 and time_between(time, ~T[13:00:00], ~T[15:30:00]) ->
        {:ok, datetime |> DateTime.from_naive!("Etc/UTC") |> DateTime.to_unix()}

      day == 5 and time_between(time, ~T[13:00:00], ~T[14:30:00]) ->
        {:ok, datetime |> DateTime.from_naive!("Etc/UTC") |> DateTime.to_unix()}

      true ->
        {:error, "city office is closed"}
    end
  end

  @eighteen_years 18 * 365
  defp find_counter_information(%NaiveDateTime{} = datetime) do
    time = NaiveDateTime.to_time(datetime)

    if time_between(time, ~T[14:00:00], ~T[14:20:00]) do
      {:coffee_break, "information counter staff on coffee break, come back in 15 minutes"}
    else
      {:ok, fn %Date{} = birthday -> 1 + div(Date.diff(datetime, birthday), @eighteen_years) end}
    end
  end

  defp stamp_form(timestamp, counter, :blue) when rem(counter, 2) == 1 do
    {:ok, 3 * (timestamp + counter) + 1}
  end

  defp stamp_form(timestamp, counter, :red) when rem(counter, 2) == 0 do
    {:ok, div(timestamp + counter, 2)}
  end

  defp stamp_form(_timestamp, _counter, _form), do: {:error, "wrong form color"}

  defp get_new_passport_number(timestamp, counter, checksum) do
    "#{timestamp}-#{counter}-#{checksum}"
  end

  defp time_between(time, from, to) do
    Time.compare(from, time) != :gt and Time.compare(to, time) == :gt
  end
end

New Passport: Tests

https://github.com/exercism/elixir/blob/main/exercises/concept/new-passport/test/new_passport_test.exs

assert NewPassport.get_new_passport(~N[2021-10-11 10:30:00], ~D[1984-09-14], :blue) ==
         {:error, "city office is closed"}

assert NewPassport.get_new_passport(~N[2021-10-08 15:00:00], ~D[1984-09-14], :blue) ==
         {:error, "city office is closed"}

assert {:ok, _} = NewPassport.get_new_passport(~N[2021-10-11 15:00:00], ~D[1984-09-14], :blue)

assert NewPassport.get_new_passport(~N[2021-10-11 14:10:00], ~D[1984-09-14], :blue) ==
         {:retry, ~N[2021-10-11 14:25:00]}

assert {:ok, _} = NewPassport.get_new_passport(~N[2021-10-11 14:25:00], ~D[1984-09-14], :blue)

assert NewPassport.get_new_passport(~N[2021-10-08 14:15:00], ~D[1984-09-14], :blue) ==
         {:retry, ~N[2021-10-08 14:30:00]}

assert NewPassport.get_new_passport(~N[2021-10-08 14:30:00], ~D[1984-09-14], :blue) ==
         {:error, "city office is closed"}

assert NewPassport.get_new_passport(
         ~N[2021-10-11 14:25:00],
         ~D[1984-09-14],
         :orange_and_purple
       ) == {:error, "wrong form color"}

assert NewPassport.get_new_passport(~N[2021-10-11 14:25:00], ~D[1984-09-14], :red) ==
         {:error, "wrong form color"}

assert {:ok, _} = NewPassport.get_new_passport(~N[2021-10-11 14:25:00], ~D[1984-09-14], :blue)

assert {:ok, passport_number} =
         NewPassport.get_new_passport(~N[2021-10-11 13:00:00], ~D[1984-09-14], :blue)

[timestamp, _counter, _checksum] = String.split(passport_number, "-")
assert timestamp == "1633957200"

assert NewPassport.get_new_passport(~N[2021-10-11 14:15:00], ~D[1984-09-14], :blue) ==
         {:retry, ~N[2021-10-11 14:30:00]}

assert {:ok, passport_number} =
         NewPassport.get_new_passport(~N[2021-10-11 14:30:00], ~D[1984-09-14], :blue)

[timestamp, _counter, _checksum] = String.split(passport_number, "-")
assert timestamp == "1633962600"

assert NewPassport.get_new_passport(~N[2021-10-11 14:00:00], ~D[1984-09-14], :blue) ==
         {:retry, ~N[2021-10-11 14:15:00]}

assert NewPassport.get_new_passport(~N[2021-10-11 14:15:00], ~D[1984-09-14], :blue) ==
         {:retry, ~N[2021-10-11 14:30:00]}

assert {:ok, passport_number} =
         NewPassport.get_new_passport(~N[2021-10-11 14:30:00], ~D[1984-09-14], :blue)

[timestamp, _counter, _checksum] = String.split(passport_number, "-")
assert timestamp == "1633962600"

assert {:ok, passport_number} =
         NewPassport.get_new_passport(~N[2021-10-11 14:30:00], ~D[2005-09-14], :blue)

[_timestamp, counter, _checksum] = String.split(passport_number, "-")
assert counter == "1"

assert {:ok, passport_number} =
         NewPassport.get_new_passport(~N[2021-10-11 14:30:00], ~D[1987-09-14], :red)

[_timestamp, counter, _checksum] = String.split(passport_number, "-")
assert counter == "2"

assert NewPassport.get_new_passport(~N[2021-10-11 15:00:00], ~D[1984-09-14], :blue) ==
         {:ok, "1633964400-3-4901893210"}

assert NewPassport.get_new_passport(~N[2021-10-11 14:15:00], ~D[1984-09-14], :blue) ==
         {:retry, ~N[2021-10-11 14:30:00]}

assert NewPassport.get_new_passport(~N[2021-10-11 14:30:00], ~D[1984-09-14], :blue) ==
         {:ok, "1633962600-3-4901887810"}

assert NewPassport.get_new_passport(~N[2021-10-11 14:00:00], ~D[1964-09-14], :red) ==
         {:retry, ~N[2021-10-11 14:15:00]}

assert NewPassport.get_new_passport(~N[2021-10-11 14:15:00], ~D[1964-09-14], :red) ==
         {:retry, ~N[2021-10-11 14:30:00]}

assert NewPassport.get_new_passport(~N[2021-10-12 14:30:00], ~D[1964-09-14], :red) ==
         {:ok, "1634049000-4-817024502"}

:passed

Top Secret

https://exercism.org/tracks/elixir/exercises/top-secret

defmodule TopSecret do
  defguardp is_func_definition(func) when func in [:def, :defp]

  @doc """
  Takes a string with Elixir code and return its AST.
  """
  @spec to_ast(string :: String.t()) :: Macro.t()
  def to_ast(string) do
    {:ok, ast} = Code.string_to_quoted(string)
    ast
  end

  @doc """
  Takes an AST node and an accumulator for the secret message (a list).
  If the operation in the AST node is defining a function then puts first
  `n` characters from the name to the accumulator, where `n` is the arity.
  Returns unchanged AST node and the accumulator. If the AST node doesn't contain
  function definition then returns unchaged accumulator.
  """
  @spec decode_secret_message_part(ast :: Macro.t(), acc :: list(String.t())) ::
          {Macro.t(), list(String.t())}
  def decode_secret_message_part({op, _meta, args} = ast, acc)
      when is_func_definition(op) do
    {name, args} = get_function_name_and_args(args)

    message_part =
      name
      |> to_string()
      |> String.slice(0, length(args))

    {ast, [message_part | acc]}
  end

  def decode_secret_message_part(ast, acc), do: {ast,</