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
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
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
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
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
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, &(score + &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
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
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
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(&if year, do: filter_by_year(&1, year), else: &1)
|> then(&if country, do: filter_by_country(&1, country), else: &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
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
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
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, & &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, &is_nil(&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
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
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(&String.trim/1)
|> Enum.filter(&(&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}) && :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(), &<<&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
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"}, &(&1.plot_id == plot_id))
end)
end
end
Community Graden: Tests
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
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(&(-1 * &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
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(&Task.async(fn -> calculator.(&1) end))
|> Enum.map(&Task.await(&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
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
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,