Initial upload

This commit is contained in:
Fabian Becker 2022-08-24 14:28:45 +02:00
parent c67653ddee
commit 57bc7b0289
370 changed files with 18479 additions and 0 deletions

1
.gitignore vendored Normal file
View File

@ -0,0 +1 @@
elixir/**/_build/

View File

@ -0,0 +1,22 @@
{
"authors": [
"neenjaw"
],
"contributors": [
"angelikatyborska",
"NobbZ"
],
"files": {
"solution": [
"lib/basketball_website.ex"
],
"test": [
"test/basketball_website_test.exs"
],
"exemplar": [
".meta/exemplar.ex"
]
},
"language_versions": ">=1.10",
"blurb": "Learn about Access Behaviour by helping extract deeply nested data for the basketball team's website."
}

View File

@ -0,0 +1 @@
{"track":"elixir","exercise":"basketball-website","id":"62c61d3995094446ad2e8d13594f8e4f","url":"https://exercism.org/tracks/elixir/exercises/basketball-website","handle":"halfdan","is_requester":true,"auto_approve":false}

View File

@ -0,0 +1,4 @@
# Used by "mix format"
[
inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"]
]

24
elixir/basketball-website/.gitignore vendored Normal file
View File

@ -0,0 +1,24 @@
# The directory Mix will write compiled artifacts to.
/_build/
# If you run "mix test --cover", coverage assets end up here.
/cover/
# The directory Mix downloads your dependencies sources to.
/deps/
# Where third-party dependencies like ExDoc output generated docs.
/doc/
# Ignore .fetch files in case you like to edit your project deps locally.
/.fetch
# If the VM crashes, it generates a dump, let's ignore it too.
erl_crash.dump
# Also ignore archive artifacts (built via "mix archive.build").
*.ez
# Ignore package tarball (built via "mix hex.build").
access-*.tar

View File

@ -0,0 +1,75 @@
# Help
## Running the tests
From the terminal, change to the base directory of the exercise then execute the tests with:
```bash
$ mix test
```
This will execute the test file found in the `test` subfolder -- a file ending in `_test.exs`
Documentation:
* [`mix test` - Elixir's test execution tool](https://hexdocs.pm/mix/Mix.Tasks.Test.html)
* [`ExUnit` - Elixir's unit test library](https://hexdocs.pm/ex_unit/ExUnit.html)
## Pending tests
In test suites of practice exercises, all but the first test have been tagged to be skipped.
Once you get a test passing, you can unskip the next one by commenting out the relevant `@tag :pending` with a `#` symbol.
For example:
```elixir
# @tag :pending
test "shouting" do
assert Bob.hey("WATCH OUT!") == "Whoa, chill out!"
end
```
If you wish to run all tests at once, you can include all skipped test by using the `--include` flag on the `mix test` command:
```bash
$ mix test --include pending
```
Or, you can enable all the tests by commenting out the `ExUnit.configure` line in the file `test/test_helper.exs`.
```elixir
# ExUnit.configure(exclude: :pending, trace: true)
```
## Useful `mix test` options
* `test/<FILE>.exs:LINENUM` - runs only a single test, the test from `<FILE>.exs` whose definition is on line `LINENUM`
* `--failed` - runs only tests that failed the last time they ran
* `--max-failures` - the suite stops evaluating tests when this number of test failures
is reached
* `--seed 0` - disables randomization so the tests in a single file will always be ran
in the same order they were defined in
## Submitting your solution
You can submit your solution using the `exercism submit lib/basketball_website.ex` command.
This command will upload your solution to the Exercism website and print the solution page's URL.
It's possible to submit an incomplete solution which allows you to:
- See how others have completed the exercise
- Request help from a mentor
## Need to get help?
If you'd like help solving the exercise, check the following pages:
- The [Elixir track's documentation](https://exercism.org/docs/tracks/elixir)
- [Exercism's support channel on gitter](https://gitter.im/exercism/support)
- The [Frequently Asked Questions](https://exercism.org/docs/using/faqs)
Should those resources not suffice, you could submit your (incomplete) solution to request mentoring.
If you're stuck on something, it may help to look at some of the [available resources](https://exercism.org/docs/tracks/elixir/resources) out there where answers might be found.
If you can't find what you're looking for in the documentation, feel free to ask help in the Exercism's BEAM [gitter channel](https://gitter.im/exercism/xerlang).

View File

@ -0,0 +1,18 @@
# Hints
## General
- Read about the [`Access` behaviour][access-behaviour] in the documentation.
## 1. Extract data from a nested map structure
- First tokenize the string path to a usable state. You can make use of [`String` module functions][string-module].
- Write a recursive function to traverse the nested-map structure to retrieve the value or return `nil`.
## 2. Refactor using included functions
- Read through the various [`Kernel` module functions][kernel-module], to find one that might shorten/simplify your approach.
[kernel-module]: https://hexdocs.pm/elixir/Kernel.html#functions
[string-module]: https://hexdocs.pm/elixir/String.html#functions
[access-behaviour]: https://hexdocs.pm/elixir/Access.html

View File

@ -0,0 +1,82 @@
# Basketball Website
Welcome to Basketball Website on Exercism's Elixir Track.
If you need help running the tests or submitting your code, check out `HELP.md`.
If you get stuck on the exercise, check out `HINTS.md`, but try and solve it without using those first :)
## Introduction
## Access Behaviour
Elixir uses code _Behaviours_ to provide common generic interfaces while facilitating specific implementations for each module which implements it. One such common example is the _Access Behaviour_.
The _Access Behaviour_ provides a common interface for retrieving data from a key-based data structure. The _Access Behaviour_ is implemented for maps and keyword lists, but let's look at its use for maps to get a feel for it. _Access Behaviour_ specifies that when you have a map, you may follow it with _square brackets_ and then use the key to retrieve the value associated with that key.
```elixir
# Suppose we have these two maps defined (note the difference in the key type)
my_map = %{key: "my value"}
your_map = %{"key" => "your value"}
# Obtain the value using the Access Behaviour
my_map[:key] == "my value"
your_map[:key] == nil
your_map["key"] == "your value"
```
If the key does not exist in the data structure, then `nil` is returned. This can be a source of unintended behavior, because it does not raise an error. Note that `nil` itself implements the Access Behaviour and always returns `nil` for any key.
## Instructions
You are working with a web development team to maintain a website for a local basketball team. The web development team is less familiar with Elixir and is asking for a function to be able to extract data from a series of nested maps to facilitate rapid development.
## 1. Extract data from a nested map structure
Implement the `extract_from_path/2` function to take two arguments:
- `data`: a nested map structure with data about the basketball team.
- `path`: a string consisting of period-delimited keys to obtain the value associated with the last key.
If the value or the key does not exist at any point in the path, `nil` should be returned
```elixir
data = %{
"team_mascot" => %{
"animal" => "bear",
"actor" => %{
"first_name" => "Noel"
}
}
}
BasketballWebsite.extract_from_path(data, "team_mascot.animal")
# => "bear"
BasketballWebsite.extract_from_path(data, "team_mascot.colors")
# => nil
```
Use the _Access Behaviour_ when implementing this function.
Do not use any `Map` or `Kernel` module functions for working with the nested map data structure.
## 2. Refactor using included functions
Your coworker reviewing your code tells you about a `Kernel` module function which does something very similar to your implementation.
Implement `get_in_path/2` to use this `Kernel` module function.
The arguments expected are the same as part 1.
```elixir
BasketballWebsite.get_in_path(data, "team_mascot.actor.first_name")
# => "Noel"
```
## Source
### Created by
- @neenjaw
### Contributed to by
- @angelikatyborska
- @NobbZ

View File

@ -0,0 +1,15 @@
defmodule BasketballWebsite do
def extract_from_path(data, path) do
paths = String.split(path, ".")
root = Enum.at(paths, 0)
if length(paths) > 1 and data[root] do
extract_from_path(data[root], Enum.slice(paths, 1..-1) |> Enum.join("."))
else
data[root]
end
end
def get_in_path(data, path) do
Kernel.get_in(data, String.split(path, "."))
end
end

View File

@ -0,0 +1,28 @@
defmodule BasketballWebsite.MixProject do
use Mix.Project
def project do
[
app: :basketball_website,
version: "0.1.0",
# elixir: "~> 1.10",
start_permanent: Mix.env() == :prod,
deps: deps()
]
end
# Run "mix help compile.app" to learn about applications.
def application do
[
extra_applications: [:logger]
]
end
# Run "mix help deps" to learn about dependencies.
defp deps do
[
# {:dep_from_hexpm, "~> 0.3.0"},
# {:dep_from_git, git: "https://github.com/elixir-lang/my_dep.git", tag: "0.1.0"}
]
end
end

View File

@ -0,0 +1,335 @@
defmodule BasketballWebsiteTest do
use ExUnit.Case
describe "extract_from_path retrieves from" do
@tag task_id: 1
test "first layer" do
team_data = %{
"coach" => %{},
"team_name" => "Hoop Masters",
"players" => %{}
}
assert BasketballWebsite.extract_from_path(team_data, "team_name") == "Hoop Masters"
end
@tag task_id: 1
test "second layer" do
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"
end
@tag task_id: 1
test "third layer" do
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"
end
@tag task_id: 1
test "fourth layer" do
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
end
end
describe "extract_from_path returns nil from nonexistent last key in" do
@tag task_id: 1
test "first layer" do
team_data = %{
"coach" => %{},
"team_name" => "Hoop Masters",
"players" => %{}
}
assert BasketballWebsite.extract_from_path(team_data, "team_song") == nil
end
@tag task_id: 1
test "second layer" do
team_data = %{
"coach" => %{
"first_name" => "Jane",
"last_name" => "Brown"
},
"team_name" => "Hoop Masters",
"players" => %{}
}
assert BasketballWebsite.extract_from_path(team_data, "coach.age") == nil
end
@tag task_id: 1
test "third layer" do
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
end
@tag task_id: 1
test "fourth layer" do
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
end
end
@tag task_id: 1
test "extract_from_path returns nil from nonexistent path" do
team_data = %{
"coach" => %{},
"team_name" => "Hoop Masters",
"players" => %{}
}
assert BasketballWebsite.extract_from_path(
team_data,
"support_personnel.physiotherapy.first_name"
) == nil
end
describe "get_in_path retrieves from" do
@tag task_id: 2
test "first layer" do
team_data = %{
"coach" => %{},
"team_name" => "Hoop Masters",
"players" => %{}
}
assert BasketballWebsite.get_in_path(team_data, "team_name") == "Hoop Masters"
end
@tag task_id: 2
test "second layer" do
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"
end
@tag task_id: 2
test "third layer" do
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"
end
@tag task_id: 2
test "fourth layer" do
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
end
end
describe "get_in_path returns nil from nonexistent last key in" do
@tag task_id: 2
test "first layer" do
team_data = %{
"coach" => %{},
"team_name" => "Hoop Masters",
"players" => %{}
}
assert BasketballWebsite.get_in_path(team_data, "team_song") == nil
end
@tag task_id: 2
test "second layer" do
team_data = %{
"coach" => %{
"first_name" => "Jane",
"last_name" => "Brown"
},
"team_name" => "Hoop Masters",
"players" => %{}
}
assert BasketballWebsite.get_in_path(team_data, "coach.age") == nil
end
@tag task_id: 2
test "third layer" do
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
end
@tag task_id: 2
test "fourth layer" do
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
end
end
@tag task_id: 2
test "get_in_path returns nil from nonexistent path" do
team_data = %{
"coach" => %{},
"team_name" => "Hoop Masters",
"players" => %{}
}
assert BasketballWebsite.get_in_path(team_data, "support_personnel.physiotherapy.first_name") ==
nil
end
end

View File

@ -0,0 +1,2 @@
ExUnit.start()
ExUnit.configure(exclude: :pending, trace: true, seed: 0)

View File

@ -0,0 +1,21 @@
{
"authors": [
"angelikatyborska"
],
"contributors": [
"neenjaw"
],
"files": {
"solution": [
"lib/captains_log.ex"
],
"test": [
"test/captains_log_test.exs"
],
"exemplar": [
".meta/exemplar.ex"
]
},
"language_versions": ">=1.10",
"blurb": "Learn about randomness and using Erlang libraries from Elixir by helping Mary generate stardates and starship registry numbers for her Star Trek themed pen-and-paper role playing sessions."
}

View File

@ -0,0 +1 @@
{"track":"elixir","exercise":"captains-log","id":"42e6cfb7bfa94354a2597ab82ac8a8ba","url":"https://exercism.org/tracks/elixir/exercises/captains-log","handle":"halfdan","is_requester":true,"auto_approve":false}

View File

@ -0,0 +1,4 @@
# Used by "mix format"
[
inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"]
]

24
elixir/captains-log/.gitignore vendored Normal file
View File

@ -0,0 +1,24 @@
# The directory Mix will write compiled artifacts to.
/_build/
# If you run "mix test --cover", coverage assets end up here.
/cover/
# The directory Mix downloads your dependencies sources to.
/deps/
# Where third-party dependencies like ExDoc output generated docs.
/doc/
# Ignore .fetch files in case you like to edit your project deps locally.
/.fetch
# If the VM crashes, it generates a dump, let's ignore it too.
erl_crash.dump
# Also ignore archive artifacts (built via "mix archive.build").
*.ez
# Ignore package tarball (built via "mix hex.build").
match_binary-*.tar

View File

@ -0,0 +1,75 @@
# Help
## Running the tests
From the terminal, change to the base directory of the exercise then execute the tests with:
```bash
$ mix test
```
This will execute the test file found in the `test` subfolder -- a file ending in `_test.exs`
Documentation:
* [`mix test` - Elixir's test execution tool](https://hexdocs.pm/mix/Mix.Tasks.Test.html)
* [`ExUnit` - Elixir's unit test library](https://hexdocs.pm/ex_unit/ExUnit.html)
## Pending tests
In test suites of practice exercises, all but the first test have been tagged to be skipped.
Once you get a test passing, you can unskip the next one by commenting out the relevant `@tag :pending` with a `#` symbol.
For example:
```elixir
# @tag :pending
test "shouting" do
assert Bob.hey("WATCH OUT!") == "Whoa, chill out!"
end
```
If you wish to run all tests at once, you can include all skipped test by using the `--include` flag on the `mix test` command:
```bash
$ mix test --include pending
```
Or, you can enable all the tests by commenting out the `ExUnit.configure` line in the file `test/test_helper.exs`.
```elixir
# ExUnit.configure(exclude: :pending, trace: true)
```
## Useful `mix test` options
* `test/<FILE>.exs:LINENUM` - runs only a single test, the test from `<FILE>.exs` whose definition is on line `LINENUM`
* `--failed` - runs only tests that failed the last time they ran
* `--max-failures` - the suite stops evaluating tests when this number of test failures
is reached
* `--seed 0` - disables randomization so the tests in a single file will always be ran
in the same order they were defined in
## Submitting your solution
You can submit your solution using the `exercism submit lib/captains_log.ex` command.
This command will upload your solution to the Exercism website and print the solution page's URL.
It's possible to submit an incomplete solution which allows you to:
- See how others have completed the exercise
- Request help from a mentor
## Need to get help?
If you'd like help solving the exercise, check the following pages:
- The [Elixir track's documentation](https://exercism.org/docs/tracks/elixir)
- [Exercism's support channel on gitter](https://gitter.im/exercism/support)
- The [Frequently Asked Questions](https://exercism.org/docs/using/faqs)
Should those resources not suffice, you could submit your (incomplete) solution to request mentoring.
If you're stuck on something, it may help to look at some of the [available resources](https://exercism.org/docs/tracks/elixir/resources) out there where answers might be found.
If you can't find what you're looking for in the documentation, feel free to ask help in the Exercism's BEAM [gitter channel](https://gitter.im/exercism/xerlang).

View File

@ -0,0 +1,38 @@
# Hints
## General
- Read about using Erlang libraries in the [official Getting Started guide][getting-started-erlang-libraries], and in particular about [formatting strings][getting-started-formatted-text-output].
## 1. Generate a random planet
- Use the provided module attribute with a list of letters representing planetary classes.
- There is a [built-in function][enum-random] for choosing an element from a list at random.
## 2. Generate a random starship registry number
- There is a [build-in function][enum-random] for choosing an element from a range at random.
## 3. Generate a random stardate
- There is no Elixir function that would return a random float.
- There is a [built-in Erlang function][erl-rand-uniform] that returns a random float x where `0.0 <= x < 1.0`.
- If `x` belongs to a range `0.0 <= x < 1.0`, but you need a number from a different range `a <= x < b`, you can shift x's range by multiplying it by the range's width (`b - a`) and adding the range's start (`a`). That is: `x * (b - a) + a`.
## 4. Format the stardate
- There is no Elixir function that would be able to, in a single step, format a float as a string with a given precision.
- There is a [built-in Erlang function][erl-io-lib-format] that takes a format string and a list of data, and returns a charlist.
- There is a [built-in function][to-string] that changes a charlist to a string.
- The format string of that function contains control sequences.
- A control sequence starts with `~` and has the pattern `~F.P.PadModC`, where `F` stands for the width of the output, `P` stands for the precision, `Pad` stands for the padding character, `Mod` stands for the control sequence modifier, and `C` is the type of the control sequence.
- To format a float with a desired precision, a control sequence with the pattern `~.PC` will suffice.
- The control sequence type for floats is `f`.
- The exact format string you need is `~.1f`.
[getting-started-erlang-libraries]: https://elixir-lang.org/getting-started/erlang-libraries.html
[getting-started-formatted-text-output]: https://elixir-lang.org/getting-started/erlang-libraries.html#formatted-text-output
[enum-random]: https://hexdocs.pm/elixir/Enum.html#random/1
[erl-rand-uniform]: http://www.erlang.org/doc/man/rand.html#uniform-0
[erl-io-lib-format]: http://www.erlang.org/doc/man/io_lib.html#format-2
[to-string]: https://hexdocs.pm/elixir/Kernel.html#to_string/1

View File

@ -0,0 +1,103 @@
# Captain's Log
Welcome to Captain's Log on Exercism's Elixir Track.
If you need help running the tests or submitting your code, check out `HELP.md`.
If you get stuck on the exercise, check out `HINTS.md`, but try and solve it without using those first :)
## Introduction
## Randomness
In Elixir, to choose a random element from an enumerable data structure (e.g. list, range), we use `Enum.random`. This function will pick a single element, with every element having equal probability of being picked.
Elixir does not have its own functions for picking a random float. To do that, we have to use Erlang directly.
## Erlang Libraries
Elixir code runs in the BEAM virtual machine. BEAM is part of the Erlang Run-Time System. Being inspired by Erlang, and sharing its run environment, Elixir provides great interoperability with Erlang libraries. This means that Elixir developers can use Erlang libraries from within their Elixir code. In fact, writing Elixir libraries for functionality already provided by Erlang libraries is discouraged in the Elixir community.
As a result, certain functionality, like mathematical operations or timer functions, is only available in Elixir via Erlang.
Erlang's standard library is available for use in our Elixir code without any extra steps necessary.
Erlang functions can be called in the same way we call Elixir functions, with one small difference. Erlang module names are `snake_case` atoms. For example, to call the Erlang `pi/0` function from the `math` module, one would write:
```elixir
:math.pi()
# => 3.141592653589793
```
The most commonly used Erlang functions that do not have an Elixir equivalent are:
- `:timer.sleep/1` which suspends a process for the given amount of milliseconds.
- `:rand.uniform/0` which generates a random float `x`, where `0.0 <= x < 1.0`.
- `:io_lib.format/2` which provides C-style string formatting (using control sequences). Using this function, we could for example print an integer in any base between 2 and 36 or format a float with desired precision. Note that this function, like many Erlang functions, returns a charlist.
- The `math` module that provides mathematical functions such as `sin/1`, `cos/1`, `log2/1`, `log10/1`, `pow/2`, and more.
To discover Erlang's standard library, explore the [STDLIB Reference Manual][erl-stdlib-ref].
[erl-stdlib-ref]: http://www.erlang.org/doc/apps/stdlib/index.html
## Instructions
Mary is a big fan of the TV series _Star Trek: The Next Generation_. She often plays pen-and-paper role playing games, where she and her friends pretend to be the crew of the _Starship Enterprise_. Mary's character is Captain Picard, which means she has to keep the captain's log. She loves the creative part of the game, but doesn't like to generate random data on the spot.
Help Mary by creating random generators for data commonly appearing in the captain's log.
## 1. Generate a random planet
The _Starship Enterprise_ encounters many planets in its travels. Planets in the Star Trek universe are split into categories based on their properties. For example, Earth is a class M planet. All possible planetary classes are: D, H, J, K, L, M, N, R, T, and Y.
Implement the `random_planet_class/0` function. It should return one of the planetary classes at random.
```elixir
CaptainsLog.random_planet_class()
# => "K"
```
## 2. Generate a random starship registry number
Enterprise (registry number NCC-1701) is not the only starship flying around! When it rendezvous with another starship, Mary needs to log the registry number of that starship.
Registry numbers start with the prefix "NCC-" and then use a number from 1000 to 9999 (inclusive).
Implement the `random_ship_registry_number/0` function that returns a random starship registry number.
```elixir
CaptainsLog.random_ship_registry_number()
# => "NCC-1947"
```
## 3. Generate a random stardate
What's the use of a log if it doesn't include dates?
A stardate is a floating point number. The adventures of the _Starship Enterprise_ from the first season of _The Next Generation_ take place between the stardates 41000.0 and 42000.0. The "4" stands for the 24th century, the "1" for the first season.
Implement the function `random_stardate/0` that returns a floating point number between 41000.0 (inclusive) and 42000.0 (exclusive).
```elixir
CaptainsLog.random_stardate()
# => 41458.15721310934
```
## 4. Format the stardate
In the captain's log, stardates are usually rounded to a single decimal place.
Implement the `format_stardate/1` function that will take a floating point number and return a string with the number rounded to a single decimal place.
```elixir
CaptainsLog.format_stardate(41458.15721310934)
# => "41458.2"
```
## Source
### Created by
- @angelikatyborska
### Contributed to by
- @neenjaw

View File

@ -0,0 +1,21 @@
defmodule CaptainsLog do
@planetary_classes ["D", "H", "J", "K", "L", "M", "N", "R", "T", "Y"]
def random_planet_class() do
Enum.random(@planetary_classes)
end
def random_ship_registry_number() do
n = Enum.random(1000..9999)
"NCC-#{n}"
end
def random_stardate() do
41000.0 + :rand.uniform() * 1000
end
def format_stardate(stardate) when is_float(stardate) do
:io_lib.format("~.1f", [stardate]) |> String.Chars.to_string()
end
def format_stardate(_), do: raise ArgumentError
end

View File

@ -0,0 +1,28 @@
defmodule CaptainsLog.MixProject do
use Mix.Project
def project do
[
app: :captains_log,
version: "0.1.0",
# elixir: "~> 1.10",
start_permanent: Mix.env() == :prod,
deps: deps()
]
end
# Run "mix help compile.app" to learn about applications.
def application do
[
extra_applications: [:logger]
]
end
# Run "mix help deps" to learn about dependencies.
defp deps do
[
# {:dep_from_hexpm, "~> 0.3.0"},
# {:dep_from_git, git: "https://github.com/elixir-lang/my_dep.git", tag: "0.1.0"}
]
end
end

View File

@ -0,0 +1,119 @@
defmodule CaptainsLogTest do
use ExUnit.Case
describe "random_planet_class" do
@tag task_id: 1
test "it always returns one of the letters: D, H, J, K, L, M, N, R, T, Y" do
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)
end
@tag task_id: 1
test "it will eventually return each of the letters at least once" do
planetary_classes = ["D", "H", "J", "K", "L", "M", "N", "R", "T", "Y"]
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 == []
end
end
describe "random_ship_registry_number" do
@tag task_id: 2
test "start with \"NCC-\"" do
assert String.starts_with?(CaptainsLog.random_ship_registry_number(), "NCC-")
end
@tag task_id: 2
test "ends with a random integer between 1000 and 9999" do
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)
end
end
describe "random_stardate" do
@tag task_id: 3
test "is a float" do
assert is_float(CaptainsLog.random_stardate())
end
@tag task_id: 3
test "is equal to or greater than 41_000.0" do
Enum.each(0..100, fn _ ->
assert CaptainsLog.random_stardate() >= 41_000.0
end)
end
@tag task_id: 3
test "is less than 42_000.0" do
Enum.each(0..100, fn _ ->
assert CaptainsLog.random_stardate() < 42_000.0
end)
end
@tag task_id: 3
test "consecutive calls return floats with different fractional parts" do
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
end
@tag task_id: 3
test "returns floats with fractional parts with more than one decimal place" do
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
end
end
describe "format_stardate" do
@tag task_id: 4
test "returns a string" do
assert is_bitstring(CaptainsLog.format_stardate(41010.7))
end
@tag task_id: 4
test "formats floats" do
assert CaptainsLog.format_stardate(41543.3) == "41543.3"
end
@tag task_id: 4
test "rounds floats to one decimal place" do
assert CaptainsLog.format_stardate(41032.4512) == "41032.5"
end
@tag task_id: 4
test "does not accept integers" do
assert_raise ArgumentError, fn -> CaptainsLog.format_stardate(41411) end
end
end
end

View File

@ -0,0 +1,2 @@
ExUnit.start()
ExUnit.configure(exclude: :pending, trace: true, seed: 0)

View File

@ -0,0 +1,22 @@
{
"authors": [
"angelikatyborska"
],
"contributors": [
"neenjaw",
"michallepicki"
],
"files": {
"solution": [
"lib/form.ex"
],
"test": [
"test/form_test.exs"
],
"exemplar": [
".meta/exemplar.ex"
]
},
"language_versions": ">=1.10",
"blurb": "Learn about writing documentation and typespecs by getting your code ready for the arrival of a new colleague at the city office."
}

View File

@ -0,0 +1 @@
{"track":"elixir","exercise":"city-office","id":"f8d9471439664a2092d435381c8af052","url":"https://exercism.org/tracks/elixir/exercises/city-office","handle":"halfdan","is_requester":true,"auto_approve":false}

View File

@ -0,0 +1,4 @@
# Used by "mix format"
[
inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"]
]

24
elixir/city-office/.gitignore vendored Normal file
View File

@ -0,0 +1,24 @@
# The directory Mix will write compiled artifacts to.
/_build/
# If you run "mix test --cover", coverage assets end up here.
/cover/
# The directory Mix downloads your dependencies sources to.
/deps/
# Where third-party dependencies like ExDoc output generated docs.
/doc/
# Ignore .fetch files in case you like to edit your project deps locally.
/.fetch
# If the VM crashes, it generates a dump, let's ignore it too.
erl_crash.dump
# Also ignore archive artifacts (built via "mix archive.build").
*.ez
# Ignore package tarball (built via "mix hex.build").
match_binary-*.tar

View File

@ -0,0 +1,75 @@
# Help
## Running the tests
From the terminal, change to the base directory of the exercise then execute the tests with:
```bash
$ mix test
```
This will execute the test file found in the `test` subfolder -- a file ending in `_test.exs`
Documentation:
* [`mix test` - Elixir's test execution tool](https://hexdocs.pm/mix/Mix.Tasks.Test.html)
* [`ExUnit` - Elixir's unit test library](https://hexdocs.pm/ex_unit/ExUnit.html)
## Pending tests
In test suites of practice exercises, all but the first test have been tagged to be skipped.
Once you get a test passing, you can unskip the next one by commenting out the relevant `@tag :pending` with a `#` symbol.
For example:
```elixir
# @tag :pending
test "shouting" do
assert Bob.hey("WATCH OUT!") == "Whoa, chill out!"
end
```
If you wish to run all tests at once, you can include all skipped test by using the `--include` flag on the `mix test` command:
```bash
$ mix test --include pending
```
Or, you can enable all the tests by commenting out the `ExUnit.configure` line in the file `test/test_helper.exs`.
```elixir
# ExUnit.configure(exclude: :pending, trace: true)
```
## Useful `mix test` options
* `test/<FILE>.exs:LINENUM` - runs only a single test, the test from `<FILE>.exs` whose definition is on line `LINENUM`
* `--failed` - runs only tests that failed the last time they ran
* `--max-failures` - the suite stops evaluating tests when this number of test failures
is reached
* `--seed 0` - disables randomization so the tests in a single file will always be ran
in the same order they were defined in
## Submitting your solution
You can submit your solution using the `exercism submit lib/form.ex` command.
This command will upload your solution to the Exercism website and print the solution page's URL.
It's possible to submit an incomplete solution which allows you to:
- See how others have completed the exercise
- Request help from a mentor
## Need to get help?
If you'd like help solving the exercise, check the following pages:
- The [Elixir track's documentation](https://exercism.org/docs/tracks/elixir)
- [Exercism's support channel on gitter](https://gitter.im/exercism/support)
- The [Frequently Asked Questions](https://exercism.org/docs/using/faqs)
Should those resources not suffice, you could submit your (incomplete) solution to request mentoring.
If you're stuck on something, it may help to look at some of the [available resources](https://exercism.org/docs/tracks/elixir/resources) out there where answers might be found.
If you can't find what you're looking for in the documentation, feel free to ask help in the Exercism's BEAM [gitter channel](https://gitter.im/exercism/xerlang).

View File

@ -0,0 +1,63 @@
# Hints
## General
- Read the official documentation for [typespecs][typespecs].
- Read the official documentation about [writing documentation][writing-documentation].
- Read about using module attributes as annotations in the [official Getting Started guide][getting-started-module-attributes].
- Read about using typespecs in the [official Getting Started guide][getting-started-typespecs].
## 1. Document the purpose of the form tools
- The module attribute `@moduledoc` can be used to write documentation for a module.
## 2. Document filling out fields with blank values
- The module attribute `@doc` can be used to write documentation for a function.
- The module attribute `@spec` can be used to write a typespec for a function.
- Place the `@doc` and `@spec` attributes right before the first function clause of the function that those attributes describe.
- Refer to the [typespecs documentation][typespecs-types] for a list of all available types.
- The correct type for strings is [defined in the `String` module][string-t].
## 3. Document splitting values into lists of uppercase letters
- The module attribute `@doc` can be used to write documentation for a function.
- The module attribute `@spec` can be used to write a typespec for a function.
- Place the `@doc` and `@spec` attributes right before the first function clause of the function that those attributes describe.
- Refer to the [typespecs documentation][typespecs-types] for a list of all available types.
- The correct type for strings is [defined in the `String` module][string-t].
- A list is a parametrized type.
## 4. Document checking if a value fits a field with a max length
- The module attribute `@doc` can be used to write documentation for a function.
- The module attribute `@spec` can be used to write a typespec for a function.
- Place the `@doc` and `@spec` attributes right before the first function clause of the function that those attributes describe.
- Refer to the [typespecs documentation][typespecs-types] for a list of all available types.
- The correct type for strings is [defined in the `String` module][string-t].
- Literal values can be used in a typespec.
- The pipe `|` can be used to represent a union of types.
## 5. Document different address formats
- The module attribute `@type` can be use to define a custom public type.
- Types can be compound, e.g. when specifying a type that's a map, you can also specify the types of the values under the specific keys.
- [The type operator `::`][type-operator] can also be used to prepend a variable name to a type.
- Custom types can be used to define other custom types.
## 6. Document formatting the address
- The module attribute `@doc` can be used to write documentation for a function.
- The module attribute `@spec` can be used to write a typespec for a function.
- Place the `@doc` and `@spec` attributes right before the first function clause of the function that those attributes describe.
- Refer to the [typespecs documentation][typespecs-types] for a list of all available types.
- The correct type for strings is [defined in the `String` module][string-t].
- Custom types can be used in a typespec.
[writing-documentation]: https://hexdocs.pm/elixir/writing-documentation.html
[typespecs]: https://hexdocs.pm/elixir/typespecs.html
[typespecs-types]: https://hexdocs.pm/elixir/typespecs.html#types-and-their-syntax
[getting-started-module-attributes]: https://elixir-lang.org/getting-started/module-attributes.html#as-annotations
[getting-started-typespecs]: https://elixir-lang.org/getting-started/typespecs-and-behaviours.html#types-and-specs
[string-t]: https://hexdocs.pm/elixir/String.html#t:t/0
[type-operator]: https://hexdocs.pm/elixir/Kernel.SpecialForms.html#::/2

View File

@ -0,0 +1,173 @@
# City Office
Welcome to City Office on Exercism's Elixir Track.
If you need help running the tests or submitting your code, check out `HELP.md`.
If you get stuck on the exercise, check out `HINTS.md`, but try and solve it without using those first :)
## Introduction
## Docs
Documentation in Elixir is a first-class citizen.
There are two module attributes commonly used to document your code - `@moduledoc` for documenting a module and `@doc` for documenting a function that follows the attribute. The `@moduledoc` attribute usually appears on the first line of the module, and the `@doc` attribute usually appears right before a function definition, or the function's typespec if it has one. The documentation is commonly written in a multiline string using the heredoc syntax.
Elixir documentation is written in [**Markdown**][markdown].
```elixir
defmodule String do
@moduledoc """
Strings in Elixir are UTF-8 encoded binaries.
"""
@doc """
Converts all characters in the given string to uppercase according to `mode`.
## Examples
iex> String.upcase("abcd")
"ABCD"
iex> String.upcase("olá")
"OLÁ"
"""
def upcase(string, mode \\ :default)
end
```
## Typespecs
Elixir is a dynamically typed language, which means it doesn't provide compile-time type checks. Still, type specifications can be used as a form of documentation.
A type specification can be added to a function using the `@spec` module attribute right before the function definition. `@spec` is followed by the function name and a list of all of its arguments' types, in parentheses, separated by commas. The type of the return value is separated from the function's arguments with a double colon `::`.
```elixir
@spec longer_than?(String.t(), non_neg_integer()) :: boolean()
def longer_than?(string, length), do: String.length(string) > length
```
### Types
Most commonly used types include:
- booleans: `boolean()`
- strings: `String.t()`
- numbers: `integer()`, `non_neg_integer()`, `pos_integer()`, `float()`
- lists: `list()`
- a value of any type: `any()`
Some types can also be parameterized, for example `list(integer)` is a list of integers.
Literal values can also be used as types.
A union of types can be written using the pipe `|`. For example, `integer() | :error` means either an integer or the atom literal `:error`.
A full list of all types can be found in the ["Typespecs" section in the official documentation][types].
### Naming arguments
Arguments in the typespec could also be named which is useful for distinguishing multiple arguments of the same type. The argument name, followed by a double colon, goes before the argument's type.
```elixir
@spec to_hex({hue :: integer, saturation :: integer, lightness :: integer}) :: String.t()
```
### Custom types
Typespecs aren't limited to just the built-in types. Custom types can be defined using the `@type` module attribute. A custom type definition starts with the type's name, followed by a double colon and then the type itself.
```elixir
@type color :: {hue :: integer, saturation :: integer, lightness :: integer}
@spec to_hex(color()) :: String.t()
```
A custom type can be used from the same module where it's defined, or from another module.
[types]: https://hexdocs.pm/elixir/typespecs.html#types-and-their-syntax
[markdown]: https://docs.github.com/en/github/writing-on-github/basic-writing-and-formatting-syntax
## Instructions
You have been working in the city office for a while, and you have developed a set of tools that speed up your day-to-day work, for example with filling out forms.
Now, a new colleague is joining you, and you realized your tools might not be self-explanatory. There are a lot of weird conventions in your office, like always filling out forms with uppercase letters and avoiding leaving fields empty.
You decide to write some documentation so that it's easier for your new colleague to hop right in and start using your tools.
## 1. Document the purpose of the form tools
Add documentation to the `Form` module that describes its purpose. It should read:
```
A collection of loosely related functions helpful for filling out various forms at the city office.
```
## 2. Document filling out fields with blank values
Add documentation and a typespec to the `Form.blanks/1` function. The documentation should read:
```
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.
```
The typespec should explain that the function accepts a single argument, a non-negative integer, and returns a string.
## 3. Document splitting values into lists of uppercase letters
Add documentation and a typespec to the `Form.letters/1` function. The documentation should read:
```
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.
```
The typespec should explain that the function accepts a single argument, a string, and returns a list of strings.
## 4. Document checking if a value fits a field with a max length
Add documentation and a typespec to the `Form.check_length/2` function. The documentation should read:
```
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.
```
The typespec should explain that the function accepts two arguments, a string and a non-negative integer, and returns one of two possible values. It returns either the `:ok` atom or a 2-tuple with the first element being the `:error` atom, and the second a positive integer.
## 5. Document different address formats
For some unknown to you reason, the city office's internal system uses two different ways of representing addresses - either as a map or as a tuple.
Document this fact by defining three custom public types:
- `address_map` - a map with the keys `:street`, `:postal_code`, and `:city`. Each key holds a value of type string.
- `address_tuple` - a tuple with three values - `street`, `postal_code`, and `city`. Each value is of type string. Differentiate the values by giving them names in the typespec.
- `address` - can be either an `address_map` or an `address_tuple`.
## 6. Document formatting the address
Add documentation and a typespec to the `Form.format_address/1` function. The documentation should read:
```
Formats the address as an uppercase multiline string.
```
The typespec should explain that the function accepts one argument, an address, and returns a string.
## Source
### Created by
- @angelikatyborska
### Contributed to by
- @neenjaw
- @michallepicki

View File

@ -0,0 +1,65 @@
defmodule Form do
@moduledoc """
A collection of loosely related functions helpful for filling out various forms at the city office.
"""
@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(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(String.t()) :: [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(String.t(), 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
@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 """
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

View File

@ -0,0 +1,28 @@
defmodule Form.MixProject do
use Mix.Project
def project do
[
app: :city_office,
version: "0.1.0",
# elixir: "~> 1.10",
start_permanent: Mix.env() == :prod,
deps: deps()
]
end
# Run "mix help compile.app" to learn about applications.
def application do
[
extra_applications: [:logger]
]
end
# Run "mix help deps" to learn about dependencies.
defp deps do
[
# {:dep_from_hexpm, "~> 0.3.0"},
# {:dep_from_git, git: "https://github.com/elixir-lang/my_dep.git", tag: "0.1.0"}
]
end
end

View File

@ -0,0 +1,292 @@
defmodule FormTest do
use ExUnit.Case
# Dear Elixir learner,
# If you're reading this test suite to gain some insights,
# please be advised that it is somewhat unusual.
#
# Don't worry if you don't understand this test suite at this stage of your learning journey.
# We had to use some advanced features to be able to write assertions about docs and typespecs.
# You wouldn't normally write assertions for that in a typical codebase.
# We're doing it here strictly for educational purposes.
defmacrop assert_moduledoc(expected_moduledoc) do
quote do
{:docs_v1, _, _, _, module_doc, _, _} = Code.fetch_docs(Form)
if module_doc == :none do
flunk("expected the module Form to have documentation")
else
actual_moduledoc = module_doc["en"]
assert actual_moduledoc == unquote(expected_moduledoc)
end
end
end
defmacrop assert_doc({function_name, function_arity}, expected_doc) do
quote do
{:docs_v1, _, _, _, _, _, docs} = Code.fetch_docs(Form)
{_, _, _, doc_content, _} =
Enum.find(docs, fn {{kind, function_name, arity}, _, _, _, _} ->
{kind, function_name, arity} ==
{:function, unquote(function_name), unquote(function_arity)}
end)
if doc_content == :none do
flunk(
"expected the function Form.#{unquote(function_name)}/#{unquote(function_arity)} to have documentation"
)
else
actual_doc = doc_content["en"]
assert actual_doc == unquote(expected_doc)
end
end
end
defmacrop assert_spec({function_name, function_arity}, arguments_specs, return_spec) do
quote do
{:ok, specs} = Code.Typespec.fetch_specs(Form)
spec =
Enum.find(specs, fn {{function_name, arity}, _} ->
{function_name, arity} == {unquote(function_name), unquote(function_arity)}
end)
assert spec,
"expected the function Form.#{unquote(function_name)}/#{unquote(function_arity)} to have a typespec"
{{unquote(function_name), unquote(function_arity)}, [{:type, _, :fun, _} = function_spec]} =
spec
{:"::", _, [arguments, return]} =
Code.Typespec.spec_to_quoted(unquote(function_name), function_spec)
accepted_arguments_specs =
Enum.map(unquote(arguments_specs), fn arguments_spec ->
"#{unquote(function_name)}(#{arguments_spec})"
end)
actual_arguments_spec = Macro.to_string(arguments)
assert actual_arguments_spec in accepted_arguments_specs
expected_return_spec = unquote(return_spec)
actual_return_spec = Macro.to_string(return)
assert actual_return_spec == expected_return_spec
end
end
defmacrop assert_type({module_name, type_name}, expected_type_definition) do
quote do
{:ok, types} = Code.Typespec.fetch_types(unquote(module_name))
type =
Enum.find(types, fn {declaration, {type_name, _, _}} ->
declaration == :type && type_name == unquote(type_name)
end)
assert type,
"expected the module #{unquote(module_name)} to have a public type named #{unquote(type_name)}"
{:type, type} = type
{:"::", _, [_, type_definition]} = Code.Typespec.type_to_quoted(type)
actual_type_definition = Macro.to_string(type_definition)
if is_list(unquote(expected_type_definition)) do
if actual_type_definition in unquote(expected_type_definition) do
assert true
else
# we know this will fail at this point, but we're using it to provide a nice failure message
assert actual_type_definition == hd(unquote(expected_type_definition))
end
else
assert actual_type_definition == unquote(expected_type_definition)
end
end
end
describe "the Form module" do
@tag task_id: 1
test "has documentation" do
expected_moduledoc = """
A collection of loosely related functions helpful for filling out various forms at the city office.
"""
assert_moduledoc(expected_moduledoc)
end
end
describe "blanks/1" do
@tag task_id: 2
test "returns a string with Xs of a given length" do
assert Form.blanks(5) == "XXXXX"
end
@tag task_id: 2
test "returns an empty string when given length is 0" do
assert Form.blanks(0) == ""
end
@tag task_id: 2
test "has documentation" do
expected_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.
"""
assert_doc({:blanks, 1}, expected_doc)
end
@tag task_id: 2
test "has a correct spec" do
assert_spec({:blanks, 1}, ["n :: non_neg_integer()", "non_neg_integer()"], "String.t()")
end
end
describe "letters/1" do
@tag task_id: 3
test "returns a list of upcase letters" do
assert Form.letters("Sao Paulo") == ["S", "A", "O", " ", "P", "A", "U", "L", "O"]
end
@tag task_id: 3
test "returns an empty list when given an empty string" do
assert Form.letters("") == []
end
@tag task_id: 3
test "has documentation" do
expected_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.
"""
assert_doc({:letters, 1}, expected_doc)
end
@tag task_id: 3
test "has a typespec" do
assert_spec({:letters, 1}, ["word :: String.t()", "String.t()"], "[String.t()]")
end
end
describe "check_length/2" do
@tag task_id: 4
test "returns :ok is value is below max length" do
assert Form.check_length("Ruiz", 6) == :ok
end
@tag task_id: 4
test "returns :ok is value is of exactly max length" do
assert Form.check_length("Martinez-Cooper", 15) == :ok
end
@tag task_id: 4
test "returns an error tuple with the difference between max length and actual length" do
assert Form.check_length("Martinez-Campbell", 10) == {:error, 7}
end
@tag task_id: 4
test "has documentation" do
expected_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.
"""
assert_doc({:check_length, 2}, expected_doc)
end
@tag task_id: 4
test "has a typespec" do
assert_spec(
{:check_length, 2},
["word :: String.t(), length :: non_neg_integer()", "String.t(), non_neg_integer()"],
":ok | {:error, pos_integer()}"
)
end
end
describe "custom types in the Form module" do
@tag task_id: 5
test "has a custom 'address_map' type" do
expected_type_definitions = [
"%{street: String.t(), postal_code: String.t(), city: String.t()}",
"%{street: String.t(), city: String.t(), postal_code: String.t()}",
"%{postal_code: String.t(), street: String.t(), city: String.t()}",
"%{postal_code: String.t(), city: String.t(), street: String.t()}",
"%{city: String.t(), street: String.t(), postal_code: String.t()}",
"%{city: String.t(), postal_code: String.t(), street: String.t()}"
]
assert_type({Form, :address_map}, expected_type_definitions)
end
@tag task_id: 5
test "has a custom 'address_tuple' type with named arguments" do
expected_type_definition =
"{street :: String.t(), postal_code :: String.t(), city :: String.t()}"
assert_type({Form, :address_tuple}, expected_type_definition)
end
@tag task_id: 5
test "has a custom 'address' type that is a union of 'address_map' and 'address_tuple'" do
expected_type_definitions = [
"address_map() | address_tuple()",
"address_tuple() | address_map()"
]
assert_type({Form, :address}, expected_type_definitions)
end
end
describe "format_address/1" do
@tag task_id: 6
test "accepts a map" do
input = %{
street: "Wiejska 4/6/8",
postal_code: "00-902",
city: "Warsaw"
}
result = """
WIEJSKA 4/6/8
00-902 WARSAW
"""
assert Form.format_address(input) == result
end
@tag task_id: 6
test "accepts a 3 string tuple" do
result = """
PLATZ DER REPUBLIK 1
11011 BERLIN
"""
assert Form.format_address({"Platz der Republik 1", "11011", "Berlin"}) == result
end
@tag task_id: 6
test "has documentation" do
expected_doc = """
Formats the address as an uppercase multiline string.
"""
assert_doc({:format_address, 1}, expected_doc)
end
@tag task_id: 6
test "has a typespec" do
assert_spec({:format_address, 1}, ["address :: address()", "address()"], "String.t()")
end
end
end

View File

@ -0,0 +1,2 @@
ExUnit.start()
ExUnit.configure(exclude: :pending, trace: true, seed: 0)

View File

@ -0,0 +1,26 @@
{
"authors": [
"Tuxified"
],
"contributors": [
"angelikatyborska",
"Cohen-Carlisle",
"devonestes",
"neenjaw",
"sotojuan"
],
"files": {
"solution": [
"lib/collatz_conjecture.ex"
],
"test": [
"test/collatz_conjecture_test.exs"
],
"example": [
".meta/example.ex"
]
},
"blurb": "Calculate the number of steps to reach 1 using the Collatz conjecture.",
"source": "An unsolved problem in mathematics named after mathematician Lothar Collatz",
"source_url": "https://en.wikipedia.org/wiki/3x_%2B_1_problem"
}

View File

@ -0,0 +1 @@
{"track":"elixir","exercise":"collatz-conjecture","id":"af0a52fbaf7d40e8b28828aff758c922","url":"https://exercism.org/tracks/elixir/exercises/collatz-conjecture","handle":"halfdan","is_requester":true,"auto_approve":false}

View File

@ -0,0 +1,4 @@
# Used by "mix format"
[
inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"]
]

24
elixir/collatz-conjecture/.gitignore vendored Normal file
View File

@ -0,0 +1,24 @@
# The directory Mix will write compiled artifacts to.
/_build/
# If you run "mix test --cover", coverage assets end up here.
/cover/
# The directory Mix downloads your dependencies sources to.
/deps/
# Where third-party dependencies like ExDoc output generated docs.
/doc/
# Ignore .fetch files in case you like to edit your project deps locally.
/.fetch
# If the VM crashes, it generates a dump, let's ignore it too.
erl_crash.dump
# Also ignore archive artifacts (built via "mix archive.build").
*.ez
# Ignore package tarball (built via "mix hex.build").
collatz_conjecture-*.tar

View File

@ -0,0 +1,75 @@
# Help
## Running the tests
From the terminal, change to the base directory of the exercise then execute the tests with:
```bash
$ mix test
```
This will execute the test file found in the `test` subfolder -- a file ending in `_test.exs`
Documentation:
* [`mix test` - Elixir's test execution tool](https://hexdocs.pm/mix/Mix.Tasks.Test.html)
* [`ExUnit` - Elixir's unit test library](https://hexdocs.pm/ex_unit/ExUnit.html)
## Pending tests
In test suites of practice exercises, all but the first test have been tagged to be skipped.
Once you get a test passing, you can unskip the next one by commenting out the relevant `@tag :pending` with a `#` symbol.
For example:
```elixir
# @tag :pending
test "shouting" do
assert Bob.hey("WATCH OUT!") == "Whoa, chill out!"
end
```
If you wish to run all tests at once, you can include all skipped test by using the `--include` flag on the `mix test` command:
```bash
$ mix test --include pending
```
Or, you can enable all the tests by commenting out the `ExUnit.configure` line in the file `test/test_helper.exs`.
```elixir
# ExUnit.configure(exclude: :pending, trace: true)
```
## Useful `mix test` options
* `test/<FILE>.exs:LINENUM` - runs only a single test, the test from `<FILE>.exs` whose definition is on line `LINENUM`
* `--failed` - runs only tests that failed the last time they ran
* `--max-failures` - the suite stops evaluating tests when this number of test failures
is reached
* `--seed 0` - disables randomization so the tests in a single file will always be ran
in the same order they were defined in
## Submitting your solution
You can submit your solution using the `exercism submit lib/collatz_conjecture.ex` command.
This command will upload your solution to the Exercism website and print the solution page's URL.
It's possible to submit an incomplete solution which allows you to:
- See how others have completed the exercise
- Request help from a mentor
## Need to get help?
If you'd like help solving the exercise, check the following pages:
- The [Elixir track's documentation](https://exercism.org/docs/tracks/elixir)
- [Exercism's support channel on gitter](https://gitter.im/exercism/support)
- The [Frequently Asked Questions](https://exercism.org/docs/using/faqs)
Should those resources not suffice, you could submit your (incomplete) solution to request mentoring.
If you're stuck on something, it may help to look at some of the [available resources](https://exercism.org/docs/tracks/elixir/resources) out there where answers might be found.
If you can't find what you're looking for in the documentation, feel free to ask help in the Exercism's BEAM [gitter channel](https://gitter.im/exercism/xerlang).

View File

@ -0,0 +1,50 @@
# Collatz Conjecture
Welcome to Collatz Conjecture on Exercism's Elixir Track.
If you need help running the tests or submitting your code, check out `HELP.md`.
## Instructions
The Collatz Conjecture or 3x+1 problem can be summarized as follows:
Take any positive integer n. If n is even, divide n by 2 to get n / 2. If n is
odd, multiply n by 3 and add 1 to get 3n + 1. Repeat the process indefinitely.
The conjecture states that no matter which number you start with, you will
always reach 1 eventually.
Given a number n, return the number of steps required to reach 1.
## Examples
Starting with n = 12, the steps would be as follows:
0. 12
1. 6
2. 3
3. 10
4. 5
5. 16
6. 8
7. 4
8. 2
9. 1
Resulting in 9 steps. So for input n = 12, the return value would be 9.
## Source
### Created by
- @Tuxified
### Contributed to by
- @angelikatyborska
- @Cohen-Carlisle
- @devonestes
- @neenjaw
- @sotojuan
### Based on
An unsolved problem in mathematics named after mathematician Lothar Collatz - https://en.wikipedia.org/wiki/3x_%2B_1_problem

View File

@ -0,0 +1,20 @@
defmodule CollatzConjecture do
@doc """
calc/1 takes an integer and returns the number of steps required to get the
number to 1 when following the rules:
- if number is odd, multiply with 3 and add 1
- if number is even, divide by 2
"""
@spec calc(input :: pos_integer()) :: non_neg_integer()
def calc(input) when is_integer(input) do
collatz(input, 0)
end
defp collatz(1, steps), do: steps
defp collatz(input, steps) when input > 0 and rem(input, 2) == 0,
do: collatz(div(input, 2), steps + 1)
defp collatz(input, steps) when input > 0, do: collatz(3 * input + 1, steps + 1)
end

View File

@ -0,0 +1,28 @@
defmodule CollatzConjecture.MixProject do
use Mix.Project
def project do
[
app: :collatz_conjecture,
version: "0.1.0",
# elixir: "~> 1.8",
start_permanent: Mix.env() == :prod,
deps: deps()
]
end
# Run "mix help compile.app" to learn about applications.
def application do
[
extra_applications: [:logger]
]
end
# Run "mix help deps" to learn about dependencies.
defp deps do
[
# {:dep_from_hexpm, "~> 0.3.0"},
# {:dep_from_git, git: "https://github.com/elixir-lang/my_dep.git", tag: "0.1.0"}
]
end
end

View File

@ -0,0 +1,39 @@
defmodule CollatzConjectureTest do
use ExUnit.Case
test "zero steps for one" do
assert CollatzConjecture.calc(1) == 0
end
test "zero is an error" do
assert_raise FunctionClauseError, fn -> CollatzConjecture.calc(0) end
end
test "divide if even" do
assert CollatzConjecture.calc(16) == 4
end
test "even and odd steps" do
assert CollatzConjecture.calc(12) == 9
end
test "Large number of even and odd steps" do
assert CollatzConjecture.calc(1_000_000) == 152
end
test "start with odd step" do
assert CollatzConjecture.calc(21) == 7
end
test "more steps than starting number" do
assert CollatzConjecture.calc(7) == 16
end
test "negative value is an error " do
assert_raise FunctionClauseError, fn -> CollatzConjecture.calc(-15) end
end
test "string as input value is an error " do
assert_raise FunctionClauseError, fn -> CollatzConjecture.calc("fubar") end
end
end

View File

@ -0,0 +1,2 @@
ExUnit.start()
ExUnit.configure(exclude: :pending, trace: true)

View File

@ -0,0 +1,22 @@
{
"authors": [
"neenjaw"
],
"contributors": [
"angelikatyborska",
"NobbZ"
],
"files": {
"solution": [
"lib/dna.ex"
],
"test": [
"test/dna_test.exs"
],
"exemplar": [
".meta/exemplar.ex"
]
},
"language_versions": ">=1.10",
"blurb": "Learn about bitstrings and tail call recursion by encoding DNA sequences as binary data."
}

View File

@ -0,0 +1 @@
{"track":"elixir","exercise":"dna-encoding","id":"b36d8706517343e99bd8187edf2b3be6","url":"https://exercism.org/tracks/elixir/exercises/dna-encoding","handle":"halfdan","is_requester":true,"auto_approve":false}

View File

@ -0,0 +1,4 @@
# Used by "mix format"
[
inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"]
]

24
elixir/dna-encoding/.gitignore vendored Normal file
View File

@ -0,0 +1,24 @@
# The directory Mix will write compiled artifacts to.
/_build/
# If you run "mix test --cover", coverage assets end up here.
/cover/
# The directory Mix downloads your dependencies sources to.
/deps/
# Where third-party dependencies like ExDoc output generated docs.
/doc/
# Ignore .fetch files in case you like to edit your project deps locally.
/.fetch
# If the VM crashes, it generates a dump, let's ignore it too.
erl_crash.dump
# Also ignore archive artifacts (built via "mix archive.build").
*.ez
# Ignore package tarball (built via "mix hex.build").
bitstrings-*.tar

View File

@ -0,0 +1,75 @@
# Help
## Running the tests
From the terminal, change to the base directory of the exercise then execute the tests with:
```bash
$ mix test
```
This will execute the test file found in the `test` subfolder -- a file ending in `_test.exs`
Documentation:
* [`mix test` - Elixir's test execution tool](https://hexdocs.pm/mix/Mix.Tasks.Test.html)
* [`ExUnit` - Elixir's unit test library](https://hexdocs.pm/ex_unit/ExUnit.html)
## Pending tests
In test suites of practice exercises, all but the first test have been tagged to be skipped.
Once you get a test passing, you can unskip the next one by commenting out the relevant `@tag :pending` with a `#` symbol.
For example:
```elixir
# @tag :pending
test "shouting" do
assert Bob.hey("WATCH OUT!") == "Whoa, chill out!"
end
```
If you wish to run all tests at once, you can include all skipped test by using the `--include` flag on the `mix test` command:
```bash
$ mix test --include pending
```
Or, you can enable all the tests by commenting out the `ExUnit.configure` line in the file `test/test_helper.exs`.
```elixir
# ExUnit.configure(exclude: :pending, trace: true)
```
## Useful `mix test` options
* `test/<FILE>.exs:LINENUM` - runs only a single test, the test from `<FILE>.exs` whose definition is on line `LINENUM`
* `--failed` - runs only tests that failed the last time they ran
* `--max-failures` - the suite stops evaluating tests when this number of test failures
is reached
* `--seed 0` - disables randomization so the tests in a single file will always be ran
in the same order they were defined in
## Submitting your solution
You can submit your solution using the `exercism submit lib/dna.ex` command.
This command will upload your solution to the Exercism website and print the solution page's URL.
It's possible to submit an incomplete solution which allows you to:
- See how others have completed the exercise
- Request help from a mentor
## Need to get help?
If you'd like help solving the exercise, check the following pages:
- The [Elixir track's documentation](https://exercism.org/docs/tracks/elixir)
- [Exercism's support channel on gitter](https://gitter.im/exercism/support)
- The [Frequently Asked Questions](https://exercism.org/docs/using/faqs)
Should those resources not suffice, you could submit your (incomplete) solution to request mentoring.
If you're stuck on something, it may help to look at some of the [available resources](https://exercism.org/docs/tracks/elixir/resources) out there where answers might be found.
If you can't find what you're looking for in the documentation, feel free to ask help in the Exercism's BEAM [gitter channel](https://gitter.im/exercism/xerlang).

View File

@ -0,0 +1,45 @@
# Hints
## General
- Use `?` to work with the character [code points][codepoint].
- `\s` can be used to represent a space.
- Use [integer binary notation][integer-literal] for working with the codes.
- Try to use the tail call recursion strategy.
## 1. Encode nucleic acid to binary value
- This function needs to map one integer to another.
- Making use of multiple clause functions may make this easier by breaking it down.
## 2. Decode the binary value to the nucleic acid
- This function is the opposite of part 1's function.
- Making use of multiple clause functions may make this easier by breaking it down.
## 3. Encode a DNA charlist
- Create a recursive function which takes a code point from the charlist and recursively builds the bitstring result.
- Remember, a [charlist][charlist] is a list of [integer code points][codepoint].
- You can get the first and remaining items from a list using a build in [`Kernel` module][kernel] function.
- You can also pattern match on a list using the [`[head | tail]`][list] notation.
- Use multiple clause functions to separate the base case from the recursive cases.
- Do not forget to specify the types of bitstring segments using the `::` operator.
## 4. Decode a DNA bitstring
- Create a recursive function which [matches the first 4 bits][bitstring-matching] from the [bitstring][bitstring] and recursively builds the [charlist][charlist] result.
- Remember the [bitstring special form][bitstring-form] can be used for matching on bitstrings.
- Do not forget to specify the types of bitstring segments using the `::` operator.
- You will need to reverse the accumulator at the end. Write a private tail-recursive `reverse` function to do that and use it in the base-case of the `decode` function.
[integer-literal]: https://hexdocs.pm/elixir/master/syntax-reference.html#integers-in-other-bases-and-unicode-code-points
[codepoint]: https://elixir-lang.org/getting-started/binaries-strings-and-char-lists.html#unicode-and-code-points
[charlist]: https://elixir-lang.org/getting-started/binaries-strings-and-char-lists.html#charlists
[bitstring]: https://elixir-lang.org/getting-started/binaries-strings-and-char-lists.html#bitstrings
[bitstring-form]: https://hexdocs.pm/elixir/Kernel.SpecialForms.html#%3C%3C%3E%3E/1
[bitstring-matching]: https://hexdocs.pm/elixir/Kernel.SpecialForms.html#%3C%3C%3E%3E/1-binary-bitstring-matching
[type-operator]: https://hexdocs.pm/elixir/Kernel.SpecialForms.html#::/2
[recursion-tco]: https://en.wikipedia.org/wiki/Tail_call
[list]: https://hexdocs.pm/elixir/List.html#content
[kernel]: https://hexdocs.pm/elixir/Kernel.html#functions

View File

@ -0,0 +1,149 @@
# DNA Encoding
Welcome to DNA Encoding on Exercism's Elixir Track.
If you need help running the tests or submitting your code, check out `HELP.md`.
If you get stuck on the exercise, check out `HINTS.md`, but try and solve it without using those first :)
## Introduction
## Bitstrings
Working with binary data is an important concept in any language, and Elixir provides an elegant syntax to write, match, and construct binary data.
In Elixir, binary data is referred to as the bitstring type. The binary data*type* (not to be confused with binary data in general) is a specific form of a bitstring, which we will discuss in a later exercise.
Bitstring literals are defined using the bitstring special form `<<>>`. When defining a bitstring literal, it is defined in segments. Each segment has a value and type, separated by the `::` operator. The type specifies how many bits will be used to encode the value. If the value of the segment overflows the capacity of the segment's type, it will be truncated from the left.
```elixir
# This defines a bitstring with three segments of a single bit each
<<0::1, 1::1, 0::1>> == <<0::size(1), 1::size(1), 0::size(1)>>
# => true
<<0::1, 1::1, 0::1>> == <<2::size(3)>>
# => true
<<2::1>> == <<0::1>>
# => true because of value overflow
```
When writing binary integer literals, we can write them directly in base-2 notation by prefixing the literal with `0b`.
```elixir
value = 0b11111011011 = 2011
```
By default, bitstrings are displayed in chunks of 8 bits (a byte).
```
<<value::11>>
# => <<251, 3::size(3)>>
```
### Constructing
We can combine bitstrings stored in variables using the special form:
```elixir
first = <<0b110::3>>
second = <<0b001::3>>
combined = <<first::bitstring, second::bitstring>>
# => <<49::size(6)>>
```
### Pattern matching
Pattern matching can also be done to obtain the value from within the special form:
```elixir
<<value::4, rest::bitstring>> = <<0b01101001::8>>
value == 0b0110
# => true
```
## Tail Call Recursion
When [recursing][exercism-recursion] through enumerables (lists, bitstrings, strings), there are often two concerns:
- how much memory is required to store the trail of recursive function calls
- how to build the solution efficiently
To deal with these concerns an _accumulator_ may be used.
An accumulator is a variable that is passed along in addition to the data. It is used to pass the current state of the function's execution, from function call to function call, until the _base case_ is reached. In the base case, the accumulator is used to return the final value of the recursive function call.
Accumulators should be initialized by the function's author, not the function's user. To achieve this, declare two functions - a public function that takes just the necessary data as arguments and initializes the accumulator, and a private function that also takes an accumulator. In Elixir, it is a common pattern to prefix the private function's name with `do_`.
```elixir
# Count the length of a list without an accumulator
def count([]), do: 0
def count([_head | tail]), do: 1 + count(tail)
# Count the length of a list with an accumulator
def count(list), do: do_count(list, 0)
defp do_count([], count), do: count
defp do_count([_head | tail], count), do: do_count(tail, count + 1)
```
The usage of an accumulator allows us to turn recursive functions into _tail-recursive_ functions. A function is tail-recursive if the _last_ thing executed by the function is a call to itself.
[exercism-recursion]: https://exercism.org/tracks/elixir/concepts/recursion
## Instructions
In your DNA research lab, you have been working through various ways to compress your research data to save storage space. One teammate suggests converting the DNA data to a binary representation:
| Nucleic Acid | Code |
| ------------ | ------ |
| a space | `0000` |
| A | `0001` |
| C | `0010` |
| G | `0100` |
| T | `1000` |
You ponder this, as it will potentially halve the required data storage costs, but at the expense of human readability. You decide to write a module to encode and decode your data to benchmark your savings.
## 1. Encode nucleic acid to binary value
Implement `encode_nucleotide/1` to accept the code point for the nucleic acid and return the integer value of the encoded code.
```elixir
DNA.encode_nucleotide(?A)
# => 0b0001
```
## 2. Decode the binary value to the nucleic acid
Implement `decode_nucleotide/1` to accept the integer value of the encoded code and return the code point for the nucleic acid.
```elixir
DNA.decode_nucleotide(0b0001)
# => ?A
```
## 3. Encode a DNA charlist
Implement `encode/1` to accept a charlist representing nucleic acids and gaps and return a bitstring of the encoded data.
```elixir
DNA.encode('AC GT')
# => <<18, 4, 8::size(4)>>
```
## 4. Decode a DNA bitstring
Implement `decode/1` to accept a bitstring representing nucleic acids and gaps and return the decoded data as a charlist.
```elixir
DNA.decode(<<132, 2, 1::size(4)>>)
# => 'TG CA'
```
## Source
### Created by
- @neenjaw
### Contributed to by
- @angelikatyborska
- @NobbZ

View File

@ -0,0 +1,17 @@
defmodule DNA do
def encode_nucleotide(code_point) do
# Please implement the encode_nucleotide/1 function
end
def decode_nucleotide(encoded_code) do
# Please implement the decode_nucleotide/1 function
end
def encode(dna) do
# Please implement the encode/1 function
end
def decode(dna) do
# Please implement the decode/1 function
end
end

View File

@ -0,0 +1,28 @@
defmodule DNA.MixProject do
use Mix.Project
def project do
[
app: :dna,
version: "0.1.0",
# elixir: "~> 1.10",
start_permanent: Mix.env() == :prod,
deps: deps()
]
end
# Run "mix help compile.app" to learn about applications.
def application do
[
extra_applications: [:logger]
]
end
# Run "mix help deps" to learn about dependencies.
defp deps do
[
# {:dep_from_hexpm, "~> 0.3.0"},
# {:dep_from_git, git: "https://github.com/elixir-lang/my_dep.git", tag: "0.1.0"}
]
end
end

View File

@ -0,0 +1,71 @@
defmodule DNATest do
use ExUnit.Case
describe "encode to 4-bit encoding" do
@tag task_id: 1
test "?\\s to 0b0000", do: assert(DNA.encode_nucleotide(?\s) == 0b0000)
@tag task_id: 1
test "?A to 0b0001", do: assert(DNA.encode_nucleotide(?A) == 0b0001)
@tag task_id: 1
test "?C to 0b0010", do: assert(DNA.encode_nucleotide(?C) == 0b0010)
@tag task_id: 1
test "?G to 0b0100", do: assert(DNA.encode_nucleotide(?G) == 0b0100)
@tag task_id: 1
test "?T to 0b1000", do: assert(DNA.encode_nucleotide(?T) == 0b1000)
end
describe "decode to code point" do
@tag task_id: 2
test "0b0000 to ?\\s", do: assert(DNA.decode_nucleotide(0b0000) == ?\s)
@tag task_id: 2
test "0b0001 to ?A", do: assert(DNA.decode_nucleotide(0b0001) == ?A)
@tag task_id: 2
test "0b0010 to ?C", do: assert(DNA.decode_nucleotide(0b0010) == ?C)
@tag task_id: 2
test "0b0100 to ?G", do: assert(DNA.decode_nucleotide(0b0100) == ?G)
@tag task_id: 2
test "0b1000 to ?T", do: assert(DNA.decode_nucleotide(0b1000) == ?T)
end
describe "encoding" do
@tag task_id: 3
test "' '", do: assert(DNA.encode(' ') == <<0b0000::4>>)
@tag task_id: 3
test "'A'", do: assert(DNA.encode('A') == <<0b0001::4>>)
@tag task_id: 3
test "'C'", do: assert(DNA.encode('C') == <<0b0010::4>>)
@tag task_id: 3
test "'G'", do: assert(DNA.encode('G') == <<0b0100::4>>)
@tag task_id: 3
test "'T'", do: assert(DNA.encode('T') == <<0b1000::4>>)
@tag task_id: 3
test "' ACGT'",
do: assert(DNA.encode(' ACGT') == <<0b0000::4, 0b0001::4, 0b0010::4, 0b0100::4, 0b1000::4>>)
@tag task_id: 3
test "'TGCA '",
do: assert(DNA.encode('TGCA ') == <<0b1000::4, 0b0100::4, 0b0010::4, 0b0001::4, 0b0000::4>>)
end
describe "decoding" do
@tag task_id: 4
test "' '", do: assert(DNA.decode(<<0b0000::4>>) == ' ')
@tag task_id: 4
test "'A'", do: assert(DNA.decode(<<0b0001::4>>) == 'A')
@tag task_id: 4
test "'C'", do: assert(DNA.decode(<<0b0010::4>>) == 'C')
@tag task_id: 4
test "'G'", do: assert(DNA.decode(<<0b0100::4>>) == 'G')
@tag task_id: 4
test "'T'", do: assert(DNA.decode(<<0b1000::4>>) == 'T')
@tag task_id: 4
test "' ACGT'",
do: assert(DNA.decode(<<0b0000::4, 0b0001::4, 0b0010::4, 0b0100::4, 0b1000::4>>) == ' ACGT')
@tag task_id: 4
test "'TGCA '",
do: assert(DNA.decode(<<0b1000::4, 0b0100::4, 0b0010::4, 0b0001::4, 0b0000::4>>) == 'TGCA ')
end
end

View File

@ -0,0 +1,2 @@
ExUnit.start()
ExUnit.configure(exclude: :pending, trace: true, seed: 0)

View File

@ -0,0 +1,24 @@
{
"authors": [
"neenjaw"
],
"contributors": [
"angelikatyborska"
],
"files": {
"solution": [
"lib/guessing_game.ex"
],
"test": [
"test/guessing_game_test.exs"
],
"exemplar": [
".meta/exemplar.ex"
]
},
"language_versions": ">=1.10",
"forked_from": [
"fsharp/guessing-game"
],
"blurb": "Learn about multiple clause functions, guards, and default arguments by implementing a simple game in which the player needs to guess a secret number."
}

View File

@ -0,0 +1 @@
{"track":"elixir","exercise":"guessing-game","id":"f70876e9539441d6b6ec0053b3682657","url":"https://exercism.org/tracks/elixir/exercises/guessing-game","handle":"halfdan","is_requester":true,"auto_approve":false}

View File

@ -0,0 +1,4 @@
# Used by "mix format"
[
inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"]
]

24
elixir/guessing-game/.gitignore vendored Normal file
View File

@ -0,0 +1,24 @@
# The directory Mix will write compiled artifacts to.
/_build/
# If you run "mix test --cover", coverage assets end up here.
/cover/
# The directory Mix downloads your dependencies sources to.
/deps/
# Where third-party dependencies like ExDoc output generated docs.
/doc/
# Ignore .fetch files in case you like to edit your project deps locally.
/.fetch
# If the VM crashes, it generates a dump, let's ignore it too.
erl_crash.dump
# Also ignore archive artifacts (built via "mix archive.build").
*.ez
# Ignore package tarball (built via "mix hex.build").
multiple_clause_functions-*.tar

View File

@ -0,0 +1,75 @@
# Help
## Running the tests
From the terminal, change to the base directory of the exercise then execute the tests with:
```bash
$ mix test
```
This will execute the test file found in the `test` subfolder -- a file ending in `_test.exs`
Documentation:
* [`mix test` - Elixir's test execution tool](https://hexdocs.pm/mix/Mix.Tasks.Test.html)
* [`ExUnit` - Elixir's unit test library](https://hexdocs.pm/ex_unit/ExUnit.html)
## Pending tests
In test suites of practice exercises, all but the first test have been tagged to be skipped.
Once you get a test passing, you can unskip the next one by commenting out the relevant `@tag :pending` with a `#` symbol.
For example:
```elixir
# @tag :pending
test "shouting" do
assert Bob.hey("WATCH OUT!") == "Whoa, chill out!"
end
```
If you wish to run all tests at once, you can include all skipped test by using the `--include` flag on the `mix test` command:
```bash
$ mix test --include pending
```
Or, you can enable all the tests by commenting out the `ExUnit.configure` line in the file `test/test_helper.exs`.
```elixir
# ExUnit.configure(exclude: :pending, trace: true)
```
## Useful `mix test` options
* `test/<FILE>.exs:LINENUM` - runs only a single test, the test from `<FILE>.exs` whose definition is on line `LINENUM`
* `--failed` - runs only tests that failed the last time they ran
* `--max-failures` - the suite stops evaluating tests when this number of test failures
is reached
* `--seed 0` - disables randomization so the tests in a single file will always be ran
in the same order they were defined in
## Submitting your solution
You can submit your solution using the `exercism submit lib/guessing_game.ex` command.
This command will upload your solution to the Exercism website and print the solution page's URL.
It's possible to submit an incomplete solution which allows you to:
- See how others have completed the exercise
- Request help from a mentor
## Need to get help?
If you'd like help solving the exercise, check the following pages:
- The [Elixir track's documentation](https://exercism.org/docs/tracks/elixir)
- [Exercism's support channel on gitter](https://gitter.im/exercism/support)
- The [Frequently Asked Questions](https://exercism.org/docs/using/faqs)
Should those resources not suffice, you could submit your (incomplete) solution to request mentoring.
If you're stuck on something, it may help to look at some of the [available resources](https://exercism.org/docs/tracks/elixir/resources) out there where answers might be found.
If you can't find what you're looking for in the documentation, feel free to ask help in the Exercism's BEAM [gitter channel](https://gitter.im/exercism/xerlang).

View File

@ -0,0 +1,33 @@
# Hints
## General
- In Elixir's ['Getting Started Guide'][guide] there is a nice refresher about named functions.
## 1. Make the response when the guess matches the secret number
- You can use a [guard][guard] to check if the numbers are the same with `===/2`.
## 2. Make the response when the guess is greater than the secret number
- You can add a [function clause][multiple-fn-clauses] and [guards][guard] to check if the guess is greater `>/2` than the secret number.
## 3. Make the response when the guess is less than the secret number
- You can add a [function clause][multiple-fn-clauses] and [guards][guard] to check if the guess is less than `</2` the secret number.
## 4. Make the responses when the guess is one more or one less than the secret number
- You can add a [function clause][multiple-fn-clauses] and [guards][guard] to check if the guess is one less or one more than the secret number.
- Guards expressions can use `and/2`, or `or/2` to combine boolean expressions.
- Pay attention to the order of the function clauses.
## 5. Make the response when there is no guess
- You can make use of a [default argument][default-arg] for a guess. The default value does not have to be an integer.
- Use a function header before all the other function clauses to define the default argument.
[default-arg]: https://elixir-lang.org/getting-started/modules-and-functions.html#default-arguments
[guard]: https://hexdocs.pm/elixir/master/Kernel.html#guards
[guide]: https://elixir-lang.org/getting-started/modules-and-functions.html#named-functions
[multiple-fn-clauses]: https://elixir-lang.org/getting-started/modules-and-functions.html#named-functions

View File

@ -0,0 +1,131 @@
# Guessing Game
Welcome to Guessing Game on Exercism's Elixir Track.
If you need help running the tests or submitting your code, check out `HELP.md`.
If you get stuck on the exercise, check out `HINTS.md`, but try and solve it without using those first :)
## Introduction
## Multiple Clause Functions
Elixir facilitates **Open-Close Principle** practices by allowing functions to have multiple clauses, so instead of sprawling and hard-coded control-logic, pointed functions can be written to add/remove behavior easily.
Elixir offers _multiple function clauses_ and _guards_ to write:
```elixir
def number(n) when n == 7 do
"Awesome, that's my favorite"
end
def number(_n) do
"That's not my favorite"
end
```
At run-time, Elixir will test, from top to bottom of the source file, which function clause to invoke.
Variables that are unused should be prefixed with an underscore.
## Guards
Guards are used to prevent Elixir from invoking functions based on evaluation of the arguments by guard functions. Guards begin with the `when` keyword, followed by a boolean expression. Guard functions are special functions which:
- Must be pure and not mutate any global states.
- Must return strict `true` or `false` values.
A list of common guards can be found in the [Elixir documentation][kernel-guards]. It includes type checks, basic arithmetic, comparisons, and strictly boolean operators.
## Default Arguments
Functions may declare default values for one or more arguments. Let's consider this function:
```elixir
def number(n \\ 13), do: "That's not my favorite"
```
When compiled, Elixir creates a function definition for `number/0` (no arguments), and `number/1` (one argument).
If more than one argument has default values, the default values will be applied to the function from left to right to fill in for missing arguments.
If the function has more than one clause, the default arguments should be defined in a function header (a function without a body) before the function clauses:
```elixir
def number(n \\ 13)
def number(n) when n < 10, do: "Dream bigger!"
def number(n) when n > 100, do: "Not that big..."
```
[kernel-guards]: https://hexdocs.pm/elixir/master/Kernel.html#guards
## Instructions
You are creating a trivial online game where a friend can guess a secret number. You want to give some feedback, but not give away the answer with a guess. You need to devise a function to provide different responses depending on how the guess relates to the secret number.
| Condition | Response |
| ------------------------------------------------------------- | -------------- |
| When the guess matches the secret number | "Correct" |
| When the guess is one more or one less than the secret number | "So close" |
| When the guess is greater than the secret number | "Too high" |
| When the guess is less than the secret number | "Too low" |
| When a guess isn't made | "Make a guess" |
All guesses and secret numbers are integer numbers.
## 1. Make the response when the guess matches the secret number
Implement the `compare/2` function which takes two arguments, `secret_number` and `guess`, which are both integers.
```elixir
GuessingGame.compare(5, 5)
# => "Correct"
```
## 2. Make the response when the guess is greater than the secret number
Modify the `compare` function to respond to guesses that are higher than the secret number.
```elixir
GuessingGame.compare(5, 8)
# => "Too high"
```
## 3. Make the response when the guess is less than the secret number
Modify the `compare` function to respond to guesses that are lower than the secret number.
```elixir
GuessingGame.compare(5, 2)
# => "Too low"
```
## 4. Make the responses when the guess is one more or one less than the secret number
Modify the `compare` function to respond to guesses that are close to the secret number.
```elixir
GuessingGame.compare(5, 6)
# => "So close"
GuessingGame.compare(5, 4)
# => "So close"
```
## 5. Make the response when there is no guess
Modify the `compare` function to respond to a lack of guess.
```elixir
GuessingGame.compare(5)
# => "Make a guess"
GuessingGame.compare(5, :no_guess)
# => "Make a guess"
```
## Source
### Created by
- @neenjaw
### Contributed to by
- @angelikatyborska

View File

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

View File

@ -0,0 +1,28 @@
defmodule GuessingGame.MixProject do
use Mix.Project
def project do
[
app: :guessing_game,
version: "0.1.0",
# elixir: "~> 1.10",
start_permanent: Mix.env() == :prod,
deps: deps()
]
end
# Run "mix help compile.app" to learn about applications.
def application do
[
extra_applications: [:logger]
]
end
# Run "mix help deps" to learn about dependencies.
defp deps do
[
# {:dep_from_hexpm, "~> 0.3.0"},
# {:dep_from_git, git: "https://github.com/elixir-lang/my_dep.git", tag: "0.1.0"}
]
end
end

View File

@ -0,0 +1,38 @@
defmodule GuessingGameTest do
use ExUnit.Case
@tag task_id: 1
test "correct when the guessed number equals secret" do
assert GuessingGame.compare(7, 7) == "Correct"
end
@tag task_id: 2
test "too high when guessed number is greater than the secret" do
assert GuessingGame.compare(9, 18) == "Too high"
end
@tag task_id: 3
test "too low when guessed number is less than the secret" do
assert GuessingGame.compare(42, 30) == "Too low"
end
@tag task_id: 4
test "so close when guess differs from secret by -1" do
assert GuessingGame.compare(64, 63) == "So close"
end
@tag task_id: 4
test "so close when guess differs from secret by +1" do
assert GuessingGame.compare(52, 53) == "So close"
end
@tag task_id: 5
test "when no guess is supplied, ask the player to make a guess" do
assert GuessingGame.compare(15) == "Make a guess"
end
@tag task_id: 5
test "when the atom :no_guess is supplied, ask the player to make a guess" do
assert GuessingGame.compare(16, :no_guess) == "Make a guess"
end
end

View File

@ -0,0 +1,2 @@
ExUnit.start()
ExUnit.configure(exclude: :pending, trace: true, seed: 0)

View File

@ -0,0 +1,21 @@
{
"authors": [
"neenjaw"
],
"contributors": [
"angelikatyborska"
],
"files": {
"solution": [
"lib/kitchen_calculator.ex"
],
"test": [
"test/kitchen_calculator_test.exs"
],
"exemplar": [
".meta/exemplar.ex"
]
},
"language_versions": ">=1.10",
"blurb": "Learn about tuples and pattern matching by converting common US baking measurements to the metric system."
}

View File

@ -0,0 +1 @@
{"track":"elixir","exercise":"kitchen-calculator","id":"fba1265584b9449783f6074ce0c8ceb8","url":"https://exercism.org/tracks/elixir/exercises/kitchen-calculator","handle":"halfdan","is_requester":true,"auto_approve":false}

View File

@ -0,0 +1,4 @@
# Used by "mix format"
[
inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"]
]

24
elixir/kitchen-calculator/.gitignore vendored Normal file
View File

@ -0,0 +1,24 @@
# The directory Mix will write compiled artifacts to.
/_build/
# If you run "mix test --cover", coverage assets end up here.
/cover/
# The directory Mix downloads your dependencies sources to.
/deps/
# Where third-party dependencies like ExDoc output generated docs.
/doc/
# Ignore .fetch files in case you like to edit your project deps locally.
/.fetch
# If the VM crashes, it generates a dump, let's ignore it too.
erl_crash.dump
# Also ignore archive artifacts (built via "mix archive.build").
*.ez
# Ignore package tarball (built via "mix hex.build").
tuples-*.tar

View File

@ -0,0 +1,75 @@
# Help
## Running the tests
From the terminal, change to the base directory of the exercise then execute the tests with:
```bash
$ mix test
```
This will execute the test file found in the `test` subfolder -- a file ending in `_test.exs`
Documentation:
* [`mix test` - Elixir's test execution tool](https://hexdocs.pm/mix/Mix.Tasks.Test.html)
* [`ExUnit` - Elixir's unit test library](https://hexdocs.pm/ex_unit/ExUnit.html)
## Pending tests
In test suites of practice exercises, all but the first test have been tagged to be skipped.
Once you get a test passing, you can unskip the next one by commenting out the relevant `@tag :pending` with a `#` symbol.
For example:
```elixir
# @tag :pending
test "shouting" do
assert Bob.hey("WATCH OUT!") == "Whoa, chill out!"
end
```
If you wish to run all tests at once, you can include all skipped test by using the `--include` flag on the `mix test` command:
```bash
$ mix test --include pending
```
Or, you can enable all the tests by commenting out the `ExUnit.configure` line in the file `test/test_helper.exs`.
```elixir
# ExUnit.configure(exclude: :pending, trace: true)
```
## Useful `mix test` options
* `test/<FILE>.exs:LINENUM` - runs only a single test, the test from `<FILE>.exs` whose definition is on line `LINENUM`
* `--failed` - runs only tests that failed the last time they ran
* `--max-failures` - the suite stops evaluating tests when this number of test failures
is reached
* `--seed 0` - disables randomization so the tests in a single file will always be ran
in the same order they were defined in
## Submitting your solution
You can submit your solution using the `exercism submit lib/kitchen_calculator.ex` command.
This command will upload your solution to the Exercism website and print the solution page's URL.
It's possible to submit an incomplete solution which allows you to:
- See how others have completed the exercise
- Request help from a mentor
## Need to get help?
If you'd like help solving the exercise, check the following pages:
- The [Elixir track's documentation](https://exercism.org/docs/tracks/elixir)
- [Exercism's support channel on gitter](https://gitter.im/exercism/support)
- The [Frequently Asked Questions](https://exercism.org/docs/using/faqs)
Should those resources not suffice, you could submit your (incomplete) solution to request mentoring.
If you're stuck on something, it may help to look at some of the [available resources](https://exercism.org/docs/tracks/elixir/resources) out there where answers might be found.
If you can't find what you're looking for in the documentation, feel free to ask help in the Exercism's BEAM [gitter channel](https://gitter.im/exercism/xerlang).

View File

@ -0,0 +1,30 @@
# Hints
## General
- [Tuples][tuple-module] are data structures which are arranged in contiguous memory and can hold any data-type.
- Atoms may be used to denote finite states, as this exercise uses `:cup`, `:fluid_ounce`, `:teaspoon`, `:tablespoon`, `:milliliter` to denote the units used.
- You may use [`Kernel`][elem] or [`Tuple`][tuple-module] functions or pattern matching to manipulate the tuples.
## 1. Get the numeric component from a volume-pair
- Consider using [a `Kernel` module function][elem] to return the volume-pair's numeric component.
## 2. Convert the volume-pair to milliliters
- Use [multiple clause functions][multi-clause] and [pattern matching][pattern-matching] to reduce conditional control flow logic.
- Implement the function for all units to milliliters, including milliliters to milliliters.
## 3. Convert the milliliter volume-pair to another unit
- Use [multiple clause functions][multi-clause] and [pattern matching][pattern-matching] to reduce conditional control flow logic.
- Implement the function for all units to milliliters, including milliliters to milliliters.
## 4. Convert from any unit to any unit
- Reuse the functions already created to perform the conversion in the `convert/2` function.
[elem]: https://hexdocs.pm/elixir/Kernel.html#elem/2
[multi-clause]: https://elixir-lang.org/getting-started/modules-and-functions.html#named-functions
[tuple-module]: https://hexdocs.pm/elixir/Tuple.html
[pattern-matching]: https://medium.com/rebirth-delivery/how-to-use-elixir-pattern-matched-functions-arguments-a793733acc6d

View File

@ -0,0 +1,143 @@
# Kitchen Calculator
Welcome to Kitchen Calculator on Exercism's Elixir Track.
If you need help running the tests or submitting your code, check out `HELP.md`.
If you get stuck on the exercise, check out `HINTS.md`, but try and solve it without using those first :)
## Introduction
## Tuples
In Elixir, a tuple is a data structure which organizes data, holding a fixed number of items of any type, but without explicit names for each element. Tuples are often used in Elixir for memory read-intensive operations, since read-access of an element is a constant-time operation. They are not usually used when elements may need to be added/removed dynamically because rather than modifying the existing tuple, a new tuple is created which requires memory to be allocated upfront.
In practice, tuples are created in Elixir using curly braces. Elements in a tuple can be individually accessed using the `elem/2` function using 0-based indexing:
```elixir
empty_tuple = {}
one_element_tuple = {1}
multiple_element_tuple = {1, :a, "hello"}
elem(multiple_element_tuple, 2)
# => "hello"
```
### Tuples as grouped information
Tuples are often used in practice to represent grouped information.
```elixir
Float.ratio(0.25)
# => {1, 4} indicating the numerator and denominator of the fraction ¼
```
## Pattern Matching
The use of pattern matching is dominant in assertive, idiomatic Elixir code. You might recall that `=/2` is described as a match operator rather than as an assignment operator. When using the match operator, if the pattern on the left matches the right, any variables on the left are bound, and the value of the right side is returned. A `MatchError` is raised if there is no match.
```elixir
2 = 2
# => 2
# Literals can be matched if they are the same
2 = 3
# => ** (MatchError) no match of right hand side value: 3
{_, denominator} = Float.ratio(0.25)
# => {1, 4}
# the variable `denominator` is bound to the value 4
```
Remember, matches occur from the right side to the left side.
In the last example if we don't need a variable in a pattern match, we can discard it by referencing `_`. Any variable starting with an `_` is not tracked by the runtime.
### Pattern matching in named functions
Pattern matching is also a useful tool when creating multiple function clauses. Pattern matching can be used on the functions' arguments which then determines which function clause to invoke -- starting from the top of the file down until the first match. Variables may be bound in a function head and used in the function body.
```elixir
defmodule Example do
def named_function(:a = variable_a) do
{variable_a, 1}
end
def named_function(:b = variable_b) do
{variable_b, 2}
end
end
Example.named_function(:a)
# => {:a, 1}
Example.named_function(:b)
# => {:b, 2}
Example.named_function(:c)
# => ** (FunctionClauseError) no function clause matching in Example.named_function/1
```
## Instructions
While preparing to bake cookies for your friends, you have found that you have to convert some of the measurements used in the recipe. Being only familiar with the metric system, you need to come up with a way to convert common US baking measurements to milliliters (mL) for your own ease.
Use this conversion chart for your solution:
| Unit to convert | volume | in milliliters (mL) |
| --------------- | ------ | ------------------- |
| mL | 1 | 1 |
| US cup | 1 | 240 |
| US fluid ounce | 1 | 30 |
| US teaspoon | 1 | 5 |
| US tablespoon | 1 | 15 |
Being a talented programmer in training, you decide to use milliliters as a transition unit to facilitate the conversion from any unit listed to any other (even itself).
## 1. Get the numeric component from a volume-pair
Implement the `KitchenCalculator.get_volume/1` function. Given a volume-pair tuple, it should return just the numeric component.
```elixir
KitchenCalculator.get_volume({:cup, 2.0})
# => 2.0
```
## 2. Convert the volume-pair to milliliters
Implement the `KitchenCalculator.to_milliliter/1` function. Given a volume-pair tuple, it should convert the volume to milliliters using the conversion chart.
Use multiple function clauses and pattern matching to create the functions for each unit. The atoms used to denote each unit are: `:cup`, `:fluid_ounce`, `:teaspoon`, `:tablespoon`, `:milliliter`. Return the result of the conversion wrapped in a tuple.
```elixir
KitchenCalculator.to_milliliter({:cup, 2.5})
# => {:milliliter, 600.0}
```
## 3. Convert the milliliter volume-pair to another unit
Implement the `KitchenCalculator.from_milliliter/2` function. Given a volume-pair tuple and the desired unit, it should convert the volume to the desired unit using the conversion chart.
Use multiple function clauses and pattern matching to create the functions for each unit. The atoms used to denote each unit are: `:cup`, `:fluid_ounce`, `:teaspoon`, `:tablespoon`, `:milliliter`
```elixir
KitchenCalculator.from_milliliter({:milliliter, 1320.0}, :cup)
# => {:cup, 5.5}
```
## 4. Convert from any unit to any unit
Implement the `KitchenCalculator.convert/2` function. Given a volume-pair tuple and the desired unit, it should convert the given volume to the desired unit.
```elixir
KitchenCalculator.convert({:teaspoon, 9.0}, :tablespoon)
# => {:tablespoon, 3.0}
```
## Source
### Created by
- @neenjaw
### Contributed to by
- @angelikatyborska

View File

@ -0,0 +1,31 @@
defmodule KitchenCalculator do
def get_volume(volume_pair) do
elem(volume_pair, 1)
end
def to_milliliter(volume_pair) do
volume = get_volume(volume_pair)
case elem(volume_pair, 0) do
:milliliter -> volume
:fluid_ounce -> 30 * volume
:cup -> 240 * volume
:teaspoon -> 5 * volume
:tablespoon -> 15 * volume
_ -> -1
end
end
def from_milliliter({:milliliter, volume}, unit) do
case unit do
:cup -> volume / 240
:fluid_ounce -> volume / 30
:teaspoon -> volume / 5
:tablespoon -> volume / 15
_ -> volume
end
end
def convert({unit1, volume}, unit2) do
from_milliliter({:milliliter, to_milliliter({unit1, volume})}, unit2)
end
end

View File

@ -0,0 +1,28 @@
defmodule KitchenCalculator.MixProject do
use Mix.Project
def project do
[
app: :kitchen_calculator,
version: "0.1.0",
# elixir: "~> 1.10",
start_permanent: Mix.env() == :prod,
deps: deps()
]
end
# Run "mix help compile.app" to learn about applications.
def application do
[
extra_applications: [:logger]
]
end
# Run "mix help deps" to learn about dependencies.
defp deps do
[
# {:dep_from_hexpm, "~> 0.3.0"},
# {:dep_from_git, git: "https://github.com/elixir-lang/my_dep.git", tag: "0.1.0"}
]
end
end

View File

@ -0,0 +1,109 @@
defmodule KitchenCalculatorTest do
use ExUnit.Case
describe "get volume from tuple pair" do
@tag task_id: 1
test "get cups" do
assert KitchenCalculator.get_volume({:cup, 1}) == 1
end
@tag task_id: 1
test "get fluid ounces" do
assert KitchenCalculator.get_volume({:fluid_ounce, 2}) == 2
end
@tag task_id: 1
test "get teaspoons" do
assert KitchenCalculator.get_volume({:teaspoon, 3}) == 3
end
@tag task_id: 1
test "get tablespoons" do
assert KitchenCalculator.get_volume({:tablespoon, 4}) == 4
end
@tag task_id: 1
test "get milliliters" do
assert KitchenCalculator.get_volume({:milliliter, 5}) == 5
end
end
describe "convert to milliliters from" do
@tag task_id: 2
test "milliliters" do
assert KitchenCalculator.to_milliliter({:milliliter, 3}) == {:milliliter, 3}
end
@tag task_id: 2
test "cups" do
assert KitchenCalculator.to_milliliter({:cup, 3}) == {:milliliter, 720}
end
@tag task_id: 2
test "fluid ounces" do
assert KitchenCalculator.to_milliliter({:fluid_ounce, 100}) == {:milliliter, 3000}
end
@tag task_id: 2
test "teaspoon" do
assert KitchenCalculator.to_milliliter({:teaspoon, 3}) == {:milliliter, 15}
end
@tag task_id: 2
test "tablespoon" do
assert KitchenCalculator.to_milliliter({:tablespoon, 3}) == {:milliliter, 45}
end
end
describe "convert from milliliters to" do
@tag task_id: 3
test "milliliters" do
assert KitchenCalculator.from_milliliter({:milliliter, 4}, :milliliter) == {:milliliter, 4}
end
@tag task_id: 3
test "cups" do
assert KitchenCalculator.from_milliliter({:milliliter, 840}, :cup) == {:cup, 3.5}
end
@tag task_id: 3
test "fluid ounces" do
assert KitchenCalculator.from_milliliter({:milliliter, 4522.5}, :fluid_ounce) ==
{:fluid_ounce, 150.75}
end
@tag task_id: 3
test "teaspoon" do
assert KitchenCalculator.from_milliliter({:milliliter, 61.25}, :teaspoon) ==
{:teaspoon, 12.25}
end
@tag task_id: 3
test "tablespoon" do
assert KitchenCalculator.from_milliliter({:milliliter, 71.25}, :tablespoon) ==
{:tablespoon, 4.75}
end
end
describe "convert from x to y:" do
@tag task_id: 4
test "teaspoon to tablespoon" do
assert KitchenCalculator.convert({:teaspoon, 15}, :tablespoon) == {:tablespoon, 5}
end
@tag task_id: 4
test "cups to fluid ounces" do
assert KitchenCalculator.convert({:cup, 4}, :fluid_ounce) == {:fluid_ounce, 32}
end
@tag task_id: 4
test "fluid ounces to teaspoons" do
assert KitchenCalculator.convert({:fluid_ounce, 4}, :teaspoon) == {:teaspoon, 24}
end
@tag task_id: 4
test "tablespoons to cups" do
assert KitchenCalculator.convert({:tablespoon, 320}, :cup) == {:cup, 20}
end
end
end

View File

@ -0,0 +1,2 @@
ExUnit.start()
ExUnit.configure(exclude: :pending, trace: true, seed: 0)

View File

@ -0,0 +1,35 @@
{
"authors": [
"rubysolo"
],
"contributors": [
"angelikatyborska",
"Cohen-Carlisle",
"dalexj",
"devonestes",
"glennular",
"jinyeow",
"korbin",
"kytrinyx",
"lpil",
"neenjaw",
"parkerl",
"sotojuan",
"Teapane",
"waiting-for-dev"
],
"files": {
"solution": [
"lib/year.ex"
],
"test": [
"test/year_test.exs"
],
"example": [
".meta/example.ex"
]
},
"blurb": "Given a year, report if it is a leap year.",
"source": "JavaRanch Cattle Drive, exercise 3",
"source_url": "http://www.javaranch.com/leap.jsp"
}

View File

@ -0,0 +1 @@
{"track":"elixir","exercise":"leap","id":"0621ad4832e546b6b1a1e5e07af0b3f0","url":"https://exercism.org/tracks/elixir/exercises/leap","handle":"halfdan","is_requester":true,"auto_approve":false}

View File

@ -0,0 +1,4 @@
# Used by "mix format"
[
inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"]
]

24
elixir/leap/.gitignore vendored Normal file
View File

@ -0,0 +1,24 @@
# The directory Mix will write compiled artifacts to.
/_build/
# If you run "mix test --cover", coverage assets end up here.
/cover/
# The directory Mix downloads your dependencies sources to.
/deps/
# Where third-party dependencies like ExDoc output generated docs.
/doc/
# Ignore .fetch files in case you like to edit your project deps locally.
/.fetch
# If the VM crashes, it generates a dump, let's ignore it too.
erl_crash.dump
# Also ignore archive artifacts (built via "mix archive.build").
*.ez
# Ignore package tarball (built via "mix hex.build").
leap-*.tar

75
elixir/leap/HELP.md Normal file
View File

@ -0,0 +1,75 @@
# Help
## Running the tests
From the terminal, change to the base directory of the exercise then execute the tests with:
```bash
$ mix test
```
This will execute the test file found in the `test` subfolder -- a file ending in `_test.exs`
Documentation:
* [`mix test` - Elixir's test execution tool](https://hexdocs.pm/mix/Mix.Tasks.Test.html)
* [`ExUnit` - Elixir's unit test library](https://hexdocs.pm/ex_unit/ExUnit.html)
## Pending tests
In test suites of practice exercises, all but the first test have been tagged to be skipped.
Once you get a test passing, you can unskip the next one by commenting out the relevant `@tag :pending` with a `#` symbol.
For example:
```elixir
# @tag :pending
test "shouting" do
assert Bob.hey("WATCH OUT!") == "Whoa, chill out!"
end
```
If you wish to run all tests at once, you can include all skipped test by using the `--include` flag on the `mix test` command:
```bash
$ mix test --include pending
```
Or, you can enable all the tests by commenting out the `ExUnit.configure` line in the file `test/test_helper.exs`.
```elixir
# ExUnit.configure(exclude: :pending, trace: true)
```
## Useful `mix test` options
* `test/<FILE>.exs:LINENUM` - runs only a single test, the test from `<FILE>.exs` whose definition is on line `LINENUM`
* `--failed` - runs only tests that failed the last time they ran
* `--max-failures` - the suite stops evaluating tests when this number of test failures
is reached
* `--seed 0` - disables randomization so the tests in a single file will always be ran
in the same order they were defined in
## Submitting your solution
You can submit your solution using the `exercism submit lib/year.ex` command.
This command will upload your solution to the Exercism website and print the solution page's URL.
It's possible to submit an incomplete solution which allows you to:
- See how others have completed the exercise
- Request help from a mentor
## Need to get help?
If you'd like help solving the exercise, check the following pages:
- The [Elixir track's documentation](https://exercism.org/docs/tracks/elixir)
- [Exercism's support channel on gitter](https://gitter.im/exercism/support)
- The [Frequently Asked Questions](https://exercism.org/docs/using/faqs)
Should those resources not suffice, you could submit your (incomplete) solution to request mentoring.
If you're stuck on something, it may help to look at some of the [available resources](https://exercism.org/docs/tracks/elixir/resources) out there where answers might be found.
If you can't find what you're looking for in the documentation, feel free to ask help in the Exercism's BEAM [gitter channel](https://gitter.im/exercism/xerlang).

56
elixir/leap/README.md Normal file
View File

@ -0,0 +1,56 @@
# Leap
Welcome to Leap on Exercism's Elixir Track.
If you need help running the tests or submitting your code, check out `HELP.md`.
## Instructions
Given a year, report if it is a leap year.
The tricky thing here is that a leap year in the Gregorian calendar occurs:
```text
on every year that is evenly divisible by 4
except every year that is evenly divisible by 100
unless the year is also evenly divisible by 400
```
For example, 1997 is not a leap year, but 1996 is. 1900 is not a leap
year, but 2000 is.
## Notes
Though our exercise adopts some very simple rules, there is more to
learn!
For a delightful, four minute explanation of the whole leap year
phenomenon, go watch [this youtube video][video].
[video]: http://www.youtube.com/watch?v=xX96xng7sAE
## Source
### Created by
- @rubysolo
### Contributed to by
- @angelikatyborska
- @Cohen-Carlisle
- @dalexj
- @devonestes
- @glennular
- @jinyeow
- @korbin
- @kytrinyx
- @lpil
- @neenjaw
- @parkerl
- @sotojuan
- @Teapane
- @waiting-for-dev
### Based on
JavaRanch Cattle Drive, exercise 3 - http://www.javaranch.com/leap.jsp

16
elixir/leap/lib/year.ex Normal file
View File

@ -0,0 +1,16 @@
defmodule Year do
@doc """
Returns whether 'year' is a leap year.
A leap year occurs:
on every year that is evenly divisible by 4
except every year that is evenly divisible by 100
unless the year is also evenly divisible by 400
"""
@spec leap_year?(non_neg_integer) :: boolean
def leap_year?(year) when rem(year, 400) == 0, do: true
def leap_year?(year) when rem(year, 100) == 0, do: false
def leap_year?(year) when rem(year, 4) == 0, do: true
def leap_year?(_year), do: false
end

28
elixir/leap/mix.exs Normal file
View File

@ -0,0 +1,28 @@
defmodule Year.MixProject do
use Mix.Project
def project do
[
app: :year,
version: "0.1.0",
# elixir: "~> 1.8",
start_permanent: Mix.env() == :prod,
deps: deps()
]
end
# Run "mix help compile.app" to learn about applications.
def application do
[
extra_applications: [:logger]
]
end
# Run "mix help deps" to learn about dependencies.
defp deps do
[
# {:dep_from_hexpm, "~> 0.3.0"},
# {:dep_from_git, git: "https://github.com/elixir-lang/my_dep.git", tag: "0.1.0"}
]
end
end

View File

@ -0,0 +1,2 @@
ExUnit.start()
ExUnit.configure(exclude: :pending, trace: true)

View File

@ -0,0 +1,48 @@
defmodule YearTest do
use ExUnit.Case
# @tag :pending
test "year not divisible by 4 is common year" do
refute Year.leap_year?(2015)
end
@tag :pending
test "year divisible by 2, not divisible by 4 is common year" do
refute Year.leap_year?(1970)
end
@tag :pending
test "year divisible by 4, not divisible by 100 is leap year" do
assert Year.leap_year?(1996)
end
@tag :pending
test "year divisible by 4 and 5 is still a leap year" do
assert Year.leap_year?(1960)
end
@tag :pending
test "year divisible by 100, not divisible by 400 is common year" do
refute Year.leap_year?(2100)
end
@tag :pending
test "year divisible by 100 but not by 3 is still not a leap year" do
refute Year.leap_year?(1900)
end
@tag :pending
test "year divisible by 400 is leap year" do
assert Year.leap_year?(2000)
end
@tag :pending
test "year divisible by 400 but not by 125 is still a leap year" do
assert Year.leap_year?(2400)
end
@tag :pending
test "year divisible by 200, not divisible by 400 in common year" do
refute Year.leap_year?(1800)
end
end

View File

@ -0,0 +1,21 @@
{
"authors": [
"angelikatyborska"
],
"contributors": [
"neenjaw"
],
"files": {
"solution": [
"lib/library_fees.ex"
],
"test": [
"test/library_fees_test.exs"
],
"exemplar": [
".meta/exemplar.ex"
]
},
"language_versions": ">=1.10",
"blurb": "Learn about dates and time by calculating late fees for the local library."
}

View File

@ -0,0 +1 @@
{"track":"elixir","exercise":"library-fees","id":"f919bcf7474041d6b5dd2b3990192dd2","url":"https://exercism.org/tracks/elixir/exercises/library-fees","handle":"halfdan","is_requester":true,"auto_approve":false}

View File

@ -0,0 +1,4 @@
# Used by "mix format"
[
inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"]
]

24
elixir/library-fees/.gitignore vendored Normal file
View File

@ -0,0 +1,24 @@
# The directory Mix will write compiled artifacts to.
/_build/
# If you run "mix test --cover", coverage assets end up here.
/cover/
# The directory Mix downloads your dependencies sources to.
/deps/
# Where third-party dependencies like ExDoc output generated docs.
/doc/
# Ignore .fetch files in case you like to edit your project deps locally.
/.fetch
# If the VM crashes, it generates a dump, let's ignore it too.
erl_crash.dump
# Also ignore archive artifacts (built via "mix archive.build").
*.ez
# Ignore package tarball (built via "mix hex.build").
match_binary-*.tar

View File

@ -0,0 +1,75 @@
# Help
## Running the tests
From the terminal, change to the base directory of the exercise then execute the tests with:
```bash
$ mix test
```
This will execute the test file found in the `test` subfolder -- a file ending in `_test.exs`
Documentation:
* [`mix test` - Elixir's test execution tool](https://hexdocs.pm/mix/Mix.Tasks.Test.html)
* [`ExUnit` - Elixir's unit test library](https://hexdocs.pm/ex_unit/ExUnit.html)
## Pending tests
In test suites of practice exercises, all but the first test have been tagged to be skipped.
Once you get a test passing, you can unskip the next one by commenting out the relevant `@tag :pending` with a `#` symbol.
For example:
```elixir
# @tag :pending
test "shouting" do
assert Bob.hey("WATCH OUT!") == "Whoa, chill out!"
end
```
If you wish to run all tests at once, you can include all skipped test by using the `--include` flag on the `mix test` command:
```bash
$ mix test --include pending
```
Or, you can enable all the tests by commenting out the `ExUnit.configure` line in the file `test/test_helper.exs`.
```elixir
# ExUnit.configure(exclude: :pending, trace: true)
```
## Useful `mix test` options
* `test/<FILE>.exs:LINENUM` - runs only a single test, the test from `<FILE>.exs` whose definition is on line `LINENUM`
* `--failed` - runs only tests that failed the last time they ran
* `--max-failures` - the suite stops evaluating tests when this number of test failures
is reached
* `--seed 0` - disables randomization so the tests in a single file will always be ran
in the same order they were defined in
## Submitting your solution
You can submit your solution using the `exercism submit lib/library_fees.ex` command.
This command will upload your solution to the Exercism website and print the solution page's URL.
It's possible to submit an incomplete solution which allows you to:
- See how others have completed the exercise
- Request help from a mentor
## Need to get help?
If you'd like help solving the exercise, check the following pages:
- The [Elixir track's documentation](https://exercism.org/docs/tracks/elixir)
- [Exercism's support channel on gitter](https://gitter.im/exercism/support)
- The [Frequently Asked Questions](https://exercism.org/docs/using/faqs)
Should those resources not suffice, you could submit your (incomplete) solution to request mentoring.
If you're stuck on something, it may help to look at some of the [available resources](https://exercism.org/docs/tracks/elixir/resources) out there where answers might be found.
If you can't find what you're looking for in the documentation, feel free to ask help in the Exercism's BEAM [gitter channel](https://gitter.im/exercism/xerlang).

View File

@ -0,0 +1,47 @@
# Hints
## General
- Review the functions available in the [`NaiveDateTime` module][naive-date-time], the [`Date` module][date], and the [`Time` module][time].
## 1. Parse the stored datetimes
- There is a [built-in function][naive-date-time-from-iso8601] that parses an ISO8601 datetime string and returns a `NaiveDateTime` struct.
## 2. Determine if a book was checked out before noon
- You can define a `Time` literal using the [`~T` sigil][time-sigil].
- There is a [built-in function][naive-date-time-to-time] that changes a `NaiveDateTime` struct to a `Time` struct.
- There is a [built-in function][time-compare] that checks which one of two `Time` structs is bigger.
## 3. Calculate the return date
- A day has `24 * 60 * 60` seconds.
- There is a [built-in function][naive-date-time-add] that adds a given number of seconds to a `NaiveDateTime` struct.
- There is a [built-in function][naive-date-time-to-date] that changes a `NaiveDateTime` struct to a `Date` struct.
## 4. Determine how late the return of the book was
- There is a [built-in function][naive-date-time-to-date] that changes a `NaiveDateTime` struct to a `Date` struct.
- There is a [built-in function][date-diff] that calculates the difference in days between two `Date` structs.
## 5. Determine if the book was returned on a Monday
- There is a [built-in function][naive-date-time-to-date] that changes a `NaiveDateTime` struct to a `Date` struct.
- There is a [built-in function][date-day-of-week] that returns the day of week for a given `Date` struct.
## 6. Calculate the late fee
- Combine together all of the functions that you defined in previous steps.
[naive-date-time]: https://hexdocs.pm/elixir/NaiveDateTime.html
[time]: https://hexdocs.pm/elixir/Time.html
[date]: https://hexdocs.pm/elixir/Date.html
[naive-date-time-from-iso8601]: https://hexdocs.pm/elixir/NaiveDateTime.html#from_iso8601!/2
[naive-date-time-to-time]: https://hexdocs.pm/elixir/NaiveDateTime.html#to_time/1
[naive-date-time-to-date]: https://hexdocs.pm/elixir/NaiveDateTime.html#to_date/1
[naive-date-time-add]: https://hexdocs.pm/elixir/NaiveDateTime.html#add/3
[time-sigil]: https://hexdocs.pm/elixir/Kernel.html#sigil_T/2
[time-compare]: https://hexdocs.pm/elixir/Time.html#compare/2
[date-diff]: https://hexdocs.pm/elixir/Date.html#diff/2
[date-day-of-week]: https://hexdocs.pm/elixir/Date.html#day_of_week/2

View File

@ -0,0 +1,124 @@
# Library Fees
Welcome to Library Fees on Exercism's Elixir Track.
If you need help running the tests or submitting your code, check out `HELP.md`.
If you get stuck on the exercise, check out `HINTS.md`, but try and solve it without using those first :)
## Introduction
## Dates and Time
Elixir's standard library offers 4 different modules for working with dates and time, each with its own struct.
- The `Date` module. A `Date` struct can be created with the `~D` sigil.
```elixir
~D[2021-01-01]
```
- The `Time` module. A `Time` struct can be created with the `~T` sigil.
```elixir
~T[12:00:00]
```
- The `NaiveDateTime` module for datetimes without a timezone. A `NaiveDateTime` struct can be created with the `~N` sigil.
```elixir
~N[2021-01-01 12:00:00]
```
- The `DateTime` module for datetimes with a timezone. Using this module for timezones other than UTC requires an external dependency, a timezone database.
### Comparisons
To compare dates or times to one another, look for a `compare` or `diff` function in the corresponding module. Comparison operators such as `==`, `>`, and `<` _seem_ to work, but they don't do a correct semantic comparison for those structs.
## Instructions
Your librarian friend has asked you to extend her library software to automatically calculate late fees.
Her current system stores the exact date and time of a book checkout as an [ISO8601](https://en.wikipedia.org/wiki/ISO_8601) datetime string.
She runs a local library in a small town in Ghana, which uses the GMT timezone (UTC +0), doesn't use daylight saving time, and doesn't need to worry about other timezones.
## 1. Parse the stored datetimes
Implement the `LibraryFees.datetime_from_string/1` function. It should take an ISO8601 datetime string as an argument, and return a `NaiveDateTime` struct.
```elixir
LibraryFees.datetime_from_string("2021-01-01T13:30:45Z")
# => ~N[2021-01-01 13:30:45]
```
## 2. Determine if a book was checked out before noon
If a book was checked out before noon, the reader has 28 days to return it. If it was checked out at or after noon, it's 29 days.
Implement the `LibraryFees.before_noon?/1` function. It should take a `NaiveDateTime` struct and return a boolean.
```elixir
LibraryFees.before_noon?(~N[2021-01-12 08:23:03])
# => true
```
## 3. Calculate the return date
Based on the checkout datetime, calculate the return date.
Implement the `LibraryFees.return_date/1` function. It should take a `NaiveDateTime` struct and return a `Date` struct, either 28 or 29 days later.
```elixir
LibraryFees.return_date(~N[2020-11-28 15:55:33])
# => ~D[2020-12-27]
```
## 4. Determine how late the return of the book was
The library has a flat rate for late returns. To be able to calculate the fee, we need to know how many days after the return date the book was actually returned.
Implement the `LibraryFees.days_late/2` function. It should take a `Date` struct - the planned return datetime, and a `NaiveDateTime` struct - the actual return datetime.
If the actual return date is on an earlier or the same day as the planned return datetime, the function should return 0. Otherwise, the function should return the difference between those two dates in days.
The library tracks both the date and time of the actual return of the book for statistical purposes, but doesn't use the time when calculating late fees.
```elixir
LibraryFees.days_late(~D[2020-12-27], ~N[2021-01-03 09:23:36])
# => 7
```
## 5. Determine if the book was returned on a Monday
The library has a special offer for returning books on Mondays.
Implement the `LibraryFees.monday?/1` function. It should take a `NaiveDateTime` struct and return a boolean.
```elixir
LibraryFees.monday?(~N[2021-01-03 13:30:45Z])
# => false
```
## 6. Calculate the late fee
Implement the `LibraryFees.calculate_late_fee/3` function. It should take three arguments - two ISO8601 datetime strings, checkout datetime and actual return datetime, and the late fee for one day. It should return the total late fee according to how late the actual return of the book was.
Include the special Monday offer. If you return the book on Monday, your late fee is 50% off, rounded down.
```elixir
# Sunday, 7 days late
LibraryFees.calculate_late_fee("2020-11-28T15:55:33Z", "2021-01-03T13:30:45Z", 100)
# => 700
# one day later, Monday, 8 days late
LibraryFees.calculate_late_fee("2020-11-28T15:55:33Z", "2021-01-04T09:02:11Z", 100)
# => 400
```
## Source
### Created by
- @angelikatyborska
### Contributed to by
- @neenjaw

View File

@ -0,0 +1,39 @@
defmodule LibraryFees do
def datetime_from_string(string) do
{:ok, dt} = NaiveDateTime.from_iso8601(string)
dt
end
def before_noon?(datetime) do
datetime.hour < 12
end
def return_date(checkout_datetime) do
dt = NaiveDateTime.to_date(checkout_datetime)
case before_noon?(checkout_datetime) do
true -> Date.add(dt, 28)
_ -> Date.add(dt, 29)
end
end
def days_late(planned_return_date, actual_return_datetime) do
actual_return_datetime
|> Date.diff(planned_return_date)
|> max(0)
end
def monday?(datetime) do
Date.day_of_week(NaiveDateTime.to_date(datetime)) == 1
end
def calculate_late_fee(checkout, return, rate) do
checkout_datetime = datetime_from_string(checkout)
return_datetime = datetime_from_string(return)
raw_fee = days_late(return_date(checkout_datetime), return_datetime) * rate
if monday?(return_datetime) do
floor(raw_fee * 0.5)
else
raw_fee
end
end
end

View File

@ -0,0 +1,28 @@
defmodule CaptainsLog.MixProject do
use Mix.Project
def project do
[
app: :library_fees,
version: "0.1.0",
# elixir: "~> 1.10",
start_permanent: Mix.env() == :prod,
deps: deps()
]
end
# Run "mix help compile.app" to learn about applications.
def application do
[
extra_applications: [:logger]
]
end
# Run "mix help deps" to learn about dependencies.
defp deps do
[
# {:dep_from_hexpm, "~> 0.3.0"},
# {:dep_from_git, git: "https://github.com/elixir-lang/my_dep.git", tag: "0.1.0"}
]
end
end

View File

@ -0,0 +1,171 @@
defmodule LibraryFeesTest do
use ExUnit.Case
describe "datetime_from_string/1" do
@tag task_id: 1
test "returns NaiveDateTime" do
result = LibraryFees.datetime_from_string("2021-01-01T12:00:00Z")
assert result.__struct__ == NaiveDateTime
end
@tag task_id: 1
test "parses an ISO8601 string" do
result = LibraryFees.datetime_from_string("2019-12-24T13:15:45Z")
assert result == ~N[2019-12-24 13:15:45Z]
end
end
describe "before_noon?/1" do
@tag task_id: 2
test "returns true if the given NaiveDateTime is before 12:00" do
assert LibraryFees.before_noon?(~N[2020-06-06 11:59:59Z]) == true
end
@tag task_id: 2
test "returns false if the given NaiveDateTime is after 12:00" do
assert LibraryFees.before_noon?(~N[2021-01-03 12:01:01Z]) == false
end
@tag task_id: 2
test "returns false if the given NaiveDateTime is exactly at 12:00" do
assert LibraryFees.before_noon?(~N[2018-11-17 12:00:00Z]) == false
end
end
describe "return_date/1" do
@tag task_id: 3
test "adds 28 days if the given NaiveDateTime is before 12:00" do
result = LibraryFees.return_date(~N[2020-02-14 11:59:59Z])
assert result == ~D[2020-03-13]
end
@tag task_id: 3
test "adds 29 days if the given NaiveDateTime is after 12:00" do
result = LibraryFees.return_date(~N[2021-01-03 12:01:01Z])
assert result == ~D[2021-02-01]
end
@tag task_id: 3
test "adds 29 days if the given NaiveDateTime is exactly at 12:00" do
result = LibraryFees.return_date(~N[2018-12-01 12:00:00Z])
assert result == ~D[2018-12-30]
end
end
describe "days_late/2" do
@tag task_id: 4
test "returns 0 when identical datetimes" do
result = LibraryFees.days_late(~D[2021-02-01], ~N[2021-02-01 12:00:00Z])
assert result == 0
end
@tag task_id: 4
test "returns 0 when identical dates, but different times" do
result = LibraryFees.days_late(~D[2019-03-11], ~N[2019-03-11 12:00:00Z])
assert result == 0
end
@tag task_id: 4
test "returns 0 when planned return date is later than actual return date" do
result = LibraryFees.days_late(~D[2020-12-03], ~N[2020-11-29 16:00:00Z])
assert result == 0
end
@tag task_id: 4
test "returns date difference in numbers of days when planned return date is earlier than actual return date" do
result = LibraryFees.days_late(~D[2020-06-12], ~N[2020-06-21 16:00:00Z])
assert result == 9
end
@tag task_id: 4
test "a new day starts at midnight" do
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
end
end
describe "monday?" do
@tag task_id: 5
test "2021-02-01 was a Monday" do
assert LibraryFees.monday?(~N[2021-02-01 14:01:00Z]) == true
end
@tag task_id: 5
test "2020-03-16 was a Monday" do
assert LibraryFees.monday?(~N[2020-03-16 09:23:52Z]) == true
end
@tag task_id: 5
test "2020-04-22 was a Monday" do
assert LibraryFees.monday?(~N[2019-04-22 15:44:03Z]) == true
end
@tag task_id: 5
test "2021-02-02 was a Tuesday" do
assert LibraryFees.monday?(~N[2021-02-02 15:07:00Z]) == false
end
@tag task_id: 5
test "2020-03-14 was a Friday" do
assert LibraryFees.monday?(~N[2020-03-14 08:54:51Z]) == false
end
@tag task_id: 5
test "2019-04-28 was a Sunday" do
assert LibraryFees.monday?(~N[2019-04-28 11:37:12Z]) == false
end
end
describe "calculate_late_fee/2" do
@tag task_id: 6
test "returns 0 if the book was returned less than 28 days after a morning checkout" do
result = LibraryFees.calculate_late_fee("2018-11-01T09:00:00Z", "2018-11-13T14:12:00Z", 123)
assert result == 0
end
@tag task_id: 6
test "returns 0 if the book was returned exactly 28 days after a morning checkout" do
result = LibraryFees.calculate_late_fee("2018-11-01T09:00:00Z", "2018-11-29T14:12:00Z", 123)
assert result == 0
end
@tag task_id: 6
test "returns the rate for one day if the book was returned exactly 29 days after a morning checkout" do
result = LibraryFees.calculate_late_fee("2018-11-01T09:00:00Z", "2018-11-30T14:12:00Z", 320)
assert result == 320
end
@tag task_id: 6
test "returns 0 if the book was returned less than 29 days after an afternoon checkout" do
result = LibraryFees.calculate_late_fee("2019-05-01T16:12:00Z", "2019-05-17T14:32:45Z", 400)
assert result == 0
end
@tag task_id: 6
test "returns 0 if the book was returned exactly 29 days after an afternoon checkout" do
result = LibraryFees.calculate_late_fee("2019-05-01T16:12:00Z", "2019-05-30T14:32:45Z", 313)
assert result == 0
end
@tag task_id: 6
test "returns the rate for one day if the book was returned exactly 30 days after an afternoon checkout" do
result = LibraryFees.calculate_late_fee("2019-05-01T16:12:00Z", "2019-05-31T14:32:45Z", 234)
assert result == 234
end
@tag task_id: 6
test "multiplies the number of days late by the rate for one day" do
result = LibraryFees.calculate_late_fee("2021-01-01T08:00:00Z", "2021-02-13T08:00:00Z", 111)
assert result == 111 * 15
end
@tag task_id: 6
test "late fees are 50% off (rounded down) when the book is returned on a Monday" do
result = LibraryFees.calculate_late_fee("2021-01-01T08:00:00Z", "2021-02-15T08:00:00Z", 111)
assert result == trunc(111 * 17 * 0.5)
end
end
end

View File

@ -0,0 +1,2 @@
ExUnit.start()
ExUnit.configure(exclude: :pending, trace: true, seed: 0)

View File

@ -0,0 +1,22 @@
{
"authors": [
"neenjaw"
],
"files": {
"solution": [
"lib/log_level.ex"
],
"test": [
"test/log_level_test.exs"
],
"exemplar": [
".meta/exemplar.ex"
]
},
"language_versions": ">=1.10",
"forked_from": [
"csharp/logs-logs-logs"
],
"icon": "log-levels",
"blurb": "Learn about atoms and the cond conditional expression by aggregating application logs."
}

View File

@ -0,0 +1 @@
{"track":"elixir","exercise":"log-level","id":"cfabdd7d8b3045aa9515e5fcc87ffaf6","url":"https://exercism.org/tracks/elixir/exercises/log-level","handle":"halfdan","is_requester":true,"auto_approve":false}

Some files were not shown because too many files have changed in this diff Show More